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)를 구분**하여 정밀한 제어를 가능하게 설계되어 있습니다.
+
+다만, 매직 넘버 사용과 일부 중복된 검증 로직은 리팩토링을 통해 개선할 여지가 있습니다.