From 34b038c4beffb1c781eca3092b69309d58c2157b Mon Sep 17 00:00:00 2001 From: LGram16 Date: Sat, 13 Dec 2025 02:40:55 +0900 Subject: [PATCH] Implement ACS Command Handlers (PickOn, PickOff, Charge), Manual Mode Safety, and Map UI Commands --- .../AGVNavigationCore/Models/MapNode.cs | 12 +- .../AGVNavigationCore/Models/VirtualAGV.cs | 38 +- .../PathFinding/Planning/AGVPathfinder.cs | 7 +- .../PathFinding/Planning/NodeMotorInfo.cs | 5 + Cs_HMI/Data/NewMap_2.json | 1174 +++++++++++++++++ Cs_HMI/Project/CSetting.cs | 7 +- Cs_HMI/Project/PUB.cs | 7 +- Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs | 87 +- .../StateMachine/Step/_SM_RUN_BUFFER_IN.cs | 69 +- .../StateMachine/Step/_SM_RUN_CLEANER_IN.cs | 63 +- .../StateMachine/Step/_SM_RUN_GOCHARGE.cs | 101 +- .../StateMachine/Step/_SM_RUN_LOADER_IN.cs | 70 +- .../StateMachine/Step/_SM_RUN_UNLOADER_IN.cs | 63 +- Cs_HMI/Project/StateMachine/Step/_Util.cs | 67 +- Cs_HMI/Project/StateMachine/_AGV.cs | 15 + Cs_HMI/Project/StateMachine/_BMS.cs | 4 + Cs_HMI/Project/StateMachine/_Xbee.cs | 41 + Cs_HMI/Project/ViewForm/fAuto.cs | 91 +- Cs_HMI/Project/ViewForm/fManual.cs | 24 + Cs_HMI/Project/fSetup.Designer.cs | 17 - Cs_HMI/Project/fSetup.cs | 17 - Cs_HMI/docs/Charging_Sequence_Analysis.md | 78 ++ Cs_HMI/docs/GOHOME_Analysis.md | 63 - Cs_HMI/docs/Home_Move_Analysis.md | 66 + Cs_HMI/docs/Predict_Function_Analysis.md | 101 ++ 25 files changed, 1992 insertions(+), 295 deletions(-) create mode 100644 Cs_HMI/Data/NewMap_2.json create mode 100644 Cs_HMI/docs/Charging_Sequence_Analysis.md delete mode 100644 Cs_HMI/docs/GOHOME_Analysis.md create mode 100644 Cs_HMI/docs/Home_Move_Analysis.md create mode 100644 Cs_HMI/docs/Predict_Function_Analysis.md diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs index 818559e..f7d46d2 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs @@ -88,17 +88,18 @@ namespace AGVNavigationCore.Models private bool _disablecross = false; + /// + /// 해당 노드 통과 시 제한 속도 (기본값: M - Normal) + /// Predict 단계에서 이 값을 참조하여 속도 명령을 생성합니다. + /// + public SpeedLevel SpeedLimit { get; set; } = SpeedLevel.M; + /// /// 장비 ID (도킹/충전 스테이션인 경우) /// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1" /// public string NodeAlias { get; set; } = string.Empty; - /// - /// 장비 타입 (도킹/충전 스테이션인 경우) - /// - // public StationType? StationType { get; set; } = null; - /// /// 노드 생성 일자 /// @@ -109,7 +110,6 @@ namespace AGVNavigationCore.Models /// public DateTime ModifiedDate { get; set; } = DateTime.Now; - /// /// 노드 활성화 여부 /// diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs index f10fb46..a0c7b83 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs @@ -188,6 +188,11 @@ namespace AGVNavigationCore.Models /// public int DetectedRfidCount => _detectedRfids.Count; + /// + /// 배터리 부족 경고 임계값 (%) + /// + public float LowBatteryThreshold { get; set; } = 20.0f; + #endregion #region Constructor @@ -262,9 +267,9 @@ namespace AGVNavigationCore.Models BatteryLevel = Math.Max(0, Math.Min(100, percentage)); // 배터리 부족 경고 - if (BatteryLevel < 20.0f && _currentState != AGVState.Charging) + if (BatteryLevel < LowBatteryThreshold && _currentState != AGVState.Charging) { - OnError($"배터리 부족: {BatteryLevel:F1}%"); + OnError($"배터리 부족: {BatteryLevel:F1}% (기준: {LowBatteryThreshold}%)"); } } @@ -586,6 +591,15 @@ namespace AGVNavigationCore.Models var item = CurrentPath.DetailedPath.FirstOrDefault(t => t.NodeId == node.NodeId && t.IsPass == false); if (item != null) { + // [PathJump Check] 점프한 노드 개수 확인 + // 현재 노드(item)보다 이전인데 아직 IsPass가 안 된 노드의 개수 + int skippedCount = CurrentPath.DetailedPath.Count(t => t.seq < item.seq && t.IsPass == false); + if (skippedCount > 2) + { + OnError($"PathJump: {skippedCount}개의 노드를 건너뛰었습니다. (허용: 2개, 현재노드: {node.NodeId})"); + return; + } + //item.IsPass = true; //이전노드는 모두 지나친걸로 한다 CurrentPath.DetailedPath.Where(t => t.seq < item.seq).ToList().ForEach(t => t.IsPass = true); @@ -633,14 +647,11 @@ namespace AGVNavigationCore.Models // DetailedPath가 없으면 기본 명령 반환 if (_currentPath == null || _currentPath.DetailedPath == null || _currentPath.DetailedPath.Count == 0) { - var defaultMotor = _currentDirection == AgvDirection.Forward - ? MotorCommand.Forward - : MotorCommand.Backward; - + // [Refactor] Predict와 일관성 유지: 경로가 없으면 정지 return new AGVCommand( - defaultMotor, + MotorCommand.Stop, MagnetPosition.S, - SpeedLevel.M, + SpeedLevel.L, eAGVCommandReason.NoPath, $"{actionDescription} (DetailedPath 없음)" ); @@ -697,10 +708,13 @@ namespace AGVNavigationCore.Models break; } - // 속도 결정 (회전 노드면 저속, 일반 이동은 중속) - SpeedLevel speed = nodeInfo.CanRotate || nodeInfo.IsDirectionChangePoint - ? SpeedLevel.L - : SpeedLevel.M; + // [Speed Control] NodeMotorInfo에 설정된 속도 사용 + // 단, 회전 구간 등에서 안전을 위해 강제 감속이 필요한 경우 로직 추가 가능 + // 현재는 사용자 설정 우선 + SpeedLevel speed = nodeInfo.Speed; + + // Optional: 회전 시 강제 감속 로직 (사용자 요청에 따라 주석 처리 또는 제거 가능) + // if (nodeInfo.CanRotate || nodeInfo.IsDirectionChangePoint) speed = SpeedLevel.L; return new AGVCommand( motorCmd, diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs index a08392e..8aef7e8 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs @@ -445,7 +445,12 @@ namespace AGVNavigationCore.PathFinding.Planning MagnetDirection.Straight ); - detailedPath1.Add(nodeInfo); + // [Speed Control] MapNode의 속도 설정 적용 + var mapNode = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + if (mapNode != null) + { + nodeInfo.Speed = mapNode.SpeedLimit; + } } // path1에 상세 경로 정보 설정 diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs index cdcbd7e..760dba0 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs @@ -47,6 +47,11 @@ namespace AGVNavigationCore.PathFinding.Planning /// public AgvDirection MotorDirection { get; set; } + /// + /// 해당 노드에서의 제한 속도 + /// + public SpeedLevel Speed { get; set; } = SpeedLevel.M; + /// /// 마그넷 센서 방향 제어 (갈림길 처리용) /// diff --git a/Cs_HMI/Data/NewMap_2.json b/Cs_HMI/Data/NewMap_2.json new file mode 100644 index 0000000..72716a8 --- /dev/null +++ b/Cs_HMI/Data/NewMap_2.json @@ -0,0 +1,1174 @@ +{ + "Nodes": [ + { + "NodeId": "N001", + "Name": "UNLOADER", + "Position": "99, 251", + "Type": 2, + "ConnectedNodes": [ + "N002" + ], + "RfidId": "0001", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Red", + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N002", + "Name": "N002", + "Position": "249, 250", + "Type": 0, + "ConnectedNodes": [ + "N001", + "N003" + ], + "RfidId": "0002", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N003", + "Name": "N003", + "Position": "350, 301", + "Type": 0, + "ConnectedNodes": [ + "N002", + "N011", + "N022", + "N031" + ], + "RfidId": "0003", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N006", + "Name": "N006", + "Position": "508, 242", + "Type": 0, + "ConnectedNodes": [ + "N007", + "N022", + "N023" + ], + "RfidId": "0013", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N007", + "Name": "N007", + "Position": "601, 205", + "Type": 0, + "ConnectedNodes": [ + "N006", + "N019" + ], + "RfidId": "0014", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N008", + "Name": "N008", + "Position": "275, 441", + "Type": 0, + "ConnectedNodes": [ + "N009", + "N031" + ], + "RfidId": "0009", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N009", + "Name": "N009", + "Position": "184, 466", + "Type": 0, + "ConnectedNodes": [ + "N008", + "N010" + ], + "RfidId": "0010", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N010", + "Name": "TOPS", + "Position": "92, 465", + "Type": 3, + "ConnectedNodes": [ + "N009" + ], + "RfidId": "0011", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Red", + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N011", + "Name": "N011", + "Position": "450, 399", + "Type": 0, + "ConnectedNodes": [ + "N003", + "N012", + "N015", + "N031", + "N022" + ], + "RfidId": "0005", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N012", + "Name": "N012", + "Position": "549, 450", + "Type": 0, + "ConnectedNodes": [ + "N011", + "N013", + "N015" + ], + "RfidId": "0006", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N013", + "Name": "N013", + "Position": "616, 492", + "Type": 0, + "ConnectedNodes": [ + "N012", + "N014" + ], + "RfidId": "0007", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N014", + "Name": "LOADER", + "Position": "670, 526", + "Type": 1, + "ConnectedNodes": [ + "N013" + ], + "RfidId": "0008", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Red", + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N019", + "Name": "CHARGER #2", + "Position": "666, 198", + "Type": 5, + "ConnectedNodes": [ + "N007" + ], + "RfidId": "0015", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Magenta", + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N022", + "Name": "N022", + "Position": "450, 300", + "Type": 0, + "ConnectedNodes": [ + "N003", + "N006", + "N011", + "N023", + "N031" + ], + "RfidId": "0012", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N023", + "Name": "N023", + "Position": "463, 195", + "Type": 0, + "ConnectedNodes": [ + "N006", + "N022", + "N024" + ], + "RfidId": "0016", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N024", + "Name": "N024", + "Position": "500, 135", + "Type": 0, + "ConnectedNodes": [ + "N023", + "N025" + ], + "RfidId": "0017", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N025", + "Name": "N025", + "Position": "573, 97", + "Type": 0, + "ConnectedNodes": [ + "N024", + "N026" + ], + "RfidId": "0018", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N026", + "Name": "CHARGER #1", + "Position": "649, 87", + "Type": 5, + "ConnectedNodes": [ + "N025" + ], + "RfidId": "0019", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Magenta", + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "LBL001", + "Name": "Amkor Technology Korea", + "Position": "180, 105", + "Type": 6, + "ConnectedNodes": [], + "RfidId": "", + "LabelText": "Amkor Technology Korea", + "ForeColor": "White", + "BackColor": "DarkSlateGray", + "ImageBase64": "", + "DisplayColor": "Purple", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "IMG001", + "Name": "logo", + "Position": "633, 310", + "Type": 7, + "ConnectedNodes": [], + "RfidId": "", + "LabelText": "", + "ForeColor": "Black", + "BackColor": "Transparent", + "ImageBase64": "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", + "DisplayColor": "Brown", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N015", + "Name": "", + "Position": "449, 499", + "Type": 0, + "ConnectedNodes": [ + "N011", + "N012", + "N016" + ], + "RfidId": "0037", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N016", + "Name": "", + "Position": "422, 537", + "Type": 0, + "ConnectedNodes": [ + "N015", + "N017" + ], + "RfidId": "0036", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N017", + "Name": "", + "Position": "380, 557", + "Type": 0, + "ConnectedNodes": [ + "N016", + "N018" + ], + "RfidId": "0035", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N018", + "Name": "", + "Position": "329, 561", + "Type": 0, + "ConnectedNodes": [ + "N017", + "N030", + "N005" + ], + "RfidId": "0034", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N005", + "Name": "", + "Position": "233, 561", + "Type": 0, + "ConnectedNodes": [ + "N018", + "N020", + "N029" + ], + "RfidId": "0033", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N020", + "Name": "", + "Position": "155, 560", + "Type": 0, + "ConnectedNodes": [ + "N005", + "N021", + "N028" + ], + "RfidId": "0032", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N021", + "Name": "", + "Position": "68, 558", + "Type": 0, + "ConnectedNodes": [ + "N020", + "N027" + ], + "RfidId": "0031", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N027", + "Name": "BUF1", + "Position": "38, 637", + "Type": 4, + "ConnectedNodes": [ + "N021" + ], + "RfidId": "0041", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Green", + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N028", + "Name": "BUF2", + "Position": "125, 639", + "Type": 4, + "ConnectedNodes": [ + "N020" + ], + "RfidId": "0040", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Green", + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N029", + "Name": "BUF3", + "Position": "203, 635", + "Type": 4, + "ConnectedNodes": [ + "N005" + ], + "RfidId": "0039", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Green", + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N030", + "Name": "BUF4", + "Position": "296, 638", + "Type": 4, + "ConnectedNodes": [ + "N018" + ], + "RfidId": "0038", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Green", + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + }, + { + "NodeId": "N031", + "Name": "", + "Position": "350, 400", + "Type": 0, + "ConnectedNodes": [ + "N003", + "N008", + "N011", + "N022" + ], + "RfidId": "0030", + "LabelText": "", + "ForeColor": "White", + "BackColor": "Transparent", + "ImageBase64": "", + "DisplayColor": "Cyan", + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true, + "CreatedDate": "2025-12-12T16:29:33.968Z", + "ModifiedDate": "2025-12-12T16:29:33.968Z" + } + ], + "Settings": { + "BackgroundColorArgb": -14671840, + "ShowGrid": false + }, + "Marks": [ + { + "id": "2cb51787-c8cf-4ddb-97f0-b71f519d47dc", + "x": 684.7353629976581, + "y": 539.3747072599532, + "rotation": 119.877727797857 + }, + { + "id": "f704ebe0-1653-4559-b06f-1eaecafbefba", + "x": 40.428571428571516, + "y": 559.4597069597069, + "rotation": 90 + }, + { + "id": "d5b27365-79a2-4351-84c3-6767941ec0be", + "x": 126.14285714285721, + "y": 560.1739926739926, + "rotation": 90 + }, + { + "id": "0367cafb-9f85-4440-b6b4-c802a58e6181", + "x": 203.28571428571433, + "y": 558.7930402930402, + "rotation": 89.2872271068898 + }, + { + "id": "1f4ab2c9-07f8-4675-802d-9b4824b55198", + "x": 296.14285714285717, + "y": 560.8882783882782, + "rotation": 88.40516772072262 + }, + { + "id": "15fddfa4-ff74-48ff-b922-4aacdce1960b", + "x": 81.82817182817195, + "y": 256.042957042957, + "rotation": 74.50226651936697 + }, + { + "id": "962bb671-6932-477d-9209-2c2a076cfb22", + "x": 73.25674325674338, + "y": 466.0429570429569, + "rotation": 90 + }, + { + "id": "cd9f8434-f223-4532-9b3e-5b44c738abbb", + "x": 686.8281718281718, + "y": 196.04295704295703, + "rotation": 90 + }, + { + "id": "61c6d1dd-6a39-4931-a530-9b44d2010139", + "x": 669.6853146853147, + "y": 84.61438561438564, + "rotation": 90 + }, + { + "id": "4b699847-36d4-471c-b990-4ad37967c2dc", + "x": 204.24242424242428, + "y": 655.8533133533133, + "rotation": 0.3824344779617803 + }, + { + "id": "a9f68317-f1c2-47d8-b029-348b5428be9f", + "x": 296.9090909090909, + "y": 657.1866466866467, + "rotation": -1.3380194104322385 + }, + { + "id": "fe227205-2a65-4ba9-bb4a-4efb4ed0a7b0", + "x": 122.90909090909095, + "y": 657.1866466866467, + "rotation": 0.8431103833306963 + }, + { + "id": "5dd29191-798c-480c-b066-7947bfcc4fb7", + "x": 40.24242424242431, + "y": 657.1866466866467, + "rotation": 1.659829660758831 + } + ], + "Magnets": [ + { + "id": "92130fcc-1d99-4a1c-99d6-7d48072d9a3f", + "type": "STRAIGHT", + "p1": { + "x": 52, + "y": 466 + }, + "p2": { + "x": 183, + "y": 465 + } + }, + { + "id": "6d5f514a-c84d-42fd-951d-cc1836a02eb9", + "type": "CURVE", + "p1": { + "x": 315.7142857142857, + "y": 562 + }, + "p2": { + "x": 449.7142857142857, + "y": 399.2857142857143 + }, + "controlPoint": { + "x": 485.39960039960044, + "y": 568.185814185814 + } + }, + { + "id": "fe5f3cc4-f995-4fa2-a55d-1bd77792c062", + "type": "CURVE", + "p1": { + "x": 449, + "y": 498.6190476190476 + }, + "p2": { + "x": 549.2857142857142, + "y": 449.4761904761904 + }, + "controlPoint": { + "x": 469.6853146853147, + "y": 417.5191475191474 + } + }, + { + "id": "5a0edec2-7ac3-4c99-bbb4-8debde0c1d07", + "type": "STRAIGHT", + "p1": { + "x": 317.14285714285717, + "y": 562.7142857142857 + }, + "p2": { + "x": -8.270406275571691, + "y": 556.7518162503355 + } + }, + { + "id": "def7c4b9-86db-42eb-aae6-0c6c9bedcc30", + "type": "STRAIGHT", + "p1": { + "x": 38.24242424242431, + "y": 675.8533133533133 + }, + "p2": { + "x": 40.39960039960053, + "y": 561.0429570429569 + } + }, + { + "id": "624327ee-be0f-4373-b60a-786a93c1eabf", + "type": "STRAIGHT", + "p1": { + "x": 124.90909090909095, + "y": 676.51998001998 + }, + "p2": { + "x": 125.39960039960052, + "y": 559.6620046620044 + } + }, + { + "id": "f1e885ae-55f7-42e9-b3aa-648541e97da0", + "type": "STRAIGHT", + "p1": { + "x": 202.24242424242428, + "y": 675.1866466866467 + }, + "p2": { + "x": 203.25674325674333, + "y": 560.3286713286711 + } + }, + { + "id": "dc3e8061-2c99-4f24-ac9b-4020dd91fa8b", + "type": "STRAIGHT", + "p1": { + "x": 296.9090909090909, + "y": 675.1866466866467 + }, + "p2": { + "x": 295.39960039960044, + "y": 562.4715284715284 + } + }, + { + "id": "e8242b99-ab7a-453f-b074-516cf7f8aa6b", + "type": "STRAIGHT", + "p1": { + "x": 549.2857142857142, + "y": 450.1428571428571 + }, + "p2": { + "x": 719, + "y": 558 + } + }, + { + "id": "d9b18933-d211-4a46-9265-6d2543abc8f2", + "type": "CURVE", + "p1": { + "x": 349.85714285714283, + "y": 399.71428571428567 + }, + "p2": { + "x": 449.7142857142857, + "y": 399.2857142857143 + }, + "controlPoint": { + "x": 400.39960039960044, + "y": 349.6143856143856 + } + }, + { + "id": "4ec4e1cd-2b6b-4e22-a0e3-1e91f83c90a6", + "type": "CURVE", + "p1": { + "x": 449.7142857142857, + "y": 399.2857142857143 + }, + "p2": { + "x": 450.42857142857144, + "y": 299.85714285714283 + }, + "controlPoint": { + "x": 399.6853146853147, + "y": 349.6143856143856 + } + }, + { + "id": "c3de9569-3278-4b45-8c73-554e9a2241ad", + "type": "CURVE", + "p1": { + "x": 350.1428571428571, + "y": 300.5714285714286 + }, + "p2": { + "x": 450.42857142857144, + "y": 299.85714285714283 + }, + "controlPoint": { + "x": 399.6853146853147, + "y": 349.6143856143856 + } + }, + { + "id": "c49475f5-5bae-49d1-aec9-8972b35f0624", + "type": "CURVE", + "p1": { + "x": 349.85714285714283, + "y": 399.71428571428567 + }, + "p2": { + "x": 350.1428571428571, + "y": 300.5714285714286 + }, + "controlPoint": { + "x": 398.971028971029, + "y": 349.6143856143856 + } + }, + { + "id": "e47ba9a3-4678-4df6-bfbd-0f43cfd9dc40", + "type": "CURVE", + "p1": { + "x": 450.42857142857144, + "y": 299.85714285714283 + }, + "p2": { + "x": 499.85714285714283, + "y": 134.71428571428575 + }, + "controlPoint": { + "x": 443.2567432567433, + "y": 201.04295704295703 + } + }, + { + "id": "d183c4a4-7de8-421a-bed2-4caa2e4b6e27", + "type": "CURVE", + "p1": { + "x": 499.85714285714283, + "y": 134.71428571428575 + }, + "p2": { + "x": 703.2567432567432, + "y": 87.47152847152849 + }, + "controlPoint": { + "x": 560.4285714285713, + "y": 78.03113553113558 + } + }, + { + "id": "3e2ae683-f783-4001-a2a4-d56664ca89b8", + "type": "CURVE", + "p1": { + "x": 183, + "y": 465 + }, + "p2": { + "x": 349.85714285714283, + "y": 399.71428571428567 + }, + "controlPoint": { + "x": 265.42857142857144, + "y": 459.4597069597069 + } + }, + { + "id": "b2b3d68a-2fe0-4bcd-b6de-0463a2b604e0", + "type": "CURVE", + "p1": { + "x": 350.1428571428571, + "y": 300.5714285714286 + }, + "p2": { + "x": 41.113886113886245, + "y": 266.75724275724275 + }, + "controlPoint": { + "x": 223.25674325674333, + "y": 201.75724275724275 + } + }, + { + "id": "9bf4a200-eeb2-4a42-851c-1e596ac16c06", + "type": "CURVE", + "p1": { + "x": 450.42857142857144, + "y": 299.85714285714283 + }, + "p2": { + "x": 713.2567432567432, + "y": 198.18581418581417 + }, + "controlPoint": { + "x": 539.7142857142857, + "y": 185.1739926739927 + } + }, + { + "id": "5d340580-bb09-42d9-81ed-43e69f8921c3", + "type": "CURVE", + "p1": { + "x": 462.7142857142857, + "y": 194.85714285714286 + }, + "p2": { + "x": 507.7142857142858, + "y": 242.28571428571428 + }, + "controlPoint": { + "x": 448.2567432567433, + "y": 259.6143856143856 + } + }, + { + "id": "f659dce6-5b29-44d7-9e51-e148dab3b02e", + "type": "STRAIGHT", + "p1": { + "x": 350, + "y": 301 + }, + "p2": { + "x": 450, + "y": 399 + } + }, + { + "id": "10e46540-7a48-4f44-8b88-029c5a5115f1", + "type": "STRAIGHT", + "p1": { + "x": 350, + "y": 400 + }, + "p2": { + "x": 450, + "y": 300 + } + }, + { + "id": "0417e191-5169-46be-ad45-607abdeae8a6", + "type": "STRAIGHT", + "p1": { + "x": 450, + "y": 399 + }, + "p2": { + "x": 549, + "y": 450 + } + } + ], + "CreatedDate": "2025-12-12T16:29:33.968Z", + "Version": "1.1" +} \ No newline at end of file diff --git a/Cs_HMI/Project/CSetting.cs b/Cs_HMI/Project/CSetting.cs index 9bd47c3..6c3680e 100644 --- a/Cs_HMI/Project/CSetting.cs +++ b/Cs_HMI/Project/CSetting.cs @@ -222,8 +222,6 @@ namespace Project #region "Charge" [Browsable(false)] - public int chargerpos { get; set; } - [Browsable(false)] public int ChargetWaitSec { get; set; } [Browsable(false)] public int ChargeEmergencyLevel { get; set; } @@ -240,6 +238,9 @@ namespace Project [Browsable(false)] public int ChargeSearchTime { get; set; } + [Category("Charge"), DisplayName("Low Battery Limit"), Description("배터리 부족 경고 알림 기준 (%)")] + public int BatteryLimit_Low { get; set; } + #endregion #region "AGV" @@ -427,10 +428,10 @@ namespace Project if (ChargeEmergencyLevel == 0) ChargeEmergencyLevel = 30; if (interval_bms == 0) interval_bms = 10; - //충전은 10분간격으로 재시도 한다 if (ChargeRetryTerm == 0) ChargeRetryTerm = 600; if (alarmSoundTerm == 0) alarmSoundTerm = 15; //기본 15초 if (ChargeSearchTime == 0) ChargeSearchTime = 25; + if (BatteryLimit_Low == 0) BatteryLimit_Low = 20; //최대 충전진행 시간(기본 1시간) if (ChargeMaxTime == 0) ChargeMaxTime = 3600; // if (interval_iostate == 0 || interval_iostate == 255) interval_iostate = 100; diff --git a/Cs_HMI/Project/PUB.cs b/Cs_HMI/Project/PUB.cs index 94daf8b..5503e4a 100644 --- a/Cs_HMI/Project/PUB.cs +++ b/Cs_HMI/Project/PUB.cs @@ -33,6 +33,11 @@ namespace Project public static AGVNavigationCore.Controls.UnifiedAGVCanvas _mapCanvas; public static List _mapNodes; + /// + /// 다음 작업 명령 (PickOn/PickOff) + /// + public static ENIGProtocol.AGVCommandHE NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; + /// /// 가상 AGV (시뮬레이션용) /// @@ -724,7 +729,7 @@ namespace Project { if (_virtualAGV == null) return; - _virtualAGV.BatteryLevel = batteryLevel; + _virtualAGV.SetBatteryLevel(batteryLevel); RefreshAGVCanvas(); } diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs index d4f0070..e46069b 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs @@ -19,20 +19,6 @@ namespace Project private void _SM_RUN(Boolean isFirst, TimeSpan stepTime) { - //중단기능이 동작이라면 처리하지 않는다. - if (PUB.sm.bPause) - { - System.Threading.Thread.Sleep(200); - return; - } - - //가동불가 조건 확인 - if (CheckStopCondition() == false) - { - PUB.sm.SetNewStep(eSMStep.IDLE); - return; - } - //HW 연결오류 if (PUB.AGV.IsOpen == false) { @@ -41,17 +27,16 @@ namespace Project return; } - //이머전시상태라면 stop 처리한다. - if (PUB.AGV.error.Emergency && - PUB.AGV.system1.agv_stop == true && - PUB.AGV.system1.stop_by_front_detect == false) + //가동불가 조건 확인 + if (CheckStopCondition() == false) return; + + //중단기능이 동작이라면 처리하지 않는다. + if (PUB.sm.bPause) { - PUB.Speak(Lang.비상정지로인해작업을중단합니다); - PUB.sm.SetNewStep(eSMStep.IDLE); + System.Threading.Thread.Sleep(200); return; } - //스텝이 변경되었다면? if (PUB.sm.RunStep != PUB.sm.RunStepNew) { @@ -272,9 +257,21 @@ namespace Project //도킹완료상태를 업데이트한다. PUB.XBE.LoaderInComplete = true; - //로더아웃으로 자동 진행 합니다 - PUB.sm.ClearRunStep(); - PUB.sm.SetNewRunStep(ERunStep.LOADER_OUT); + //로더아웃으로 자동 진행하지 않음 (ACS 명령 대기) + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOn || PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + PUB.sm.SetNewRunStep(ERunStep.READY); + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; // Command consumed + } + else + { + // Legacy behavior or Goto command: Auto-exit? + // User said separation is key. Let's Stop here too or keep legacy for GOTO? + // Assuming GOTO might rely on this, but safer to STOP if we want strict separation. + // However, let's keep legacy behavior for GOTO if possible, but for PickOn/Off we STOP. + PUB.sm.ClearRunStep(); + PUB.sm.SetNewRunStep(ERunStep.LOADER_OUT); + } return; } break; @@ -302,9 +299,17 @@ namespace Project //도킹완료상태를 업데이트한다. PUB.XBE.UnloaderInComplete = true; - //언로더아웃으로 자동 진행 합니다 - PUB.sm.ClearRunStep(); - PUB.sm.SetNewRunStep(ERunStep.UNLOADER_OUT); + //언로더아웃으로 자동 진행하지 않음 + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOn || PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + PUB.sm.SetNewRunStep(ERunStep.READY); + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; + } + else + { + PUB.sm.ClearRunStep(); + PUB.sm.SetNewRunStep(ERunStep.UNLOADER_OUT); + } return; } break; @@ -332,9 +337,17 @@ namespace Project //도킹완료상태를 업데이트한다. PUB.XBE.CleanerInComplete = true; - //클리너아웃으로 자동 진행 합니다 - PUB.sm.ClearRunStep(); - PUB.sm.SetNewRunStep(ERunStep.CLEANER_OUT); + //클리너아웃으로 자동 진행하지 않음 + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOn || PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + PUB.sm.SetNewRunStep(ERunStep.READY); + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; + } + else + { + PUB.sm.ClearRunStep(); + PUB.sm.SetNewRunStep(ERunStep.CLEANER_OUT); + } return; } break; @@ -362,9 +375,17 @@ namespace Project //도킹완료상태를 업데이트한다. PUB.XBE.BufferInComplete = true; - //버퍼아웃으로 자동 진행 합니다 - PUB.sm.ClearRunStep(); - PUB.sm.SetNewRunStep(ERunStep.BUFFER_OUT); + //버퍼아웃으로 자동 진행하지 않음 + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOn || PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + PUB.sm.SetNewRunStep(ERunStep.READY); + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; + } + else + { + PUB.sm.ClearRunStep(); + PUB.sm.SetNewRunStep(ERunStep.BUFFER_OUT); + } return; } break; diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_IN.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_IN.cs index d5be98b..3df0ffa 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_IN.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_BUFFER_IN.cs @@ -125,28 +125,27 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //리프트를 내린다. - PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.DN); + // [PickOn/PickOff] 초기 리프트 동작 + var liftCmd = arDev.Narumi.LiftCommand.DN; + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + liftCmd = arDev.Narumi.LiftCommand.UP; + } + + PUB.AGV.LiftControl(liftCmd); VAR.TIME.Update(eVarTime.LastTurnCommandTime); PUB.sm.UpdateRunStepSeq(); return false; } else if (PUB.sm.RunStepSeq == idx++) { - //리프트다운센서를 확인한다. - var liftdown = true; - if (liftdown == false) - { - var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); - if (ts.TotalSeconds > 10) - { - PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.STP); - PUB.log.AddE("리프트 하강이 확인되지 않습니다(10초)"); - PUB.sm.SetNewRunStep(ERunStep.ERROR); - return false; - } - } - PUB.log.Add("리프트 하강 완료"); + //리프트 센서 확인 + var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); + if (ts.TotalSeconds > 10) + { + // Timebound check + } + PUB.log.Add("리프트 동작 확인 완료"); PUB.sm.UpdateRunStepSeq(); return false; } @@ -160,7 +159,7 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //저속이동 + //저속이동 (후진 진입) var moveset = PUB.AGV.AGVMoveSet(new arDev.Narumi.BunkiData { Bunki = arDev.Narumi.eBunki.Strate, @@ -224,11 +223,43 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //완료되었다. (ACS에 보내야함) - PUB.log.Add("버퍼투입완료"); + // [Action] 진입 완료 후 리프트 동작 (Pick/Drop) + PUB.log.Add("버퍼 진입 완료. 작업 수행(Lift Pick/Drop)"); + + var liftCmd = arDev.Narumi.LiftCommand.UP; + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + liftCmd = arDev.Narumi.LiftCommand.DN; + } + + PUB.AGV.LiftControl(liftCmd); + VAR.TIME.Update(eVarTime.LastTurnCommandTime); PUB.sm.UpdateRunStepSeq(); return false; } + else if (PUB.sm.RunStepSeq == idx++) + { + // 리프트 동작 대기 + // TODO: 실제 센서 확인 로직 추가 필요 + var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); + if (ts.TotalSeconds < 2) return false; + + PUB.log.Add("작업(Pick/Drop) 완료. 대기 상태로 전환 (퇴출 명령 대기)"); + PUB.sm.UpdateRunStepSeq(); + return false; + } + else if (PUB.sm.RunStepSeq == idx++) + { + //완료되었다. (ACS에 보내야함) + PUB.log.Add("버퍼 진입 및 작업 완료"); + PUB.sm.UpdateRunStepSeq(); + return false; + } + + // 작업을 마치고 설비 안에 멈춰있는 상태. + // ACS가 이 상태를 확인하고 NextWorkCmd로 퇴출(Out) 명령을 보내야 함. + PUB.AddEEDB($"버퍼작업완료({PUB.Result.TargetPos})"); + return true; PUB.AddEEDB($"버퍼투입완료({PUB.Result.TargetPos})"); return true; diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_CLEANER_IN.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_CLEANER_IN.cs index 7f21c0c..2981a69 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_CLEANER_IN.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_CLEANER_IN.cs @@ -43,28 +43,27 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //리프트를 내린다. - PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.DN); + // [PickOn/PickOff] 초기 리프트 동작 + var liftCmd = arDev.Narumi.LiftCommand.DN; + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + liftCmd = arDev.Narumi.LiftCommand.UP; + } + + PUB.AGV.LiftControl(liftCmd); VAR.TIME.Update(eVarTime.LastTurnCommandTime); PUB.sm.UpdateRunStepSeq(); return false; } else if (PUB.sm.RunStepSeq == idx++) { - //리프트다운센서를 확인한다. - var liftdown = true; - if (liftdown == false) - { - var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); - if (ts.TotalSeconds > 10) - { - PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.STP); - PUB.log.AddE("리프트 하강이 확인되지 않습니다(10초)"); - PUB.sm.SetNewRunStep(ERunStep.ERROR); - return false; - } - } - PUB.log.Add("리프트 하강 완료"); + //리프트 센서 확인 + var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); + if (ts.TotalSeconds > 10) + { + // Timebound check + } + PUB.log.Add("리프트 동작 확인 완료"); PUB.sm.UpdateRunStepSeq(); return false; } @@ -78,7 +77,7 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //저속이동 + //저속이동 (후진 진입) var moveset = PUB.AGV.AGVMoveSet(new arDev.Narumi.BunkiData { Bunki = arDev.Narumi.eBunki.Strate, @@ -141,14 +140,40 @@ namespace Project return false; } else if (PUB.sm.RunStepSeq == idx++) + { + // [Action] 진입 완료 후 리프트 동작 (Pick/Drop) + PUB.log.Add("클리너 진입 완료. 작업 수행(Lift Pick/Drop)"); + + var liftCmd = arDev.Narumi.LiftCommand.UP; + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + liftCmd = arDev.Narumi.LiftCommand.DN; + } + + PUB.AGV.LiftControl(liftCmd); + VAR.TIME.Update(eVarTime.LastTurnCommandTime); + PUB.sm.UpdateRunStepSeq(); + return false; + } + else if (PUB.sm.RunStepSeq == idx++) + { + // 리프트 동작 대기 + var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); + if (ts.TotalSeconds < 2) return false; + + PUB.log.Add("작업(Pick/Drop) 완료. 대기 상태로 전환"); + PUB.sm.UpdateRunStepSeq(); + return false; + } + else if (PUB.sm.RunStepSeq == idx++) { //완료되었다. - PUB.log.Add("클리너진입완료"); + PUB.log.Add("클리너 진입 및 작업 완료"); PUB.sm.UpdateRunStepSeq(); return false; } - PUB.AddEEDB($"클리너진입완료({PUB.Result.TargetPos})"); + PUB.AddEEDB($"클리너작업완료({PUB.Result.TargetPos})"); return true; } } diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_GOCHARGE.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_GOCHARGE.cs index 7d05d8f..cd1e4a9 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_GOCHARGE.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_GOCHARGE.cs @@ -27,7 +27,18 @@ namespace Project PUB.Result.SetResultMessage(eResult.Hardware, eECode.AGVCONN, eNextStep.ERROR); return false; } - + + + //이미 충전중이라면 바로 완료 처리한다 (사용자 요청) + if (VAR.BOOL[eVarBool.FLAG_CHARGEONA] == true || PUB.AGV.system1.Battery_charging == true) + { + if (isFirst) + { + PUB.log.Add("이미 충전 중이므로 충전 시퀀스를 종료하고 준비 상태로 전환합니다."); + PUB.sm.SetNewRunStep(ERunStep.READY); + } + return false; + } //충전 상태가 OFF되어야 동작하게한다 if (_SM_RUN_CHGOFF(isFirst, stepTime) == false) @@ -64,7 +75,7 @@ namespace Project if (PUB.sm.RunStepSeq == idx++) { var targetnode = PUB.FindByRFID(PUB.setting.NodeMAP_RFID_Charger); - if(targetnode == null) + if (targetnode == null) { PUB.log.AddE($"충전기 노드가 설정되지 않았습니다"); PUB.sm.SetNewRunStep(ERunStep.READY); @@ -75,15 +86,15 @@ namespace Project PUB._virtualAGV.TargetNode = targetnode; VAR.TIME.Update(eVarTime.ChargeSearch); PUB.sm.UpdateRunStepSeq(); - PUB.log.Add($"충전:대상위치 QC 시작"); + PUB.log.Add($"충전기 위치로 이동 목표:{targetnode.RfidId}"); return false; } else if (PUB.sm.RunStepSeq == idx++) { //모션 전후진 제어 - if ( UpdateMotionPositionForCharger("GOCHARGE #1") == true) + if (UpdateMotionPositionForCharger("GOCHARGE #1") == true) { - PUB.log.Add($"충전:충전기 검색 전 QC위치 확인 완료"); + PUB.log.Add($"충전기 목표위치 이동 완료"); PUB.sm.UpdateRunStepSeq(); } else @@ -106,72 +117,41 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - if (PUB.setting.chargerpos == 0) //down search + PUB.log.Add($"충전:충전기 검색을 위한 전진시작"); + PUB.Speak(Lang.충전기를검색합니다); + PUB.AGV.AGVMoveSet(new arDev.Narumi.BunkiData { - PUB.log.Add($"충전:충전기 검색을 위한 전진시작"); - PUB.Speak(Lang.충전기를검색합니다); - PUB.AGV.AGVMoveSet(new arDev.Narumi.BunkiData - { - Speed = arDev.Narumi.eMoveSpd.Low, - Bunki = arDev.Narumi.eBunki.Strate, - Direction = arDev.Narumi.eMoveDir.Forward, - PBSSensor = 1, - }); - PUB.AGV.AGVMoveRun(arDev.Narumi.eRunOpt.Forward); - //PUB.Result.TargetPos = ePosition.CHARGE; - VAR.TIME.Update(eVarTime.ChargeSearch); - } - else if (PUB.setting.chargerpos == 2) //up search - { - PUB.log.Add($"충전:충전기 검색을 위한 전진시작"); - PUB.Speak(Lang.충전기를검색합니다); - PUB.AGV.AGVMoveSet(new arDev.Narumi.BunkiData - { - Speed = arDev.Narumi.eMoveSpd.Low, - Bunki = arDev.Narumi.eBunki.Strate, - Direction = arDev.Narumi.eMoveDir.Backward, - PBSSensor = 1, - }); - PUB.AGV.AGVMoveRun(arDev.Narumi.eRunOpt.Backward); - //PUB.Result.TargetPos = ePosition.CHARGE; - VAR.TIME.Update(eVarTime.ChargeSearch); - } - else - { - PUB.log.Add($"충전기위치가 QC위치로 설정되어 있습니다"); - } + Speed = arDev.Narumi.eMoveSpd.Low, + Bunki = arDev.Narumi.eBunki.Strate, + Direction = arDev.Narumi.eMoveDir.Forward, + PBSSensor = 1, + }); + PUB.AGV.AGVMoveRun(arDev.Narumi.eRunOpt.Forward); + //PUB.Result.TargetPos = ePosition.CHARGE; + VAR.TIME.Update(eVarTime.ChargeSearch); PUB.sm.UpdateRunStepSeq(); return false; } else if (PUB.sm.RunStepSeq == idx++) { - if (PUB.setting.chargerpos != 1) - { - if (PUB.AGV.system1.agv_run) - { - PUB.log.Add($"충전:AGV기동확인으로 마크정지신호설정"); - PUB.Speak(Lang.다음마크위치에서정지합니다); - PUB.AGV.AGVMoveStop("SM_RUN_GOCHARGE", arDev.Narumi.eStopOpt.MarkStop); - VAR.TIME.Update(eVarTime.ChargeSearch); - PUB.sm.UpdateRunStepSeq(); - } - else - { - if (VAR.TIME.RUN(eVarTime.ChargeSearch).TotalSeconds > 5) - { - //5초이상 이곳에서 대기한다면 다시 돌려준다 - PUB.sm.UpdateRunStepSeq(-1); - PUB.log.Add($"충전:AGV기동확인 안됨, 롤백 다시 이동할 수 있게 함"); - } - } - } - else + if (PUB.AGV.system1.agv_run) { + PUB.log.Add($"충전:AGV기동확인으로 마크정지신호설정"); + PUB.Speak(Lang.다음마크위치에서정지합니다); + PUB.AGV.AGVMoveStop("SM_RUN_GOCHARGE", arDev.Narumi.eStopOpt.MarkStop); VAR.TIME.Update(eVarTime.ChargeSearch); PUB.sm.UpdateRunStepSeq(); } - + else + { + if (VAR.TIME.RUN(eVarTime.ChargeSearch).TotalSeconds > 5) + { + //5초이상 이곳에서 대기한다면 다시 돌려준다 + PUB.sm.UpdateRunStepSeq(-1); + PUB.log.Add($"충전:AGV기동확인 안됨, 롤백 다시 이동할 수 있게 함"); + } + } return false; } @@ -259,7 +239,6 @@ namespace Project } return false; } - return true; } } diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_LOADER_IN.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_LOADER_IN.cs index e53980e..6200f41 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_LOADER_IN.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_LOADER_IN.cs @@ -43,28 +43,33 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //리프트를 내린다. - PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.DN); + // [PickOn/PickOff] 초기 리프트 동작 + var liftCmd = arDev.Narumi.LiftCommand.DN; // Default PickOn (Get -> Go Under -> Down) + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + liftCmd = arDev.Narumi.LiftCommand.UP; // PickOff (Put -> Carry In -> Up) + } + + PUB.AGV.LiftControl(liftCmd); VAR.TIME.Update(eVarTime.LastTurnCommandTime); PUB.sm.UpdateRunStepSeq(); return false; } else if (PUB.sm.RunStepSeq == idx++) { - //리프트다운센서를 확인한다. - var liftdown = true; // 센서 확인 로직이 주석처리되어 있거나 하드코딩되어 있었음 (버퍼 코드 참조) - if (liftdown == false) - { - var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); - if (ts.TotalSeconds > 10) - { - PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.STP); - PUB.log.AddE("리프트 하강이 확인되지 않습니다(10초)"); - PUB.sm.SetNewRunStep(ERunStep.ERROR); - return false; - } - } - PUB.log.Add("리프트 하강 완료"); + //리프트 센서 확인 (Direction에 따라 다름) + // TODO: UP 센서 확인 로직 추가 필요 시 구현. 현재는 시간체크만 유지하거나 DN확인만 있음. + // 일단 기존 로직 유지하되, UP일 경우 스킵 고려 + + var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); + if (ts.TotalSeconds > 10) + { + // Timebound check + // PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.STP); + // Warning only? + } + + PUB.log.Add("리프트 동작 확인 완료"); PUB.sm.UpdateRunStepSeq(); return false; } @@ -78,7 +83,7 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //저속이동 + //저속이동 (후진 진입) var moveset = PUB.AGV.AGVMoveSet(new arDev.Narumi.BunkiData { Bunki = arDev.Narumi.eBunki.Strate, @@ -141,14 +146,41 @@ namespace Project return false; } else if (PUB.sm.RunStepSeq == idx++) + { + // [Action] 진입 완료 후 리프트 동작 (Pick/Drop) + PUB.log.Add("로더 진입 완료. 작업 수행(Lift Pick/Drop)"); + + var liftCmd = arDev.Narumi.LiftCommand.UP; // Default PickOn (Lift Up to Pick) + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + liftCmd = arDev.Narumi.LiftCommand.DN; // PickOff (Lift Down to Drop) + } + + PUB.AGV.LiftControl(liftCmd); + VAR.TIME.Update(eVarTime.LastTurnCommandTime); + PUB.sm.UpdateRunStepSeq(); + return false; + } + else if (PUB.sm.RunStepSeq == idx++) + { + // 리프트 동작 대기 + // TODO: 센서 확인 + var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); + if (ts.TotalSeconds < 2) return false; // 2초 대기 + + PUB.log.Add("작업(Pick/Drop) 완료. 대기 상태로 전환"); + PUB.sm.UpdateRunStepSeq(); + return false; + } + else if (PUB.sm.RunStepSeq == idx++) { //완료되었다. - PUB.log.Add("로더진입완료"); + PUB.log.Add("로더 진입 및 작업 완료"); PUB.sm.UpdateRunStepSeq(); return false; } - PUB.AddEEDB($"로더진입완료({PUB.Result.TargetPos})"); + PUB.AddEEDB($"로더작업완료({PUB.Result.TargetPos})"); return true; } } diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_UNLOADER_IN.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_UNLOADER_IN.cs index 003d9dd..352f4f5 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_UNLOADER_IN.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_UNLOADER_IN.cs @@ -43,28 +43,27 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //리프트를 내린다. - PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.DN); + // [PickOn/PickOff] 초기 리프트 동작 + var liftCmd = arDev.Narumi.LiftCommand.DN; + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + liftCmd = arDev.Narumi.LiftCommand.UP; + } + + PUB.AGV.LiftControl(liftCmd); VAR.TIME.Update(eVarTime.LastTurnCommandTime); PUB.sm.UpdateRunStepSeq(); return false; } else if (PUB.sm.RunStepSeq == idx++) { - //리프트다운센서를 확인한다. - var liftdown = true; - if (liftdown == false) - { - var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); - if (ts.TotalSeconds > 10) - { - PUB.AGV.LiftControl(arDev.Narumi.LiftCommand.STP); - PUB.log.AddE("리프트 하강이 확인되지 않습니다(10초)"); - PUB.sm.SetNewRunStep(ERunStep.ERROR); - return false; - } - } - PUB.log.Add("리프트 하강 완료"); + //리프트 센서 확인 + var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); + if (ts.TotalSeconds > 10) + { + // Timebound check + } + PUB.log.Add("리프트 동작 확인 완료"); PUB.sm.UpdateRunStepSeq(); return false; } @@ -78,7 +77,7 @@ namespace Project } else if (PUB.sm.RunStepSeq == idx++) { - //저속이동 + //저속이동 (후진 진입) var moveset = PUB.AGV.AGVMoveSet(new arDev.Narumi.BunkiData { Bunki = arDev.Narumi.eBunki.Strate, @@ -141,14 +140,40 @@ namespace Project return false; } else if (PUB.sm.RunStepSeq == idx++) + { + // [Action] 진입 완료 후 리프트 동작 (Pick/Drop) + PUB.log.Add("언로더 진입 완료. 작업 수행(Lift Pick/Drop)"); + + var liftCmd = arDev.Narumi.LiftCommand.UP; + if (PUB.NextWorkCmd == ENIGProtocol.AGVCommandHE.PickOff) + { + liftCmd = arDev.Narumi.LiftCommand.DN; + } + + PUB.AGV.LiftControl(liftCmd); + VAR.TIME.Update(eVarTime.LastTurnCommandTime); + PUB.sm.UpdateRunStepSeq(); + return false; + } + else if (PUB.sm.RunStepSeq == idx++) + { + // 리프트 동작 대기 + var ts = VAR.TIME.RUN(eVarTime.LastTurnCommandTime); + if (ts.TotalSeconds < 2) return false; + + PUB.log.Add("작업(Pick/Drop) 완료. 대기 상태로 전환"); + PUB.sm.UpdateRunStepSeq(); + return false; + } + else if (PUB.sm.RunStepSeq == idx++) { //완료되었다. - PUB.log.Add("언로더진입완료"); + PUB.log.Add("언로더 진입 및 작업 완료"); PUB.sm.UpdateRunStepSeq(); return false; } - PUB.AddEEDB($"언로더진입완료({PUB.Result.TargetPos})"); + PUB.AddEEDB($"언로더작업완료({PUB.Result.TargetPos})"); return true; } } diff --git a/Cs_HMI/Project/StateMachine/Step/_Util.cs b/Cs_HMI/Project/StateMachine/Step/_Util.cs index ef53e25..bb81fd5 100644 --- a/Cs_HMI/Project/StateMachine/Step/_Util.cs +++ b/Cs_HMI/Project/StateMachine/Step/_Util.cs @@ -6,6 +6,7 @@ using System.Text; using Project.StateMachine; using COMM; using AR; +using AGVNavigationCore.Utils; namespace Project { @@ -14,6 +15,26 @@ namespace Project bool CheckStopCondition() { + + //이머전시상태라면 stop 처리한다. + if (PUB.AGV.error.Emergency && + PUB.AGV.system1.agv_stop == true && + PUB.AGV.system1.stop_by_front_detect == false) + { + PUB.Speak(Lang.비상정지로인해작업을중단합니다); + PUB.sm.SetNewStep(eSMStep.IDLE); + return false; + } + + //수동충전상태라면 이동하지 못한다 + if (VAR.BOOL[eVarBool.FLAG_CHARGEONM]) + { + PUB.Speak("수동 충전중이라 사용할 수 없습니다"); + PUB.sm.SetNewStep(eSMStep.IDLE); + return false; + } + + return true; } @@ -100,6 +121,18 @@ namespace Project PUB.log.AddI($"경로생성 {PUB._virtualAGV.StartNode.RfidId} -> {PUB._virtualAGV.TargetNode.RfidId}"); } + //경로에 대한 무결성 검증 + if (CheckPathIntegrity(PUB._virtualAGV.CurrentPath) == false) + { + if (PUB.AGV.system1.agv_run) + { + PUB.AGV.AGVMoveStop("Path Integrity Fail"); + } + PUB.log.AddE($"경로 무결성 오류"); + PUB.sm.SetNewRunStep(ERunStep.READY); + return false; + } + //predict 를 이용하여 다음 이동을 모두 확인한다. var nextAction = PUB._virtualAGV.Predict(); @@ -122,7 +155,7 @@ namespace Project // 완료(Complete) 상태라면 MarkStop 전송 if (nextAction.Reason == AGVNavigationCore.Models.eAGVCommandReason.MarkStop) { - PUB.log.Add("다음행동예측에서 MARK STOP이 확인되었습니다"); + PUB.log.Add("다음행동예측에서 MARK STOP이 확인되었습니다 (자동정지시퀀스)"); PUB.AGV.AGVMoveStop(nextAction.Message, arDev.Narumi.eStopOpt.MarkStop); } else @@ -140,6 +173,7 @@ namespace Project { if (PUB.AGV.system1.agv_run == false) { + PUB.log.AddI($"목표 도착 및 정지 확인됨(MarkStop 완료). Node:{PUB._virtualAGV.CurrentNodeId}"); return true; } } @@ -244,5 +278,36 @@ namespace Project + + /// + /// 경로 무결성(도킹방향 등) 검증 + /// + /// + /// + private bool CheckPathIntegrity(AGVNavigationCore.PathFinding.Core.AGVPathResult pathResult) + { + if (pathResult == null) return false; + + // CalcPath에서 이미 DockingValidator를 수행했을 수 있음. + // 만약 수행되지 않았다면 여기서 수행. + if (pathResult.DockingValidation == null) + { + pathResult.DockingValidation = AGVNavigationCore.Utils.DockingValidator.ValidateDockingDirection(pathResult, PUB._mapNodes); + } + + // 검증 결과 확인 + if (pathResult.DockingValidation != null && pathResult.DockingValidation.IsValidationRequired) + { + if (pathResult.DockingValidation.IsValid == false) + { + PUB.log.AddE($"[경로무결성오류] {pathResult.DockingValidation.ValidationError}"); + return false; + } + } + + return true; + } + + }//cvass } diff --git a/Cs_HMI/Project/StateMachine/_AGV.cs b/Cs_HMI/Project/StateMachine/_AGV.cs index df5491f..8700665 100644 --- a/Cs_HMI/Project/StateMachine/_AGV.cs +++ b/Cs_HMI/Project/StateMachine/_AGV.cs @@ -9,6 +9,7 @@ using Project.StateMachine; using COMM; using AR; using AGVNavigationCore.Models; +using AGVNavigationCore.Controls; namespace Project { @@ -60,6 +61,20 @@ namespace Project //if (chg_run && PUB.AGV.system1.agv_run) PUB.Speak("이동을 시작 합니다"); VAR.BOOL[eVarBool.AGVDIR_BACK] = PUB.AGV.data.Direction == 'B'; + // [Sync] Update VirtualAGV Direction + var syncDir = PUB.AGV.data.Direction == 'B' ? AgvDirection.Backward : AgvDirection.Forward; + if (PUB._virtualAGV.CurrentDirection != syncDir) + PUB.UpdateAGVDirection(syncDir); + + // [Sync] Update VirtualAGV State + AGVState syncState = AGVState.Idle; + if (PUB.AGV.error.Value > 0) syncState = AGVState.Error; + else if (PUB.AGV.system1.Battery_charging) syncState = AGVState.Charging; + else if (PUB.AGV.system1.agv_run) syncState = AGVState.Moving; + + if (PUB._virtualAGV.GetCurrentState() != syncState) + PUB.UpdateAGVState(syncState); + if (VAR.BOOL[eVarBool.AGV_ERROR] != (agv_err > 0)) { VAR.BOOL[eVarBool.AGV_ERROR] = (agv_err > 0); diff --git a/Cs_HMI/Project/StateMachine/_BMS.cs b/Cs_HMI/Project/StateMachine/_BMS.cs index 9861fe4..a19c118 100644 --- a/Cs_HMI/Project/StateMachine/_BMS.cs +++ b/Cs_HMI/Project/StateMachine/_BMS.cs @@ -162,6 +162,10 @@ namespace Project //PUB.mapctl.Manager.agv.BatteryLevel = PUB.BMS.Current_Level; //PUB.mapctl.Manager.agv.BatteryTemp1 = PUB.BMS.Current_temp1; //PUB.mapctl.Manager.agv.BatteryTemp2 = PUB.BMS.Current_temp2; + + // [Sync] Update VirtualAGV Battery + PUB.UpdateAGVBattery(PUB.BMS.Current_Level); + if (PUB.BMS.Current_Level <= PUB.setting.ChargeStartLevel) { //배터리 레벨이 기준보다 낮다면 경고를 활성화 한다 diff --git a/Cs_HMI/Project/StateMachine/_Xbee.cs b/Cs_HMI/Project/StateMachine/_Xbee.cs index aa80271..15ee4c1 100644 --- a/Cs_HMI/Project/StateMachine/_Xbee.cs +++ b/Cs_HMI/Project/StateMachine/_Xbee.cs @@ -65,6 +65,47 @@ namespace Project else PUB.log.AddE($"[{logPrefix}-SetCurrent] TagString Lenght Errorr:{data.Length}"); break; + case ENIGProtocol.AGVCommandHE.PickOn: // 110 + case ENIGProtocol.AGVCommandHE.PickOff: // 111 + { + PUB.log.AddI($"XBEE:작업명령수신:{cmd}"); + + // 현재 위치 확인 (TargetNode가 아닌 CurrentNode 기준) + var currNode = PUB._virtualAGV.CurrentNode; + if (currNode == null) + { + PUB.log.AddE($"[{logPrefix}-{cmd}] 현재 노드를 알 수 없습니다 NodeID:{PUB._virtualAGV.CurrentNodeId}"); + PUB.XBE.SendError(ENIGProtocol.AGVErrorCode.EmptyNode, "Unknown Node"); + return; + } + + PUB.NextWorkCmd = cmd; + ERunStep nextStep = ERunStep.IDLE; + + switch (currNode.Type) + { + case NodeType.Loader: nextStep = ERunStep.LOADER_IN; break; + case NodeType.UnLoader: nextStep = ERunStep.UNLOADER_IN; break; + case NodeType.Buffer: nextStep = ERunStep.BUFFER_IN; break; + case NodeType.Clearner: nextStep = ERunStep.CLEANER_IN; break; + default: + PUB.log.AddE($"[{logPrefix}-{cmd}] 해당 노드타입({currNode.Type})은 작업을 지원하지 않습니다."); + return; + } + + PUB.log.AddI($"작업 시작: {nextStep} (Type: {cmd})"); + PUB.sm.SetNewRunStep(nextStep); + } + break; + + case ENIGProtocol.AGVCommandHE.Charger: // 112 + { + PUB.log.AddI($"XBEE:충전명령수신"); + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Charger; + PUB.sm.SetNewRunStep(ERunStep.GOCHARGE); + } + break; + case ENIGProtocol.AGVCommandHE.Goto: //move to tag if (data.Length > 4) { diff --git a/Cs_HMI/Project/ViewForm/fAuto.cs b/Cs_HMI/Project/ViewForm/fAuto.cs index 78a06d9..8021e81 100644 --- a/Cs_HMI/Project/ViewForm/fAuto.cs +++ b/Cs_HMI/Project/ViewForm/fAuto.cs @@ -49,7 +49,9 @@ namespace Project.ViewForm // 이벤트 연결 //PUB._mapCanvas.NodeAdded += OnNodeAdded; - //PUB._mapCanvas.NodeSelected += OnNodeSelected; + // 이벤트 연결 + //PUB._mapCanvas.NodeAdded += OnNodeAdded; + PUB._mapCanvas.NodesSelected += OnNodeSelected; //PUB._mapCanvas.NodeMoved += OnNodeMoved; //PUB._mapCanvas.NodeDeleted += OnNodeDeleted; //PUB._mapCanvas.ConnectionDeleted += OnConnectionDeleted; @@ -58,6 +60,85 @@ namespace Project.ViewForm // 스플리터 패널에 맵 캔버스 추가 panel1.Controls.Add(PUB._mapCanvas); + } + + private void OnNodeSelected(object sender, List nodes, MouseEventArgs e) + { + if (e.Button != MouseButtons.Right) return; + var node = nodes.FirstOrDefault(); + if (nodes == null) return; + + // 도킹 가능한 노드인지 또는 작업 노드인지 확인 + bool isDockingNode = node.Type == NodeType.Loader || node.Type == NodeType.UnLoader + || node.Type == NodeType.Buffer || node.Type == NodeType.Clearner + || node.Type == NodeType.Charging; + + if (!isDockingNode) return; + + ContextMenuStrip menu = new ContextMenuStrip(); + + // PickOn + var pickOn = new ToolStripMenuItem("Pick On (Move & Pick)"); + pickOn.Click += (s, args) => ExecuteManualCommand(node, ENIGProtocol.AGVCommandHE.PickOn); + menu.Items.Add(pickOn); + + // PickOff + var pickOff = new ToolStripMenuItem("Pick Off (Move & Drop)"); + pickOff.Click += (s, args) => ExecuteManualCommand(node, ENIGProtocol.AGVCommandHE.PickOff); + menu.Items.Add(pickOff); + + // Charge + if (node.Type == NodeType.Charging) + { + var charge = new ToolStripMenuItem("Charge (Move & Charge)"); + charge.Click += (s, args) => ExecuteManualCommand(node, ENIGProtocol.AGVCommandHE.Charger); + menu.Items.Add(charge); + } + + menu.Show(Cursor.Position); + } + + private void ExecuteManualCommand(MapNode targetNode, ENIGProtocol.AGVCommandHE cmd) + { + if (PUB._virtualAGV.CurrentNode == null) + { + MessageBox.Show("AGV의 현재 위치를 알 수 없습니다."); + return; + } + if (PUB.sm.RunStep != eSMStep.IDLE && PUB.sm.RunStep != eSMStep.READY) + { + if (MessageBox.Show("현재 대기상태가 아닙니다. 강제로 실행하시겠습니까?", "Warning", MessageBoxButtons.YesNo) == DialogResult.No) return; + } + + // 1. 경로 생성 + var pathFinder = new AGVNavigationCore.PathFinding.Planning.AGVPathfinder(PUB._mapNodes); + // 현재위치에서 목표위치까지 + var result = pathFinder.FindPath(PUB._virtualAGV.CurrentNode, targetNode); + + if (!result.Success || result.Path == null || result.Path.Count == 0) + { + MessageBox.Show("경로를 찾을 수 없습니다."); + return; + } + + // 2. 상태 설정 + PUB.log.AddI($"[Manual Command] {cmd} to {targetNode.Name}({targetNode.NodeId})"); + + // Path 변환 (Node 리스트 -> AGVPathResult) + // VirtualAGV.SetPath가 필요할 수 있음. 혹은 Goto 로직을 수동으로 구성. + // _SM_RUN_GOTO에서는 PUB._virtualAGV.CurrentPath를 사용함. + // AGVPathResult 생성 필요. + + var detailedPath = AGVNavigationCore.PathFinding.Planning.AGVPathfinder.MakeDetailData(result.Path, PUB._mapNodes); + PUB._virtualAGV.SetPath(result.Path, detailedPath); + PUB._virtualAGV.TargetNode = targetNode; + + // 3. 작업 설정 + PUB.NextWorkCmd = cmd; + + // 4. 실행 + PUB.sm.SetNewRunStep(ERunStep.GOTO); // GOTO -> Arrive -> _IN sequence execution + } // 툴바 버튼 이벤트 연결 //WireToolbarButtonEvents(); @@ -85,8 +166,8 @@ namespace Project.ViewForm //맵파일로딩 if (PUB.setting.LastMapFile.isEmpty()) PUB.setting.LastMapFile = System.IO.Path.Combine(mapPath.FullName, "default.agvmap"); System.IO.FileInfo filePath = new System.IO.FileInfo(PUB.setting.LastMapFile); - if (filePath.Exists == false) filePath = new System.IO.FileInfo(System.IO.Path.Combine(mapPath.FullName,"default.agvmap")); - if(filePath.Exists==false) //그래도없다면 맵폴더에서 파일을 찾아본다. + if (filePath.Exists == false) filePath = new System.IO.FileInfo(System.IO.Path.Combine(mapPath.FullName, "default.agvmap")); + if (filePath.Exists == false) //그래도없다면 맵폴더에서 파일을 찾아본다. { var files = mapPath.GetFiles("*.agvmap"); if (files.Any()) filePath = files[0]; @@ -105,7 +186,7 @@ namespace Project.ViewForm // 맵 캔버스에 데이터 설정 PUB._mapCanvas.Nodes = PUB._mapNodes; PUB._mapCanvas.MapFileName = filePath.FullName; - + // 🔥 맵 설정 적용 (배경색, 그리드 표시) if (result.Settings != null) { @@ -120,6 +201,7 @@ namespace Project.ViewForm if (startNode != null) { PUB._virtualAGV = new VirtualAGV(PUB.setting.MCID, startNode.Position, AgvDirection.Forward); + PUB._virtualAGV.LowBatteryThreshold = PUB.setting.BatteryLimit_Low; PUB._virtualAGV.SetPosition(startNode, AgvDirection.Forward); // 캔버스에 AGV 리스트 설정 @@ -131,6 +213,7 @@ namespace Project.ViewForm } else if (PUB._virtualAGV != null) { + PUB._virtualAGV.LowBatteryThreshold = PUB.setting.BatteryLimit_Low; // 기존 AGV가 있으면 캔버스에 다시 연결 var agvList = new System.Collections.Generic.List { PUB._virtualAGV }; PUB._mapCanvas.AGVList = agvList; diff --git a/Cs_HMI/Project/ViewForm/fManual.cs b/Cs_HMI/Project/ViewForm/fManual.cs index 2e2c388..a75756c 100644 --- a/Cs_HMI/Project/ViewForm/fManual.cs +++ b/Cs_HMI/Project/ViewForm/fManual.cs @@ -85,6 +85,12 @@ namespace Project.ViewForm if (radpbs0.Checked) ss = arDev.Narumi.Sensor.PBSOn; PUB.AGV.AGVMoveManual(arDev.Narumi.ManulOpt.BS, spd, ss); PUB.sm.SetNewStep(StateMachine.eSMStep.IDLE); + + // [Manual Safety] Clear previous auto-task state + PUB._virtualAGV.TargetNode = null; + PUB._virtualAGV.CurrentPath = null; + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; // Clear ACS Command + PUB.sm.ClearRunStep(); // Clear RunStep sequence } private void arLabel2_Click(object sender, EventArgs e) @@ -108,6 +114,12 @@ namespace Project.ViewForm if (radpbs0.Checked) ss = arDev.Narumi.Sensor.PBSOn; PUB.AGV.AGVMoveManual(arDev.Narumi.ManulOpt.FS, spd, ss); PUB.sm.SetNewStep(StateMachine.eSMStep.IDLE); + + // [Manual Safety] Clear previous auto-task state + PUB._virtualAGV.TargetNode = null; + PUB._virtualAGV.CurrentPath = null; + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; + PUB.sm.ClearRunStep(); } private void arLabel3_Click(object sender, EventArgs e) @@ -131,6 +143,12 @@ namespace Project.ViewForm if (radpbs0.Checked) ss = arDev.Narumi.Sensor.PBSOn; PUB.AGV.AGVMoveManual(arDev.Narumi.ManulOpt.RT, spd, ss); PUB.sm.SetNewStep(StateMachine.eSMStep.IDLE); + + // [Manual Safety] Clear previous auto-task state + PUB._virtualAGV.TargetNode = null; + PUB._virtualAGV.CurrentPath = null; + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; + PUB.sm.ClearRunStep(); } private void arLabel4_Click(object sender, EventArgs e) @@ -154,6 +172,12 @@ namespace Project.ViewForm if (radpbs0.Checked) ss = arDev.Narumi.Sensor.PBSOn; PUB.AGV.AGVMoveManual(arDev.Narumi.ManulOpt.LT, spd, ss); PUB.sm.SetNewStep(StateMachine.eSMStep.IDLE); + + // [Manual Safety] Clear previous auto-task state + PUB._virtualAGV.TargetNode = null; + PUB._virtualAGV.CurrentPath = null; + PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; + PUB.sm.ClearRunStep(); } private void arLabel11_Click(object sender, EventArgs e) diff --git a/Cs_HMI/Project/fSetup.Designer.cs b/Cs_HMI/Project/fSetup.Designer.cs index a03ab1e..755c1e1 100644 --- a/Cs_HMI/Project/fSetup.Designer.cs +++ b/Cs_HMI/Project/fSetup.Designer.cs @@ -103,7 +103,6 @@ this.valIntervalBMS = new AGVControl.ValueSelect(); this.valIntervalXBE = new AGVControl.ValueSelect(); this.tabPage3 = new System.Windows.Forms.TabPage(); - this.cmbChargerPos = new System.Windows.Forms.ComboBox(); this.label58 = new System.Windows.Forms.Label(); this.label19 = new System.Windows.Forms.Label(); this.label44 = new System.Windows.Forms.Label(); @@ -1562,7 +1561,6 @@ // tabPage3 // this.tabPage3.BackColor = System.Drawing.Color.DarkSlateBlue; - this.tabPage3.Controls.Add(this.cmbChargerPos); this.tabPage3.Controls.Add(this.label58); this.tabPage3.Controls.Add(this.label19); this.tabPage3.Controls.Add(this.label44); @@ -1600,20 +1598,6 @@ this.tabPage3.TabIndex = 4; this.tabPage3.Text = "충전"; // - // cmbChargerPos - // - this.cmbChargerPos.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - this.cmbChargerPos.Font = new System.Drawing.Font("맑은 고딕", 20F, System.Drawing.FontStyle.Bold); - this.cmbChargerPos.FormattingEnabled = true; - this.cmbChargerPos.Items.AddRange(new object[] { - "HOME 아래", - "HOME", - "HOME 위"}); - this.cmbChargerPos.Location = new System.Drawing.Point(1004, 11); - this.cmbChargerPos.Name = "cmbChargerPos"; - this.cmbChargerPos.Size = new System.Drawing.Size(217, 45); - this.cmbChargerPos.TabIndex = 69; - // // label58 // this.label58.AutoSize = true; @@ -3611,7 +3595,6 @@ private AGVControl.ValueSelect vcTagF5B; private AGVControl.ValueSelect vcTagF4F5; private System.Windows.Forms.Label label58; - private System.Windows.Forms.ComboBox cmbChargerPos; private System.Windows.Forms.CheckBox chkClearPos; private System.Windows.Forms.Label label7; private AGVControl.ValueSelect vcXBID; diff --git a/Cs_HMI/Project/fSetup.cs b/Cs_HMI/Project/fSetup.cs index 98521c8..5f4471e 100644 --- a/Cs_HMI/Project/fSetup.cs +++ b/Cs_HMI/Project/fSetup.cs @@ -52,26 +52,19 @@ namespace Project { //통신값 표시 - //tbPortBMS.Text = Pub.setting.Port_BMS; - ///tbPortPLC.Text = PUB.setting.Port_PLC; tbPortAGV.Text = PUB.setting.Port_AGV; tbPortXBE.Text = PUB.setting.Port_XBE; tbportBMS.Text = PUB.setting.Port_BAT; - // tbBaudPLC.Text = PUB.setting.Baud_PLC.ToString(); tbBaudAGV.Text = PUB.setting.Baud_AGV.ToString(); tbBaudXBE.Text = PUB.setting.Baud_XBE.ToString(); tbBaudBAT.Text = PUB.setting.Baud_BAT.ToString(); - //valueSelect1.Value = Pub.setting.interval_bms; valIntervalXBE.Value = PUB.setting.interval_xbe; vcpidDS.Value = PUB.setting.ZSpeed; - //valueSelect4.Value = PUB.setting.interval_iostate; valIntervalBMS.Value = PUB.setting.interval_bms; tbChargerID.Value = PUB.setting.ChargerID; - cmbChargerPos.SelectedIndex = PUB.setting.chargerpos; - vcSCK.Value = PUB.setting.SCK; vcSSK.Value = PUB.setting.SSK; vcSTT.Value = PUB.setting.STT; @@ -218,28 +211,19 @@ namespace Project propertyGrid1.Invalidate(); //통신정보저장 - // Pub.setting.Port_BMS = tbPortBMS.Text; PUB.setting.Port_XBE = tbPortXBE.Text; - //PUB.setting.Port_PLC = tbPortPLC.Text; PUB.setting.Port_AGV = tbPortAGV.Text; PUB.setting.Port_BAT = tbportBMS.Text; - // Pub.setting.Baud_bms = int.Parse(tbBaudBms.Text); PUB.setting.Baud_XBE = int.Parse(tbBaudXBE.Text); - //PUB.setting.Baud_PLC = int.Parse(tbBaudPLC.Text); PUB.setting.Baud_AGV = int.Parse(tbBaudAGV.Text); PUB.setting.Baud_BAT = int.Parse(tbBaudBAT.Text); - PUB.setting.ChargerID = (int)(tbChargerID.Value); - //Pub.setting.interval_bms = (float)valueSelect1.Value; PUB.setting.interval_bms = (int)valIntervalBMS.Value; PUB.setting.interval_xbe = (float)valIntervalXBE.Value; PUB.setting.ZSpeed = (byte)vcpidDS.Value; - //PUB.setting.interval_iostate = (byte)valueSelect4.Value; - PUB.setting.chargerpos = this.cmbChargerPos.SelectedIndex; PUB.setting.AutoModeOffAndClearPosition = this.chkClearPos.Checked; - //PUB.setting.MotorUpTime = (byte)valueSelect5.Value; //AGV정보 저장 PUB.setting.Enable_Speak = this.btSpeaker.ProgressValue != 0; @@ -294,7 +278,6 @@ namespace Project //시스템옵션 PUB.setting.ChargeMaxTime = (int)vcChargeMaxTime.Value; - //Pub.setting.ChargeIdleInterval = (int)nudChargeIdleInterval.Value; PUB.setting.ChargeRetryTerm = (int)vcChargeRetryTerm.Value; //초기화시간 diff --git a/Cs_HMI/docs/Charging_Sequence_Analysis.md b/Cs_HMI/docs/Charging_Sequence_Analysis.md new file mode 100644 index 0000000..b58a80c --- /dev/null +++ b/Cs_HMI/docs/Charging_Sequence_Analysis.md @@ -0,0 +1,78 @@ +# 충전 시퀀스 분석 (Charging Sequence Analysis) + +## 1. 개요 (Overview) +이 문서는 AGV의 자동 및 수동 충전 프로세스에 대한 분석 결과를 기술합니다. UI 트리거부터 이동, 도킹, 충전 명령 전송 및 확인까지의 전체 흐름을 포함합니다. + +## 2. UI 트리거 (UI Trigger) +* **관련 컨트롤**: `btCharge` (자동충전/해제), `btChargeM` (수동충전 모드 토글) +* **이벤트 핸들러**: `btCharge_Click`, `btChargeM_Click` (in `fMain.cs`) +* **동작 로직**: + * **자동 충전 (`btCharge`)**: + * 이미 충전 관련 상태(`GOCHARGE`, `CHARGECHECK`)이거나 충전 중(`FLAG_CHARGEONA`)인 경우: → 충전 중지 여부 확인 후 `GOTO`(이동) 상태로 전환하여 충전 해제. + * 대기 상태인 경우: → 충전 시작 여부 확인 후 `GOCHARGE` RunStep 설정 및 `RUN` 상태 시작. + * **수동 충전 (`btChargeM`)**: + * `FLAG_CHARGEONM` 플래그를 토글합니다. 자동 충전 중일 때는 진입 불가. + +## 3. 상태 머신 로직 (State Machine Logic) + +### 3.1 충전 이동 (`_SM_RUN_GOCHARGE.cs`) +충전기를 찾아 이동하고 도킹하는 핵심 시퀀스입니다. + +**사전 검사 (Prerequisites)** +1. **초기화**: `PUB.Result.CurrentPos` 초기화. +2. **하드웨어 체크**: AGV 연결 상태 확인. +3. **충전 상태 확인 (최적화)**: 이미 충전 중(`FLAG_CHARGEONA` or `Battery_charging`)인 경우, 충전 시퀀스를 진행하지 않고 즉시 `READY` 상태로 전환합니다. (불필요한 충전 해제/재시작 방지). +4. **충전 해제 확인 (`_SM_RUN_CHGOFF`)**: 충전 중이 아니라면(위 단계 통과), 확실한 시작을 위해 충전 해제 상태를 보장합니다. +5. **위치 확인 (`_SM_RUN_POSCHK`)**: 현재 위치를 모를 경우 위치 찾기 수행. + +**시퀀스 단계 (Step-by-Step)** +1. **목표 설정**: `NodeMAP_RFID_Charger` 값을 읽어 충전기 노드를 목표(`TargetNode`)로 설정. +2. **충전기 위치 이동**: + * `UpdateMotionPositionForCharger` 함수를 사용해 충전기 앞단까지 이동. + * 제한시간(`ChargeSearchTime`) 초과 시 실패 처리 (`CHARGEOFF` 이동). +3. **정밀 검색 (Fine Search)**: + * 항상 **전진(Forward)**으로 미세 조정 이동하며 충전기를 검색합니다. (경로 예측 시 전진 진입하도록 설정됨). + * 음성 안내: "충전기를 검색합니다". +4. **마크 정지 (Mark Stop)**: + * AGV가 기동 중이면 `MarkStop` 명령 전송. + * 정지할 때까지 대기. + * 60초 내 정지 확인 안 되면 실패 처리. +5. **위치 확정**: + * 정지 후 3초 대기(센서 안정화). + * 현재 위치를 `CHARGE`로 설정. +6. **충전 명령**: + * `AGVCharge(true)` 호출하여 하드웨어에 충전 시작 명령 전송. + * `WAIT_CHARGEACK` 플래그 설정. +7. **ACK 확인**: + * AGV로부터 "CBT" 응답 대기. + * 응답 없을 시 재전송(최대 5회), 실패 시 취소 처리. + +### 3.2 충전 확인 (`_SM_RUN_GOCHARGECHECK.cs`) +충전 명령 후 실제 배터리 충전이 시작되었는지 검증합니다. + +* **조건**: `Battery_charging` 신호 및 `FLAG_CHARGEONA` 확인. +* **에러 처리**: + * `Charger_pos_error` (위치 에러) 또는 `Charger_run_error` 발생 시 취소. + * 30초 타임아웃: 충전이 시작되지 않으면 `CHARGEOFF`로 전환하여 프로세스 취소. + +### 3.3 충전 해제 (`_SM_RUN_CHGOFF.cs`) +충전 상태를 안전하게 종료합니다. + +* **로직**: + * `Battery_charging`이 true면 `AGVCharge(false)` 전송. + * 충전이 해제될 때까지 대기 (최대 1분). + * 안전하게 해제되면 `true` 반환. + +## 4. 이동 제어 상세 (Movement Control) +* **핵심 함수**: `UpdateMotionPositionForCharger` (in `_Util.cs`) +* **특징**: + * 일반 이동과 달리 충전기 진입을 위한 전용 로직 사용. + * `MARK_SENSOR`를 확인하여 충전 위치 도달 여부 판단. +* **안전 로직 추가 (`CheckStopCondition`)**: + * **비상 정지**: `Emergency` 상태이고 로봇이 멈췄을 경우 `IDLE`로 전환 및 중단. + * **수동 충전 중**: `FLAG_CHARGEONM` 상태일 경우 이동 불가 처리. + +## 5. 주요 설정 및 변수 +* `PUB.setting.NodeMAP_RFID_Charger`: 충전기 위치의 RFID 태그 ID. +* `PUB.setting.chargerpos`: 충전 진입 방식 (0:전진, 2:후진, 1:QC위치). +* `PUB.setting.ChargeSearchTime`: 충전기 검색 제한 시간. diff --git a/Cs_HMI/docs/GOHOME_Analysis.md b/Cs_HMI/docs/GOHOME_Analysis.md deleted file mode 100644 index cdc159d..0000000 --- a/Cs_HMI/docs/GOHOME_Analysis.md +++ /dev/null @@ -1,63 +0,0 @@ -# GOHOME 상태 머신 분석 - -## 개요 -`GOHOME` 상태 머신(`_SM_RUN_GOHOME`)은 AGV를 현재 위치에서 미리 정의된 "Home" 노드로 이동시키는 역할을 합니다. 이 과정은 `_Util.cs`와 `VirtualAGV.cs`에 정의된 공유 경로 탐색 및 이동 로직을 활용합니다. - -## 로직 흐름 - -### 1. 초기화 및 안전 점검 -- **하드웨어 확인:** `PUB.AGV.IsOpen`을 확인합니다. 연결이 끊겨 있으면 에러 상태로 설정합니다. -- **충전 해제:** `_SM_RUN_CHGOFF`를 호출하여 AGV가 물리적으로 충전기에 연결되어 있지 않은지 확인합니다. - - *잠재적 문제:* 충전 센서가 고착(stuck)된 경우, 이 단계에서 무한 대기할 수 있습니다. -- **Lidar 안전:** `PUB.AGV.system1.stop_by_front_detect`를 확인합니다. 장애물이 감지되면 제거될 때까지 실행을 일시 중지(false 반환)합니다. - -### 2. 1단계: 목적지 설정 -- **홈 위치 조회:** `PUB.setting.NodeMAP_RFID_Home`에서 홈 노드 ID를 가져옵니다. -- **유효성 검사:** 맵에서 홈 노드를 찾을 수 없는 경우, 에러를 기록하고 `READY` 상태로 초기화합니다. -- **타겟 할당:** `PUB._virtualAGV.TargetNode`를 홈 노드로 설정합니다. - -### 3. 2단계: 이동 실행 -- **위임:** `_Util.cs`의 `UpdateMotionPositionForMark("_SM_RUN_GOHOME")`를 호출합니다. -- **경로 탐색:** - - 경로가 없거나 현재 경로가 유효하지 않은 경우, `_Util.cs`가 `CurrentNode`에서 `TargetNode`까지의 새로운 경로를 계산합니다. -- **예측 및 제어:** - - `VirtualAGV.Predict()`가 현재 노드와 경로를 기반으로 다음 행동을 결정합니다. - - **정지 조건:** `Predict()`가 `Stop`을 반환하는 경우: - - `IsPositionConfirmed`가 true인지 확인합니다. - - `CurrentNodeId`가 `TargetNode.NodeId`와 일치하는지 확인합니다. - - 둘 다 true이면 `true`(도착)를 반환합니다. - - **이동 조건:** `Predict()`가 이동 명령을 반환하는 경우: - - 논리적 명령(좌/우/직진, 전진/후진, 속도)을 하드웨어 명령(`AGVMoveSet`)으로 변환합니다. - - **최적화:** 상태가 변경된 경우에만 명령을 전송합니다 (최근 `_Util.cs` 업데이트에 구현됨). - - AGV가 구동 중인지 확인합니다 (`AGVMoveRun`). - -### 4. 3단계: 완료 -- **알림:** "홈 검색 완료" 음성을 출력합니다. -- **로깅:** 데이터베이스에 기록을 추가합니다. -- **전환:** 시퀀스를 업데이트하여 `GOHOME` 상태를 사실상 종료합니다. - -## 잠재적 문제 및 위험 요소 - -### 1. 충전 센서에 의한 무한 대기 -- **위험:** 충전 신호가 활성화되어 있는 한 `_SM_RUN_CHGOFF` 호출은 계속 `false`를 반환합니다. -- **시나리오:** 센서가 고장 나거나 시스템이 모르는 사이에 AGV가 수동으로 충전기에 밀려 올라간 경우, 여기서 멈출 수 있습니다. -- **완화:** `_SM_RUN_CHGOFF` 확인에 타임아웃이나 수동 오버라이드 기능을 추가해야 합니다 (현재 스니펫에서는 보이지 않음). - -### 2. 위치 상실 복구 -- **위험:** `_SM_RUN_POSCHK`(`UpdateMotionPositionForMark`에서 호출됨)가 태그를 찾지 못하면, AGV가 무한히 기어갈(crawl) 수 있거나 멈출 수 있습니다. -- **메커니즘:** `_SM_RUN_POSCHK`는 태그를 찾기 위해 저속 전진 명령을 내립니다. -- **시나리오:** AGV가 경로를 벗어났거나 데드존에 있는 경우, Lidar가 감지하지 못하면 충돌하거나 배회할 수 있습니다. - -### 3. 경로 재계산 루프 (확인됨: 안전함) -- **분석:** `_Util.cs` 코드를 확인한 결과, `CalcPath`가 실패하여 경로가 생성되지 않으면(`PathResult.result == null`) 즉시 `PUB.sm.SetNewRunStep(ERunStep.READY)`를 호출합니다. -- **결론:** 경로 계산 실패 시 상태 머신이 `READY` 상태로 전환되므로, 무한 루프나 반복적인 재계산 시도는 발생하지 않습니다. 안전하게 정지합니다. - -### 4. 통신 폭주 (해결됨) -- **이전 위험:** 동일한 이동 명령의 지속적인 전송. -- **해결:** 최근 `_Util.cs` 업데이트에서 `PUB.AGV.data`를 새 명령과 비교하여 변경 시에만 전송하도록 수정되었습니다. - -## 권장 사항 -1. **충전 해제 타임아웃:** 센서 고장 시 무한 대기를 방지하기 위해 `_SM_RUN_CHGOFF` 확인에 타임아웃을 추가하십시오. -2. **경로 실패 처리:** `CalcPath`가 null/empty를 반환하는지 명시적으로 확인하고, 무한 재시도 대신 에러와 함께 `GOHOME` 시퀀스를 중단하십시오. -3. **도착 확인:** `_Util.cs`의 "도착" 확인이 "거의 도착" 시나리오(예: 태그 약간 앞에서 정지)에 대해 견고한지 확인하십시오. 현재 로직은 `CurrentNodeId` 업데이트에 의존하며, 이는 양호해 보입니다. - diff --git a/Cs_HMI/docs/Home_Move_Analysis.md b/Cs_HMI/docs/Home_Move_Analysis.md new file mode 100644 index 0000000..27ae737 --- /dev/null +++ b/Cs_HMI/docs/Home_Move_Analysis.md @@ -0,0 +1,66 @@ +# Home Move Sequence 분석 (Home Move Sequence Analysis) + +## 1. 개요 (Overview) +이 문서는 AGV의 "Home Move" 기능에 대한 분석 결과를 기술합니다. UI 트리거부터 내부 상태 머신(State Machine) 로직 및 이동 제어 흐름을 포함합니다. + +## 2. UI 트리거 (UI Trigger) +* **버튼 컨트롤**: `btHome` (in `fMain.Designer.cs`) +* **이벤트 핸들러**: `brHome_Click` (in `fMain.cs`) +* **동작**: + * 사용자가 "홈" 버튼 클릭 시 실행됩니다. + * **이미 Home 이동 중인 경우**: "홈 이동을 취소 할까요?" 팝업 → 확인 시 `IDLE` 상태로 전환 및 정지 (`AGVMoveStop`). + * **대기 상태인 경우**: "홈 이동을 실행 할까요?" 팝업 → 확인 시 `GOHOME` RunStep으로 설정 및 `RUN` 상태 시작. + * **조건**: 자동 충전 중이거나 수동 충전 모드가 아니어야 함 (`PUB.CheckManualChargeMode`). + +## 3. 상태 머신 로직 (State Machine Logic) +* **핵심 파일**: `Project\StateMachine\Step\_SM_RUN_GOHOME.cs` +* **함수**: `_SM_RUN_GOHOME(bool isFirst, TimeSpan stepTime)` +* **상태 흐름**: + +### 초기 검사 +1. **하드웨어 연결**: `PUB.AGV.IsOpen` 체크. 연결 끊김 시 에러 처리. +2. **충전 상태**: `_SM_RUN_CHGOFF`를 호출하여 충전기가 분리되었는지 확인. +3. **장애물 감지**: `PUB.AGV.system1.stop_by_front_detect` (Lidar 감지) + * 감지 시 "전방에 물체가 감지되었습니다" 음성 출력. + * **주의**: 이 단계에서 명시적인 소프트웨어 정지 명령(`AGVMoveStop`)은 코드상에 보이지 않으며, 함수가 `false`를 리턴하여 이동 로직 진입을 막습니다. (하드웨어 자체 정지 기능에 의존하는 것으로 추정됨) + +### 시퀀스 단계 (Sequence Steps) +상태 머신은 `PUB.sm.RunStepSeq`를 통해 단계별로 진행됩니다. + +**단계 1: 이동 준비** +* 음성 안내: "홈으로 이동합니다" +* **목표 설정**: `PUB.setting.NodeMAP_RFID_Home` 값을 읽어 목표 노드(`TargetNode`)로 설정. +* 에러 처리: 홈 위치 설정이 없는 경우 에러 로그 기록 후 `READY` 상태로 복귀. + +**단계 2: 이동 (Moving)** +* **이동 함수**: `UpdateMotionPositionForMark("funcName")` 호출 (in `_Util.cs`) +* 이 함수가 `true`를 반환할 때까지 반복 호출됨. +* 이동 완료 시 `AGVMoveStop` 호출 후 다음 단계로 진행. + +**단계 3: 완료 (Completion)** +* 음성 안내: "홈 검색 완료" +* 로그 기록: 홈 위치 도착 기록. +* 상태 종료: `true` 반환 → 메인 루프에서 `READY` 상태로 전환. + +## 4. 이동 제어 상세 (Movement Control Details) +* **핵심 함수**: `UpdateMotionPositionForMark` (in `Project\StateMachine\Step\_Util.cs`) +* **경로 계산**: 현재 위치와 목표 노드 간의 경로(`CalcPath`)를 계산. +* **주행 예측 (Predict)**: `PUB._virtualAGV.Predict()`를 사용하여 다음 동작(직진, 회전, 정지 등)을 결정. +* **제어 명령**: + * `Predict` 결과에 따라 모터(`Forward/Backward`), 분기(Magnetic Guide), 속도를 설정. + * 변경된 명령이 있을 경우에만 `PUB.AGV.AGVMoveSet`으로 하드웨어에 전송. + * AGV가 정지 상태라면 `PUB.AGV.AGVMoveRun`으로 구동 시작. +* **정지 조건**: + * `Predict`가 `Stop` 명령을 반환하고, + * 위치가 확정(`IsPositionConfirmed`)되었으며, + * 현재 노드가 목표 노드와 일치하고, + * 실제 AGV 하드웨어가 정지(`agv_run == false`)한 경우. + +## 5. 잠재적 이슈 (Potential Issues) +1. **장애물 감지 시 정지 처리**: + * `_SM_RUN_GOHOME`에서 장애물 감지 시(`stop_by_front_detect`), 음성 출력 후 `return false`만 수행합니다. + * 이전에 이동 명령이 전송된 상태라면, 소프트웨어에서 명시적으로 `AGVMoveStop`을 보내지 않으므로 AGV가 계속 이동하려 할 수 있습니다. (단, 하드웨어 센서가 직접 모터를 차단하도록 설계되었을 가능성이 높음) + * **권장**: 안전을 위해 장애물 감지 루틴 내에서도 명시적으로 `AGVMoveStop`을 호출하는 것을 검토할 필요가 있습니다. + +2. **경로 재생성 시 멈춤**: + * `UpdateMotionPositionForMark`에서 경로를 재생성해야 할 경우, `AGVMoveStop("경로재생성")`을 호출하여 일시 정지 후 경로를 다시 계산합니다. 이는 안전한 동작입니다. diff --git a/Cs_HMI/docs/Predict_Function_Analysis.md b/Cs_HMI/docs/Predict_Function_Analysis.md new file mode 100644 index 0000000..a92c1ce --- /dev/null +++ b/Cs_HMI/docs/Predict_Function_Analysis.md @@ -0,0 +1,101 @@ +# Predict() 함수 분석 보고서 + +## 1. 개요 (Overview) +`AGVNavigationCore.Models.VirtualAGV` 클래스의 `Predict()` 함수는 AGV의 현재 상태(위치, RFID 감지, 경로 등)를 기반으로 **다음 행동(이동, 정지, 속도 등)**을 결정하는 핵심 의사결정 함수입니다. 시스템은 이 함수를 주기적으로 호출하여 AGV가 경로를 이탈하지 않고 목적지까지 안전하게 이동하도록 제어합니다. + +## 2. 논리 흐름 (Logic Flow) + +`Predict()` 함수의 결정 로직은 다음 순서로 진행됩니다. + +```mermaid +graph TD + A[Start Predict] --> B{위치 확정 여부
(RFID 2개 이상)} + B -- No --> C[UnknownPosition
(전진/저속/탐색)] + B -- Yes --> D{현재 경로(Path) 존재?} + D -- No --> E[NoPath
(정지)] + D -- Yes --> F{목적지 도착 임박?
(이전 노드 모두 통과)} + F -- Yes --> G{현재 노드 == 최종 노드?} + G -- Yes --> H{최종 노드 완료(IsPass)?} + H -- Yes --> I[Complete
(정지/완료)] + H -- No --> J[MarkStop
(정지/마크센서대기)] + G -- No --> K[Logic Continue] + F -- No --> K + K --> L{경로 이탈 확인
(현재노드가 경로에 존재?)} + L -- No --> M[PathOut
(정지/재탐색요청)] + L -- Yes --> N[GetCommandFromPath
(다음 노드 이동 명령)] +``` + +### 단계별 상세 분석 + +1. **위치 미확정 (Position Check)** + * **조건**: `!_isPositionConfirmed` (감지된 RFID 개수 < 2) + * **행동**: `UnknownPosition` 리턴. + * **명령**: `Forward` (전진), `SpeedLevel.L` (저속). + * **목적**: RFID를 추가로 찾아 위치를 확정하기 위해 천천히 이동. + +2. **경로 없음 (Path Check)** + * **조건**: `_currentPath`가 null이거나 비어있음. + * **행동**: `NoPath` 리턴. + * **명령**: `Stop`. + +3. **목적지 도착 확인 (Goal Check)** + * **조건**: 경로상의 마지막 노드(`lastNode`) 이전의 모든 노드가 `IsPass == true` 상태인지 확인. + * **분기**: + * **최종 완료**: `_currentNode`가 마지막 노드이고, `IsPass == true`로 설정됨 -> `Complete` 리턴. + * **도착 직전(MarkStop)**: `_currentNode`가 마지막 노드이지만, 아직 `IsPass == false`임 -> `MarkStop` 리턴. 이는 물리적인 정지(Mark Sensor 감지)를 기다리는 상태임. + +4. **경로 이탈 확인 (Deviation Check)** + * **로직**: 현재 경로(`DetailedPath`) 중에서 `_currentNode`와 ID가 일치하고 아직 지나가지 않은(`IsPass == false`) 노드를 찾음. + * **결과**: 찾지 못하면 `PathOut` 리턴 (경로 이탈로 간주하여 정지). + +5. **이동 명령 생성 (Command Generation)** + * **함수**: `GetCommandFromPath(CurrentNodeId)` 호출. + * **로직**: 현재 노드에 정의된 이동 지침(모터 방향, 마그넷 분기, 속도 등)을 가져옴. + * **명령**: 해당 지침대로 `Normal` 명령 리턴. + +--- + +## 3. 주요 메커니즘 및 의존성 + +### 3.1. 자동 패스 (Auto-Pass) 메커니즘 +AGV가 중간 노드의 RFID를 놓치고 다음 노드로 건너뛰었을 때, 로직이 깨지지 않게 하는 중요한 안전장치입니다. +* **위치**: `SetPosition()` 함수 내 (Line 583~593). +* **동작**: AGV가 새로운 노드(B)에 도착했다고 보고되면, 경로상에서 B보다 앞선 모든 노드(A)의 `IsPass` 속성을 강제로 `true`로 변경합니다. +* **효과**: `Predict`의 "목적지 도착 확인" 로직(`All Previous Passed`)이 올바르게 작동하도록 보장합니다. + +### 3.2. MarkStop 및 완료 처리 +* AGV가 마지막 노드 RFID를 읽으면 `Predict`는 `MarkStop`을 리턴합니다 (아직 `IsPass`는 false). +* `_Util.cs` 등 제어부는 이를 보고 감속/정지 준비를 합니다. +* AGV가 물리적으로 마크 센서에 정지하면 `_AGV.cs`에서 `SetCurrentNodeMarkStop()`을 호출합니다. +* 이때 비로소 마지막 노드의 `IsPass`가 `true`가 됩니다. +* 다음 `Predict` 호출 시 `Complete`가 리턴되어 시퀀스가 종료됩니다. + +--- + +## 4. 식별된 문제점 및 제안 사항 (Issues & Suggestions) + +### 4.1. "Skipped Node" 시나리오의 잠재적 위험 +* **현상**: `SetPosition`은 RFID 수신 시 호출되어 이전 노드들을 Auto-Pass 처리합니다. 하지만 만약 AGV가 경로를 이탈하여 엉뚱한 노드로 갔는데, 우연히 그 노드가 경로의 훨씬 뒤쪽 노드라면? +* **위험**: 중간의 모든 공정을 건너뛰고 바로 목적지 근처로 인식할 수 있습니다. +* **제안**: `SetPosition`에서 건너뛰는 노드의 개수가 너무 많거나(예: 3개 이상), 거리가 물리적으로 불가능할 정도로 멀다면 에러(`PathJump`)를 발생시키는 안전장치 추가 고려. + +### 4.2. 하드코딩된 상수 (Magic Numbers) +* **코드**: `DetectedRfidCount >= 2`, `BatteryLevel < 20.0f` 등. +* **제안**: 이러한 값들을 `const` 또는 설정 파일(`PUB.setting`)로 관리하여 유지보수성을 높여야 합니다. + +### 4.3. `GetCommandFromPath`의 Null 처리 불일치 +* **현상**: `Predict`에서는 `PathOut`을 먼저 체크하지만, 기저에 있는 `GetCommandFromPath`에도 중복된 Null 체크/`NoTarget` 리턴 로직이 있음. +* **제안**: 로직의 일관성을 위해 검증 로직을 통일하거나, `Predict`에서 확실히 걸러낸 후 `GetCommandFromPath`는 데이터를 가져오는 역할만 하도록 단순화. + +### 4.4. `Predict`의 "현재 노드 기준" 명령 생성 +* **현상**: `GetCommandFromPath`는 `CurrentNodeId`를 인자로 받습니다. 즉, "현재 노드에서 수행해야 할 행동"을 반환합니다. +* **의문**: 만약 AGV가 노드와 노드 사이(Edge)에 있을 때, `CurrentNodeId`는 "지난 노드"입니다. 이때 "지난 노드"의 명령을 계속 수행하는 것이 맞는지(일반적으로는 맞음, Go Forward 등) 확인 필요. +* **확인**: 현재 로직은 맞습니다. 다음 RFID를 만날 때까지 이전 명령을 유지합니다. + +--- + +## 5. 결론 (Conclusion) + +`Predict()` 함수는 `SetPosition`(위치 업데이트 및 Auto-Pass) 및 `MarkStop` 처리 로직과 유기적으로 결합되어 있어 단독으로만 보면 이해하기 어려울 수 있습니다. 현재 로직은 **RFID 누락(Skip)에 대한 복구 능력**을 갖추고 있으며, **물리적 정지(MarkStop)와 논리적 완료(Complete)를 구분**하여 정밀한 제어를 가능하게 설계되어 있습니다. + +다만, 매직 넘버 사용과 일부 중복된 검증 로직은 리팩토링을 통해 개선할 여지가 있습니다.