using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using AGVNavigationCore.Models; using AGVNavigationCore.Utils; using AGVNavigationCore.PathFinding.Core; using AGVNavigationCore.PathFinding.Analysis; namespace AGVNavigationCore.PathFinding.Planning { /// /// AGV 경로 계획기 /// 물리적 제약사항과 마그넷 센서를 고려한 실제 AGV 경로 생성 /// public class AGVPathfinder { private readonly List _mapNodes; private readonly AStarPathfinder _basicPathfinder; private readonly DirectionalPathfinder _directionPathfinder; private readonly JunctionAnalyzer _junctionAnalyzer; private readonly DirectionChangePlanner _directionChangePlanner; public AGVPathfinder(List mapNodes) { _mapNodes = mapNodes ?? new List(); _basicPathfinder = new AStarPathfinder(); _basicPathfinder.SetMapNodes(_mapNodes); _junctionAnalyzer = new JunctionAnalyzer(_mapNodes); _directionChangePlanner = new DirectionChangePlanner(_mapNodes); _directionPathfinder = new DirectionalPathfinder(); } /// /// 지정한 노드에서 가장 가까운 교차로(3개 이상 연결된 노드)를 찾는다. /// /// 기준이 되는 노드 /// 가장 가까운 교차로 노드 (또는 null) public MapNode FindNearestJunction(MapNode startNode) { if (startNode == null || _mapNodes == null || _mapNodes.Count == 0) return null; // 교차로: 3개 이상의 노드가 연결된 노드 var junctions = _mapNodes.Where(n => n.IsActive && n.IsNavigationNode() && n.ConnectedNodes != null && n.DisableCross == false && n.ConnectedNodes.Count >= 3 && n.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false && n.Id != startNode.Id ).ToList(); // docking 포인트가 연결된 노드는 제거한다. if (junctions.Count == 0) return null; // 직선 거리 기반으로 가장 가까운 교차로 찾기 MapNode nearestJunction = null; float minDistance = float.MaxValue; foreach (var junction in junctions) { float dx = junction.Position.X - startNode.Position.X; float dy = junction.Position.Y - startNode.Position.Y; float distance = (float)Math.Sqrt(dx * dx + dy * dy); if (distance < minDistance) { minDistance = distance; nearestJunction = junction; } } return nearestJunction; } /// /// 지정한 노드에서 경로상 가장 가까운 교차로를 찾는다. /// (최단 경로 내에서 3개 이상 연결된 교차로를 찾음) /// /// 시작 노드 /// 목적지 노드 /// 경로상의 가장 가까운 교차로 노드 (또는 null) public MapNode FindNearestJunctionOnPath(AGVPathResult pathResult) { if (pathResult == null || !pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0) return null; // 경로상의 모든 노드 중 교차로(3개 이상 연결) 찾기 var StartNode = pathResult.Path.First(); foreach (var pathNode in pathResult.Path) { if (pathNode != null && pathNode.IsActive && pathNode.IsNavigationNode() && pathNode.DisableCross == false && pathNode.ConnectedNodes != null && pathNode.ConnectedNodes.Count >= 3 && pathNode.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false) { if (pathNode.Id.Equals(StartNode.Id) == false) return pathNode; } } return null; } public AGVPathResult FindPathAStar(MapNode startNode, MapNode targetNode) { // 기본값으로 경로 탐색 (이전 위치 = 현재 위치, 방향 = 전진) return _basicPathfinder.FindPathAStar(startNode, targetNode); } /// /// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요 /// /// /// 단순 경로 찾기 (복잡한 제약조건/방향전환 로직 없이 A* 결과만 반환) /// public AGVPathResult FindBasicPath(MapNode startNode, MapNode targetNode, MapNode _prevNode, AgvDirection prevDirection) { // 1. 입력 검증 if (startNode == null || targetNode == null) return AGVPathResult.CreateFailure("노드 정보 오류", 0, 0); // 2. A* 경로 탐색 var pathResult = _basicPathfinder.FindPathAStar(startNode, targetNode); pathResult.PrevNode = _prevNode; pathResult.PrevDirection = prevDirection; if (!pathResult.Success) return AGVPathResult.CreateFailure(pathResult.Message ?? "경로 없음", 0, 0); // 3. 상세 데이터 생성 (갈림길 마그넷 방향 계산 포함) // 3. 상세 데이터 생성 (갈림길 마그넷 방향 계산 포함) if (pathResult.Path != null && pathResult.Path.Count > 0) { var detailedPath = new List(); for (int i = 0; i < pathResult.Path.Count; i++) { var node = pathResult.Path[i]; var nextNode = (i + 1 < pathResult.Path.Count) ? pathResult.Path[i + 1] : null; // 마그넷 방향 계산 (갈림길인 경우) // 마그넷 방향 계산 (갈림길인 경우) MagnetDirection magnetDirection = MagnetDirection.Straight; //갈림길에 있다면 미리 방향을 저장해준다. if ((node.ConnectedNodes?.Count ?? 0) > 2 && nextNode != null) { //다음 노드ID를 확인해서 마그넷 방향 데이터를 찾는다. if (node.MagnetDirections.ContainsKey(nextNode.Id) == false) { return AGVPathResult.CreateFailure($"{node.ID2}->{nextNode.ID2} 의 (목표)갈림길 방향이 입력되지 않았습니다", 0, 0); } else { var magdir = node.MagnetDirections[nextNode.Id].ToString(); if (magdir == "L") magnetDirection = MagnetDirection.Left; else if (magdir == "R") magnetDirection = MagnetDirection.Right; } } var nodeInfo = new NodeMotorInfo(i + 1, node.Id, node.RfidId, prevDirection, nextNode, magnetDirection); // 속도 설정 var mapNode = _mapNodes.FirstOrDefault(n => n.Id == node.Id); if (mapNode != null) { nodeInfo.Speed = mapNode.SpeedLimit; detailedPath.Add(nodeInfo); } } pathResult.DetailedPath = detailedPath; } return pathResult; } #region Helper Classes & Methods for CalculatePath_new private class SearchState { public MapNode CurrentNode { get; set; } public MapNode PreviousNode { get; set; } public AgvDirection CurrentDirection { get; set; } // 현재 모터 방향 (Forward/Backward) public float GCost { get; set; } public float HCost { get; set; } public float FCost => GCost + HCost; public SearchState Parent { get; set; } public string TurnType { get; set; } // Debug info } private struct MoveResult { public bool IsPossible; public float Cost; public AgvDirection NextDirection; public string TurnType; } private string GetStateKey(SearchState state) { string prevId = state.PreviousNode?.Id ?? "null"; // 모터 방향도 상태에 포함해야 함 (같은 노드, 같은 진입이라도 모터방향 다르면 다른 상태 - 스위치백 때문) return $"{state.CurrentNode.Id}_{prevId}_{state.CurrentDirection}"; } private SearchState GetLowestFCostState(List openSet) { SearchState lowest = openSet[0]; for (int i = 1; i < openSet.Count; i++) { if (openSet[i].FCost < lowest.FCost) lowest = openSet[i]; } return lowest; } private float CalculateHeuristic(MapNode from, MapNode to) { // 유클리드 거리 float dx = from.Position.X - to.Position.X; float dy = from.Position.Y - to.Position.Y; return (float)Math.Sqrt(dx * dx + dy * dy); } private MoveResult CheckMove(SearchState current, MapNode next) { var res = new MoveResult { IsPossible = false, Cost = float.MaxValue }; // 1. 기본 거리 비용 float dist = (float)Math.Sqrt(Math.Pow(current.CurrentNode.Position.X - next.Position.X, 2) + Math.Pow(current.CurrentNode.Position.Y - next.Position.Y, 2)); // 2. 회전 각도 계산 // Vector Prev -> Curr // Vector Curr -> Next double angle = 180.0; // Straight if (current.PreviousNode != null) { // Vector 1 (Prev -> Curr) double v1x = current.CurrentNode.Position.X - current.PreviousNode.Position.X; double v1y = current.CurrentNode.Position.Y - current.PreviousNode.Position.Y; // Vector 2 (Curr -> Next) double v2x = next.Position.X - current.CurrentNode.Position.X; double v2y = next.Position.Y - current.CurrentNode.Position.Y; // 각도 계산 (0 ~ 180) // 내적 이용: a·b = |a||b|cosθ double dot = v1x * v2x + v1y * v2y; double mag1 = Math.Sqrt(v1x * v1x + v1y * v1y); double mag2 = Math.Sqrt(v2x * v2x + v2y * v2y); if (mag1 > 0 && mag2 > 0) { double cosTheta = dot / (mag1 * mag2); // 부동소수점 오차 보정 if (cosTheta > 1.0) cosTheta = 1.0; if (cosTheta < -1.0) cosTheta = -1.0; double rad = Math.Acos(cosTheta); // 외적을 이용해 좌/우 판별이 가능하지만, 여기서는 "꺾인 정도"만 중요하므로 내적각(0~180)만 사용 // 180도 = 직진, 90도 = 직각, 0도 = U턴(완전 뒤로) // 주의: 벡터 방향 기준임. // Prev->Curr 가 (1,0) 이고 Curr->Next가 (1,0) 이면 각도 0도? 아니면 180? // 위 내적 계산에서 같은 방향이면 cos=1 -> acos=0. 즉 0도가 직진임. // 180도가 U턴(역방향). // 변환: 사용자가 "120도 이상 완만해야" 라고 했음. // 그림상 11->3->4는 예각(Sharp turn). // 직선 주행시 각도 변화량(Deviation)으로 생각하면: // 직진 = 0도 변화. // 90도 턴 = 90도 변화. // U턴 = 180도 변화. angle = rad * (180.0 / Math.PI); // 0(직진) ~ 180(U턴) } } else { angle = 0; // 시작점에서는 직진으로 간주 (또는 자유) } // 제약 조건 체크 // 조건 2: 120도 미만 예각 회전 불가? // 사용자 멘트: "11->3->4 ... 120도 이상 완만해야 이동 가능... 3->4는 각도가 훨씬 작아서 커브 못틈" // 여기서 "각도"가 내각(Inner Angle)을 말하는지, 진행 경로의 꺾임각을 말하는지 중요. // 11->3->4 그림을 보면 "V"자 형태에 가까움. (Sharp Turn) // 즉, "진행 방향의 꺾임각"이 크다(예: 150도 꺾임) = "내각"이 작다(30도). // "120도 이상 완만해야" -> 내각이 120도 이상(점잖은 턴)이어야 한다. // 즉, 꺾임각(Deviation)은 60도 이하여야 한다. // 내 알고리즘의 `angle`은 "꺾임각(Deviation)"임. (0=직진, 180=U턴) // 따라서 "내각 120도 이상" == "꺾임각 60도 이하". // 그러므로 angle <= 60 이어야 Normal Turn 가능. // 3번 노드 (RFID 3) 특수성: // "3번 노드에서만 180도 턴 가능" // "3->4 처럼 각이 좁은 경우 3번으로 가서 모터방향 바꿔서 4번으로 감" bool isRfid3 = current.CurrentNode.RfidId == 3; bool isSharpTurn = angle > 60.0; // 60도보다 많이 꺾이면 Sharp Turn (내각 120도 미만) // Case A: Normal Move (완만한 턴) if (!isSharpTurn) { res.IsPossible = true; res.Cost = dist; res.NextDirection = current.CurrentDirection; // 방향 유지 res.TurnType = "Normal"; return res; } // Case B: Sharp Turn or U-Turn if (isSharpTurn) { if (isRfid3) { // 3번 노드에서는 무슨 짓이든(?) 가능하다. (Switch Back) // 멈춰서 방향을 바꾸므로, 모터 방향도 바뀜 (FWD -> BWD or BWD -> FWD) // 비용 패널티 추가 (멈추고 바꾸는 시간 등) res.IsPossible = true; res.Cost = dist + 5000; // 큰 비용 (Switch Penalty) res.NextDirection = (current.CurrentDirection == AgvDirection.Forward) ? AgvDirection.Backward : AgvDirection.Forward; res.TurnType = "SwitchBack"; return res; } else { // 3번 노드가 아닌데 Sharp Turn 하려고 함 -> 불가 res.IsPossible = false; return res; } } return res; } private bool IsDockingDirectionValid(SearchState state, MapNode target) { if (target.DockDirection == DockingDirection.DontCare) return true; // 도착 시 모터 방향 확인 // SearchState에 저장된 CurrentDirection이 도착 시 모터 방향임. if (target.DockDirection == DockingDirection.Forward) return state.CurrentDirection == AgvDirection.Forward; if (target.DockDirection == DockingDirection.Backward) return state.CurrentDirection == AgvDirection.Backward; return true; } private AGVPathResult ReconstructPath_New(SearchState endState) { var result = new AGVPathResult { Success = true, TotalDistance = endState.GCost }; var pathList = new List(); var detailedList = new List(); var current = endState; var pathStack = new Stack(); while (current != null) { pathStack.Push(current); current = current.Parent; } // Path 재구성 int seq = 1; while (pathStack.Count > 0) { var step = pathStack.Pop(); pathList.Add(step.CurrentNode); // Detailed Info (마그넷 방향 계산 등은 후처리 필요할 수도 있으나 여기선 단순화) // SearchState에는 "어떤 방향으로 왔는지"가 저장되어 있음. // 마그넷 방향 계산 (다음 노드가 있을 때) MagnetDirection magDir = MagnetDirection.Straight; if (pathStack.Count > 0) { var nextStep = pathStack.Peek(); // Current -> NextStep 이동 시 마그넷 방향 if (step.CurrentNode.MagnetDirections.ContainsKey(nextStep.CurrentNode.Id)) { var magPos = step.CurrentNode.MagnetDirections[nextStep.CurrentNode.Id]; if (magPos == MagnetPosition.L) magDir = MagnetDirection.Left; else if (magPos == MagnetPosition.R) magDir = MagnetDirection.Right; // 만약 SwitchBack 상황이라면? // 모터 방향이 바뀌었다면 마그넷 방향도 그에 맞춰야 하나? // 기존 로직 참고: MagnetDirection은 "진행 방향 기준" 좌/우 인가? 아님 절대적? // 보통 "갈림길에서 어느 쪽" 인지 나타냄. } } detailedList.Add(new NodeMotorInfo(seq++, step.CurrentNode.Id, step.CurrentNode.RfidId, step.CurrentDirection, null, magDir)); // NextNode는 일단 null } // NextNode 정보 채우기 for (int i = 0; i < detailedList.Count - 1; i++) { detailedList[i].NextNode = _mapNodes.FirstOrDefault(n => n.Id == detailedList[i + 1].NodeId); } result.Path = pathList; result.DetailedPath = detailedList; return result; } #endregion public enum MapZoneMonitor { LeftTop, RightBtm, } public enum MapZone { None, Buffer, // 91 ~ 07 Charger, // 73 ~ 10 Plating, // 72 ~ 05 Loader, // 71 ~ 04 Cleaner, // 70 ~ 01 Junction, // Hub (11, 12, etc) Turn, } public class MapZonePathData { public StationType NodeSta { get; set; } public StationType NodeEnd { get; set; } public MapZoneMonitor Monitor { get; set; } public List Path { get; set; } } public List GetMapZonePathData() { var retval = new List(); // Buffer -> ... retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List { "36F", "35F", "31F", "32F", "33F", "34F", "20F", "9F", "8F", "7F", "11F", "3T", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List { "36B", "35B", "31B", "32B", "33B", "34B", "20B", "9B", "8B", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.RightBtm, Path = new List { "36F", "35F", "31F", "32F", "33F", "34F", "20F", "9F", "8F", "7F", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.LeftTop, Path = new List { "36B", "35B", "31B", "32B", "33B", "34B", "20B", "9B", "8B", "7B", "11B", "3T", "3B", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.RightBtm, Path = new List { "36F", "35F", "31F", "32F", "33F", "34F", "20F", "9F", "8F", "7F", "11F", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.LeftTop, Path = new List { "36B", "35B", "31B", "32B", "33B", "34B", "20B", "9B", "8B", "7B", "11B", "3T", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.RightBtm, Path = new List { "36F", "35F", "31F", "32F", "33F", "34F", "20F", "9F", "8F", "7F", "11F", "3T", "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.LeftTop, Path = new List { "36B", "35B", "31B", "32B", "33B", "34B", "20B", "9B", "8B", "7B", "11B", "3B", "70B" } }); // Loader -> ... retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.RightBtm, Path = new List { "71B", "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.LeftTop, Path = new List { "71F", "3T", "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List { "71B", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List { "71F", "3T", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.RightBtm, Path = new List { "71B", "3T", "3B", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.LeftTop, Path = new List { "71F", "3B", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.RightBtm, Path = new List { "71B", "3T", "3B", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.LeftTop, Path = new List { "71F", "3B", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); // Cleaner -> ... retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.RightBtm, Path = new List { "70F", "3T", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.LeftTop, Path = new List { "70B", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List { "70F", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List { "70B", "3T", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.RightBtm, Path = new List { "70F", "3T", "3B", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.LeftTop, Path = new List { "70B", "3B", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.RightBtm, Path = new List { "70F", "3T", "3B", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.LeftTop, Path = new List { "70B", "3B", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); // Plating -> ... retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.RightBtm, Path = new List { "72F", "11F", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.LeftTop, Path = new List { "72B", "11B", "3T", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List { "72F", "11F", "3T", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List { "72B", "11B", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.RightBtm, Path = new List { "72F", "11F", "3T", "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.LeftTop, Path = new List { "72B", "11B", "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.RightBtm, Path = new List { "72F", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.LeftTop, Path = new List { "72B", "11B", "3T", "3B", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); //일반노드도 일부 경로를 계산한다. // 일반노드 3 -> retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.LeftTop, Path = new List { "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.LeftTop, Path = new List { "3T", "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.LeftTop, Path = new List { "3B", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List { "3T", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.LeftTop, Path = new List { "3B", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.RightBtm, Path = new List { "3T", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.RightBtm, Path = new List { "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.RightBtm, Path = new List { "3T", "3B", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List { "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.RightBtm, Path = new List { "3T", "3B", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); // 일반노드 7 -> retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.LeftTop, Path = new List { "7F", "11F", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.LeftTop, Path = new List { "7F", "11F", "3T", "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.LeftTop, Path = new List { "7F", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List { "7F", "11F", "3T", "3F", "11F", "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.LeftTop, Path = new List { "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.RightBtm, Path = new List { "7B", "11B", "3T", "3B", "71B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.RightBtm, Path = new List { "7B", "11B", "3B", "70B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.RightBtm, Path = new List { "7B", "11B", "3T", "3B", "11B", "72B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List { "7B", "10B", "6B", "73B" } }); retval.Add(new MapZonePathData { NodeSta = StationType.Normal, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.RightBtm, Path = new List { "7B", "11B", "3T", "3B", "11B", "7B", "8B", "9B", "20B", "34B", "33B", "32B", "31B", "35B", "36B" } }); return retval; } /// /// 해당 노드가 속하는 존을 반환한다. /// /// /// public MapZone GetMapZone(MapNode node) { if (node == null) return MapZone.None; int rfid = node.RfidId; Dictionary ZoneList = GetMapZoneNodeList(); var zone = ZoneList.Where(t => t.Value.Contains(rfid)).FirstOrDefault(); if (zone.Value == null) return MapZone.None; return zone.Key; } public Dictionary GetMapZoneNodeList() { Dictionary ZoneList = new Dictionary(); ZoneList.Add(MapZone.Turn, new int[] { 3 }); ZoneList.Add(MapZone.Buffer, new int[] { 91, 36, 35, 31, 32, 33, 34, 20, 9, 8, 7 }); ZoneList.Add(MapZone.Charger, new int[] { 73, 6, 10 }); ZoneList.Add(MapZone.Junction, new int[] { 11 }); ZoneList.Add(MapZone.Plating, new int[] { 72 }); ZoneList.Add(MapZone.Loader, new int[] { 71 }); ZoneList.Add(MapZone.Cleaner, new int[] { 70 }); return ZoneList; } public AGVPathResult CalculateScriptedPath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection prevDir) { var startZone = GetMapZone(startNode); var targetZone = GetMapZone(targetNode); // 존이 확인되지 않는다면 오류 if (startZone == MapZone.None || targetZone == MapZone.None) { // return AGVPathResult.CreateFailure($"Zone not found: {startNode.ID2}->{targetNode.ID2}"); } var monitorMode = GetMonitorMode(startNode, prevNode, prevDir); var motDir = prevDir == AgvDirection.Forward ? 'F' : 'B'; // 시작 태그 검색용 (예: "91F") string startTag = $"{startNode.RfidId}{motDir}"; // 시작과 লক্ষ্য가 다른 존일 때 시작점이 일반 노드(예: 3번)라면, // (예: 70->3 전진 후 3에서 71 로더로 갈 때) // 현재 방향 체계상 일반노드에서의 GetMonitorMode() 결과가 최종 zonepath의 Monitor 기준과 // 반전되어 구해지는 현상이 있으므로, 이 경우에만 monitorMode를 반대로 뒤집어서 찾는다. if (startZone != targetZone && startNode.StationType == StationType.Normal) { //monitorMode = monitorMode == MapZoneMonitor.LeftTop ? MapZoneMonitor.RightBtm : MapZoneMonitor.LeftTop; } // 모니터방향이 일치하고 대상노드가 동일한 경로를 찾는다 var zonepath = GetMapZonePathData(); IEnumerable candidates; //시작이 일반노드라면 , 경로가 등록된 주변 노드로 이동해서 이후경로를 계산하자 //일반노드용 기준은 7,3이있다. //11번은 3로 이동해서 경로를 계산한다. //10,6번은 7로 이동해서 경로를 계산한다. //대상노드까지 CalculateScriptedPath를 재귀로 호출해서 계산을 완료한 후, 대상에서 목적지 계산해서 전체 경로를 만들어낸다. if (startNode.StationType == StationType.Normal && startNode.RfidId != 3 && startNode.RfidId != 7) { var newtargetRFID = startNode.RfidId == 11 ? 3 : 7; var refNode = _mapNodes.FirstOrDefault(n => n.RfidId == newtargetRFID); if (refNode != null && refNode.Id != startNode.Id) { // 기준 노드(7 or 3)까지의 기본 경로를 먼저 구함 var pathToRef = this.FindBasicPath(startNode, refNode, prevNode, prevDir); if (pathToRef != null && pathToRef.Success) { var lastDet = pathToRef.DetailedPath.Last(); var secondLastNode = pathToRef.Path.Count > 1 ? pathToRef.Path[pathToRef.Path.Count - 2] : prevNode; // 기준 노드(7 or 3)에서 목적지까지의 경로를 재귀적으로 계산 (이 때 하드코딩된 zonepath가 활용됨) var pathFromRef = CalculateScriptedPath(refNode, targetNode, secondLastNode, lastDet.MotorDirection); if (pathFromRef != null && pathFromRef.Success) { // 중복 노드 제거를 위해 첫 번째 경로의 마지막 노드 정보를 제거하고 합침 pathToRef.Path.RemoveAt(pathToRef.Path.Count - 1); pathToRef.DetailedPath.RemoveAt(pathToRef.DetailedPath.Count - 1); return CombinePaths(pathToRef, pathFromRef); } } } } //이곳에서 시작,종료노드가 완전히 일치하는 경로를 찾고 있다면 그것을 바로 반환한다 //그런경우는 복잡하게 추가 계산할 필요가 없으니까 var exactMatchList = zonepath.Where(d => d.NodeSta == startNode.StationType && d.NodeEnd == targetNode.StationType && d.Monitor == monitorMode); var exactMatch = exactMatchList.FirstOrDefault(d=> d.Path.First().StartsWith(startNode.RfidId.ToString()) && d.Path.Last().StartsWith(targetNode.RfidId.ToString())); if (exactMatch != null) { int startIndex = exactMatch.Path.FindIndex(p => p == startTag); if (startIndex == -1) startIndex = exactMatch.Path.FindIndex(p => p.StartsWith(startNode.RfidId.ToString())); int endIndex = exactMatch.Path.FindLastIndex(p => p.StartsWith(targetNode.RfidId.ToString())); if (startIndex != -1 && endIndex != -1 && startIndex <= endIndex) { var slicedPath = exactMatch.Path.Skip(startIndex).Take(endIndex - startIndex + 1).ToList(); return ConvertHardcodedPathToResult(slicedPath, startNode, prevNode, prevDir); } } // 시작Zone과 목표Zone이 다른 경우에만 하드코딩된 zonepath 검색 if (startZone != targetZone) { // 목적지가 특정 장비(StationType)인 경우를 우선 검색 candidates = zonepath.Where(d => d.Monitor == monitorMode && d.NodeEnd == targetNode.StationType && d.Path.Any(p => p.StartsWith(startNode.RfidId.ToString())) && // 시작 포인트 포함 d.Path.Any(p => p.StartsWith(targetNode.RfidId.ToString())) // 끝 포인트 포함 ).ToList(); // 목적지가 일반 노드이거나 StationType이 매칭되지 않아 결과가 없을 경우, // StationType 조건 없이 모니터 방향과 노드 포함 여부만으로 경로 검색 if (!candidates.Any()) { candidates = zonepath.Where(d => d.Monitor == monitorMode && d.Path.Any(p => p.StartsWith(startNode.RfidId.ToString())) && // 시작 포인트 포함 d.Path.Any(p => p.StartsWith(targetNode.RfidId.ToString())) // 끝 포인트 포함 ).ToList(); } if (candidates.Any()) { MapZonePathData bestPath = null; int bestStartIndex = -1; int bestEndIndex = -1; int minPathLength = int.MaxValue; foreach (var candidate in candidates) { // 시작 태그와 가장 일치하는 인덱스 찾기 (방향까지 고려 "91F") int startIndex = candidate.Path.FindIndex(p => p == startTag); if (startIndex == -1) // 방향이 안 맞으면 그냥 RFID로만 찾기 startIndex = candidate.Path.FindIndex(p => p.StartsWith(startNode.RfidId.ToString())); // 끝 태그 인덱스 (뒤에서부터 찾기) int endIndex = candidate.Path.FindLastIndex(p => p.StartsWith(targetNode.RfidId.ToString())); if (startIndex != -1 && endIndex != -1 && startIndex < endIndex) { int length = endIndex - startIndex; if (length < minPathLength) { minPathLength = length; bestPath = candidate; bestStartIndex = startIndex; bestEndIndex = endIndex; } } } if (bestPath != null) { // 추출된 경로 조각 var slicedPath = bestPath.Path.Skip(bestStartIndex).Take(bestEndIndex - bestStartIndex + 1).ToList(); var a = ConvertHardcodedPathToResult(slicedPath, startNode, prevNode, prevDir); return a; } } } //추가로 처리해준다. if (startZone == MapZone.Buffer && targetZone == MapZone.Buffer) { if (startNode.Id == targetNode.Id) { //시작과 목표가 동일하다 if (monitorMode == MapZoneMonitor.RightBtm) { //방향과 모두 일치하므로 더이상 이동할 필요가 없다 //현재위치를 그대로 반환하자 var result = new AGVPathResult { Success = true }; result.Path = new List { startNode }; result.DetailedPath = new List { new NodeMotorInfo(1, startNode.Id, startNode.RfidId, prevDir, null, MagnetDirection.Straight, false) }; result.TotalDistance = 0; return result; } else { //위치는 현재위치이나 모니터방향이 일치하지 않으므로 턴을 한후 경로를 다시 찾아야한다. // 태그 내 숫자로 정확히 매칭하기 위한 람다 (예: 3을 찾을 때 "36"이 매칭되지 않도록) Func matchNode = (tag, rfid) => { string idStr = ""; foreach (char c in tag) if (char.IsDigit(c)) idStr += c; return idStr == rfid.ToString(); }; // 1.현재위치에서 턴포인트(3)까지 이동하는 경로를 찾는다. var path1Candidate = zonepath.FirstOrDefault(d => { int sIdx = d.Path.FindIndex(p => p == startTag); int tIdx = d.Path.FindIndex(p => p == "3T"); return sIdx != -1 && tIdx != -1 && sIdx <= tIdx; }); if (path1Candidate == null) return AGVPathResult.CreateFailure("턴 포인트(3)로 향하는 하드코딩 경로를 찾을 수 없습니다."); int path1StartIdx = path1Candidate.Path.FindIndex(p => p == startTag); // 방향까지 일치하는게 우선 int path1EndIdx = path1Candidate.Path.FindIndex(p => p == "3T"); var slicedPath1 = path1Candidate.Path.Skip(path1StartIdx).Take(path1EndIdx - path1StartIdx + 1).ToList(); var path1 = ConvertHardcodedPathToResult(slicedPath1, startNode, prevNode, prevDir); if (path1 == null || !path1.Success) return AGVPathResult.CreateFailure("턴 포인트(3) 경로 변환 실패"); var lastPrev = path1.Path.Count > 1 ? path1.Path[path1.Path.Count - 2] : prevNode; var lastDir = path1.DetailedPath.Last().MotorDirection; var turnNode = path1.Path.Last(); // 2.턴포인트에서 목표까지 이동하는 경로를 찾는다. var path2Candidate = zonepath.FirstOrDefault(d => { if (d.Monitor != monitorMode) return false; int tIdx = d.Path.FindIndex(p => p == "3T"); int eIdx = d.Path.FindLastIndex(p => p == $"{targetNode.RfidId}B"); return tIdx != -1 && eIdx != -1 && tIdx <= eIdx; }); if (path2Candidate == null) return AGVPathResult.CreateFailure("턴 포인트에서 목표로 향하는 하드코딩 경로를 찾을 수 없습니다."); int path2StartIdx = path2Candidate.Path.FindIndex(p => p == "3T"); int path2EndIdx = path2Candidate.Path.FindLastIndex(p => p == $"{targetNode.RfidId}B"); var slicedPath2 = path2Candidate.Path.Skip(path2StartIdx).Take(path2EndIdx - path2StartIdx + 1).ToList(); var path2 = ConvertHardcodedPathToResult(slicedPath2, turnNode, lastPrev, lastDir); if (path2 == null || !path2.Success) return AGVPathResult.CreateFailure("턴 포인트에서 목표로 향하는 경로 변환 실패"); // 3. 1+2 경로를 생성한다. path1.Path.RemoveAt(path1.Path.Count - 1); path1.DetailedPath.RemoveAt(path1.DetailedPath.Count - 1); // 경로를 반환한다. return CombinePaths(path1, path2); } } else { //시작과 목표노드가 다른경우의 처리 if (monitorMode == MapZoneMonitor.RightBtm) { //모니터가 우측이라면 방향은 맞다 if (startNode.StationType == StationType.Normal) { //일반노드에서 시작했다면 그대로 목표로 이동한다 //A*로 경로를 생성해서 진행한다.모터는 B로 이동하면된다 return this.FindBasicPath(startNode, targetNode, prevNode, AgvDirection.Backward); } else { //버퍼위치에서 다른 버퍼위치로 이동하는 경우인데. //목표위치가 좌측에 있다면 그대로 이동하면된다. bool isTargetLeft = targetNode.Position.X < startNode.Position.X; if (isTargetLeft) { return this.FindBasicPath(startNode, targetNode, prevNode, AgvDirection.Backward); } else { // 목표위치가 우측에 있다면 목표위치보다 한번 더 우측으로 이동해서 좌측으로 다시 진입 var endBufferNode = _mapNodes.FirstOrDefault(n => n.RfidId == 7); if (endBufferNode == null) return AGVPathResult.CreateFailure("버퍼 끝 노드(7)를 찾을 수 없습니다."); var overPathFull = this.FindBasicPath(startNode, endBufferNode, prevNode, AgvDirection.Forward); if (overPathFull == null || !overPathFull.Success) return AGVPathResult.CreateFailure("Overshoot 전체 경로(7번 방향) 탐색 실패"); int targetIdx = overPathFull.Path.FindIndex(n => n.Id == targetNode.Id); if (targetIdx == -1 || targetIdx == overPathFull.Path.Count - 1) return AGVPathResult.CreateFailure("Overshoot를 위한 여유 공간(다음 노드)이 없습니다."); // 목표 노드 다음 노드(오버슈트 지점)까지만 잘라내어 새 경로 구성 var overPath = new AGVPathResult { Success = true, Path = overPathFull.Path.Take(targetIdx + 2).ToList(), DetailedPath = overPathFull.DetailedPath.Take(targetIdx + 2).ToList() }; var autoOverNode = overPath.Path.Last(); // 오버슈트 된 곳 var lastDet = overPath.DetailedPath.Last(); lastDet.MotorDirection = AgvDirection.Backward; //방향을 변경 해준다. // 오버슈트 위치에서 다시 Backward로 뒤로 한 칸 이동해 targetNode에 최종 진입 overPath.Path.Add(targetNode); overPath.DetailedPath.Add(new NodeMotorInfo(lastDet.seq + 1, targetNode.Id, targetNode.RfidId, AgvDirection.Backward) { Speed = SpeedLevel.L, }); return overPath; } } } else { //모니터가 좌측이라면 턴이 필요하다. 버퍼에서 -> 턴위치까지 경로를 계산한다. //1. 현재위치에서 턴까지 후진으로 이동을한다(현재는 사용자의 코딩 스타일에 맞춰 하드코딩된 zonepath에서 추출 적용) var path1Candidate = zonepath.FirstOrDefault(d => { int sIdx = d.Path.FindIndex(p => p == startTag); int tIdx = d.Path.FindIndex(p => p == "3T"); return sIdx != -1 && tIdx != -1 && sIdx <= tIdx; }); if (path1Candidate == null) return AGVPathResult.CreateFailure("턴 포인트(3)로 향하는 하드코딩 경로를 찾을 수 없습니다."); int path1StartIdx = path1Candidate.Path.FindIndex(p => p == startTag); int path1EndIdx = path1Candidate.Path.FindIndex(p => p == "3T"); var slicedPath1 = path1Candidate.Path.Skip(path1StartIdx).Take(path1EndIdx - path1StartIdx + 1).ToList(); var path1 = ConvertHardcodedPathToResult(slicedPath1, startNode, prevNode, prevDir); if (path1 == null || !path1.Success) return AGVPathResult.CreateFailure("턴 포인트(3) 경로 변환 실패"); var lastPrev = path1.Path.Count > 1 ? path1.Path[path1.Path.Count - 2] : prevNode; var lastDir = path1.DetailedPath.Last().MotorDirection; var turnNode = path1.Path.Last(); //2. zonepath 에서 해당 경로를 찾아서 업데이트 한다. var path2Candidate = zonepath.FirstOrDefault(d => { if (d.Monitor != monitorMode) return false; int tIdx = d.Path.FindIndex(p => p == "3T"); int eIdx = d.Path.FindLastIndex(p => p == $"{targetNode.RfidId}B"); return tIdx != -1 && eIdx != -1 && tIdx <= eIdx; }); if (path2Candidate == null) return AGVPathResult.CreateFailure("턴 포인트에서 목표로 향하는 하드코딩 경로를 찾을 수 없습니다."); int path2StartIdx = path2Candidate.Path.FindIndex(p => p == "3T"); int path2EndIdx = path2Candidate.Path.FindLastIndex(p => p == $"{targetNode.RfidId}B"); var slicedPath2 = path2Candidate.Path.Skip(path2StartIdx).Take(path2EndIdx - path2StartIdx + 1).ToList(); var path2 = ConvertHardcodedPathToResult(slicedPath2, turnNode, lastPrev, lastDir); if (path2 == null || !path2.Success) return AGVPathResult.CreateFailure("턴 포인트에서 목표로 향하는 경로 변환 실패"); // 3. 1+2 경로 통합 path1.Path.RemoveAt(path1.Path.Count - 1); path1.DetailedPath.RemoveAt(path1.DetailedPath.Count - 1); return CombinePaths(path1, path2); } } } else { //다른경우는 아직 처리하지 않는다 } return AGVPathResult.CreateFailure("경로를 계산할 수 없습니다"); } private MapZoneMonitor GetMonitorMode(MapNode startNode, MapNode prevNode, AgvDirection prevDir) { if (prevNode == null) return MapZoneMonitor.RightBtm; //모니터방향도 상황에 따라 다른경우가 있다. 이것도 하드코딩하다. //prev -> start 와 모터방향(prevdir) 에 따라서 경우의 수를 입력한다. //일반노드가아닌 노드와, 일반노드중 7,11을 포함해서 모든 경우를 정의해야한다. //3 - junction(turn) if (startNode.RfidId == 3) { if (prevDir == AgvDirection.Forward) { if (prevNode.RfidId == 70) return MapZoneMonitor.RightBtm; else if (prevNode.RfidId == 11 || prevNode.RfidId == 71) return MapZoneMonitor.LeftTop; } else if (prevDir == AgvDirection.Backward) { if (prevNode.RfidId == 70) return MapZoneMonitor.LeftTop; else if (prevNode.RfidId == 11 || prevNode.RfidId == 71) return MapZoneMonitor.RightBtm; } } //7 - junction if (startNode.RfidId == 7) { if (prevDir == AgvDirection.Forward) { if (prevNode.RfidId == 10 || prevNode.RfidId == 11) return MapZoneMonitor.RightBtm; else if (prevNode.RfidId == 8) return MapZoneMonitor.LeftTop; } else if (prevDir == AgvDirection.Backward) { if (prevNode.RfidId == 10 || prevNode.RfidId == 11) return MapZoneMonitor.LeftTop; else if (prevNode.RfidId == 8) return MapZoneMonitor.RightBtm; } } //70 - cleaner if (startNode.RfidId == 70) { if (prevDir == AgvDirection.Forward) { if (prevNode.RfidId == 3) return MapZoneMonitor.LeftTop; } else if (prevDir == AgvDirection.Backward) { if (prevNode.RfidId == 3) return MapZoneMonitor.RightBtm; } } //71 - loader if (startNode.RfidId == 71) { if (prevDir == AgvDirection.Forward) { if (prevNode.RfidId == 3) return MapZoneMonitor.RightBtm; } else if (prevDir == AgvDirection.Backward) { if (prevNode.RfidId == 3) return MapZoneMonitor.LeftTop; } } //73 - charger if (startNode.RfidId == 73) { if (prevDir == AgvDirection.Forward) { if (prevNode.RfidId == 6) return MapZoneMonitor.LeftTop; } else if (prevDir == AgvDirection.Backward) { if (prevNode.RfidId == 6) return MapZoneMonitor.RightBtm; } } //72 -- plating if (startNode.RfidId == 72) { if (prevDir == AgvDirection.Forward) { if (prevNode.RfidId == 11) return MapZoneMonitor.LeftTop; } else if (prevDir == AgvDirection.Backward) { if (prevNode.RfidId == 11) return MapZoneMonitor.RightBtm; } } //31~34,35,36 --buffer if (startNode.RfidId >= 31 && startNode.RfidId <= 36) { if (prevDir == AgvDirection.Forward) { if (prevNode.RfidId == 20) return MapZoneMonitor.LeftTop; } else if (prevDir == AgvDirection.Backward) { if (prevNode.RfidId == 20) return MapZoneMonitor.RightBtm; } } int dx = startNode.Position.X - prevNode.Position.X; int dy = startNode.Position.Y - prevNode.Position.Y; bool isMonitorLeft = false; if (Math.Abs(dx) > Math.Abs(dy)) // Horizontal { isMonitorLeft = (prevDir == AgvDirection.Backward); } else // Vertical { isMonitorLeft = (prevDir == AgvDirection.Forward); } return isMonitorLeft ? MapZoneMonitor.LeftTop : MapZoneMonitor.RightBtm; } private AGVPathResult ConvertHardcodedPathToResult(List pathStrings, MapNode startNode, MapNode prevNode, AgvDirection prevDir) { var result = new AGVPathResult { Success = true }; var pathList = new List(); var detailedList = new List(); int seq = 1; for (int i = 0; i < pathStrings.Count; i++) { string s = pathStrings[i]; if (string.IsNullOrEmpty(s)) continue; string rfIdStr = ""; char flag = ' '; foreach (char c in s) { if (char.IsDigit(c)) rfIdStr += c; else flag = c; } var node = _mapNodes.FirstOrDefault(n => n.RfidId.ToString() == rfIdStr); if (node == null) continue; // Determine Motor Direction from Flag or Maintain Previous AgvDirection motorDir = detailedList.Count > 0 ? detailedList.Last().MotorDirection : prevDir; bool isTurn = false; if (flag == 'F') motorDir = AgvDirection.Forward; else if (flag == 'B') motorDir = AgvDirection.Backward; else if (flag == 'T') isTurn = true; pathList.Add(node); // Magnet direction lookup MagnetDirection magDir = MagnetDirection.Straight; if (i + 1 < pathStrings.Count) { var nextTag = pathStrings[i + 1]; string nextRfidStr = ""; foreach (char c in nextTag) if (char.IsDigit(c)) nextRfidStr += c; var nextNode = _mapNodes.FirstOrDefault(n => n.RfidId.ToString() == nextRfidStr); if (nextNode != null && node.MagnetDirections.ContainsKey(nextNode.Id)) { var magPos = node.MagnetDirections[nextNode.Id]; if (magPos == MagnetPosition.R) magDir = MagnetDirection.Right; else if (magPos == MagnetPosition.L) magDir = MagnetDirection.Left; } } var info = new NodeMotorInfo(seq++, node.Id, node.RfidId, motorDir, null, magDir, isTurn); detailedList.Add(info); } // Connect NextNode pointers for (int i = 0; i < detailedList.Count - 1; i++) { detailedList[i].NextNode = pathList[i + 1]; } result.Path = pathList; result.DetailedPath = detailedList; result.TotalDistance = CalculatePathDistance(pathList); return result; } private float CalculatePathDistance(List path) { float dist = 0; for (int i = 0; i < path.Count - 1; i++) { dist += (float)Math.Sqrt(Math.Pow(path[i].Position.X - path[i + 1].Position.X, 2) + Math.Pow(path[i].Position.Y - path[i + 1].Position.Y, 2)); } return dist; } private MapNode GetZoneExitNode(MapZone zone) { int exitRfid = -1; switch (zone) { case MapZone.Buffer: exitRfid = 7; break; case MapZone.Charger: exitRfid = 10; break; // Or 6? Assuming 10 based on flow case MapZone.Plating: exitRfid = 72; break; case MapZone.Loader: exitRfid = 71; break; case MapZone.Cleaner: exitRfid = 70; break; case MapZone.Junction: return null; } return _mapNodes.FirstOrDefault(n => n.RfidId == exitRfid); } private MapNode GetZoneEntryNode(MapZone zone) { int entryRfid = -1; switch (zone) { case MapZone.Buffer: entryRfid = 7; break; // Bi-directional entry/exit? // Usually Buffer entry might be different (e.g. 91?). // But user didn't specify directional flow constraints for zones other than turn logic. // Let's assume Entry = Exit for single-lane spurs, or define specific entry points. // If Buffer is 91~07, maybe Entry is 7? case MapZone.Charger: entryRfid = 10; break; case MapZone.Plating: entryRfid = 72; break; case MapZone.Loader: entryRfid = 71; break; case MapZone.Cleaner: entryRfid = 70; break; } return _mapNodes.FirstOrDefault(n => n.RfidId == entryRfid); } /// /// 길목(Gateway) 기반 고급 경로 계산 (기존 SimulatorForm.CalcPath 이관) /// public AGVPathResult CalculatePath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection prevDir) { AGVPathResult Retval; // var o_StartNode = startNode; // startNode, targetNode는 이미 인자로 받음 if (startNode == null || targetNode == null) return AGVPathResult.CreateFailure("시작/종료노드가 지정되지 않음"); try { // 종료노드라면 이전위치로 이동시켜야한다. (Simulator Logic) // 만약 시작노드가 끝단(ConnectedMapNodes.Count == 1)이라면, // AGV가 해당 노드에 '도착'한 상태가 아니라 '작업' 중일 수 있으므로 // 이전 노드(진입점)로 위치를 보정하여 경로를 계산한다. AGVPathResult LimitPath = null; if (startNode.ConnectedMapNodes.Count == 1) { // 시작점 -> 이전점 경로 (보통 후진이나 전진 1칸) LimitPath = this.FindPathAStar(startNode, prevNode); if (LimitPath.Success) { 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)); } // 시작 위치 및 방향 변경 // var org_start = startNode; // Unused startNode = prevNode; prevNode = LimitPath.Path.First(); // startNode (original) prevDir = (prevDir == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward); } else { // 경로 생성 실패 시 보정 없이 진행하거나 에러 처리 // 여기서는 일단 기존 로직대로 진행 } } // 2. Buffer-to-Buffer 예외 처리 // 05~31 구간 체크 var node_buff_sta = _mapNodes.Where(t => t.StationType == StationType.Buffer).OrderBy(s => s.RfidId).FirstOrDefault();// (n => n.RfidId == 5); var node_buff_end = _mapNodes.Where(t => t.StationType == StationType.Buffer).OrderByDescending(s => s.RfidId).FirstOrDefault();// bool fixpath = false; Retval = null; MapNode gatewayNode = null; if (node_buff_sta != null && node_buff_end != null) { // 버퍼 구간 경로 테스트 var rlt = this.FindPathAStar(node_buff_sta, node_buff_end); if (rlt.Success) { // 버퍼구간내에 시작과 종료가 모두 포함되어있다 if (rlt.Path.Find(n => n.Id == startNode.Id) != null && rlt.Path.Find(n => n.Id == targetNode.Id) != null) { Retval = CalcPathBufferToBuffer(startNode, targetNode, prevNode, prevDir); fixpath = true; } } } if (!fixpath) { if (targetNode.StationType == StationType.Limit || targetNode.StationType == StationType.Normal) { //일반노드라면 방향 무관하게 그냥 이동하게한다. Retval = this.FindBasicPath(startNode, targetNode, prevNode, prevDir); } else { //목적지까지 그대로 방향을 계산한다. var pathToTaget = this.FindBasicPath(startNode, targetNode, prevNode, prevDir); if (pathToTaget.Success == false) return AGVPathResult.CreateFailure($"Target({targetNode.ID2})까지 경로 실패: {pathToTaget.Message}"); // 방향을 확인하여, 왔던 방향으로 되돌아가야 한다면 방향 반전 //다음이동방향이 이전노드와 동일하다면? 되돌아가야한다는것이다 var predictNext = pathToTaget.Path[1]; if (predictNext.Id == prevNode.Id) { var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward; foreach (var item in pathToTaget.DetailedPath) item.MotorDirection = reverseDir; } if (targetNode.DockDirection != DockingDirection.DontCare) { Retval = pathToTaget; } //현재 진행방향과 목적지의 도킹방향이 일치하는지 확인한다. else if ((pathToTaget.DetailedPath.Last().MotorDirection == AgvDirection.Backward && targetNode.DockDirection == DockingDirection.Backward) || (pathToTaget.DetailedPath.Last().MotorDirection == AgvDirection.Forward && targetNode.DockDirection == DockingDirection.Forward)) { //일치하는 경우 그대로 사용하낟. Retval = pathToTaget; } else { //불일치하는경우라면 Turn 가능노드를 찾아서 이동한 후 턴을 한다. // 3. 목적지별 Turn 및 진입 조건 확인 gatewayNode = GetTurnNode(targetNode); // Gateway Node 찾음 // 4. Start -> Gateway 경로 계산 (A*) var pathToGateway = this.FindBasicPath(startNode, gatewayNode, prevNode, prevDir); if (pathToGateway.Success == false) return AGVPathResult.CreateFailure($"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.Message}"); // 방향을 확인하여, 왔던 방향으로 되돌아가야 한다면 방향 반전 if (pathToGateway.Path.Count > 1) { //다음이동방향이 이전노드와 동일하다면? 되돌아가야한다는것이다 predictNext = pathToGateway.Path[1]; if (predictNext.Id == prevNode.Id) { var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward; foreach (var item in pathToGateway.DetailedPath) item.MotorDirection = reverseDir; } } // 마지막 경로는 게이트웨이이므로 제거 (Gateway 진입 후 처리는 GetPathFromGateway에서 담당) if (pathToGateway.Path.Count > 0 && pathToGateway.Path.Last().Id == gatewayNode.Id) { var idx = pathToGateway.Path.Count - 1; pathToGateway.Path.RemoveAt(idx); pathToGateway.DetailedPath.RemoveAt(idx); } // 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함) MapNode GateprevNode = pathToGateway.Path.LastOrDefault() ?? prevNode; NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.LastOrDefault(); var arrivalOrientation = GatePrevDetail?.MotorDirection ?? prevDir; var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, GateprevNode, arrivalOrientation); if (!gatewayPathResult.Success) return AGVPathResult.CreateFailure($"{gatewayPathResult.Message}"); Retval = CombinePaths(pathToGateway, gatewayPathResult); } } } //게이트웨이 Retval.Gateway = gatewayNode; // 경로 오류 검사 if (Retval == null || Retval.Success == false) return Retval ?? AGVPathResult.CreateFailure("경로 계산 결과 없음"); if (LimitPath != null) { Retval = CombinePaths(LimitPath, Retval); } // 해당 경로와 대상의 도킹포인트 방향 검사 if (targetNode.DockDirection != DockingDirection.DontCare) { var lastPath = Retval.DetailedPath.LastOrDefault(); if (lastPath != null) { if (targetNode.DockDirection == DockingDirection.Forward && lastPath.MotorDirection != AgvDirection.Forward) { return AGVPathResult.CreateFailure($"생성된 경로와 목적지의 도킹방향이 일치하지 않습니다(FWD) Target:{targetNode.DockDirection}"); } if (targetNode.DockDirection == DockingDirection.Backward && lastPath.MotorDirection != AgvDirection.Backward) { return AGVPathResult.CreateFailure($"생성된 경로와 목적지의 도킹방향이 일치하지 않습니다(BWD) Target:{targetNode.DockDirection}"); } } } // 경로 최적화: A -> B -> A 패턴 제거 // 6[F][R] → 13[B][L] → 6[F][L] 같은 경우 제거 while (fixpath == false) { 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; } // 불가능한 회전 경로 검사 (사용자 요청 로직 반영) 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 && n1.MotorDirection == n2.MotorDirection) // Fix: 중간 노드 방향도 같을 때만 에러 { return AGVPathResult.CreateFailure($"불가능한 회전 경로가 포함되어있습니다. {n1.RfidId}->{n2.RfidId}->{n3.RfidId}"); } } // 기타 검증 로직 (마지막 노드 도킹, 시작노드 일치 등) var lastnode = Retval.Path.Last(); if (lastnode.StationType != StationType.Normal) { var lastnodePath = Retval.DetailedPath.Last(); if (lastnode.DockDirection == DockingDirection.Forward && lastnodePath.MotorDirection != AgvDirection.Forward) return AGVPathResult.CreateFailure($"목적지의 모터방향({lastnode.DockDirection}) 불일치 경로방향({lastnodePath.MotorDirection})"); if (lastnode.DockDirection == DockingDirection.Backward && lastnodePath.MotorDirection != AgvDirection.Backward) return AGVPathResult.CreateFailure($"목적지의 모터방향({lastnode.DockDirection}) 불일치 경로방향({lastnodePath.MotorDirection})"); } // 첫번째 노드 일치 검사 - 필요시 수행 (startNode가 변경될 수 있어서 o_StartNode 등 필요할 수도 있음) // 여기서는 생략 혹은 간단히 체크 // 되돌아가는 길 방향 일치 검사 if (Retval.DetailedPath.Count > 1) { var FirstDetailPath = Retval.DetailedPath[0]; var NextDetailPath = Retval.DetailedPath[1]; AgvDirection? PredictNextDir = null; if (NextDetailPath.NodeId == prevNode.Id) { if (NextDetailPath.MagnetDirection == MagnetDirection.Straight) PredictNextDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward; } if (PredictNextDir != null && (FirstDetailPath.MotorDirection != (AgvDirection)PredictNextDir)) { // return AGVPathResult.CreateFailure($"되돌아가는 길인데 방향이 일치하지않음"); // 경고 수준이나 무시 가능한 경우도 있음 } } // 연결성 검사 for (int i = 0; i < Retval.DetailedPath.Count - 1; i++) { var cnode = Retval.Path[i]; var nnode = Retval.Path[i + 1]; if (cnode.ConnectedNodes.Contains(nnode.Id) == false && cnode.Id != nnode.Id) { return AGVPathResult.CreateFailure($"[{cnode.RfidId}] 노드에 연결되지 않은 [{nnode.RfidId}]노드가 지정됨"); } } //각 도킹포인트별로 절대 움직이면 안되는 조건확인 var firstnode = Retval.Path.FirstOrDefault(); var firstDet = Retval.DetailedPath.First(); var failmessage = $"[{firstnode.ID2}] 노드의 시작모터 방향({firstDet.MotorDirection})이 올바르지 않습니다"; if (firstnode.StationType == StationType.Charger && firstDet.MotorDirection != AgvDirection.Forward) return AGVPathResult.CreateFailure(failmessage); else if (firstnode.StationType == StationType.Loader && firstDet.MotorDirection != AgvDirection.Backward) return AGVPathResult.CreateFailure(failmessage); else if (firstnode.StationType == StationType.Cleaner && firstDet.MotorDirection != AgvDirection.Backward) return AGVPathResult.CreateFailure(failmessage); else if (firstnode.StationType == StationType.Plating && firstDet.MotorDirection != AgvDirection.Backward) return AGVPathResult.CreateFailure(failmessage); else if (firstnode.StationType == StationType.Buffer) { //버퍼는 도킹이되어잇느닞 확인하고. 그때 방향을 체크해야한다. } return Retval; } catch (Exception ex) { return AGVPathResult.CreateFailure($"[계산오류] {ex.Message}"); } } private AGVPathResult CalcPathBufferToBuffer(MapNode start, MapNode target, MapNode prev, AgvDirection prevDir) { // Monitor Side 판단 및 Buffer 간 이동 로직 int deltaX = 0; int deltaY = 0; if (prev == null) return AGVPathResult.CreateFailure("이전 노드 정보가 없습니다"); else { deltaX = start.Position.X - prev.Position.X; deltaY = -(start.Position.Y - prev.Position.Y); } if (Math.Abs(deltaY) > Math.Abs(deltaX)) deltaX = deltaY; bool isMonitorLeft = false; if (deltaX > 0) // 오른쪽(Forward)으로 이동해 옴 isMonitorLeft = (prevDir == AgvDirection.Backward); else if (deltaX < 0) // 왼쪽(Reverse)으로 이동해 옴 isMonitorLeft = (prevDir == AgvDirection.Forward); else return AGVPathResult.CreateFailure("이전 노드와의 방향을 알 수 없습니다"); if (isMonitorLeft) { // Monitor Left -> Gateway 탈출 var GateWayNode = _mapNodes.FirstOrDefault(n => n.RfidId == 6); var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward; AGVPathResult escPath = null; if (start.Position.X > prev.Position.X) escPath = this.FindBasicPath(start, GateWayNode, prev, prevDir); else escPath = this.FindBasicPath(start, GateWayNode, prev, reverseDir); if (!escPath.Success) return AGVPathResult.CreateFailure("버퍼 탈출 경로 실패"); var lastNode = escPath.Path.Last(); var lastPrev = escPath.Path[escPath.Path.Count - 2]; var lastDir = escPath.DetailedPath.Last().MotorDirection; var gateToTarget = GetPathFromGateway(GateWayNode, target, lastPrev, lastDir); escPath.Path.RemoveAt(escPath.Path.Count - 1); escPath.DetailedPath.RemoveAt(escPath.DetailedPath.Count - 1); return CombinePaths(escPath, gateToTarget); } else { // Monitor Right -> 직접 진입 또는 Overshoot bool isTargetLeft = target.Position.X < start.Position.X; if (target == start) { // 제자리 재정렬 (Same as Simulator logic) var list = new List(); var retval = AGVPathResult.CreateSuccess(list, new List(), 0, 0); var resversedir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward; retval.Path.Add(target); if (deltaX < 0) { var nextNode = start.ConnectedMapNodes.Where(t => t.Id != prev.Id && t.StationType == StationType.Buffer).FirstOrDefault(); if (nextNode != null) { retval.DetailedPath.Add(new NodeMotorInfo(1, target.Id, target.RfidId, prevDir)); retval.Path.Add(nextNode); var lastDefailt = retval.DetailedPath.Last(); retval.DetailedPath.Add(new NodeMotorInfo(lastDefailt.seq + 1, nextNode.Id, nextNode.RfidId, AgvDirection.Forward) { Speed = SpeedLevel.M, }); retval.Path.Add(target); retval.DetailedPath.Add(new NodeMotorInfo((retval.DetailedPath.Max(t => t.seq) + 1), target.Id, target.RfidId, AgvDirection.Forward)); retval.Path.Add(target); retval.DetailedPath.Add(new NodeMotorInfo(retval.DetailedPath.Max(t => t.seq) + 1, target.Id, target.RfidId, AgvDirection.Backward)); } else { retval.DetailedPath.Add(new NodeMotorInfo(1, target.Id, target.RfidId, resversedir)); retval.Path.Add(prev); retval.DetailedPath.Add(new NodeMotorInfo(retval.DetailedPath.Last().seq + 1, prev.Id, prev.RfidId, prevDir) { Speed = SpeedLevel.M, }); retval.Path.Add(target); retval.DetailedPath.Add(new NodeMotorInfo(retval.DetailedPath.Max(t => t.seq) + 1, target.Id, target.RfidId, prevDir)); } } else { retval.DetailedPath.Add(new NodeMotorInfo(1, target.Id, target.RfidId, prevDir)); var nextNode = start.ConnectedMapNodes.Where(t => t.Id != prev.Id && t.StationType == StationType.Buffer).FirstOrDefault(); retval.Path.Add(nextNode); var lastDefailt = retval.DetailedPath.Last(); retval.DetailedPath.Add(new NodeMotorInfo(lastDefailt.seq + 1, nextNode.Id, nextNode.RfidId, AgvDirection.Backward) { Speed = SpeedLevel.L, }); retval.Path.Add(target); retval.DetailedPath.Add(new NodeMotorInfo(retval.DetailedPath.Max(t => t.seq) + 1, target.Id, target.RfidId, AgvDirection.Backward)); } return retval; } else if (isTargetLeft) { return this.FindBasicPath(start, target, prev, AgvDirection.Backward); } else { // Overshoot var path1 = this.FindBasicPath(start, target, prev, AgvDirection.Forward); if (path1.Path.Count < 2) return AGVPathResult.CreateFailure("Overshoot 경로 생성 실패"); var last = path1.Path.Last(); var lastD = path1.DetailedPath.Last(); path1.Path.RemoveAt(path1.Path.Count - 1); path1.DetailedPath.RemoveAt(path1.DetailedPath.Count - 1); path1.Path.Add(last); path1.DetailedPath.Add(new NodeMotorInfo(lastD.seq + 1, lastD.NodeId, lastD.RfidId, AgvDirection.Backward) { Speed = SpeedLevel.L, }); return path1; } } } private AGVPathResult GetPathFromGateway(MapNode GTNode, MapNode targetNode, MapNode PrevNode, AgvDirection PrevDirection) { AGVPathResult resultPath = null; var deltaX = GTNode.Position.X - PrevNode.Position.X; var isMonitorLeft = false; if (deltaX > 0) isMonitorLeft = PrevDirection == AgvDirection.Backward; else isMonitorLeft = PrevDirection == AgvDirection.Forward; if (targetNode.StationType == StationType.Loader) { deltaX = GTNode.Position.Y - PrevNode.Position.Y; if (deltaX < 0) isMonitorLeft = PrevDirection == AgvDirection.Backward; else isMonitorLeft = PrevDirection == AgvDirection.Forward; } switch (targetNode.StationType) { case StationType.Loader: case StationType.Charger: case StationType.Cleaner: case StationType.Plating: case StationType.Buffer: var rlt1 = new AGVPathResult(); rlt1.Success = true; //단순 경로를 찾는다 var motdir = targetNode.DockDirection == DockingDirection.Backward ? AgvDirection.Backward : AgvDirection.Forward; var pathtarget = this.FindBasicPath(GTNode, targetNode, PrevNode, motdir); if ((targetNode.DockDirection == DockingDirection.Backward && isMonitorLeft) || (targetNode.DockDirection == DockingDirection.Forward && !isMonitorLeft)) { var turnPatterns = GetTurnaroundPattern(GTNode, targetNode); if (turnPatterns == null || !turnPatterns.Any()) return new AGVPathResult { Success = false, Message = $"회차 패턴 없음: Dir {PrevDirection}" }; foreach (var item in turnPatterns) { var rfidvalue = ushort.Parse(item.Substring(0, 4)); var node = _mapNodes.FirstOrDefault(t => t.RfidId == rfidvalue); rlt1.Path.Add(node); AgvDirection nodedir = item.Substring(4, 1) == "F" ? AgvDirection.Forward : AgvDirection.Backward; MagnetDirection magnet = MagnetDirection.Straight; var magchar = item.Substring(5, 1); if (magchar == "L") magnet = MagnetDirection.Left; else if (magchar == "R") magnet = MagnetDirection.Right; rlt1.DetailedPath.Add(new NodeMotorInfo(rlt1.DetailedPath.Count, node.Id, node.RfidId, nodedir, null, magnet) { Speed = SpeedLevel.L, }); } if (pathtarget.DetailedPath.First().NodeId != rlt1.DetailedPath.Last().NodeId || pathtarget.DetailedPath.First().MotorDirection != rlt1.DetailedPath.Last().MotorDirection) { // Gateway 턴 마지막 주소 불일치 경고 (로깅 등) } pathtarget.Path.RemoveAt(0); pathtarget.DetailedPath.RemoveAt(0); } return CombinePaths(rlt1, pathtarget); default: return AGVPathResult.CreateFailure($"지원되지 않는 StationType: {targetNode.StationType}"); } } private MapNode GetTurnNode(MapNode node) { var rfid = 0; if (node.StationType == StationType.Cleaner) rfid = 3; else if (node.StationType == StationType.Charger) rfid = 3; else if (node.StationType == StationType.Plating) rfid = 3; else if (node.StationType == StationType.Loader) rfid = 3; else if (node.StationType == StationType.Buffer) rfid = 3; if (rfid == 0) return null; return _mapNodes.FirstOrDefault(t => t.RfidId == rfid); } private List GetTurnaroundPattern(MapNode gatewayNode, MapNode targetNode) { switch (gatewayNode.RfidId) { case 6: if (targetNode.StationType == StationType.Buffer) return new List { "0006BL", "0007FS", "0013BL", "0006BL" }; else return new List { "0006BL", "0007FS", "0013BL", "0006BS" }; case 9: return new List { "0009FL", "0010BS", "0007FL", "0009FS" }; case 10: return new List { "0010BR", "0009FR", "0007BS", "0010BS" }; case 13: return new List { "0013BL", "0006FL", "0007BS", "0013BS" }; default: return null; } } private AGVPathResult CombinePaths(AGVPathResult p1, AGVPathResult p2) { var res = new AGVPathResult(); res.Success = true; var p1last = p1.DetailedPath.LastOrDefault(); var p2fist = p2.DetailedPath.FirstOrDefault(); if (p1last != null && p2fist != null && (p1last.NodeId == p2fist.NodeId && p1last.MotorDirection == p2fist.MotorDirection && p1last.MagnetDirection == p2fist.MagnetDirection)) { p1.Path.RemoveAt(p1.Path.Count - 1); p1.DetailedPath.RemoveAt(p1.DetailedPath.Count - 1); } foreach (var item in p1.Path) res.Path.Add(item); 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); item.seq = maxseq + 1; res.DetailedPath.Add(item); } foreach (var item in p2.DetailedPath) { var maxseq = res.DetailedPath.Count == 0 ? 0 : res.DetailedPath.Max(t => t.seq); item.seq = maxseq + 1; res.DetailedPath.Add(item); } return res; } } }