using AGVControl.Models; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.CompilerServices; using System.Security.Cryptography.X509Certificates; using System.Threading; using static System.Windows.Forms.VisualStyles.VisualStyleElement.TextBox; namespace AGVControl { public class MapControlManager { public AGVActionPrediction PredictResult = null; public AGV agv = new AGV(); public List RFIDPoints = new List(); public HashSet rfidConnections = new HashSet(); private ManualResetEvent mrepredict = new ManualResetEvent(true); public MapControlManager() { } ~MapControlManager() { } /// /// 목표지정으로 모터방향이 이동하고 있는가? /// history 데이터가 있어야 하며 기준데이터가 없는 경우 null 반환 /// /// public bool? IsMotDirection_To_Target() { if (agv.MovementHistory.Any() == false || agv.MovementHistory.Count < 2) return null; if (agv.MainPath.Any() == false) return null; var prept = agv.MovementHistory.Skip(agv.MovementHistory.Count - 2).First(); var lstpt = agv.MovementHistory.Last(); //현재 이후의 경로를 가져온다 var curidx = agv.MainPath.FindIndex(t => t.Value == lstpt.Value); var preidx = agv.MainPath.FindIndex(t => t.Value == prept.Value); if (curidx == -1 || preidx == -1) return null; //지정된경로 반대방향으로 이동하고 있다 return preidx < curidx; } public float GetDistance(Point p1, Point p2) { float dx = p1.X - p2.X; float dy = p1.Y - p2.Y; return (float)Math.Sqrt(dx * dx + dy * dy); // double을 float로 명시적 캐스팅 } public AGVActionPrediction PredictNextAction() { if (mrepredict.WaitOne(1) == false) { PredictResult = CreatePrediction("이전 작업이 완료되지 않았습니다", AGVActionReasonCode.busy, AGVMoveState.Stop, agv.Current_Motor_Direction, false); return PredictResult; } mrepredict.Reset(); try { // 0. 설정경로와 리프트 방향 체크 (경로설정이 없을때에는 직선이동경로내의 방향들과 체크한다) agv.IsTargetDirectionMatch = IsLiftDirectionMatch() ?? false; // 1. 위치를 모를 때 (CurrentRFID가 0 또는 미설정) if (agv.CurrentRFID.Value == 0) { PredictResult = CreatePrediction("AGV 위치 미확정(처음 기동)", AGVActionReasonCode.NoPosition, AGVMoveState.Run, AgvDir.Backward, true, moveSpeed: AgvSpeed.Low, moveDiv: AgvSts.Straight); return PredictResult; } //2. 이동방향을 모른다 if (agv.MovementHistory.Any() == false || agv.MovementHistory.Count < 2) { PredictResult = CreatePrediction("AGV이동방향 알수없음", AGVActionReasonCode.NoDirection, AGVMoveState.Run, AgvDir.Backward, true, moveSpeed: AgvSpeed.Low, moveDiv: AgvSts.Straight); return PredictResult; } // 3. 경로가 없거나 현재 위치가 경로에 없음 if ((agv.MainPath?.Count ?? 0) < 2 || agv.MainPath.Last().Value != agv.TargetRFID.Value) { //목적지가 없다면 진행할 수 없다 if (agv.TargetRFID == null) { PredictResult = CreatePrediction("경로 없음 또는 현재 위치 미확정", AGVActionReasonCode.NoPath, AGVMoveState.Stop, agv.Current_Motor_Direction, true); return PredictResult; } else { //목적지가 있는데 경로가 없다면 경로 예측을 진행해야한다. var CurPt = agv.MovementHistory.Last(); var prlt = CalculatePath(CurPt, agv.TargetRFID); if (prlt.Success == false) { PredictResult = CreatePrediction("목적지 경로 예측 실패", AGVActionReasonCode.Unknown, AGVMoveState.Stop, agv.Current_Motor_Direction, true, nextRFID: agv.TargetRFID); return PredictResult; } else { //신규목적지에 대한 경로예측이 성공했다 agv.SubPath.Clear(); agv.MainPath = prlt.Path; } } } // 4. 경로상에서 다음 RFID 예측 int idx = agv.MainPath.FindIndex(p => p.Value == agv.CurrentRFID.Value); if (idx < 0) { PredictResult = CreatePrediction("현재 위치가 경로에 없음", AGVActionReasonCode.NotOnPath, AGVMoveState.Stop, agv.Current_Motor_Direction, true); return PredictResult; } // 4. 목적지 도달 전, 회전이 필요한경우인가? // 목적지 RFID 정보 var destRFID = agv.MainPath.Last(); //리프트 방향이 맞는가? var IsLiftDir = IsLiftDirectionMatch() ?? false; //모션이동방향이 맞는가? var IsMotDir = IsMotDirection_To_Target() ?? false; var PrePT = agv.MovementHistory.Skip(agv.MovementHistory.Count - 2).First(); var curPT = agv.MovementHistory.Last(); //리프트방향이 맞지 않다면 회전가능한 위치로 이동을 해야한다 if (IsLiftDir == false) { AgvSpeed? agv_spd = null; AgvSts? agv_dir = null; //회전가능한 위치로 이동을 해야한다 //1. 가까운 회전위치를 찾는다 var nearTurnPoint = RFIDPoints.Where(t => t.IsRotatable)?.OrderBy(t => GetDistance(t.Location, agv.CurrentRFID.Location)).FirstOrDefault() ?? null; if (nearTurnPoint == null) { PredictResult = CreatePrediction("회전 가능한 위치가 없습니다", AGVActionReasonCode.NoTurnPoint, AGVMoveState.Stop, agv.Current_Motor_Direction, true); return PredictResult; } //2. 이동하기위한 경로계산 및 이동을 한다 (생성조건) //2-1. 서브경로가없는경우 //2-2. 시작과 종료번호가 다른 경우(경로가 변경이 되는 조건이다) if (agv.CurrentRFID.Value != nearTurnPoint.Value) { if (agv.SubPath.Any() == false || agv.SubPath.Count < 2 || agv.SubPath.First().Value != agv.CurrentRFID.Value || agv.SubPath.Last().Value != nearTurnPoint.Value) { var rlt = CalculatePath(agv.CurrentRFID, nearTurnPoint); //이전포인트도 추가를 해준다 if (rlt.Success) agv.SubPath = rlt.Path; else { agv.SubPath.Clear(); PredictResult = CreatePrediction("회전 위치까지의 경로를 계산할 수 없습니다", AGVActionReasonCode.PathCalcError, AGVMoveState.Stop, agv.Current_Motor_Direction, true, nextRFID: nearTurnPoint); return PredictResult; } } //현재 모터방향을 확인하여 대상까지 이동하도록 해야한다 var curidx = agv.SubPath.FindIndex(t => t.Value == curPT.Value); var preidx = agv.SubPath.FindIndex(t => t.Value == PrePT.Value); AgvDir newdirection = agv.Current_Motor_Direction; string message = "턴위치로 이동중"; if (preidx > curidx) { //지정경로를 거꾸로 이동하고 있다 if (agv.Current_Motor_Direction == AgvDir.Forward) newdirection = AgvDir.Backward; else newdirection = AgvDir.Forward; message += "(방향전환)"; } //도로정보를 확인하여 속도와 분기명령을 실행한다 var roadinfo = GetRoadInfo(agv.SubPath, curPT); agv_spd = roadinfo.spd; agv_dir = roadinfo.dir; PredictResult = CreatePrediction(message, AGVActionReasonCode.MoveForTurn, AGVMoveState.Run, newdirection, true, moveSpeed: agv_spd, moveDiv: agv_dir, nextRFID: nearTurnPoint); return PredictResult; } var roadinfo2 = GetRoadInfo(agv.SubPath.Any() ? agv.SubPath : agv.MainPath, curPT); agv_spd = roadinfo2.spd; agv_dir = roadinfo2.dir; PredictResult = CreatePrediction("턴(이동) 완료 대기", AGVActionReasonCode.NeedTurnMove, AGVMoveState.Stop, agv.Current_Motor_Direction, true, moveSpeed: agv_spd, moveDiv: agv_dir, nextRFID: nearTurnPoint); return PredictResult; } //보조이동선제거 if (agv.SubPath != null && agv.SubPath.Any()) agv.SubPath.Clear(); //현재위치의 RFID에서 턴이 필요한경우 if (agv.CurrentRFID.NeedTurn) { //Turn이 완료되지 않았다면 턴 완료를 대기한다. if (agv.CurrentRFID.TurnOK == false) { //아직 턴위치에 멈추지 않았다 if (agv.CurrentRFID.TurnStop == false) { //현재위치로 마크스탑이동을 하게한다 PredictResult = CreatePrediction("Wait for Turn(P)-Mark Stop", AGVActionReasonCode.WaitForMarkStop, AGVMoveState.Run, agv.Current_Motor_Direction, true, moveSpeed: agv.CurrentSpeed, moveDiv: agv.CurrentSTS); } else { //턴위치에 정지했으니. 턴완료를 기다려야 한다 PredictResult = CreatePrediction("Wait for Turn(P)", AGVActionReasonCode.NeedTurnPoint, AGVMoveState.Run, agv.Current_Motor_Direction, true, moveSpeed: agv.CurrentSpeed, moveDiv: agv.CurrentSTS); } return PredictResult; } } //3. 목적지위치까지 이동이 완료되지 않았다면 계속 이동을 하게한다 if (agv.CurrentRFID.Value != destRFID.Value) { //현재 모터방향을 확인하여 대상까지 이동하도록 해야한다 var curidx = agv.MainPath.FindIndex(t => t.Value == curPT.Value); var preidx = agv.MainPath.FindIndex(t => t.Value == PrePT.Value); AgvDir newdirection = agv.Current_Motor_Direction; string message = "목적지 이동중"; if (preidx > curidx) { //지정경로를 거꾸로 이동하고 있다 if (agv.Current_Motor_Direction == AgvDir.Forward) newdirection = AgvDir.Backward; else newdirection = AgvDir.Forward; message += "(방향전환)"; } //경로상 바로 다음 위치를 확인한다 var nexstRFID = agv.MainPath.Skip(agv.MainPath.FindIndex(t => t.Value == curPT.Value) + 1).First(); var roadinfo = GetRoadInfo(agv.MainPath, curPT); PredictResult = CreatePrediction(message, AGVActionReasonCode.Normal, AGVMoveState.Run, newdirection, true, moveSpeed: roadinfo.spd, moveDiv: roadinfo.dir, nextRFID: nexstRFID); return PredictResult; } // 5. 목적지 도달 시 PredictResult = CreatePrediction("경로의 마지막 지점(목적지 도달)", AGVActionReasonCode.Arrived, AGVMoveState.Stop, agv.Current_Motor_Direction, true, nextRFID: destRFID); return PredictResult; } catch (Exception ex) { PredictResult = CreatePrediction($"ERR:{ex.Message}", AGVActionReasonCode.Unknown, AGVMoveState.Stop, agv.Current_Motor_Direction, true); return PredictResult; } finally { mrepredict.Set(); } } (AgvSpeed? spd, AgvSts? dir, RFIDConnection info) GetRoadInfo(List paths, RFIDPoint curPT) { //도로정보를 확인하여 속도와 분기명령을 실행한다 AgvSpeed? agv_spd = null; AgvSts? agv_div = null; RFIDConnection info = null; var nextpt = paths.Skip(paths.FindIndex(t => t.Value == curPT.Value) + 1).FirstOrDefault(); if (nextpt != null) { var p1 = rfidConnections.Where(t => t.P1.Value == curPT.Value && t.P2.Value == nextpt.Value).FirstOrDefault(); if (p1 != null) { //positive agv_spd = p1.MoveSpeedP; agv_div = p1.MoveDirectionP; info = p1; } var p2 = rfidConnections.Where(t => t.P2.Value == curPT.Value && t.P1.Value == nextpt.Value).FirstOrDefault(); if (p2 != null) { //negative agv_spd = p2.MoveSpeedN; agv_div = p2.MoveDirectionN; info = p2; } } return (agv_spd, agv_div, info); } /// /// 이웃포인터를 반환합니다 /// /// /// public List GetNeighbors(RFIDPoint pt) { var neighbors = new List(); //값이 없는 경우 오류 반환 if (pt == null) return neighbors; //연결정보에서 데이터를 찾은 후 반환한다 foreach (var connection in rfidConnections) { RFIDPoint nPT = null; if (connection.P1.Value == pt.Value) { nPT = connection.P2; } else if (connection.P2.Value == pt.Value) { nPT = connection.P1; } if (nPT != null) neighbors.Add(nPT); } //중복제거후 반한 return neighbors.Distinct().ToList(); } public RFIDPoint FindRFIDPoint(uint rfidValue) { if (RFIDPoints == null || RFIDPoints.Any() == false) return null; return RFIDPoints.FirstOrDefault(r => r.Value == rfidValue); } private float Heuristic(Point a, Point b) { return (float)Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2)); } private PathResult ReconstructPath(Dictionary cameFrom, RFIDPoint current) { var path = new List { current }; while (cameFrom.ContainsKey(current.Value)) { current = cameFrom[current.Value]; if (path.Contains(current) == false) path.Insert(0, current); else break; //왜인지 반복되는경우가있어 종료한다 } return new PathResult { Path = path, }; } public PathResult CalculatePath(RFIDPoint start, RFIDPoint end) { var openList = new List { start }; var closedList = new List(); var cameFrom = new Dictionary(); var gScore = new Dictionary { { start, 0 } }; var fScore = new Dictionary { { start, Heuristic(start.Location, end.Location) } }; if (start.Value == end.Value) { return new PathResult { Message = "시작위치와 대상위치가 동일 합니다", Path = null, }; } //if (autorun) openList.Clear(); while (openList.Count > 0) { var current = openList.OrderBy(p => fScore.ContainsKey(p) ? fScore[p] : float.MaxValue).First(); if (current.Value == end.Value) { return ReconstructPath(cameFrom, current); } openList.Remove(current); closedList.Add(current); var nb = GetNeighbors(current); foreach (var neighbor in nb) { if (closedList.Contains(neighbor)) continue; float tentativeGScore = gScore[current] + GetDistance(current.Location, neighbor.Location); if (!openList.Contains(neighbor)) openList.Add(neighbor); else if (tentativeGScore >= gScore[neighbor]) continue; cameFrom[neighbor.Value] = (RFIDPoint)current; gScore[neighbor] = tentativeGScore; fScore[neighbor] = gScore[neighbor] + Heuristic(neighbor.Location, end.Location); } } return new PathResult { Path = openList, }; } public PathResult CalculatePath(uint tagStrt, uint tagEnd) { var retval = new PathResult { Message = string.Empty, Path = new List(), }; var startPoint = FindRFIDPoint(tagStrt); var endPoint = FindRFIDPoint(tagEnd); if (startPoint == null || endPoint == null) { retval.Message = "유효한 RFID 값을 입력해주세요."; return retval; } retval = CalculatePath(startPoint, endPoint); if (retval.Success == false) retval.Message = "경로를 찾을 수 없습니다"; return retval; } /// /// 리프트방향과 대상위치와의 방향이 일치하는가? /// 목적지경로가 셋팅된 경우 현재 이동방향이 목적지방향과 일치하는가? /// 이동경로정보가 없거나 목적지가 없으면 null 이 반환됨 /// /// public bool? IsLiftDirectionMatch() { if (agv.MovementHistory.Any() && agv.MovementHistory.Count > 1) { RFIDPoint TargetPT = null; var prept = agv.MovementHistory.Skip(agv.MovementHistory.Count - 2).First(); var lstpt = agv.MovementHistory.Last(); //뒤로이동하는경우라면 이전위치에 리프트가 있다. if (lstpt.Direction == AgvDir.Backward) { TargetPT = prept; } else //앞으로 이동한다면 이동방향과 동일하다 { //이전위치는 제거 하고 처음발견된 것을 대상으로 한다 TargetPT = GetNeighbors(lstpt).Where(t => t.Value != prept.Value).FirstOrDefault(); } //목적지가 있다면 목적지의 방향과 일치하는지 확인해야한다 //남은경로중에 방향이 고정된 핀이 있다면 그것과 일치하는지 확인해야 한다 if (agv.MainPath.Any()) { //지정된경로 반대방향으로 이동하고 있다 if ((IsMotDirection_To_Target() ?? false) == false) { return false; } else { var nextRoutes = agv.MainPath.Skip(agv.MainPath.FindIndex(t => t.Value == lstpt.Value) + 1).ToList(); var DirectionMatch = true; foreach (var item in nextRoutes) { if (item.FixedDirection != null && item.FixedDirection != lstpt.Direction) { DirectionMatch = false; break; } } return DirectionMatch; } } else { //대상포인트와의 방향만 체크한다. //고정대상이없다면 방향이 맞는것으로 한다 return (TargetPT.FixedDirection ?? lstpt.Direction) == lstpt.Direction; } } else { //이동된경로정보가 없다면 리프트 방향을 체크할 수 없으므로 대상과 위치가 맞지 않는걸로 기본값을 설정한다 //이렇게 설정하면 대상으로 이동불가하고 뒤로 가도록 유도된다 return null; } } // Changed 속성 설정을 위한 헬퍼 메서드 private bool IsPredictionChanged(AGVActionPrediction newPrediction) { if (PredictResult == null) return true; // 이전 예측이 없으면 변경됨 // Idx와 Changed를 제외하고 비교 return !PredictResult.Equals(newPrediction); } private AGVActionPrediction CreatePrediction( string reason, AGVActionReasonCode reasonCode, AGVMoveState moveState, AgvDir direction, bool IDXUpdate = true, AgvSpeed? moveSpeed = null, AgvSts? moveDiv = null, RFIDPoint nextRFID = null ) { var newPrediction = new AGVActionPrediction { NextRFID = nextRFID, Reason = reason, ReasonCode = reasonCode, MoveState = moveState, Direction = direction, MoveSpeed = moveSpeed, MoveDiv = moveDiv, Idx = IDXUpdate ? (PredictResult?.Idx + 1 ?? 1) : (PredictResult?.Idx ?? 0), CreateTime = DateTime.Now, }; newPrediction.Changed = IsPredictionChanged(newPrediction); return newPrediction; } } }