diff --git a/Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs b/Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs index 6ad2a96..b8d79a0 100644 --- a/Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs +++ b/Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs @@ -35,10 +35,8 @@ namespace AGVMapEditor.Forms public class NodeConnectionInfo { public string FromNodeId { get; set; } - public string FromNodeName { get; set; } public ushort FromRfidId { get; set; } public string ToNodeId { get; set; } - public string ToNodeName { get; set; } public ushort ToRfidId { get; set; } public string ConnectionType { get; set; } @@ -46,12 +44,12 @@ namespace AGVMapEditor.Forms { // RFID가 있으면 RFID(노드이름), 없으면 NodeID(노드이름) 형태로 표시 string fromDisplay = FromRfidId > 0 - ? $"{FromRfidId}({FromNodeName})" - : $"---({FromNodeId})"; + ? $"{FromRfidId:0000}(*{FromNodeId.PadLeft(4,'0')})" + : $"(*{FromNodeId})"; string toDisplay = ToRfidId > 0 - ? $"{ToRfidId}({ToNodeName})" - : $"---({ToNodeId})"; + ? $"{ToRfidId:0000}(*{ToNodeId.PadLeft(4, '0')})" + : $"(*{ToNodeId})"; // 양방향 연결은 ↔ 기호 사용 string arrow = ConnectionType == "양방향" ? "↔" : "→"; @@ -828,7 +826,7 @@ namespace AGVMapEditor.Forms var item = node as MapNode; if (item.StationType == StationType.Normal) foreColor = Color.DimGray; - else if (item.StationType == StationType.Charger) + else if (item.StationType == StationType.Charger1 || item.StationType == StationType.Charger2) foreColor = Color.Red; else foreColor = Color.DarkGreen; @@ -907,10 +905,8 @@ namespace AGVMapEditor.Forms connections.Add(new NodeConnectionInfo { FromNodeId = firstNode.Id, - FromNodeName = "", FromRfidId = firstNode.RfidId, ToNodeId = secondNode.Id, - ToNodeName = "", ToRfidId = secondNode.RfidId, ConnectionType = "양방향" // 모든 연결이 양방향 }); @@ -923,6 +919,7 @@ namespace AGVMapEditor.Forms } // 리스트박스에 표시 + lstNodeConnection.Font = new Font("돋움체", 10); lstNodeConnection.DataSource = null; lstNodeConnection.DataSource = connections; lstNodeConnection.DisplayMember = "ToString"; diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs index acffaf0..e307c4f 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs @@ -594,7 +594,7 @@ namespace AGVNavigationCore.Controls { if (junctionNode == null) return; - int radius = isGateway ? 35 : 25; // 게이트웨이는 좀 더 크게 + int radius = isGateway ? 23 : 18; // 게이트웨이는 좀 더 크게 // 색상 결정: Gateway=진한 주황/골드, 일반 교차로=기존 파랑 Color fillColor = isGateway ? Color.FromArgb(100, 255, 140, 0) : Color.FromArgb(80, 70, 130, 200); @@ -674,7 +674,7 @@ namespace AGVNavigationCore.Controls { case NodeType.Normal: var item = _selectedNode as MapNode; - if (item.StationType == StationType.Charger) + if ( item.StationType == StationType.Charger1 || item.StationType == StationType.Charger2) DrawTriangleGhost(g, ghostBrush); else DrawPentagonGhost(g, ghostBrush); @@ -869,7 +869,8 @@ namespace AGVNavigationCore.Controls case StationType.Buffer: DrawPentagonNodeShape(g, node, brush); break; - case StationType.Charger: + case StationType.Charger1: + case StationType.Charger2: DrawTriangleNodeShape(g, node, brush); break; case StationType.Limit: @@ -1309,8 +1310,9 @@ namespace AGVNavigationCore.Controls Color fgColor = Color.Black; Color bgColor = Color.White; switch (node.StationType) - { - case StationType.Charger: + { + case StationType.Charger1: + case StationType.Charger2: fgColor = Color.White; bgColor = Color.Tomato; break; @@ -1607,7 +1609,8 @@ namespace AGVNavigationCore.Controls switch (node.StationType) { case StationType.Normal: bgColor = Color.DeepSkyBlue; break; - case StationType.Charger: bgColor = Color.Tomato; break; + case StationType.Charger1: bgColor = Color.Tomato; break; + case StationType.Charger2: bgColor = Color.Tomato; break; case StationType.Loader: case StationType.UnLoader: bgColor = Color.Gold; break; case StationType.Clearner: bgColor = Color.DeepSkyBlue; break; diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs index d094aee..45e91c5 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs @@ -492,7 +492,8 @@ namespace AGVNavigationCore.Controls case StationType.Clearner: case StationType.Buffer: return IsPointInPentagon(point, node); - case StationType.Charger: + case StationType.Charger2: + case StationType.Charger1: return IsPointInTriangle(point, node); default: return IsPointInCircle(point, node); diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs index b3bea81..6e72daf 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs @@ -72,8 +72,10 @@ namespace AGVNavigationCore.Models UnLoader, /// 버퍼 Buffer, - /// 충전기 - Charger, + /// 충전기1 + Charger1, + /// 충전기2 + Charger2, /// /// 끝점(더이상 이동불가) diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs index 35583af..8ad63d4 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs @@ -30,7 +30,8 @@ namespace AGVNavigationCore.Models if (StationType == StationType.Loader) return true; if (StationType == StationType.UnLoader) return true; if (StationType == StationType.Clearner) return true; - if (StationType == StationType.Charger) return true; + if (StationType == StationType.Charger1) return true; + if (StationType == StationType.Charger2) return true; return false; } } @@ -115,7 +116,7 @@ namespace AGVNavigationCore.Models { get { - if (StationType == StationType.Charger || StationType == StationType.Buffer || + if (StationType == StationType.Charger1 || StationType == StationType.Charger2 || StationType == StationType.Buffer || StationType == StationType.Clearner || StationType == StationType.Loader || StationType == StationType.UnLoader) return true; return false; @@ -141,10 +142,10 @@ namespace AGVNavigationCore.Models public void SetChargingStation(string stationId) { - StationType = StationType.Charger; - Id = stationId; - DockDirection = DockingDirection.Forward; - ModifiedDate = DateTime.Now; + //StationType = StationType.Charger; + //Id = stationId; + //DockDirection = DockingDirection.Forward; + //ModifiedDate = DateTime.Now; } public override string ToString() diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs index f62f41e..ace68de 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs @@ -101,31 +101,32 @@ namespace AGVNavigationCore.PathFinding.Core /// 시작 노드 ID /// 목적지 노드 ID /// 경로 계산 결과 - public AGVPathResult FindPathAStar(string startNodeId, string endNodeId) + public AGVPathResult FindPathAStar(MapNode start, MapNode end) { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); try { - if (!_nodeMap.ContainsKey(startNodeId)) + if (!_nodeMap.ContainsKey(start.Id)) { - return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds, 0); + return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {start.Id}", stopwatch.ElapsedMilliseconds, 0); } - if (!_nodeMap.ContainsKey(endNodeId)) + if (!_nodeMap.ContainsKey(end.Id)) { - return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds, 0); + return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {end.Id}", stopwatch.ElapsedMilliseconds, 0); } - if (startNodeId == endNodeId) + //출발지와 목적지가 동일한 경우 + if (start.Id == end.Id) { - var startMapNode = GetMapNode(startNodeId); - var singlePath = new List { startMapNode }; + //var startMapNode = GetMapNode(start); + var singlePath = new List { start }; return AGVPathResult.CreateSuccess(singlePath, new List(), 0, stopwatch.ElapsedMilliseconds); } - var startNode = _nodeMap[startNodeId]; - var endNode = _nodeMap[endNodeId]; + var startNode = _nodeMap[start.Id]; + var endNode = _nodeMap[end.Id]; var openSet = new List(); var closedSet = new HashSet(); var exploredCount = 0; @@ -142,7 +143,7 @@ namespace AGVNavigationCore.PathFinding.Core closedSet.Add(currentNode.NodeId); exploredCount++; - if (currentNode.NodeId == endNodeId) + if (currentNode.NodeId == end.Id) { var path = ReconstructPath(currentNode); var totalDistance = CalculatePathDistance(path); @@ -180,157 +181,157 @@ namespace AGVNavigationCore.PathFinding.Core } } - /// - /// 경유지를 거쳐 경로 찾기 (오버로드) - /// 여러 경유지를 순차적으로 거쳐서 최종 목적지까지의 경로를 계산합니다. - /// 기존 FindPath를 여러 번 호출하여 각 구간의 경로를 합칩니다. - /// - /// 시작 노드 ID - /// 최종 목적지 노드 ID - /// 경유지 노드 ID 배열 (선택사항) - /// 경로 계산 결과 (모든 경유지를 거친 전체 경로) - public AGVPathResult FindPath(string startNodeId, string endNodeId, params string[] waypointNodeIds) - { - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + ///// + ///// 경유지를 거쳐 경로 찾기 (오버로드) + ///// 여러 경유지를 순차적으로 거쳐서 최종 목적지까지의 경로를 계산합니다. + ///// 기존 FindPath를 여러 번 호출하여 각 구간의 경로를 합칩니다. + ///// + ///// 시작 노드 ID + ///// 최종 목적지 노드 ID + ///// 경유지 노드 ID 배열 (선택사항) + ///// 경로 계산 결과 (모든 경유지를 거친 전체 경로) + //public AGVPathResult FindPath(string startNodeId, string endNodeId, params string[] waypointNodeIds) + //{ + // var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - try - { - // 경유지가 없으면 기본 FindPath 호출 - if (waypointNodeIds == null || waypointNodeIds.Length == 0) - { - return FindPathAStar(startNodeId, endNodeId); - } + // try + // { + // // 경유지가 없으면 기본 FindPath 호출 + // if (waypointNodeIds == null || waypointNodeIds.Length == 0) + // { + // return FindPathAStar(startNodeId, endNodeId); + // } - // 경유지 유효성 검증 - var validWaypoints = new List(); - foreach (var waypointId in waypointNodeIds) - { - if (string.IsNullOrEmpty(waypointId)) - continue; + // // 경유지 유효성 검증 + // var validWaypoints = new List(); + // foreach (var waypointId in waypointNodeIds) + // { + // if (string.IsNullOrEmpty(waypointId)) + // continue; - if (!_nodeMap.ContainsKey(waypointId)) - { - return AGVPathResult.CreateFailure($"경유지 노드를 찾을 수 없습니다: {waypointId}", stopwatch.ElapsedMilliseconds, 0); - } + // if (!_nodeMap.ContainsKey(waypointId)) + // { + // return AGVPathResult.CreateFailure($"경유지 노드를 찾을 수 없습니다: {waypointId}", stopwatch.ElapsedMilliseconds, 0); + // } - validWaypoints.Add(waypointId); - } + // validWaypoints.Add(waypointId); + // } - // 경유지가 없으면 기본 경로 계산 - if (validWaypoints.Count == 0) - { - return FindPathAStar(startNodeId, endNodeId); - } + // // 경유지가 없으면 기본 경로 계산 + // if (validWaypoints.Count == 0) + // { + // return FindPathAStar(startNodeId, endNodeId); + // } - // 첫 번째 경유지가 시작노드와 같은지 검사 - if (validWaypoints[0] == startNodeId) - { - return AGVPathResult.CreateFailure( - $"첫 번째 경유지({validWaypoints[0]})가 시작 노드({startNodeId})와 동일합니다. 경유지는 시작노드와 달라야 합니다.", - stopwatch.ElapsedMilliseconds, 0); - } + // // 첫 번째 경유지가 시작노드와 같은지 검사 + // if (validWaypoints[0] == startNodeId) + // { + // return AGVPathResult.CreateFailure( + // $"첫 번째 경유지({validWaypoints[0]})가 시작 노드({startNodeId})와 동일합니다. 경유지는 시작노드와 달라야 합니다.", + // stopwatch.ElapsedMilliseconds, 0); + // } - // 마지막 경유지가 목적지노드와 같은지 검사 - if (validWaypoints[validWaypoints.Count - 1] == endNodeId) - { - return AGVPathResult.CreateFailure( - $"마지막 경유지({validWaypoints[validWaypoints.Count - 1]})가 목적지 노드({endNodeId})와 동일합니다. 경유지는 목적지노드와 달라야 합니다.", - stopwatch.ElapsedMilliseconds, 0); - } + // // 마지막 경유지가 목적지노드와 같은지 검사 + // if (validWaypoints[validWaypoints.Count - 1] == endNodeId) + // { + // return AGVPathResult.CreateFailure( + // $"마지막 경유지({validWaypoints[validWaypoints.Count - 1]})가 목적지 노드({endNodeId})와 동일합니다. 경유지는 목적지노드와 달라야 합니다.", + // stopwatch.ElapsedMilliseconds, 0); + // } - // 연속된 중복만 제거 (순서 유지) - // 예: [1, 2, 2, 3, 2] -> [1, 2, 3, 2] (연속 중복만 제거) - var deduplicatedWaypoints = new List(); - string lastWaypoint = null; - foreach (var waypoint in validWaypoints) - { - if (waypoint != lastWaypoint) - { - deduplicatedWaypoints.Add(waypoint); - lastWaypoint = waypoint; - } - } - validWaypoints = deduplicatedWaypoints; + // // 연속된 중복만 제거 (순서 유지) + // // 예: [1, 2, 2, 3, 2] -> [1, 2, 3, 2] (연속 중복만 제거) + // var deduplicatedWaypoints = new List(); + // string lastWaypoint = null; + // foreach (var waypoint in validWaypoints) + // { + // if (waypoint != lastWaypoint) + // { + // deduplicatedWaypoints.Add(waypoint); + // lastWaypoint = waypoint; + // } + // } + // validWaypoints = deduplicatedWaypoints; - // 최종 경로 리스트와 누적 값 - var combinedPath = new List(); - float totalDistance = 0; - long totalCalculationTime = 0; + // // 최종 경로 리스트와 누적 값 + // var combinedPath = new List(); + // float totalDistance = 0; + // long totalCalculationTime = 0; - // 현재 시작점 - string currentStart = startNodeId; + // // 현재 시작점 + // string currentStart = startNodeId; - // 1단계: 각 경유지까지의 경로 계산 - for (int i = 0; i < validWaypoints.Count; i++) - { - string waypoint = validWaypoints[i]; + // // 1단계: 각 경유지까지의 경로 계산 + // for (int i = 0; i < validWaypoints.Count; i++) + // { + // string waypoint = validWaypoints[i]; - // 현재 위치에서 경유지까지의 경로 계산 - var segmentResult = FindPathAStar(currentStart, waypoint); + // // 현재 위치에서 경유지까지의 경로 계산 + // var segmentResult = FindPathAStar(currentStart, waypoint); - if (!segmentResult.Success) - { - return AGVPathResult.CreateFailure( - $"경유지 {i + 1}({waypoint})까지의 경로 계산 실패: {segmentResult.ErrorMessage}", - stopwatch.ElapsedMilliseconds, 0); - } + // if (!segmentResult.Success) + // { + // return AGVPathResult.CreateFailure( + // $"경유지 {i + 1}({waypoint})까지의 경로 계산 실패: {segmentResult.ErrorMessage}", + // stopwatch.ElapsedMilliseconds, 0); + // } - // 경로 합치기 (첫 번째 구간이 아니면 시작점 제거하여 중복 방지) - if (combinedPath.Count > 0 && segmentResult.Path.Count > 0) - { - // 시작 노드 제거 (이전 경로의 마지막 노드와 동일) - combinedPath.AddRange(segmentResult.Path.Skip(1)); - } - else - { - combinedPath.AddRange(segmentResult.Path); - } + // // 경로 합치기 (첫 번째 구간이 아니면 시작점 제거하여 중복 방지) + // if (combinedPath.Count > 0 && segmentResult.Path.Count > 0) + // { + // // 시작 노드 제거 (이전 경로의 마지막 노드와 동일) + // combinedPath.AddRange(segmentResult.Path.Skip(1)); + // } + // else + // { + // combinedPath.AddRange(segmentResult.Path); + // } - totalDistance += segmentResult.TotalDistance; - totalCalculationTime += segmentResult.CalculationTimeMs; + // totalDistance += segmentResult.TotalDistance; + // totalCalculationTime += segmentResult.CalculationTimeMs; - // 다음 경유지의 시작점은 현재 경유지 - currentStart = waypoint; - } + // // 다음 경유지의 시작점은 현재 경유지 + // currentStart = waypoint; + // } - // 2단계: 마지막 경유지에서 최종 목적지까지의 경로 계산 - var finalSegmentResult = FindPathAStar(currentStart, endNodeId); + // // 2단계: 마지막 경유지에서 최종 목적지까지의 경로 계산 + // var finalSegmentResult = FindPathAStar(currentStart, endNodeId); - if (!finalSegmentResult.Success) - { - return AGVPathResult.CreateFailure( - $"최종 목적지까지의 경로 계산 실패: {finalSegmentResult.ErrorMessage}", - stopwatch.ElapsedMilliseconds, 0); - } + // if (!finalSegmentResult.Success) + // { + // return AGVPathResult.CreateFailure( + // $"최종 목적지까지의 경로 계산 실패: {finalSegmentResult.ErrorMessage}", + // stopwatch.ElapsedMilliseconds, 0); + // } - // 최종 경로 합치기 (시작점 제거) - if (combinedPath.Count > 0 && finalSegmentResult.Path.Count > 0) - { - combinedPath.AddRange(finalSegmentResult.Path.Skip(1)); - } - else - { - combinedPath.AddRange(finalSegmentResult.Path); - } + // // 최종 경로 합치기 (시작점 제거) + // if (combinedPath.Count > 0 && finalSegmentResult.Path.Count > 0) + // { + // combinedPath.AddRange(finalSegmentResult.Path.Skip(1)); + // } + // else + // { + // combinedPath.AddRange(finalSegmentResult.Path); + // } - totalDistance += finalSegmentResult.TotalDistance; - totalCalculationTime += finalSegmentResult.CalculationTimeMs; + // totalDistance += finalSegmentResult.TotalDistance; + // totalCalculationTime += finalSegmentResult.CalculationTimeMs; - stopwatch.Stop(); + // stopwatch.Stop(); - // 결과 생성 - return AGVPathResult.CreateSuccess( - combinedPath, - new List(), - totalDistance, - totalCalculationTime - ); - } - catch (Exception ex) - { - return AGVPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0); - } - } + // // 결과 생성 + // return AGVPathResult.CreateSuccess( + // combinedPath, + // new List(), + // totalDistance, + // totalCalculationTime + // ); + // } + // catch (Exception ex) + // { + // return AGVPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0); + // } + //} /// /// 두 경로 결과를 합치기 @@ -425,31 +426,31 @@ namespace AGVNavigationCore.PathFinding.Core } - /// - /// 여러 목적지 중 가장 가까운 노드로의 경로 찾기 - /// - /// 시작 노드 ID - /// 목적지 후보 노드 ID 목록 - /// 경로 계산 결과 - public AGVPathResult FindNearestPath(string startNodeId, List targetNodeIds) - { - if (targetNodeIds == null || targetNodeIds.Count == 0) - { - return AGVPathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0); - } + ///// + ///// 여러 목적지 중 가장 가까운 노드로의 경로 찾기 + ///// + ///// 시작 노드 ID + ///// 목적지 후보 노드 ID 목록 + ///// 경로 계산 결과 + //public AGVPathResult FindNearestPath(string startNodeId, List targetNodeIds) + //{ + // if (targetNodeIds == null || targetNodeIds.Count == 0) + // { + // return AGVPathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0); + // } - AGVPathResult bestResult = null; - foreach (var targetId in targetNodeIds) - { - var result = FindPathAStar(startNodeId, targetId); - if (result.Success && (bestResult == null || result.TotalDistance < bestResult.TotalDistance)) - { - bestResult = result; - } - } + // AGVPathResult bestResult = null; + // foreach (var targetId in targetNodeIds) + // { + // var result = FindPathAStar(startNodeId, targetId); + // if (result.Success && (bestResult == null || result.TotalDistance < bestResult.TotalDistance)) + // { + // bestResult = result; + // } + // } - return bestResult ?? AGVPathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0); - } + // return bestResult ?? AGVPathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0); + //} /// /// 휴리스틱 거리 계산 (유클리드 거리) diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs index 0bdba8e..55dfd37 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs @@ -111,329 +111,13 @@ namespace AGVNavigationCore.PathFinding.Planning return null; } - public AGVPathResult FindPath(MapNode startNode, MapNode targetNode) - { - // 기본값으로 경로 탐색 (이전 위치 = 현재 위치, 방향 = 전진) - return FindPath(startNode, targetNode, startNode, AgvDirection.Forward, AgvDirection.Forward, false); - } + public AGVPathResult FindPathAStar(MapNode startNode, MapNode targetNode) { // 기본값으로 경로 탐색 (이전 위치 = 현재 위치, 방향 = 전진) - return _basicPathfinder.FindPathAStar(startNode.Id, targetNode.Id); - } - public AGVPathResult FindPath(MapNode startNode, MapNode targetNode, - MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection, bool crossignore = false) - { - // 입력 검증 - if (startNode == null) - return AGVPathResult.CreateFailure("시작 노드가 null입니다.", 0, 0); - if (targetNode == null) - return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0); - if (prevNode == null) - return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0); - if (targetNode.isDockingNode == false && targetNode.Type != NodeType.Normal) - return AGVPathResult.CreateFailure("이동 가능한 노드가 아닙니다", 0, 0); - - var tnode = targetNode as MapNode; - - //시작노드와 종료노드가 동일위치이고 도킹방향도 맞다면 그대로 OK 한다 - if (startNode.Id == targetNode.Id && tnode.DockDirection.MatchAGVDirection(prevDirection)) - return AGVPathResult.CreateSuccess(new List { startNode, startNode }, new List(), 0, 0); - - //반대방향값 지정 - var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward); - - //1.목적지까지의 최단거리 경로를 찾는다.(AStar 방식) - var pathResult = _basicPathfinder.FindPathAStar(startNode.Id, targetNode.Id); - pathResult.PrevNode = prevNode; - pathResult.PrevDirection = prevDirection; - if (!pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0) - return AGVPathResult.CreateFailure("각 노드간 최단 경로 계산이 실패되었습니다", 0, 0); - - //정방향/역방향 이동 시 다음 노드 확인 - // 경로 계획 단계에서는 마그넷 방향이 미리 알려지지 않으므로 Straight로 기본값 사용 - - // ✅ 현재 방향 유지: prevDirection = currentDirection (방향 일관성) - var nextNodeForward = DirectionalHelper.GetNextNodeByDirection( - startNode, prevNode, currentDirection, currentDirection, MagnetDirection.Straight, _mapNodes); - - // ✅ 방향 전환: prevDirection = currentDirection, direction = ReverseDirection - var nextNodeBackward = DirectionalHelper.GetNextNodeByDirection( - startNode, prevNode, currentDirection, ReverseDirection, MagnetDirection.Straight, _mapNodes); - - - //2.AGV방향과 목적지에 설정된 방향이 일치하면 그대로 진행하면된다.(목적지에 방향이 없는 경우에도 그대로 진행) - if (tnode.DockDirection == DockingDirection.DontCare || - (tnode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward) || - (tnode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward)) - { - if ((nextNodeForward?.Id ?? string.Empty) == pathResult.Path[1].Id) //예측경로와 다음진행방향 경로가 일치하면 해당 방향이 맞다 - { - MakeDetailData(pathResult, currentDirection); - MakeMagnetDirection(pathResult); - for (int i = 0; i < pathResult.DetailedPath.Count; i++) - pathResult.DetailedPath[i].seq = i + 1; - return pathResult; - } - } - - - //2-1 현재위치의 반대방향과 대상의 방향이 맞는 경우에도 그대로 사용가능하다. - //if (targetNode.DockDirection == DockingDirection.DontCare || - // (targetNode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Backward) || - // (targetNode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Forward)) - //{ - // // 반대 방향으로 이동하여 목적지에 진입해야 함 - // // 현재 방향으로 가면서 역방향으로 도킹 - // MakeDetailData(pathResult, ReverseDirection); - // MakeMagnetDirection(pathResult); - // return pathResult; - //} - - - //뒤로 이동시 경로상의 처음 만나는 노드가 같다면 그 방향으로 이동하면 된다. - // ⚠️ 단, 현재 방향과 목적지 도킹 방향이 일치해야 함! - if (nextNodeBackward != null && pathResult.Path.Count > 1 && - nextNodeBackward.Id == pathResult.Path[1].Id) // ✅ 추가: 현재도 Backward여야 함 - { - if (tnode.DockDirection == DockingDirection.Forward && ReverseDirection == AgvDirection.Forward || - tnode.DockDirection == DockingDirection.Backward && ReverseDirection == AgvDirection.Backward) - { - MakeDetailData(pathResult, ReverseDirection); - MakeMagnetDirection(pathResult); - for (int i = 0; i < pathResult.DetailedPath.Count; i++) - pathResult.DetailedPath[i].seq = i + 1; - return pathResult; - } - } - - if (nextNodeForward != null && pathResult.Path.Count > 1 && - nextNodeForward.Id == pathResult.Path[1].Id) // ✅ 추가: 현재도 Forward여야 함 - { - if (tnode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward || - tnode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward) - { - MakeDetailData(pathResult, currentDirection); - MakeMagnetDirection(pathResult); - for (int i = 0; i < pathResult.DetailedPath.Count; i++) - pathResult.DetailedPath[i].seq = i + 1; - return pathResult; - } - - } - - //if(nextNodeForward.NodeId == pathResult.Path[1]) - //{ - // MakeDetailData(pathResult, currentDirection); - // MakeMagnetDirection(pathResult); - // return pathResult; - //} - - //현재 내 포인트가 교차로라면.. 무조건 왓던 방향 혹은 그 반대방향으로 이동해서 경로를 계산해야한다. - //교차로에 멈춰있을때에는 바로 방향전환을 할 수없으니. 정/역(straight)로 이동해서 다시 계산을 해야한다 - if (crossignore == false && startNode.ConnectedNodes.Count > 2) - { - //진행방향으로 이동했을때 나오는 노드를 사용한다. - if (nextNodeForward != null) - { - var Path0 = _basicPathfinder.FindPathAStar(startNode.Id, nextNodeForward.Id); - Path0.PrevNode = prevNode; - Path0.PrevDirection = prevDirection; - MakeDetailData(Path0, prevDirection); - - var Path1 = FindPath(nextNodeForward, targetNode, startNode, prevDirection, currentDirection, true); - Path1.PrevNode = startNode; - Path1.PrevDirection = prevDirection; - //MakeDetailData(Path1, ReverseDirection); - - var combinedResult0 = Path0; - combinedResult0 = _basicPathfinder.CombineResults(combinedResult0, Path1); - MakeMagnetDirection(combinedResult0); - - for (int i = 0; i < combinedResult0.DetailedPath.Count; i++) - combinedResult0.DetailedPath[i].seq = i + 1; - return combinedResult0; - } - else if (nextNodeBackward != null) - { - return AGVPathResult.CreateFailure("backward 처리코드가 없습니다 오류", 0, 0); - } - else - { - return AGVPathResult.CreateFailure("교차로에서 시작하는 조건중 forward/backrad 노드 검색 실패", 0, 0); - } - } - - - //3. 도킹방향이 일치하지 않으니 교차로에서 방향을 회전시켜야 한다 - //최단거리(=minpath)경로에 속하는 교차로가 있다면 그것을 사용하고 없다면 가장 가까운 교차로를 찾는다. - var JunctionInPath = FindNearestJunctionOnPath(pathResult); - if (JunctionInPath == null) - { - //시작노드로부터 가까운 교차로 검색 - JunctionInPath = FindNearestJunction(startNode); - - //종료노드로부터 가까운 교차로 검색 - if (JunctionInPath == null) JunctionInPath = FindNearestJunction(tnode); - } - if (JunctionInPath == null) - return AGVPathResult.CreateFailure("교차로가 없어 경로계산을 할 수 없습니다", 0, 0); - - //경유지를 포함하여 경로를 다시 계산한다. - - //1.시작위치 - 교차로(여기까지는 현재 방향으로 그대로 이동을 한다) - var path1 = _basicPathfinder.FindPathAStar(startNode.Id, JunctionInPath.Id); - path1.PrevNode = prevNode; - path1.PrevDirection = prevDirection; - - //다음좌표를 보고 교차로가 진행방향인지 반대방향인지 체크한다. - bool ReverseCheck = false; - //if (path1.Path.Count > 1 && nextNodeForward != null && nextNodeForward.NodeId.Equals(path1.Path[1].NodeId)) - //{ - // ReverseCheck = false; //현재 진행 방향으로 이동해야한다 - // MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정) - //} - if (path1.Path.Count > 1 && nextNodeBackward != null && nextNodeBackward.Id.Equals(path1.Path[1].Id)) - { - ReverseCheck = true; //현재 방향의 반대방향으로 이동해야한다 - MakeDetailData(path1, ReverseDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정) - } - else - { - ReverseCheck = false; //현재 진행 방향으로 이동해야한다 - MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정) - } - - - - - - - //2.교차로 - 종료위치 - var path2 = _basicPathfinder.FindPathAStar(JunctionInPath.Id, targetNode.Id); - path2.PrevNode = prevNode; - path2.PrevDirection = prevDirection; - - //2번paths느최종 목적지 이므로 목적지와 도킹방향 확인해서 결정한다 - if ((path2.Path.Last().DockDirection == DockingDirection.Forward && ReverseDirection == AgvDirection.Forward) || - (path2.Path.Last().DockDirection == DockingDirection.Backward && ReverseDirection == AgvDirection.Backward)) - { - MakeDetailData(path2, ReverseDirection); - } - else if ((path2.Path.Last().DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward) || - (path2.Path.Last().DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward)) - { - MakeDetailData(path2, currentDirection); - } - - MapNode tempNode = null; - - - //3.방향전환을 위환 대체 노드찾기 - tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.Id, - path1.Path[path1.Path.Count - 2].Id, - path2.Path[1].Id); - - //4. path1 + tempnode + path2 가 최종 위치가 된다. - if (tempNode == null) - return AGVPathResult.CreateFailure("방향 전환을 위한 대체 노드를 찾을 수 없습니다.", 0, 0); - - - - // path1 (시작 → 교차로) - var combinedResult = path1; - - //교차로 대체노드를 사용한 경우 - //if (tempNode != null) - { - // 교차로 → 대체노드 경로 계산 - var pathToTemp = _basicPathfinder.FindPathAStar(JunctionInPath.Id, tempNode.Id); - pathToTemp.PrevNode = JunctionInPath; - pathToTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection); - if (!pathToTemp.Success) - return AGVPathResult.CreateFailure("교차로에서 대체 노드까지의 경로를 찾을 수 없습니다.", 0, 0); - if (ReverseCheck) MakeDetailData(pathToTemp, ReverseDirection); - else MakeDetailData(pathToTemp, currentDirection); - - //교차로찍고 원래방향으로 돌어가야한다. - if (pathToTemp.DetailedPath.Count > 1) - pathToTemp.DetailedPath[pathToTemp.DetailedPath.Count - 1].MotorDirection = currentDirection; - - // path1 + pathToTemp 합치기 - combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp); - - // 대체노드 → 교차로 경로 계산 (역방향) - var pathFromTemp = _basicPathfinder.FindPathAStar(tempNode.Id, JunctionInPath.Id); - pathFromTemp.PrevNode = JunctionInPath; - pathFromTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection); - if (!pathFromTemp.Success) - return AGVPathResult.CreateFailure("대체 노드에서 교차로까지의 경로를 찾을 수 없습니다.", 0, 0); - - if (ReverseCheck) MakeDetailData(pathFromTemp, currentDirection); - else MakeDetailData(pathFromTemp, ReverseDirection); - - // (path1 + pathToTemp) + pathFromTemp 합치기 - combinedResult = _basicPathfinder.CombineResults(combinedResult, pathFromTemp); - - //현재까지 노드에서 목적지까지의 방향이 일치하면 그대로 사용한다. - bool temp3ok = false; - var TempCheck3 = _basicPathfinder.FindPathAStar(combinedResult.Path.Last().Id, targetNode.Id); - if (TempCheck3.Path.First().Id.Equals(combinedResult.Path.Last().Id)) - { - if (tnode.DockDirection == DockingDirection.Forward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Forward) - { - temp3ok = true; - } - else if (tnode.DockDirection == DockingDirection.Backward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Backward) - { - temp3ok = true; - } - } - - - - //대체노드에서 최종 목적지를 다시 확인한다. - if (temp3ok == false) - { - //목적지와 방향이 맞지 않다. 그러므로 대체노드를 추가로 더 찾아야한다. - var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.Id, - combinedResult.Path[combinedResult.Path.Count - 2].Id, - path2.Path[1].Id); - - var pathToTemp2 = _basicPathfinder.FindPathAStar(JunctionInPath.Id, tempNode2.Id); - if (ReverseCheck) MakeDetailData(pathToTemp2, currentDirection); - else MakeDetailData(pathToTemp2, ReverseDirection); - - combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp2); - - //교차로찍고 원래방향으로 돌어가야한다. - if (combinedResult.DetailedPath.Count > 1) - { - if (ReverseCheck) - combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = ReverseDirection; - else - combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = currentDirection; - } - - var pathToTemp3 = _basicPathfinder.FindPathAStar(tempNode2.Id, JunctionInPath.Id); - if (ReverseCheck) MakeDetailData(pathToTemp3, ReverseDirection); - else MakeDetailData(pathToTemp3, currentDirection); - - combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp3); - } - } - - // (path1 + pathToTemp + pathFromTemp) + path2 합치기 - combinedResult = _basicPathfinder.CombineResults(combinedResult, path2); - - MakeMagnetDirection(combinedResult); - - for (int i = 0; i < combinedResult.DetailedPath.Count; i++) - combinedResult.DetailedPath[i].seq = i + 1; - return combinedResult; - - + return _basicPathfinder.FindPathAStar(startNode, targetNode); } + /// /// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요 @@ -449,7 +133,7 @@ namespace AGVNavigationCore.PathFinding.Planning return AGVPathResult.CreateFailure("노드 정보 오류", 0, 0); // 2. A* 경로 탐색 - var pathResult = _basicPathfinder.FindPathAStar(startNode.Id, targetNode.Id); + var pathResult = _basicPathfinder.FindPathAStar(startNode, targetNode); pathResult.PrevNode = prevNode; pathResult.PrevDirection = prevDirection; diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs index 79a1686..7b9838d 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs @@ -65,716 +65,7 @@ namespace AGVNavigationCore.PathFinding.Planning _pathfinder.SetMapNodes(_mapNodes); } - /// - /// 방향 전환이 필요한 경로 계획 - /// - public DirectionChangePlan PlanDirectionChange(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) - { - // 방향이 같으면 직접 경로 계산 - if (currentDirection == requiredDirection) - { - var directPath = _pathfinder.FindPathAStar(startNodeId, targetNodeId); - if (directPath.Success) - { - return DirectionChangePlan.CreateSuccess( - directPath.Path, - null, - "방향 전환 불필요 - 직접 경로 사용" - ); - } - } - // 방향 전환이 필요한 경우 - 먼저 간단한 직접 경로 확인 - var directPath2 = _pathfinder.FindPathAStar(startNodeId, targetNodeId); - if (directPath2.Success) - { - // 직접 경로에 갈림길이 포함된 경우 그 갈림길에서 방향 전환 - foreach (var node in directPath2.Path.Skip(1).Take(directPath2.Path.Count - 2)) // 시작과 끝 제외 - { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(node.Id); - if (junctionInfo != null && junctionInfo.IsJunction) - { - // 간단한 방향 전환: 직접 경로 사용하되 방향 전환 노드 표시 - return DirectionChangePlan.CreateSuccess( - directPath2.Path, - node.Id, - $"갈림길 {node.Id}에서 방향 전환: {currentDirection} → {requiredDirection}" - ); - } - } - } - // 복잡한 방향 전환이 필요한 경우 - return PlanDirectionChangeRoute(startNodeId, targetNodeId, currentDirection, requiredDirection); - } - - /// - /// 방향 전환 경로 계획 - /// - private DirectionChangePlan PlanDirectionChangeRoute(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) - { - // 1. 방향 전환 가능한 갈림길 찾기 - var changeJunctions = FindSuitableChangeJunctions(startNodeId, targetNodeId, currentDirection, requiredDirection); - - if (changeJunctions.Count == 0) - { - return DirectionChangePlan.CreateFailure("방향 전환 가능한 갈림길을 찾을 수 없습니다."); - } - - // 2. 각 갈림길에 대해 경로 계획 시도 - foreach (var junction in changeJunctions) - { - var plan = TryDirectionChangeAtJunction(startNodeId, targetNodeId, junction, currentDirection, requiredDirection); - if (plan.Success) - { - return plan; - } - } - - return DirectionChangePlan.CreateFailure("모든 갈림길에서 방향 전환 경로 계획이 실패했습니다."); - } - - /// - /// 방향 전환에 적합한 갈림길 검색 (인근 우회 경로 우선) - /// - private List FindSuitableChangeJunctions(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) - { - var suitableJunctions = new List(); - - // 1. 시작점 인근의 갈림길들을 우선 검색 (경로 진행 중 우회용) - var nearbyJunctions = FindNearbyJunctions(startNodeId, 2); // 2단계 내의 갈림길 - foreach (var junction in nearbyJunctions) - { - if (junction == startNodeId) continue; // 시작점 제외 - - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junction); - if (junctionInfo != null && junctionInfo.IsJunction) - { - // 이 갈림길을 통해 목적지로 갈 수 있는지 확인 - if (CanReachTargetViaJunction(junction, targetNodeId) && - HasSuitableDetourOptions(junction, startNodeId)) - { - suitableJunctions.Add(junction); - } - } - } - - // 2. 직진 경로상의 갈림길들도 검색 (단, 되돌아가기 방지) - var directPath = _pathfinder.FindPathAStar(startNodeId, targetNodeId); - if (directPath.Success) - { - foreach (var node in directPath.Path.Skip(2)) // 시작점과 다음 노드는 제외 - { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(node.Id); - if (junctionInfo != null && junctionInfo.IsJunction) - { - // 직진 경로상에서는 더 엄격한 조건 적용 - if (!suitableJunctions.Contains(node.Id) && - HasMultipleExitOptions(node.Id)) - { - suitableJunctions.Add(node.Id); - } - } - } - } - - // 거리순으로 정렬 (가까운 갈림길 우선 - 인근 우회용) - return SortJunctionsByDistance(startNodeId, suitableJunctions); - } - - /// - /// 특정 노드 주변의 갈림길 검색 - /// - private List FindNearbyJunctions(string nodeId, int maxSteps) - { - var junctions = new List(); - var visited = new HashSet(); - var queue = new Queue<(string NodeId, int Steps)>(); - - queue.Enqueue((nodeId, 0)); - visited.Add(nodeId); - - while (queue.Count > 0) - { - var (currentNodeId, steps) = queue.Dequeue(); - - if (steps > maxSteps) continue; - - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(currentNodeId); - if (junctionInfo != null && junctionInfo.IsJunction && currentNodeId != nodeId) - { - junctions.Add(currentNodeId); - } - - // 연결된 노드들을 큐에 추가 - var connectedNodes = GetAllConnectedNodes(currentNodeId); - foreach (var connectedId in connectedNodes) - { - if (!visited.Contains(connectedId)) - { - visited.Add(connectedId); - queue.Enqueue((connectedId, steps + 1)); - } - } - } - - return junctions; - } - - /// - /// 양방향 연결을 고려한 연결 노드 검색 - /// - private List GetAllConnectedNodes(string nodeId) - { - var node = _mapNodes.FirstOrDefault(n => n.Id == nodeId); - if (node == null) return new List(); - - var connected = new HashSet(); - - // 직접 연결 - foreach (var connectedNode in node.ConnectedMapNodes) - { - if (connectedNode != null) - { - connected.Add(connectedNode.Id); - } - } - - // 역방향 연결 - foreach (var otherNode in _mapNodes) - { - if (otherNode.Id != nodeId && otherNode.ConnectedMapNodes.Any(n => n.Id == nodeId)) - { - connected.Add(otherNode.Id); - } - } - - return connected.ToList(); - } - - /// - /// 갈림길을 거리순으로 정렬 - /// - private List SortJunctionsByDistance(string startNodeId, List junctions) - { - var distances = new List<(string NodeId, double Distance)>(); - - foreach (var junction in junctions) - { - var path = _pathfinder.FindPathAStar(startNodeId, junction); - double distance = path.Success ? path.TotalDistance : double.MaxValue; - distances.Add((junction, distance)); - } - - return distances.OrderBy(d => d.Distance).Select(d => d.NodeId).ToList(); - } - - /// - /// 특정 갈림길에서 방향 전환 시도 - /// - private DirectionChangePlan TryDirectionChangeAtJunction(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) - { - try - { - // 방향 전환 경로 생성 - var changePath = GenerateDirectionChangePath(startNodeId, targetNodeId, junctionNodeId, currentDirection, requiredDirection); - - if (changePath.Count > 0) - { - // **VALIDATION**: 되돌아가기 패턴 검증 - var validationResult = ValidateDirectionChangePath(changePath, startNodeId, junctionNodeId); - if (!validationResult.IsValid) - { - System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ❌ 갈림길 {junctionNodeId} 경로 검증 실패: {validationResult.ValidationError}"); - return DirectionChangePlan.CreateFailure($"갈림길 {junctionNodeId} 검증 실패: {validationResult.ValidationError}"); - } - - // 실제 방향 전환 노드 찾기 (우회 노드) - string actualDirectionChangeNode = FindActualDirectionChangeNode(changePath, junctionNodeId); - - string description = $"갈림길 {GetDisplayName(junctionNodeId)}를 통해 {GetDisplayName(actualDirectionChangeNode)}에서 방향 전환: {currentDirection} → {requiredDirection}"; - System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ✅ 유효한 방향전환 경로: {string.Join(" → ", changePath.Select(n => n.Id))}"); - return DirectionChangePlan.CreateSuccess(changePath, actualDirectionChangeNode, description); - } - - return DirectionChangePlan.CreateFailure($"갈림길 {junctionNodeId}에서 방향 전환 경로 생성 실패"); - } - catch (Exception ex) - { - return DirectionChangePlan.CreateFailure($"갈림길 {junctionNodeId}에서 오류: {ex.Message}"); - } - } - - /// - /// 방향 전환 경로 생성 (인근 갈림길 우회 방식) - /// - private List GenerateDirectionChangePath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) - { - var fullPath = new List(); - - // 1. 시작점에서 갈림길까지의 경로 - var toJunctionPath = _pathfinder.FindPathAStar(startNodeId, junctionNodeId); - if (!toJunctionPath.Success) - return fullPath; - - // 2. 인근 갈림길을 통한 우회인지, 직진 경로상 갈림길인지 판단 - var directPath = _pathfinder.FindPathAStar(startNodeId, targetNodeId); - bool isNearbyDetour = !directPath.Success || !directPath.Path.Any(n => n.Id == junctionNodeId); - - if (isNearbyDetour) - { - // 인근 갈림길 우회: 직진하다가 마그넷으로 방향 전환 - return GenerateNearbyDetourPath(startNodeId, targetNodeId, junctionNodeId, currentDirection, requiredDirection); - } - else - { - // 직진 경로상 갈림길: 기존 방식으로 처리 (단, 되돌아가기 방지) - return GenerateDirectPathChangeRoute(startNodeId, targetNodeId, junctionNodeId, currentDirection, requiredDirection); - } - } - - /// - /// 인근 갈림길을 통한 우회 경로 생성 (예: 012 → 013 → 마그넷으로 016 방향) - /// - private List GenerateNearbyDetourPath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) - { - var fullPath = new List(); - - // 1. 시작점에서 갈림길까지 직진 (현재 방향 유지) - var toJunctionPath = _pathfinder.FindPathAStar(startNodeId, junctionNodeId); - if (!toJunctionPath.Success) - return fullPath; - - fullPath.AddRange(toJunctionPath.Path); - - // 2. 갈림길에서 방향 전환 후 목적지로 - // 이때 마그넷 센서를 이용해 목적지 방향으로 진입 - var fromJunctionPath = _pathfinder.FindPathAStar(junctionNodeId, targetNodeId); - if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1) - { - fullPath.AddRange(fromJunctionPath.Path.Skip(1)); // 중복 노드 제거 - } - - return fullPath; - } - - /// - /// 직진 경로상 갈림길에서 방향 전환 경로 생성 (기존 방식 개선) - /// - private List GenerateDirectPathChangeRoute(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) - { - var fullPath = new List(); - - // 1. 시작점에서 갈림길까지의 경로 - var toJunctionPath = _pathfinder.FindPathAStar(startNodeId, junctionNodeId); - if (!toJunctionPath.Success) - return fullPath; - - fullPath.AddRange(toJunctionPath.Path); - - // 2. 갈림길에서 방향 전환 처리 (되돌아가기 방지) - if (currentDirection != requiredDirection) - { - string fromNodeId = toJunctionPath.Path.Count >= 2 ? - toJunctionPath.Path[toJunctionPath.Path.Count - 2].Id : startNodeId; - - var changeSequence = GenerateDirectionChangeSequence(junctionNodeId, fromNodeId, currentDirection, requiredDirection); - if (changeSequence.Count > 1) - { - fullPath.AddRange(changeSequence.Skip(1).Select(nodeId => _mapNodes.FirstOrDefault(n => n.Id == nodeId)).Where(n => n != null)); - } - } - - // 3. 갈림길에서 목표점까지의 경로 - string lastNode = fullPath.LastOrDefault()?.Id ?? junctionNodeId; - var fromJunctionPath = _pathfinder.FindPathAStar(lastNode, targetNodeId); - if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1) - { - fullPath.AddRange(fromJunctionPath.Path.Skip(1)); - } - - return fullPath; - } - - /// - /// 갈림길에서 방향 전환 시퀀스 생성 - /// 물리적으로 실현 가능한 방향 전환 경로 생성 - /// - private List GenerateDirectionChangeSequence(string junctionNodeId, string fromNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) - { - var sequence = new List { junctionNodeId }; - - // 방향이 같으면 변경 불필요 - if (currentDirection == requiredDirection) - return sequence; - - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junctionNodeId); - if (junctionInfo == null || !junctionInfo.IsJunction) - return sequence; - - // 물리적으로 실현 가능한 방향 전환 시퀀스 생성 - // 핵심 원리: AGV는 RFID 태그를 읽자마자 바로 방향전환하면 안됨 - // 왔던 길로 되돌아가지 않도록 다른 노드로 우회한 후 방향전환 - var connectedNodes = junctionInfo.ConnectedNodes; - - // 왔던 노드(fromNodeId)를 제외한 연결 노드들만 후보로 선택 - // 이렇게 해야 AGV가 되돌아가는 것을 방지할 수 있음 - var availableNodes = connectedNodes.Where(nodeId => nodeId != fromNodeId).ToList(); - - if (availableNodes.Count > 0) - { - // 방향 전환을 위한 우회 경로 생성 - // 예시: 003→004(전진) 상태에서 후진 필요한 경우 - // 잘못된 방법: 004→003 (왔던 길로 되돌아감) - // 올바른 방법: 004→005→004 (005로 우회하여 방향전환) - - // 가장 적합한 우회 노드 선택 (직진 방향 우선, 각도 변화 최소) - string detourNode = FindBestDetourNode(junctionNodeId, availableNodes, fromNodeId); - if (!string.IsNullOrEmpty(detourNode)) - { - // 1단계: 갈림길에서 우회 노드로 이동 (현재 방향 유지) - // AGV는 계속 전진하여 한 태그 더 지나감 - sequence.Add(detourNode); - - // 2단계: 우회 노드에서 갈림길로 다시 돌아옴 (요구 방향으로 변경) - // 이때 AGV는 안전한 위치에서 방향을 전환할 수 있음 - sequence.Add(junctionNodeId); - } - } - else - { - // 사용 가능한 우회 노드가 없는 경우 (2갈래 길목) - // 이 경우 물리적으로 방향 전환이 불가능할 수 있음 - // 별도의 처리 로직이 필요할 수 있음 - return sequence; - } - - return sequence; - } - - /// - /// 방향 전환을 위한 최적의 우회 노드 선택 - /// AGV의 물리적 특성을 고려한 각도 기반 선택 - /// - private string FindBestDetourNode(string junctionNodeId, List availableNodes, string excludeNodeId) - { - // 왔던 길(excludeNodeId)를 제외한 노드 중에서 최적의 우회 노드 선택 - // 우선순위: 1) 막다른 길이 아닌 노드 (우회 후 복귀 가능) 2) 직진방향 3) 목적지 방향 - - var junctionNode = _mapNodes.FirstOrDefault(n => n.Id == junctionNodeId); - var fromNode = _mapNodes.FirstOrDefault(n => n.Id == excludeNodeId); - - if (junctionNode == null || fromNode == null) - return availableNodes.FirstOrDefault(); - - string bestNode = null; - double minAngleChange = double.MaxValue; - bool foundNonDeadEnd = false; - - // AGV가 들어온 방향 벡터 계산 (fromNode → junctionNode) - double incomingAngle = CalculateAngle(fromNode.Position, junctionNode.Position); - - foreach (var nodeId in availableNodes) - { - if (nodeId == excludeNodeId) continue; // 왔던 길 제외 - - var candidateNode = _mapNodes.FirstOrDefault(n => n.Id == nodeId); - if (candidateNode == null) continue; - - // 갈림길에서 후보 노드로의 방향 벡터 계산 (junctionNode → candidateNode) - double outgoingAngle = CalculateAngle(junctionNode.Position, candidateNode.Position); - - // 방향 변화 각도 계산 (0도가 직진, 180도가 유턴) - double angleChange = CalculateAngleChange(incomingAngle, outgoingAngle); - - // 막다른 길 여부 확인 - var nodeConnections = GetAllConnectedNodes(nodeId); - bool isDeadEnd = nodeConnections.Count <= 1; - - // 최적 노드 선택 로직 - bool shouldUpdate = false; - - if (!foundNonDeadEnd && !isDeadEnd) - { - // 첫 번째 막다른 길이 아닌 노드 발견 - shouldUpdate = true; - foundNonDeadEnd = true; - } - else if (foundNonDeadEnd && isDeadEnd) - { - // 이미 막다른 길이 아닌 노드를 찾았으므로 막다른 길은 제외 - continue; - } - else if (foundNonDeadEnd == isDeadEnd) - { - // 같은 조건(둘 다 막다른길 or 둘 다 아님)에서는 각도가 작은 것 선택 - shouldUpdate = angleChange < minAngleChange; - } - - if (shouldUpdate) - { - minAngleChange = angleChange; - bestNode = nodeId; - } - } - - return bestNode ?? availableNodes.FirstOrDefault(n => n != excludeNodeId); - } - - /// - /// 두 점 사이의 각도 계산 (라디안 단위) - /// - private double CalculateAngle(System.Drawing.Point from, System.Drawing.Point to) - { - double dx = to.X - from.X; - double dy = to.Y - from.Y; - return Math.Atan2(dy, dx); - } - - /// - /// 두 방향 사이의 각도 변화량 계산 (0~180도 범위) - /// 0도에 가까울수록 직진, 180도에 가까울수록 유턴 - /// - private double CalculateAngleChange(double fromAngle, double toAngle) - { - // 각도 차이 계산 - double angleDiff = Math.Abs(toAngle - fromAngle); - - // 0~π 범위로 정규화 (0~180도) - if (angleDiff > Math.PI) - { - angleDiff = 2 * Math.PI - angleDiff; - } - - return angleDiff; - } - - /// - /// 실제 방향 전환이 일어나는 노드 찾기 - /// - private string FindActualDirectionChangeNode(List changePath, string junctionNodeId) - { - // 방향전환 경로 구조: [start...junction, detourNode, junction...target] - // 실제 방향전환은 detourNode에서 일어남 (AGV가 한 태그 더 지나간 후) - - if (changePath.Count < 3) - return junctionNodeId; // 기본값으로 갈림길 반환 - - // 갈림길이 두 번 나타나는 위치 찾기 - int firstJunctionIndex = changePath.FindIndex(n => n.Id == junctionNodeId); - int lastJunctionIndex = -1; - for (int i = changePath.Count - 1; i >= 0; i--) - { - if (changePath[i].Id == junctionNodeId) - { - lastJunctionIndex = i; - break; - } - } - - // 갈림길이 두 번 나타나고, 그 사이에 노드가 있는 경우 - if (firstJunctionIndex != -1 && lastJunctionIndex != -1 && - firstJunctionIndex != lastJunctionIndex && lastJunctionIndex - firstJunctionIndex == 2) - { - // 첫 번째와 두 번째 갈림길 사이에 있는 노드가 실제 방향전환 노드 - string detourNode = changePath[firstJunctionIndex + 1].Id; - return detourNode; - } - - // 방향전환 구조를 찾지 못한 경우 기본값 반환 - return junctionNodeId; - } - - /// - /// 갈림길에서 적절한 우회 옵션이 있는지 확인 - /// - private bool HasSuitableDetourOptions(string junctionNodeId, string excludeNodeId) - { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junctionNodeId); - if (junctionInfo == null || !junctionInfo.IsJunction) - return false; - - // 제외할 노드(직전 노드)를 뺀 연결된 노드가 2개 이상이어야 적절한 우회 가능 - var availableConnections = junctionInfo.ConnectedNodes - .Where(nodeId => nodeId != excludeNodeId) - .ToList(); - - // 최소 2개의 우회 옵션이 있어야 함 (갈림길에서 방향전환 후 다시 나갈 수 있어야 함) - return availableConnections.Count >= 2; - } - - /// - /// 갈림길을 통해 목적지에 도달할 수 있는지 확인 - /// - private bool CanReachTargetViaJunction(string junctionNodeId, string targetNodeId) - { - // 갈림길에서 목적지까지의 경로가 존재하는지 확인 - var pathToTarget = _pathfinder.FindPathAStar(junctionNodeId, targetNodeId); - return pathToTarget.Success; - } - - /// - /// 갈림길에서 여러 출구 옵션이 있는지 확인 (직진 경로상 갈림길용) - /// - private bool HasMultipleExitOptions(string junctionNodeId) - { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junctionNodeId); - if (junctionInfo == null || !junctionInfo.IsJunction) - return false; - - // 최소 3개 이상의 연결 노드가 있어야 적절한 방향전환 가능 - return junctionInfo.ConnectedNodes.Count >= 3; - } - - /// - /// 방향전환 경로 검증 - 되돌아가기 패턴 및 물리적 실현성 검증 - /// - private PathValidationResult ValidateDirectionChangePath(List path, string startNodeId, string junctionNodeId) - { - if (path == null || path.Count == 0) - { - return PathValidationResult.CreateInvalid(startNodeId, "", "경로가 비어있습니다."); - } - - // 1. 되돌아가기 패턴 검증 (A → B → A) - var backtrackingPatterns = DetectBacktrackingPatterns(path); - if (backtrackingPatterns.Count > 0) - { - var issues = new List(); - foreach (var pattern in backtrackingPatterns) - { - issues.Add($"되돌아가기 패턴 발견: {pattern}"); - } - - string errorMessage = $"되돌아가기 패턴 검출 ({backtrackingPatterns.Count}개): {string.Join(", ", issues)}"; - System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 경로: {string.Join(" → ", path.Select(n => n.Id))}"); - System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 되돌아가기 패턴: {errorMessage}"); - - return PathValidationResult.CreateInvalidWithBacktracking( - path.Select(n => n.Id).ToList(), backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage); - } - - // 2. 연속된 중복 노드 검증 - var duplicates = DetectConsecutiveDuplicates(path); - if (duplicates.Count > 0) - { - string errorMessage = $"연속된 중복 노드 발견: {string.Join(", ", duplicates)}"; - return PathValidationResult.CreateInvalid(startNodeId, "", errorMessage); - } - - // 3. 경로 연결성 검증 - var connectivity = ValidatePathConnectivity(path); - if (!connectivity.IsValid) - { - return PathValidationResult.CreateInvalid(startNodeId, "", $"경로 연결성 오류: {connectivity.ValidationError}"); - } - - // 4. 갈림길 포함 여부 검증 - if (!path.Any(n => n.Id == junctionNodeId)) - { - return PathValidationResult.CreateInvalid(startNodeId, "", $"갈림길 {junctionNodeId}이 경로에 포함되지 않음"); - } - - System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path.Select(n => n.Id))}"); - return PathValidationResult.CreateValid(path.Select(n => n.Id).ToList(), startNodeId, "", junctionNodeId); - } - - /// - /// 되돌아가기 패턴 검출 (A → B → A) - /// - private List DetectBacktrackingPatterns(List path) - { - var patterns = new List(); - - for (int i = 0; i < path.Count - 2; i++) - { - string nodeA = path[i].Id; - string nodeB = path[i + 1].Id; - string nodeC = path[i + 2].Id; - - // A → B → A 패턴 검출 - if (nodeA == nodeC && nodeA != nodeB) - { - var pattern = BacktrackingPattern.Create(nodeA, nodeB, nodeA, i, i + 2); - patterns.Add(pattern); - } - } - - return patterns; - } - - /// - /// 연속된 중복 노드 검출 - /// - private List DetectConsecutiveDuplicates(List path) - { - var duplicates = new List(); - - for (int i = 0; i < path.Count - 1; i++) - { - if (path[i].Id == path[i + 1].Id) - { - duplicates.Add(path[i].Id); - } - } - - return duplicates; - } - - /// - /// 경로 연결성 검증 - /// - private PathValidationResult ValidatePathConnectivity(List path) - { - for (int i = 0; i < path.Count - 1; i++) - { - string currentNode = path[i].Id; - string nextNode = path[i + 1].Id; - - // 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedMapNodes 리스트 사용) - var currentMapNode = _mapNodes.FirstOrDefault(n => n.Id == currentNode); - if (currentMapNode == null || !currentMapNode.ConnectedMapNodes.Any(n => n.Id == nextNode)) - { - return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음"); - } - } - - return PathValidationResult.CreateNotRequired(); - } - - /// - /// 두 점 사이의 거리 계산 - /// - private float CalculateDistance(System.Drawing.Point p1, System.Drawing.Point p2) - { - float dx = p2.X - p1.X; - float dy = p2.Y - p1.Y; - return (float)Math.Sqrt(dx * dx + dy * dy); - } - - - /// - /// 경로 계획 요약 정보 - /// - public string GetPlanSummary() - { - var junctions = _junctionAnalyzer.GetJunctionSummary(); - return string.Join("\n", junctions); - } - - /// - /// 노드의 표시명 가져오기 (RFID 우선, 없으면 (NodeID) 형태) - /// - /// 노드 ID - /// 표시할 이름 - private string GetDisplayName(string nodeId) - { - var node = _mapNodes.FirstOrDefault(n => n.Id == nodeId); - if (node != null && node.HasRfid()) - { - return node.RfidId.ToString("0000"); - } - return $"({nodeId})"; - } } } \ No newline at end of file diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs index 77f13e6..fb5f7b0 100644 --- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs +++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs @@ -93,8 +93,8 @@ namespace AGVSimulator.Forms this._agvCountLabel = new System.Windows.Forms.Label(); this._simulationStatusLabel = new System.Windows.Forms.Label(); this._pathGroup = new System.Windows.Forms.GroupBox(); + this.btPath2 = new System.Windows.Forms.Button(); this._clearPathButton = new System.Windows.Forms.Button(); - this.btPath1 = new System.Windows.Forms.Button(); this._targetCalcButton = new System.Windows.Forms.Button(); this._avoidRotationCheckBox = new System.Windows.Forms.CheckBox(); this._targetNodeCombo = new System.Windows.Forms.ComboBox(); @@ -120,7 +120,6 @@ namespace AGVSimulator.Forms this._liftDirectionLabel = new System.Windows.Forms.Label(); this._motorDirectionLabel = new System.Windows.Forms.Label(); this.timer1 = new System.Windows.Forms.Timer(this.components); - this.btPath2 = new System.Windows.Forms.Button(); this._menuStrip.SuspendLayout(); this._toolStrip.SuspendLayout(); this._statusStrip.SuspendLayout(); @@ -142,7 +141,7 @@ namespace AGVSimulator.Forms this.helpToolStripMenuItem}); this._menuStrip.Location = new System.Drawing.Point(0, 0); this._menuStrip.Name = "_menuStrip"; - this._menuStrip.Size = new System.Drawing.Size(1034, 24); + this._menuStrip.Size = new System.Drawing.Size(1248, 24); this._menuStrip.TabIndex = 0; this._menuStrip.Text = "menuStrip"; // @@ -310,7 +309,7 @@ namespace AGVSimulator.Forms this.btMakeMap}); this._toolStrip.Location = new System.Drawing.Point(0, 24); this._toolStrip.Name = "_toolStrip"; - this._toolStrip.Size = new System.Drawing.Size(1034, 25); + this._toolStrip.Size = new System.Drawing.Size(1248, 25); this._toolStrip.TabIndex = 1; this._toolStrip.Text = "toolStrip"; // @@ -436,7 +435,7 @@ namespace AGVSimulator.Forms this.prb1}); this._statusStrip.Location = new System.Drawing.Point(0, 689); this._statusStrip.Name = "_statusStrip"; - this._statusStrip.Size = new System.Drawing.Size(1034, 22); + this._statusStrip.Size = new System.Drawing.Size(1248, 22); this._statusStrip.TabIndex = 2; this._statusStrip.Text = "statusStrip"; // @@ -464,7 +463,7 @@ namespace AGVSimulator.Forms this._controlPanel.Controls.Add(this._pathGroup); this._controlPanel.Controls.Add(this._agvControlGroup); this._controlPanel.Dock = System.Windows.Forms.DockStyle.Right; - this._controlPanel.Location = new System.Drawing.Point(801, 49); + this._controlPanel.Location = new System.Drawing.Point(1015, 49); this._controlPanel.Name = "_controlPanel"; this._controlPanel.Size = new System.Drawing.Size(233, 640); this._controlPanel.TabIndex = 3; @@ -532,7 +531,6 @@ namespace AGVSimulator.Forms // this._pathGroup.Controls.Add(this.btPath2); this._pathGroup.Controls.Add(this._clearPathButton); - this._pathGroup.Controls.Add(this.btPath1); this._pathGroup.Controls.Add(this._targetCalcButton); this._pathGroup.Controls.Add(this._avoidRotationCheckBox); this._pathGroup.Controls.Add(this._targetNodeCombo); @@ -547,6 +545,16 @@ namespace AGVSimulator.Forms this._pathGroup.TabStop = false; this._pathGroup.Text = "경로 제어"; // + // btPath2 + // + this.btPath2.Location = new System.Drawing.Point(12, 201); + this.btPath2.Name = "btPath2"; + this.btPath2.Size = new System.Drawing.Size(106, 25); + this.btPath2.TabIndex = 10; + this.btPath2.Text = "경로 계산2"; + this.btPath2.UseVisualStyleBackColor = true; + this.btPath2.Click += new System.EventHandler(this.btPath2_Click); + // // _clearPathButton // this._clearPathButton.Location = new System.Drawing.Point(121, 177); @@ -557,16 +565,6 @@ namespace AGVSimulator.Forms this._clearPathButton.UseVisualStyleBackColor = true; this._clearPathButton.Click += new System.EventHandler(this.OnClearPath_Click); // - // btPath1 - // - this.btPath1.Location = new System.Drawing.Point(12, 174); - this.btPath1.Name = "btPath1"; - this.btPath1.Size = new System.Drawing.Size(106, 25); - this.btPath1.TabIndex = 4; - this.btPath1.Text = "경로 계산"; - this.btPath1.UseVisualStyleBackColor = true; - this.btPath1.Click += new System.EventHandler(this.OnCalculatePath_Click); - // // _targetCalcButton // this._targetCalcButton.Location = new System.Drawing.Point(10, 148); @@ -741,7 +739,7 @@ namespace AGVSimulator.Forms this._canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill; this._canvasPanel.Location = new System.Drawing.Point(0, 129); this._canvasPanel.Name = "_canvasPanel"; - this._canvasPanel.Size = new System.Drawing.Size(801, 560); + this._canvasPanel.Size = new System.Drawing.Size(1015, 560); this._canvasPanel.TabIndex = 4; // // lbPredict @@ -749,7 +747,7 @@ namespace AGVSimulator.Forms this.lbPredict.Dock = System.Windows.Forms.DockStyle.Bottom; this.lbPredict.Location = new System.Drawing.Point(0, 513); this.lbPredict.Name = "lbPredict"; - this.lbPredict.Size = new System.Drawing.Size(801, 47); + this.lbPredict.Size = new System.Drawing.Size(1015, 47); this.lbPredict.TabIndex = 0; this.lbPredict.Text = ""; // @@ -764,7 +762,7 @@ namespace AGVSimulator.Forms this._agvInfoPanel.Dock = System.Windows.Forms.DockStyle.Top; this._agvInfoPanel.Location = new System.Drawing.Point(0, 49); this._agvInfoPanel.Name = "_agvInfoPanel"; - this._agvInfoPanel.Size = new System.Drawing.Size(801, 80); + this._agvInfoPanel.Size = new System.Drawing.Size(1015, 80); this._agvInfoPanel.TabIndex = 5; // // _pathDebugLabel @@ -775,7 +773,7 @@ namespace AGVSimulator.Forms this._pathDebugLabel.Location = new System.Drawing.Point(10, 30); this._pathDebugLabel.Multiline = true; this._pathDebugLabel.Name = "_pathDebugLabel"; - this._pathDebugLabel.Size = new System.Drawing.Size(947, 43); + this._pathDebugLabel.Size = new System.Drawing.Size(947, 45); this._pathDebugLabel.TabIndex = 4; this._pathDebugLabel.Text = "경로: 설정되지 않음"; // @@ -814,21 +812,11 @@ namespace AGVSimulator.Forms this.timer1.Interval = 500; this.timer1.Tick += new System.EventHandler(this.timer1_Tick); // - // btPath2 - // - this.btPath2.Location = new System.Drawing.Point(12, 201); - this.btPath2.Name = "btPath2"; - this.btPath2.Size = new System.Drawing.Size(106, 25); - this.btPath2.TabIndex = 10; - this.btPath2.Text = "경로 계산2"; - this.btPath2.UseVisualStyleBackColor = true; - this.btPath2.Click += new System.EventHandler(this.btPath2_Click); - // // SimulatorForm // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(1034, 711); + this.ClientSize = new System.Drawing.Size(1248, 711); this.Controls.Add(this._canvasPanel); this.Controls.Add(this._agvInfoPanel); this.Controls.Add(this._controlPanel); @@ -901,7 +889,6 @@ namespace AGVSimulator.Forms private System.Windows.Forms.ComboBox _startNodeCombo; private System.Windows.Forms.Label targetNodeLabel; private System.Windows.Forms.ComboBox _targetNodeCombo; - private System.Windows.Forms.Button btPath1; private System.Windows.Forms.Button _clearPathButton; private System.Windows.Forms.Button _targetCalcButton; private System.Windows.Forms.CheckBox _avoidRotationCheckBox; diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs index aac6a47..e483194 100644 --- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs +++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs @@ -422,8 +422,6 @@ namespace AGVSimulator.Forms var displayText = GetDisplayName(selectedNode.Id); _statusLabel.Text = $"타겟계산 - 목적지: {displayText}"; - // 자동으로 경로 계산 수행 - OnCalculatePath_Click(this, EventArgs.Empty); } } catch (Exception ex) @@ -1009,8 +1007,8 @@ namespace AGVSimulator.Forms _stopSimulationButton.Enabled = _simulationState.IsRunning; _removeAgvButton.Enabled = _agvListCombo.SelectedItem != null; - btPath1.Enabled = _startNodeCombo.SelectedItem != null && - _targetNodeCombo.SelectedItem != null; + // btPath1.Enabled = _startNodeCombo.SelectedItem != null && + // _targetNodeCombo.SelectedItem != null; // RFID 위치 설정 관련 var hasSelectedAGV = _agvListCombo.SelectedItem != null; @@ -1577,7 +1575,7 @@ namespace AGVSimulator.Forms MotorDirection = directionName, CurrentPosition = GetNodeDisplayName(currentNode), TargetPosition = GetNodeDisplayName(targetNode), - DockingPosition = targetNode.StationType == StationType.Charger ? "충전기" : "장비" + DockingPosition = (targetNode.StationType == StationType.Charger1 || targetNode.StationType == StationType.Charger2) ? "충전기" : "장비" }; if (calcResult.result) @@ -1750,19 +1748,19 @@ namespace AGVSimulator.Forms SetTargetNodeComboBox(dockingTarget.Id); // 경로 계산 버튼 클릭 (실제 사용자 동작) - var calcResult = CalcPath(); + //var calcResult = CalcPath(); - // 테스트 결과 생성 - 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(); }); @@ -2414,94 +2412,94 @@ namespace AGVSimulator.Forms return hex.PadLeft(2, '0'); } - (bool result, string message) CalcPath() - { - // 시작 RFID가 없으면 AGV 현재 위치로 설정 - if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요") - { - SetStartNodeFromAGVPosition(); - } + //(bool result, string message) CalcPath() + //{ + // // 시작 RFID가 없으면 AGV 현재 위치로 설정 + // if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요") + // { + // SetStartNodeFromAGVPosition(); + // } - if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null) - { - return (false, "시작 RFID와 목표 RFID를 선택해주세요."); - } + // if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null) + // { + // return (false, "시작 RFID와 목표 RFID를 선택해주세요."); + // } - var startItem = _startNodeCombo.SelectedItem as ComboBoxItem; - var targetItem = _targetNodeCombo.SelectedItem as ComboBoxItem; - var startNode = startItem?.Value; - var targetNode = targetItem?.Value; + // var startItem = _startNodeCombo.SelectedItem as ComboBoxItem; + // var targetItem = _targetNodeCombo.SelectedItem as ComboBoxItem; + // var startNode = startItem?.Value; + // var targetNode = targetItem?.Value; - if (startNode == null || targetNode == null) - { - return (false, "선택한 노드 정보가 올바르지 않습니다."); - } + // if (startNode == null || targetNode == null) + // { + // return (false, "선택한 노드 정보가 올바르지 않습니다."); + // } - if (_advancedPathfinder == null) - { - _advancedPathfinder = new AGVPathfinder(_simulatorCanvas.Nodes); - } + // if (_advancedPathfinder == null) + // { + // _advancedPathfinder = new AGVPathfinder(_simulatorCanvas.Nodes); + // } - // 현재 AGV 방향 가져오기 - var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; - if (selectedAGV == null) - { - return (false, "Virtual AGV 가 없습니다"); - } - var currentDirection = selectedAGV.CurrentDirection; + // // 현재 AGV 방향 가져오기 + // var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; + // if (selectedAGV == null) + // { + // return (false, "Virtual AGV 가 없습니다"); + // } + // var currentDirection = selectedAGV.CurrentDirection; - // AGV의 이전 위치에서 가장 가까운 노드 찾기 - var prevNode = selectedAGV.PrevNode; - var prevDir = selectedAGV.PrevDirection; + // // AGV의 이전 위치에서 가장 가까운 노드 찾기 + // var prevNode = selectedAGV.PrevNode; + // var prevDir = selectedAGV.PrevDirection; - // 고급 경로 계획 사용 (노드 객체 직접 전달) - var advancedResult = _advancedPathfinder.FindPath(startNode, targetNode, prevNode, prevDir, currentDirection); + // // 고급 경로 계획 사용 (노드 객체 직접 전달) + // var advancedResult = _advancedPathfinder.FindPath(startNode, targetNode, prevNode, prevDir, currentDirection); - _simulatorCanvas.FitToNodes(); - if (advancedResult.Success) - { - // 도킹 검증이 없는 경우 추가 검증 수행 - if (advancedResult.DockingValidation == null || !advancedResult.DockingValidation.IsValidationRequired) - { - advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _simulatorCanvas.Nodes); - } + // _simulatorCanvas.FitToNodes(); + // if (advancedResult.Success) + // { + // // 도킹 검증이 없는 경우 추가 검증 수행 + // if (advancedResult.DockingValidation == null || !advancedResult.DockingValidation.IsValidationRequired) + // { + // advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _simulatorCanvas.Nodes); + // } - //마지막대상이 버퍼라면 시퀀스처리를 해야한다 - if (targetNode.StationType == StationType.Buffer) - { - var lastDetailPath = advancedResult.DetailedPath.Last(); - if (lastDetailPath.NodeId == targetNode.Id) //마지막노드 재확인 - { - //버퍼에 도킹할때에는 마지막 노드에서 멈추고 시퀀스를 적용해야한다 - advancedResult.DetailedPath = advancedResult.DetailedPath.Take(advancedResult.DetailedPath.Count - 1).ToList(); - Console.WriteLine("최종위치가 버퍼이므로 마지막 RFID에서 멈추도록 합니다"); - } - } + // //마지막대상이 버퍼라면 시퀀스처리를 해야한다 + // if (targetNode.StationType == StationType.Buffer) + // { + // var lastDetailPath = advancedResult.DetailedPath.Last(); + // if (lastDetailPath.NodeId == targetNode.Id) //마지막노드 재확인 + // { + // //버퍼에 도킹할때에는 마지막 노드에서 멈추고 시퀀스를 적용해야한다 + // advancedResult.DetailedPath = advancedResult.DetailedPath.Take(advancedResult.DetailedPath.Count - 1).ToList(); + // Console.WriteLine("최종위치가 버퍼이므로 마지막 RFID에서 멈추도록 합니다"); + // } + // } - _simulatorCanvas.CurrentPath = advancedResult; - _pathLengthLabel.Text = $"경로 길이: {advancedResult.TotalDistance:F1}"; - _statusLabel.Text = $"경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)"; + // _simulatorCanvas.CurrentPath = advancedResult; + // _pathLengthLabel.Text = $"경로 길이: {advancedResult.TotalDistance:F1}"; + // _statusLabel.Text = $"경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)"; - // 🔥 VirtualAGV에도 경로 설정 (Predict()가 동작하려면 필요) - selectedAGV.SetPath(advancedResult); + // // 🔥 VirtualAGV에도 경로 설정 (Predict()가 동작하려면 필요) + // selectedAGV.SetPath(advancedResult); - // 도킹 검증 결과 확인 및 UI 표시 - CheckAndDisplayDockingValidation(advancedResult); + // // 도킹 검증 결과 확인 및 UI 표시 + // CheckAndDisplayDockingValidation(advancedResult); - // 고급 경로 디버깅 정보 표시 - UpdateAdvancedPathDebugInfo(advancedResult); - return (true, string.Empty); - } - else - { - // 경로 실패시 디버깅 정보 초기화 - _pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}"; + // // 고급 경로 디버깅 정보 표시 + // UpdateAdvancedPathDebugInfo(advancedResult); + // return (true, string.Empty); + // } + // else + // { + // // 경로 실패시 디버깅 정보 초기화 + // _pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}"; - return (false, $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}"); - } - } + // return (false, $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}"); + // } + //} #endregion private void btPath2_Click(object sender, EventArgs e) @@ -2569,17 +2567,31 @@ namespace AGVSimulator.Forms _simulatorCanvas.HighlightNodeId = gatewayNode.Id; // Gateway 강조 설정 // 4. Start -> Gateway 경로 계산 (A*) - var pathToGateway = _advancedPathfinder.FindPath(startNode, gatewayNode, prevNode, prevDir, currentAgvDir); + var pathToGateway = _advancedPathfinder.FindBasicPath(startNode, gatewayNode, prevNode, prevDir); if (!pathToGateway.Success) return (false, $"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.ErrorMessage}"); - // 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함) - var arrivalOrientation = pathToGateway.DetailedPath.Last().MotorDirection; - AGVPathResult finalPath = pathToGateway; + //마지막경로는 게이트웨이이므로 제거하낟. + if(pathToGateway.Path.Count > 1) + { + pathToGateway.Path.RemoveAt(pathToGateway.Path.Count - 1); + pathToGateway.DetailedPath.RemoveAt(pathToGateway.DetailedPath.Count - 1); + } - var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, pathToGateway.Path.Last(), arrivalOrientation); + // 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함) + + + MapNode GateprevNode = pathToGateway.Path.Last(); + NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.Last(); + + + var arrivalOrientation = GatePrevDetail.MotorDirection; + + //아래코드오류발생함 + var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, GateprevNode, arrivalOrientation); if (!gatewayPathResult.Success) return (false, $"{gatewayPathResult.ErrorMessage}"); + AGVPathResult finalPath = pathToGateway; finalPath = CombinePaths(finalPath, gatewayPathResult); @@ -2617,59 +2629,57 @@ namespace AGVSimulator.Forms /// /// Gateway 도착 후, Target까지의 경로(회차 및 최종진입 포함)를 계산합니다. /// - /// 게이트웨이 노드값 + /// 게이트웨이 노드값(현재노드값) /// 최종 목표값 - /// 게이트웨이 진입 전 노드 - /// 게이트웨이 진입 전 모터방향 + /// 게이트웨이 진입 전 노드 + /// 게이트웨이 진입 전 모터방향 /// - private AGVPathResult GetPathFromGateway(MapNode gatewayNode, MapNode targetNode, MapNode GTprevNode, AgvDirection GTprevDirection) + private AGVPathResult GetPathFromGateway(MapNode GTNode, MapNode targetNode, MapNode PrevNode, AgvDirection PrevDirection) { AGVPathResult resultPath = null; - MapNode currentNode = gatewayNode; - MapNode currentPrev = GTprevNode; // Gateway 바로 이전 노드 (방향 계산용) - AgvDirection currentDir = GTprevDirection; - //게이트웨이 진입 한 방향을 보고. 목적지와 도킹방향이 일치하는지 결정한다. - var deltaX = gatewayNode.Position.X - GTprevNode.Position.X; - + var deltaX = GTNode.Position.X - PrevNode.Position.X; var isMonitorLeft = false; bool requiredDir = false; switch (targetNode.StationType) { + case StationType.Charger1: + case StationType.UnLoader: + case StationType.Clearner: case StationType.Buffer: - - //버퍼는 게이트웨이가 6번이고 좌/우로 판단한다 - if (deltaX > 0) //게이트웨이가 더 오른쪽에있으니 좌->우 이동을 한경우이다. 이떄 모터방향이 후진이라면 모니터는 왼쪽이고, 반대는 오른쪽이다 + if (deltaX > 0) //게이트웨이가 우측에 있다 { - isMonitorLeft = GTprevDirection == AgvDirection.Backward; + //이떄 모터방향이 후진이라면 모니터는 왼쪽이고, 반대는 오른쪽이다 + isMonitorLeft = PrevDirection == AgvDirection.Backward; } else { - isMonitorLeft = GTprevDirection == AgvDirection.Forward; + isMonitorLeft = PrevDirection == AgvDirection.Forward; } //버퍼는 모니터가 왼쪽에 있으면 안된다. + //충전기1만 전진 도킹을 한다. List turnPatterns = new List(); AGVPathResult rlt1 = new AGVPathResult(); rlt1.Success = true; //목적지까지 바로 계산한다 - var pathtarget = _advancedPathfinder.FindBasicPath(gatewayNode, targetNode, GTprevNode, AgvDirection.Backward); + var pathtarget = _advancedPathfinder.FindBasicPath(GTNode, targetNode, PrevNode, AgvDirection.Backward); - if (isMonitorLeft) + if (targetNode.DockDirection == DockingDirection.Backward && isMonitorLeft) { //턴을 하는 - turnPatterns = GetTurnaroundPattern(gatewayNode, targetNode); - if (turnPatterns == null || turnPatterns.Any() == false) return new AGVPathResult { Success = false, ErrorMessage = $"회차 패턴 없음: Dir {currentDir}" }; + turnPatterns = GetTurnaroundPattern(GTNode, targetNode); + if (turnPatterns == null || turnPatterns.Any() == false) return new AGVPathResult { Success = false, ErrorMessage = $"회차 패턴 없음: Dir {PrevDirection}" }; foreach (var item in turnPatterns) { var rfidvalue = ushort.Parse(item.Substring(0, 4)); var node = _simulatorCanvas.Nodes.FirstOrDefault(t => t.RfidId == rfidvalue); //경로노드추가 - rlt1.Path.Add(node); + rlt1.Path.Add(node); //Detail 정보도 추가한다. AgvDirection nodedir = item.Substring(4, 1) == "F" ? AgvDirection.Forward : AgvDirection.Backward; @@ -2685,7 +2695,7 @@ namespace AGVSimulator.Forms //시작위치가 겹치므로 제거해줘야하낟. if (pathtarget.DetailedPath.First().NodeId != rlt1.DetailedPath.Last().NodeId || - pathtarget.DetailedPath.First().MotorDirection != rlt1.DetailedPath.Last().MotorDirection ) + pathtarget.DetailedPath.First().MotorDirection != rlt1.DetailedPath.Last().MotorDirection) { new AGVPathResult { Success = false, ErrorMessage = $"게이트웨이 턴 마지막 주소와, 이 후 주소의 시작 노드ID가 일치하지 않습니다" }; } @@ -2817,7 +2827,7 @@ namespace AGVSimulator.Forms //게이트웨이까지 후진으로 이동했다면 모니터방향이 오른쪽이다 => 방향전환필요 var gateToTarget = GetPathFromGateway(GateWayNode, target, lastPrev, lastDir); - escPath.Path.RemoveAt(escPath.Path.Count-1); + escPath.Path.RemoveAt(escPath.Path.Count - 1); escPath.DetailedPath.RemoveAt(escPath.DetailedPath.Count - 1); @@ -2888,15 +2898,15 @@ namespace AGVSimulator.Forms var res = new AGVPathResult(); res.Success = true; - foreach(var item in p1.Path) + foreach (var item in p1.Path) { res.Path.Add(item); } - foreach(var item in p2.Path) + foreach (var item in p2.Path) { res.Path.Add(item); } - + foreach (var item in p1.DetailedPath) { var maxseq = res.DetailedPath.Count == 0 ? 0 : res.DetailedPath.Max(t => t.seq); @@ -2905,7 +2915,7 @@ namespace AGVSimulator.Forms } foreach (var item in p2.DetailedPath) { - var maxseq = res.DetailedPath.Count == 0 ? 0 : res.DetailedPath.Max(t => t.seq); + var maxseq = res.DetailedPath.Count == 0 ? 0 : res.DetailedPath.Max(t => t.seq); item.seq = maxseq + 1; res.DetailedPath.Add(item); } @@ -2922,13 +2932,6 @@ namespace AGVSimulator.Forms _simulatorCanvas.FitToNodes(); } - private void OnCalculatePath_Click(object sender, EventArgs e) - { - var rlt = CalcPath(); - if (rlt.result == false) MessageBox.Show(rlt.message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - - } } \ No newline at end of file diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs index b405854..2839809 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs @@ -138,7 +138,8 @@ namespace Project PUB.sm.SetNewRunStep(ERunStep.ERROR); } break; - case AGVNavigationCore.Models.StationType.Charger: + case AGVNavigationCore.Models.StationType.Charger1: + case AGVNavigationCore.Models.StationType.Charger2: break; diff --git a/Cs_HMI/Project/ViewForm/fAuto.cs b/Cs_HMI/Project/ViewForm/fAuto.cs index 4ed9a91..2472335 100644 --- a/Cs_HMI/Project/ViewForm/fAuto.cs +++ b/Cs_HMI/Project/ViewForm/fAuto.cs @@ -72,7 +72,7 @@ namespace Project.ViewForm menu.Items.Add(pickOff); // Charge - if (mapnode.StationType == StationType.Charger) + if (mapnode.StationType == StationType.Charger1 || mapnode.StationType == StationType.Charger2) { var charge = new ToolStripMenuItem("Charge (Move & Charge)"); charge.Click += (s, args) => ExecuteManualCommand(mapnode, ENIGProtocol.AGVCommandHE.Charger); @@ -105,14 +105,14 @@ namespace Project.ViewForm // 1. 경로 생성 var pathFinder = new AGVNavigationCore.PathFinding.Planning.AGVPathfinder(PUB._mapCanvas.Nodes); - // 현재위치에서 목표위치까지 - var result = pathFinder.FindPath(PUB._virtualAGV.CurrentNode, targetNode); + //// 현재위치에서 목표위치까지 + //var result = pathFinder.FindBasicPath(PUB._virtualAGV.CurrentNode, targetNode); - if (!result.Success || result.Path == null || result.Path.Count == 0) - { - MessageBox.Show("경로를 찾을 수 없습니다."); - return; - } + //if (!result.Success || result.Path == null || result.Path.Count == 0) + //{ + // MessageBox.Show("경로를 찾을 수 없습니다."); + // return; + //} // 2. 상태 설정 @@ -123,7 +123,7 @@ namespace Project.ViewForm PUB.log.AddI($"[Manual Command] {cmd} to ({targetNode.Id})"); // FindPathResult contains DetailedPath already. - PUB._virtualAGV.SetPath(result); + // PUB._virtualAGV.SetPath(result); PUB._virtualAGV.TargetNode = targetNode as MapNode; // 3. 작업 설정 @@ -243,7 +243,7 @@ namespace Project.ViewForm ENIGProtocol.AGVCommandHE targetCmd = ENIGProtocol.AGVCommandHE.Goto; string confirmMsg = ""; - if (targetNode.StationType == StationType.Charger) + if (targetNode.StationType == StationType.Charger1 || targetNode.StationType == StationType.Charger2) { if (MessageBox.Show($"[{targetNode.Id}] 충전기로 이동하여 충전을 진행하시겠습니까?", "작업 확인", MessageBoxButtons.YesNo) == DialogResult.Yes) {