diff --git a/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs b/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs index 3e89fda..a4d40aa 100644 --- a/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs +++ b/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs @@ -82,7 +82,7 @@ namespace AGVNavigationCore.Controls DrawNodeLabels(g); DrawLabels(g); // 추가: 텍스트 라벨 - + } finally { @@ -91,7 +91,7 @@ namespace AGVNavigationCore.Controls // UI 정보 그리기 (변환 없이) //if (_showGrid) - DrawUIInfo(g); + DrawUIInfo(g); // 동기화 화면 그리기 (변환 없이, 최상위) if (_canvasMode == CanvasMode.Sync) @@ -104,6 +104,7 @@ namespace AGVNavigationCore.Controls { g.DrawString(this.PredictMessage, this.Font, Brushes.White, 10, 100); } + DrawSystemMessage(g); DrawAlertMessage(g); } @@ -134,18 +135,18 @@ namespace AGVNavigationCore.Controls var start = node.Position; var end = targetNode.Position; var angle = Math.Atan2(end.Y - start.Y, end.X - start.X); - + // 박스(텍스트) 중심 위치: 약 40px 거리 var boxDist = 40; var boxX = start.X + boxDist * Math.Cos(angle); var boxY = start.Y + boxDist * Math.Sin(angle); string text = dir.ToString(); - + Color color = Color.Blue; - if (dir == MagnetPosition.L) color = Color.LimeGreen; + if (dir == MagnetPosition.L) color = Color.LimeGreen; else if (dir == MagnetPosition.R) color = Color.Red; - + // 화살표 및 텍스트 설정 using (var arrowBrush = new SolidBrush(color)) using (var arrowPen = new Pen(color, 2)) // 두께 약간 증가 @@ -156,10 +157,10 @@ namespace AGVNavigationCore.Controls // 끝점: 박스 너머 (55px) var arrowStartDist = 25; var arrowEndDist = 55; - + var pStart = new PointF((float)(start.X + arrowStartDist * Math.Cos(angle)), (float)(start.Y + arrowStartDist * Math.Sin(angle))); var pEnd = new PointF((float)(start.X + arrowEndDist * Math.Cos(angle)), (float)(start.Y + arrowEndDist * Math.Sin(angle))); - + // 화살표 선 그리기 g.DrawLine(arrowPen, pStart, pEnd); @@ -169,7 +170,7 @@ namespace AGVNavigationCore.Controls // 삼각형 머리 (채우기) var pBackL = new PointF((float)(pEnd.X + arrowSize * Math.Cos(angle + 2.5)), (float)(pEnd.Y + arrowSize * Math.Sin(angle + 2.5))); var pBackR = new PointF((float)(pEnd.X + arrowSize * Math.Cos(angle - 2.5)), (float)(pEnd.Y + arrowSize * Math.Sin(angle - 2.5))); - + // pHead1이 가장 먼 쪽이 되도록 조정 (pEnd가 삼각형의 뒷부분 중심이 되도록) // pEnd에서 시작해서 앞으로 나가는 삼각형 // pTip = pEnd + size * angle @@ -184,7 +185,7 @@ namespace AGVNavigationCore.Controls var textPoint = new PointF((float)(boxX - textSize.Width / 2), (float)(boxY - textSize.Height / 2)); //편집모드에서만 글자를 표시한다. - if(Mode == CanvasMode.Edit) + if (Mode == CanvasMode.Edit) { // 텍스트 배경 (반투명 - 선이 은은하게 보이도록 투명도 조절하거나, 가독성을 위해 불투명하게 처리) // 사용자가 "박스를 가로지르는" 느낌을 원했으므로 선이 보여야 함. 하지만 텍스트 가독성도 필요. @@ -197,8 +198,8 @@ namespace AGVNavigationCore.Controls } - - + + } } } @@ -207,9 +208,9 @@ namespace AGVNavigationCore.Controls } } - void DrawAlertMessage(Graphics g) + void DrawSystemMessage(Graphics g) { - if (!showalert || String.IsNullOrEmpty(this._alertmesage)) return; + if (!showalertsystem || String.IsNullOrEmpty(this._systemmesage)) return; // 상단 중앙에 반투명 빨간색 배경 바 표시 int barHeight = 40; @@ -231,7 +232,7 @@ namespace AGVNavigationCore.Controls { g.FillPath(brush, path); } - + using (var pen = new Pen(Color.FromArgb(255, 255, 255), 2)) { g.DrawPath(pen, path); @@ -239,29 +240,80 @@ namespace AGVNavigationCore.Controls } // 텍스트 깜박임 효과 (배경은 유지하고 텍스트만 깜박임) - if (_isAlertBlinkOn) + + using (var font = new Font("Malgun Gothic", 12, FontStyle.Bold)) + using (var brush = new SolidBrush(Color.White)) { - using (var font = new Font("Malgun Gothic", 12, FontStyle.Bold)) - using (var brush = new SolidBrush(Color.White)) + var textSize = g.MeasureString(_systemmesage, font); + g.DrawString(_systemmesage, font, brush, + barX + (barWidth - textSize.Width) / 2, + barY + (barHeight - textSize.Height) / 2); + } + + // 경고 아이콘 그리기 (왼쪽) + // 간단한 느낌표 아이콘 + int iconX = barX + 15; + int iconY = barY + barHeight / 2; + using (var brush = new SolidBrush(Color.White)) + { + g.FillEllipse(brush, iconX - 2, iconY - 8, 4, 16); // body + g.FillEllipse(brush, iconX - 2, iconY + 10, 4, 4); // dot + } + + } + void DrawAlertMessage(Graphics g) + { + if (!showinfo || String.IsNullOrEmpty(this._infomessage)) return; + + // 상단 중앙에 반투명 빨간색 배경 바 표시 + int barHeight = 40; + int barWidth = Math.Min(600, this.Width - 40); // 최대 600px, 좌우 여백 20px + int barX = (this.Width - barWidth) / 2; + int barY = 20+50; + + // 둥근 사각형 배경 + using (var path = new GraphicsPath()) + { + int radius = 10; + path.AddArc(barX, barY, radius * 2, radius * 2, 180, 90); + path.AddArc(barX + barWidth - radius * 2, barY, radius * 2, radius * 2, 270, 90); + path.AddArc(barX + barWidth - radius * 2, barY + barHeight - radius * 2, radius * 2, radius * 2, 0, 90); + path.AddArc(barX, barY + barHeight - radius * 2, radius * 2, radius * 2, 90, 90); + path.CloseFigure(); + + using (var brush = new SolidBrush(Color.FromArgb(255, Color.Lime))) { - var textSize = g.MeasureString(_alertmesage, font); - g.DrawString(_alertmesage, font, brush, - barX + (barWidth - textSize.Width) / 2, - barY + (barHeight - textSize.Height) / 2); + g.FillPath(brush, path); } - // 경고 아이콘 그리기 (왼쪽) - // 간단한 느낌표 아이콘 - int iconX = barX + 15; - int iconY = barY + barHeight / 2; - using (var brush = new SolidBrush(Color.White)) + using (var pen = new Pen(Color.FromArgb(255, 255, 255), 2)) { - g.FillEllipse(brush, iconX - 2, iconY - 8, 4, 16); // body - g.FillEllipse(brush, iconX - 2, iconY + 10, 4, 4); // dot + g.DrawPath(pen, path); } } - } + // 텍스트 깜박임 효과 (배경은 유지하고 텍스트만 깜박임) + + using (var font = new Font("Malgun Gothic", 12, FontStyle.Bold)) + using (var brush = new SolidBrush(Color.Black)) + { + var textSize = g.MeasureString(_infomessage, font); + g.DrawString(_infomessage, font, brush, + barX + (barWidth - textSize.Width) / 2, + barY + (barHeight - textSize.Height) / 2); + } + + // 경고 아이콘 그리기 (왼쪽) + // 간단한 느낌표 아이콘 + int iconX = barX + 15; + int iconY = barY + barHeight / 2; + using (var brush = new SolidBrush(Color.Black)) + { + g.FillEllipse(brush, iconX - 2, iconY - 8, 4, 16); // body + g.FillEllipse(brush, iconX - 2, iconY + 10, 4, 4); // dot + } + + } private void DrawSyncScreen(Graphics g) { // 반투명 검은색 배경 @@ -374,10 +426,10 @@ namespace AGVNavigationCore.Controls foreach (var targetNode in node.ConnectedMapNodes) { if (targetNode == null) continue; - + // 강조된 연결은 나중에 그리기 위해 건너뜀 if (IsConnectionHighlighted(node.Id, targetNode.Id)) continue; - + DrawConnection(g, node, targetNode); } } @@ -386,12 +438,12 @@ namespace AGVNavigationCore.Controls // 1.1 강조된 연결 그리기 (항상 위에 표시되도록) if (_highlightedConnection.HasValue) { - var n1 = _nodes.FirstOrDefault(n => n.Id == _highlightedConnection.Value.FromNodeId); - var n2 = _nodes.FirstOrDefault(n => n.Id == _highlightedConnection.Value.ToNodeId); - if (n1 != null && n2 != null) - { - DrawConnection(g, n1, n2); - } + var n1 = _nodes.FirstOrDefault(n => n.Id == _highlightedConnection.Value.FromNodeId); + var n2 = _nodes.FirstOrDefault(n => n.Id == _highlightedConnection.Value.ToNodeId); + if (n1 != null && n2 != null) + { + DrawConnection(g, n1, n2); + } } // 2. 마그넷 그리기 (별도 리스트 사용) @@ -723,14 +775,14 @@ namespace AGVNavigationCore.Controls var angle = Math.Atan2(nextNode.Position.Y - currentNode.Position.Y, nextNode.Position.X - currentNode.Position.X); - + // 상세 경로 정보가 있으면 해당 방향 사용, 없으면 Forward AgvDirection arrowDir = AgvDirection.Forward; if (path.DetailedPath != null && i < path.DetailedPath.Count) { arrowDir = path.DetailedPath[i].MotorDirection; } - + DrawDirectionArrow(g, midPoint, angle, arrowDir); } } @@ -769,7 +821,7 @@ namespace AGVNavigationCore.Controls // 현재는 사용자 요청에 따라 Gateway 지정이 안된 경우(일반 경로)에는 교차로 강조를 끄는 것이 맞아 보임. // 하지만 일반 주행시에도 교차로 정보가 필요할 수 있으니 일단 둡니다. // 단, Gateway 로직을 타는 경우(HighlightNodeId가 Set됨)에는 위에서 return 되므로 OK. - + /* const int JUNCTION_CONNECTIONS = 3; @@ -798,8 +850,8 @@ namespace AGVNavigationCore.Controls Color fillColor = isGateway ? Color.FromArgb(100, 255, 140, 0) : Color.FromArgb(80, 70, 130, 200); Color penColor = isGateway ? Color.OrangeRed : Color.FromArgb(150, 100, 150, 220); - using (var highlightBrush = new SolidBrush(fillColor)) - using (var highlightPen = new Pen(penColor, 3)) + using (var highlightBrush = new SolidBrush(fillColor)) + using (var highlightPen = new Pen(penColor, 3)) { g.FillEllipse( highlightBrush, @@ -872,7 +924,7 @@ namespace AGVNavigationCore.Controls { case NodeType.Normal: var item = _selectedNode as MapNode; - if ( item.StationType == StationType.Charger) + if (item.StationType == StationType.Charger) DrawTriangleGhost(g, ghostBrush); else DrawPentagonGhost(g, ghostBrush); @@ -1507,7 +1559,7 @@ namespace AGVNavigationCore.Controls Color fgColor = Color.Black; Color bgColor = Color.White; switch (node.StationType) - { + { case StationType.Charger: fgColor = Color.White; bgColor = Color.Tomato; @@ -1530,7 +1582,7 @@ namespace AGVNavigationCore.Controls break; } - + @@ -1554,7 +1606,7 @@ namespace AGVNavigationCore.Controls } - + using (var descBrush = new SolidBrush(node.NodeTextForeColor)) { g.DrawString(BottomLabelText, btmFont, descBrush, roundRect, new StringFormat @@ -1808,9 +1860,9 @@ namespace AGVNavigationCore.Controls switch (node.StationType) { case StationType.Normal: - if(node.CanTurnLeft || node.CanTurnRight) + if (node.CanTurnLeft || node.CanTurnRight) bgColor = Color.Violet; - else + else bgColor = Color.DeepSkyBlue; break; case StationType.Charger: bgColor = Color.Tomato; break; diff --git a/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs b/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs index 4ef8de4..bab1891 100644 --- a/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs +++ b/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs @@ -138,47 +138,22 @@ namespace AGVNavigationCore.Controls private float _syncProgress = 0.0f; private string _syncDetail = ""; - string _alertmesage = ""; - bool showalert = false; + string _systemmesage = ""; + string _infomessage = ""; + bool showalertsystem = false; + bool showinfo = false; - // 깜박임 효과를 위한 타이머 및 상태 - private Timer _alertBlinkTimer; - private bool _isAlertBlinkOn = true; - - public void SetAlertMessage(string m) + public void SetInfoMessage(string m) { - _alertmesage = m; - showalert = !string.IsNullOrEmpty(m); - - //if (showalert) - //{ - // if (_alertBlinkTimer == null) - // { - // _alertBlinkTimer = new Timer(); - // _alertBlinkTimer.Interval = 500; // 0.5초 간격 - // _alertBlinkTimer.Tick += _alertBlinkTimer_Tick; - // } - // _alertBlinkTimer.Start(); - // _isAlertBlinkOn = true; - //} - //else - //{ - // if (_alertBlinkTimer != null) - // { - // _alertBlinkTimer.Stop(); - // } - // _isAlertBlinkOn = false; - //} - //Invalidate(); // 즉시 갱신 + _infomessage = m; + showinfo = !string.IsNullOrEmpty(m); } - - private void _alertBlinkTimer_Tick(object sender, EventArgs e) + public void SetSystemMessage(string m) { - //_isAlertBlinkOn = !_isAlertBlinkOn; - //Invalidate(); + _systemmesage = m; + showalertsystem = !string.IsNullOrEmpty(m); } - // 브러쉬 및 펜 private Brush _normalNodeBrush; private Brush _rotationNodeBrush; @@ -963,13 +938,6 @@ namespace AGVNavigationCore.Controls // 이미지 정리 _companyLogo?.Dispose(); - // 타이머 정리 - if (_alertBlinkTimer != null) - { - _alertBlinkTimer.Stop(); - _alertBlinkTimer.Dispose(); - _alertBlinkTimer = null; - } } base.Dispose(disposing); diff --git a/AGVLogic/EnigProtocol/enigprotocol/Commands.cs b/AGVLogic/EnigProtocol/enigprotocol/Commands.cs index b846ce9..a4134d4 100644 --- a/AGVLogic/EnigProtocol/enigprotocol/Commands.cs +++ b/AGVLogic/EnigProtocol/enigprotocol/Commands.cs @@ -75,6 +75,8 @@ namespace ENIGProtocol LIDAR_STOP, NOT_BUFFERPOINT, NOT_EQUIPMENTPOINT, + PATH_COMPLETE_INTEGRITY_FAIL, + UPDATEMOTION_TIMEOUT } public static class AGVUtility @@ -113,7 +115,9 @@ namespace ENIGProtocol case AGVErrorCode.CHARGE_RETRY_OVER: return $"충전명령 재전송 횟수 초과"; case AGVErrorCode.NOT_BUFFERPOINT: return "현재 위치가 버퍼가 아닙니다"; case AGVErrorCode.NOT_EQUIPMENTPOINT: return "현재 위치가 장비 노드가 아닙니다"; + case AGVErrorCode.PATH_COMPLETE_INTEGRITY_FAIL: return "목적지 도착 완료 했으나 노드 혹은 방향이 일치지하지 않습니다"; default: return ecode.ToString(); + } } } diff --git a/HMI/Project/StateMachine/Step/_SM_RUN.cs b/HMI/Project/StateMachine/Step/_SM_RUN.cs index 9e5e422..b0aede7 100644 --- a/HMI/Project/StateMachine/Step/_SM_RUN.cs +++ b/HMI/Project/StateMachine/Step/_SM_RUN.cs @@ -135,7 +135,7 @@ namespace Project //마지막위치가 아닌 다른 위치에 있으니 버퍼 작업을 할 수없다 PUB.XBE.StepMC = Device.eDocStep.NotSet; PUB.log.AddE($"목적지가 버퍼이나 노드가 불일치 한다 오류사항"); - PUB._mapCanvas.SetAlertMessage("목적지가 버퍼이나 노드 불일치 오류"); + PUB._mapCanvas.SetInfoMessage("목적지가 버퍼이나 노드 불일치 오류"); PUB.sm.SetNewRunStep(ERunStep.ERROR); } break; diff --git a/HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_IN.cs b/HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_IN.cs index 4cc824b..3851c8e 100644 --- a/HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_IN.cs +++ b/HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_IN.cs @@ -138,7 +138,7 @@ namespace Project PUB.AGV.AGVMoveLeft180Turn(); PUB.log.Add($"[{funcname}-{PUB.sm.RunStepSeq}] AGV Left Turn"); VAR.TIME.Update(eVarTime.LastTurnCommandTime); - PUB._mapCanvas.SetAlertMessage($"턴 실행"); + PUB._mapCanvas.SetInfoMessage($"턴 실행"); } PUB.sm.UpdateRunStepSeq(); return false; @@ -166,7 +166,7 @@ namespace Project SetRunStepError(ENIGProtocol.AGVErrorCode.TURN_FAIL, $"[{funcname}] {overtime}초이내 턴 감지 안됨"); return false; } - else PUB._mapCanvas.SetAlertMessage($"턴 진행 중({PUB.AGV.TurnInformation.Runtime.TotalSeconds:N0}/{overtime})"); + else PUB._mapCanvas.SetInfoMessage($"턴 진행 중({PUB.AGV.TurnInformation.Runtime.TotalSeconds:N0}/{overtime})"); return false; } PUB._virtualAGV.Turn = AGVNavigationCore.Models.AGVTurn.L90; //턴완료 @@ -215,7 +215,7 @@ namespace Project VAR.I32[eVarInt32.RetryLift] = 0; } } - else PUB._mapCanvas.SetAlertMessage($"리프트 하강 확인 중({seqtime.TotalSeconds:N0}/20)"); + else PUB._mapCanvas.SetInfoMessage($"리프트 하강 확인 중({seqtime.TotalSeconds:N0}/20)"); return false; } VAR.I32[eVarInt32.RetryManget] = 0; @@ -294,7 +294,7 @@ namespace Project } PUB.log.Add($"[{funcname}-{PUB.sm.RunStepSeq}] 도킹을 위해 후진 이동 시작 (Dir:Backward, Spd:Low)"); - PUB._mapCanvas.SetAlertMessage($"도킹을 위해 후진 이동 시작"); + PUB._mapCanvas.SetInfoMessage($"도킹을 위해 후진 이동 시작"); VAR.I32[eVarInt32.RetryMove] = 0; PUB.sm.UpdateRunStepSeq(); return false; @@ -325,7 +325,7 @@ namespace Project VAR.I32[eVarInt32.RetryMove] = 0; } } - else PUB._mapCanvas.SetAlertMessage($"AGV이동 확인 중"); + else PUB._mapCanvas.SetInfoMessage($"AGV이동 확인 중"); return false; } @@ -342,7 +342,7 @@ namespace Project } PUB.AGV.AGVMoveStop(funcname, arDev.Narumi.eStopOpt.MarkStop); PUB.log.Add($"[{funcname}-{PUB.sm.RunStepSeq}] MarkStop 명령 전송 (도킹 위치 정렬)"); - PUB._mapCanvas.SetAlertMessage($"도킹을 위해 후진 이동 시작"); + PUB._mapCanvas.SetInfoMessage($"도킹을 위해 후진 이동 시작"); PUB.sm.UpdateRunStepSeq(); return false; } diff --git a/HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_OUT.cs b/HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_OUT.cs index b95305e..98375a9 100644 --- a/HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_OUT.cs +++ b/HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_OUT.cs @@ -288,7 +288,7 @@ namespace Project PUB.AGV.AGVMoveRight180Turn(); PUB.log.Add($"[{funcname}-{PUB.sm.RunStepSeq}] AGV Right Turn"); VAR.TIME.Update(eVarTime.LastTurnCommandTime); - PUB._mapCanvas.SetAlertMessage($"턴 실행"); + PUB._mapCanvas.SetInfoMessage($"턴 실행"); } PUB.sm.UpdateRunStepSeq(); return false; @@ -316,7 +316,7 @@ namespace Project SetRunStepError(ENIGProtocol.AGVErrorCode.TURN_FAIL, $"[{funcname}] {overtime}초이내 턴 감지 안됨"); return false; } - else PUB._mapCanvas.SetAlertMessage($"턴 진행 중({PUB.AGV.TurnInformation.Runtime.TotalSeconds:N0}/{overtime})"); + else PUB._mapCanvas.SetInfoMessage($"턴 진행 중({PUB.AGV.TurnInformation.Runtime.TotalSeconds:N0}/{overtime})"); return false; } PUB._virtualAGV.Turn = AGVNavigationCore.Models.AGVTurn.None; //턴완료 diff --git a/HMI/Project/StateMachine/Step/_SM_RUN_EXIT.cs b/HMI/Project/StateMachine/Step/_SM_RUN_EXIT.cs index 26217d4..5adaffa 100644 --- a/HMI/Project/StateMachine/Step/_SM_RUN_EXIT.cs +++ b/HMI/Project/StateMachine/Step/_SM_RUN_EXIT.cs @@ -92,7 +92,7 @@ namespace Project VAR.I32[eVarInt32.RetryLift] = 0; } } - else PUB._mapCanvas.SetAlertMessage($"리프트 하강 확인 중({seqtime.TotalSeconds:N0}/20)"); + else PUB._mapCanvas.SetInfoMessage($"리프트 하강 확인 중({seqtime.TotalSeconds:N0}/20)"); return false; } VAR.I32[eVarInt32.RetryMoveset] = 0; @@ -138,7 +138,7 @@ namespace Project VAR.I32[eVarInt32.RetryMoveset] = 0; } } - else PUB._mapCanvas.SetAlertMessage($"이동설정 확인 중({seqtime.TotalSeconds:N0}/20)"); + else PUB._mapCanvas.SetInfoMessage($"이동설정 확인 중({seqtime.TotalSeconds:N0}/20)"); return false; } VAR.I32[eVarInt32.RetryMove] = 0; @@ -173,7 +173,7 @@ namespace Project VAR.I32[eVarInt32.RetryMove] = 0; } } - else PUB._mapCanvas.SetAlertMessage($"AGV RUN 확인 중({seqtime.TotalSeconds:N0}/20)"); + else PUB._mapCanvas.SetInfoMessage($"AGV RUN 확인 중({seqtime.TotalSeconds:N0}/20)"); return false; } @@ -209,7 +209,7 @@ namespace Project VAR.I32[eVarInt32.RetryMarkStop] = 0; } } - else PUB._mapCanvas.SetAlertMessage($"리프트 하강 확인 중({seqtime.TotalSeconds:N0}/20)"); + else PUB._mapCanvas.SetInfoMessage($"리프트 하강 확인 중({seqtime.TotalSeconds:N0}/20)"); return false; } PUB.sm.UpdateRunStepSeq(); diff --git a/HMI/Project/StateMachine/Step/_SM_RUN_GOTO.cs b/HMI/Project/StateMachine/Step/_SM_RUN_GOTO.cs index ff59644..e105ee6 100644 --- a/HMI/Project/StateMachine/Step/_SM_RUN_GOTO.cs +++ b/HMI/Project/StateMachine/Step/_SM_RUN_GOTO.cs @@ -103,11 +103,18 @@ namespace Project PUB.AGV.AGVMoveStop(funcname); PUB.sm.UpdateRunStepSeq(); } + else + { + if (seqtime.TotalMinutes > 10) + { + //ACS에 이동 관련 타임아웃 오류를 보낸다 + SetRunStepError(ENIGProtocol.AGVErrorCode.UPDATEMOTION_TIMEOUT, $"[{funcname}-{PUB.sm.RunStepSeq}] (UpdateMotionPositionForMark)이 완료되지 않습니다"); + } + } return false; } else if (PUB.sm.RunStepSeq == idx++) { - //QC까지 모두 완료되었다.(완전히 정차할때까지 기다린다) PUB.log.Add($"[GOTO] Step {idx - 1}: Movement Finished. Waiting for full stop."); PUB.Speak(Lang.이동완료, true); PUB.AddEEDB($"이동완료({PUB._virtualAGV.TargetNode.ID2})"); diff --git a/HMI/Project/StateMachine/Step/_Util.cs b/HMI/Project/StateMachine/Step/_Util.cs index 18a4533..baa61ce 100644 --- a/HMI/Project/StateMachine/Step/_Util.cs +++ b/HMI/Project/StateMachine/Step/_Util.cs @@ -85,7 +85,7 @@ namespace Project PUB.AGV.AGVMoveStop(errmsg); PUB.log.AddE(errmsg); - PUB._mapCanvas.SetAlertMessage(errmsg); + PUB._mapCanvas.SetInfoMessage(errmsg); PUB.Result.RunStepErrorCode = ecode; PUB.sm.SetNewRunStep(ERunStep.ERROR); } @@ -195,6 +195,8 @@ namespace Project else VAR.I32[eVarInt32.PathValidationError] = 0; //현재위치 기준으로 재 계산하여. 더 최적화된 루트가 있다면 처리를 해준다. + /* + //TODO: 이제 경로가 하드코딩되어있으니 자동 갱신은 사용하지 않는다. if (PUB._virtualAGV.CurrentPath.DetailedPath.Count > 5) { var PathResult2 = CalcPath(PUB._virtualAGV.CurrentNode, PUB._virtualAGV.TargetNode); @@ -213,6 +215,7 @@ namespace Project } } + */ //predict 를 이용하여 다음 이동을 모두 확인한다. var nextAction = PUB._virtualAGV.Predict(); @@ -234,20 +237,21 @@ namespace Project return false; } - bool Moveforce = false; - if(nextAction.Motor == MotorCommand.Stop && nextAction.Reason == eAGVCommandReason.MarkStop) + //마크스탑을 해야하는데 움직이지 않는다면 움직이도록 해야한다 + if (nextAction.Motor == MotorCommand.Stop && nextAction.Reason == eAGVCommandReason.MarkStop) { - //마크스탑을 해야하는데 움직이지 않는다면 움직이도록 해야한다 - if(PUB.AGV.system1.agv_run==false) + if (PUB.AGV.system1.agv_run == false) { PUB.log.Add("마크스탑을 해야하는데 중지상태라서 강제 이동하도록 합니다"); - Moveforce = true; + PUB.AGV.AGVMoveRun(); + VAR.TIME[eVarTime.LastMarkStopCommandTime] = DateTime.Now.AddDays(-1); + return false; } } //모터에서 정지를 요청했다 - if (Moveforce == false && nextAction.Motor == AGVNavigationCore.Models.MotorCommand.Stop) + if (nextAction.Motor == AGVNavigationCore.Models.MotorCommand.Stop) { if (PUB.AGV.system1.agv_run) { @@ -256,34 +260,26 @@ namespace Project { if (PUB.AGV.data.Speed != 'S') { - // [쿨타임 적용] 정지 명령은 별도의 타이머(LastStopCommandTime)를 사용하여 - // 이동 직후라도 즉시 반응할 수 있도록 한다. (이미 정지신호를 보냈다면 2초 대기) - var tsCmd = VAR.TIME.RUN(eVarTime.LastStopCommandTime); + //2초간격으로 명령을 전송한다 + var tsCmd = VAR.TIME.RUN(eVarTime.LastMarkStopCommandTime); if (tsCmd.TotalSeconds >= 2.0) { - PUB.log.Add("다음행동예측에서 MARK STOP이 확인되었습니다 (자동정지시퀀스)"); + PUB.log.Add("다음행동예측에서 MARK-STOP이 확인되었습니다 (자동정지시퀀스)"); PUB.AGV.AGVMoveStop(nextAction.Message, arDev.Narumi.eStopOpt.MarkStop); - - // 정지 타이머 갱신 (연속 정지 방지) - VAR.TIME.Update(eVarTime.LastStopCommandTime); - // 일반 타이머도 갱신 (정지 직후 불필요한 이동 방지) + VAR.TIME.Update(eVarTime.LastMarkStopCommandTime); LastCommandTime = DateTime.Now; } } } else { - // [쿨타임 적용] 정지 명령은 별도의 타이머(LastStopCommandTime)를 사용하여 - // 이동 직후라도 즉시 반응할 수 있도록 한다. (이미 정지신호를 보냈다면 2초 대기) + //마크스탑이 아니므로 바로 멈춰야한다 var tsCmd = VAR.TIME.RUN(eVarTime.LastStopCommandTime); if (tsCmd.TotalSeconds >= 2.0) { - PUB.log.Add($"다음행동예측에서 장비 멈춤이 확인되었습니다({nextAction.Reason})"); + PUB.log.Add($"다음행동예측에서 멈춤이 확인되었습니다({nextAction.Reason})"); PUB.AGV.AGVMoveStop(nextAction.Message); - // 정지 타이머 갱신 (연속 정지 방지) VAR.TIME.Update(eVarTime.LastStopCommandTime); - - // 일반 타이머도 갱신 (정지 직후 불필요한 이동 방지) LastCommandTime = DateTime.Now; } } @@ -292,63 +288,49 @@ namespace Project return false; } + //여기시점에서는 장비는 멈춘 상태이다 if (nextAction.Reason == eAGVCommandReason.Complete) { // 목적지 도착 여부 확인 if (PUB.AGV.signal1.mark_sensor == false) { - if(PUB._virtualAGV.TargetNode.StationType != StationType.Normal) + if (PUB._virtualAGV.TargetNode.StationType != StationType.Normal) { - PUB.log.AddAT($"목표도착되었으나 마크센서가 감지되지 않아 완료처리 하지않습니다"); + PUB.log.AddAT($"목표({PUB._virtualAGV.TargetNode.RfidId})도착되었으나 마크센서가 감지되지 않아 완료처리 하지않습니다"); return false; } else { - //일반노드가목표인경우에는 완료처리한다. - PUB.log.AddI($"일반노드는 마크센서가 감지되지 않아도 완료처리 합니다"); + PUB.log.AddI($"일반노드({PUB._virtualAGV.TargetNode.RfidId})는 마크센서가 감지되지 않아도 완료처리 합니다"); return true; } } - // 경로가 존재한다면... - if (PUB._virtualAGV.CurrentPath != null && PUB._virtualAGV.CurrentPath.DetailedPath.Any()) - { - var lastInfo = PUB._virtualAGV.CurrentPath.DetailedPath.Last(); + //완료되었고 마크센서가 확인된 상태이다 + var lastInfo = PUB._virtualAGV.CurrentPath.DetailedPath.Last(); - // 위치와 방향이 모두 일치해야 완료된 것으로 본다. - if (PUB._virtualAGV.CurrentNode.Id == lastInfo.NodeId && - PUB._virtualAGV.CurrentDirection == lastInfo.MotorDirection) - { - var node = PUB._mapCanvas.Nodes.Where(t => t.Id == PUB._virtualAGV.CurrentNodeId).FirstOrDefault(); - var rfid = node?.ID2 ?? "(X)"; - PUB.log.AddI($"목표 도착 및 정지 확인됨(MarkStop 완료) Node:{rfid}, Dir:{PUB._virtualAGV.CurrentDirection}"); - return true; - } - else - { - // [DEBUG] 도착했으나 조건 불일치, 그러면.. predict 가 stop을 반환하면 안된다. - PUB.log.Add($"[DEBUG] Arrived but condition mismatch. CurNode:{PUB._virtualAGV.CurrentNode.Id}, Target:{lastInfo.NodeId}, CurDir:{PUB._virtualAGV.CurrentDirection}, TargetDir:{lastInfo.MotorDirection}"); - } + // 위치와 방향이 모두 일치해야 완료된 것으로 본다. + if (PUB._virtualAGV.CurrentNode.Id == lastInfo.NodeId && + PUB._virtualAGV.CurrentDirection == lastInfo.MotorDirection) + { + var node = PUB._mapCanvas.Nodes.Where(t => t.Id == PUB._virtualAGV.CurrentNodeId).FirstOrDefault(); + var rfid = node?.ID2 ?? "(X)"; + PUB.log.AddI($"목표 도착 및 정지 확인됨(MarkStop 완료) Node:{rfid}, Dir:{PUB._virtualAGV.CurrentDirection}"); + return true; } else { - // ... - if (PUB._virtualAGV.CurrentNode.Id == PUB._virtualAGV.TargetNode.Id) - { - // ... - PUB.log.AddI($"목표 도착 및 정지 확인됨(MarkStop 완료, No Path Info) Node:..."); - return true; - } + var errmsg = $"[도착오류] 목적지 정보 불일치. CurNode:{PUB._virtualAGV.CurrentNode.Id}, Target:{lastInfo.NodeId}, CurDir:{PUB._virtualAGV.CurrentDirection}, TargetDir:{lastInfo.MotorDirection}"; + SetRunStepError(ENIGProtocol.AGVErrorCode.PATH_COMPLETE_INTEGRITY_FAIL, errmsg); + return false; } } - - return false; } else { - // 이동 명령 변환 (AGVNavigationCore -> arDev.Narumi) + //PREDICT에서 이동을 명령했따 var bunki = arDev.Narumi.eBunki.Strate; if (nextAction.Magnet == AGVNavigationCore.Models.MagnetPosition.L) bunki = arDev.Narumi.eBunki.Left; else if (nextAction.Magnet == AGVNavigationCore.Models.MagnetPosition.R) bunki = arDev.Narumi.eBunki.Right; @@ -360,19 +342,24 @@ namespace Project if (nextAction.Speed == AGVNavigationCore.Models.SpeedLevel.M) spd = arDev.Narumi.eMoveSpd.Mid; else if (nextAction.Speed == AGVNavigationCore.Models.SpeedLevel.H) spd = arDev.Narumi.eMoveSpd.High; - // 방향 전환 시 정지 로직 추가 - // 이동 중인데 방향이 다르면 먼저 정지시킨다. + var char_bunki = bunki.ToString()[0]; + var char_speed = spd.ToString()[0]; + var char_dir = dir.ToString()[0]; + + + // 방향 전환 시 정지 로직 추가 ( 이동 중인데 방향이 다르면 먼저 정지시킨다.) if (PUB.AGV.system1.agv_run) { - if (PUB.AGV.data.Direction != dir.ToString()[0]) + if ((PUB.AGV.data.Direction == 'F' && dir == arDev.Narumi.eMoveDir.Backward) || + (PUB.AGV.data.Direction == 'B' && dir == arDev.Narumi.eMoveDir.Forward)) { - // 2초 쿨타임 (정지 명령도 빈번한 전송 방지) - var tsCmd = DateTime.Now - LastCommandTime; + var tsCmd = VAR.TIME.RUN(eVarTime.LastStopCommandTime); if (tsCmd.TotalSeconds >= 2.0) { - PUB.log.Add($"방향 전환을 위해 정지 명령을 전송합니다 Current:{PUB.AGV.data.Direction} Target:{dir}"); - PUB.AGV.AGVMoveStop("Direction Change"); - LastCommandTime = DateTime.Now; + var msg = "$[predict] Direction Change({ PUB.AGV.data.Direction} → { dir})"; + PUB.log.Add(msg); + PUB.AGV.AGVMoveStop(msg); + VAR.TIME[eVarTime.LastStopCommandTime] = DateTime.Now; } return false; } @@ -380,45 +367,60 @@ namespace Project // 명령 설정 // 현재 상태와 다를 때만 전송 (불필요한 통신 부하 방지) - if (PUB.AGV.data.Sts != bunki.ToString()[0] || - PUB.AGV.data.Direction != dir.ToString()[0] || - PUB.AGV.data.Speed != spd.ToString()[0] || + if (PUB.AGV.data.Sts != char_bunki || + PUB.AGV.data.Direction != char_dir || + PUB.AGV.data.Speed != char_speed || PUB.AGV.PBSSensor != arDev.eNarmiPBSSensor.on) { // 2초 쿨타임 적용 - var tsCmd = DateTime.Now - LastCommandTime; + var tsCmd = VAR.TIME.RUN(eVarTime.LastMoveSetCommandTime);// DateTime.Now - VAR.TIME; if (tsCmd.TotalSeconds >= 2.0) { - var ret = PUB.AGV.AGVMoveSet(new arDev.Narumi.BunkiData + var mp = new arDev.Narumi.BunkiData { Bunki = bunki, Direction = dir, PBSSensor = 2, Speed = spd, - }); + }; + var ret = PUB.AGV.AGVMoveSet(mp); + var msg = $"Predict Move Setting = bunki:{mp.Bunki},dir:{mp.Direction},pbs:1,spd:{mp.Speed}"; if (ret == arDev.eNarumiCommandResult.Success) - PUB.log.Add($"Predict Run Setting = bunki:{bunki},dir:{dir},pbs:1,spd:{spd}"); + PUB.log.Add(msg); else - PUB.log.AddE($"Predict Run Setting = bunki:{bunki},dir:{dir},pbs:1,spd:{spd}"); + PUB.log.AddE(msg); - LastCommandTime = DateTime.Now; + VAR.TIME[eVarTime.LastMoveSetCommandTime] = DateTime.Now; } + return false; } // AGV가 정지 상태라면 구동 시작 (라이다가켜져있을때에만 사용한다) - if (PUB.AGV.system1.agv_run == false && PUB.AGV.PBSSensor == arDev.eNarmiPBSSensor.on) + if (PUB.AGV.system1.agv_run == false ) { - // 2초 쿨타임 적용 (AGVMoveSet과 동일한 타이머 사용) - var tsCmd = DateTime.Now - LastCommandTime; - if (tsCmd.TotalSeconds >= 2.0) + if(PUB.AGV.PBSSensor == arDev.eNarmiPBSSensor.on) { - var runOpt = (dir == arDev.Narumi.eMoveDir.Forward) ? arDev.Narumi.eRunOpt.Forward : arDev.Narumi.eRunOpt.Backward; - PUB.AGV.AGVMoveRun(runOpt); - LastCommandTime = DateTime.Now; + // 2초 쿨타임 적용 (AGVMoveSet과 동일한 타이머 사용) + var tsCmd = VAR.TIME.RUN(eVarTime.LastRunCommandTime);// DateTime.Now - LastCommandTime; + if (tsCmd.TotalSeconds >= 2.0) + { + var msg = $"[PREDICT] Run AGV Dir:{dir}"; + PUB.log.Add(msg); + var runOpt = (dir == arDev.Narumi.eMoveDir.Forward) ? arDev.Narumi.eRunOpt.Forward : arDev.Narumi.eRunOpt.Backward; + PUB.AGV.AGVMoveRun(runOpt); + VAR.TIME[eVarTime.LastRunCommandTime] = DateTime.Now; + } } + else + { + PUB._mapCanvas.SetInfoMessage("AGV가동이 필요하나 PBS가 OFF되어있습니다"); + } + return false; } + + return false; } diff --git a/HMI/Project/ViewForm/fAuto.cs b/HMI/Project/ViewForm/fAuto.cs index cd9e7c0..fc9f652 100644 --- a/HMI/Project/ViewForm/fAuto.cs +++ b/HMI/Project/ViewForm/fAuto.cs @@ -281,9 +281,7 @@ namespace Project.ViewForm errmsg = "자동모드가 아닙니다"; } - PUB._mapCanvas.SetAlertMessage(errmsg); - - + PUB._mapCanvas.SetSystemMessage(errmsg); tmrun = false; } diff --git a/HMI/Project/fMain.cs b/HMI/Project/fMain.cs index 71dbaa1..6338796 100644 --- a/HMI/Project/fMain.cs +++ b/HMI/Project/fMain.cs @@ -519,7 +519,7 @@ namespace Project PUB.Speak(Lang.자동전환, addlog: false); PUB.log.Add($"자동전환 이전스텝:{PUB.sm.RunStep},IDX:{PUB.sm.RunStepSeq}]"); - PUB._mapCanvas.SetAlertMessage("START"); + PUB._mapCanvas.SetInfoMessage($"START ({DateTime.Now.ToShortTimeString()})"); } else { diff --git a/HMI/SubProject/CommData/Enum.cs b/HMI/SubProject/CommData/Enum.cs index 8e98a9c..fc78469 100644 --- a/HMI/SubProject/CommData/Enum.cs +++ b/HMI/SubProject/CommData/Enum.cs @@ -230,5 +230,10 @@ namespace COMM /// 마지막을 실행 명령을 전송한 시간 /// LastRunCommandTime, + + /// + /// 이동명령 설정 시간 + /// + LastMoveSetCommandTime, } } \ No newline at end of file