diff --git a/Cs_HMI/AGVCSharp.sln b/Cs_HMI/AGVCSharp.sln index 10be184..0cc2418 100644 --- a/Cs_HMI/AGVCSharp.sln +++ b/Cs_HMI/AGVCSharp.sln @@ -42,10 +42,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "솔루션 항목", "솔루 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVNavigationCore", "AGVNavigationCore\AGVNavigationCore.csproj", "{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVPathTester", "AGVPathTester\AGVPathTester.csproj", "{F1E2D3C4-B5A6-9788-0123-456789ABCDEF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PathLogic", "PathLogic\PathLogic.csproj", "{12345678-1234-5678-9012-123456789ABC}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -212,30 +208,6 @@ Global {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|x64.Build.0 = Release|Any CPU {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|x86.ActiveCfg = Release|x86 {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|x86.Build.0 = Release|x86 - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Debug|x64.ActiveCfg = Debug|Any CPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Debug|x64.Build.0 = Debug|Any CPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Debug|x86.ActiveCfg = Debug|x86 - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Debug|x86.Build.0 = Debug|x86 - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Release|Any CPU.Build.0 = Release|Any CPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Release|x64.ActiveCfg = Release|Any CPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Release|x64.Build.0 = Release|Any CPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Release|x86.ActiveCfg = Release|x86 - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF}.Release|x86.Build.0 = Release|x86 - {12345678-1234-5678-9012-123456789ABC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Debug|x64.ActiveCfg = Debug|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Debug|x64.Build.0 = Debug|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Debug|x86.ActiveCfg = Debug|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Debug|x86.Build.0 = Debug|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Release|Any CPU.Build.0 = Release|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Release|x64.ActiveCfg = Release|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Release|x64.Build.0 = Release|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Release|x86.ActiveCfg = Release|Any CPU - {12345678-1234-5678-9012-123456789ABC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Cs_HMI/AGVPathTester/AGVPathTester.csproj b/Cs_HMI/AGVPathTester/AGVPathTester.csproj deleted file mode 100644 index 10b28c9..0000000 --- a/Cs_HMI/AGVPathTester/AGVPathTester.csproj +++ /dev/null @@ -1,82 +0,0 @@ - - - - - Debug - AnyCPU - {F1E2D3C4-B5A6-9788-0123-456789ABCDEF} - Exe - AGVPathTester - AGVPathTester - v4.8 - 512 - true - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - prompt - - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - prompt - - - - - - - - - - - - - ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - - - - - {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C} - AGVNavigationCore - - - - \ No newline at end of file diff --git a/Cs_HMI/AGVPathTester/App.config b/Cs_HMI/AGVPathTester/App.config deleted file mode 100644 index d8ef5cf..0000000 --- a/Cs_HMI/AGVPathTester/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Cs_HMI/AGVPathTester/PathTester.cs b/Cs_HMI/AGVPathTester/PathTester.cs deleted file mode 100644 index 279752e..0000000 --- a/Cs_HMI/AGVPathTester/PathTester.cs +++ /dev/null @@ -1,1463 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AGVNavigationCore.Models; -using AGVNavigationCore.PathFinding.Planning; - -namespace AGVPathTester -{ - /// - /// AGV 경로 탐색 및 방향전환 로직 테스트 클래스 - /// - public class PathTester - { - private readonly string _mapFilePath; - private List _mapNodes; - private AGVPathfinder _pathfinder; - private DirectionChangePlanner _directionChangePlanner; - - public PathTester(string mapFilePath) - { - _mapFilePath = mapFilePath; - _mapNodes = new List(); - } - - /// - /// PathTester 초기화 - /// - public bool Initialize() - { - try - { - // 맵 파일 로딩 - var mapLoadResult = MapLoader.LoadMapFromFile(_mapFilePath); - if (!mapLoadResult.Success) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 맵 로딩 실패: {mapLoadResult.ErrorMessage}"); - Console.ResetColor(); - return false; - } - - _mapNodes = mapLoadResult.Nodes; - - // PathFinder 초기화 - _pathfinder = new AGVPathfinder(_mapNodes); - - // DirectionChangePlanner 초기화 - _directionChangePlanner = new DirectionChangePlanner(_mapNodes); - - return true; - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 초기화 중 오류: {ex.Message}"); - Console.ResetColor(); - return false; - } - } - - /// - /// 로드된 노드 수 반환 - /// - public int GetNodeCount() - { - return _mapNodes?.Count ?? 0; - } - - /// - /// 기본 경로 탐색 테스트 실행 - /// - public void RunBasicPathTests() - { - Console.WriteLine("🔍 기본 경로 탐색 테스트 시작..."); - - var testCases = TestCases.GetBasicPathTestCases(); - int passCount = 0; - int totalCount = testCases.Count; - - foreach (var testCase in testCases) - { - Console.WriteLine($"\n--- 테스트: {testCase.StartNodeId} → {testCase.TargetNodeId} ---"); - - var result = _pathfinder.FindPath( - GetNodeById(testCase.StartNodeId), - GetNodeById(testCase.TargetNodeId), - testCase.CurrentDirection - ); - - bool passed = EvaluateBasicPathResult(result, testCase); - if (passed) passCount++; - - DisplayPathResult(result, testCase.Description); - } - - // 결과 요약 - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine($"\n📊 기본 경로 테스트 결과: {passCount}/{totalCount} 통과"); - if (passCount == totalCount) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("🎉 모든 기본 경로 테스트 통과!"); - } - else - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"⚠️ {totalCount - passCount}개 테스트 실패"); - } - Console.ResetColor(); - } - - /// - /// 방향 전환 경로 테스트 실행 - /// - public void RunDirectionChangeTests() - { - Console.WriteLine("🔄 방향 전환 경로 테스트 시작..."); - - var testCases = TestCases.GetDirectionChangeTestCases(); - int passCount = 0; - int totalCount = testCases.Count; - - foreach (var testCase in testCases) - { - Console.WriteLine($"\n--- 방향전환 테스트: {testCase.StartNodeId} → {testCase.TargetNodeId} ---"); - Console.WriteLine($"현재방향: {testCase.CurrentDirection}, 요구방향: {testCase.RequiredDirection}"); - - var plan = _directionChangePlanner.PlanDirectionChange( - testCase.StartNodeId, - testCase.TargetNodeId, - testCase.CurrentDirection, - testCase.RequiredDirection - ); - - bool passed = EvaluateDirectionChangeResult(plan, testCase); - if (passed) passCount++; - - DisplayDirectionChangePlan(plan, testCase.Description); - } - - // 결과 요약 - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine($"\n📊 방향전환 테스트 결과: {passCount}/{totalCount} 통과"); - if (passCount == totalCount) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("🎉 모든 방향전환 테스트 통과!"); - } - else - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"⚠️ {totalCount - passCount}개 테스트 실패"); - } - Console.ResetColor(); - } - - /// - /// 단일 테스트 실행 - /// - public void RunSingleTest(string startNodeId, string targetNodeId, AgvDirection currentDirection) - { - Console.WriteLine($"\n🎯 단일 테스트: {startNodeId} → {targetNodeId} (방향: {currentDirection})"); - - var startNode = GetNodeById(startNodeId); - var targetNode = GetNodeById(targetNodeId); - - if (startNode == null) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 시작 노드를 찾을 수 없습니다: {startNodeId}"); - Console.ResetColor(); - return; - } - - if (targetNode == null) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 목표 노드를 찾을 수 없습니다: {targetNodeId}"); - Console.ResetColor(); - return; - } - - // 1. 기본 경로 탐색 - Console.WriteLine("\n📍 기본 경로 탐색:"); - var basicResult = _pathfinder.FindPath(startNode, targetNode, currentDirection); - DisplayPathResult(basicResult, "사용자 정의 테스트"); - - // 2. 방향 전환이 필요한지 확인 - var requiredDirection = GetRequiredDockingDirection(targetNode); - if (requiredDirection.HasValue && requiredDirection.Value != currentDirection) - { - Console.WriteLine($"\n🔄 방향 전환 필요: {currentDirection} → {requiredDirection.Value}"); - var plan = _directionChangePlanner.PlanDirectionChange( - startNodeId, targetNodeId, currentDirection, requiredDirection.Value); - DisplayDirectionChangePlan(plan, "방향전환 경로"); - } - else - { - Console.WriteLine("\n✅ 방향 전환 불필요"); - } - } - - /// - /// 배치 테스트 실행 - /// - public void RunBatchTests() - { - Console.WriteLine("📦 배치 테스트 실행 중...\n"); - - RunBasicPathTests(); - Console.WriteLine("\n" + new string('=', 50) + "\n"); - RunDirectionChangeTests(); - Console.WriteLine("\n" + new string('=', 50) + "\n"); - RunMovementBasedTests(); - } - - /// - /// 이동 기반 테스트 실행 (AGV 물리적 제약사항 포함) - /// - public void RunMovementBasedTests() - { - Console.WriteLine("🚀 이동 기반 테스트 실행...\n"); - - var testCases = TestCases.GetMovementBasedTestCases(); - int successCount = 0; - int totalCount = testCases.Count; - - foreach (var testCase in testCases) - { - Console.WriteLine($"--- 이동기반 테스트: {testCase.PreviousRfid} → {testCase.CurrentRfid} → {testCase.TargetRfid} ---"); - Console.WriteLine($"🎯 {testCase.Description}"); - - try - { - // 실제 계산된 방향과 기대 방향 비교 - var calculatedDirection = CalculateDirectionFromMovement(testCase.PreviousRfid, testCase.CurrentRfid); - Console.WriteLine($"📊 계산된 방향: {calculatedDirection}, 기대 방향: {testCase.ExpectedDirection}"); - - if (calculatedDirection == testCase.ExpectedDirection) - { - Console.WriteLine("✅ 방향 계산 정확!"); - - // 물리적 제약사항을 고려한 경로 계산 - RunMovementBasedTestWithUserDirection(testCase.PreviousRfid, testCase.CurrentRfid, testCase.TargetRfid, testCase.ExpectedDirection); - successCount++; - Console.WriteLine("✅ 이동기반 테스트 성공"); - } - else - { - Console.WriteLine("❌ 방향 계산 불일치!"); - } - } - catch (Exception ex) - { - Console.WriteLine($"❌ 테스트 실행 실패: {ex.Message}"); - } - - Console.WriteLine(); // 구분용 빈 줄 - } - - Console.WriteLine($"🎯 이동기반 테스트 결과: {successCount}/{totalCount} 성공"); - if (successCount == totalCount) - { - Console.WriteLine("🎉 모든 이동기반 테스트 성공!"); - } - else - { - Console.WriteLine($"⚠️ {totalCount - successCount}개 테스트 실패"); - } - } - - /// - /// 맵 정보 표시 - /// - public void ShowMapInfo() - { - Console.WriteLine($"📊 맵 파일: {_mapFilePath}"); - Console.WriteLine($"🔢 총 노드 수: {_mapNodes.Count}"); - - // 노드 타입별 통계 - var nodeStats = _mapNodes.GroupBy(n => n.Type) - .ToDictionary(g => g.Key, g => g.Count()); - - Console.WriteLine("\n📈 노드 타입별 통계:"); - foreach (var stat in nodeStats) - { - Console.WriteLine($" {stat.Key}: {stat.Value}개"); - } - - // 도킹 방향별 통계 - var dockingStats = _mapNodes.GroupBy(n => n.DockDirection) - .ToDictionary(g => g.Key, g => g.Count()); - - Console.WriteLine("\n🚢 도킹 방향별 통계:"); - foreach (var stat in dockingStats) - { - Console.WriteLine($" {stat.Key}: {stat.Value}개"); - } - - // 연결 정보 - var totalConnections = _mapNodes.Sum(n => n.ConnectedNodes.Count); - Console.WriteLine($"\n🔗 총 연결 수: {totalConnections}"); - - // 갈림길 정보 분석 - ShowJunctionInfo(); - - // RFID 매핑 정보 - ShowRfidMappings(); - - // 샘플 노드 정보 - Console.WriteLine("\n📋 샘플 노드 (처음 5개):"); - foreach (var node in _mapNodes.Take(5)) - { - var rfidInfo = string.IsNullOrEmpty(node.RfidId) ? "RFID 없음" : node.RfidId; - Console.WriteLine($" {node.NodeId}: {node.Type}, {node.DockDirection}, {rfidInfo}, 연결:{node.ConnectedNodes.Count}개"); - } - } - - /// - /// 갈림길 정보 분석 및 표시 - /// - public void ShowJunctionInfo() - { - Console.WriteLine("\n🛤️ 갈림길 분석:"); - - var junctions = _mapNodes.Where(n => n.ConnectedNodes.Count > 2).ToList(); - Console.WriteLine($"갈림길 노드 수: {junctions.Count}개"); - - foreach (var node in junctions) - { - Console.WriteLine($" {node.NodeId}: {node.ConnectedNodes.Count}개 연결 - {string.Join(", ", node.ConnectedNodes)}"); - - // DirectionChangePlanner의 JunctionAnalyzer에서 실제로 갈림길로 인식되는지 확인 - var junctionInfo = _directionChangePlanner.GetType() - .GetField("_junctionAnalyzer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)? - .GetValue(_directionChangePlanner); - - if (junctionInfo != null) - { - var getJunctionInfoMethod = junctionInfo.GetType().GetMethod("GetJunctionInfo"); - var info = getJunctionInfoMethod?.Invoke(junctionInfo, new object[] { node.NodeId }); - - if (info != null) - { - Console.WriteLine($" -> JunctionAnalyzer 인식: {info}"); - } - else - { - Console.WriteLine($" -> JunctionAnalyzer 인식: null (인식 실패)"); - } - } - } - - if (junctions.Count == 0) - { - Console.WriteLine(" ⚠️ 갈림길 노드가 없습니다! 이것이 방향전환 실패의 원인일 수 있습니다."); - } - } - - /// - /// RFID → NodeID 매핑 정보 표시 - /// - public void ShowRfidMappings() - { - Console.WriteLine("\n🏷️ RFID → NodeID 매핑:"); - - // PATHSCENARIO.md에서 사용되는 핵심 RFID들 - var importantRfids = new[] { "001", "005", "007", "015", "019", "037", "011", "004", "012", "016" }; - - foreach (var rfid in importantRfids) - { - var node = GetNodeByRfid(rfid); - if (node != null) - { - Console.WriteLine($" RFID {rfid} → {node.NodeId} ({node.Type}, {node.DockDirection})"); - } - else - { - Console.WriteLine($" RFID {rfid} → NOT FOUND"); - } - } - - Console.WriteLine("\n모든 RFID 매핑 (처음 15개):"); - var allMappings = _mapNodes.Where(n => !string.IsNullOrEmpty(n.RfidId)).Take(15); - foreach (var node in allMappings) - { - Console.WriteLine($" RFID {node.RfidId} → {node.NodeId}"); - } - } - - #region Private Helper Methods - - private MapNode GetNodeById(string nodeId) - { - return _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); - } - - private MapNode GetNodeByRfid(string rfidId) - { - return _mapNodes.FirstOrDefault(n => n.RfidId == rfidId); - } - - /// - /// RFID ID를 NodeID로 변환 - /// - private string ConvertRfidToNodeId(string rfidId) - { - var node = GetNodeByRfid(rfidId); - return node?.NodeId ?? rfidId; // RFID로 찾지 못하면 원래 값 반환 - } - - private AgvDirection? GetRequiredDockingDirection(MapNode targetNode) - { - switch (targetNode.DockDirection) - { - case DockingDirection.Forward: - return AgvDirection.Forward; - case DockingDirection.Backward: - return AgvDirection.Backward; - case DockingDirection.DontCare: - default: - return null; - } - } - - private bool EvaluateBasicPathResult(AGVNavigationCore.PathFinding.Core.AGVPathResult result, TestCases.BasicPathTestCase testCase) - { - // 기본 평가: 성공 여부와 경로 존재 여부 - bool basicSuccess = result.Success && result.Path != null && result.Path.Count > 0; - - if (!basicSuccess) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 기본 조건 실패: Success={result.Success}, PathCount={result.Path?.Count ?? 0}"); - Console.ResetColor(); - return false; - } - - // 되돌아가기 패턴 체크 - if (HasBacktrackingPattern(result.Path)) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("❌ 되돌아가기 패턴 발견!"); - Console.ResetColor(); - return false; - } - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("✅ 기본 경로 테스트 통과"); - Console.ResetColor(); - return true; - } - - private bool EvaluateDirectionChangeResult(DirectionChangePlanner.DirectionChangePlan plan, TestCases.DirectionChangeTestCase testCase) - { - if (!plan.Success) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 방향전환 계획 실패: {plan.ErrorMessage}"); - Console.ResetColor(); - return false; - } - - // 되돌아가기 패턴 체크 - if (HasBacktrackingPattern(plan.DirectionChangePath)) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("❌ 방향전환 경로에서 되돌아가기 패턴 발견!"); - Console.ResetColor(); - return false; - } - - // 기대 경로와 비교 (기대 경로가 정의된 경우) - if (testCase.ExpectedPath != null && testCase.ExpectedPath.Count > 0) - { - bool pathMatches = ComparePathsWithTolerance(plan.DirectionChangePath, testCase.ExpectedPath); - if (!pathMatches) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("⚠️ 경로가 기대 결과와 다릅니다:"); - Console.WriteLine($" 기대: {string.Join(" → ", testCase.ExpectedPath)}"); - Console.WriteLine($" 실제: {string.Join(" → ", plan.DirectionChangePath)}"); - Console.ResetColor(); - // 경로가 다르더라도 되돌아가기가 없으면 부분 성공으로 처리 - return true; - } - else - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("✅ 경로가 기대 결과와 일치합니다!"); - Console.ResetColor(); - } - } - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("✅ 방향전환 테스트 통과"); - Console.ResetColor(); - return true; - } - - private bool HasBacktrackingPattern(List path) - { - if (path == null || path.Count < 3) return false; - - for (int i = 0; i < path.Count - 2; i++) - { - if (path[i] == path[i + 2] && path[i] != path[i + 1]) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"⚠️ 되돌아가기 패턴: {path[i]} → {path[i + 1]} → {path[i + 2]}"); - Console.ResetColor(); - return true; - } - } - return false; - } - - private void DisplayPathResult(AGVNavigationCore.PathFinding.Core.AGVPathResult result, string description) - { - Console.WriteLine($"📝 {description}"); - - if (result.Success) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"✅ 성공 - 경로: {string.Join(" → ", result.Path)}"); - Console.WriteLine($"📏 거리: {result.TotalDistance:F1}, 단계: {result.Path.Count}"); - Console.ResetColor(); - } - else - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 실패 - {result.ErrorMessage}"); - Console.ResetColor(); - } - } - - private void DisplayDirectionChangePlan(DirectionChangePlanner.DirectionChangePlan plan, string description) - { - Console.WriteLine($"📝 {description}"); - - if (plan.Success) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"✅ 성공 - 경로: {string.Join(" → ", plan.DirectionChangePath)}"); - Console.WriteLine($"🔄 방향전환 노드: {plan.DirectionChangeNode}"); - Console.WriteLine($"📋 설명: {plan.PlanDescription}"); - Console.ResetColor(); - } - else - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 실패 - {plan.ErrorMessage}"); - Console.ResetColor(); - } - } - - private bool ComparePathsWithTolerance(List actualPath, List expectedPath) - { - if (actualPath == null || expectedPath == null) - return false; - - if (actualPath.Count != expectedPath.Count) - return false; - - for (int i = 0; i < actualPath.Count; i++) - { - if (actualPath[i] != expectedPath[i]) - return false; - } - - return true; - } - - /// - /// 이전 위치를 고려한 AGV 방향 계산 - /// AGV 방향은 "이 방향으로 계속 이동할 때 어디로 가는가"를 의미 - /// - public AgvDirection CalculateDirectionFromMovement(string previousRfid, string currentRfid) - { - var previousNode = GetNodeByRfid(previousRfid); - var currentNode = GetNodeByRfid(currentRfid); - - if (previousNode == null || currentNode == null) - { - Console.WriteLine($"⚠️ 방향 계산 실패: RFID {previousRfid} 또는 {currentRfid}를 찾을 수 없음"); - return AgvDirection.Forward; // 기본값 - } - - Console.WriteLine($"🔍 연결 분석:"); - Console.WriteLine($" {previousNode.NodeId} 연결: [{string.Join(", ", previousNode.ConnectedNodes)}]"); - Console.WriteLine($" {currentNode.NodeId} 연결: [{string.Join(", ", currentNode.ConnectedNodes)}]"); - - // 💡 개선된 AGV 방향 계산 로직 - // 실제 노드들의 위치와 특성을 고려한 방향 결정 - - // 특별 케이스들을 우선 처리 - if (previousRfid == "033" && currentRfid == "032") - { - Console.WriteLine($"📍 특별 케이스: 033→032 이동은 Backward 방향"); - return AgvDirection.Backward; - } - else if (previousRfid == "032" && currentRfid == "031") - { - Console.WriteLine($"📍 특별 케이스: 032→031 이동은 Backward 방향"); - return AgvDirection.Backward; - } - else if (previousRfid == "001" && currentRfid == "002") - { - Console.WriteLine($"📍 특별 케이스: 001→002 이동은 Forward 방향"); - return AgvDirection.Forward; - } - else if (previousRfid == "002" && currentRfid == "001") - { - Console.WriteLine($"📍 특별 케이스: 002→001 이동은 Backward 방향"); - return AgvDirection.Backward; - } - else if (previousRfid == "019" && currentRfid == "018") - { - Console.WriteLine($"📍 특별 케이스: 019→018 충전기 출고는 Forward 방향"); - return AgvDirection.Forward; - } - else if (previousRfid == "018" && currentRfid == "019") - { - Console.WriteLine($"📍 특별 케이스: 018→019 충전기 진입은 Forward 방향"); - return AgvDirection.Forward; - } - // 실패한 케이스들 추가 - else if (previousRfid == "015" && currentRfid == "014") - { - Console.WriteLine($"📍 특별 케이스: 015→014 충전기에서 출고는 Backward 방향"); - return AgvDirection.Backward; - } - else if (previousRfid == "005" && currentRfid == "004") - { - Console.WriteLine($"📍 특별 케이스: 005→004 좌회전 이동은 Backward 방향"); - return AgvDirection.Backward; - } - else if (previousRfid == "016" && currentRfid == "012") - { - Console.WriteLine($"📍 특별 케이스: 016→012 이동은 Backward 방향"); - return AgvDirection.Backward; - } - else if (previousRfid == "007" && currentRfid == "006") - { - Console.WriteLine($"📍 특별 케이스: 007→006 이동은 Backward 방향"); - return AgvDirection.Backward; - } - - // 일반적인 경우: 노드 연결성 기반 판단 - if (previousNode.ConnectedNodes.Contains(currentNode.NodeId) && - currentNode.ConnectedNodes.Contains(previousNode.NodeId)) - { - // 양방향 연결된 경우, 노드 번호나 위치 기반으로 판단 - // 일반적으로 번호가 증가하는 방향을 Forward로 간주 - var prevNumber = int.TryParse(previousRfid, out var prev) ? prev : 0; - var currNumber = int.TryParse(currentRfid, out var curr) ? curr : 0; - - if (prevNumber > 0 && currNumber > 0) - { - if (currNumber > prevNumber) - { - Console.WriteLine($"📍 일반 케이스: {previousRfid}→{currentRfid} 번호 증가는 Forward 방향"); - return AgvDirection.Forward; - } - else - { - Console.WriteLine($"📍 일반 케이스: {previousRfid}→{currentRfid} 번호 감소는 Backward 방향"); - return AgvDirection.Backward; - } - } - } - - // 연결성이 없는 경우 기본값 - Console.WriteLine($"⚠️ 연결되지 않은 노드 또는 특수 케이스: {previousRfid} → {currentRfid}, Forward 기본값 사용"); - return AgvDirection.Forward; - } - - /// - /// 이전 위치 기반 정확한 테스트 - /// - public void RunMovementBasedTest(string previousRfid, string currentRfid, string targetRfid) - { - Console.WriteLine($"\n🚀 이동 기반 테스트: RFID {previousRfid} → {currentRfid} → {targetRfid}"); - - var previousNode = GetNodeByRfid(previousRfid); - var currentNode = GetNodeByRfid(currentRfid); - var targetNode = GetNodeByRfid(targetRfid); - - if (previousNode == null || currentNode == null || targetNode == null) - { - Console.WriteLine($"❌ 노드를 찾을 수 없습니다."); - return; - } - - // AGV의 실제 방향 계산 - var realDirection = CalculateDirectionFromMovement(previousRfid, currentRfid); - - Console.WriteLine($"📍 매핑: {previousRfid}→{previousNode.NodeId}, {currentRfid}→{currentNode.NodeId}, {targetRfid}→{targetNode.NodeId}"); - Console.WriteLine($"🧭 AGV 실제 방향: {realDirection} ({previousRfid}→{currentRfid} 이동 기준)"); - - // 기본 경로 탐색 및 RFID 변환 - var basicResult = _pathfinder.FindPath(currentNode, targetNode, realDirection); - if (basicResult.Success) - { - Console.WriteLine($"\n🗺️ RFID 경로: {ConvertPathToRfid(basicResult.Path)}"); - Console.WriteLine($"📏 단계: {basicResult.Path.Count}"); - } - - RunSingleTest(currentNode.NodeId, targetNode.NodeId, realDirection); - } - - /// - /// 사용자 지정 방향을 사용한 이동 기반 테스트 - /// - public void RunMovementBasedTestWithUserDirection(string previousRfid, string currentRfid, string targetRfid, AgvDirection userDirection) - { - Console.WriteLine($"\n🚀 사용자 지정 방향 테스트: RFID {previousRfid} → {currentRfid} → {targetRfid}"); - - var previousNode = GetNodeByRfid(previousRfid); - var currentNode = GetNodeByRfid(currentRfid); - var targetNode = GetNodeByRfid(targetRfid); - - if (previousNode == null || currentNode == null || targetNode == null) - { - Console.WriteLine($"❌ 노드를 찾을 수 없습니다."); - return; - } - - Console.WriteLine($"📍 매핑: {previousRfid}→{previousNode.NodeId}, {currentRfid}→{currentNode.NodeId}, {targetRfid}→{targetNode.NodeId}"); - Console.WriteLine($"🧭 사용자 지정 AGV 방향: {userDirection} ({previousRfid}→{currentRfid} 이동을 {userDirection}으로 정의)"); - - // AGV의 물리적 향하는 방향을 고려한 경로 계산 - var correctedPath = CalculatePhysicalDirectionBasedPath(previousNode, currentNode, targetNode, userDirection); - - if (correctedPath != null && correctedPath.Count > 0) - { - Console.WriteLine($"\n🎯 물리적 방향 고려 RFID 경로: {ConvertPathToRfid(correctedPath)}"); - Console.WriteLine($"📏 물리적 방향 기준 단계: {correctedPath.Count}"); - } - - // 기존 방식도 비교를 위해 실행 - var basicResult = _pathfinder.FindPath(currentNode, targetNode, userDirection); - if (basicResult.Success) - { - Console.WriteLine($"\n🗺️ 기존 방식 RFID 경로: {ConvertPathToRfid(basicResult.Path)}"); - Console.WriteLine($"📏 기존 방식 단계: {basicResult.Path.Count}"); - } - - RunSingleTest(currentNode.NodeId, targetNode.NodeId, userDirection); - } - - /// - /// AGV의 물리적 향하는 방향을 고려한 경로 계산 - /// - private List CalculatePhysicalDirectionBasedPath(MapNode previousNode, MapNode currentNode, MapNode targetNode, AgvDirection motorDirection) - { - Console.WriteLine($"\n🔍 물리적 방향 기반 경로 계산:"); - Console.WriteLine($" 이전: {previousNode.NodeId} → 현재: {currentNode.NodeId}"); - Console.WriteLine($" 모터 방향: {motorDirection}"); - - // 1. AGV가 향하는 물리적 방향 결정 - var physicalDirection = DeterminePhysicalOrientation(previousNode, currentNode, motorDirection); - Console.WriteLine($" AGV 물리적 향하는 방향: {physicalDirection}"); - - // 2. 현재 위치에서 같은 모터 방향으로 다음에 갈 수 있는 노드들 찾기 - var nextPossibleNodes = GetNextNodesInPhysicalDirection(currentNode, physicalDirection, motorDirection); - - Console.WriteLine($" 다음 가능한 노드들: [{string.Join(", ", nextPossibleNodes)}]"); - - // 3. 목적지까지의 경로 계산 (물리적 방향 고려) - var path = FindPathWithPhysicalConstraints(currentNode, targetNode, nextPossibleNodes, motorDirection); - - // 4. 물리적 제약사항 위반 체크 (012→016 직접 이동) - bool hasInvalidPath = false; - if (path.Count > 0 && motorDirection == AgvDirection.Forward) - { - for (int i = 0; i < path.Count - 1; i++) - { - // 012에서 016으로 전진 직접 이동은 불가능 - if (path[i] == "N022" && path[i + 1] == "N023") // 012→016 - { - Console.WriteLine($" ⚠️ 물리적 제약사항 위반 감지: 012→016 전진 직접 이동 불가"); - hasInvalidPath = true; - break; - } - } - } - - // 5. 물리적 방향으로 갈 수 있는 노드가 없거나 경로 계산 실패 또는 제약사항 위반 시 특별 처리 - if (nextPossibleNodes.Count == 0 || path.Count == 0 || hasInvalidPath) - { - Console.WriteLine($" ⚠️ 물리적 방향 제약 또는 경로 계산 실패 - 특별 시나리오 체크"); - - // 특별 시나리오: 032→031 후진 이동 AGV → 008 - if (currentNode.NodeId == "N021" && targetNode.NodeId == "N014") - { - Console.WriteLine($" 🎯 특별 시나리오 강제 실행: 031→008"); - path = FindDetourPath(currentNode, targetNode, new List(), motorDirection); - } - // 특별 시나리오: 015→019 충전기 간 이동 (012 제약사항) - else if (currentNode.NodeId == "N019" && targetNode.NodeId == "N026") - { - Console.WriteLine($" 🎯 특별 시나리오 강제 실행: 015→019 (012 우회)"); - path = FindDetourPath(currentNode, targetNode, new List(), motorDirection); - } - // 특별 시나리오: 010→011 후진 도킹 전용 노드 (방향 전환 필요) - else if (currentNode.NodeId == "N009" && targetNode.NodeId == "N010") - { - Console.WriteLine($" 🎯 특별 시나리오 강제 실행: 010→011 후진 도킹"); - path = FindDetourPath(currentNode, targetNode, new List(), motorDirection); - } - - if (path.Count == 0) - { - Console.WriteLine($" ❌ 모든 경로 계산 실패"); - return new List(); - } - } - - return path; - } - - /// - /// AGV의 물리적 향하는 방향 결정 - /// - private string DeterminePhysicalOrientation(MapNode previousNode, MapNode currentNode, AgvDirection motorDirection) - { - // 이전→현재 이동 벡터 분석 - var movementVector = $"{previousNode.NodeId}→{currentNode.NodeId}"; - - // 모터 방향과 이동 방향을 조합하여 AGV가 향하는 방향 결정 - // 예: 033→032 후진 이동 = AGV가 031 방향을 향함 - if (motorDirection == AgvDirection.Backward) - { - // 후진 모터로 이동했다면, AGV는 이동 방향의 연장선 방향을 향함 - return $"toward_next_from_{currentNode.NodeId}"; - } - else - { - // 전진 모터로 이동했다면, AGV는 이동 방향과 같은 방향을 향함 - return $"toward_next_from_{currentNode.NodeId}"; - } - } - - /// - /// 물리적 방향에서 다음에 갈 수 있는 노드들 찾기 - /// AGV 이동 벡터의 연장선 기반으로 계산 - /// - private List GetNextNodesInPhysicalDirection(MapNode currentNode, string physicalDirection, AgvDirection motorDirection) - { - var possibleNodes = new List(); - Console.WriteLine($" 🧭 {currentNode.NodeId}에서 물리적 방향 계산 (모터: {motorDirection})"); - - // 💡 핵심: AGV 물리적 방향 = 이동 벡터의 연장선 - // 예: 032→031 후진 이동 시, AGV는 031에서 041 방향을 향함 (032→031 벡터 연장) - - // 특별 처리: 주요 물리적 방향 시나리오들 - if (currentNode.NodeId == "N021") // 031에서 - { - // 032→031 후진 이동 후 031에서의 물리적 방향 - // 031은 직선 끝점이므로 일반적으로 전진으로 005 방향으로 가야 함 - Console.WriteLine($" 🎯 031 특수 처리: 전진으로 005 방향으로 이동 필요"); - // 이 경우는 우회 로직에서 처리하므로 빈 배열 반환 - } - else if (currentNode.NodeId == "N020") // 032에서 - { - if (motorDirection == AgvDirection.Backward) - { - // 032에서 후진으로 계속 이동 시 031 방향 - if (currentNode.ConnectedNodes.Contains("N021")) - { - possibleNodes.Add("N021"); - Console.WriteLine($" ✅ 032에서 후진 → 031 방향"); - } - } - else if (motorDirection == AgvDirection.Forward) - { - // 032에서 전진 시 033 방향 - if (currentNode.ConnectedNodes.Contains("N005")) - { - possibleNodes.Add("N005"); - Console.WriteLine($" ✅ 032에서 전진 → 033 방향"); - } - } - } - else if (currentNode.NodeId == "N026" && motorDirection == AgvDirection.Forward) - { - // 019 충전기에서 전진 시 018 방향으로만 가능 - if (currentNode.ConnectedNodes.Contains("N025")) - { - possibleNodes.Add("N025"); - Console.WriteLine($" ✅ 019 충전기: 018 방향으로만 이동"); - } - } - else if (currentNode.NodeId == "N022") // 012에서 - { - if (motorDirection == AgvDirection.Forward) - { - // 012에서 전진 시 004 방향으로만 가능 (016 직접 이동 불가) - if (currentNode.ConnectedNodes.Contains("N004")) - { - possibleNodes.Add("N004"); - Console.WriteLine($" ✅ 012에서 전진 → 004 방향 (016 직접 이동 불가)"); - } - // 006 방향도 가능 - if (currentNode.ConnectedNodes.Contains("N006")) - { - possibleNodes.Add("N006"); - Console.WriteLine($" ✅ 012에서 전진 → 006 방향"); - } - } - else if (motorDirection == AgvDirection.Backward) - { - // 012에서 후진 시 016 방향으로 가능 - if (currentNode.ConnectedNodes.Contains("N023")) - { - possibleNodes.Add("N023"); - Console.WriteLine($" ✅ 012에서 후진 → 016 방향"); - } - } - } - else - { - // 일반적인 경우: 모든 연결 노드 추가 (추후 벡터 계산으로 정교화) - foreach (var connectedNodeId in currentNode.ConnectedNodes) - { - possibleNodes.Add(connectedNodeId); - Console.WriteLine($" 📍 일반 처리: {connectedNodeId} 추가"); - } - } - - Console.WriteLine($" 🎯 물리적 방향 가능 노드들: [{string.Join(", ", possibleNodes)}]"); - return possibleNodes; - } - - /// - /// 물리적 제약사항을 고려한 경로 찾기 - /// AGV 갈림길 즉시 방향전환 금지 규칙 적용 - /// - private List FindPathWithPhysicalConstraints(MapNode startNode, MapNode targetNode, List nextPossibleNodes, AgvDirection motorDirection) - { - Console.WriteLine($"\n🔧 물리적 제약사항 경로 계산:"); - Console.WriteLine($" 시작: {startNode.NodeId}, 목표: {targetNode.NodeId}, 모터방향: {motorDirection}"); - - // AGV 물리적 제약사항 체크: 갈림길에서 즉시 방향전환 금지 - if (IsImmediateDirectionChangeRequired(startNode, targetNode, motorDirection)) - { - Console.WriteLine($" ⚠️ {startNode.NodeId}에서 즉시 방향전환 필요 - 우회 경로 탐색"); - return FindDetourPath(startNode, targetNode, nextPossibleNodes, motorDirection); - } - - // 일반적인 경로 계산 - if (nextPossibleNodes.Count > 0) - { - var nextNode = _mapNodes.FirstOrDefault(n => n.NodeId == nextPossibleNodes[0]); - if (nextNode != null) - { - // 다음 노드에서 목적지까지의 경로 계산 - var pathFromNext = _pathfinder.FindPath(nextNode, targetNode, motorDirection); - if (pathFromNext.Success) - { - // 현재 노드 + 다음 노드부터의 경로 - var fullPath = new List { startNode.NodeId }; - fullPath.AddRange(pathFromNext.Path.Skip(1)); // 중복 제거 - return fullPath; - } - } - } - - return new List(); - } - - /// - /// 빠른 단일 테스트 (콘솔 입력 없이) - /// - public void RunQuickTest(string startRfid, string targetRfid, AgvDirection currentDirection) - { - Console.WriteLine($"\n🚀 빠른 테스트: RFID {startRfid} → RFID {targetRfid} (방향: {currentDirection})"); - - var startNode = GetNodeByRfid(startRfid); - var targetNode = GetNodeByRfid(targetRfid); - - if (startNode == null) - { - Console.WriteLine($"❌ 시작 RFID {startRfid}를 찾을 수 없습니다."); - return; - } - - if (targetNode == null) - { - Console.WriteLine($"❌ 목표 RFID {targetRfid}를 찾을 수 없습니다."); - return; - } - - Console.WriteLine($"📍 매핑: RFID {startRfid} → {startNode.NodeId}, RFID {targetRfid} → {targetNode.NodeId}"); - - // 기본 경로 탐색 및 RFID 변환 - var basicResult = _pathfinder.FindPath(startNode, targetNode, currentDirection); - if (basicResult.Success) - { - Console.WriteLine($"\n🗺️ RFID 경로: {ConvertPathToRfid(basicResult.Path)}"); - Console.WriteLine($"📏 단계: {basicResult.Path.Count}"); - } - - RunSingleTest(startNode.NodeId, targetNode.NodeId, currentDirection); - } - - /// - /// 완전한 RFID 매핑 표시 (맵 파일 기반) - /// - public void ShowCompleteRfidMapping() - { - Console.WriteLine("\n📋 완전한 RFID → NodeID 매핑:"); - - // 실제 맵 파일에서 추출한 완전한 매핑 - var completeMapping = new Dictionary - { - {"001", "N001"}, {"002", "N002"}, {"003", "N003"}, {"004", "N004"}, - {"005", "N011"}, {"006", "N012"}, {"007", "N013"}, {"008", "N014"}, {"009", "N008"}, - {"010", "N009"}, {"011", "N010"}, {"012", "N022"}, {"013", "N006"}, {"014", "N007"}, - {"015", "N019"}, {"016", "N023"}, {"017", "N024"}, {"018", "N025"}, {"019", "N026"}, - {"030", "N031"}, {"031", "N021"}, {"032", "N020"}, {"033", "N005"}, {"034", "N018"}, - {"035", "N017"}, {"036", "N016"}, {"037", "N015"}, {"038", "N030"}, {"039", "N029"}, - {"040", "N028"}, {"041", "N027"} - }; - - foreach (var mapping in completeMapping.OrderBy(m => m.Key)) - { - var node = _mapNodes.FirstOrDefault(n => n.NodeId == mapping.Value); - if (node != null) - { - Console.WriteLine($" RFID {mapping.Key} → {mapping.Value} ({node.Type}, {node.DockDirection})"); - } - } - } - - /// - /// RFID 기반 경로를 실제 RFID 번호로 변환하여 표시 - /// - public string ConvertPathToRfid(List path) - { - if (path == null || path.Count == 0) return ""; - - var rfidPath = new List(); - foreach (var nodeId in path) - { - var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); - if (node != null && !string.IsNullOrEmpty(node.RfidId)) - { - rfidPath.Add(node.RfidId); - } - else - { - rfidPath.Add($"{nodeId}(RFID없음)"); - } - } - - return string.Join(" → ", rfidPath); - } - - /// - /// 갈림길에서 즉시 방향전환이 필요한지 체크 - /// - private bool IsImmediateDirectionChangeRequired(MapNode currentNode, MapNode targetNode, AgvDirection motorDirection) - { - // ✅ 1단계: 일반화된 도킹 방향 체크 (최우선) - var targetDockingDirection = GetNodeDockingDirection(targetNode.NodeId); - if (motorDirection != targetDockingDirection) - { - Console.WriteLine($" 🎯 일반화된 방향전환 감지: {motorDirection} → {targetDockingDirection}"); - return true; - } - - // 2단계: 기존 특별 케이스들 - // 특별 처리: 032→031 후진 이동 AGV가 008로 가는 경우 - if (currentNode.NodeId == "N021" && targetNode.NodeId == "N014") // 031→008 - { - Console.WriteLine($" 🎯 특별 케이스 감지: 031→008 (032→031 후진 이동 AGV)"); - Console.WriteLine($" 📝 이 경우 특별 우회 로직 필요: 031→005→004→008"); - return true; - } - - // 3단계: 갈림길 물리적 제약사항 체크 - if (currentNode.ConnectedNodes.Count < 3) - { - return false; // 갈림길이 아니면 문제없음 - } - - // 기본 경로로 목적지까지 가는 첫 번째 노드를 찾음 - var basicPath = _pathfinder.FindPath(currentNode, targetNode, motorDirection); - if (!basicPath.Success || basicPath.Path.Count < 2) - { - return false; - } - - var nextNodeInPath = basicPath.Path[1]; // 기본 경로의 다음 노드 - - // 현재 노드에서 물리적으로 같은 방향으로 계속 갈 수 있는 노드와 비교 - var physicalDirection = $"toward_next_from_{currentNode.NodeId}"; - var nextPossibleNodes = GetNextNodesInPhysicalDirection(currentNode, physicalDirection, motorDirection); - - // 기본 경로의 다음 노드가 물리적으로 가능한 노드와 다르면 즉시 방향전환 필요 - if (nextPossibleNodes.Count > 0 && !nextPossibleNodes.Contains(nextNodeInPath)) - { - Console.WriteLine($" 🚨 즉시 방향전환 감지: 물리적 방향({string.Join(",", nextPossibleNodes)}) vs 경로 방향({nextNodeInPath})"); - return true; - } - - return false; - } - - /// - /// 우회 경로 탐색 (갈림길 즉시 방향전환 회피) - /// 032→031→008 시나리오: 031→005(전진)→004(좌회전)→008(후진) - /// - private List FindDetourPath(MapNode startNode, MapNode targetNode, List nextPossibleNodes, AgvDirection motorDirection) - { - Console.WriteLine($" 🔄 우회 경로 탐색 시작:"); - - // 일반화된 처리: 방향전환이 필요한 경우 037 버퍼 활용 - var startNodeType = GetNodeDockingDirection(startNode.NodeId); - var targetNodeType = GetNodeDockingDirection(targetNode.NodeId); - - // 현재 전진 방향이고 목적지가 후진 도킹인 경우 - if (motorDirection == AgvDirection.Forward && targetNodeType == AgvDirection.Backward) - { - Console.WriteLine($" 🎯 방향전환 필요: {motorDirection} → {targetNodeType}"); - return HandleDirectionChangeViaBuffer(startNode, targetNode, motorDirection); - } - - // 현재 후진 방향이고 목적지가 전진 도킹인 경우 - if (motorDirection == AgvDirection.Backward && targetNodeType == AgvDirection.Forward) - { - Console.WriteLine($" 🎯 방향전환 필요: {motorDirection} → {targetNodeType}"); - return HandleDirectionChangeViaBuffer(startNode, targetNode, motorDirection); - } - - // 특별 처리: 010→011 후진 도킹 전용 노드 (방향 전환 필요) - if (startNode.NodeId == "N009" && targetNode.NodeId == "N010") // 010→011 - { - Console.WriteLine($" 🎯 특별 시나리오: 010→011 후진 도킹 (방향 전환 필요)"); - Console.WriteLine($" 📝 경로 설명: 010→009→030→004→003(방향조정)→004→030→009→010→011(후진도킹)"); - - // 1단계: 010→009→030→004 (후진으로 방향전환 지점까지) - var node009 = _mapNodes.First(n => n.NodeId == "N008"); // 009 - var node030 = _mapNodes.First(n => n.NodeId == "N031"); // 030 - var node004 = _mapNodes.First(n => n.NodeId == "N004"); // 004 - var node003 = _mapNodes.First(n => n.NodeId == "N003"); // 003 - - // 010→009→030→004 후진 경로 - var pathToJunction = new List { "N009", "N008", "N031", "N004" }; - Console.WriteLine($" 📍 1단계: 010→009→030→004 후진 이동"); - - // 2단계: 004→003 전진 이동 (방향 조정) - var pathForward = new List { "N004", "N003" }; - Console.WriteLine($" 📍 2단계: 004→003 전진 이동 (방향 조정)"); - - // 3단계: 003→004→030→009→010→011 후진 도킹 - var pathBackToDocking = new List { "N003", "N004", "N031", "N008", "N009", "N010" }; - Console.WriteLine($" 📍 3단계: 003→004→030→009→010→011 후진 도킹"); - - // 전체 경로 조합 - var specialPath = new List(); - specialPath.AddRange(pathToJunction); - - // 중복 제거하며 전진 구간 추가 - foreach (var nodeId in pathForward.Skip(1)) - { - if (!specialPath.Contains(nodeId)) - specialPath.Add(nodeId); - } - - // 중복 제거하며 후진 도킹 구간 추가 - foreach (var nodeId in pathBackToDocking.Skip(1)) - { - if (!specialPath.Contains(nodeId)) - specialPath.Add(nodeId); - } - - return specialPath; - } - - // 특별 처리: 015→019 충전기 간 이동 (012에서 004를 거쳐야 함) - if (startNode.NodeId == "N019" && targetNode.NodeId == "N026") // 015→019 - { - Console.WriteLine($" 🎯 특별 시나리오: 015→019 충전기 간 이동 (012→004→016 경로)"); - - // 1단계: 015→014→013→012 경로 - var node012 = _mapNodes.First(n => n.NodeId == "N022"); // 012 - var pathTo012 = _pathfinder.FindPath(startNode, node012, AgvDirection.Forward); - if (!pathTo012.Success) - { - Console.WriteLine($" ❌ 015→012 경로 계산 실패"); - return new List(); - } - - // 2단계: 012→004 이동 (물리적 제약으로 인한 우회) - var node004 = _mapNodes.First(n => n.NodeId == "N004"); // 004 - var pathTo004 = _pathfinder.FindPath(node012, node004, AgvDirection.Forward); - if (!pathTo004.Success) - { - Console.WriteLine($" ❌ 012→004 경로 계산 실패"); - return new List(); - } - - // 3단계: 004→016→017→018→019 경로 (방향전환 후) - var node016 = _mapNodes.First(n => n.NodeId == "N023"); // 016 - var pathFrom004 = _pathfinder.FindPath(node004, targetNode, AgvDirection.Backward); - if (!pathFrom004.Success) - { - Console.WriteLine($" ❌ 004→019 후진 경로 계산 실패"); - return new List(); - } - - // 전체 경로 조합: 015→014→013→012→004→016→017→018→019 - var specialPath = new List(); - - // 015→012 경로 추가 - specialPath.AddRange(pathTo012.Path); - - // 012→004 경로 추가 (중복 제거) - foreach (var nodeId in pathTo004.Path.Skip(1)) - { - if (!specialPath.Contains(nodeId)) - specialPath.Add(nodeId); - } - - // 004→019 경로 추가 (중복 제거) - foreach (var nodeId in pathFrom004.Path.Skip(1)) - { - if (!specialPath.Contains(nodeId)) - specialPath.Add(nodeId); - } - - Console.WriteLine($" 📝 경로 설명: 015→012(전진)→004(우회)→방향전환→016→019(후진)"); - return specialPath; - } - - // 특별 처리: 032→031 후진 이동 AGV가 008로 가는 경우 - if (startNode.NodeId == "N021" && targetNode.NodeId == "N014") // 031→008 - { - Console.WriteLine($" 🎯 특별 시나리오: 031→008 (032→031 후진 이동 AGV)"); - - // 1단계: 031→005 전진 이동 (갈림길까지) - var pathTo005 = _pathfinder.FindPath(startNode, _mapNodes.First(n => n.NodeId == "N011"), AgvDirection.Forward); - if (!pathTo005.Success) - { - Console.WriteLine($" ❌ 031→005 경로 계산 실패"); - return new List(); - } - - // 2단계: 005→004 전진 이동 (좌회전, 리프트 방향 변경) - var node004 = _mapNodes.First(n => n.NodeId == "N004"); - - // 3단계: 004→008 후진 이동 (리프트가 004 방향을 향하므로) - var pathFrom004 = _pathfinder.FindPath(node004, targetNode, AgvDirection.Backward); - if (!pathFrom004.Success) - { - Console.WriteLine($" ❌ 004→008 후진 경로 계산 실패"); - return new List(); - } - - // 전체 경로 조합: 031→...→005→004→...→008 - var specialPath = new List(); - - // 031→005 경로 추가 - specialPath.AddRange(pathTo005.Path); - - // 005→004 추가 (중복 제거) - if (!specialPath.Contains("N004")) - { - specialPath.Add("N004"); - } - - // 004→008 경로 추가 (중복 제거) - for (int i = 1; i < pathFrom004.Path.Count; i++) - { - if (!specialPath.Contains(pathFrom004.Path[i])) - { - specialPath.Add(pathFrom004.Path[i]); - } - } - - Console.WriteLine($" ✅ 특별 우회 경로: {string.Join(" → ", specialPath)}"); - Console.WriteLine($" 📝 경로 설명: 031→005(전진)→004(좌회전,리프트방향변경)→008(후진)"); - return specialPath; - } - - // 기존 일반적인 우회 로직 - if (nextPossibleNodes.Count == 0) - { - Console.WriteLine($" ❌ 물리적 방향으로 갈 수 있는 노드가 없음"); - return new List(); - } - - var detourNodeId = nextPossibleNodes[0]; - var detourNode = _mapNodes.FirstOrDefault(n => n.NodeId == detourNodeId); - if (detourNode == null) - { - Console.WriteLine($" ❌ 우회 노드 {detourNodeId}를 찾을 수 없음"); - return new List(); - } - - Console.WriteLine($" 📍 우회 노드: {detourNodeId}"); - - // 일반 우회 경로 계산 - var returnPath = _pathfinder.FindPath(detourNode, startNode, AgvDirection.Forward); - if (!returnPath.Success) - { - Console.WriteLine($" ❌ 우회 노드에서 복귀 경로 계산 실패"); - return new List(); - } - - var finalPath = _pathfinder.FindPath(startNode, targetNode, AgvDirection.Forward); - if (!finalPath.Success) - { - Console.WriteLine($" ❌ 갈림길에서 목적지까지 경로 계산 실패"); - return new List(); - } - - var fullDetourPath = new List(); - fullDetourPath.Add(startNode.NodeId); - fullDetourPath.Add(detourNodeId); - - for (int i = 1; i < returnPath.Path.Count; i++) - { - if (!fullDetourPath.Contains(returnPath.Path[i])) - { - fullDetourPath.Add(returnPath.Path[i]); - } - } - - for (int i = 1; i < finalPath.Path.Count; i++) - { - if (!fullDetourPath.Contains(finalPath.Path[i])) - { - fullDetourPath.Add(finalPath.Path[i]); - } - } - - Console.WriteLine($" ✅ 우회 경로 완성: {string.Join(" → ", fullDetourPath)}"); - return fullDetourPath; - } - - /// - /// 노드의 도킹 방향 요구사항 조회 - /// - private AgvDirection GetNodeDockingDirection(string nodeId) - { - var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); - if (node == null) return AgvDirection.Forward; - - // DockDirection에 따른 도킹 방향 - // DockDirection.Forward: 전진 도킹 (충전기) - // DockDirection.Backward: 후진 도킹 (장비) - return (node.DockDirection == DockingDirection.Backward) ? AgvDirection.Backward : AgvDirection.Forward; - } - - /// - /// 동적 버퍼를 활용한 방향전환 경로 생성 (완전 일반화) - /// - private List HandleDirectionChangeViaBuffer(MapNode startNode, MapNode targetNode, AgvDirection currentDirection) - { - Console.WriteLine($" 🔄 동적 방향전환 로직 시작"); - - // 1단계: 목적지까지의 기본 경로를 구해서 가장 가까운 갈림길 찾기 - var basicPath = _pathfinder.FindPath(startNode, targetNode, currentDirection); - if (!basicPath.Success) - { - Console.WriteLine($" ❌ 기본 경로 계산 실패"); - return new List(); - } - - // 2단계: 경로상에서 연결 노드가 3개 이상인 갈림길 찾기 - MapNode junctionNode = null; - foreach (var nodeId in basicPath.Path) - { - var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); - if (node != null && node.ConnectedNodes.Count >= 3) - { - junctionNode = node; - Console.WriteLine($" 📍 갈림길 발견: {nodeId} (연결: {node.ConnectedNodes.Count}개)"); - break; - } - } - - if (junctionNode == null) - { - Console.WriteLine($" ❌ 경로상에 갈림길을 찾을 수 없음"); - return new List(); - } - - // 3단계: 갈림길에서 기본 경로가 아닌 버퍼 노드 찾기 - var mainPathNextNode = basicPath.Path[basicPath.Path.IndexOf(junctionNode.NodeId) + 1]; - string bufferNodeId = null; - - foreach (var connectedNodeId in junctionNode.ConnectedNodes) - { - if (connectedNodeId != mainPathNextNode) - { - var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); - // 버퍼로 사용 가능한 노드 (연결 수가 적은 노드 우선) - if (connectedNode != null && connectedNode.ConnectedNodes.Count <= 2) - { - bufferNodeId = connectedNodeId; - Console.WriteLine($" 📍 버퍼 노드 발견: {bufferNodeId}"); - break; - } - } - } - - if (bufferNodeId == null) - { - Console.WriteLine($" ❌ 적절한 버퍼 노드를 찾을 수 없음"); - return new List(); - } - - // 4단계: 갈림길까지의 경로 계산 - var pathToJunction = _pathfinder.FindPath(startNode, junctionNode, currentDirection); - if (!pathToJunction.Success) - { - Console.WriteLine($" ❌ {startNode.NodeId}→{junctionNode.NodeId} 경로 계산 실패"); - return new List(); - } - - // 5단계: 버퍼→목적지까지 변경된 방향으로 경로 계산 - var bufferNode = _mapNodes.FirstOrDefault(n => n.NodeId == bufferNodeId); - var targetDirection = GetNodeDockingDirection(targetNode.NodeId); - var pathFromBuffer = _pathfinder.FindPath(bufferNode, targetNode, targetDirection); - if (!pathFromBuffer.Success) - { - Console.WriteLine($" ❌ {bufferNodeId}→{targetNode.NodeId} 경로 계산 실패"); - return new List(); - } - - // 6단계: 전체 경로 조합 - var fullPath = new List(); - - // 시작→갈림길 경로 추가 - fullPath.AddRange(pathToJunction.Path); - - // 버퍼 노드 추가 - if (!fullPath.Contains(bufferNodeId)) - { - fullPath.Add(bufferNodeId); - } - - // 버퍼→목적지 경로 추가 (중복 제거) - foreach (var nodeId in pathFromBuffer.Path.Skip(1)) - { - if (!fullPath.Contains(nodeId)) - fullPath.Add(nodeId); - } - - Console.WriteLine($" ✅ 동적 버퍼 경유 경로: {string.Join(" → ", fullPath)}"); - Console.WriteLine($" 📝 갈림길: {junctionNode.NodeId}, 버퍼: {bufferNodeId}"); - Console.WriteLine($" 📝 방향전환: {currentDirection} → {targetDirection}"); - return fullPath; - } - - #endregion - } -} \ No newline at end of file diff --git a/Cs_HMI/AGVPathTester/Program.cs b/Cs_HMI/AGVPathTester/Program.cs deleted file mode 100644 index f364dc0..0000000 --- a/Cs_HMI/AGVPathTester/Program.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace AGVPathTester -{ - /// - /// AGV 경로 탐색 및 방향전환 로직 테스트 프로그램 - /// - class Program - { - static void Main(string[] args) - { - Console.Title = "AGV Path Tester - ENIG Navigation System"; - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine("======================================"); - Console.WriteLine(" AGV Path Tester v1.0"); - Console.WriteLine(" ENIG Navigation System"); - Console.WriteLine("======================================"); - Console.ResetColor(); - Console.WriteLine(); - - try - { - // 맵 파일 경로 - string mapFilePath = @"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap"; - - if (!File.Exists(mapFilePath)) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 맵 파일을 찾을 수 없습니다: {mapFilePath}"); - Console.ResetColor(); - Console.WriteLine("Enter 키를 눌러 종료하세요..."); - Console.ReadLine(); - return; - } - - // PathTester 초기화 - var pathTester = new PathTester(mapFilePath); - - if (!pathTester.Initialize()) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("❌ PathTester 초기화 실패"); - Console.ResetColor(); - Console.WriteLine("Enter 키를 눌러 종료하세요..."); - Console.ReadLine(); - return; - } - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("✅ PathTester 초기화 완료"); - Console.WriteLine($"📍 로드된 맵 노드 수: {pathTester.GetNodeCount()}"); - Console.ResetColor(); - Console.WriteLine(); - - // 자동 테스트 모드 체크 - bool autoMode = args.Length > 0 && (args[0].ToLower() == "auto" || args[0].ToLower() == "batch"); - - if (autoMode) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("🚀 자동 테스트 모드 실행"); - Console.ResetColor(); - Console.WriteLine(); - - // 먼저 맵 정보 표시 - ShowMapInfo(pathTester); - Console.WriteLine("\n" + new string('=', 50) + "\n"); - - // 자동으로 모든 테스트 실행 - RunBatchTests(pathTester); - - Console.WriteLine(); - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine("📊 모든 자동 테스트 완료!"); - Console.ResetColor(); - return; - } - - // 빠른 테스트 모드 체크 (quick 002 015 forward) - if (args.Length >= 4 && args[0].ToLower() == "quick") - { - string startRfid = args[1]; - string targetRfid = args[2]; - string directionStr = args[3].ToLower(); - - var direction = directionStr == "backward" ? - AGVNavigationCore.Models.AgvDirection.Backward : - AGVNavigationCore.Models.AgvDirection.Forward; - - pathTester.RunQuickTest(startRfid, targetRfid, direction); - return; - } - - // 이동 기반 테스트 모드 체크 (movement 033 032 001 backward) - if (args.Length >= 5 && args[0].ToLower() == "movement") - { - string previousRfid = args[1]; - string currentRfid = args[2]; - string targetRfid = args[3]; - string directionStr = args[4].ToLower(); - - var userDirection = directionStr == "backward" ? - AGVNavigationCore.Models.AgvDirection.Backward : - AGVNavigationCore.Models.AgvDirection.Forward; - - pathTester.RunMovementBasedTestWithUserDirection(previousRfid, currentRfid, targetRfid, userDirection); - return; - } - - // 대화형 메뉴 - bool continueRunning = true; - while (continueRunning) - { - ShowMenu(); - - Console.Write("선택: "); - var input = Console.ReadLine()?.Trim(); - - switch (input) - { - case "1": - RunBasicPathTests(pathTester); - break; - case "2": - RunDirectionChangeTests(pathTester); - break; - case "3": - RunCustomTest(pathTester); - break; - case "4": - RunBatchTests(pathTester); - break; - case "5": - ShowMapInfo(pathTester); - break; - case "0": - case "q": - case "quit": - case "exit": - continueRunning = false; - break; - default: - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("❓ 잘못된 선택입니다."); - Console.ResetColor(); - break; - } - - if (continueRunning) - { - Console.WriteLine("\nEnter 키를 눌러 계속하세요..."); - Console.ReadLine(); - // Console.Clear(); // Windows 콘솔 호환성 문제로 제거 - } - } - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"❌ 프로그램 오류: {ex.Message}"); - Console.WriteLine($"상세 정보: {ex}"); - Console.ResetColor(); - Console.WriteLine("Enter 키를 눌러 종료하세요..."); - Console.ReadLine(); - } - } - - static void ShowMenu() - { - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine("============ 메뉴 ============"); - Console.WriteLine("1. 기본 경로 탐색 테스트"); - Console.WriteLine("2. 방향 전환 경로 테스트"); - Console.WriteLine("3. 사용자 정의 테스트"); - Console.WriteLine("4. 배치 테스트 실행"); - Console.WriteLine("5. 맵 정보 보기"); - Console.WriteLine("0. 종료"); - Console.WriteLine("============================="); - Console.ResetColor(); - } - - static void RunBasicPathTests(PathTester pathTester) - { - Console.ForegroundColor = ConsoleColor.Magenta; - Console.WriteLine("🧪 기본 경로 탐색 테스트 실행"); - Console.ResetColor(); - - pathTester.RunBasicPathTests(); - } - - static void RunDirectionChangeTests(PathTester pathTester) - { - Console.ForegroundColor = ConsoleColor.Magenta; - Console.WriteLine("🔄 방향 전환 경로 테스트 실행"); - Console.ResetColor(); - - pathTester.RunDirectionChangeTests(); - } - - static void RunCustomTest(PathTester pathTester) - { - Console.ForegroundColor = ConsoleColor.Magenta; - Console.WriteLine("🎯 사용자 정의 테스트"); - Console.ResetColor(); - - Console.Write("시작 노드 ID: "); - string startNodeId = Console.ReadLine()?.Trim(); - - Console.Write("목표 노드 ID: "); - string targetNodeId = Console.ReadLine()?.Trim(); - - Console.WriteLine("현재 방향 선택:"); - Console.WriteLine("1. Forward (전진)"); - Console.WriteLine("2. Backward (후진)"); - Console.Write("선택 (기본값: Forward): "); - - var directionInput = Console.ReadLine()?.Trim(); - var currentDirection = (directionInput == "2") ? - AGVNavigationCore.Models.AgvDirection.Backward : - AGVNavigationCore.Models.AgvDirection.Forward; - - if (!string.IsNullOrEmpty(startNodeId) && !string.IsNullOrEmpty(targetNodeId)) - { - pathTester.RunSingleTest(startNodeId, targetNodeId, currentDirection); - } - else - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("❓ 시작 노드 또는 목표 노드 ID가 비어있습니다."); - Console.ResetColor(); - } - } - - static void RunBatchTests(PathTester pathTester) - { - Console.ForegroundColor = ConsoleColor.Magenta; - Console.WriteLine("📦 배치 테스트 실행"); - Console.ResetColor(); - - pathTester.RunBatchTests(); - } - - static void ShowMapInfo(PathTester pathTester) - { - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine("🗺️ 맵 정보"); - Console.ResetColor(); - - pathTester.ShowMapInfo(); - } - } -} \ No newline at end of file diff --git a/Cs_HMI/AGVPathTester/Properties/AssemblyInfo.cs b/Cs_HMI/AGVPathTester/Properties/AssemblyInfo.cs deleted file mode 100644 index bef7c27..0000000 --- a/Cs_HMI/AGVPathTester/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("AGVPathTester")] -[assembly: AssemblyDescription("AGV 경로 탐색 및 방향전환 로직 테스트 도구")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("ENIG")] -[assembly: AssemblyProduct("AGV Navigation System")] -[assembly: AssemblyCopyright("Copyright © ENIG 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -[assembly: ComVisible(false)] - -[assembly: Guid("a1b2c3d4-e5f6-7890-abcd-ef1234567890")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/Cs_HMI/AGVPathTester/TestCases.cs b/Cs_HMI/AGVPathTester/TestCases.cs deleted file mode 100644 index 0e170a8..0000000 --- a/Cs_HMI/AGVPathTester/TestCases.cs +++ /dev/null @@ -1,274 +0,0 @@ -using System.Collections.Generic; -using AGVNavigationCore.Models; - -namespace AGVPathTester -{ - /// - /// AGV 경로 탐색 테스트 케이스 정의 - /// - public static class TestCases - { - /// - /// 기본 경로 탐색 테스트 케이스 - /// - public class BasicPathTestCase - { - public string StartNodeId { get; set; } - public string TargetNodeId { get; set; } - public AgvDirection CurrentDirection { get; set; } - public string Description { get; set; } - public bool ExpectedSuccess { get; set; } - - public BasicPathTestCase(string startNodeId, string targetNodeId, AgvDirection currentDirection, string description, bool expectedSuccess = true) - { - StartNodeId = startNodeId; - TargetNodeId = targetNodeId; - CurrentDirection = currentDirection; - Description = description; - ExpectedSuccess = expectedSuccess; - } - } - - /// - /// 방향 전환 테스트 케이스 - /// - public class DirectionChangeTestCase - { - public string StartNodeId { get; set; } - public string TargetNodeId { get; set; } - public AgvDirection CurrentDirection { get; set; } - public AgvDirection RequiredDirection { get; set; } - public string Description { get; set; } - public bool ExpectedSuccess { get; set; } - public List ExpectedPath { get; set; } - - public DirectionChangeTestCase(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection, string description, bool expectedSuccess = true, List expectedPath = null) - { - StartNodeId = startNodeId; - TargetNodeId = targetNodeId; - CurrentDirection = currentDirection; - RequiredDirection = requiredDirection; - Description = description; - ExpectedSuccess = expectedSuccess; - ExpectedPath = expectedPath ?? new List(); - } - } - - /// - /// 이동 기반 테스트 케이스 (이전 위치 → 현재 위치 → 목표 위치) - /// - public class MovementBasedTestCase - { - public string PreviousRfid { get; set; } - public string CurrentRfid { get; set; } - public string TargetRfid { get; set; } - public AgvDirection ExpectedDirection { get; set; } - public string Description { get; set; } - public bool ExpectedSuccess { get; set; } - public List ExpectedRfidPath { get; set; } - - public MovementBasedTestCase(string previousRfid, string currentRfid, string targetRfid, AgvDirection expectedDirection, string description, bool expectedSuccess = true, List expectedRfidPath = null) - { - PreviousRfid = previousRfid; - CurrentRfid = currentRfid; - TargetRfid = targetRfid; - ExpectedDirection = expectedDirection; - Description = description; - ExpectedSuccess = expectedSuccess; - ExpectedRfidPath = expectedRfidPath ?? new List(); - } - } - - /// - /// 기본 경로 탐색 테스트 케이스 목록 (RFID 기반으로 수정) - /// RFID 매핑: 001→N001, 005→N011, 037→N015, 041→존재하지않음 - /// - public static List GetBasicPathTestCases() - { - return new List - { - // 단순 직선 경로 - new BasicPathTestCase("N001", "N002", AgvDirection.Forward, "단순 인접 노드 이동"), - new BasicPathTestCase("N001", "N003", AgvDirection.Forward, "단거리 직선 경로"), - - // 중거리 경로 - new BasicPathTestCase("N001", "N010", AgvDirection.Forward, "중거리 경로 탐색"), - new BasicPathTestCase("N011", "N019", AgvDirection.Backward, "RFID 005→015: 후진 상태에서 중거리 이동"), - - // 갈림길을 포함한 경로 - new BasicPathTestCase("N001", "N020", AgvDirection.Forward, "갈림길 포함 경로"), - new BasicPathTestCase("N010", "N030", AgvDirection.Forward, "복잡한 갈림길 경로"), - - // RFID 기반 실제 경로 (PATHSCENARIO.md 기반) - 올바른 NodeID 매핑 사용 - new BasicPathTestCase("N013", "N019", AgvDirection.Forward, "RFID 007→015: 직진 경로"), - new BasicPathTestCase("N013", "N026", AgvDirection.Forward, "RFID 007→019: 충전기 경로"), - new BasicPathTestCase("N013", "N001", AgvDirection.Forward, "RFID 007→001: 장비 경로"), - - // 갈림길 테스트 (RFID 005는 N011 갈림길) - new BasicPathTestCase("N004", "N019", AgvDirection.Forward, "RFID 004→015: N011 갈림길 통과"), - new BasicPathTestCase("N012", "N023", AgvDirection.Backward, "RFID 012→016: 갈림길 역방향"), - }; - } - - /// - /// 방향 전환 테스트 케이스 목록 (PATHSCENARIO.md RFID 기반) - /// RFID → NodeID 매핑: 005→N011, 037→N028, 007→N013, 015→N019, 001→N001, 004→N004 - /// - public static List GetDirectionChangeTestCases() - { - return new List - { - // PATHSCENARIO.md Case 1: AGV가 전진방향으로 008→007 이동 후 - // Q1: 007→015 (충전기, 전진 도킹) - 방향전환 불필요 - new DirectionChangeTestCase("N013", "N019", AgvDirection.Forward, AgvDirection.Forward, - "Q1: RFID 007→015 (충전기, 전진 도킹) - 방향전환 불필요", - true, new List { "N013", "N012", "N011", "N004", "N022", "N023", "N024", "N019" }), - - // Q2: 007→019 (충전기, 전진 도킹) - 방향전환 불필요 - new DirectionChangeTestCase("N013", "N026", AgvDirection.Forward, AgvDirection.Forward, - "Q2: RFID 007→019 (충전기, 전진 도킹) - 방향전환 불필요", - true, new List { "N013", "N012", "N011", "N004", "N022", "N023", "N024", "N025", "N026" }), - - // Q3: 007→001 (장비, 후진 도킹) - 005 갈림길에서 037 우회 - new DirectionChangeTestCase("N013", "N001", AgvDirection.Forward, AgvDirection.Backward, - "Q3: RFID 007→001 (장비, 후진 도킹) - 005 갈림길에서 037 우회", - true, new List { "N013", "N012", "N011", "N028", "N011", "N004", "N003", "N002", "N001" }), - - // Q4: 007→011 (장비, 후진 도킹) - 005 갈림길에서 037 우회 - // RFID 011 → N010 - new DirectionChangeTestCase("N013", "N010", AgvDirection.Forward, AgvDirection.Backward, - "Q4: RFID 007→011 (장비, 후진 도킹) - 005 갈림길에서 037 우회", - true, new List { "N013", "N012", "N011", "N028", "N011", "N004", "N031", "N008", "N009", "N010" }), - - // Case 2: AGV가 후진방향으로 008→007 이동 후 - // Q7: 007→015 (충전기, 전진 도킹) - 005에서 037 우회 후 전환 - new DirectionChangeTestCase("N013", "N019", AgvDirection.Backward, AgvDirection.Forward, - "Q7: RFID 007→015 (충전기, 전진 도킹) - 005에서 037 우회 후 전환", - true, new List { "N013", "N012", "N011", "N028", "N011", "N004", "N022", "N023", "N024", "N019" }), - - // 실제 맵 기반 간단한 테스트들 - // N004 갈림길 활용 (N003, N022, N031 연결) - new DirectionChangeTestCase("N003", "N022", AgvDirection.Forward, AgvDirection.Backward, - "N004 갈림길: N003→N022 (전진→후진 전환)", - true, new List { "N003", "N004", "N022" }), - - new DirectionChangeTestCase("N003", "N031", AgvDirection.Forward, AgvDirection.Backward, - "N004 갈림길: N003→N031 (전진→후진 전환)", - true, new List { "N003", "N004", "N031" }), - - // N011 갈림길 활용 (N012, N004, N015 연결) - RFID 005는 N011 - new DirectionChangeTestCase("N012", "N019", AgvDirection.Forward, AgvDirection.Backward, - "N011 갈림길 (RFID 005): N012→N019 (전진→후진 전환)", - true, new List { "N012", "N011", "N004", "N022", "N023", "N024", "N019" }), - - // 방향 전환이 필요 없는 경우 (같은 방향) - new DirectionChangeTestCase("N013", "N019", AgvDirection.Forward, AgvDirection.Forward, - "방향 전환 불필요: RFID 007→015 (전진→전진)", - true, new List { "N013", "N012", "N011", "N004", "N022", "N023", "N024", "N019" }), - - new DirectionChangeTestCase("N012", "N019", AgvDirection.Backward, AgvDirection.Backward, - "방향 전환 불필요: N012→N019 (후진→후진)", - true, new List { "N012", "N011", "N004", "N022", "N023", "N024", "N019" }) - }; - } - - /// - /// 특정 문제 시나리오 테스트 케이스 (디버깅용) - /// - public static List GetProblematicTestCases() - { - return new List - { - // 사용자가 직접 보고한 문제들 (RFID 매핑 적용) - new DirectionChangeTestCase("N004", "N015", AgvDirection.Forward, AgvDirection.Backward, - "🚨 사용자 보고: RFID 004→037(N015) 되돌아가기 발생"), - new DirectionChangeTestCase("N012", "N015", AgvDirection.Backward, AgvDirection.Backward, - "🚨 사용자 보고: RFID 012→037(N015) 비효율적 경로"), - new DirectionChangeTestCase("N012", "N023", AgvDirection.Backward, AgvDirection.Forward, - "🚨 사용자 보고: RFID 012→016(N023) 되돌아가기 발생"), - }; - } - - /// - /// 스트레스 테스트용 대량 케이스 생성 - /// - public static List GenerateStressTestCases(List allNodeIds, int count = 50) - { - var testCases = new List(); - var random = new System.Random(42); // 고정 시드로 재현 가능한 테스트 - - for (int i = 0; i < count; i++) - { - var startNodeId = allNodeIds[random.Next(allNodeIds.Count)]; - var targetNodeId = allNodeIds[random.Next(allNodeIds.Count)]; - var direction = random.Next(2) == 0 ? AgvDirection.Forward : AgvDirection.Backward; - - if (startNodeId != targetNodeId) - { - testCases.Add(new BasicPathTestCase( - startNodeId, - targetNodeId, - direction, - $"스트레스 테스트 #{i + 1}" - )); - } - } - - return testCases; - } - - /// - /// 이동 기반 테스트 케이스 목록 (실제 AGV 이동 시나리오) - /// - public static List GetMovementBasedTestCases() - { - return new List - { - // 핵심 시나리오: 033→032 후진 이동 중인 AGV가 001로 가는 경우 - new MovementBasedTestCase("033", "032", "001", AgvDirection.Backward, - "🎯 핵심 시나리오: RFID 033→032 후진 이동 AGV가 001(장비) 목적지", - true, new List { "032", "031", "030", "...", "001" }), - - // 다양한 이동 패턴 테스트 - new MovementBasedTestCase("001", "002", "015", AgvDirection.Forward, - "RFID 001→002 전진 이동 AGV가 015(충전기) 목적지", - true, new List { "002", "003", "004", "012", "013", "014", "015" }), - - new MovementBasedTestCase("015", "014", "001", AgvDirection.Backward, - "RFID 015→014 후진 이동 AGV가 001(장비) 목적지", - true, new List { "014", "013", "012", "004", "003", "002", "001" }), - - new MovementBasedTestCase("004", "005", "019", AgvDirection.Forward, - "RFID 004→005 전진 이동(갈림길 진입) AGV가 019(충전기) 목적지", - true, new List { "005", "037", "036", "035", "034", "018", "019" }), - - new MovementBasedTestCase("005", "004", "037", AgvDirection.Backward, - "RFID 005→004 후진 이동(갈림길 이탈) AGV가 037 목적지", - true, new List { "004", "005", "037" }), - - // 🚨 AGV 물리적 제약사항 테스트: 갈림길 즉시 방향전환 금지 - new MovementBasedTestCase("018", "019", "015", AgvDirection.Forward, - "🚨 물리적 제약: RFID 018→019 전진 이동 AGV가 015(충전기) 목적지 - 012 갈림길 즉시 방향전환 금지", - true, new List { "019", "018", "017", "016", "012", "004", "012", "013", "014", "015" }), - - new MovementBasedTestCase("016", "012", "013", AgvDirection.Backward, - "🚨 물리적 제약: RFID 016→012 후진 이동 AGV가 013 목적지 - 012에서 즉시 013 전진 금지", - true, new List { "012", "004", "012", "013" }), - - // 🔥 핵심 시나리오: AGV 물리적 방향과 갈림길 방향전환 - new MovementBasedTestCase("032", "031", "008", AgvDirection.Backward, - "🔥 핵심 시나리오: RFID 032→031 후진 이동 AGV가 008 목적지 - 031→005(전진)→004(좌회전)→008(후진)", - true, new List { "031", "032", "033", "034", "035", "036", "037", "005", "004", "012", "006", "007", "008" }), - - // 방향 전환이 필요한 시나리오 - new MovementBasedTestCase("032", "033", "001", AgvDirection.Forward, - "RFID 032→033 전진 이동 AGV가 001(후진 도킹) 목적지 - 방향 전환 필요", - true, new List { "033", "034", "018", "017", "016", "037", "005", "004", "003", "002", "001" }), - - new MovementBasedTestCase("007", "006", "015", AgvDirection.Backward, - "RFID 007→006 후진 이동 AGV가 015(전진 도킹) 목적지 - 방향 전환 필요", - true, new List { "006", "005", "037", "005", "004", "012", "013", "014", "015" }), - }; - } - } -} \ No newline at end of file diff --git a/Cs_HMI/AGVPathTester/packages.config b/Cs_HMI/AGVPathTester/packages.config deleted file mode 100644 index 8b1a8d0..0000000 --- a/Cs_HMI/AGVPathTester/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Cs_HMI/PathLogic/App.config b/Cs_HMI/PathLogic/App.config deleted file mode 100644 index d8ef5cf..0000000 --- a/Cs_HMI/PathLogic/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Cs_HMI/PathLogic/Core/MapLoader.cs b/Cs_HMI/PathLogic/Core/MapLoader.cs deleted file mode 100644 index 7b6a0a2..0000000 --- a/Cs_HMI/PathLogic/Core/MapLoader.cs +++ /dev/null @@ -1,336 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; -using PathLogic.Models; - -namespace PathLogic.Core -{ - /// - /// 맵 파일 로더 클래스 - /// 기존 AGV 맵 에디터에서 생성한 JSON 파일을 로드 - /// - public class MapLoader - { - /// - /// 파일에서 맵 데이터 로드 - /// - /// 맵 파일 경로 - /// 로드된 맵 데이터 - public MapData LoadFromFile(string filePath) - { - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"맵 파일을 찾을 수 없습니다: {filePath}"); - } - - try - { - string jsonContent = File.ReadAllText(filePath); - return LoadFromJson(jsonContent); - } - catch (Exception ex) - { - throw new Exception($"맵 파일 로드 중 오류 발생: {ex.Message}", ex); - } - } - - /// - /// JSON 문자열에서 맵 데이터 로드 - /// - /// JSON 문자열 - /// 로드된 맵 데이터 - public MapData LoadFromJson(string jsonContent) - { - if (string.IsNullOrEmpty(jsonContent)) - { - throw new ArgumentException("JSON 내용이 비어있습니다."); - } - - try - { - // JSON 역직렬화 설정 - var settings = new JsonSerializerSettings - { - DateFormatHandling = DateFormatHandling.IsoDateFormat, - NullValueHandling = NullValueHandling.Ignore, - MissingMemberHandling = MissingMemberHandling.Ignore - }; - - // JSON 파일 구조 분석 - var jsonObject = JsonConvert.DeserializeObject(jsonContent, settings); - - var mapData = new MapData(); - - // 메타데이터 로드 - if (jsonObject.CreatedDate != null) - { - mapData.CreatedDate = jsonObject.CreatedDate; - } - - if (jsonObject.Version != null) - { - mapData.Version = jsonObject.Version; - } - - // 노드 데이터 로드 - if (jsonObject.Nodes != null) - { - foreach (var nodeJson in jsonObject.Nodes) - { - var node = LoadNodeFromJson(nodeJson); - if (node != null) - { - mapData.Nodes.Add(node); - } - } - } - - // 맵 데이터 유효성 검증 - var validationIssues = mapData.ValidateMap(); - if (validationIssues.Count > 0) - { - Console.WriteLine("맵 데이터 검증 경고:"); - foreach (var issue in validationIssues) - { - Console.WriteLine($" - {issue}"); - } - } - - Console.WriteLine($"맵 로드 완료: {mapData.GetStatistics()}"); - return mapData; - } - catch (JsonException ex) - { - throw new Exception($"JSON 파싱 오류: {ex.Message}", ex); - } - catch (Exception ex) - { - throw new Exception($"맵 데이터 로드 중 오류: {ex.Message}", ex); - } - } - - /// - /// JSON 객체에서 노드 데이터 로드 - /// - /// 노드 JSON 객체 - /// 로드된 노드 - private MapNode LoadNodeFromJson(dynamic nodeJson) - { - try - { - var node = new MapNode(); - - // 필수 필드 - node.NodeId = nodeJson.NodeId ?? string.Empty; - node.Name = nodeJson.Name ?? string.Empty; - - // 위치 정보 파싱 - if (nodeJson.Position != null) - { - var position = nodeJson.Position.ToString(); - node.Position = ParsePosition(position); - } - - // 노드 타입 - if (nodeJson.Type != null) - { - if (Enum.TryParse(nodeJson.Type.ToString(), out NodeType nodeType)) - { - node.Type = nodeType; - } - } - - // 도킹 방향 - if (nodeJson.DockDirection != null) - { - if (Enum.TryParse(nodeJson.DockDirection.ToString(), out DockingDirection dockDirection)) - { - node.DockDirection = dockDirection; - } - } - - // 연결된 노드들 - if (nodeJson.ConnectedNodes != null) - { - foreach (var connectedNodeId in nodeJson.ConnectedNodes) - { - if (connectedNodeId != null) - { - node.ConnectedNodes.Add(connectedNodeId.ToString()); - } - } - } - - // 기타 속성들 - if (nodeJson.CanRotate != null) - node.CanRotate = nodeJson.CanRotate; - - if (nodeJson.StationId != null) - node.StationId = nodeJson.StationId; - - if (nodeJson.StationType != null && Enum.TryParse(nodeJson.StationType.ToString(), out StationType stationType)) - node.StationType = stationType; - - if (nodeJson.CreatedDate != null) - node.CreatedDate = nodeJson.CreatedDate; - - if (nodeJson.ModifiedDate != null) - node.ModifiedDate = nodeJson.ModifiedDate; - - if (nodeJson.IsActive != null) - node.IsActive = nodeJson.IsActive; - - // RFID 정보 - if (nodeJson.RfidId != null) - node.RfidId = nodeJson.RfidId; - - if (nodeJson.RfidStatus != null) - node.RfidStatus = nodeJson.RfidStatus; - - if (nodeJson.RfidDescription != null) - node.RfidDescription = nodeJson.RfidDescription; - - // UI 관련 속성들 (맵 에디터 호환성을 위해) - LoadUIProperties(node, nodeJson); - - // 기본 색상 설정 - node.SetDefaultColorByType(node.Type); - - return node; - } - catch (Exception ex) - { - Console.WriteLine($"노드 로드 오류: {ex.Message}"); - return null; - } - } - - /// - /// UI 관련 속성 로드 - /// - /// 대상 노드 - /// 노드 JSON 객체 - private void LoadUIProperties(MapNode node, dynamic nodeJson) - { - try - { - if (nodeJson.LabelText != null) - node.LabelText = nodeJson.LabelText; - - if (nodeJson.FontFamily != null) - node.FontFamily = nodeJson.FontFamily; - - if (nodeJson.FontSize != null) - node.FontSize = (float)nodeJson.FontSize; - - if (nodeJson.ImagePath != null) - node.ImagePath = nodeJson.ImagePath; - - if (nodeJson.Opacity != null) - node.Opacity = (float)nodeJson.Opacity; - - if (nodeJson.Rotation != null) - node.Rotation = (float)nodeJson.Rotation; - - if (nodeJson.ShowBackground != null) - node.ShowBackground = nodeJson.ShowBackground; - - // Scale 파싱 - if (nodeJson.Scale != null) - { - var scale = nodeJson.Scale.ToString(); - node.Scale = ParseScale(scale); - } - } - catch (Exception ex) - { - Console.WriteLine($"UI 속성 로드 오류: {ex.Message}"); - } - } - - /// - /// 위치 문자열을 Point로 파싱 - /// 예: "65, 229" -> Point(65, 229) - /// - /// 위치 문자열 - /// 파싱된 Point - private System.Drawing.Point ParsePosition(string positionString) - { - try - { - if (string.IsNullOrEmpty(positionString)) - return System.Drawing.Point.Empty; - - var parts = positionString.Split(','); - if (parts.Length >= 2) - { - int x = int.Parse(parts[0].Trim()); - int y = int.Parse(parts[1].Trim()); - return new System.Drawing.Point(x, y); - } - } - catch (Exception ex) - { - Console.WriteLine($"위치 파싱 오류: {positionString}, {ex.Message}"); - } - - return System.Drawing.Point.Empty; - } - - /// - /// 스케일 문자열을 SizeF로 파싱 - /// 예: "1, 1" -> SizeF(1.0f, 1.0f) - /// - /// 스케일 문자열 - /// 파싱된 SizeF - private System.Drawing.SizeF ParseScale(string scaleString) - { - try - { - if (string.IsNullOrEmpty(scaleString)) - return new System.Drawing.SizeF(1.0f, 1.0f); - - var parts = scaleString.Split(','); - if (parts.Length >= 2) - { - float width = float.Parse(parts[0].Trim()); - float height = float.Parse(parts[1].Trim()); - return new System.Drawing.SizeF(width, height); - } - } - catch (Exception ex) - { - Console.WriteLine($"스케일 파싱 오류: {scaleString}, {ex.Message}"); - } - - return new System.Drawing.SizeF(1.0f, 1.0f); - } - - /// - /// 맵 데이터를 JSON 파일로 저장 - /// - /// 저장할 맵 데이터 - /// 저장할 파일 경로 - public void SaveToFile(MapData mapData, string filePath) - { - try - { - var settings = new JsonSerializerSettings - { - DateFormatHandling = DateFormatHandling.IsoDateFormat, - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented - }; - - string jsonContent = JsonConvert.SerializeObject(mapData, settings); - File.WriteAllText(filePath, jsonContent); - - Console.WriteLine($"맵 데이터 저장 완료: {filePath}"); - } - catch (Exception ex) - { - throw new Exception($"맵 파일 저장 중 오류: {ex.Message}", ex); - } - } - } -} \ No newline at end of file diff --git a/Cs_HMI/PathLogic/Description.txt b/Cs_HMI/PathLogic/Description.txt deleted file mode 100644 index af7f47e..0000000 --- a/Cs_HMI/PathLogic/Description.txt +++ /dev/null @@ -1,30 +0,0 @@ -맵파일위치 : C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap - -AGV 하드웨어 구조 -마그넷라인을따라가는 라인트레이서 구조 -모터는 전/후진만 가능함 -리프트 - 몸체 - 모니터 형태이 가로로 긴 직사각형 형태 -제자리에서 회전하는 기능은 없음 -직진이동하면서 마그넷의 가중치를 이용하여 좌/우 갈림길을 선택할 수 있음. (좌우는 각 magent left , right 로 표현한다) -모니터방향(모터전진방향으로 이동하는 방향) 기준으로 magnet left, right를 표현한다. -직진이동시 갈림길 선택이 가능하므로 갈림길에서 모터방향만 바꿔서 다른 길로 갈수는 없음 , 충분한 이동거리가 있어야 그 갈림길을 이용할 수 있음 -경로예측이 주 기능이나 경로 예측이 쉽지않음. AGV방향과 도킹조건이 서로 맞지 않으면 단순 경로 예측이 아니고. 우회경로 예측이 필요함. -맵파일에는 nodeid (고유값) 과 rfid id값이있는데... 실제 사용자와 대화하는건 RFID값이므로 내부적으로 nodeid 를 쓰더라도 ui상에는 rfid 를 가지고 설명을 해야함 -예를들면 버퍼의 경우 리프트가 도킹이되어야함(후면도킹) 그런데 AGV의 이동방향이 서로 맞지 않으면 바로 버퍼로 가지못하고 갈림길에서 방향회전을 하고 들어와야함. -맵파일의 형태를 보면 각 RFID노드가 어느위치에 있고 노드연결정보를 통해서 갈림길도 확인할 수 있음 -노드연결정보는 node a -> node b 식으로 연결되어있는데. 이는 표현은단방향이지만 실제로는 양방향을 의미함, 그냥 ui 편하게 처리하려고 저장은 한쪽만 저장하고 있는것임. -AGV의 이전 이동정보를 알고 있어야 방향을 확인하고 결정하는것이 가능함, 즉 이전이동정보와 현재 위치 정보가 없으면 경로계산은 할 수없다. -모든 번호 000스타일은 RFID 값이므로 노드id와 혼동하지 않기를 바람 -AGV가 모터를 후진상태로 두고 002 -> 003 위치로 이동했다면 마지막위치는 003이고 모니터는 002쪽을 바라보고 있고 리프트는 004쪽을 바라보고 있게된다. -003노드는 맵파일을 확인하면 002 와 004에 연결되어있기때문이다. 갈림길은 없고 외길로 연결되어있다. 004는 총 4개의 인접 노드가 있는 갈림길이다. -갈림길은 인접노드가 3개이상으 되야 방향전환으로 사용가능하다. -002 -> 003 으로 후진모터상태로 이동하는 AGV가 001(언로더)에 도킹을 해야하는 경로계산을 한다고 가정하자. 이 상황에서 모터를 전진으로 진행하면 003에서 002 001 로 이동하겠지만. 언로더는 리프트와 도킹을 해야한다. 즉 리프트가 001방향에 있어야하는데. 이러면 맞지가 않는다. 이런경우 방향전환을 해야한다. -물론 agv 002 -> 003 으로 이동할때 모터를 전진방향으로 이동하고 있었다면 리프트는 002방향에 모니터는 003 004 방향을 바라보고 있었을테니 그래도 모터를 전진이동해서 001까지 이동하면 도킹에 문제가 없다. -자 그럼 방향전환이 필요한 aGV의 002 -> 003을 후진모터로 이동하는경우이다 ,, 몸체를 회전을 시켜야하는데 회전 기능이 없으니 가장 가까운 갈림길을 찾아서 그쪽으로 이동을 하자, 물론 그전에 방향을 고려하지 않고 현재위치와 목적지까지의 최단거리 노선목록을 계산해야한다. 이 경우 003 -> 002 -> 001 이 될것이다. 이것을 기본 경로로 방향 전환 등을 판단하여 경로를 고도화하는 것이다. -우선 003 -> 001로 방향전환업이 바로 가면 충돌하니 이 경로기준에서 목적지 전까지 갈림길이 있다면 그것을 사용한다. 이경우에는 없다 003 -> 001까지는 외길이다. 그러면 가장 가까운 갈림일을 찾자. 맵 데이터상으로는 004가 된다. -그럼 004까지는 그대로 후진모터로 이동하는데 이때 012 혹은 030 으로 방향을 틀어서 이동을 해야하는데. 이럴경우 갈림길의 직진선을 보고 확인하자. 1순위로 이동할것은 직진(straight)다 004의 경우 내가 온 방향 003을 제외하면 인접노드가 012 005 030 총 3개가 있다. 즉 방향전환에 총 3개를 쓸수있는데. 003 -> 004이동방향으로 보면 005는 직진방향 , 012는 magnet right 방향, 030은 magnet left 방향이다. -이렇게나온다면 우선순위는 1. 직진, 2. 왼쪽, 3오른쪽 이므로 005로 이동한다. -magent 을 straight 모드로 003 에서 004로 (후진모터) 진입을 하고 005까지 이동한다. 이제 방향전환을 위해서 모터를 전진으로 바꾸고 갈림길 004에 진입해야 한다. 방향전환을 위해 갈림길을 벗어난 노드 이동시에는 반드시 내가 들어온 노드는 제외해야한다. -005에서004는 이제 전진으로 이동하고 003은 내가 왔던 경로상의 길이니 배제하고 (최종 방향전환해서 가야할길) 005는 현재 지나가는 길이니 빼고 , 030과 012가 남았는데. 우선순위 1,2,3을 보면 1번은 내가 가야할 경로상 길이니 불가하고 2left 는 내경로가 아닌 인접노드이므로 magent left 모드로 004를 전진으로 진입한다 그러면 030으로 이동이 된다. -이제 이 상황에서 후진모터를 켜고 004 -> 003 방향으로 이동을 해야하므로, Magnet right 모드로 후진이동을 한다. 그러면 결국 후진상태로 004 003으로 이동하게 하므로 -최종 목적지 001까지 이동했을때 리프트가 장비에 도킹될수 있는 구조이다. diff --git a/Cs_HMI/PathLogic/INSTRUCTION.md b/Cs_HMI/PathLogic/INSTRUCTION.md deleted file mode 100644 index 45e7e92..0000000 --- a/Cs_HMI/PathLogic/INSTRUCTION.md +++ /dev/null @@ -1,636 +0,0 @@ -# AGV 경로 계산 알고리즘 설계 문서 - -## 1. 시스템 개요 - -### 1.1 AGV 하드웨어 특성 -- **구조**: 리프트 - 몸체 - 모니터 (가로 직사각형) -- **이동**: 전진/후진만 가능, 제자리 회전 불가 -- **제어**: 마그넷 라인 트레이서 (magnet left/right/straight) -- **도킹**: 전진 도킹(충전기), 후진 도킹(버퍼, 로더 등) - -### 1.2 핵심 제약사항 -1. **제자리 회전 불가**: 방향 전환을 위해서는 갈림길 이용 필수 -2. **갈림길 조건**: 인접 노드 3개 이상 필요 -3. **도킹 조건**: AGV 방향과 목적지 도킹 방향이 일치해야 함 -4. **상태 의존성**: 이전 이동 정보 없이는 현재 방향 판단 불가 -5. **인접노드 경유 필수**: 갈림길에서 방향전환시 반드시 인접노드를 경유해야 함 - -## 2. 경로 계산 알고리즘 아키텍처 - -### 2.1 다단계 경로 계산 방식 - -``` -1단계: 기본 최단 경로 계산 - ↓ -2단계: 방향 호환성 검증 - ↓ -3단계: 방향 전환 경로 생성 (필요시) - ↓ -4단계: 최종 경로 최적화 -``` - -### 2.2 핵심 클래스 구조 - -``` -AGVPathCalculator (메인 경로 계산기) -├── BasicPathFinder (기본 최단 경로) -├── DirectionAnalyzer (방향 분석 및 호환성 검증) -├── TurnAroundPlanner (방향 전환 경로 계획) -└── PathOptimizer (경로 최적화) -``` - -## 3. 세부 알고리즘 설계 - -### 3.1 AGV 상태 모델 - -```csharp -public class AgvState -{ - public string CurrentNodeId { get; set; } // 현재 위치 (RFID) - public string PreviousNodeId { get; set; } // 이전 위치 (방향 판단용) - public AgvDirection MotorDirection { get; set; } // 현재 모터 방향 - public AgvDirection LiftDirection { get; set; } // 리프트가 향하는 방향 - public AgvDirection MonitorDirection { get; set; } // 모니터가 향하는 방향 -} -``` - -### 3.2 방향 판단 알고리즘 - -#### 3.2.1 현재 AGV 방향 계산 -``` -IF 이전노드 → 현재노드 이동 시 모터방향이 전진: - 리프트 방향 = 이전노드 방향 - 모니터 방향 = 현재노드에서 이전노드 반대 방향 - -IF 이전노드 → 현재노드 이동 시 모터방향이 후진: - 리프트 방향 = 현재노드에서 이전노드 반대 방향 - 모니터 방향 = 이전노드 방향 -``` - -#### 3.2.2 도킹 호환성 검증 -``` -FOR 목적지 노드: - IF 목적지.도킹타입 == 전진도킹 (충전기): - RETURN 모니터방향 == 목적지방향 - - IF 목적지.도킹타입 == 후진도킹 (버퍼, 로더): - RETURN 리프트방향 == 목적지방향 -``` - -### 3.3 기본 경로 계산 (1단계) - -#### 3.3.1 단순 최단 경로 알고리즘 -- **알고리즘**: Dijkstra 또는 BFS (AGV 특성상 가중치가 단순) -- **목적**: 방향 고려 없이 순수한 최단 거리 경로 -- **출력**: 노드 시퀀스 [시작 → ... → 목적지] - -``` -기본경로 = BFS_최단경로(시작노드, 목적지노드) -예시: 003 → 002 → 001 (언로더) -``` - -### 3.4 방향 호환성 검증 (2단계) - -#### 3.4.1 경로상 방향 시뮬레이션 -``` -FOR 각 경로 단계: - 현재_AGV_방향 = 시뮬레이션_이동(이전노드, 현재노드, 모터방향) - - IF 현재노드 == 목적지: - 도킹_가능 = 검증_도킹_호환성(현재_AGV_방향, 목적지_도킹타입) - -IF 도킹_가능: - RETURN 기본경로 (방향 전환 불필요) -ELSE: - PROCEED TO 3단계 (방향 전환 필요) -``` - -### 3.5 방향 전환 경로 계획 (3단계) - -#### 3.5.1 갈림길 탐색 알고리즘 -``` -갈림길_후보 = [] - -FOR 기본경로상의 각 노드: - IF 노드.인접노드수 >= 3: - 갈림길_후보.ADD(노드) - -IF 갈림길_후보 == 빈목록: - // 기본경로에 갈림길 없음 → 외부 갈림길 탐색 - 가장_가까운_갈림길 = BFS_갈림길_탐색(현재위치) - 갈림길_후보.ADD(가장_가까운_갈림길) -``` - -#### 3.5.2 방향 전환 시퀀스 계획 -``` -선택된_갈림길 = 갈림길_후보[0] // 가장 가까운 갈림길 - -// Phase 1: 갈림길까지 이동 (현재 모터 방향 유지) -Phase1_경로 = 현재위치 → 선택된_갈림길 - -// Phase 2: 갈림길에서 우회 (인접 노드 경유 필수) -우회_노드 = 계산_우회_방향(선택된_갈림길, 현재_진입_방향) -Phase2_경로 = 선택된_갈림길 → 우회_노드 -// 중요: AGV는 갈림길에서 직접 되돌아갈 수 없으므로 반드시 인접노드 방문 - -// Phase 3: 모터 방향 전환 + 갈림길 재진입 -모터방향 = 반전(현재_모터방향) -Phase3_경로 = 우회_노드 → 선택된_갈림길 -// 우회노드에서 모터방향 전환 후 갈림길로 복귀 - -// Phase 4: 목적지까지 이동 (전환된 방향으로) -Phase4_경로 = 선택된_갈림길 → 목적지 -``` - -#### 3.5.3 갈림길 방향 선택 우선순위 -``` -FOR 갈림길의 각 인접노드 (진입방향 제외): - 방향타입 = 계산_마그넷_방향(진입방향, 인접노드방향) - - 우선순위: - 1. Straight (직진) - 2. Left (왼쪽) - 3. Right (오른쪽) - -선택된_방향 = 우선순위가_가장_높은_방향 -``` - -### 3.6 최종 경로 최적화 (4단계) - -#### 3.6.1 경로 검증 -``` -FOR 최종경로의 각 단계: - 1. 연결성 검증 (모든 인접 노드가 실제 연결되어 있는가) - 2. 마그넷 방향 일관성 검증 - 3. 도킹 방향 최종 검증 -``` - -#### 3.6.2 경로 최적화 -``` -IF 여러 갈림길 옵션 존재: - 각_옵션별_총거리 = 계산_총_이동거리(옵션) - 최적_경로 = MIN(각_옵션별_총거리) -``` - -## 4. 데이터 구조 설계 - -### 4.1 입력 데이터 -```csharp -public class PathRequest -{ - public AgvState CurrentState { get; set; } // 현재 AGV 상태 - public string TargetRfidId { get; set; } // 목적지 RFID - public MapData MapData { get; set; } // 맵 데이터 -} -``` - -### 4.2 출력 데이터 -```csharp -public class PathResult -{ - public bool Success { get; set; } - public List Steps { get; set; } // 단계별 상세 경로 - public string ErrorMessage { get; set; } - public PathMetrics Metrics { get; set; } // 성능 지표 -} - -public class PathStep -{ - public string RfidId { get; set; } // UI 표시용 RFID - public string NodeId { get; set; } // 내부 처리용 NodeID - public AgvDirection MotorDirection { get; set; } - public MagnetDirection MagnetDirection { get; set; } - public string Action { get; set; } // "이동", "방향전환", "도킹" 등 - public string Description { get; set; } // 사용자 친화적 설명 -} -``` - -## 5. 예외 상황 처리 - -### 5.1 불가능한 경로 -- **갈림길 없음**: 방향 전환이 불가능한 맵 구조 -- **고립된 노드**: 연결이 끊어진 노드 -- **순환 참조**: 무한 루프 방지 - -### 5.2 복구 전략 -``` -IF 방향전환_불가능: - RETURN 오류 "목적지 도킹 불가능 - 갈림길 부족" - -IF 경로_없음: - RETURN 오류 "목적지 접근 불가능 - 맵 연결 확인 필요" - -IF 계산시간_초과: - RETURN 오류 "경로 계산 시간 초과 - 맵 복잡도 점검 필요" -``` - -## 6. 성능 최적화 전략 - -### 6.1 캐싱 전략 -- **맵 구조 캐싱**: 갈림길 노드, 인접 노드 정보 -- **거리 매트릭스**: 자주 사용되는 노드 간 거리 -- **방향 전환 패턴**: 성공한 방향 전환 시퀀스 - -### 6.2 알고리즘 최적화 -- **조기 종료**: 방향 호환 시 3-4단계 건너뛰기 -- **휴리스틱**: 갈림길 선택 시 목적지 방향 고려 -- **병렬 처리**: 여러 갈림길 옵션 동시 계산 - -## 7. 테스트 시나리오 - -### 7.1 기본 시나리오 -1. **직선 경로**: 003 → 002 → 001 (방향 호환) -2. **방향 전환**: 003(후진) → 001(언로더) 도킹 -3. **복잡한 갈림길**: 여러 갈림길이 있는 경우 - -### 7.3 실제 맵 검증 시나리오 (NewMap.agvmap 기반) -**시나리오**: AGV가 034→033 (전진) 상태에서 040(BUF2, 후진도킹) 목적지 -- **기본경로**: 033 → 032 → 040 -- **방향호환성**: 전진 ≠ 후진 → 방향전환 필요 -- **갈림길**: 032 (3개 연결: 031, 033, 040) -- **4단계 시퀀스**: - 1. 033 → 032 (전진, magnet right) - 2. 032 → 031 (전진, magnet left) - 인접노드 경유 - 3. 031 → 032 (후진, magnet right) - 모터방향 전환 - 4. 032 → 040 (후진, magnet straight) - 도킹성공 - -**검증포인트**: -- 032는 Position(148,545)를 중심으로 031(서쪽), 033(동쪽), 040(남쪽) 연결 -- 마그넷 방향 계산시 실제 좌표 기반 방향 확인 필수 -- 040에서 032로의 복귀는 인접노드(031) 경유 없이 불가능 - -### 7.2 경계 조건 -1. **동일 위치**: 시작 = 목적지 -2. **인접 노드**: 한 번만 이동하면 되는 경우 -3. **최대 거리**: 맵에서 가장 먼 두 지점 - -## 8. 구현 우선순위 - -### Phase 1: 기본 기능 -1. AgvState 및 PathStep 데이터 모델 -2. 기본 최단 경로 계산 -3. 방향 판단 로직 - -### Phase 2: 고급 기능 -1. 방향 호환성 검증 -2. 갈림길 탐색 및 방향 전환 -3. 최종 경로 최적화 - -### Phase 3: 최적화 -1. 성능 최적화 및 캐싱 -2. 예외 처리 강화 -3. 테스트 케이스 완성 - -## 9. 핵심 알고리즘 의사코드 - -### 9.1 메인 경로 계산 함수 -``` -FUNCTION CalculatePath(currentState, targetRfidId, mapData): - // 1단계: 기본 최단 경로 - basicPath = FindShortestPath(currentState.CurrentNodeId, targetRfidId) - - // 2단계: 방향 호환성 검증 - IF IsDirectionCompatible(currentState, basicPath, targetRfidId): - RETURN CreateSuccessResult(basicPath) - - // 3단계: 방향 전환 필요 - turnAroundPath = PlanTurnAround(currentState, basicPath, targetRfidId) - - // 4단계: 최적화 - optimizedPath = OptimizePath(turnAroundPath) - - RETURN CreateSuccessResult(optimizedPath) -``` - -### 9.2 방향 전환 계획 함수 -``` -FUNCTION PlanTurnAround(currentState, basicPath, targetRfidId): - // 갈림길 찾기 - junctions = FindJunctionsInPath(basicPath) - IF junctions.IsEmpty(): - junctions = FindNearestJunctions(currentState.CurrentNodeId) - - bestJunction = SelectBestJunction(junctions, targetRfidId) - - // 4단계 경로 생성 (인접노드 경유 필수) - phase1 = PathToJunction(currentState, bestJunction) - phase2 = DetourToAdjacentNode(bestJunction, currentState.MotorDirection) - phase3 = ReturnToJunctionWithReversedMotor(phase2.EndNode, bestJunction) - phase4 = PathFromJunctionToTarget(bestJunction, targetRfidId, !currentState.MotorDirection) - - RETURN CombinePaths(phase1, phase2, phase3, phase4) -``` - -### 9.3 마그넷 방향 계산 함수 -``` -FUNCTION CalculateMagnetDirection(fromNodeId, toNodeId, junctionNodeId): - fromPos = GetNodePosition(fromNodeId) - toPos = GetNodePosition(toNodeId) - junctionPos = GetNodePosition(junctionNodeId) - - // 갈림길 진입 방향 벡터 - entryVector = Normalize(junctionPos - fromPos) - - // 갈림길 진출 방향 벡터 - exitVector = Normalize(toPos - junctionPos) - - // 외적을 이용한 방향 판단 - crossProduct = CrossProduct(entryVector, exitVector) - dotProduct = DotProduct(entryVector, exitVector) - - IF Abs(dotProduct) > 0.8: // 직진 허용 오차 - RETURN MagnetDirection.Straight - ELSE IF crossProduct > 0: - RETURN MagnetDirection.Left - ELSE: - RETURN MagnetDirection.Right -``` - -## 10. 핵심 설계 원칙 요약 - -### 10.1 AGV 하드웨어 제약사항 핵심 -1. **방향전환 불가**: 제자리 회전 기능 없음 → 갈림길 필수 활용 -2. **인접노드 경유**: 갈림길에서 직접 복귀 불가 → 반드시 인접노드 방문 -3. **양방향 연결**: 맵 연결 정보는 단방향 저장, 실제는 양방향 해석 -4. **좌표 기반 마그넷**: 노드 Position 좌표로 실제 마그넷 방향 계산 - -### 10.2 알고리즘 핵심 검증점 -1. **갈림길 조건**: 인접노드 3개 이상 (진입방향 제외시 2개 선택지) -2. **방향 호환성**: 현재 AGV 방향 ≠ 목적지 도킹 방향 → 4단계 전환 -3. **마그넷 정확성**: 벡터 외적/내적 기반 좌/우/직진 판단 -4. **경로 유효성**: 모든 연결이 실제 맵에서 존재하는지 검증 - -### 10.3 실제 구현시 주의사항 -- **RFID vs NodeID**: 사용자 인터페이스는 RFID, 내부 로직은 NodeID -- **좌표계 정확성**: Position 문자열 파싱시 정수 변환 검증 -- **양방향 탐색**: ConnectedNodes 뿐만 아니라 역방향 연결도 고려 -- **에러 처리**: 갈림길 부족, 경로 없음, 순환참조 등 예외 상황 - -## 11. NewMap.agvmap 갈림길 데이터 (참조용) - -### 11.1 확인된 갈림길 노드 목록 - -**주요 갈림길 (3개 이상 연결)**: - -| RFID | NodeId | Position | 연결 노드 | 연결 RFID | 용도 | -|------|--------|----------|-----------|-----------|------| -| 004 | N004 | 380,340 | N003, N022, N031 | 003, 012, 030 | 서쪽 주요 갈림길 | -| 005 | N011 | 460,420 | N012, N004, N015 | 006, 004, 037 | 중앙 주요 갈림길 | -| 012 | N022 | 459,279 | N004, N023, N006 | 004, 016, 013 | 중앙-북쪽 갈림길 | -| 032 | N020 | 148,545 | N021, N005, N028 | 031, 033, 040 | 남쪽 갈림길 | - -### 11.2 갈림길별 방향전환 전략 - -**RFID 005 (중앙 갈림길)**: -- **동쪽에서 진입** (006 → 005): 서쪽(004) 또는 남쪽(037)으로 우회 가능 -- **서쪽에서 진입** (004 → 005): 동쪽(006) 또는 남쪽(037)으로 우회 가능 -- **남쪽에서 진입** (037 → 005): 동쪽(006) 또는 서쪽(004)으로 우회 가능 - -**RFID 004 (서쪽 갈림길)**: -- **동쪽에서 진입** (005 → 004): 서쪽(003) 또는 남쪽(030)으로 우회 가능 -- **서쪽에서 진입** (003 → 004): 동쪽(005) 또는 남쪽(030)으로 우회 가능 -- **남쪽에서 진입** (030 → 004): 동쪽(005) 또는 서쪽(003)으로 우회 가능 - -**RFID 012 (중앙-북쪽 갈림길)**: -- **서쪽에서 진입** (004 → 012): 동쪽(013) 또는 북쪽(016)으로 우회 가능 -- **동쪽에서 진입** (013 → 012): 서쪽(004) 또는 북쪽(016)으로 우회 가능 -- **북쪽에서 진입** (016 → 012): 서쪽(004) 또는 동쪽(013)으로 우회 가능 - -**RFID 032 (남쪽 갈림길)**: -- **동쪽에서 진입** (033 → 032): 서쪽(031) 또는 남쪽(040)으로 우회 가능 -- **서쪽에서 진입** (031 → 032): 동쪽(033) 또는 남쪽(040)으로 우회 가능 -- **남쪽에서 진입** (040 → 032): 동쪽(033) 또는 서쪽(031)으로 우회 가능 - -### 11.3 경로 계산시 갈림길 활용 지침 - -1. **갈림길 우선순위**: 목적지와 가까운 갈림길 우선 선택 -2. **우회 방향 선택**: 막다른 길 < 2개 연결 < 3개 이상 연결 순서로 우선순위 -3. **마그넷 방향 계산**: 실제 좌표 기반 벡터 계산으로 정확한 left/right/straight 판단 -4. **방향전환 검증**: 갈림길에서 최소 1회 인접노드 경유 후 모터방향 전환 - -### 11.4 실제 활용 예시 - -**007→006 전진모터 → 011(TOPS) 경로**: -- **기본경로**: 006 → 005 → 004 → 030 → 009 → 010 → 011 -- **방향전환**: 005 갈림길 활용 (006 → 005 → 037 → 005 → 004...) -- **결과**: 후진 도킹으로 TOPS 접근 성공 - -**034→033 전진모터 → 041(BUF1) 경로** ✅ **검증됨**: -- **기본경로**: 033 → 032 → 031 → 041 -- **방향호환성**: 전진 ≠ 후진 → 방향전환 필요 -- **4단계 시퀀스**: - 1. 033 → 032 (전진, left) - 2. 032 → 040 (전진, straight) - 인접노드 경유 - 3. 040 → 032 (후진, straight) - 모터방향 전환 - 4. 032 → 031 → 041 (후진, right → straight) - 도킹성공 - -## 12. 구현을 위한 핵심 알고리즘 요약 - -### 12.1 필수 구현 클래스 - -```csharp -// 1. 메인 경로 계산기 -public class AGVPathCalculator -{ - public PathResult CalculatePath(AgvState currentState, string targetRfidId, MapData mapData) - - // 핵심 의존성 클래스들 - private BasicPathFinder _basicPathFinder; - private DirectionAnalyzer _directionAnalyzer; - private TurnAroundPlanner _turnAroundPlanner; - private JunctionFinder _junctionFinder; -} - -// 2. 갈림길 탐색기 (중요!) -public class JunctionFinder -{ - public List FindJunctionsInPath(List path, MapData mapData) - public List FindNearestJunctions(string nodeId, MapData mapData) - public bool IsJunction(string nodeId, MapData mapData) // 3개 이상 연결 확인 -} - -// 3. 방향 전환 계획기 -public class TurnAroundPlanner -{ - public PathResult PlanTurnAroundPath(AgvState state, List basicPath, string targetId) - public string SelectBestDetourNode(string junctionId, string entryDirection, MapData mapData) -} - -// 4. 마그넷 방향 계산기 -public class MagnetDirectionCalculator -{ - public MagnetDirection CalculateDirection(string fromNodeId, string toNodeId, string junctionId, MapData mapData) - // 벡터 외적/내적 기반 left/right/straight 판단 -} -``` - -### 12.2 핵심 알고리즘 단계별 체크리스트 - -**1단계: 기본 최단 경로** -- [ ] BFS/Dijkstra로 최단 경로 계산 -- [ ] 양방향 연결 해석 (ConnectedNodes + 역방향 탐색) -- [ ] RFID ↔ NodeID 매핑 처리 - -**2단계: 방향 호환성 검증** -- [ ] 현재 AGV 방향 계산 (이전노드 → 현재노드 + 모터방향) -- [ ] 목적지 도킹 방향 요구사항 확인 (Type + DockDirection) -- [ ] 최종 도착 시 AGV 방향 시뮬레이션 - -**3단계: 방향 전환 (필요시)** -- [ ] 경로상 갈림길 탐색 (3개 이상 연결 노드) -- [ ] 가장 가까운 갈림길 선택 -- [ ] 4단계 시퀀스 생성: - - Phase1: 갈림길까지 이동 - - Phase2: 인접노드 경유 (필수!) - - Phase3: 모터방향 전환 후 갈림길 복귀 - - Phase4: 목적지까지 이동 - -**4단계: 최적화 및 검증** -- [ ] 마그넷 방향 정확성 검증 (좌표 기반 벡터 계산) -- [ ] 연결성 검증 (모든 단계가 실제 연결되어 있는지) -- [ ] 최종 도킹 방향 재검증 - -### 12.3 검증된 테스트 케이스 - -1. **034→033→041**: 방향전환 성공 (032 갈림길, 040 인접노드) ✅ -2. **034→033→040**: 이전 분석에서 방향전환 성공 ✅ -3. **007→006→019**: 직선 경로, 방향호환 ✅ -4. **007→006→001**: 방향전환 필요 (004 갈림길) ✅ -5. **007→006→011**: 방향전환 성공 (005 갈림길) ✅ -6. **031→032→001**: 방향전환 필요 (004 갈림길, 030 인접노드) ✅ -7. **014→015→019**: 방향전환 필요 (012 갈림길, 004 인접노드) ✅ -8. **018→019→015**: 직선 경로, 방향호환 (막다른 길 시작) ✅ - -### 12.4 구현 우선순위 - -**Phase 1 (핵심 기능)**: -1. MapData, AgvState, PathResult 모델 구현 -2. BasicPathFinder (BFS 기반) -3. JunctionFinder (갈림길 탐색) -4. DirectionAnalyzer (방향 호환성) - -**Phase 2 (고급 기능)**: -1. TurnAroundPlanner (4단계 방향전환) -2. MagnetDirectionCalculator (벡터 기반) -3. PathOptimizer (경로 최적화) - -**Phase 3 (완성)**: -1. 예외 처리 강화 -2. 성능 최적화 -3. 테스트 케이스 완성 - -## 13. 중요한 구현 주의사항 및 함정 - -### 13.1 AGV 방향성 함정 (매우 중요!) - -**함정 1: 막다른 길에서의 방향 전환 불가** -- ❌ 잘못된 분석: "010에서 009로 후진 → 009에서 010으로 후진" -- ✅ 올바른 이해: 010(막다른 길)에서는 들어온 방향으로만 되돌아갈 수 있음 -- **교훈**: 갈림길에서만 방향전환 가능, 막다른 길/편도에서는 불가능 - -**함정 2: 입구 방향과 출구 방향 혼동** -- ❌ 잘못된 분석: "015→014 전진 후 014→015 전진" -- ✅ 올바른 이해: 014→015 전진으로 왔으면 015→014는 후진만 가능 -- **교훈**: AGV는 들어온 방향의 반대로만 되돌아갈 수 있음 - -**함정 3: 가짜 갈림길 식별** -- ❌ 잘못된 분석: "015-014-013-012는 갈림길" -- ✅ 올바른 이해: 이것은 편도 1차선, 진짜 갈림길은 3개 이상 연결 -- **교훈**: 연결 개수와 실제 구조를 정확히 파악해야 함 - -### 13.2 경로 계산 알고리즘 핵심 원칙 - -**원칙 1: 막다른 길 우선순위** -- 막다른 길에서 시작 → 방향 자동 결정 → 가장 간단한 케이스 -- 예: 018→019→015 (후진→전진, 방향호환) - -**원칙 2: 가장 가까운 갈림길 활용** -- 경로상 갈림길 없음 → 더 먼 갈림길 탐색 -- 예: 014→015→019 (012 갈림길 활용, 004까지 갈 필요 없음) - -**원칙 3: 인접노드 경유 필수** -- 갈림길에서 직접 되돌아가기 불가능 -- 반드시 인접노드 방문 후 모터방향 전환 - -### 13.3 디버깅 체크리스트 - -**방향 계산 검증**: -- [ ] 현재 AGV 방향 정확히 계산 (이전→현재 + 모터방향) -- [ ] 목적지 도킹 방향 요구사항 확인 -- [ ] 최종 도착 시 AGV 방향 시뮬레이션 - -**갈림길 검증**: -- [ ] 3개 이상 연결 확인 (편도 vs 진짜 갈림길) -- [ ] 양방향 연결 해석 (ConnectedNodes + 역방향) -- [ ] 인접노드 경유 가능성 확인 - -**경로 유효성 검증**: -- [ ] 모든 연결이 실제 존재하는지 확인 -- [ ] 마그넷 방향 계산 정확성 -- [ ] 막다른 길에서 방향전환 시도하지 않는지 - -### 13.4 성공 패턴 - -1. **직선 경로 (방향호환)**: 034→033→040, 018→019→015 -2. **단일 갈림길 방향전환**: 034→033→041 (032 갈림길) -3. **원거리 갈림길 활용**: 014→015→019 (012 갈림길) -4. **복합 경로**: 031→032→001 (004 갈림길, 030 인접노드) - -이 설계 문서를 바탕으로 단계별로 구현을 진행하면, AGV의 복잡한 방향 전환 요구사항을 체계적으로 해결할 수 있습니다. - -### 테스트 예제 (각 목표별 답안계산 : 경유 노드의 RFID도 모두 포함) - -Q1-1.033->032(전진) : 목표 040,041,008,001,011,019,015 - 040 : 032 ->(F) 031 ->(R) 032 -> 040 - 041 : 032 ->(F) 040 ->(R) 032 -> 031 -> 041 - 008 : 032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 -> 007 -> 008 - 001 : 032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 -> 002 -> 001 - 011 : 032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011 - 019 : 032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019 - 015 : 032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 ->(F) -> 012 -> 013 -> 014 -> 015 - -Q1-2.033->032(후진) : 목표 040,041,008,001,011,019,015 - 040 : 032 ->(F) 033 ->(R) 032 -> 040 - 041 : 032 ->(R) 031 -> 041 - 008 : 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 ->(B) -> 005 -> 006 -> 007 -> 008 - 001 : 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 ->(B) -> 004 -> 003 -> 002 -> 001 - 011 : 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 ->(B) -> 004 -> 030 -> 009 -> 010 -> 011 - 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 ->(B) -> 004 -> 030 -> 009 -> 010 -> 011 - 019 : 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019 - 015 : 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 -> 014 -> 015 - -Q2-1.006->007(전진) : 목표 040,041,008,001,011,019,015 - 040 : 007 ->(B) 006 -> 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 040 - 041 : 007 ->(B) 006 -> 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 031 -> 041 - 008 : 007 ->(F) 006 -> 005 -> 037 ->(B) 005 -> 006 -> 007 -> 008 - 001 : 007 ->(B) 006 -> 005 -> 004 -> 003 -> 002 -> 001 - 011 : 007 ->(B) 006 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011 - 019 : 007 ->(B) 006 -> 005 -> 004 -> 012 -> 013 ->(F) 012 -> 016 -> 017 -> 018 -> 019 - 015 : 007 ->(B) 006 -> 005 -> 004 -> 012 -> 016 ->(F) 012 -> 013 -> 014 -> 015 - - -Q2-2.006->007(후진) : 목표 040,041,008,001,011,019,015 - 040 : 007 ->(F) 006 -> 005 -> 004 ->(B) 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 040 - 041 : 007 ->(F) 006 -> 005 -> 004 ->(B) 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 031 -> 041 - 008 : 007 ->(B) 008 - 001 : 007 ->(F) 006 -> 005 -> 004 -> 030 ->(B) 004 -> 003 -> 002 -> 001 - : 007 ->(F) 006 -> 005 -> 004 -> 012 ->(B) 004 -> 003 -> 002 -> 001 - 011 : 007 ->(F) 006 -> 005 -> 004 -> 003 ->(B) 004 -> 030 -> 009 -> 010 -> 011 - 007 ->(F) 006 -> 005 -> 004 -> 012 ->(B) 004 -> 030 -> 009 -> 010 -> 011 - 019 : 007 ->(F) 006 -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019 - 015 : 007 ->(F) 006 -> 005 -> 004 -> 012 -> 013 -> 014 -> 015 - -Q3-1.009->010(전진) : 목표 040,041,008,001,011,019,015 -Q3-2.009->010(후진) : 목표 040,041,008,001,011,019,015 -Q4-1.013->014(전진) : 목표 040,041,008,001,011,019,015 -Q4-2.013->014(후진) : 목표 040,041,008,001,011,019,015 - -Q5-1.033->032(전진) : 목표 040,041,008,001,011,019,015 -Q5-2.033->032(후진) : 목표 040,041,008,001,011,019,015 -Q6-1.006->007(전진) : 목표 040,041,008,001,011,019,015 -Q6-2.006->007(후진) : 목표 040,041,008,001,011,019,015 -Q7-1.009->010(전진) : 목표 040,041,008,001,011,019,015 -Q7-2.009->010(후진) : 목표 040,041,008,001,011,019,015 -Q8-1.013->014(전진) : 목표 040,041,008,001,011,019,015 -Q8-2.013->014(후진) : 목표 040,041,008,001,011,019,015 - diff --git a/Cs_HMI/PathLogic/MapData.json b/Cs_HMI/PathLogic/MapData.json deleted file mode 100644 index ecbac87..0000000 --- a/Cs_HMI/PathLogic/MapData.json +++ /dev/null @@ -1,1050 +0,0 @@ -{ - "Nodes": [ - { - "NodeId": "N001", - "Name": "UNLOADER", - "Position": "65, 229", - "Type": 2, - "DockDirection": 2, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:44.9548285+09:00", - "ModifiedDate": "2025-09-15T11:19:44.6879389+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "001", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N001 - UNLOADER - [001]" - }, - { - "NodeId": "N002", - "Name": "N002", - "Position": "206, 244", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N001" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:48.2957516+09:00", - "ModifiedDate": "2025-09-15T10:16:10.1841326+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "002", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N002 - N002 - [002]" - }, - { - "NodeId": "N003", - "Name": "N003", - "Position": "278, 278", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N002" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:49.2226656+09:00", - "ModifiedDate": "2025-09-15T10:16:09.1753358+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "003", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N003 - N003 - [003]" - }, - { - "NodeId": "N004", - "Name": "N004", - "Position": "380, 340", - "Type": 1, - "DockDirection": 0, - "ConnectedNodes": [ - "N003", - "N022", - "N031" - ], - "CanRotate": true, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:50.1681027+09:00", - "ModifiedDate": "2025-09-15T11:18:47.8876112+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "004", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N004 - N004 - [004]" - }, - { - "NodeId": "N006", - "Name": "N006", - "Position": "520, 220", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N007" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:51.1111368+09:00", - "ModifiedDate": "2025-09-15T10:16:24.8315093+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "013", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N006 - N006 - [013]" - }, - { - "NodeId": "N007", - "Name": "N007", - "Position": "600, 180", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:51.9266982+09:00", - "ModifiedDate": "2025-09-11T11:46:43.5813583+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "014", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N007 - N007 - [014]" - }, - { - "NodeId": "N008", - "Name": "N008", - "Position": "299, 456", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N009", - "N031" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:53.9595825+09:00", - "ModifiedDate": "2025-09-15T11:18:49.8874367+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "009", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N008 - N008 - [009]" - }, - { - "NodeId": "N009", - "Name": "N009", - "Position": "193, 477", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N010" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:54.5035702+09:00", - "ModifiedDate": "2025-09-15T10:16:14.696211+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "010", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N009 - N009 - [010]" - }, - { - "NodeId": "N010", - "Name": "TOPS", - "Position": "52, 466", - "Type": 2, - "DockDirection": 2, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:55.0563237+09:00", - "ModifiedDate": "2025-09-15T11:19:40.1582831+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "011", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N010 - TOPS - [011]" - }, - { - "NodeId": "N011", - "Name": "N011", - "Position": "460, 420", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N012", - "N004", - "N015" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:55.8875335+09:00", - "ModifiedDate": "2025-09-15T10:16:28.6957855+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "005", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N011 - N011 - [005]" - }, - { - "NodeId": "N012", - "Name": "N012", - "Position": "540, 480", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N013" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:56.3678144+09:00", - "ModifiedDate": "2025-09-11T11:46:27.9224943+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "006", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N012 - N012 - [006]" - }, - { - "NodeId": "N013", - "Name": "N013", - "Position": "620, 520", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N014" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:56.8390845+09:00", - "ModifiedDate": "2025-09-11T11:46:29.5788308+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "007", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N013 - N013 - [007]" - }, - { - "NodeId": "N014", - "Name": "LOADER", - "Position": "720, 580", - "Type": 2, - "DockDirection": 2, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:34:57.2549726+09:00", - "ModifiedDate": "2025-09-15T11:19:35.3431797+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "008", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N014 - LOADER - [008]" - }, - { - "NodeId": "N019", - "Name": "CHARGER #2", - "Position": "679, 199", - "Type": 3, - "DockDirection": 1, - "ConnectedNodes": [ - "N007" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:35:56.5359098+09:00", - "ModifiedDate": "2025-09-15T11:19:49.2931335+09:00", - "IsActive": true, - "DisplayColor": "Red", - "RfidId": "015", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N019 - CHARGER #2 - [015]" - }, - { - "NodeId": "N022", - "Name": "N022", - "Position": "459, 279", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N023", - "N006" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T08:36:48.0311551+09:00", - "ModifiedDate": "2025-09-15T10:16:22.8799696+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "012", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N022 - N022 - [012]" - }, - { - "NodeId": "N023", - "Name": "N023", - "Position": "440, 220", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N024" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T09:41:36.8738794+09:00", - "ModifiedDate": "2025-09-15T10:16:20.0378544+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "016", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N023 - N023 - [016]" - }, - { - "NodeId": "N024", - "Name": "N024", - "Position": "500, 160", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N025" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T09:41:37.4551853+09:00", - "ModifiedDate": "2025-09-15T10:16:20.8801598+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "017", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N024 - N024 - [017]" - }, - { - "NodeId": "N025", - "Name": "N025", - "Position": "600, 120", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N026" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T09:41:38.0142374+09:00", - "ModifiedDate": "2025-09-15T10:16:21.6723809+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "018", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N025 - N025 - [018]" - }, - { - "NodeId": "N026", - "Name": "CHARGER #1", - "Position": "660, 100", - "Type": 3, - "DockDirection": 1, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T09:41:38.5834487+09:00", - "ModifiedDate": "2025-09-15T11:19:58.0225184+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "019", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N026 - CHARGER #1 - [019]" - }, - { - "NodeId": "LBL001", - "Name": "Amkor Technology Korea", - "Position": "58, 64", - "Type": 4, - "DockDirection": 0, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T11:08:22.4048927+09:00", - "ModifiedDate": "2025-09-15T11:22:15.1196535+09:00", - "IsActive": true, - "DisplayColor": "Purple", - "RfidId": "", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "Amkor Technology Korea", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "255, 255, 192", - "ShowBackground": true, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "LBL001 - Amkor Technology Korea" - }, - { - "NodeId": "IMG001", - "Name": "logo", - "Position": "720, 371", - "Type": 5, - "DockDirection": 0, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-11T11:08:44.7897541+09:00", - "ModifiedDate": "2025-09-17T15:39:07.5229808+09:00", - "IsActive": true, - "DisplayColor": "Brown", - "RfidId": "", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "C:\\Data\\Users\\Pictures\\짤방\\아아악.png", - "Scale": "0.7, 0.7", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "IMG001 - logo" - }, - { - "NodeId": "N015", - "Name": "", - "Position": "436, 485", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N016" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:47.8065756+09:00", - "ModifiedDate": "2025-09-15T15:40:38.2050196+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "037", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N015 - [037]" - }, - { - "NodeId": "N016", - "Name": "", - "Position": "425, 524", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N017" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:48.6628848+09:00", - "ModifiedDate": "2025-09-15T15:40:36.7952276+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "036", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N016 - [036]" - }, - { - "NodeId": "N017", - "Name": "", - "Position": "387, 557", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N018" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:49.8138877+09:00", - "ModifiedDate": "2025-09-15T15:40:35.5342054+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "035", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N017 - [035]" - }, - { - "NodeId": "N018", - "Name": "", - "Position": "314, 549", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N005" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:50.6790623+09:00", - "ModifiedDate": "2025-09-15T15:40:33.4719206+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "034", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N018 - [034]" - }, - { - "NodeId": "N005", - "Name": "", - "Position": "229, 553", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N020" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:51.5267199+09:00", - "ModifiedDate": "2025-09-15T15:40:31.7321878+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "033", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N005 - [033]" - }, - { - "NodeId": "N020", - "Name": "", - "Position": "148, 545", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:52.3666114+09:00", - "ModifiedDate": "2025-09-15T15:40:30.1486235+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "032", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N020 - [032]" - }, - { - "NodeId": "N021", - "Name": "", - "Position": "66, 547", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [ - "N020" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:53.0958619+09:00", - "ModifiedDate": "2025-09-15T15:40:27.7345798+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "031", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N021 - [031]" - }, - { - "NodeId": "N027", - "Name": "BUF1", - "Position": "65, 644", - "Type": 2, - "DockDirection": 2, - "ConnectedNodes": [ - "N021" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:54.7345704+09:00", - "ModifiedDate": "2025-09-16T16:25:24.8062758+09:00", - "IsActive": true, - "DisplayColor": "Green", - "RfidId": "041", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N027 - BUF1 - [041]" - }, - { - "NodeId": "N028", - "Name": "BUF2", - "Position": "149, 645", - "Type": 2, - "DockDirection": 2, - "ConnectedNodes": [ - "N020" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:55.5263512+09:00", - "ModifiedDate": "2025-09-16T16:25:28.6358219+09:00", - "IsActive": true, - "DisplayColor": "Green", - "RfidId": "040", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N028 - BUF2 - [040]" - }, - { - "NodeId": "N029", - "Name": "BUF3", - "Position": "231, 639", - "Type": 2, - "DockDirection": 2, - "ConnectedNodes": [ - "N005" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:56.6623294+09:00", - "ModifiedDate": "2025-09-16T16:25:34.5699894+09:00", - "IsActive": true, - "DisplayColor": "Green", - "RfidId": "039", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N029 - BUF3 - [039]" - }, - { - "NodeId": "N030", - "Name": "BUF4", - "Position": "314, 639", - "Type": 2, - "DockDirection": 2, - "ConnectedNodes": [ - "N018" - ], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-12T17:22:57.5510908+09:00", - "ModifiedDate": "2025-09-16T16:25:40.3838199+09:00", - "IsActive": true, - "DisplayColor": "Green", - "RfidId": "038", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N030 - BUF4 - [038]" - }, - { - "NodeId": "N031", - "Name": "", - "Position": "337, 397", - "Type": 0, - "DockDirection": 0, - "ConnectedNodes": [], - "CanRotate": false, - "StationId": "", - "StationType": null, - "CreatedDate": "2025-09-15T11:18:40.5366059+09:00", - "ModifiedDate": "2025-09-15T15:40:24.0443882+09:00", - "IsActive": true, - "DisplayColor": "Blue", - "RfidId": "030", - "RfidStatus": "정상", - "RfidDescription": "", - "LabelText": "", - "FontFamily": "Arial", - "FontSize": 12.0, - "FontStyle": 0, - "ForeColor": "Black", - "BackColor": "Transparent", - "ShowBackground": false, - "ImagePath": "", - "Scale": "1, 1", - "Opacity": 1.0, - "Rotation": 0.0, - "DisplayText": "N031 - [030]" - } - ], - "CreatedDate": "2025-09-17T15:39:10.9736288+09:00", - "Version": "1.0" -} \ No newline at end of file diff --git a/Cs_HMI/PathLogic/Models/Enums.cs b/Cs_HMI/PathLogic/Models/Enums.cs deleted file mode 100644 index 582f43f..0000000 --- a/Cs_HMI/PathLogic/Models/Enums.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; - -namespace PathLogic.Models -{ - /// - /// 노드 타입 열거형 - /// - public enum NodeType - { - /// 일반 경로 노드 - Normal, - /// 회전 가능 지점 - Rotation, - /// 도킹 스테이션 - Docking, - /// 충전 스테이션 - Charging, - /// 라벨 (UI 요소) - Label, - /// 이미지 (UI 요소) - Image - } - - /// - /// 도킹 방향 열거형 - /// - public enum DockingDirection - { - /// 도킹 방향 상관없음 (일반 경로 노드) - DontCare, - /// 전진 도킹 (충전기) - Forward, - /// 후진 도킹 (로더, 클리너, 오프로더, 버퍼) - Backward - } - - /// - /// AGV 이동 방향 열거형 - /// - public enum AgvDirection - { - /// 전진 (모니터 방향) - Forward, - /// 후진 (리프트 방향) - Backward, - /// 좌회전 - Left, - /// 우회전 - Right, - /// 정지 - Stop - } - - /// - /// 장비 타입 열거형 - /// - public enum StationType - { - /// 로더 - Loader, - /// 클리너 - Cleaner, - /// 오프로더 - Offloader, - /// 버퍼 - Buffer, - /// 충전기 - Charger - } - - /// - /// 경로 찾기 결과 상태 - /// - public enum PathFindingStatus - { - /// 성공 - Success, - /// 경로를 찾을 수 없음 - NoPathFound, - /// 시작 노드가 유효하지 않음 - InvalidStartNode, - /// 목표 노드가 유효하지 않음 - InvalidTargetNode, - /// 맵 데이터가 없음 - NoMapData, - /// 계산 오류 - CalculationError - } -} \ No newline at end of file diff --git a/Cs_HMI/PathLogic/Models/MapData.cs b/Cs_HMI/PathLogic/Models/MapData.cs deleted file mode 100644 index 5fbcd39..0000000 --- a/Cs_HMI/PathLogic/Models/MapData.cs +++ /dev/null @@ -1,360 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace PathLogic.Models -{ - /// - /// 맵 데이터를 관리하는 클래스 - /// 기존 AGV 맵 파일 형식과 호환 - /// - public class MapData - { - /// - /// 맵의 모든 노드 목록 - /// - public List Nodes { get; set; } = new List(); - - /// - /// 맵 생성 일자 - /// - public DateTime CreatedDate { get; set; } = DateTime.Now; - - /// - /// 맵 버전 - /// - public string Version { get; set; } = "1.0"; - - /// - /// 기본 생성자 - /// - public MapData() - { - } - - /// - /// 노드 ID로 노드 찾기 - /// - /// 노드 ID - /// 해당 노드, 없으면 null - public MapNode GetNodeById(string nodeId) - { - return Nodes.FirstOrDefault(n => n.NodeId == nodeId); - } - - /// - /// RFID ID로 노드 찾기 - /// - /// RFID ID - /// 해당 노드, 없으면 null - public MapNode GetNodeByRfidId(string rfidId) - { - return Nodes.FirstOrDefault(n => n.RfidId == rfidId); - } - - /// - /// 네비게이션 가능한 노드만 반환 - /// - /// 네비게이션 가능한 노드 목록 - public List GetNavigationNodes() - { - return Nodes.Where(n => n.IsNavigationNode()).ToList(); - } - - /// - /// 특정 타입의 노드들 반환 - /// - /// 노드 타입 - /// 해당 타입의 노드 목록 - public List GetNodesByType(NodeType nodeType) - { - return Nodes.Where(n => n.Type == nodeType).ToList(); - } - - /// - /// 도킹 스테이션 노드들 반환 - /// - /// 도킹 스테이션 노드 목록 - public List GetDockingStations() - { - return Nodes.Where(n => n.Type == NodeType.Docking || n.Type == NodeType.Charging).ToList(); - } - - /// - /// 충전 스테이션 노드들 반환 - /// - /// 충전 스테이션 노드 목록 - public List GetChargingStations() - { - return Nodes.Where(n => n.Type == NodeType.Charging).ToList(); - } - - /// - /// 회전 가능한 노드들 반환 - /// - /// 회전 가능한 노드 목록 - public List GetRotationNodes() - { - return Nodes.Where(n => n.CanPerformRotation()).ToList(); - } - - /// - /// 노드 추가 - /// - /// 추가할 노드 - /// 추가 성공 여부 - public bool AddNode(MapNode node) - { - if (node == null || GetNodeById(node.NodeId) != null) - return false; - - Nodes.Add(node); - return true; - } - - /// - /// 노드 제거 - /// - /// 제거할 노드 ID - /// 제거 성공 여부 - public bool RemoveNode(string nodeId) - { - var node = GetNodeById(nodeId); - if (node == null) return false; - - // 다른 노드들의 연결에서도 제거 - foreach (var otherNode in Nodes) - { - otherNode.ConnectedNodes.Remove(nodeId); - } - - return Nodes.Remove(node); - } - - /// - /// 두 노드 간 연결 추가 - /// - /// 시작 노드 ID - /// 도착 노드 ID - /// 양방향 연결 여부 - /// 연결 성공 여부 - public bool AddConnection(string fromNodeId, string toNodeId, bool bidirectional = true) - { - var fromNode = GetNodeById(fromNodeId); - var toNode = GetNodeById(toNodeId); - - if (fromNode == null || toNode == null) return false; - - // 단방향 연결 - if (!fromNode.ConnectedNodes.Contains(toNodeId)) - { - fromNode.ConnectedNodes.Add(toNodeId); - fromNode.ModifiedDate = DateTime.Now; - } - - // 양방향 연결 - if (bidirectional && !toNode.ConnectedNodes.Contains(fromNodeId)) - { - toNode.ConnectedNodes.Add(fromNodeId); - toNode.ModifiedDate = DateTime.Now; - } - - return true; - } - - /// - /// 두 노드 간 연결 제거 - /// - /// 시작 노드 ID - /// 도착 노드 ID - /// 양방향 제거 여부 - /// 제거 성공 여부 - public bool RemoveConnection(string fromNodeId, string toNodeId, bool bidirectional = true) - { - var fromNode = GetNodeById(fromNodeId); - var toNode = GetNodeById(toNodeId); - - if (fromNode == null || toNode == null) return false; - - bool removed = false; - - // 단방향 제거 - if (fromNode.ConnectedNodes.Remove(toNodeId)) - { - fromNode.ModifiedDate = DateTime.Now; - removed = true; - } - - // 양방향 제거 - if (bidirectional && toNode.ConnectedNodes.Remove(fromNodeId)) - { - toNode.ModifiedDate = DateTime.Now; - removed = true; - } - - return removed; - } - - /// - /// 두 노드가 연결되어 있는지 확인 - /// - /// 시작 노드 ID - /// 도착 노드 ID - /// 연결 여부 - public bool AreConnected(string fromNodeId, string toNodeId) - { - var fromNode = GetNodeById(fromNodeId); - return fromNode?.IsConnectedTo(toNodeId) ?? false; - } - - /// - /// 특정 노드의 이웃 노드들 반환 - /// - /// 노드 ID - /// 이웃 노드 목록 - public List GetNeighbors(string nodeId) - { - var node = GetNodeById(nodeId); - if (node == null) return new List(); - - var neighbors = new List(); - foreach (var connectedId in node.ConnectedNodes) - { - var neighbor = GetNodeById(connectedId); - if (neighbor != null && neighbor.IsNavigationNode()) - { - neighbors.Add(neighbor); - } - } - - return neighbors; - } - - /// - /// 맵 데이터 유효성 검증 - /// - /// 검증 결과 메시지 - public List ValidateMap() - { - var issues = new List(); - - // 노드 ID 중복 검사 - var nodeIds = Nodes.Select(n => n.NodeId).ToList(); - var duplicateIds = nodeIds.GroupBy(id => id) - .Where(g => g.Count() > 1) - .Select(g => g.Key); - - foreach (var duplicateId in duplicateIds) - { - issues.Add($"중복된 노드 ID: {duplicateId}"); - } - - // RFID ID 중복 검사 - var rfidIds = Nodes.Where(n => n.HasRfid()) - .Select(n => n.RfidId) - .ToList(); - var duplicateRfids = rfidIds.GroupBy(id => id) - .Where(g => g.Count() > 1) - .Select(g => g.Key); - - foreach (var duplicateRfid in duplicateRfids) - { - issues.Add($"중복된 RFID ID: {duplicateRfid}"); - } - - // 잘못된 연결 검사 - foreach (var node in Nodes) - { - foreach (var connectedId in node.ConnectedNodes) - { - if (GetNodeById(connectedId) == null) - { - issues.Add($"노드 {node.NodeId}가 존재하지 않는 노드 {connectedId}에 연결됨"); - } - } - } - - // 고립된 네비게이션 노드 검사 - var navigationNodes = GetNavigationNodes(); - foreach (var node in navigationNodes) - { - if (node.ConnectedNodes.Count == 0) - { - issues.Add($"고립된 노드: {node.NodeId}"); - } - } - - return issues; - } - - /// - /// 맵 통계 정보 반환 - /// - /// 맵 통계 - public MapStatistics GetStatistics() - { - var stats = new MapStatistics(); - var navigationNodes = GetNavigationNodes(); - - stats.TotalNodes = Nodes.Count; - stats.NavigationNodes = navigationNodes.Count; - stats.DockingStations = GetNodesByType(NodeType.Docking).Count; - stats.ChargingStations = GetNodesByType(NodeType.Charging).Count; - stats.RotationNodes = GetRotationNodes().Count; - stats.LabelNodes = GetNodesByType(NodeType.Label).Count; - stats.ImageNodes = GetNodesByType(NodeType.Image).Count; - - // 연결 수 계산 - stats.TotalConnections = navigationNodes.Sum(n => n.ConnectedNodes.Count) / 2; // 양방향이므로 2로 나눔 - - // RFID 할당된 노드 수 - stats.NodesWithRfid = Nodes.Count(n => n.HasRfid()); - - return stats; - } - - /// - /// 맵 데이터 복사 - /// - /// 복사된 맵 데이터 - public MapData Clone() - { - var clonedMap = new MapData - { - CreatedDate = CreatedDate, - Version = Version - }; - - foreach (var node in Nodes) - { - clonedMap.Nodes.Add(node.Clone()); - } - - return clonedMap; - } - } - - /// - /// 맵 통계 정보 클래스 - /// - public class MapStatistics - { - public int TotalNodes { get; set; } - public int NavigationNodes { get; set; } - public int DockingStations { get; set; } - public int ChargingStations { get; set; } - public int RotationNodes { get; set; } - public int LabelNodes { get; set; } - public int ImageNodes { get; set; } - public int TotalConnections { get; set; } - public int NodesWithRfid { get; set; } - - public override string ToString() - { - return $"총 노드: {TotalNodes}, 네비게이션: {NavigationNodes}, " + - $"도킹: {DockingStations}, 충전: {ChargingStations}, " + - $"회전: {RotationNodes}, 연결: {TotalConnections}, " + - $"RFID 할당: {NodesWithRfid}"; - } - } -} \ No newline at end of file diff --git a/Cs_HMI/PathLogic/Models/MapNode.cs b/Cs_HMI/PathLogic/Models/MapNode.cs deleted file mode 100644 index 16f48ea..0000000 --- a/Cs_HMI/PathLogic/Models/MapNode.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; - -namespace PathLogic.Models -{ - /// - /// 맵 노드 정보를 관리하는 클래스 - /// 기존 AGVNavigationCore의 MapNode와 호환되도록 설계 - /// - public class MapNode - { - /// - /// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID) - /// - public string NodeId { get; set; } = string.Empty; - - /// - /// 노드 표시 이름 - /// - public string Name { get; set; } = string.Empty; - - /// - /// 맵 상의 위치 좌표 (픽셀 단위) - /// - public Point Position { get; set; } = Point.Empty; - - /// - /// 노드 타입 - /// - public NodeType Type { get; set; } = NodeType.Normal; - - /// - /// 도킹 방향 - /// - public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare; - - /// - /// 연결된 노드 ID 목록 - /// - public List ConnectedNodes { get; set; } = new List(); - - /// - /// 회전 가능 여부 - /// - public bool CanRotate { get; set; } = false; - - /// - /// 장비 ID - /// - public string StationId { get; set; } = string.Empty; - - /// - /// 장비 타입 - /// - public StationType? StationType { get; set; } = null; - - /// - /// 노드 생성 일자 - /// - public DateTime CreatedDate { get; set; } = DateTime.Now; - - /// - /// 노드 수정 일자 - /// - public DateTime ModifiedDate { get; set; } = DateTime.Now; - - /// - /// 노드 활성화 여부 - /// - public bool IsActive { get; set; } = true; - - /// - /// 노드 색상 (맵 에디터 표시용) - /// - public Color DisplayColor { get; set; } = Color.Blue; - - /// - /// RFID 태그 ID - /// - public string RfidId { get; set; } = string.Empty; - - /// - /// RFID 상태 - /// - public string RfidStatus { get; set; } = "정상"; - - /// - /// RFID 설치 위치 설명 - /// - public string RfidDescription { get; set; } = string.Empty; - - // UI 관련 속성들 (맵 에디터 호환성을 위해 포함) - public string LabelText { get; set; } = string.Empty; - public string FontFamily { get; set; } = "Arial"; - public float FontSize { get; set; } = 12.0f; - public FontStyle FontStyle { get; set; } = FontStyle.Regular; - public Color ForeColor { get; set; } = Color.Black; - public Color BackColor { get; set; } = Color.Transparent; - public bool ShowBackground { get; set; } = false; - public string ImagePath { get; set; } = string.Empty; - public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f); - public float Opacity { get; set; } = 1.0f; - public float Rotation { get; set; } = 0.0f; - - /// - /// 기본 생성자 - /// - public MapNode() - { - } - - /// - /// 매개변수 생성자 - /// - public MapNode(string nodeId, string name, Point position, NodeType type) - { - NodeId = nodeId; - Name = name; - Position = position; - Type = type; - CreatedDate = DateTime.Now; - ModifiedDate = DateTime.Now; - SetDefaultColorByType(type); - } - - /// - /// 노드 타입에 따른 기본 색상 설정 - /// - public void SetDefaultColorByType(NodeType type) - { - switch (type) - { - case NodeType.Normal: - DisplayColor = Color.Blue; - break; - case NodeType.Rotation: - DisplayColor = Color.Orange; - break; - case NodeType.Docking: - DisplayColor = Color.Green; - break; - case NodeType.Charging: - DisplayColor = Color.Red; - break; - case NodeType.Label: - DisplayColor = Color.Purple; - break; - case NodeType.Image: - DisplayColor = Color.Brown; - break; - } - } - - /// - /// 경로 찾기에 사용 가능한 노드인지 확인 - /// - public bool IsNavigationNode() - { - return Type != NodeType.Label && Type != NodeType.Image && IsActive; - } - - /// - /// RFID가 할당되어 있는지 확인 - /// - public bool HasRfid() - { - return !string.IsNullOrEmpty(RfidId); - } - - /// - /// 도킹이 필요한 노드인지 확인 - /// - public bool RequiresDocking() - { - return Type == NodeType.Docking || Type == NodeType.Charging; - } - - /// - /// 회전이 가능한 노드인지 확인 - /// - public bool CanPerformRotation() - { - return CanRotate || Type == NodeType.Rotation; - } - - /// - /// 노드의 필요한 도킹 방향 반환 - /// - public AgvDirection GetRequiredDirection() - { - switch (DockDirection) - { - case DockingDirection.Forward: - return AgvDirection.Forward; - case DockingDirection.Backward: - return AgvDirection.Backward; - default: - return AgvDirection.Forward; // 기본값 - } - } - - /// - /// 다른 노드와 연결되어 있는지 확인 - /// - public bool IsConnectedTo(string nodeId) - { - return ConnectedNodes.Contains(nodeId); - } - - /// - /// 두 노드 간의 유클리드 거리 계산 - /// - public double DistanceTo(MapNode other) - { - if (other == null) return double.MaxValue; - - double dx = Position.X - other.Position.X; - double dy = Position.Y - other.Position.Y; - return Math.Sqrt(dx * dx + dy * dy); - } - - /// - /// 두 노드 간의 맨하탄 거리 계산 - /// - public double ManhattanDistanceTo(MapNode other) - { - if (other == null) return double.MaxValue; - - return Math.Abs(Position.X - other.Position.X) + Math.Abs(Position.Y - other.Position.Y); - } - - /// - /// 노드 정보를 문자열로 반환 - /// - public override string ToString() - { - string rfidInfo = HasRfid() ? $"[{RfidId}]" : ""; - return $"{NodeId}{rfidInfo}: {Name} ({Type}) at ({Position.X}, {Position.Y})"; - } - - /// - /// 표시용 텍스트 반환 - /// - public string DisplayText - { - get - { - var displayText = NodeId; - - if (!string.IsNullOrEmpty(Name)) - { - displayText += $" - {Name}"; - } - - if (!string.IsNullOrEmpty(RfidId)) - { - displayText += $" - [{RfidId}]"; - } - - return displayText; - } - } - - /// - /// 노드 복사 - /// - public MapNode Clone() - { - return new MapNode - { - NodeId = NodeId, - Name = Name, - Position = Position, - Type = Type, - DockDirection = DockDirection, - ConnectedNodes = new List(ConnectedNodes), - CanRotate = CanRotate, - StationId = StationId, - StationType = StationType, - CreatedDate = CreatedDate, - ModifiedDate = ModifiedDate, - IsActive = IsActive, - DisplayColor = DisplayColor, - RfidId = RfidId, - RfidStatus = RfidStatus, - RfidDescription = RfidDescription, - LabelText = LabelText, - FontFamily = FontFamily, - FontSize = FontSize, - FontStyle = FontStyle, - ForeColor = ForeColor, - BackColor = BackColor, - ShowBackground = ShowBackground, - ImagePath = ImagePath, - Scale = Scale, - Opacity = Opacity, - Rotation = Rotation - }; - } - } -} \ No newline at end of file diff --git a/Cs_HMI/PathLogic/PathLogic.csproj b/Cs_HMI/PathLogic/PathLogic.csproj deleted file mode 100644 index 901b3bf..0000000 --- a/Cs_HMI/PathLogic/PathLogic.csproj +++ /dev/null @@ -1,66 +0,0 @@ - - - - - Debug - AnyCPU - {12345678-1234-5678-9012-123456789ABC} - Exe - PathLogic - PathLogic - v4.8 - 512 - true - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Cs_HMI/PathLogic/Program.cs b/Cs_HMI/PathLogic/Program.cs deleted file mode 100644 index d4623e1..0000000 --- a/Cs_HMI/PathLogic/Program.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.IO; -using PathLogic.Core; -using PathLogic.Models; - -namespace PathLogic -{ - /// - /// PathLogic 메인 프로그램 - /// AGV 길찾기 알고리즘을 테스트하고 개발하기 위한 콘솔 애플리케이션 - /// - class Program - { - private static MapLoader _mapLoader; - private static MapData _mapData; - - static void Main(string[] args) - { - Console.WriteLine("===== AGV PathLogic 개발 도구 ====="); - Console.WriteLine("AGV 길찾기 알고리즘 개발을 위한 기본 프로젝트"); - Console.WriteLine(); - - try - { - InitializeSystem(); - RunMainLoop(); - } - catch (Exception ex) - { - Console.WriteLine($"오류 발생: {ex.Message}"); - Console.WriteLine("아무 키나 눌러 종료하세요."); - Console.ReadKey(); - } - } - - /// - /// 시스템 초기화 - /// - private static void InitializeSystem() - { - Console.WriteLine("시스템 초기화 중..."); - - _mapLoader = new MapLoader(); - - // 기본 맵 파일 로드 - string defaultMapPath = @"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap"; - - if (File.Exists(defaultMapPath)) - { - LoadMap(defaultMapPath); - Console.WriteLine("기본 맵 파일 로드 완료."); - } - else - { - Console.WriteLine("기본 맵 파일을 찾을 수 없습니다."); - Console.WriteLine($"경로: {defaultMapPath}"); - } - - Console.WriteLine("초기화 완료."); - Console.WriteLine(); - } - - /// - /// 메인 루프 - /// - private static void RunMainLoop() - { - while (true) - { - ShowMenu(); - - string input = Console.ReadLine(); - - if (string.IsNullOrEmpty(input)) - continue; - - if (input.ToLower() == "q" || input.ToLower() == "quit" || input.ToLower() == "exit") - { - Console.WriteLine("프로그램을 종료합니다."); - break; - } - - ProcessCommand(input); - Console.WriteLine(); - } - } - - /// - /// 메뉴 표시 - /// - private static void ShowMenu() - { - Console.WriteLine("===== 메뉴 ====="); - Console.WriteLine("1. 맵 파일 로드 (load)"); - Console.WriteLine("2. 맵 정보 표시 (info)"); - Console.WriteLine("3. 노드 목록 표시 (nodes)"); - Console.WriteLine("q. 종료 (quit)"); - Console.Write("명령을 입력하세요: "); - } - - /// - /// 명령 처리 - /// - /// 사용자 입력 명령 - private static void ProcessCommand(string command) - { - try - { - switch (command.ToLower().Trim()) - { - case "1": - case "load": - LoadMapCommand(); - break; - case "2": - case "info": - ShowMapInfo(); - break; - case "3": - case "nodes": - ShowNodes(); - break; - default: - Console.WriteLine("알 수 없는 명령입니다."); - break; - } - } - catch (Exception ex) - { - Console.WriteLine($"명령 처리 중 오류: {ex.Message}"); - } - } - - /// - /// 맵 파일 로드 명령 - /// - private static void LoadMapCommand() - { - Console.Write("맵 파일 경로를 입력하세요 (엔터: 기본 경로): "); - string path = Console.ReadLine(); - - if (string.IsNullOrEmpty(path)) - { - path = @"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap"; - } - - if (LoadMap(path)) - { - Console.WriteLine("맵 파일 로드 성공!"); - } - else - { - Console.WriteLine("맵 파일 로드 실패!"); - } - } - - /// - /// 맵 파일 로드 - /// - /// 맵 파일 경로 - /// 로드 성공 여부 - private static bool LoadMap(string filePath) - { - try - { - _mapData = _mapLoader.LoadFromFile(filePath); - return true; - } - catch (Exception ex) - { - Console.WriteLine($"맵 로드 오류: {ex.Message}"); - return false; - } - } - - /// - /// 맵 정보 표시 - /// - private static void ShowMapInfo() - { - if (_mapData == null) - { - Console.WriteLine("로드된 맵이 없습니다."); - return; - } - - Console.WriteLine("===== 맵 정보 ====="); - var stats = _mapData.GetStatistics(); - Console.WriteLine(stats.ToString()); - } - - /// - /// 노드 목록 표시 - /// - private static void ShowNodes() - { - if (_mapData == null) - { - Console.WriteLine("로드된 맵이 없습니다."); - return; - } - - Console.WriteLine("===== 노드 목록 ====="); - foreach (var node in _mapData.GetNavigationNodes()) - { - Console.WriteLine($"{node.NodeId} ({node.RfidId}) - {node.Name} [{node.Type}] - 연결: {node.ConnectedNodes.Count}개"); - } - } - } -} \ No newline at end of file diff --git a/Cs_HMI/PathLogic/Properties/AssemblyInfo.cs b/Cs_HMI/PathLogic/Properties/AssemblyInfo.cs deleted file mode 100644 index b745471..0000000 --- a/Cs_HMI/PathLogic/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해 -// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면 -// 이러한 특성 값을 변경하세요. -[assembly: AssemblyTitle("PathLogic")] -[assembly: AssemblyDescription("AGV 길찾기 알고리즘 테스트 및 개발 도구")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("ENIG")] -[assembly: AssemblyProduct("PathLogic")] -[assembly: AssemblyCopyright("Copyright © ENIG 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에 -// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면 -// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요. -[assembly: ComVisible(false)] - -// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다. -[assembly: Guid("12345678-1234-5678-9012-123456789abc")] - -// 어셈블리의 버전 정보는 다음 네 개의 값으로 구성됩니다. -// -// 주 버전 -// 부 버전 -// 빌드 번호 -// 수정 버전 -// -// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호를 -// 기본값으로 할 수 있습니다. -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/Cs_HMI/PathLogic/__pycache__/agv_pathfinder.cpython-313.pyc b/Cs_HMI/PathLogic/__pycache__/agv_pathfinder.cpython-313.pyc deleted file mode 100644 index 8d93c7b..0000000 Binary files a/Cs_HMI/PathLogic/__pycache__/agv_pathfinder.cpython-313.pyc and /dev/null differ diff --git a/Cs_HMI/PathLogic/__pycache__/universal_pathfinder.cpython-313.pyc b/Cs_HMI/PathLogic/__pycache__/universal_pathfinder.cpython-313.pyc deleted file mode 100644 index a6abb83..0000000 Binary files a/Cs_HMI/PathLogic/__pycache__/universal_pathfinder.cpython-313.pyc and /dev/null differ diff --git a/Cs_HMI/PathLogic/agv_path_planner.py b/Cs_HMI/PathLogic/agv_path_planner.py deleted file mode 100644 index a0b2d2d..0000000 --- a/Cs_HMI/PathLogic/agv_path_planner.py +++ /dev/null @@ -1,361 +0,0 @@ -# agv_path_planner.py (v1.1) -# ------------------------------------------------------------ -# AGV 경로 계획 + F/B(전/후진) 주석 출력 (단일 해답 보장) -# - 입력: 현재 RFID, 직전 RFID, 마지막 모터(F/B), 목표 RFID -# - MapData.json에서 맵/좌표/도킹/연결 파싱 -# - 결정 규칙: 목적지 근접 TP(갈림길) 우선 + 포크 루프(Left>Right>Straight) + 최종 도킹 모터 강제 -# - robust: utf-8-sig 로딩, RFID 정규화(007/7 등), --debug 모드 -# ------------------------------------------------------------ -import re, math, argparse, sys -from collections import defaultdict, deque -from pathlib import Path - -# ---------- RFID 정규화 ---------- -def norm_rfid_token(s: str) -> str: - """ - 숫자만 추출하여 3자리 zero-pad (예: '7'->'007', '007'->'007'). - 숫자가 하나도 없으면 원문 반환. - """ - if s is None: return s - digits = re.sub(r'\D+', '', s) - return digits.zfill(3) if digits else s.strip() - -def make_rfid_aliases(raw: str): - """ - 한 RFID에 대해 가능한 별칭 세트를 만듭니다. - 예: '007' -> {'007','7'} - '40' -> {'040','40'} - """ - if raw is None: return set() - raw = raw.strip() - z3 = norm_rfid_token(raw) - nz = raw.lstrip('0') or '0' - znz = z3.lstrip('0') or '0' - return {raw, z3, nz, znz} - -# ---------- Map 파서 ---------- -import json - -def parse_map_json(text: str): - """JSON 형식 맵 데이터 파싱 (MapData.json, NewMap.agvmap용)""" - try: - data = json.loads(text) - nodes = {} - - for node_data in data.get("Nodes", []): - nid = node_data.get("NodeId") - if not nid: - continue - - # Position 파싱 "x, y" → (x, y) - pos_str = node_data.get("Position", "0, 0") - try: - x, y = map(int, pos_str.split(", ")) - pos = (x, y) - except: - pos = (0, 0) - - nodes[nid] = { - 'NodeId': nid, - 'RfidId': node_data.get('RfidId'), - 'Type': node_data.get('Type', 0), - 'DockDirection': node_data.get('DockDirection', 0), - 'Position': pos, - 'ConnectedNodes': node_data.get('ConnectedNodes', []), - } - - # 양방향 인접 - adj = defaultdict(set) - for u, d in nodes.items(): - for v in d['ConnectedNodes']: - if v in nodes: - adj[u].add(v); adj[v].add(u) - - # RFID 인덱스(별칭 포함) - nid2rfid = {} - rfid2nid = {} - for nid, nd in nodes.items(): - rf = nd.get('RfidId') - if rf: - nid2rfid[nid] = norm_rfid_token(rf) # 표준 3자리로 보관 - for alias in make_rfid_aliases(rf): - rfid2nid[alias] = nid - - return nodes, adj, nid2rfid, rfid2nid - - except json.JSONDecodeError: - # JSON 파싱 실패 시 텍스트 파서로 폴백 - return parse_map_text(text) - -def parse_map_text(text: str): - parts = re.split(r'\bNodeId\s+', text) - nodes = {} - for part in parts: - part = part.strip() - if not part: - continue - m = re.match(r'(\S+)', part) - if not m: - continue - nid = m.group(1) - - def find_num(key): - m = re.search(fr'\b{key}\s+(-?\d+)', part) - return int(m.group(1)) if m else None - - def find_str(key): - m = re.search(fr'\b{key}\s+([^\s\r\n]+)', part) - return m.group(1).strip() if m else None - - mpos = re.search(r'\bPosition\s+(-?\d+)\s*,\s*(-?\d+)', part) - pos = (int(mpos.group(1)), int(mpos.group(2))) if mpos else None - - mconn = re.search(r'\bConnectedNodes\s+([^\r\n]+)', part) - conns = [] - if mconn: - seg = mconn.group(1) - seg = re.split(r'\b(CanRotate|StationId|StationType|CreatedDate|ModifiedDate|IsActive|DisplayColor|RfidId|RfidStatus|RfidDescription|LabelText|FontFamily|FontSize|FontStyle|ForeColor|BackColor|ShowBackground|ImagePath|Scale|Opacity|Rotation|DisplayText|Name|Type|DockDirection|Position|NodeId)\b', seg)[0] - conns = re.findall(r'N\d+', seg) - - nodes[nid] = { - 'NodeId': nid, - 'RfidId': find_str('RfidId'), - 'Type': find_num('Type'), # 2=Station/Buffer, 3=Charger 등 - 'DockDirection': find_num('DockDirection'), # 1=전면(F), 2=후면(B) - 'Position': pos, - 'ConnectedNodes': conns, - } - - # 양방향 인접 - adj = defaultdict(set) - for u, d in nodes.items(): - for v in d['ConnectedNodes']: - if v in nodes: - adj[u].add(v); adj[v].add(u) - - # RFID 인덱스(별칭 포함) - nid2rfid = {} - rfid2nid = {} - for nid, nd in nodes.items(): - rf = nd.get('RfidId') - if rf: - nid2rfid[nid] = norm_rfid_token(rf) # 표준 3자리로 보관 - for alias in make_rfid_aliases(rf): - rfid2nid[alias] = nid - - return nodes, adj, nid2rfid, rfid2nid - -# ---------- 기하/유틸 ---------- -def is_fork(adj, n): return len(adj[n]) >= 3 - -def vec(nodes, a, b): - ax, ay = nodes[a]['Position']; bx, by = nodes[b]['Position'] - return (bx-ax, by-ay) - -def angle_between(u, v): - ux,uy=u; vx,vy=v - du=max((ux*ux+uy*uy)**0.5,1e-9); dv=max((vx*vx+vy*vy)**0.5,1e-9) - ux/=du; uy/=du; vx/=dv; vy/=dv - dot=max(-1.0,min(1.0,ux*vx+uy*vy)) - ang=math.degrees(math.acos(dot)) - cross=ux*vy-uy*vx - return ang, cross - -def classify_at_fork(nodes, adj, fork, came_from): - vin = vec(nodes, fork, came_from) - cand=[] - for nb in adj[fork]: - if nb==came_from: continue - v=vec(nodes, fork, nb) - ang,cross=angle_between(vin, v) - dev=abs(180-ang) - side='left' if cross>0 else 'right' # y-down 화면 기준 보정 - cand.append((nb,dev,side)) - if not cand: - return {'straight':None,'left':None,'right':None} - straight=min(cand, key=lambda x:x[1])[0] - lefts=[x for x in cand if x[2]=='left' and x[0]!=straight] - rights=[x for x in cand if x[2]=='right'and x[0]!=straight] - left=min(lefts, key=lambda x:x[1])[0] if lefts else None - right=min(rights, key=lambda x:x[1])[0] if rights else None - return {'straight':straight,'left':left,'right':right} - -def shortest_path(adj, s, t): - q=deque([s]); prev={s:None} - while q: - u=q.popleft() - if u==t: break - for v in adj[u]: - if v not in prev: - prev[v]=u; q.append(v) - if t not in prev: return None - path=[]; cur=t - while cur is not None: - path.append(cur); cur=prev[cur] - return list(reversed(path)) - -def desired_final_motor(nodes, goal_nid): - dd = nodes[goal_nid].get('DockDirection') - ty = nodes[goal_nid].get('Type') - if dd in (1,2): return 'F' if dd==1 else 'B' - if ty==3: return 'F' - if ty==2: return 'B' - return 'F' - -def choose_turning_point(nodes, adj, nid2rfid, rfid2nid, goal_nid, current_nid, preferred_tp_rfids=('004','039','040','038','005')): - forks=[n for n in adj if is_fork(adj,n) and n!=goal_nid and n!=current_nid] - if not forks: return None - # 목표에서 BFS 거리 - q=deque([goal_nid]); dist={goal_nid:0} - while q: - u=q.popleft() - for v in adj[u]: - if v not in dist: - dist[v]=dist[u]+1; q.append(v) - pref_index={rfid2nid.get(rf, None):i for i,rf in enumerate(preferred_tp_rfids) if rf in rfid2nid} - best=None; best_key=None - for n in forks: - if n in dist: - key=(dist[n], pref_index.get(n, 9999)) - if best_key is None or key 바로 최단경로 - if last_motor == final_motor: - for i in range(len(sp)-1): - a,b=sp[i], sp[i+1] - push(a,b,final_motor) - rpath=[nid2rfid[n] for n in sp] - return rpath, annotated - - # 모터 불일치 -> TP 사용 + 포크 루프 - tp = choose_turning_point(nodes, adj, nid2rfid, rfid2nid, goal, cur, preferred_tp_rfids) - if tp is None: - # 비권장 fallback: 마지막 홉만 final_motor - for i in range(len(sp)-1): - a,b=sp[i], sp[i+1] - motor = final_motor if i==len(sp)-2 else last_motor - push(a,b,motor) - rpath=[nid2rfid[n] for n in sp] - return rpath, annotated - - # A) cur -> TP : F - path_to_tp = shortest_path(adj, cur, tp) - for i in range(len(path_to_tp)-1): - a,b=path_to_tp[i], path_to_tp[i+1] - push(a,b,'F') - - # B) TP 포크 루프 결정(결정적): exit_to_goal, came_from - path_back = shortest_path(adj, tp, goal) - exit_to_goal = path_back[1] if len(path_back)>=2 else None - came_from = path_to_tp[-2] if len(path_to_tp)>=2 else None - - loop_branch=None - if is_fork(adj, tp) and came_from is not None: - cls = classify_at_fork(nodes, adj, tp, came_from) - for cand in [cls.get('left'), cls.get('right'), cls.get('straight')]: - if cand and cand != came_from and cand != exit_to_goal: - loop_branch = cand; break - if loop_branch is None: - for nb in adj[tp]: - if nb != came_from and nb != exit_to_goal: - loop_branch=nb; break - - if loop_branch: - push(tp, loop_branch, 'F') # 가지로 전진 - push(loop_branch, tp, 'B') # TP로 역진입 - - # C) TP -> goal : final_motor - path_back = shortest_path(adj, tp, goal) - for i in range(len(path_back)-1): - a,b=path_back[i], path_back[i+1] - push(a,b, final_motor) - - # RFID 경로 구성 (annotated 기반) - rpath=[annotated[0][0]] if annotated else [nid2rfid[cur]] - for (_,_,to_rfid) in annotated: rpath.append(to_rfid) - return rpath, annotated - -def format_annotated(annotated): - return " ".join([f"{a} -({m})-> {b}" for (a,m,b) in annotated]) - -# ---------- CLI ---------- -def main(): - ap = argparse.ArgumentParser(description="AGV RFID 경로 계획 + F/B 주석 (단일 해답)") - ap.add_argument("--map", default="MapData.json", help="맵 데이터 파일 (기본: MapData.json)") - ap.add_argument("--current", required=True, help="현재 RFID (예: 007)") - ap.add_argument("--prev", required=True, help="직전 RFID (예: 006)") - ap.add_argument("--last", required=True, help="마지막 모터(F/B 또는 forward/backward)") - ap.add_argument("--goal", required=True, help="목표 RFID (예: 040)") - ap.add_argument("--tp-order", default="004,039,040,038,005", help="TP 우선순위 RFID(쉼표구분)") - args = ap.parse_args() - - # 파일 읽기: utf-8-sig (BOM 안전) - map_path = Path(args.map).resolve() - try: - text = map_path.read_text(encoding="utf-8-sig", errors="ignore") - except Exception as e: - print(f"[오류] 맵 파일을 읽을 수 없습니다: {map_path}\n{e}", file=sys.stderr) - sys.exit(1) - - nodes, adj, nid2rfid, rfid2nid = parse_map_json(text) - - if getattr(args, "debug", False): - rfids = sorted({*rfid2nid.keys()}) - # 숫자형 RFID만 보기 좋게 필터(3자리 패드) - rfids_num = sorted({norm_rfid_token(r) for r in rfids if re.fullmatch(r'\d+', re.sub(r'\D','','r'))}) - print(">> DEBUG") - print(f" Map path : {map_path}") - print(f" RFID count : {len({nid2rfid[n] for n in nid2rfid})}") - print(f" Sample RFIDs (정규화) :", ", ".join(sorted({nid2rfid[n] for n in nid2rfid})[:20])) - print() - - last_motor = 'F' if str(args.last).lower().startswith('f') else 'B' - tp_pref = tuple([x.strip() for x in args.tp_order.split(",") if x.strip()]) - - rpath, annotated = plan_route_with_fb( - nodes, adj, nid2rfid, rfid2nid, - args.current, args.prev, last_motor, args.goal, - preferred_tp_rfids=tp_pref - ) - - print("\n=== 결과 ===") - print("RFID Path :", " → ".join(rpath)) - print("F/B Path :", format_annotated(annotated)) - -if __name__ == "__main__": - main() diff --git a/Cs_HMI/PathLogic/agv_pathfinder.py b/Cs_HMI/PathLogic/agv_pathfinder.py deleted file mode 100644 index 33e8da3..0000000 --- a/Cs_HMI/PathLogic/agv_pathfinder.py +++ /dev/null @@ -1,1318 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -AGV PathFinder Algorithm Prototype -AGV 길찾기 알고리즘 프로토타입 - -Based on INSTRUCTION.md requirements: -- AGV hardware constraints (no rotation, magnet line following) -- Direction compatibility for docking -- Turnaround logic using junctions -""" - -import json -from enum import Enum -from typing import Dict, List, Tuple, Optional, Set -from dataclasses import dataclass -from collections import deque - -class NodeType(Enum): - NAVIGATION = 0 - CHARGING = 1 - DOCKING = 2 - LOADER = 3 - UNLOADER = 4 - -class AgvDirection(Enum): - FORWARD = "Forward" - BACKWARD = "Backward" - -class MagnetDirection(Enum): - STRAIGHT = "Straight" - LEFT = "Left" - RIGHT = "Right" - -@dataclass -class MapNode: - node_id: str - name: str - rfid_id: str - node_type: NodeType - position: Tuple[int, int] - connected_nodes: List[str] - - def is_junction(self) -> bool: - """갈림길 여부 판단 (3개 이상 연결)""" - return len(self.connected_nodes) >= 3 - - def is_dead_end(self) -> bool: - """막다른 길 여부 판단 (1개 연결)""" - return len(self.connected_nodes) == 1 - -@dataclass -class PathStep: - from_node: str - to_node: str - motor_direction: AgvDirection - magnet_direction: MagnetDirection - - def __str__(self): - return f"{self.from_node}→{self.to_node} ({self.motor_direction.value}, {self.magnet_direction.value})" - - def to_rfid_string(self, agv_map) -> str: - """RFID 형식으로 출력""" - from_rfid = agv_map.get_node(self.from_node).rfid_id if agv_map.get_node(self.from_node) else self.from_node - to_rfid = agv_map.get_node(self.to_node).rfid_id if agv_map.get_node(self.to_node) else self.to_node - return f"{from_rfid}→{to_rfid} ({self.motor_direction.value}, {self.magnet_direction.value})" - -@dataclass -class PathResult: - success: bool - path_steps: List[PathStep] - total_distance: int - needs_turnaround: bool - turnaround_junction: Optional[str] - error_message: Optional[str] - -class AGVMap: - def __init__(self): - self.nodes: Dict[str, MapNode] = {} - - def load_from_file(self, filepath: str): - """맵 파일 로드 (JSON 형식)""" - try: - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - - # 노드 데이터 파싱 - for node_data in data.get('Nodes', []): - node_type_value = node_data.get('Type', 0) - try: - node_type = NodeType(node_type_value) - except ValueError: - node_type = NodeType.NAVIGATION - - node = MapNode( - node_id=node_data.get('NodeId', ''), - name=node_data.get('Name', ''), - rfid_id=node_data.get('RfidId', ''), - node_type=node_type, - position=self._parse_position(node_data.get('Position', '0,0')), - connected_nodes=node_data.get('ConnectedNodes', []) - ) - self.nodes[node.node_id] = node - - # 양방향 연결 처리 (Description.txt에 따르면 저장은 단방향이지만 실제로는 양방향) - self._make_bidirectional_connections() - - print(f"맵 로드 완료: {len(self.nodes)}개 노드") - return True - - except Exception as e: - print(f"맵 로드 오류: {e}") - return False - - def _parse_position(self, pos_str: str) -> Tuple[int, int]: - """위치 문자열 파싱 "x, y" -> (x, y)""" - try: - parts = pos_str.split(',') - return (int(parts[0].strip()), int(parts[1].strip())) - except: - return (0, 0) - - def _make_bidirectional_connections(self): - """단방향 연결을 양방향으로 변환""" - # 모든 노드의 연결 정보를 수집 - all_connections = set() - - for node_id, node in self.nodes.items(): - for connected_id in node.connected_nodes: - # 양방향 연결 추가 (작은 ID가 앞에 오도록 정렬) - pair = tuple(sorted([node_id, connected_id])) - all_connections.add(pair) - - # 각 노드의 연결 목록을 양방향으로 업데이트 - for node_id, node in self.nodes.items(): - bidirectional_connections = [] - for pair in all_connections: - if node_id in pair: - # 자신이 아닌 다른 노드 추가 - other_node = pair[0] if pair[1] == node_id else pair[1] - bidirectional_connections.append(other_node) - - node.connected_nodes = bidirectional_connections - - def get_node(self, node_id: str) -> Optional[MapNode]: - """노드 조회""" - return self.nodes.get(node_id) - - def get_node_by_rfid(self, rfid_id: str) -> Optional[MapNode]: - """RFID ID로 노드 조회""" - for node in self.nodes.values(): - if node.rfid_id == rfid_id: - return node - return None - - def resolve_node_id(self, identifier: str) -> Optional[str]: - """RFID나 NodeID로 실제 NodeID 반환""" - # 먼저 NodeID로 시도 - if identifier in self.nodes: - return identifier - - # RFID로 시도 - node = self.get_node_by_rfid(identifier) - if node: - return node.node_id - - return None - - def get_junctions(self) -> List[MapNode]: - """모든 갈림길 노드 반환""" - return [node for node in self.nodes.values() if node.is_junction()] - - def find_shortest_path(self, start: str, target: str) -> List[str]: - """최단 경로 계산 (BFS)""" - if start == target: - return [start] - - queue = deque([(start, [start])]) - visited = {start} - - while queue: - current, path = queue.popleft() - current_node = self.get_node(current) - - if not current_node: - continue - - for neighbor in current_node.connected_nodes: - if neighbor == target: - return path + [neighbor] - - if neighbor not in visited: - visited.add(neighbor) - queue.append((neighbor, path + [neighbor])) - - return [] # 경로 없음 - -class AGVPathfinder: - def __init__(self, agv_map: AGVMap): - self.map = agv_map - - def get_required_direction(self, node_id: str) -> AgvDirection: - """노드 타입에 따른 요구 방향 결정""" - node = self.map.get_node(node_id) - if not node: - return AgvDirection.FORWARD - - if node.node_type == NodeType.CHARGING: - return AgvDirection.FORWARD # 충전기는 전진 도킹 - elif node.node_type in [NodeType.DOCKING, NodeType.LOADER, NodeType.UNLOADER]: - return AgvDirection.BACKWARD # 장비는 후진 도킹 - else: - # Navigation 노드는 방향 제약 없음 - 현재 방향 유지 - return None - - def determine_magnet_direction(self, from_node: str, junction: str, to_node: str) -> MagnetDirection: - """갈림길에서 마그넷 방향 결정""" - # 현재는 단순화 - 실제로는 맵 좌표 기반 각도 계산 필요 - junction_node = self.map.get_node(junction) - if not junction_node or not junction_node.is_junction(): - return MagnetDirection.STRAIGHT - - # 연결된 노드들 중에서 from_node를 제외한 나머지에서 to_node의 위치 결정 - other_nodes = [n for n in junction_node.connected_nodes if n != from_node] - - if to_node in other_nodes: - # 임시로 인덱스 기반으로 방향 결정 (실제로는 좌표 기반 각도 계산) - index = other_nodes.index(to_node) - if index == 0: - return MagnetDirection.STRAIGHT - elif index == 1: - return MagnetDirection.LEFT - else: - return MagnetDirection.RIGHT - - return MagnetDirection.STRAIGHT - - def _can_move_in_direction(self, from_node: str, to_node: str, direction: AgvDirection, came_from: str = None) -> bool: - """특정 방향으로 이동 가능한지 확인 - AGV 물리적 제약 반영""" - start_node = self.map.get_node(from_node) - if not start_node or to_node not in start_node.connected_nodes: - return False - - # came_from이 없으면 이동 가능 (첫 이동) - if not came_from: - return True - - # AGV 물리적 제약: 현재 모터 방향과 이동 가능한 노드의 관계 - if direction == AgvDirection.FORWARD: - # 전진: 왔던 방향이 아닌 다른 방향으로 갈 수 있음 - return to_node != came_from - else: # BACKWARD - # 후진: 왔던 방향으로만 갈 수 있음 - return to_node == came_from - - def _find_direct_path(self, start: str, target: str, current_direction: AgvDirection, came_from: str = None) -> PathResult: - """직진 경로 찾기 - 방향전환 없이 목표 도달 가능한지 확인""" - basic_path = self.map.find_shortest_path(start, target) - if not basic_path or len(basic_path) < 2: - return PathResult(False, [], 0, False, None, "경로를 찾을 수 없습니다") - - # 목표 노드의 도킹 방향 확인 - required_direction = self.get_required_direction(target) - - # 첫 번째 이동이 가능한지 확인 - first_move = basic_path[1] - if not self._can_move_in_direction(start, first_move, current_direction, came_from): - return PathResult(False, [], 0, False, None, "첫 번째 이동이 물리적으로 불가능합니다") - - # 전체 경로를 같은 방향으로 갈 수 있는지 확인 - path_steps = [] - current_dir = current_direction - - for i in range(len(basic_path) - 1): - from_node = basic_path[i] - to_node = basic_path[i + 1] - - step = PathStep(from_node, to_node, current_dir, MagnetDirection.STRAIGHT) - path_steps.append(step) - - # 최종 도킹 방향 확인 - if required_direction and current_dir != required_direction: - return PathResult(False, [], 0, False, None, f"도킹 방향 불일치: 필요={required_direction.value}, 현재={current_dir.value}") - - return PathResult(True, path_steps, len(path_steps), False, None, "성공") - - def find_turnaround_path(self, start: str, target: str, current_direction: AgvDirection, came_from: str = None) -> PathResult: - """AGV 경로 계산 - 정답 패턴 기반 접근""" - - # 특별한 경우들을 정답 패턴에 맞게 처리 - start_rfid = self.map.get_node(start).rfid_id if self.map.get_node(start) else start - target_rfid = self.map.get_node(target).rfid_id if self.map.get_node(target) else target - came_from_rfid = self.map.get_node(came_from).rfid_id if came_from and self.map.get_node(came_from) else came_from - - # Q1-1: 033→032(전진) 케이스 - if start_rfid == "032" and came_from_rfid == "033" and current_direction == AgvDirection.FORWARD: - return self._handle_q1_1_case(start, target, came_from) - - # Q1-2: 033→032(후진) 케이스 - elif start_rfid == "032" and came_from_rfid == "033" and current_direction == AgvDirection.BACKWARD: - return self._handle_q1_2_case(start, target, came_from) - - # Q2-1: 006→007(전진) 케이스 - elif start_rfid == "007" and came_from_rfid == "006" and current_direction == AgvDirection.FORWARD: - return self._handle_q2_1_case(start, target, came_from) - - # Q2-2: 006→007(후진) 케이스 - elif start_rfid == "007" and came_from_rfid == "006" and current_direction == AgvDirection.BACKWARD: - return self._handle_q2_2_case(start, target, came_from) - - # 기본 경우 - else: - return self._handle_general_case(start, target, current_direction, came_from) - - def _handle_q1_1_case(self, start: str, target: str, came_from: str) -> PathResult: - """Q1-1: 033→032(전진) 케이스 처리""" - target_rfid = self.map.get_node(target).rfid_id - - if target_rfid == "040": - # 032 ->(F) 031 ->(R) 032 -> 040 - return self._create_pattern_path([ - ("032", "031", AgvDirection.FORWARD), - ("031", "032", AgvDirection.BACKWARD), - ("032", "040", AgvDirection.BACKWARD) - ]) - elif target_rfid == "041": - # 032 ->(F) 040 ->(R) 032 -> 031 -> 041 - return self._create_pattern_path([ - ("032", "040", AgvDirection.FORWARD), - ("040", "032", AgvDirection.BACKWARD), - ("032", "031", AgvDirection.BACKWARD), - ("031", "041", AgvDirection.BACKWARD) - ]) - elif target_rfid == "019": - # 032 ->(B) 033 -> ... -> 012 -> 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019 - return self._create_special_019_path() - elif target_rfid == "015": - # 032 ->(B) 033 -> ... -> 012 -> 016 ->(F) -> 012 -> 013 -> 014 -> 015 - return self._create_special_015_path() - else: - # 008, 001, 011: 032 ->(B) 033 -> ... - basic_path = self.map.find_shortest_path(start, target) - if basic_path and len(basic_path) >= 2: - return self._create_straight_path(basic_path, AgvDirection.BACKWARD, target) - - return PathResult(False, [], 0, False, None, "Q1-1 케이스 처리 실패") - - def _create_pattern_path(self, pattern: List[Tuple[str, str, AgvDirection]]) -> PathResult: - """패턴 기반 경로 생성""" - steps = [] - for from_rfid, to_rfid, direction in pattern: - from_node = self.map.resolve_node_id(from_rfid) - to_node = self.map.resolve_node_id(to_rfid) - if from_node and to_node: - step = PathStep(from_node, to_node, direction, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), len(pattern) > 1, None, "패턴 경로 생성 성공") - - def _create_complex_pattern_path(self, base_path: List[str], turnaround_path: List[str], base_direction: AgvDirection, turnaround_direction: AgvDirection) -> PathResult: - """복잡한 패턴 경로 생성 (중간에 방향전환 포함)""" - steps = [] - - # 기본 경로 생성 - for i in range(len(base_path) - 1): - from_node = self.map.resolve_node_id(base_path[i]) - to_node = self.map.resolve_node_id(base_path[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, base_direction, MagnetDirection.STRAIGHT) - steps.append(step) - - # 방향전환 경로 생성 - for i in range(len(turnaround_path) - 1): - from_node = self.map.resolve_node_id(turnaround_path[i]) - to_node = self.map.resolve_node_id(turnaround_path[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, turnaround_direction, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), True, None, "복합 패턴 경로 생성 성공") - - def _create_special_019_path(self) -> PathResult: - """특별 처리: 032 ->(B) 033 -> ... -> 012 -> 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019""" - steps = [] - - # 기본 경로: 032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 - base_rfids = ["032", "033", "034", "035", "036", "037", "005", "004", "012", "013"] - for i in range(len(base_rfids) - 1): - from_node = self.map.resolve_node_id(base_rfids[i]) - to_node = self.map.resolve_node_id(base_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.BACKWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - # 방향전환: 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019 - # 첫 번째 단계만 FORWARD로 표시 (방향전환 표시를 위해) - turnaround_rfids = ["013", "012", "016", "017", "018", "019"] - from_node = self.map.resolve_node_id("013") - to_node = self.map.resolve_node_id("012") - if from_node and to_node: - # 013 -> 012 는 F로 표시 (방향전환 의미) - step = PathStep(from_node, to_node, AgvDirection.FORWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - # 나머지는 일반 FORWARD - for i in range(1, len(turnaround_rfids) - 1): - from_node = self.map.resolve_node_id(turnaround_rfids[i]) - to_node = self.map.resolve_node_id(turnaround_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.FORWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), True, self.map.resolve_node_id("013"), "019 특별 경로 생성 성공") - - def _create_special_015_path(self) -> PathResult: - """특별 처리: 032 ->(B) 033 -> ... -> 012 -> 016 ->(F) -> 012 -> 013 -> 014 -> 015""" - steps = [] - - # 기본 경로: 032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 - base_rfids = ["032", "033", "034", "035", "036", "037", "005", "004", "012", "016"] - for i in range(len(base_rfids) - 1): - from_node = self.map.resolve_node_id(base_rfids[i]) - to_node = self.map.resolve_node_id(base_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.BACKWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - # 방향전환: 016 ->(F) -> 012 -> 013 -> 014 -> 015 - # 첫 번째 단계만 FORWARD로 표시 (방향전환 표시를 위해) - turnaround_rfids = ["016", "012", "013", "014", "015"] - from_node = self.map.resolve_node_id("016") - to_node = self.map.resolve_node_id("012") - if from_node and to_node: - # 016 -> 012 는 F로 표시 (방향전환 의미) - step = PathStep(from_node, to_node, AgvDirection.FORWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - # 나머지는 일반 FORWARD - for i in range(1, len(turnaround_rfids) - 1): - from_node = self.map.resolve_node_id(turnaround_rfids[i]) - to_node = self.map.resolve_node_id(turnaround_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.FORWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), True, self.map.resolve_node_id("016"), "015 특별 경로 생성 성공") - - def _create_q1_2_special_008_path(self) -> PathResult: - """Q1-2 008: 032 ->(F) 033 -> ... -> 005 -> 004 ->(B) -> 005 -> 006 -> 007 -> 008""" - steps = [] - - # 기본 경로: 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 - base_rfids = ["032", "033", "034", "035", "036", "037", "005", "004"] - for i in range(len(base_rfids) - 1): - from_node = self.map.resolve_node_id(base_rfids[i]) - to_node = self.map.resolve_node_id(base_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.FORWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - # 방향전환: 004 ->(B) -> 005 -> 006 -> 007 -> 008 - turnaround_rfids = ["004", "005", "006", "007", "008"] - for i in range(len(turnaround_rfids) - 1): - from_node = self.map.resolve_node_id(turnaround_rfids[i]) - to_node = self.map.resolve_node_id(turnaround_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.BACKWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), True, self.map.resolve_node_id("004"), "Q1-2 008 특별 경로 생성 성공") - - def _create_q1_2_special_001_path(self) -> PathResult: - """Q1-2 001: 032 ->(F) 033 -> ... -> 005 -> 006 ->(B) -> 004 -> 003 -> 002 -> 001""" - steps = [] - - # 기본 경로: 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 - base_rfids = ["032", "033", "034", "035", "036", "037", "005", "006"] - for i in range(len(base_rfids) - 1): - from_node = self.map.resolve_node_id(base_rfids[i]) - to_node = self.map.resolve_node_id(base_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.FORWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - # 특별 처리: 006 ->(B) -> 004 (실제로는 006 -> 005 -> 004이지만 출력에서 단축) - from_node = self.map.resolve_node_id("006") - to_node = self.map.resolve_node_id("004") - step = PathStep(from_node, to_node, AgvDirection.BACKWARD, MagnetDirection.STRAIGHT) - step._is_direct_jump = True # 직접 점프 표시용 - steps.append(step) - - # 나머지 경로: 004 -> 003 -> 002 -> 001 - remaining_rfids = ["004", "003", "002", "001"] - for i in range(len(remaining_rfids) - 1): - from_node = self.map.resolve_node_id(remaining_rfids[i]) - to_node = self.map.resolve_node_id(remaining_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.BACKWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), True, self.map.resolve_node_id("006"), "Q1-2 001 특별 경로 생성 성공") - - def _create_q1_2_special_011_path(self) -> PathResult: - """Q1-2 011: 032 ->(F) 033 -> ... -> 005 -> 004 -> 003 ->(B) -> 004 -> 030 -> 009 -> 010 -> 011""" - steps = [] - - # 기본 경로: 032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 - base_rfids = ["032", "033", "034", "035", "036", "037", "005", "004", "003"] - for i in range(len(base_rfids) - 1): - from_node = self.map.resolve_node_id(base_rfids[i]) - to_node = self.map.resolve_node_id(base_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.FORWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - # 방향전환: 003 ->(B) -> 004 -> 030 -> 009 -> 010 -> 011 - turnaround_rfids = ["003", "004", "030", "009", "010", "011"] - for i in range(len(turnaround_rfids) - 1): - from_node = self.map.resolve_node_id(turnaround_rfids[i]) - to_node = self.map.resolve_node_id(turnaround_rfids[i + 1]) - if from_node and to_node: - step = PathStep(from_node, to_node, AgvDirection.BACKWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), True, self.map.resolve_node_id("003"), "Q1-2 011 특별 경로 생성 성공") - - def _handle_q1_2_case(self, start: str, target: str, came_from: str) -> PathResult: - """Q1-2: 033→032(후진) 케이스 처리""" - target_rfid = self.map.get_node(target).rfid_id - - if target_rfid == "040": - # 032 ->(F) 033 ->(R) 032 -> 040 - return self._create_pattern_path([ - ("032", "033", AgvDirection.FORWARD), - ("033", "032", AgvDirection.BACKWARD), - ("032", "040", AgvDirection.BACKWARD) - ]) - elif target_rfid == "041": - # 032 ->(R) 031 -> 041 (즉시 방향전환) - 특별 표시 - steps = [] - from_node = self.map.resolve_node_id("032") - to_node = self.map.resolve_node_id("031") - step = PathStep(from_node, to_node, AgvDirection.BACKWARD, MagnetDirection.STRAIGHT) - step._is_immediate_turn = True # 즉시 방향전환 마킹 - steps.append(step) - - from_node = self.map.resolve_node_id("031") - to_node = self.map.resolve_node_id("041") - step = PathStep(from_node, to_node, AgvDirection.BACKWARD, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), True, self.map.resolve_node_id("032"), "Q1-2 041 즉시 방향전환 성공") - elif target_rfid == "008": - # 032 ->(F) 033 -> ... -> 005 -> 004 ->(B) -> 005 -> 006 -> 007 -> 008 - return self._create_q1_2_special_008_path() - elif target_rfid == "001": - # 032 ->(F) 033 -> ... -> 005 -> 006 ->(B) -> 004 -> 003 -> 002 -> 001 - return self._create_q1_2_special_001_path() - elif target_rfid == "011": - # 032 ->(F) 033 -> ... -> 005 -> 004 -> 003 ->(B) -> 004 -> 030 -> 009 -> 010 -> 011 - return self._create_q1_2_special_011_path() - elif target_rfid == "019": - # 032 ->(F) 033 -> ... -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019 - basic_path = ["032", "033", "034", "035", "036", "037", "005", "004", "012", "016", "017", "018", "019"] - return self._create_straight_path(basic_path, AgvDirection.FORWARD, target) - elif target_rfid == "015": - # 032 ->(F) 033 -> ... -> 005 -> 004 -> 012 -> 013 -> 014 -> 015 - basic_path = ["032", "033", "034", "035", "036", "037", "005", "004", "012", "013", "014", "015"] - return self._create_straight_path(basic_path, AgvDirection.FORWARD, target) - else: - return self._handle_general_case(start, target, AgvDirection.BACKWARD, came_from) - - def _handle_q2_1_case(self, start: str, target: str, came_from: str) -> PathResult: - """Q2-1: 006→007(전진) 케이스 처리 - 임시 구현""" - return self._handle_general_case(start, target, AgvDirection.FORWARD, came_from) - - def _handle_q2_2_case(self, start: str, target: str, came_from: str) -> PathResult: - """Q2-2: 006→007(후진) 케이스 처리 - 임시 구현""" - return self._handle_general_case(start, target, AgvDirection.BACKWARD, came_from) - - def _handle_general_case(self, start: str, target: str, current_direction: AgvDirection, came_from: str = None) -> PathResult: - """일반적인 경우 처리""" - # 기본 최단 경로로 처리 - basic_path = self.map.find_shortest_path(start, target) - if not basic_path: - return PathResult(False, [], 0, False, None, "경로를 찾을 수 없습니다") - - return self._create_straight_path(basic_path, current_direction, target) - - def _get_first_valid_move(self, start: str, direction: AgvDirection, came_from: str = None) -> Optional[str]: - """현재 방향으로 이동 가능한 첫 번째 노드 반환""" - start_node = self.map.get_node(start) - if not start_node: - return None - - for connected_node in start_node.connected_nodes: - if self._can_move_in_direction(start, connected_node, direction, came_from): - return connected_node - return None - - def _create_straight_path(self, path: List[str], direction: AgvDirection, target: str) -> PathResult: - """직진 경로 생성""" - # 목표 도킹 방향 확인 - required_direction = self.get_required_direction(target) - if required_direction and direction != required_direction: - return PathResult(False, [], 0, False, None, f"도킹 방향 불일치: 현재={direction.value}, 필요={required_direction.value}") - - # 경로 단계 생성 - steps = [] - for i in range(len(path) - 1): - from_node = path[i] - to_node = path[i + 1] - step = PathStep(from_node, to_node, direction, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), False, None, "성공") - - def _create_detour_path(self, start: str, target: str, current_direction: AgvDirection, came_from: str) -> PathResult: - """우회 경로 생성 (방향전환 포함)""" - # 현재 방향으로 갈 수 있는 노드로 먼저 이동 - first_move = self._get_first_valid_move(start, current_direction, came_from) - if not first_move: - return PathResult(False, [], 0, False, None, "이동할 수 있는 노드가 없습니다") - - # 첫 번째 이동 - first_step = PathStep(start, first_move, current_direction, MagnetDirection.STRAIGHT) - - # 그 다음 노드에서 목표까지의 경로를 반대 방향으로 계산 - new_direction = AgvDirection.FORWARD if current_direction == AgvDirection.BACKWARD else AgvDirection.BACKWARD - - # 재귀 호출로 나머지 경로 계산 - remaining_result = self.find_turnaround_path(first_move, target, new_direction, start) - - if remaining_result.success: - all_steps = [first_step] + remaining_result.path_steps - return PathResult(True, all_steps, len(all_steps), True, first_move, "성공") - else: - return PathResult(False, [], 0, False, None, "우회 경로를 찾을 수 없습니다") - - def _find_turnaround_path(self, start: str, target: str, current_direction: AgvDirection, came_from: str = None) -> PathResult: - """방향전환을 통한 경로 계산""" - - # 목표 노드의 요구 방향 - required_direction = self.get_required_direction(target) - - # 현재 방향으로 갈 수 있는 인접 노드들 찾기 - start_node = self.map.get_node(start) - if not start_node: - return PathResult(False, [], 0, False, None, "시작 노드를 찾을 수 없습니다") - - possible_moves = [] - for connected_node in start_node.connected_nodes: - if self._can_move_in_direction(start, connected_node, current_direction, came_from): - possible_moves.append(connected_node) - - if not possible_moves: - return PathResult(False, [], 0, False, None, "현재 방향으로 이동할 수 있는 노드가 없습니다") - - # 각 가능한 이동에 대해 최적 경로 탐색 - best_result = None - min_distance = float('inf') - - for next_node in possible_moves: - # 이 노드에서 목표까지의 경로 계산 - result = self._explore_path_from_node(next_node, target, current_direction, start) - - if result.success and result.total_distance < min_distance: - min_distance = result.total_distance - # 첫 번째 단계 추가 - first_step = PathStep(start, next_node, current_direction, MagnetDirection.STRAIGHT) - all_steps = [first_step] + result.path_steps - best_result = PathResult(True, all_steps, len(all_steps), result.needs_turnaround, result.turnaround_junction, "성공") - - if best_result: - return best_result - else: - return PathResult(False, [], 0, False, None, "모든 경로에서 목표에 도달할 수 없습니다") - - def _explore_path_from_node(self, start: str, target: str, current_direction: AgvDirection, came_from: str) -> PathResult: - """특정 노드에서 목표까지의 최적 경로 탐색""" - - # 도착했으면 성공 - if start == target: - required_direction = self.get_required_direction(target) - if required_direction and current_direction != required_direction: - # 방향이 맞지 않으면 갈림길에서 방향전환 시도 - return self._try_direction_change_at_junction(start, current_direction, required_direction, came_from) - return PathResult(True, [], 0, False, None, "성공") - - # 직진 경로 시도 - direct_result = self._find_direct_path(start, target, current_direction, came_from) - if direct_result.success: - return direct_result - - # 갈림길에서 방향전환 시도 - start_node = self.map.get_node(start) - if start_node and start_node.is_junction(): - return self._try_direction_change_at_junction(start, current_direction, None, came_from) - - # 다음 노드로 이동해서 재탐색 - possible_moves = [] - for connected_node in start_node.connected_nodes: - if self._can_move_in_direction(start, connected_node, current_direction, came_from): - possible_moves.append(connected_node) - - best_result = None - min_distance = float('inf') - - for next_node in possible_moves: - result = self._explore_path_from_node(next_node, target, current_direction, start) - if result.success and result.total_distance < min_distance: - min_distance = result.total_distance - step = PathStep(start, next_node, current_direction, MagnetDirection.STRAIGHT) - all_steps = [step] + result.path_steps - best_result = PathResult(True, all_steps, len(all_steps), result.needs_turnaround, result.turnaround_junction, "성공") - - return best_result if best_result else PathResult(False, [], 0, False, None, "경로를 찾을 수 없습니다") - - def _try_direction_change_at_junction(self, junction: str, current_direction: AgvDirection, target_direction: AgvDirection, came_from: str) -> PathResult: - """갈림길에서 방향전환 시도""" - junction_node = self.map.get_node(junction) - if not junction_node or not junction_node.is_junction(): - return PathResult(False, [], 0, False, None, "갈림길이 아닙니다") - - # 방향전환을 위해 다른 노드로 이동 후 돌아오기 - available_nodes = [node for node in junction_node.connected_nodes if node != came_from] - - if len(available_nodes) == 0: - return PathResult(False, [], 0, False, None, "방향전환할 수 있는 노드가 없습니다") - - # 첫 번째 이용 가능한 노드로 방향전환 수행 - detour_node = available_nodes[0] - - # 4단계 방향전환: junction -> detour -> junction (반대방향) - new_direction = AgvDirection.FORWARD if current_direction == AgvDirection.BACKWARD else AgvDirection.BACKWARD - - steps = [ - PathStep(junction, detour_node, current_direction, MagnetDirection.STRAIGHT), - PathStep(detour_node, junction, new_direction, MagnetDirection.STRAIGHT) - ] - - return PathResult(True, steps, 2, True, junction, "방향전환 완료") - - def _find_nearest_junction(self, start: str, basic_path: List[str]) -> Optional[str]: - """기본 경로에서 가장 가까운 갈림길 찾기""" - # 기본 경로의 노드들 중 갈림길 찾기 - for node_id in basic_path: - node = self.map.get_node(node_id) - if node and node.is_junction(): - return node_id - - # 기본 경로에 갈림길이 없으면 전체 맵에서 가장 가까운 갈림길 찾기 - junctions = self.map.get_junctions() - if not junctions: - return None - - # 시작점에서 가장 가까운 갈림길 (BFS 거리 기준) - min_distance = float('inf') - nearest_junction = None - - for junction in junctions: - path_to_junction = self.map.find_shortest_path(start, junction.node_id) - if path_to_junction and len(path_to_junction) < min_distance: - min_distance = len(path_to_junction) - nearest_junction = junction.node_id - - return nearest_junction - - def _create_basic_path_result(self, path: List[str], direction: AgvDirection) -> PathResult: - """기본 경로 결과 생성 (방향 전환 없음)""" - path_steps = [] - - for i in range(len(path) - 1): - from_node = path[i] - to_node = path[i + 1] - - # 갈림길인지 확인하여 마그넷 방향 결정 - from_node_obj = self.map.get_node(from_node) - if from_node_obj and from_node_obj.is_junction() and i > 0: - prev_node = path[i - 1] - magnet_dir = self.determine_magnet_direction(prev_node, from_node, to_node) - else: - magnet_dir = MagnetDirection.STRAIGHT - - step = PathStep(from_node, to_node, direction, magnet_dir) - path_steps.append(step) - - return PathResult( - success=True, - path_steps=path_steps, - total_distance=len(path) - 1, - needs_turnaround=False, - turnaround_junction=None, - error_message=None - ) - - def _create_turnaround_path(self, start: str, target: str, current_dir: AgvDirection, - required_dir: AgvDirection, junction: str, came_from: str = None) -> PathResult: - """방향 전환 경로 생성 - AGV는 제자리 회전 불가, 반드시 다른 노드 경유""" - - # 시작점이 갈림길인 경우에도 방향 전환을 위해 다른 노드를 경유해야 함 - start_node = self.map.get_node(start) - if start_node and start_node.is_junction(): - # 목표로 가는 최단 경로 확인 - target_path = self.map.find_shortest_path(start, target) - if not target_path or len(target_path) < 2: - return PathResult(False, [], 0, True, start, "목표까지 경로를 찾을 수 없습니다") - - # 현재 방향으로 목표 방향의 첫 번째 노드로 이동하면 안됨 - # 방향 전환을 위해 다른 인접 노드를 먼저 방문 - next_target_node = target_path[1] - - # 방향 전환을 위한 인접 노드 선택 (목표 방향과 온 방향이 아닌 다른 노드) - available_detour_nodes = [node for node in start_node.connected_nodes - if node != next_target_node and node != came_from] - - if not available_detour_nodes: - return PathResult(False, [], 0, True, start, "방향 전환을 위한 우회 노드가 없습니다") - - detour_node = available_detour_nodes[0] - - path_steps = [] - - # 1. 우회 노드로 이동 (현재 방향) - step = PathStep(start, detour_node, current_dir, MagnetDirection.STRAIGHT) - path_steps.append(step) - - # 2. 갈림길로 복귀 (방향 전환됨) - step = PathStep(detour_node, start, required_dir, MagnetDirection.STRAIGHT) - path_steps.append(step) - - # 3. 목표까지 이동 (전환된 방향) - for i in range(1, len(target_path)): - from_node = target_path[i-1] - to_node = target_path[i] - step = PathStep(from_node, to_node, required_dir, MagnetDirection.STRAIGHT) - path_steps.append(step) - - return PathResult( - success=True, - path_steps=path_steps, - total_distance=len(path_steps), - needs_turnaround=True, - turnaround_junction=start, - error_message=None - ) - - # Phase 1: 갈림길까지 이동 - path_to_junction = self.map.find_shortest_path(start, junction) - if not path_to_junction: - return PathResult(False, [], 0, True, junction, "갈림길까지 경로를 찾을 수 없습니다") - - # Phase 2: 방향 전환을 위한 인접 노드 선택 - junction_node = self.map.get_node(junction) - if not junction_node: - return PathResult(False, [], 0, True, junction, "갈림길 노드를 찾을 수 없습니다") - - # 갈림길에서 목표로 가는 경로 - from_junction_to_target = self.map.find_shortest_path(junction, target) - if not from_junction_to_target or len(from_junction_to_target) < 2: - return PathResult(False, [], 0, True, junction, "갈림길에서 목표까지 경로를 찾을 수 없습니다") - - next_node_in_target_path = from_junction_to_target[1] - came_from_node = path_to_junction[-2] if len(path_to_junction) > 1 else None - - # 방향 전환을 위한 인접 노드 선택 (내가 온 방향과 목표 방향 제외) - available_nodes = [node for node in junction_node.connected_nodes - if node != came_from_node and node != next_node_in_target_path] - - if not available_nodes: - return PathResult(False, [], 0, True, junction, "방향 전환을 위한 인접 노드가 없습니다") - - detour_node = available_nodes[0] - - # Phase 3: 방향 전환 경로 구성 - path_steps = [] - - # 1. 갈림길까지 이동 (현재 방향) - for i in range(len(path_to_junction) - 1): - from_node = path_to_junction[i] - to_node = path_to_junction[i + 1] - step = PathStep(from_node, to_node, current_dir, MagnetDirection.STRAIGHT) - path_steps.append(step) - - # 2. 인접 노드로 이동 (현재 방향) - step = PathStep(junction, detour_node, current_dir, MagnetDirection.STRAIGHT) - path_steps.append(step) - - # 3. 갈림길로 복귀 (방향 전환됨) - step = PathStep(detour_node, junction, required_dir, MagnetDirection.STRAIGHT) - path_steps.append(step) - - # 4. 목표까지 이동 (전환된 방향) - for i in range(1, len(from_junction_to_target)): - from_node = from_junction_to_target[i-1] - to_node = from_junction_to_target[i] - step = PathStep(from_node, to_node, required_dir, MagnetDirection.STRAIGHT) - path_steps.append(step) - - return PathResult( - success=True, - path_steps=path_steps, - total_distance=len(path_steps), - needs_turnaround=True, - turnaround_junction=junction, - error_message=None - ) - -def run_comprehensive_test(): - """포괄적 테스트 - 모든 경우의 수""" - print("=== AGV PathFinder 포괄적 테스트 ===") - - # 맵 로드 - agv_map = AGVMap() - map_file = r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap" - - if not agv_map.load_from_file(map_file): - print("맵 로드 실패") - return - - # PathFinder 생성 - pathfinder = AGVPathfinder(agv_map) - - # 사용자 요청 테스트 케이스들 - test_scenarios = [ - # (came_from_rfid, current_rfid, direction, targets) - ("033", "032", AgvDirection.FORWARD, ["040", "041", "008", "001", "011", "019", "015"]), - ("033", "032", AgvDirection.BACKWARD, ["040", "041", "008", "001", "011", "019", "015"]), - ("006", "007", AgvDirection.FORWARD, ["040", "041", "008", "001", "011", "019", "015"]), - ("006", "007", AgvDirection.BACKWARD, ["040", "041", "008", "001", "011", "019", "015"]), - ("009", "010", AgvDirection.FORWARD, ["040", "041", "008", "001", "011", "019", "015"]), - ("009", "010", AgvDirection.BACKWARD, ["040", "041", "008", "001", "011", "019", "015"]), - ("013", "014", AgvDirection.FORWARD, ["040", "041", "008", "001", "011", "019", "015"]), - ("013", "014", AgvDirection.BACKWARD, ["040", "041", "008", "001", "011", "019", "015"]), - ] - - for came_from_rfid, current_rfid, direction, targets in test_scenarios: - print(f"\n" + "="*80) - print(f"시나리오: {came_from_rfid}→{current_rfid}({direction.value})") - print("="*80) - - # 현재 위치와 온 곳 확인 - current_pos = agv_map.resolve_node_id(current_rfid) - came_from_pos = agv_map.resolve_node_id(came_from_rfid) - - if not current_pos: - print(f"[ERROR] 현재 위치 {current_rfid} 를 찾을 수 없습니다") - continue - - for target_rfid in targets: - target_node_id = agv_map.resolve_node_id(target_rfid) - if not target_node_id: - print(f"[ERROR] 목표 {target_rfid} 를 찾을 수 없습니다") - continue - - print(f"\n목표: {target_rfid}") - result = pathfinder.find_turnaround_path(current_pos, target_node_id, direction, came_from_pos) - - if result.success: - # RFID 경로 추출 - rfid_path = [] - - # 시작점 추가 - start_node = agv_map.get_node(current_pos) - if start_node: - rfid_path.append(start_node.rfid_id) - - # 경로의 모든 목표 노드 추가 - for step in result.path_steps: - to_node = agv_map.get_node(step.to_node) - if to_node: - rfid_path.append(to_node.rfid_id) - - # 중복 제거하면서 순서 유지 - unique_rfid_path = [] - for rfid in rfid_path: - if rfid not in unique_rfid_path: - unique_rfid_path.append(rfid) - - # 모터 방향을 포함한 상세 경로 표시 - detailed_path = [] - for i, step in enumerate(result.path_steps): - from_node = agv_map.get_node(step.from_node) - to_node = agv_map.get_node(step.to_node) - from_rfid = from_node.rfid_id if from_node else step.from_node - to_rfid = to_node.rfid_id if to_node else step.to_node - direction_marker = "F" if step.motor_direction == AgvDirection.FORWARD else "B" - detailed_path.append(f"{from_rfid} →({direction_marker}) {to_rfid}") - - print(f" 상세경로: {' | '.join(detailed_path)}") - print(f" 요약경로: {' → '.join(unique_rfid_path)}") - print(f" 총 단계: {result.total_distance}") - if result.needs_turnaround: - junction_node = agv_map.get_node(result.turnaround_junction) - junction_rfid = junction_node.rfid_id if junction_node else result.turnaround_junction - print(f" 방향전환: {junction_rfid}") - else: - print(f" [FAILED] {result.error_message}") - -def test_pathfinder(): - """기본 테스트 함수""" - print("=== AGV PathFinder 기본 테스트 ===") - - # 맵 로드 - agv_map = AGVMap() - map_file = r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap" - - if not agv_map.load_from_file(map_file): - print("맵 로드 실패") - return - - # 갈림길 정보 출력 - junctions = agv_map.get_junctions() - print(f"\n갈림길 목록 ({len(junctions)}개):") - for junction in junctions: - print(f" {junction.node_id} ({junction.rfid_id}): {junction.connected_nodes}") - - # PathFinder 생성 - pathfinder = AGVPathfinder(agv_map) - - # 테스트 케이스들 (RFID 형식 사용) - test_cases = [ - # (start, target, current_direction, description) - ("007", "019", AgvDirection.FORWARD, "007→006→019 (직진 경로)"), - ("007", "001", AgvDirection.FORWARD, "007→006→001 (방향 전환 필요)"), - ("007", "011", AgvDirection.FORWARD, "007→006→011 (방향 전환 필요)"), - ("034", "040", AgvDirection.BACKWARD, "034→033→040 (방향 전환 필요)"), - ("031", "001", AgvDirection.BACKWARD, "031→032→001 (방향 전환 필요)"), - ] - - print(f"\n=== 기본 테스트 케이스 실행 ===") - for start, target, direction, description in test_cases: - print(f"\n테스트: {description}") - - # RFID를 NodeID로 변환 - start_node_id = agv_map.resolve_node_id(start) - target_node_id = agv_map.resolve_node_id(target) - - if not start_node_id or not target_node_id: - print(f"[FAILED] 노드를 찾을 수 없음: {start} → {target}") - continue - - print(f"시작: {start}({start_node_id}), 목표: {target}({target_node_id}), 현재방향: {direction.value}") - - result = pathfinder.find_turnaround_path(start_node_id, target_node_id, direction) - - if result.success: - print(f"[SUCCESS] 성공 (총 {result.total_distance}단계)") - if result.needs_turnaround: - print(f" 방향전환: {result.turnaround_junction}에서 수행") - - print(" 경로:") - for i, step in enumerate(result.path_steps, 1): - print(f" {i}. {step.to_rfid_string(agv_map)}") - else: - print(f"[FAILED] 실패: {result.error_message}") - -def test_q2_2(): - """Q2-2: 006→007(후진) 테스트""" - print("\n" + "="*80) - print("Q2-2 테스트: 006→007(후진)") - print("="*80) - - agv_map = AGVMap() - agv_map.load_from_file(r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap") - pathfinder = AGVPathfinder(agv_map) - - # 006→007(후진) 상태에서 각 목표로의 경로 - start_rfid = "007" - start_node_id = agv_map.resolve_node_id(start_rfid) - current_direction = AgvDirection.BACKWARD # 후진 상태 - came_from = agv_map.resolve_node_id("006") # 006에서 왔음 - - targets = ["040", "041", "008", "001", "011", "019", "015"] - - for target_rfid in targets: - target_node_id = agv_map.resolve_node_id(target_rfid) - if not target_node_id: - print(f"목표 {target_rfid}: 노드를 찾을 수 없음") - continue - - result = pathfinder.find_turnaround_path(start_node_id, target_node_id, current_direction, came_from) - - print(f"\n목표 {target_rfid}:") - if result.success: - # 상세경로 출력 (RFID 기반) - path_detail = " | ".join([f"{agv_map.get_node(step.from_node).rfid_id} →({step.motor_direction.value[0]}) {agv_map.get_node(step.to_node).rfid_id}" - for step in result.path_steps]) - print(f" 상세경로: {path_detail}") - - # 간단경로 출력 (RFID 기반) - first_rfid = agv_map.get_node(result.path_steps[0].from_node).rfid_id - path_rfids = [first_rfid] + [agv_map.get_node(step.to_node).rfid_id for step in result.path_steps] - rfid_path = " → ".join(path_rfids) - print(f" 간단경로: {rfid_path}") - print(f" 총 단계: {result.total_distance}") - - if result.needs_turnaround: - turnaround_rfid = agv_map.get_node(result.turnaround_junction).rfid_id - print(f" 방향전환: {turnaround_rfid}") - else: - print(f" 실패: {result.error_message}") - -def test_all_scenarios(): - """모든 정답 시나리오 검증""" - print("="*80) - print("전체 테스트 케이스 검증") - print("="*80) - - agv_map = AGVMap() - agv_map.load_from_file(r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap") - pathfinder = AGVPathfinder(agv_map) - - # 모든 테스트 케이스 정의 - test_scenarios = [ - { - "name": "Q1-1: 033→032(전진)", - "start": "032", "came_from": "033", "direction": AgvDirection.FORWARD, - "targets": { - "040": "032 ->(F) 031 ->(R) 032 -> 040", - "041": "032 ->(F) 040 ->(R) 032 -> 031 -> 041", - "008": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 -> 007 -> 008", - "001": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 -> 002 -> 001", - "011": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 ->(F) -> 012 -> 013 -> 014 -> 015" - } - }, - { - "name": "Q1-2: 033→032(후진)", - "start": "032", "came_from": "033", "direction": AgvDirection.BACKWARD, - "targets": { - "040": "032 ->(F) 033 ->(R) 032 -> 040", - "041": "032 ->(R) 031 -> 041", - "008": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 ->(B) -> 005 -> 006 -> 007 -> 008", - "001": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 ->(B) -> 004 -> 003 -> 002 -> 001", - "011": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 ->(B) -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 -> 014 -> 015" - } - }, - { - "name": "Q2-1: 006→007(전진)", - "start": "007", "came_from": "006", "direction": AgvDirection.FORWARD, - "targets": { - "040": "007 ->(B) 006 -> 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 040", - "041": "007 ->(B) 006 -> 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 031 -> 041", - "008": "007 ->(F) 006 -> 005 -> 037 ->(B) 005 -> 006 -> 007 -> 008", - "001": "007 ->(B) 006 -> 005 -> 004 -> 003 -> 002 -> 001", - "011": "007 ->(B) 006 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "007 ->(B) 006 -> 005 -> 004 -> 012 -> 013 ->(F) 012 -> 016 -> 017 -> 018 -> 019", - "015": "007 ->(B) 006 -> 005 -> 004 -> 012 -> 016 ->(F) 012 -> 013 -> 014 -> 015" - } - }, - { - "name": "Q2-2: 006→007(후진)", - "start": "007", "came_from": "006", "direction": AgvDirection.BACKWARD, - "targets": { - "040": "007 ->(F) 006 -> 005 -> 004 ->(B) 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 040", - "041": "007 ->(F) 006 -> 005 -> 004 ->(B) 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 031 -> 041", - "008": "007 ->(B) 008", - "001": "007 ->(F) 006 -> 005 -> 004 -> 030 ->(B) 004 -> 003 -> 002 -> 001", - "011": "007 ->(F) 006 -> 005 -> 004 -> 003 ->(B) 004 -> 030 -> 009 -> 010 -> 011", - "019": "007 ->(F) 006 -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "007 ->(F) 006 -> 005 -> 004 -> 012 -> 013 -> 014 -> 015" - } - } - ] - - for scenario in test_scenarios: - print(f"\n{scenario['name']}") - print("-" * 60) - - start_node_id = agv_map.resolve_node_id(scenario['start']) - came_from_node_id = agv_map.resolve_node_id(scenario['came_from']) - - for target_rfid, expected_path in scenario['targets'].items(): - target_node_id = agv_map.resolve_node_id(target_rfid) - - result = pathfinder.find_turnaround_path(start_node_id, target_node_id, scenario['direction'], came_from_node_id) - - print(f"\n목표 {target_rfid}:") - print(f" 정답: {expected_path}") - - if result.success: - # 내 결과 출력 (정답 형식에 맞게) - path_nodes = [] - path_directions = [] - - # 노드와 방향 정보 수집 - for i, step in enumerate(result.path_steps): - from_rfid = agv_map.get_node(step.from_node).rfid_id - to_rfid = agv_map.get_node(step.to_node).rfid_id - direction = step.motor_direction.value[0] - - if i == 0: - path_nodes.append(from_rfid) - path_nodes.append(to_rfid) - path_directions.append(direction) - - # 경로 문자열 구성 - path_detail = path_nodes[0] - for i in range(len(path_directions)): - next_node = path_nodes[i + 1] - - # 즉시 방향전환 검사 (첫 번째 단계에서) - if i == 0 and hasattr(result.path_steps[i], '_is_immediate_turn') and result.path_steps[i]._is_immediate_turn: - path_detail += f" ->(R) {next_node}" - # 방향 변경 확인 - elif i > 0 and path_directions[i] != path_directions[i-1]: - # 특별한 경우: F->B 전환은 (R)로 표시 (사용자 정답 형식) - if path_directions[i-1] == 'F' and path_directions[i] == 'B': - path_detail += f" ->(R) -> {next_node}" - else: - path_detail += f" ->({path_directions[i]}) -> {next_node}" - else: - direction_symbol = path_directions[i] - if i == 0: - path_detail += f" ->({direction_symbol}) {next_node}" - else: - path_detail += f" -> {next_node}" - print(f" 내결과: {path_detail}") - - # 일치여부 확인 - if path_detail in expected_path or expected_path in path_detail: - print(f" [OK] 일치") - else: - print(f" [ERROR] 불일치") - else: - print(f" [FAIL] 실패: {result.error_message}") - -def test_universal_algorithm(): - """범용 알고리즘 테스트""" - print("="*80) - print("범용 AGV PathFinder 테스트") - print("="*80) - - # 맵 로드 - agv_map = AGVMap() - agv_map.load_from_file(r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap") - - # 범용 경로 계산기 생성 - import sys - sys.path.append(".") - from universal_pathfinder import UniversalAGVPathfinder, UniversalPathFormatter - universal_pathfinder = UniversalAGVPathfinder(agv_map) - - # 모든 테스트 케이스 - test_scenarios = [ - { - "name": "Q1-1: 033→032(전진)", - "start": "032", "came_from": "033", "direction": AgvDirection.FORWARD, - "targets": { - "040": "032 ->(F) 031 ->(R) 032 -> 040", - "041": "032 ->(F) 040 ->(R) 032 -> 031 -> 041", - "008": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 -> 007 -> 008", - "001": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 -> 002 -> 001", - "011": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 ->(F) -> 012 -> 013 -> 014 -> 015" - } - }, - { - "name": "Q1-2: 033→032(후진)", - "start": "032", "came_from": "033", "direction": AgvDirection.BACKWARD, - "targets": { - "040": "032 ->(F) 033 ->(R) 032 -> 040", - "041": "032 ->(R) 031 -> 041", - "008": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 ->(B) -> 005 -> 006 -> 007 -> 008", - "001": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 ->(B) -> 004 -> 003 -> 002 -> 001", - "011": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 ->(B) -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 -> 014 -> 015" - } - } - ] - - success_count = 0 - total_count = 0 - - for scenario in test_scenarios: - print(f"\n{scenario['name']}") - print("-" * 60) - - for target_rfid, expected_path in scenario["targets"].items(): - total_count += 1 - - # 범용 경로 계산 - result = universal_pathfinder.find_path( - scenario["start"], - target_rfid, - scenario["direction"], - scenario["came_from"] - ) - - print(f"\n목표 {target_rfid}:") - print(f" 정답: {expected_path}") - - if result.success: - # 범용 포맷터로 출력 생성 - calculated_path = UniversalPathFormatter.format_path(result, agv_map) - print(f" 결과: {calculated_path}") - - # 일치 여부 확인 - if calculated_path == expected_path: - print(f" [OK] 완전 일치") - success_count += 1 - else: - print(f" [ERROR] 불일치") - else: - print(f" [FAIL] 실패: {result.error_message}") - - print(f"\n" + "="*80) - print(f"전체 결과: {success_count}/{total_count} ({success_count/total_count*100:.1f}%) 성공") - print("="*80) - -if __name__ == "__main__": - test_universal_algorithm() \ No newline at end of file diff --git a/Cs_HMI/PathLogic/debug_015.py b/Cs_HMI/PathLogic/debug_015.py deleted file mode 100644 index 2d62abc..0000000 --- a/Cs_HMI/PathLogic/debug_015.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from universal_pathfinder import UniversalAGVPathfinder, UniversalPathFormatter -from agv_pathfinder import AGVMap, AgvDirection - -def test_015_only(): - """015 케이스만 테스트""" - print("=== 015 Case Debug ===") - - # 맵 로드 - agv_map = AGVMap() - agv_map.load_from_file(r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap") - - pathfinder = UniversalAGVPathfinder(agv_map) - - # 015 케이스 - print("\n--- Testing: 033->032(F) to 015 ---") - result = pathfinder.find_path(start_rfid="032", target_rfid="015", current_direction=AgvDirection.FORWARD, came_from_rfid="033") - - if result.success: - print(f"Steps count: {len(result.path_steps)}") - for i, step in enumerate(result.path_steps): - from_rfid = agv_map.get_node(step.from_node).rfid_id - to_rfid = agv_map.get_node(step.to_node).rfid_id - print(f" Step {i}: {from_rfid} -> {to_rfid} ({step.motor_direction.value})") - - actual_path = UniversalPathFormatter.format_path(result, agv_map) - print(f"\n실제: {actual_path}") - - expected = "032 ->(F) 031 ->(R) -> 032 -> 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 ->(R) -> 012 -> 030 -> 029 -> 028 -> 027 -> 026 -> 025 -> 024 -> 023 -> 022 -> 021 -> 020 -> 014 -> 015" - print(f"정답: {expected}") - - print(f"\n매치: {actual_path == expected}") - else: - print(f"FAILED: {result.error_message}") - -if __name__ == "__main__": - test_015_only() \ No newline at end of file diff --git a/Cs_HMI/PathLogic/debug_universal.py b/Cs_HMI/PathLogic/debug_universal.py deleted file mode 100644 index ac4620c..0000000 --- a/Cs_HMI/PathLogic/debug_universal.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from universal_pathfinder import UniversalAGVPathfinder, UniversalPathFormatter -from agv_pathfinder import AGVMap, AgvDirection - -def test_q1_1_all(): - """Q1-1 전체 케이스 테스트""" - print("=== Q1-1 All Cases Test ===") - - # 맵 로드 - agv_map = AGVMap() - agv_map.load_from_file(r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap") - - pathfinder = UniversalAGVPathfinder(agv_map) - - # Q1-1 정답들 (실제 사용자가 제공한 정답) - expected_answers = { - "040": "032 ->(F) 031 ->(R) 032 -> 040", - "041": "032 ->(F) 040 ->(R) 032 -> 031 -> 041", - "008": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 -> 007 -> 008", - "001": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 -> 002 -> 001", - "011": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 ->(F) -> 012 -> 013 -> 014 -> 015" - } - - print(f"\nQ1-1 시나리오: 033->032(F) 상황에서의 목적지별 경로") - success_count = 0 - - for target, expected in expected_answers.items(): - print(f"\n--- Target: {target} ---") - result = pathfinder.find_path(start_rfid="032", target_rfid=target, current_direction=AgvDirection.FORWARD, came_from_rfid="033") - - if target == "015": # 015 케이스 디버깅 - print(f"DEBUG - Steps count: {len(result.path_steps)}") - for i, step in enumerate(result.path_steps): - from_rfid = agv_map.get_node(step.from_node).rfid_id - to_rfid = agv_map.get_node(step.to_node).rfid_id - print(f" Step {i}: {from_rfid} -> {to_rfid} ({step.motor_direction.value})") - print() - - if result.success: - actual_path = UniversalPathFormatter.format_path(result, agv_map) - print(f"실제: {actual_path}") - print(f"정답: {expected}") - - if actual_path == expected: - print("[SUCCESS]") - success_count += 1 - else: - print("[FAILED] - Path mismatch") - else: - print(f"[FAILED]: {result.error_message}") - - print(f"\n=== Q1-1 결과: {success_count}/7 성공 ===") - return success_count == 7 - -if __name__ == "__main__": - test_q1_1_all() \ No newline at end of file diff --git a/Cs_HMI/PathLogic/packages.config b/Cs_HMI/PathLogic/packages.config deleted file mode 100644 index 8b1a8d0..0000000 --- a/Cs_HMI/PathLogic/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Cs_HMI/PathLogic/show_map_info.py b/Cs_HMI/PathLogic/show_map_info.py deleted file mode 100644 index bac9025..0000000 --- a/Cs_HMI/PathLogic/show_map_info.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from agv_pathfinder import AGVMap - -def show_map_info(): - """맵 정보 출력: RFID 목록과 연결 정보""" - print("=== AGV 맵 정보 ===") - - # 맵 로드 - agv_map = AGVMap() - agv_map.load_from_file(r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap") - - print(f"총 노드 수: {len(agv_map.nodes)}") - - # RFID 목록 수집 및 정렬 - rfid_nodes = [] - for node in agv_map.nodes.values(): - if node.rfid_id: - rfid_nodes.append((node.rfid_id, node)) - - # RFID 번호순으로 정렬 - rfid_nodes.sort(key=lambda x: int(x[0]) if x[0].isdigit() else 999) - - print(f"RFID 노드 수: {len(rfid_nodes)}") - - # RFID 목록 출력 (10개씩 줄바꿈) - print("\n--- RFID 목록 ---") - rfid_list = [rfid for rfid, _ in rfid_nodes] - for i, rfid in enumerate(rfid_list): - if i % 10 == 0: - print() # 10개마다 줄바꿈 - print(f"{rfid:>3}", end=" ") - print() # 마지막 줄바꿈 - - # 연결 정보 출력 - print("\n--- RFID 연결 정보 ---") - for rfid, node in rfid_nodes: - # 노드 타입 정보 - type_str = "" - if node.node_type == 2: - type_str = f"[Station/Buffer, DockDir:{node.dock_direction}]" - elif node.node_type == 3: - type_str = f"[Charger, DockDir:{node.dock_direction}]" - else: - type_str = f"[Type:{node.node_type}]" - - # 연결된 노드들의 RFID 찾기 - connected_rfids = [] - for connected_node in node.connected_nodes: - connected_node_obj = agv_map.nodes.get(connected_node) - if connected_node_obj and connected_node_obj.rfid_id: - connected_rfids.append(connected_node_obj.rfid_id) - - # RFID 번호순으로 정렬 - connected_rfids.sort(key=lambda x: int(x) if x.isdigit() else 999) - connected_str = " -> ".join(connected_rfids) if connected_rfids else "없음" - - print(f"RFID {rfid:>3} {type_str:<25} Pos:({node.position[0]:>4},{node.position[1]:>4}) -> {connected_str}") - - # 갈림길(Junction) 정보 - print("\n--- 갈림길 정보 (연결 노드 3개 이상) ---") - junctions = [] - for rfid, node in rfid_nodes: - connection_count = len([n for n in node.connected_nodes if agv_map.nodes.get(n)]) - if connection_count >= 3: - junctions.append(rfid) - - print(f"갈림길 RFID ({len(junctions)}개): {', '.join(junctions)}") - - # 특별 노드 분류 - print("\n--- 노드 타입별 분류 ---") - chargers = [] - stations = [] - normal = [] - - for rfid, node in rfid_nodes: - if node.node_type == 3: - chargers.append(rfid) - elif node.node_type == 2: - stations.append(rfid) - else: - normal.append(rfid) - - print(f"충전기 ({len(chargers)}개): {', '.join(chargers)}") - print(f"스테이션/버퍼 ({len(stations)}개): {', '.join(stations)}") - print(f"일반 노드 ({len(normal)}개): {', '.join(normal)}") - -if __name__ == "__main__": - show_map_info() \ No newline at end of file diff --git a/Cs_HMI/PathLogic/test_all_scenarios.py b/Cs_HMI/PathLogic/test_all_scenarios.py deleted file mode 100644 index 8f9b60e..0000000 --- a/Cs_HMI/PathLogic/test_all_scenarios.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from universal_pathfinder import UniversalAGVPathfinder, UniversalPathFormatter -from agv_pathfinder import AGVMap, AgvDirection - -def test_all_scenarios(): - """전체 28개 테스트 케이스 검증""" - print("="*80) - print("전체 28개 테스트 케이스 검증") - print("="*80) - - # 맵 로드 - agv_map = AGVMap() - agv_map.load_from_file(r"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap") - - pathfinder = UniversalAGVPathfinder(agv_map) - - # 모든 테스트 케이스 정의 (사용자 제공 정답) - test_scenarios = [ - { - "name": "Q1-1: 033→032(전진)", - "start": "032", "came_from": "033", "direction": AgvDirection.FORWARD, - "targets": { - "040": "032 ->(F) 031 ->(R) 032 -> 040", - "041": "032 ->(F) 040 ->(R) 032 -> 031 -> 041", - "008": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 -> 007 -> 008", - "001": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 -> 002 -> 001", - "011": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 ->(F) -> 012 -> 013 -> 014 -> 015" - } - }, - { - "name": "Q1-2: 033→032(후진)", - "start": "032", "came_from": "033", "direction": AgvDirection.BACKWARD, - "targets": { - "040": "032 ->(F) 033 ->(R) 032 -> 040", - "041": "032 ->(R) 031 -> 041", - "008": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 ->(B) -> 005 -> 006 -> 007 -> 008", - "001": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 ->(B) -> 004 -> 003 -> 002 -> 001", - "011": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 ->(B) -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "032 ->(F) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 012 -> 013 -> 014 -> 015" - } - }, - { - "name": "Q2-1: 006→007(전진)", - "start": "007", "came_from": "006", "direction": AgvDirection.FORWARD, - "targets": { - "040": "007 ->(B) 006 -> 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 040", - "041": "007 ->(B) 006 -> 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 031 -> 041", - "008": "007 ->(F) 006 -> 005 -> 037 ->(B) 005 -> 006 -> 007 -> 008", - "001": "007 ->(B) 006 -> 005 -> 004 -> 003 -> 002 -> 001", - "011": "007 ->(B) 006 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011", - "019": "007 ->(B) 006 -> 005 -> 004 -> 012 -> 013 ->(F) 012 -> 016 -> 017 -> 018 -> 019", - "015": "007 ->(B) 006 -> 005 -> 004 -> 012 -> 016 ->(F) 012 -> 013 -> 014 -> 015" - } - }, - { - "name": "Q2-2: 006→007(후진)", - "start": "007", "came_from": "006", "direction": AgvDirection.BACKWARD, - "targets": { - "040": "007 ->(F) 006 -> 005 -> 004 ->(B) 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 040", - "041": "007 ->(F) 006 -> 005 -> 004 ->(B) 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 031 -> 041", - "008": "007 ->(B) 008", - "001": "007 ->(F) 006 -> 005 -> 004 -> 030 ->(B) 004 -> 003 -> 002 -> 001", - "011": "007 ->(F) 006 -> 005 -> 004 -> 003 ->(B) 004 -> 030 -> 009 -> 010 -> 011", - "019": "007 ->(F) 006 -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019", - "015": "007 ->(F) 006 -> 005 -> 004 -> 012 -> 013 -> 014 -> 015" - } - } - ] - - total_tests = 0 - total_success = 0 - - for scenario in test_scenarios: - print(f"\n{scenario['name']}") - print("-" * 60) - - scenario_success = 0 - scenario_total = len(scenario['targets']) - - for target_rfid, expected_path in scenario['targets'].items(): - total_tests += 1 - - print(f"\n목표 {target_rfid}:") - print(f" 정답: {expected_path}") - - result = pathfinder.find_path( - start_rfid=scenario['start'], - target_rfid=target_rfid, - current_direction=scenario['direction'], - came_from_rfid=scenario['came_from'] - ) - - if result.success: - actual_path = UniversalPathFormatter.format_path(result, agv_map) - print(f" 실제: {actual_path}") - - if actual_path == expected_path: - print(" [SUCCESS]") - scenario_success += 1 - total_success += 1 - else: - print(" [FAILED] - Path mismatch") - else: - print(f" [FAILED]: {result.error_message}") - - print(f"\n{scenario['name']} 결과: {scenario_success}/{scenario_total} 성공") - - print(f"\n{'='*80}") - print(f"전체 결과: {total_success}/{total_tests} 성공 ({total_success/total_tests*100:.1f}%)") - print(f"{'='*80}") - - if total_success == total_tests: - print("*** 모든 테스트 케이스 성공! 범용 알고리즘 완성! ***") - else: - print(f"*** {total_tests - total_success}개 케이스 실패. 추가 수정 필요. ***") - - return total_success == total_tests - -if __name__ == "__main__": - test_all_scenarios() \ No newline at end of file diff --git a/Cs_HMI/PathLogic/universal_pathfinder.py b/Cs_HMI/PathLogic/universal_pathfinder.py deleted file mode 100644 index 861bc44..0000000 --- a/Cs_HMI/PathLogic/universal_pathfinder.py +++ /dev/null @@ -1,612 +0,0 @@ -#!/usr/bin/env python3 -""" -범용 AGV PathFinder - 100% 정확도 달성 -모든 케이스를 일관된 로직으로 처리 -""" - -import json -from typing import List, Dict, Optional, Tuple -from dataclasses import dataclass -from agv_pathfinder import AgvDirection, MagnetDirection, PathStep, PathResult - -class UniversalAGVPathfinder: - """범용 AGV 경로 계산기""" - - def __init__(self, map_data): - self.map = map_data - - def find_path(self, start_rfid: str, target_rfid: str, current_direction: AgvDirection, came_from_rfid: str = None) -> PathResult: - """통합 경로 계산 - 모든 케이스 100% 정확도""" - - # 모든 케이스가 정답 패턴을 따르므로 바로 시나리오별 처리 - return self._create_turnaround_path(start_rfid, target_rfid, current_direction, None, came_from_rfid) - - def _get_required_direction(self, target_rfid: str) -> AgvDirection: - """목표 노드의 요구 방향 결정""" - # 모든 정답을 분석해보니 대부분의 경우 현재 방향과 관계없이 특정 패턴을 따름 - # 실제로는 시나리오별로 정답이 정해져 있으므로 항상 방향전환이 필요하다고 가정 - return AgvDirection.FORWARD # 기본값 - - def _create_straight_path(self, path_nodes: List[str], direction: AgvDirection) -> PathResult: - """직진 경로 생성""" - steps = [] - - for i in range(len(path_nodes) - 1): - from_node = path_nodes[i] - to_node = path_nodes[i + 1] - step = PathStep(from_node, to_node, direction, MagnetDirection.STRAIGHT) - steps.append(step) - - return PathResult(True, steps, len(steps), False, None, "직진 경로 생성 성공") - - def _create_turnaround_path(self, start_rfid: str, target_rfid: str, current_dir: AgvDirection, required_dir: AgvDirection, came_from_rfid: str) -> PathResult: - """방향전환 경로 생성 - 정답 패턴 기반""" - - - # Q1-1: 033->032(F) 시나리오 - if came_from_rfid == "033" and start_rfid == "032" and current_dir == AgvDirection.FORWARD: - return self._handle_q1_1_scenario(target_rfid) - - # Q1-2: 033->032(B) 시나리오 - elif came_from_rfid == "033" and start_rfid == "032" and current_dir == AgvDirection.BACKWARD: - return self._handle_q1_2_scenario(target_rfid) - - # Q2-1: 006->007(F) 시나리오 - elif came_from_rfid == "006" and start_rfid == "007" and current_dir == AgvDirection.FORWARD: - return self._handle_q2_1_scenario(target_rfid) - - # Q2-2: 006->007(B) 시나리오 - elif came_from_rfid == "006" and start_rfid == "007" and current_dir == AgvDirection.BACKWARD: - return self._handle_q2_2_scenario(target_rfid) - - else: - # 일반적인 방향전환 로직 - return self._handle_general_turnaround(start_rfid, target_rfid, current_dir, required_dir) - - def _handle_q1_1_scenario(self, target_rfid: str) -> PathResult: - """Q1-1: 033->032(전진) 케이스 처리""" - - # Q1-1 정답 패턴 - 실제 정답에 맞게 수정 - q1_1_patterns = { - "040": [ - # 정답: "032 ->(F) 031 ->(R) 032 -> 040" - ("032", "031", AgvDirection.FORWARD), - ("031", "032", AgvDirection.FORWARD), - ("032", "040", AgvDirection.FORWARD) - ], - "041": [ - # 정답: "032 ->(F) 040 ->(R) 032 -> 031 -> 041" - ("032", "040", AgvDirection.FORWARD), - ("040", "032", AgvDirection.FORWARD), - ("032", "031", AgvDirection.FORWARD), - ("031", "041", AgvDirection.FORWARD) - ], - "008": [ - # 정답: "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 006 -> 007 -> 008" - ("032", "033", AgvDirection.BACKWARD), - ("033", "034", AgvDirection.BACKWARD), - ("034", "035", AgvDirection.BACKWARD), - ("035", "036", AgvDirection.BACKWARD), - ("036", "037", AgvDirection.BACKWARD), - ("037", "005", AgvDirection.BACKWARD), - ("005", "006", AgvDirection.BACKWARD), - ("006", "007", AgvDirection.BACKWARD), - ("007", "008", AgvDirection.BACKWARD) - ], - "001": [ - # 정답: "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 003 -> 002 -> 001" - ("032", "033", AgvDirection.BACKWARD), - ("033", "034", AgvDirection.BACKWARD), - ("034", "035", AgvDirection.BACKWARD), - ("035", "036", AgvDirection.BACKWARD), - ("036", "037", AgvDirection.BACKWARD), - ("037", "005", AgvDirection.BACKWARD), - ("005", "004", AgvDirection.BACKWARD), - ("004", "003", AgvDirection.BACKWARD), - ("003", "002", AgvDirection.BACKWARD), - ("002", "001", AgvDirection.BACKWARD) - ], - "011": [ - # 정답: "032 ->(B) 033 -> 034 -> 035 -> 036 -> 037 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011" - ("032", "033", AgvDirection.BACKWARD), - ("033", "034", AgvDirection.BACKWARD), - ("034", "035", AgvDirection.BACKWARD), - ("035", "036", AgvDirection.BACKWARD), - ("036", "037", AgvDirection.BACKWARD), - ("037", "005", AgvDirection.BACKWARD), - ("005", "004", AgvDirection.BACKWARD), - ("004", "030", AgvDirection.BACKWARD), - ("030", "009", AgvDirection.BACKWARD), - ("009", "010", AgvDirection.BACKWARD), - ("010", "011", AgvDirection.BACKWARD) - ], - "019": [ - # 정답: "032 ->(B) 033 -> ... -> 012 -> 013 ->(F) -> 012 -> 016 -> 017 -> 018 -> 019" - ("032", "033", AgvDirection.BACKWARD), - ("033", "034", AgvDirection.BACKWARD), - ("034", "035", AgvDirection.BACKWARD), - ("035", "036", AgvDirection.BACKWARD), - ("036", "037", AgvDirection.BACKWARD), - ("037", "005", AgvDirection.BACKWARD), - ("005", "004", AgvDirection.BACKWARD), - ("004", "012", AgvDirection.BACKWARD), - ("012", "013", AgvDirection.BACKWARD), - # 013에서 방향전환 - ("013", "012", AgvDirection.FORWARD), - ("012", "016", AgvDirection.FORWARD), - ("016", "017", AgvDirection.FORWARD), - ("017", "018", AgvDirection.FORWARD), - ("018", "019", AgvDirection.FORWARD) - ], - "015": [ - # 정답: 032 ->(B) 033 -> ... -> 012 -> 016 ->(F) -> 012 -> 013 -> 014 -> 015 - ("032", "033", AgvDirection.BACKWARD), - ("033", "034", AgvDirection.BACKWARD), - ("034", "035", AgvDirection.BACKWARD), - ("035", "036", AgvDirection.BACKWARD), - ("036", "037", AgvDirection.BACKWARD), - ("037", "005", AgvDirection.BACKWARD), - ("005", "004", AgvDirection.BACKWARD), - ("004", "012", AgvDirection.BACKWARD), - ("012", "016", AgvDirection.BACKWARD), - # 016에서 방향전환하여 012로 돌아가 013 → 014 → 015 - ("016", "012", AgvDirection.FORWARD), - ("012", "013", AgvDirection.FORWARD), - ("013", "014", AgvDirection.FORWARD), - ("014", "015", AgvDirection.FORWARD) - ] - } - - if target_rfid not in q1_1_patterns: - return PathResult(False, [], 0, False, None, f"Q1-1 패턴 없음: {target_rfid}") - - pattern = q1_1_patterns[target_rfid] - steps = [] - - for i, (from_rfid, to_rfid, direction) in enumerate(pattern): - from_node = self.map.resolve_node_id(from_rfid) - to_node = self.map.resolve_node_id(to_rfid) - - if from_node and to_node: - step = PathStep(from_node, to_node, direction, MagnetDirection.STRAIGHT) - - # Q1-1 방향전환 지점 마킹 - if (from_rfid == "031" and to_rfid == "032") or \ - (from_rfid == "040" and to_rfid == "032") or \ - (from_rfid == "013" and to_rfid == "012") or \ - (from_rfid == "016" and to_rfid == "012"): - step._is_turnaround_point = True - - steps.append(step) - - # 방향전환 지점 찾기 (019, 015의 경우) - turnaround_junction = None - if target_rfid == "019": - turnaround_junction = self.map.resolve_node_id("013") - elif target_rfid == "015": - turnaround_junction = self.map.resolve_node_id("016") - elif target_rfid in ["040", "041"]: - # 040: 031에서 방향전환, 041: 040에서 방향전환 - turnaround_junction = self.map.resolve_node_id("031" if target_rfid == "040" else "040") - - needs_turnaround = turnaround_junction is not None - - return PathResult(True, steps, len(steps), needs_turnaround, turnaround_junction, f"Q1-1 {target_rfid} 성공") - - def _handle_q1_2_scenario(self, target_rfid: str) -> PathResult: - """Q1-2: 033->032(후진) 케이스 처리""" - - # 정답 패턴 매핑 - q1_2_patterns = { - "040": [ - ("032", "033", AgvDirection.FORWARD), - ("033", "032", AgvDirection.BACKWARD), - ("032", "040", AgvDirection.BACKWARD) - ], - "041": [ - ("032", "031", AgvDirection.BACKWARD), - ("031", "041", AgvDirection.BACKWARD) - ], - "008": [ - # 전진 부분 - ("032", "033", AgvDirection.FORWARD), - ("033", "034", AgvDirection.FORWARD), - ("034", "035", AgvDirection.FORWARD), - ("035", "036", AgvDirection.FORWARD), - ("036", "037", AgvDirection.FORWARD), - ("037", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - # 방향전환 부분 - ("004", "005", AgvDirection.BACKWARD), - ("005", "006", AgvDirection.BACKWARD), - ("006", "007", AgvDirection.BACKWARD), - ("007", "008", AgvDirection.BACKWARD) - ], - "001": [ - # 전진 부분 - ("032", "033", AgvDirection.FORWARD), - ("033", "034", AgvDirection.FORWARD), - ("034", "035", AgvDirection.FORWARD), - ("035", "036", AgvDirection.FORWARD), - ("036", "037", AgvDirection.FORWARD), - ("037", "005", AgvDirection.FORWARD), - ("005", "006", AgvDirection.FORWARD), - # 직접 점프 (사용자 정답 패턴) - ("006", "004", AgvDirection.BACKWARD), # 실제로는 006->005->004 - ("004", "003", AgvDirection.BACKWARD), - ("003", "002", AgvDirection.BACKWARD), - ("002", "001", AgvDirection.BACKWARD) - ], - "011": [ - # 전진 부분 - ("032", "033", AgvDirection.FORWARD), - ("033", "034", AgvDirection.FORWARD), - ("034", "035", AgvDirection.FORWARD), - ("035", "036", AgvDirection.FORWARD), - ("036", "037", AgvDirection.FORWARD), - ("037", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "003", AgvDirection.FORWARD), - # 방향전환 부분 - ("003", "004", AgvDirection.BACKWARD), - ("004", "030", AgvDirection.BACKWARD), - ("030", "009", AgvDirection.BACKWARD), - ("009", "010", AgvDirection.BACKWARD), - ("010", "011", AgvDirection.BACKWARD) - ], - "019": [ - ("032", "033", AgvDirection.FORWARD), - ("033", "034", AgvDirection.FORWARD), - ("034", "035", AgvDirection.FORWARD), - ("035", "036", AgvDirection.FORWARD), - ("036", "037", AgvDirection.FORWARD), - ("037", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "012", AgvDirection.FORWARD), - ("012", "016", AgvDirection.FORWARD), - ("016", "017", AgvDirection.FORWARD), - ("017", "018", AgvDirection.FORWARD), - ("018", "019", AgvDirection.FORWARD) - ], - "015": [ - ("032", "033", AgvDirection.FORWARD), - ("033", "034", AgvDirection.FORWARD), - ("034", "035", AgvDirection.FORWARD), - ("035", "036", AgvDirection.FORWARD), - ("036", "037", AgvDirection.FORWARD), - ("037", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "012", AgvDirection.FORWARD), - ("012", "013", AgvDirection.FORWARD), - ("013", "014", AgvDirection.FORWARD), - ("014", "015", AgvDirection.FORWARD) - ] - } - - if target_rfid not in q1_2_patterns: - return PathResult(False, [], 0, False, None, f"Q1-2 패턴 없음: {target_rfid}") - - pattern = q1_2_patterns[target_rfid] - steps = [] - - for from_rfid, to_rfid, direction in pattern: - from_node = self.map.resolve_node_id(from_rfid) - to_node = self.map.resolve_node_id(to_rfid) - if from_node and to_node: - step = PathStep(from_node, to_node, direction, MagnetDirection.STRAIGHT) - # 특별 표시 - if target_rfid == "041" and from_rfid == "032" and to_rfid == "031": - step._is_immediate_turn = True # 즉시 방향전환 - elif target_rfid == "001" and from_rfid == "006" and to_rfid == "004": - step._is_direct_jump = True # 직접 점프 - steps.append(step) - - # 방향전환 지점 결정 - turnaround_junction = None - if target_rfid == "040": - turnaround_junction = self.map.resolve_node_id("033") - elif target_rfid == "041": - turnaround_junction = self.map.resolve_node_id("032") # 즉시 방향전환 - elif target_rfid == "008": - turnaround_junction = self.map.resolve_node_id("004") - elif target_rfid == "001": - turnaround_junction = self.map.resolve_node_id("006") - elif target_rfid == "011": - turnaround_junction = self.map.resolve_node_id("003") - - needs_turnaround = turnaround_junction is not None - - return PathResult(True, steps, len(steps), needs_turnaround, turnaround_junction, f"Q1-2 {target_rfid} 성공") - - def _handle_q2_1_scenario(self, target_rfid: str) -> PathResult: - """Q2-1: 006->007(전진) 케이스 처리""" - - # Q2-1 패턴 정의 (006->007 전진에서 각 목표까지) - q2_1_patterns = { - "040": [ - # 정답: "007 ->(B) 006 -> 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 040" - ("007", "006", AgvDirection.BACKWARD), - ("006", "005", AgvDirection.BACKWARD), - ("005", "037", AgvDirection.BACKWARD), - ("037", "036", AgvDirection.BACKWARD), - ("036", "035", AgvDirection.BACKWARD), - ("035", "034", AgvDirection.BACKWARD), - ("034", "033", AgvDirection.BACKWARD), - ("033", "032", AgvDirection.BACKWARD), - ("032", "040", AgvDirection.BACKWARD) - ], - "041": [ - # 정답: "007 ->(B) 006 -> 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 031 -> 041" - ("007", "006", AgvDirection.BACKWARD), - ("006", "005", AgvDirection.BACKWARD), - ("005", "037", AgvDirection.BACKWARD), - ("037", "036", AgvDirection.BACKWARD), - ("036", "035", AgvDirection.BACKWARD), - ("035", "034", AgvDirection.BACKWARD), - ("034", "033", AgvDirection.BACKWARD), - ("033", "032", AgvDirection.BACKWARD), - ("032", "031", AgvDirection.BACKWARD), - ("031", "041", AgvDirection.BACKWARD) - ], - "008": [ - # 정답: "007 ->(F) 006 -> 005 -> 037 ->(B) 005 -> 006 -> 007 -> 008" - ("007", "006", AgvDirection.FORWARD), - ("006", "005", AgvDirection.FORWARD), - ("005", "037", AgvDirection.FORWARD), - ("037", "005", AgvDirection.BACKWARD), - ("005", "006", AgvDirection.BACKWARD), - ("006", "007", AgvDirection.BACKWARD), - ("007", "008", AgvDirection.BACKWARD) - ], - "001": [ - # 정답: "007 ->(B) 006 -> 005 -> 004 -> 003 -> 002 -> 001" - ("007", "006", AgvDirection.BACKWARD), - ("006", "005", AgvDirection.BACKWARD), - ("005", "004", AgvDirection.BACKWARD), - ("004", "003", AgvDirection.BACKWARD), - ("003", "002", AgvDirection.BACKWARD), - ("002", "001", AgvDirection.BACKWARD) - ], - "011": [ - # 정답: "007 ->(B) 006 -> 005 -> 004 -> 030 -> 009 -> 010 -> 011" - ("007", "006", AgvDirection.BACKWARD), - ("006", "005", AgvDirection.BACKWARD), - ("005", "004", AgvDirection.BACKWARD), - ("004", "030", AgvDirection.BACKWARD), - ("030", "009", AgvDirection.BACKWARD), - ("009", "010", AgvDirection.BACKWARD), - ("010", "011", AgvDirection.BACKWARD) - ], - "019": [ - # 정답: "007 ->(B) 006 -> 005 -> 004 -> 012 -> 013 ->(F) 012 -> 016 -> 017 -> 018 -> 019" - ("007", "006", AgvDirection.BACKWARD), - ("006", "005", AgvDirection.BACKWARD), - ("005", "004", AgvDirection.BACKWARD), - ("004", "012", AgvDirection.BACKWARD), - ("012", "013", AgvDirection.BACKWARD), - ("013", "012", AgvDirection.FORWARD), - ("012", "016", AgvDirection.FORWARD), - ("016", "017", AgvDirection.FORWARD), - ("017", "018", AgvDirection.FORWARD), - ("018", "019", AgvDirection.FORWARD) - ], - "015": [ - # 정답: "007 ->(B) 006 -> 005 -> 004 -> 012 -> 016 ->(F) 012 -> 013 -> 014 -> 015" - ("007", "006", AgvDirection.BACKWARD), - ("006", "005", AgvDirection.BACKWARD), - ("005", "004", AgvDirection.BACKWARD), - ("004", "012", AgvDirection.BACKWARD), - ("012", "016", AgvDirection.BACKWARD), - ("016", "012", AgvDirection.FORWARD), - ("012", "013", AgvDirection.FORWARD), - ("013", "014", AgvDirection.FORWARD), - ("014", "015", AgvDirection.FORWARD) - ] - } - - if target_rfid not in q2_1_patterns: - return PathResult(False, [], 0, False, None, f"Q2-1 패턴 없음: {target_rfid}") - - pattern = q2_1_patterns[target_rfid] - steps = [] - - for i, (from_rfid, to_rfid, direction) in enumerate(pattern): - from_node = self.map.resolve_node_id(from_rfid) - to_node = self.map.resolve_node_id(to_rfid) - - if from_node and to_node: - step = PathStep(from_node, to_node, direction, MagnetDirection.STRAIGHT) - - # Q2-1 방향전환 지점 마킹 - if (from_rfid == "037" and to_rfid == "005") or \ - (from_rfid == "013" and to_rfid == "012") or \ - (from_rfid == "016" and to_rfid == "012"): - step._is_turnaround_point = True - - steps.append(step) - - # 방향전환 지점 찾기 - turnaround_junction = None - if target_rfid == "008": - turnaround_junction = self.map.resolve_node_id("037") - elif target_rfid == "019": - turnaround_junction = self.map.resolve_node_id("013") - elif target_rfid == "015": - turnaround_junction = self.map.resolve_node_id("016") - - needs_turnaround = turnaround_junction is not None - - return PathResult(True, steps, len(steps), needs_turnaround, turnaround_junction, f"Q2-1 {target_rfid} 성공") - - def _handle_q2_2_scenario(self, target_rfid: str) -> PathResult: - """Q2-2: 006->007(후진) 케이스 처리""" - - # Q2-2 패턴 정의 (006->007 후진에서 각 목표까지) - q2_2_patterns = { - "040": [ - # 정답: "007 ->(F) 006 -> 005 -> 004 ->(B) 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 040" - ("007", "006", AgvDirection.FORWARD), - ("006", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "005", AgvDirection.BACKWARD), - ("005", "037", AgvDirection.BACKWARD), - ("037", "036", AgvDirection.BACKWARD), - ("036", "035", AgvDirection.BACKWARD), - ("035", "034", AgvDirection.BACKWARD), - ("034", "033", AgvDirection.BACKWARD), - ("033", "032", AgvDirection.BACKWARD), - ("032", "040", AgvDirection.BACKWARD) - ], - "041": [ - # 정답: "007 ->(F) 006 -> 005 -> 004 ->(B) 005 -> 037 -> 036 -> 035 -> 034 -> 033 -> 032 -> 031 -> 041" - ("007", "006", AgvDirection.FORWARD), - ("006", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "005", AgvDirection.BACKWARD), - ("005", "037", AgvDirection.BACKWARD), - ("037", "036", AgvDirection.BACKWARD), - ("036", "035", AgvDirection.BACKWARD), - ("035", "034", AgvDirection.BACKWARD), - ("034", "033", AgvDirection.BACKWARD), - ("033", "032", AgvDirection.BACKWARD), - ("032", "031", AgvDirection.BACKWARD), - ("031", "041", AgvDirection.BACKWARD) - ], - "008": [ - # 정답: "007 ->(B) 008" - ("007", "008", AgvDirection.BACKWARD) - ], - "001": [ - # 정답: "007 ->(F) 006 -> 005 -> 004 -> 030 ->(B) 004 -> 003 -> 002 -> 001" - ("007", "006", AgvDirection.FORWARD), - ("006", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "030", AgvDirection.FORWARD), - ("030", "004", AgvDirection.BACKWARD), - ("004", "003", AgvDirection.BACKWARD), - ("003", "002", AgvDirection.BACKWARD), - ("002", "001", AgvDirection.BACKWARD) - ], - "011": [ - # 정답: "007 ->(F) 006 -> 005 -> 004 -> 003 ->(B) 004 -> 030 -> 009 -> 010 -> 011" - ("007", "006", AgvDirection.FORWARD), - ("006", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "003", AgvDirection.FORWARD), - ("003", "004", AgvDirection.BACKWARD), - ("004", "030", AgvDirection.BACKWARD), - ("030", "009", AgvDirection.BACKWARD), - ("009", "010", AgvDirection.BACKWARD), - ("010", "011", AgvDirection.BACKWARD) - ], - "019": [ - # 정답: "007 ->(F) 006 -> 005 -> 004 -> 012 -> 016 -> 017 -> 018 -> 019" - ("007", "006", AgvDirection.FORWARD), - ("006", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "012", AgvDirection.FORWARD), - ("012", "016", AgvDirection.FORWARD), - ("016", "017", AgvDirection.FORWARD), - ("017", "018", AgvDirection.FORWARD), - ("018", "019", AgvDirection.FORWARD) - ], - "015": [ - # 정답: "007 ->(F) 006 -> 005 -> 004 -> 012 -> 013 -> 014 -> 015" - ("007", "006", AgvDirection.FORWARD), - ("006", "005", AgvDirection.FORWARD), - ("005", "004", AgvDirection.FORWARD), - ("004", "012", AgvDirection.FORWARD), - ("012", "013", AgvDirection.FORWARD), - ("013", "014", AgvDirection.FORWARD), - ("014", "015", AgvDirection.FORWARD) - ] - } - - if target_rfid not in q2_2_patterns: - return PathResult(False, [], 0, False, None, f"Q2-2 패턴 없음: {target_rfid}") - - pattern = q2_2_patterns[target_rfid] - steps = [] - - for i, (from_rfid, to_rfid, direction) in enumerate(pattern): - from_node = self.map.resolve_node_id(from_rfid) - to_node = self.map.resolve_node_id(to_rfid) - - if from_node and to_node: - step = PathStep(from_node, to_node, direction, MagnetDirection.STRAIGHT) - - # Q2-2 방향전환 지점 마킹 - if (from_rfid == "004" and to_rfid == "005") or \ - (from_rfid == "030" and to_rfid == "004") or \ - (from_rfid == "003" and to_rfid == "004"): - step._is_turnaround_point = True - - steps.append(step) - - # 방향전환 지점 찾기 - turnaround_junction = None - if target_rfid in ["040", "041"]: - turnaround_junction = self.map.resolve_node_id("004") - elif target_rfid == "001": - turnaround_junction = self.map.resolve_node_id("030") - elif target_rfid == "011": - turnaround_junction = self.map.resolve_node_id("003") - - needs_turnaround = turnaround_junction is not None - - return PathResult(True, steps, len(steps), needs_turnaround, turnaround_junction, f"Q2-2 {target_rfid} 성공") - - def _handle_general_turnaround(self, start_rfid: str, target_rfid: str, current_dir: AgvDirection, required_dir: AgvDirection) -> PathResult: - """일반적인 방향전환 처리""" - return PathResult(False, [], 0, False, None, "일반 방향전환 구현 필요") - -# 범용 출력 포맷터 -class UniversalPathFormatter: - """정답 형식에 맞는 경로 출력 생성""" - - @staticmethod - def format_path(result: PathResult, agv_map) -> str: - """정답 형식으로 경로 포맷""" - if not result.success: - return f"실패: {result.error_message}" - - path_nodes = [] - path_directions = [] - - # 노드와 방향 정보 수집 - for i, step in enumerate(result.path_steps): - from_rfid = agv_map.get_node(step.from_node).rfid_id - to_rfid = agv_map.get_node(step.to_node).rfid_id - direction = step.motor_direction.value[0] - - if i == 0: - path_nodes.append(from_rfid) - path_nodes.append(to_rfid) - path_directions.append(direction) - - # 경로 문자열 구성 - path_detail = path_nodes[0] - for i in range(len(path_directions)): - next_node = path_nodes[i + 1] - - # 특별 케이스 처리 - if i > 0 and path_directions[i] != path_directions[i-1]: - # 방향 변경 확인 - 실제 변경되는 방향 표시 - path_detail += f" ->({path_directions[i]}) -> {next_node}" - elif hasattr(result.path_steps[i], '_is_turnaround_point') and result.path_steps[i]._is_turnaround_point: - path_detail += f" ->(R) -> {next_node}" - elif hasattr(result.path_steps[i], '_is_immediate_turn') and result.path_steps[i]._is_immediate_turn: - path_detail += f" ->(R) -> {next_node}" - elif hasattr(result.path_steps[i], '_is_direct_jump') and result.path_steps[i]._is_direct_jump: - path_detail += f" ->(B) -> {next_node}" - else: - direction_symbol = path_directions[i] - if i == 0: - path_detail += f" ->({direction_symbol}) {next_node}" - else: - path_detail += f" -> {next_node}" - - return path_detail - -if __name__ == "__main__": - print("Universal AGV PathFinder - 범용 알고리즘 테스트") \ No newline at end of file diff --git a/Cs_HMI/run_claude.bat b/Cs_HMI/run_claude.bat new file mode 100644 index 0000000..29a2144 --- /dev/null +++ b/Cs_HMI/run_claude.bat @@ -0,0 +1 @@ +claude --dangerously-skip-permissions