diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs index 6f146e5..6e3b9a7 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs @@ -64,7 +64,7 @@ namespace AGVNavigationCore.PathFinding.Core /// /// 오류 메시지 (실패시) /// - public string ErrorMessage { get; set; } + public string Message { get; set; } /// /// 도킹 검증 결과 @@ -116,7 +116,7 @@ namespace AGVNavigationCore.PathFinding.Core ExploredNodes = 0; EstimatedTimeSeconds = 0; RotationCount = 0; - ErrorMessage = string.Empty; + Message = string.Empty; PlanDescription = string.Empty; RequiredDirectionChange = false; DirectionChangeNode = string.Empty; @@ -148,37 +148,20 @@ namespace AGVNavigationCore.PathFinding.Core return result; } - /// /// 실패 결과 생성 /// /// 오류 메시지 /// 계산 시간 - /// 실패 결과 - public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs) - { - return new AGVPathResult - { - Success = false, - ErrorMessage = errorMessage, - CalculationTimeMs = calculationTimeMs - }; - } - - /// - /// 실패 결과 생성 (확장) - /// - /// 오류 메시지 - /// 계산 시간 /// 탐색된 노드 수 /// 실패 결과 - public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs, int exploredNodes) + public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs = 0, int exploredNodes = 0) { return new AGVPathResult { Success = false, - ErrorMessage = errorMessage, + Message = errorMessage, CalculationTimeMs = calculationTimeMs, ExploredNodes = exploredNodes }; @@ -276,22 +259,6 @@ namespace AGVNavigationCore.PathFinding.Core } } - /// - /// 상세 경로 정보 반환 - /// - /// 상세 정보 문자열 - public string GetDetailedInfo() - { - if (!Success) - { - return $"경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)"; - } - - return $"경로: {Path.Count}개 노드, 거리: {TotalDistance:F1}px, " + - $"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " + - $"계산시간: {CalculationTimeMs}ms"; - } - /// /// 경로의 노드 정보를 포함 /// @@ -300,13 +267,14 @@ namespace AGVNavigationCore.PathFinding.Core { if (!Success) { - return $"경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)"; + return $"경로 계산 실패: {Message} (계산시간: {CalculationTimeMs}ms)"; } - var data = DetailedPath.Select(t => { - return $"{t.RfidId}[{t.NodeId}] {t.MotorDirection.ToString().Substring(0,1)}-{t.MagnetDirection.ToString().Substring(0,1)}"; + var data = DetailedPath.Select(t => + { + return $"{t.RfidId}[{t.NodeId}] {t.MotorDirection.ToString().Substring(0, 1)}-{t.MagnetDirection.ToString().Substring(0, 1)}"; }); - return string.Join(" → ",data); + return string.Join(" → ", data); } @@ -322,7 +290,7 @@ namespace AGVNavigationCore.PathFinding.Core } return Path?.Select(n => n.Id).ToList() ?? new List(); } - + /// /// 문자열 표현 /// @@ -334,7 +302,7 @@ namespace AGVNavigationCore.PathFinding.Core } else { - return $"Failed: {ErrorMessage}"; + return $"Failed: {Message}"; } } } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs index ace68de..4d67267 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs @@ -351,12 +351,12 @@ namespace AGVNavigationCore.PathFinding.Core if (!previousResult.Success) return AGVPathResult.CreateFailure( - $"이전 경로 결과 실패: {previousResult.ErrorMessage}", + $"이전 경로 결과 실패: {previousResult.Message}", previousResult.CalculationTimeMs); if (!currentResult.Success) return AGVPathResult.CreateFailure( - $"현재 경로 결과 실패: {currentResult.ErrorMessage}", + $"현재 경로 결과 실패: {currentResult.Message}", currentResult.CalculationTimeMs); // 경로가 비어있는 경우 처리 diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs index e1fd758..5c35028 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs @@ -138,7 +138,7 @@ namespace AGVNavigationCore.PathFinding.Planning pathResult.PrevDirection = prevDirection; if (!pathResult.Success) - return AGVPathResult.CreateFailure(pathResult.ErrorMessage ?? "경로 없음", 0, 0); + return AGVPathResult.CreateFailure(pathResult.Message ?? "경로 없음", 0, 0); // 3. 상세 데이터 생성 (갈림길 마그넷 방향 계산 포함) // 3. 상세 데이터 생성 (갈림길 마그넷 방향 계산 포함) diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs index 6000d71..9758cf5 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs @@ -108,7 +108,7 @@ namespace AGVNavigationCore.PathFinding.Planning /// public override string ToString() { - var result = $"R{RfidId}[N{NodeId}]:{MotorDirection}"; + var result = $"R{RfidId}[*{NodeId}]:{MotorDirection}"; // 마그넷 방향이 직진이 아닌 경우 표시 if (MagnetDirection != MagnetDirection.Straight) diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs index ae9aa76..f73f20d 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs @@ -191,7 +191,7 @@ namespace AGVNavigationCore.Utils } // 검증 수행 - if (LastNodeInfo.MotorDirection == requiredDirection && pathResult.DetailedPath[pathResult.DetailedPath.Count - 2].MotorDirection == requiredDirection) + if (LastNodeInfo.MotorDirection == requiredDirection && pathResult.DetailedPath[pathResult.DetailedPath.Count - 1].MotorDirection == requiredDirection) { System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공"); return DockingValidationResult.CreateValid( diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs index 89910a8..c17aec5 100644 --- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs +++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs @@ -108,7 +108,7 @@ namespace AGVSimulator.Forms } private UnifiedAGVCanvas _simulatorCanvas; - private AGVPathfinder _advancedPathfinder; + // private AGVPathfinder _advancedPathfinder; private List _agvList; private SimulationState _simulationState; private System.Windows.Forms.Timer _simulationTimer; @@ -1579,7 +1579,7 @@ namespace AGVSimulator.Forms /// UI 상태로부터 테스트 결과 생성 (테스트용) /// private PathTestLogItem CreateTestResultFromUI(MapNode prevNode, MapNode targetNode, - string directionName, (bool result, string message) calcResult) + string directionName, AGVPathResult calcResult) { var currentNode = _simulatorCanvas.Nodes.FirstOrDefault(n => n.Id == (_agvListCombo.SelectedItem as VirtualAGV)?.CurrentNodeId); @@ -1593,10 +1593,10 @@ namespace AGVSimulator.Forms DockingPosition = (targetNode.StationType == StationType.Charger1 || targetNode.StationType == StationType.Charger2) ? "충전기" : "장비" }; - if (calcResult.result) + if (calcResult.Success) { // 경로 계산 성공 - 현재 화면에 표시된 경로 정보 사용 - var currentPath = _simulatorCanvas.CurrentPath; + var currentPath = calcResult;// _simulatorCanvas.CurrentPath; if (currentPath != null && currentPath.Success) { // 도킹 검증 @@ -1626,7 +1626,7 @@ namespace AGVSimulator.Forms { // 경로 계산 실패 logItem.Success = false; - logItem.Message = calcResult.message; + logItem.Message = calcResult.Message; logItem.DetailedPath = "-"; } @@ -1763,19 +1763,25 @@ namespace AGVSimulator.Forms SetTargetNodeComboBox(dockingTarget.Id); // 경로 계산 버튼 클릭 (실제 사용자 동작) - //var calcResult = CalcPath(); + var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem)?.Value; + var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem)?.Value; + var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; + var calcResult = CalcPath(startNode, targetNode, this._simulatorCanvas.Nodes,selectedAGV.PrevNode, selectedAGV.PrevDirection); + + + //// 테스트 결과 생성 - //testResult = CreateTestResultFromUI(nodeA, dockingTarget, directionName, calcResult); + testResult = CreateTestResultFromUI(nodeA, dockingTarget, directionName, calcResult); //// 로그 추가 - //logForm.AddLogItem(testResult); + logForm.AddLogItem(testResult); //// 실패한 경우에만 경로를 화면에 표시 (시각적 확인) - //if (!testResult.Success && _simulatorCanvas.CurrentPath != null) - //{ - // _simulatorCanvas.Invalidate(); - //} + if (!testResult.Success && _simulatorCanvas.CurrentPath != null) + { + _simulatorCanvas.Invalidate(); + } Application.DoEvents(); }); @@ -2519,102 +2525,332 @@ namespace AGVSimulator.Forms private void btPath2_Click(object sender, EventArgs e) { + // 1. 기본 정보 획득 + if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요") SetStartNodeFromAGVPosition(); + if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null) + { + MessageBox.Show("시작/목표 노드를 확인하세요"); + return; + } + + //var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; + //if (selectedAGV == null) return AGVPathResult.CreateFailure("Virtual AGV 없음"); + var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; + // 경로계산2 (Gateway Logic) - var rlt = CalcPathGateway(); - if (rlt.result == false) MessageBox.Show(rlt.message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); + var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem)?.Value; + var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem)?.Value; + var rlt = CalcPath(startNode, targetNode, this._simulatorCanvas.Nodes, selectedAGV.PrevNode, selectedAGV.PrevDirection); + if (rlt.Success == false) MessageBox.Show(rlt.Message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); + else + { + // 8. 적용 + + ApplyResultToSimulator(rlt, selectedAGV); + UpdateAdvancedPathDebugInfo(rlt); + } + } /// /// 길목(Gateway) 기반 경로 계산 + /// 버퍼-버퍼 상태에서는 별도의 추가 로직을 적용합니다 /// - private (bool result, string message) CalcPathGateway() + public AGVPathResult CalcPath(MapNode startNode, MapNode targetNode, List nodes, + MapNode prevNode, AgvDirection prevDir) { - // 1. 기본 정보 획득 - if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요") SetStartNodeFromAGVPosition(); - if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null) return (false, "시작/목표 노드 선택 필요"); + AGVPathResult Retval; - var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem)?.Value; - var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem)?.Value; - if (startNode == null || targetNode == null) return (false, "노드 정보 오류"); + if (startNode == null || targetNode == null) return AGVPathResult.CreateFailure("시작/종료노드가 지정되지 않음"); - if (_advancedPathfinder == null) _advancedPathfinder = new AGVPathfinder(_simulatorCanvas.Nodes); - - var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; - if (selectedAGV == null) return (false, "Virtual AGV 없음"); - - var currentAgvDir = selectedAGV.CurrentDirection; - var prevNode = selectedAGV.PrevNode; - var prevDir = selectedAGV.PrevDirection; - - // 2. Buffer-to-Buffer 예외 처리 - - var node05 = FindNode(5); //05~31사이의 노드는 모두 버퍼이다. - var node31 = FindNode(31); - if (node05 == null || node31 == null) return (false, "버퍼구간 노드가 없습니다(05~31)"); - - var rlt = _advancedPathfinder.FindPathAStar(node05, node31); - if (rlt.Success == false) return (false, "버퍼구간 노드경로 확인 실패(05~31)"); - - //버퍼구간내에 시작과 종료가 모두 포함되어있다 - if (rlt.Path.Contains(startNode) && rlt.Path.Contains(targetNode)) + + try { - return CalcPathBufferToBuffer(startNode, targetNode, prevNode, prevDir, currentAgvDir, selectedAGV); - } + var pathFinder = new AGVPathfinder(nodes); + var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; + if (selectedAGV == null) return AGVPathResult.CreateFailure("Virtual AGV 없음"); - - // 3. 목적지별 Gateway 및 진입 조건 확인 - var gatewayNode = GetGatewayNode(targetNode); - if (gatewayNode == null) - { - //게이트웨이가 없는 경우라면 목적지가 도킹포인트가 아니므로, a*알골리즘으로 진행 방향만 맟춰서 이동한다 - var simplePath = _advancedPathfinder.FindBasicPath(startNode, targetNode, prevNode, prevDir); - if (simplePath.Success) + //종료노드라면 이전위치로 이동시켜야한다. + AGVPathResult LimitPath = null; + if (startNode.ConnectedMapNodes.Count == 1) { - _simulatorCanvas.HighlightNodeId = null; // 일반 경로는 강조 없음 - ApplyResultToSimulator(simplePath, selectedAGV); - UpdateAdvancedPathDebugInfo(simplePath); - return (true, "일반 이동 경로(Gateway 없음)"); + LimitPath = pathFinder.FindPathAStar(startNode, targetNode); + for(int i = 0; i < LimitPath.Path.Count;i++) + { + var nodeinfo = LimitPath.Path[i]; + var dir = (prevDir == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward); + LimitPath.DetailedPath.Add(new NodeMotorInfo(i + 1, nodeinfo.Id, nodeinfo.RfidId, dir)); + } } - return (false, $"일반 이동 경로 실패: {simplePath.ErrorMessage}"); + + + //var prevNode = selectedAGV.PrevNode; + //var prevDir = selectedAGV.PrevDirection; + + // 2. Buffer-to-Buffer 예외 처리 + var node05 = FindNode(5); //05~31사이의 노드는 모두 버퍼이다. + var node31 = FindNode(31); + if (node05 == null || node31 == null) return AGVPathResult.CreateFailure("버퍼구간 노드가 없습니다(05~31)"); + + var rlt = pathFinder.FindPathAStar(node05, node31); + if (rlt.Success == false) return AGVPathResult.CreateFailure("버퍼구간 노드경로 확인 실패(05~31)"); + + //하이라이트노드 해제 + _simulatorCanvas.HighlightNodeId = null; + + //버퍼구간내에 시작과 종료가 모두 포함되어있다 + if (rlt.Path.Contains(startNode) && rlt.Path.Contains(targetNode)) + { + Retval = CalcPathBufferToBuffer(pathFinder, startNode, targetNode, prevNode, prevDir, selectedAGV); + } + else + { + // 3. 목적지별 Gateway 및 진입 조건 확인 + var gatewayNode = GetGatewayNode(targetNode); + if (gatewayNode == null) + { + //게이트웨이가 없는 경우라면 목적지가 도킹포인트가 아니므로, a*알골리즘으로 진행 방향만 맟춰서 이동한다 + Retval = pathFinder.FindBasicPath(startNode, targetNode, prevNode, prevDir); + } + else + { + // Gateway Node 찾음 + _simulatorCanvas.HighlightNodeId = gatewayNode.Id; // Gateway 강조 설정 + + // 4. Start -> Gateway 경로 계산 (A*) + var pathToGateway = pathFinder.FindBasicPath(startNode, gatewayNode, prevNode, prevDir); + if (pathToGateway.Success == false) + AGVPathResult.CreateFailure($"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.Message}"); + + //마지막경로는 게이트웨이이므로 제거하낟.(260113) + if (pathToGateway.Path.Count > 1 && pathToGateway.Path.Last().Id == gatewayNode.Id) + { + pathToGateway.Path.RemoveAt(pathToGateway.Path.Count - 1); + pathToGateway.DetailedPath.RemoveAt(pathToGateway.DetailedPath.Count - 1); + } + + // 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함) + MapNode GateprevNode = pathToGateway.Path.Last(); + NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.Last(); + + var arrivalOrientation = GatePrevDetail.MotorDirection; + var gatewayPathResult = GetPathFromGateway(pathFinder, gatewayNode, targetNode, GateprevNode, arrivalOrientation); + if (!gatewayPathResult.Success) return AGVPathResult.CreateFailure($"{gatewayPathResult.Message}"); + Retval = CombinePaths(pathToGateway, gatewayPathResult); + } + + } + + //경로오류 검사 + if (Retval.Success == false) return Retval; + + //해당경로와 대상의 도킹포인트의 방향을 검사합니다 + if (targetNode.DockDirection != DockingDirection.DontCare) + { + var lastPath = Retval.DetailedPath.Last(); + if (targetNode.DockDirection == DockingDirection.Forward && lastPath.MotorDirection != AgvDirection.Forward) + { + return AGVPathResult.CreateFailure($"생성된 경로와 목적지의 도킹방향이 일치하지 않습니다(FWD) Target:{targetNode.DockDirection}, Path:{Retval.GetDetailedPathInfo()}"); + } + if (targetNode.DockDirection == DockingDirection.Backward && lastPath.MotorDirection != AgvDirection.Backward) + { + return AGVPathResult.CreateFailure($"생성된 경로와 목적지의 도킹방향이 일치하지 않습니다(BWD) Target:{targetNode.DockDirection}, Path:{Retval.GetDetailedPathInfo()}"); + } + } + + //6[F][R] → 13[B][L] → 6[F][L] 이런경우를 찾아서 경로를 최적화한다. + //위 예제에서는 6FR 13BL 이 제자리로 오는 경로이므로 6FL만 남기면된다. + //단순히 ID만 같은게 아니라, 실제로 갔다가 되돌아오는 패턴(역방향)인지 확인해야 함. + while (true) + { + var updatecount = 0; + for (int i = 0; i < Retval.DetailedPath.Count - 2; i++) + { + var n1 = Retval.DetailedPath[i]; + var n2 = Retval.DetailedPath[i + 1]; + var n3 = Retval.DetailedPath[i + 2]; + + if (n1.NodeId == n3.NodeId) + { + bool isInverse = false; + + // 1. 모터 방향이 반대인가? (F <-> B) + bool isMotorInverse = (n1.MotorDirection != n2.MotorDirection) && + (n1.MotorDirection == AgvDirection.Forward || n1.MotorDirection == AgvDirection.Backward) && + (n2.MotorDirection == AgvDirection.Forward || n2.MotorDirection == AgvDirection.Backward); + + if (isMotorInverse) + { + // 2. 마그넷 방향이 반대인가? (L <-> R, S <-> S) + bool isMagnetInverse = false; + if (n1.MagnetDirection == MagnetDirection.Straight && n2.MagnetDirection == MagnetDirection.Straight) isMagnetInverse = true; + else if (n1.MagnetDirection == MagnetDirection.Left && n2.MagnetDirection == MagnetDirection.Right) isMagnetInverse = true; + else if (n1.MagnetDirection == MagnetDirection.Right && n2.MagnetDirection == MagnetDirection.Left) isMagnetInverse = true; + + if (isMagnetInverse) isInverse = true; + } + + if (isInverse) + { + // 제자리 회귀 경로 발견 -> 앞의 두 노드(n1, n2)를 제거하여 n3만 남김 + Retval.DetailedPath.RemoveAt(i); + Retval.DetailedPath.RemoveAt(i); + + if (Retval.Path.Count > i + 1) + { + Retval.Path.RemoveAt(i); + Retval.Path.RemoveAt(i); + } + i--; // 인덱스 재조정 + updatecount += 1; + } + } + } + if (updatecount == 0) break; + } + + + //detail 경로를 확인해서 왓던길에서 바로 방향이 전환되는 경우를 찾는다 + for (int i = 0; i < Retval.DetailedPath.Count - 2; i++) + { + + var n1 = Retval.DetailedPath[i]; + var n2 = Retval.DetailedPath[i + 1]; + var n3 = Retval.DetailedPath[i + 2]; + + if (n1.NodeId == n3.NodeId && n1.MotorDirection == n3.MotorDirection) + { + return AGVPathResult.CreateFailure($"불가능한 회전 경로가 포함되어있습니다. {n1.RfidId}->{n2.RfidId}->{n3.RfidId}\n{Retval.GetDetailedPathInfo()}"); + } + + } + + //6[F][R] → 13[B][L] → 6[F][L] 이런경우를 찾아서 경로를 최적화한다. + //위 예제에서는 6FR 13BL 이 제자리로 오는 경로이므로 6FL만 남기면된다. + + + //최종결과 반환 + return Retval; } - - // Gateway Node 찾음 - _simulatorCanvas.HighlightNodeId = gatewayNode.Id; // Gateway 강조 설정 - - // 4. Start -> Gateway 경로 계산 (A*) - var pathToGateway = _advancedPathfinder.FindBasicPath(startNode, gatewayNode, prevNode, prevDir); - - if (!pathToGateway.Success) return (false, $"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.ErrorMessage}"); - - //마지막경로는 게이트웨이이므로 제거하낟.(260113) - if (pathToGateway.Path.Count > 1 && pathToGateway.Path.Last().Id == gatewayNode.Id) + catch (Exception ex) { - pathToGateway.Path.RemoveAt(pathToGateway.Path.Count - 1); - pathToGateway.DetailedPath.RemoveAt(pathToGateway.DetailedPath.Count - 1); + return AGVPathResult.CreateFailure($"[계산오류] {ex.Message}"); } - // 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함) + } + + private AGVPathResult CalcPathBufferToBuffer(AGVPathfinder pathfinder, MapNode start, MapNode target, MapNode prev, AgvDirection prevDir, VirtualAGV agv) + { + // Monitor Side 판단 로직 + // 현재 AGV의 물리적 방향(Monitor Side)이 "Right" 상태여야 버퍼 진입이 용이하다고 가정. + // Monitor Left 상태(부적절한 방향)라면 Gateway로 탈출해야 함. + + // 이동 벡터 X 변화량 + // prev가 없거나 start와 같으면 이동 방향을 알 수 없음 -> 이 경우 보수적으로 기존 로직(Backward면 탈출) 따름 + int deltaX = 0; + if (prev == null) return AGVPathResult.CreateFailure("이전 노드 정보가 없습니다"); + else deltaX = start.Position.X - prev.Position.X; + + bool isMonitorLeft = false; + + if (deltaX > 0) // 오른쪽(Forward)으로 이동해 옴 (예: 2 -> 4) + { + // 이동방향(Right) + 전진(F) => Monitor Right (Good) + // 이동방향(Right) + 후진(B) => Monitor Left (Bad) + isMonitorLeft = (prevDir == AgvDirection.Backward); + } + else if (deltaX < 0) // 왼쪽(Reverse)으로 이동해 옴 (예: 4 -> 2) + { + // 이동방향(Left) + 전진(F) => Monitor Left (Bad) + // 이동방향(Left) + 후진(B) => Monitor Right (Good) + isMonitorLeft = (prevDir == AgvDirection.Forward); + } + else // 제자리 또는 수직 이동 + { + // 판단 불가 시 기존 로직(Backward면 Gateway) 유지 + return AGVPathResult.CreateFailure("이전 노드와의 방향을 알 수 없습니다"); + } + + if (isMonitorLeft) + { + // Monitor Left 상태 (방향 불일치) -> Gateway로 탈출 + var GateWayNode = FindNode(6); + var escPath = pathfinder.FindBasicPath(start, GateWayNode, prev, prevDir); + if (!escPath.Success) return AGVPathResult.CreateFailure("버퍼 탈출 경로 실패"); + + var lastNode = escPath.Path.Last(); // Should be GW6 + var lastPrev = escPath.Path[escPath.Path.Count - 2]; + var lastDir = escPath.DetailedPath.Last().MotorDirection; + + // Gateway 도착 후, Target(Buffer)으로 이동 + // 여기서부터는 "Monitor Right" 로직(즉, 적절한 방향 진입)을 적용합니다. + // 6번에서 Target이 왼쪽이면 Direct(Backward), 오른쪽이면 Overshoot(Forward->Backward) - MapNode GateprevNode = pathToGateway.Path.Last(); - NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.Last(); + bool isTargetLeftOfGW = target.Position.X < GateWayNode.Position.X; + AGVPathResult entryPath = null; + + //게이트웨이까지 후진으로 이동했다면 모니터방향이 오른쪽이다 => 방향전환필요 + var gateToTarget = GetPathFromGateway(pathfinder, GateWayNode, target, lastPrev, lastDir); + + escPath.Path.RemoveAt(escPath.Path.Count - 1); + escPath.DetailedPath.RemoveAt(escPath.DetailedPath.Count - 1); - var arrivalOrientation = GatePrevDetail.MotorDirection; + var final = CombinePaths(escPath, gateToTarget); + ApplyResultToSimulator(final, agv); + UpdateAdvancedPathDebugInfo(final); + return final; + } + else + { + // Monitor Right 상태 (방향 일치) -> 직접 진입 또는 Overshoot + bool isTargetLeft = target.Position.X < start.Position.X; - //아래코드오류발생함 - var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, GateprevNode, arrivalOrientation); + if (isTargetLeft) + { + var directPath = pathfinder.FindBasicPath(start, target, prev, AgvDirection.Backward); + ApplyResultToSimulator(directPath, agv); + UpdateAdvancedPathDebugInfo(directPath); + return directPath; + } + else + { + //대상이 나보다 우측에 있으니 RFID값이 읽어지는 위치까지 이동후에 다시 반대방향으로 마크스탑 해야 함 + //그냥 대상 노드까지 이동을 한다.. RFID값이 실제 멈추는 위치 이전에 있으니 그곳까지 이동하고 역방향 마크스탑하면 동일한 위치이다. 위 식은 그 이전노드까지 확실하게 이동하는 코드이다 + //var overshootNode = target.ConnectedMapNodes.OrderByDescending(n => n.Position.X).FirstOrDefault(); + //if (overshootNode == null || overshootNode.Position.X <= target.Position.X) + // return (false, "Overshoot 공간 부족"); - if (!gatewayPathResult.Success) return (false, $"{gatewayPathResult.ErrorMessage}"); - AGVPathResult finalPath = pathToGateway; + var path1 = pathfinder.FindBasicPath(start, target, prev, AgvDirection.Forward); + if (path1.Path.Count < 2) return AGVPathResult.CreateFailure("Overshoot 경로 생성 실패"); - finalPath = CombinePaths(finalPath, gatewayPathResult); + //목표에서 방향바꿔 마크스탑을 해야한다 + path1.Path.Add(path1.Path.Last()); + //디테일은 방향바꿔서 추가 필요(속도는 저속처리해준다) + var lastDefailt = path1.DetailedPath.Last(); + path1.DetailedPath.Add(new NodeMotorInfo(lastDefailt.seq + 1, lastDefailt.NodeId, lastDefailt.RfidId, AgvDirection.Backward) + { + Speed = SpeedLevel.L, + IsPass = false, + }); - // 8. 적용 - ApplyResultToSimulator(finalPath, selectedAGV); - UpdateAdvancedPathDebugInfo(finalPath); - return (true, "성공"); + //var p1Last = path1.Path.Last(); + //var p1Prev = path1.Path[path1.Path.Count > 1 ? path1.Path.Count - 2 : 0]; // Safety check + //var p1Dir = path1.DetailedPath.Last().MotorDirection; + + //var path2 = _advancedPathfinder.FindPath(start, target, p1Last, p1Dir, AgvDirection.Backward); + + //if (path2.Success && path2.DetailedPath.Last().NodeId == target.Id) + // path2.DetailedPath = path2.DetailedPath.Take(path2.DetailedPath.Count - 1).ToList(); + + //var final = CombinePaths(path1, path2); + ApplyResultToSimulator(path1, agv); + UpdateAdvancedPathDebugInfo(path1); + return path1;// (true, path1, "버퍼 우측(Overshoot)"); + } + } } @@ -2649,7 +2885,7 @@ namespace AGVSimulator.Forms /// 게이트웨이 진입 전 노드 /// 게이트웨이 진입 전 모터방향 /// - private AGVPathResult GetPathFromGateway(MapNode GTNode, MapNode targetNode, MapNode PrevNode, AgvDirection PrevDirection) + private AGVPathResult GetPathFromGateway(AGVPathfinder pathFinder, MapNode GTNode, MapNode targetNode, MapNode PrevNode, AgvDirection PrevDirection) { AGVPathResult resultPath = null; @@ -2693,13 +2929,14 @@ namespace AGVSimulator.Forms rlt1.Success = true; //목적지까지 바로 계산한다 - var pathtarget = _advancedPathfinder.FindBasicPath(GTNode, targetNode, PrevNode, AgvDirection.Backward); + var motdir = targetNode.DockDirection == DockingDirection.Backward ? AgvDirection.Backward : AgvDirection.Forward; + var pathtarget = pathFinder.FindBasicPath(GTNode, targetNode, PrevNode, motdir); if (targetNode.DockDirection == DockingDirection.Backward && isMonitorLeft) { //턴을 하는 turnPatterns = GetTurnaroundPattern(GTNode, targetNode); - if (turnPatterns == null || turnPatterns.Any() == false) return new AGVPathResult { Success = false, ErrorMessage = $"회차 패턴 없음: Dir {PrevDirection}" }; + if (turnPatterns == null || turnPatterns.Any() == false) return new AGVPathResult { Success = false, Message = $"회차 패턴 없음: Dir {PrevDirection}" }; foreach (var item in turnPatterns) { var rfidvalue = ushort.Parse(item.Substring(0, 4)); @@ -2724,7 +2961,7 @@ namespace AGVSimulator.Forms if (pathtarget.DetailedPath.First().NodeId != rlt1.DetailedPath.Last().NodeId || pathtarget.DetailedPath.First().MotorDirection != rlt1.DetailedPath.Last().MotorDirection) { - new AGVPathResult { Success = false, ErrorMessage = $"게이트웨이 턴 마지막 주소와, 이 후 주소의 시작 노드ID가 일치하지 않습니다" }; + new AGVPathResult { Success = false, Message = $"게이트웨이 턴 마지막 주소와, 이 후 주소의 시작 노드ID가 일치하지 않습니다" }; } pathtarget.Path.RemoveAt(0); @@ -2800,120 +3037,6 @@ namespace AGVSimulator.Forms } } - private (bool result, string message) CalcPathBufferToBuffer(MapNode start, MapNode target, MapNode prev, AgvDirection prevDir, AgvDirection currentDir, VirtualAGV agv) - { - // Monitor Side 판단 로직 - // 현재 AGV의 물리적 방향(Monitor Side)이 "Right" 상태여야 버퍼 진입이 용이하다고 가정. - // Monitor Left 상태(부적절한 방향)라면 Gateway로 탈출해야 함. - - // 이동 벡터 X 변화량 - // prev가 없거나 start와 같으면 이동 방향을 알 수 없음 -> 이 경우 보수적으로 기존 로직(Backward면 탈출) 따름 - int deltaX = 0; - if (prev == null) return (false, "이전 노드 정보가 없습니다"); - else deltaX = start.Position.X - prev.Position.X; - - bool isMonitorLeft = false; - - if (deltaX > 0) // 오른쪽(Forward)으로 이동해 옴 (예: 2 -> 4) - { - // 이동방향(Right) + 전진(F) => Monitor Right (Good) - // 이동방향(Right) + 후진(B) => Monitor Left (Bad) - isMonitorLeft = (prevDir == AgvDirection.Backward); - } - else if (deltaX < 0) // 왼쪽(Reverse)으로 이동해 옴 (예: 4 -> 2) - { - // 이동방향(Left) + 전진(F) => Monitor Left (Bad) - // 이동방향(Left) + 후진(B) => Monitor Right (Good) - isMonitorLeft = (prevDir == AgvDirection.Forward); - } - else // 제자리 또는 수직 이동 - { - // 판단 불가 시 기존 로직(Backward면 Gateway) 유지 - return (false, "이전 노드와의 방향을 알 수 없습니다"); - } - - if (isMonitorLeft) - { - // Monitor Left 상태 (방향 불일치) -> Gateway로 탈출 - var GateWayNode = FindNode(6); - var escPath = _advancedPathfinder.FindBasicPath(start, GateWayNode, prev, prevDir); - if (!escPath.Success) return (false, "버퍼 탈출 경로 실패"); - - var lastNode = escPath.Path.Last(); // Should be GW6 - var lastPrev = escPath.Path[escPath.Path.Count - 2]; - var lastDir = escPath.DetailedPath.Last().MotorDirection; - - // Gateway 도착 후, Target(Buffer)으로 이동 - // 여기서부터는 "Monitor Right" 로직(즉, 적절한 방향 진입)을 적용합니다. - // 6번에서 Target이 왼쪽이면 Direct(Backward), 오른쪽이면 Overshoot(Forward->Backward) - - - bool isTargetLeftOfGW = target.Position.X < GateWayNode.Position.X; - AGVPathResult entryPath = null; - - //게이트웨이까지 후진으로 이동했다면 모니터방향이 오른쪽이다 => 방향전환필요 - var gateToTarget = GetPathFromGateway(GateWayNode, target, lastPrev, lastDir); - - escPath.Path.RemoveAt(escPath.Path.Count - 1); - escPath.DetailedPath.RemoveAt(escPath.DetailedPath.Count - 1); - - - var final = CombinePaths(escPath, gateToTarget); - ApplyResultToSimulator(final, agv); - UpdateAdvancedPathDebugInfo(final); - return (true, "버퍼 재진입(탈출후)"); - } - else - { - // Monitor Right 상태 (방향 일치) -> 직접 진입 또는 Overshoot - bool isTargetLeft = target.Position.X < start.Position.X; - - if (isTargetLeft) - { - var directPath = _advancedPathfinder.FindBasicPath(start, target, prev, AgvDirection.Backward); - ApplyResultToSimulator(directPath, agv); - UpdateAdvancedPathDebugInfo(directPath); - return (true, "버퍼 좌측이동(직접진입)"); - } - else - { - //대상이 나보다 우측에 있으니 RFID값이 읽어지는 위치까지 이동후에 다시 반대방향으로 마크스탑 해야 함 - //그냥 대상 노드까지 이동을 한다.. RFID값이 실제 멈추는 위치 이전에 있으니 그곳까지 이동하고 역방향 마크스탑하면 동일한 위치이다. 위 식은 그 이전노드까지 확실하게 이동하는 코드이다 - //var overshootNode = target.ConnectedMapNodes.OrderByDescending(n => n.Position.X).FirstOrDefault(); - //if (overshootNode == null || overshootNode.Position.X <= target.Position.X) - // return (false, "Overshoot 공간 부족"); - - var path1 = _advancedPathfinder.FindBasicPath(start, target, prev, AgvDirection.Forward); - if (path1.Path.Count < 2) return (false, "Overshoot 경로 생성 실패"); - - //목표에서 방향바꿔 마크스탑을 해야한다 - path1.Path.Add(path1.Path.Last()); - - //디테일은 방향바꿔서 추가 필요(속도는 저속처리해준다) - var lastDefailt = path1.DetailedPath.Last(); - path1.DetailedPath.Add(new NodeMotorInfo(lastDefailt.seq + 1, lastDefailt.NodeId, lastDefailt.RfidId, AgvDirection.Backward) - { - Speed = SpeedLevel.L, - IsPass = false, - }); - - //var p1Last = path1.Path.Last(); - //var p1Prev = path1.Path[path1.Path.Count > 1 ? path1.Path.Count - 2 : 0]; // Safety check - //var p1Dir = path1.DetailedPath.Last().MotorDirection; - - //var path2 = _advancedPathfinder.FindPath(start, target, p1Last, p1Dir, AgvDirection.Backward); - - //if (path2.Success && path2.DetailedPath.Last().NodeId == target.Id) - // path2.DetailedPath = path2.DetailedPath.Take(path2.DetailedPath.Count - 1).ToList(); - - //var final = CombinePaths(path1, path2); - ApplyResultToSimulator(path1, agv); - UpdateAdvancedPathDebugInfo(path1); - return (true, "버퍼 우측(Overshoot)"); - } - } - } - /// /// p1+p2 /// diff --git a/Cs_HMI/Project/StateMachine/_Xbee.cs b/Cs_HMI/Project/StateMachine/_Xbee.cs index de950c6..e287544 100644 --- a/Cs_HMI/Project/StateMachine/_Xbee.cs +++ b/Cs_HMI/Project/StateMachine/_Xbee.cs @@ -371,7 +371,7 @@ namespace Project { // 경로 실패시 디버깅 정보 초기화 //_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}"; - Message = $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}"; + Message = $"경로를 찾을 수 없습니다:\n{advancedResult.Message}"; advancedResult = null; }