diff --git a/Cs_HMI/.claude/settings.local.json b/Cs_HMI/.claude/settings.local.json
new file mode 100644
index 0000000..3f52f3c
--- /dev/null
+++ b/Cs_HMI/.claude/settings.local.json
@@ -0,0 +1,16 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(find:*)",
+ "Bash(wc:*)",
+ "Bash(dotnet build:*)",
+ "Bash(.build.bat)",
+ "Bash(cmd.exe:*)",
+ "Bash(timeout:*)",
+ "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\MSBuild\\Current\\Bin\\MSBuild.exe\" \"C:\\Data\\Source\\(5613#) ENIG AGV\\Source\\Cs_HMI\\AGVCSharp.sln\" -property:Configuration=Debug -property:Platform=x86 -verbosity:minimal -nologo)",
+ "Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\MSBuild\\Current\\Bin\\MSBuild.exe\" \"C:\\Data\\Source\\(5613#) ENIG AGV\\Source\\Cs_HMI\\AGVSimulator\\AGVSimulator.csproj\" -property:Configuration=Debug -property:Platform=x86 -verbosity:minimal -nologo)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVCSharp.sln b/Cs_HMI/AGVCSharp.sln
index d7dd581..7594e59 100644
--- a/Cs_HMI/AGVCSharp.sln
+++ b/Cs_HMI/AGVCSharp.sln
@@ -34,6 +34,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "솔루션 항목", "솔루션 항목", "{2A3A057F-5D22-31FD-628C-DF5EF75AEF1E}"
ProjectSection(SolutionItems) = preProject
build.bat = build.bat
+ CHANGELOG.md = CHANGELOG.md
CLAUDE.md = CLAUDE.md
TODO.md = TODO.md
EndProjectSection
diff --git a/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj b/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
index a30a039..2a73530 100644
--- a/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
+++ b/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
@@ -54,8 +54,6 @@
-
-
Form
diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.cs b/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
index d1bb41c..88e8fe5 100644
--- a/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
+++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
@@ -105,7 +105,6 @@ namespace AGVMapEditor.Forms
{
_mapCanvas = new UnifiedAGVCanvas();
_mapCanvas.Dock = DockStyle.Fill;
- _mapCanvas.Mode = UnifiedAGVCanvas.CanvasMode.Edit;
_mapCanvas.Nodes = _mapNodes;
// RfidMappings 제거 - MapNode에 통합됨
diff --git a/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs b/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs
deleted file mode 100644
index 47523ec..0000000
--- a/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs
+++ /dev/null
@@ -1,309 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using AGVNavigationCore.Models;
-
-namespace AGVMapEditor.Models
-{
- ///
- /// RFID 값을 논리적 노드로 변환하는 클래스
- /// 실제 AGV 시스템에서 RFID 리더가 읽은 값을 맵 노드 정보로 변환
- ///
- public class NodeResolver
- {
- private List _rfidMappings;
- private List _mapNodes;
-
- ///
- /// 기본 생성자
- ///
- public NodeResolver()
- {
- _rfidMappings = new List();
- _mapNodes = new List();
- }
-
- ///
- /// 매개변수 생성자
- ///
- /// RFID 매핑 목록
- /// 맵 노드 목록
- public NodeResolver(List rfidMappings, List mapNodes)
- {
- _rfidMappings = rfidMappings ?? new List();
- _mapNodes = mapNodes ?? new List();
- }
-
- ///
- /// RFID 값으로 맵 노드 검색
- ///
- /// RFID 리더에서 읽은 값
- /// 해당하는 맵 노드, 없으면 null
- public MapNode GetNodeByRfid(string rfidValue)
- {
- if (string.IsNullOrEmpty(rfidValue))
- return null;
-
- // 1. RFID 매핑에서 논리적 노드 ID 찾기
- var mapping = _rfidMappings.FirstOrDefault(m =>
- m.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase) && m.IsActive);
-
- if (mapping == null)
- return null;
-
- // 2. 논리적 노드 ID로 실제 맵 노드 찾기
- var mapNode = _mapNodes.FirstOrDefault(n =>
- n.NodeId.Equals(mapping.LogicalNodeId, StringComparison.OrdinalIgnoreCase) && n.IsActive);
-
- return mapNode;
- }
-
- ///
- /// 논리적 노드 ID로 맵 노드 검색
- ///
- /// 논리적 노드 ID
- /// 해당하는 맵 노드, 없으면 null
- public MapNode GetNodeById(string nodeId)
- {
- if (string.IsNullOrEmpty(nodeId))
- return null;
-
- return _mapNodes.FirstOrDefault(n =>
- n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase) && n.IsActive);
- }
-
- ///
- /// 맵 노드로 연결된 RFID 값 검색
- ///
- /// 논리적 노드 ID
- /// 연결된 RFID 값, 없으면 null
- public string GetRfidByNodeId(string nodeId)
- {
- if (string.IsNullOrEmpty(nodeId))
- return null;
-
- var mapping = _rfidMappings.FirstOrDefault(m =>
- m.LogicalNodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase) && m.IsActive);
-
- return mapping?.RfidId;
- }
-
- ///
- /// RFID 매핑 정보 검색
- ///
- /// RFID 값
- /// 매핑 정보, 없으면 null
- public RfidMapping GetRfidMapping(string rfidValue)
- {
- if (string.IsNullOrEmpty(rfidValue))
- return null;
-
- return _rfidMappings.FirstOrDefault(m =>
- m.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase) && m.IsActive);
- }
-
- ///
- /// 새로운 RFID 매핑 추가
- ///
- /// RFID 값
- /// 논리적 노드 ID
- /// 설명
- /// 추가 성공 여부
- public bool AddRfidMapping(string rfidId, string nodeId, string description = "")
- {
- if (string.IsNullOrEmpty(rfidId) || string.IsNullOrEmpty(nodeId))
- return false;
-
- // 중복 RFID 체크
- if (_rfidMappings.Any(m => m.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase)))
- return false;
-
- // 해당 노드 존재 체크
- if (!_mapNodes.Any(n => n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase)))
- return false;
-
- var mapping = new RfidMapping(rfidId, nodeId, description);
- _rfidMappings.Add(mapping);
- return true;
- }
-
- ///
- /// RFID 매핑 제거
- ///
- /// 제거할 RFID 값
- /// 제거 성공 여부
- public bool RemoveRfidMapping(string rfidId)
- {
- if (string.IsNullOrEmpty(rfidId))
- return false;
-
- var mapping = _rfidMappings.FirstOrDefault(m =>
- m.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
-
- if (mapping != null)
- {
- _rfidMappings.Remove(mapping);
- return true;
- }
-
- return false;
- }
-
- ///
- /// 맵 노드 추가
- ///
- /// 추가할 맵 노드
- /// 추가 성공 여부
- public bool AddMapNode(MapNode node)
- {
- if (node == null || string.IsNullOrEmpty(node.NodeId))
- return false;
-
- // 중복 노드 ID 체크
- if (_mapNodes.Any(n => n.NodeId.Equals(node.NodeId, StringComparison.OrdinalIgnoreCase)))
- return false;
-
- _mapNodes.Add(node);
- return true;
- }
-
- ///
- /// 맵 노드 제거
- ///
- /// 제거할 노드 ID
- /// 제거 성공 여부
- public bool RemoveMapNode(string nodeId)
- {
- if (string.IsNullOrEmpty(nodeId))
- return false;
-
- var node = _mapNodes.FirstOrDefault(n =>
- n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase));
-
- if (node != null)
- {
- // 연관된 RFID 매핑도 함께 제거
- var associatedMappings = _rfidMappings.Where(m =>
- m.LogicalNodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase)).ToList();
-
- foreach (var mapping in associatedMappings)
- {
- _rfidMappings.Remove(mapping);
- }
-
- // 다른 노드의 연결 정보에서도 제거
- foreach (var otherNode in _mapNodes.Where(n => n.ConnectedNodes.Contains(nodeId)))
- {
- otherNode.RemoveConnection(nodeId);
- }
-
- _mapNodes.Remove(node);
- return true;
- }
-
- return false;
- }
-
- ///
- /// 특정 타입의 노드들 검색
- ///
- /// 노드 타입
- /// 해당 타입의 노드 목록
- public List GetNodesByType(NodeType nodeType)
- {
- return _mapNodes.Where(n => n.Type == nodeType && n.IsActive).ToList();
- }
-
- ///
- /// 장비 ID로 노드 검색
- ///
- /// 장비 ID
- /// 해당 장비의 노드, 없으면 null
- public MapNode GetNodeByStationId(string stationId)
- {
- if (string.IsNullOrEmpty(stationId))
- return null;
-
- return _mapNodes.FirstOrDefault(n =>
- n.StationId.Equals(stationId, StringComparison.OrdinalIgnoreCase) && n.IsActive);
- }
-
- ///
- /// 매핑되지 않은 노드들 검색 (RFID가 연결되지 않은 노드)
- ///
- /// 매핑되지 않은 노드 목록
- public List GetUnmappedNodes()
- {
- var mappedNodeIds = _rfidMappings.Where(m => m.IsActive)
- .Select(m => m.LogicalNodeId)
- .ToHashSet(StringComparer.OrdinalIgnoreCase);
-
- return _mapNodes.Where(n => n.IsActive && !mappedNodeIds.Contains(n.NodeId)).ToList();
- }
-
- ///
- /// 사용되지 않는 RFID 매핑들 검색 (노드가 삭제된 매핑)
- ///
- /// 사용되지 않는 매핑 목록
- public List GetOrphanedMappings()
- {
- var activeNodeIds = _mapNodes.Where(n => n.IsActive)
- .Select(n => n.NodeId)
- .ToHashSet(StringComparer.OrdinalIgnoreCase);
-
- return _rfidMappings.Where(m => m.IsActive && !activeNodeIds.Contains(m.LogicalNodeId)).ToList();
- }
-
- ///
- /// 데이터 초기화
- ///
- public void Clear()
- {
- _rfidMappings.Clear();
- _mapNodes.Clear();
- }
-
- ///
- /// 데이터 유효성 검증
- ///
- /// 검증 결과 메시지 목록
- public List ValidateData()
- {
- var errors = new List();
-
- // 중복 RFID 체크
- var duplicateRfids = _rfidMappings.GroupBy(m => m.RfidId.ToLower())
- .Where(g => g.Count() > 1)
- .Select(g => g.Key);
- foreach (var rfid in duplicateRfids)
- {
- errors.Add($"중복된 RFID: {rfid}");
- }
-
- // 중복 노드 ID 체크
- var duplicateNodeIds = _mapNodes.GroupBy(n => n.NodeId.ToLower())
- .Where(g => g.Count() > 1)
- .Select(g => g.Key);
- foreach (var nodeId in duplicateNodeIds)
- {
- errors.Add($"중복된 노드 ID: {nodeId}");
- }
-
- // 고아 매핑 체크
- var orphanedMappings = GetOrphanedMappings();
- foreach (var mapping in orphanedMappings)
- {
- errors.Add($"존재하지 않는 노드를 참조하는 RFID 매핑: {mapping.RfidId} → {mapping.LogicalNodeId}");
- }
-
- // 매핑되지 않은 노드 경고 (에러는 아님)
- var unmappedNodes = GetUnmappedNodes();
- foreach (var node in unmappedNodes)
- {
- errors.Add($"RFID가 매핑되지 않은 노드: {node.NodeId} ({node.Name})");
- }
-
- return errors;
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs b/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
deleted file mode 100644
index f3324eb..0000000
--- a/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
+++ /dev/null
@@ -1,432 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Drawing;
-using System.Linq;
-using AGVNavigationCore.Models;
-using AGVNavigationCore.PathFinding;
-
-namespace AGVMapEditor.Models
-{
- ///
- /// AGV 전용 경로 계산기 (AGVNavigationCore 래퍼)
- /// AGVMapEditor와 AGVNavigationCore 간의 호환성 제공
- /// RFID 기반 경로 계산을 우선 사용
- ///
- public class PathCalculator
- {
- private AGVPathfinder _agvPathfinder;
- private AStarPathfinder _astarPathfinder;
- private RfidBasedPathfinder _rfidPathfinder;
-
- ///
- /// 생성자
- ///
- public PathCalculator()
- {
- _agvPathfinder = new AGVPathfinder();
- _astarPathfinder = new AStarPathfinder();
- _rfidPathfinder = new RfidBasedPathfinder();
- }
-
- ///
- /// 맵 노드 설정
- ///
- /// 맵 노드 목록
- public void SetMapNodes(List mapNodes)
- {
- _agvPathfinder.SetMapNodes(mapNodes);
- _astarPathfinder.SetMapNodes(mapNodes);
- }
-
- ///
- /// 맵 데이터 설정
- ///
- /// 맵 노드 목록
- public void SetMapData(List mapNodes)
- {
- _agvPathfinder.SetMapNodes(mapNodes);
- _astarPathfinder.SetMapNodes(mapNodes);
- // RfidPathfinder는 MapNode의 RFID 정보를 직접 사용
- _rfidPathfinder.SetMapNodes(mapNodes);
- // 도킹 조건 검색용 내부 노드 목록 업데이트
- UpdateInternalMapNodes(mapNodes);
- }
-
- ///
- /// AGV 경로 계산
- ///
- /// 시작 노드 ID
- /// 목적지 노드 ID
- /// 목적지 도착 방향
- /// AGV 경로 계산 결과
- public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection = null)
- {
- return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
- }
-
- ///
- /// AGV 경로 계산 (옵션 지정 가능)
- ///
- /// 시작 노드 ID
- /// 목적지 노드 ID
- /// 목적지 도착 방향
- /// 경로 탐색 옵션
- /// AGV 경로 계산 결과
- public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, PathfindingOptions options)
- {
- return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection, options);
- }
-
- ///
- /// AGV 경로 계산 (현재 방향 및 PathfindingOptions 지원)
- ///
- /// 시작 노드 ID
- /// 목적지 노드 ID
- /// 현재 AGV 방향
- /// 목적지 도착 방향
- /// 경로 탐색 옵션
- /// AGV 경로 계산 결과
- public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? currentDirection, AgvDirection? targetDirection, PathfindingOptions options)
- {
- return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, currentDirection, targetDirection, options);
- }
-
- ///
- /// 충전 스테이션으로의 경로 찾기
- ///
- /// 시작 노드 ID
- /// AGV 경로 계산 결과
- public AGVPathResult FindPathToChargingStation(string startNodeId)
- {
- return _agvPathfinder.FindPathToChargingStation(startNodeId);
- }
-
- ///
- /// 도킹 스테이션으로의 경로 찾기
- ///
- /// 시작 노드 ID
- /// 장비 타입
- /// AGV 경로 계산 결과
- public AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
- {
- return _agvPathfinder.FindPathToDockingStation(startNodeId, stationType);
- }
-
- ///
- /// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
- ///
- /// 시작 노드 ID
- /// 목적지 후보 노드 ID 목록
- /// 경로 계산 결과
- public PathResult FindNearestPath(string startNodeId, List targetNodeIds)
- {
- return _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
- }
-
- ///
- /// 두 노드가 연결되어 있는지 확인
- ///
- /// 노드 1 ID
- /// 노드 2 ID
- /// 연결 여부
- public bool AreNodesConnected(string nodeId1, string nodeId2)
- {
- return _astarPathfinder.AreNodesConnected(nodeId1, nodeId2);
- }
-
- ///
- /// 경로 유효성 검증
- ///
- /// 검증할 경로
- /// 유효성 검증 결과
- public bool ValidatePath(List path)
- {
- return _agvPathfinder.ValidatePath(path);
- }
-
- ///
- /// 네비게이션 가능한 노드 목록 반환
- ///
- /// 노드 ID 목록
- public List GetNavigationNodes()
- {
- return _astarPathfinder.GetNavigationNodes();
- }
-
- ///
- /// AGV 현재 방향 설정
- ///
- /// 현재 방향
- public void SetCurrentDirection(AgvDirection direction)
- {
- _agvPathfinder.CurrentDirection = direction;
- }
-
- ///
- /// 회전 비용 가중치 설정
- ///
- /// 회전 비용 가중치
- public void SetRotationCostWeight(float weight)
- {
- _agvPathfinder.RotationCostWeight = weight;
- }
-
- ///
- /// 휴리스틱 가중치 설정
- ///
- /// 휴리스틱 가중치
- public void SetHeuristicWeight(float weight)
- {
- _astarPathfinder.HeuristicWeight = weight;
- }
-
- ///
- /// 최대 탐색 노드 수 설정
- ///
- /// 최대 탐색 노드 수
- public void SetMaxSearchNodes(int maxNodes)
- {
- _astarPathfinder.MaxSearchNodes = maxNodes;
- }
-
- // ==================== RFID 기반 경로 계산 메서드들 ====================
-
- ///
- /// RFID 기반 AGV 경로 계산
- ///
- /// 시작 RFID
- /// 목적지 RFID
- /// 목적지 도착 방향
- /// RFID 기반 AGV 경로 계산 결과
- public RfidPathResult FindAGVPathByRfid(string startRfidId, string endRfidId, AgvDirection? targetDirection = null)
- {
- return _rfidPathfinder.FindAGVPath(startRfidId, endRfidId, targetDirection);
- }
-
- ///
- /// RFID 기반 충전소 경로 찾기
- ///
- /// 시작 RFID
- /// RFID 기반 경로 계산 결과
- public RfidPathResult FindPathToChargingStationByRfid(string startRfidId)
- {
- return _rfidPathfinder.FindPathToChargingStation(startRfidId);
- }
-
- ///
- /// RFID 기반 도킹 스테이션 경로 찾기
- ///
- /// 시작 RFID
- /// 장비 타입
- /// RFID 기반 경로 계산 결과
- public RfidPathResult FindPathToDockingStationByRfid(string startRfidId, StationType stationType)
- {
- return _rfidPathfinder.FindPathToDockingStation(startRfidId, stationType);
- }
-
- ///
- /// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
- ///
- /// 시작 RFID
- /// 목적지 후보 RFID 목록
- /// RFID 기반 경로 계산 결과
- public RfidPathResult FindNearestPathByRfid(string startRfidId, List targetRfidIds)
- {
- return _rfidPathfinder.FindNearestPath(startRfidId, targetRfidIds);
- }
-
- ///
- /// RFID 매핑 정보 조회 (MapNode 반환)
- ///
- /// RFID
- /// MapNode 또는 null
- public MapNode GetRfidMapping(string rfidId)
- {
- return _rfidPathfinder.GetRfidMapping(rfidId);
- }
-
- ///
- /// RFID로 NodeId 조회
- ///
- /// RFID
- /// NodeId 또는 null
- public string GetNodeIdByRfid(string rfidId)
- {
- return _rfidPathfinder.GetNodeIdByRfid(rfidId);
- }
-
- ///
- /// NodeId로 RFID 조회
- ///
- /// NodeId
- /// RFID 또는 null
- public string GetRfidByNodeId(string nodeId)
- {
- return _rfidPathfinder.GetRfidByNodeId(nodeId);
- }
-
- ///
- /// 활성화된 RFID 목록 반환
- ///
- /// 활성화된 RFID 목록
- public List GetActiveRfidList()
- {
- return _rfidPathfinder.GetActiveRfidList();
- }
-
- ///
- /// RFID pathfinder의 AGV 현재 방향 설정
- ///
- /// 현재 방향
- public void SetRfidPathfinderCurrentDirection(AgvDirection direction)
- {
- _rfidPathfinder.CurrentDirection = direction;
- }
-
- ///
- /// RFID pathfinder의 회전 비용 가중치 설정
- ///
- /// 회전 비용 가중치
- public void SetRfidPathfinderRotationCostWeight(float weight)
- {
- _rfidPathfinder.RotationCostWeight = weight;
- }
-
- #region 도킹 조건 검색 기능
-
- // 내부 노드 목록 저장
- private List _mapNodes;
-
- ///
- /// 맵 노드 설정 (도킹 조건 검색용)
- ///
- private void UpdateInternalMapNodes(List mapNodes)
- {
- _mapNodes = mapNodes;
- }
-
- ///
- /// 도킹 방향 기반 노드 검색
- ///
- /// 도킹 방향
- /// 해당 도킹 방향의 노드 목록
- public List GetNodesByDockingDirection(DockingDirection dockingDirection)
- {
- if (_mapNodes == null) return new List();
-
- var result = new List();
-
- foreach (var node in _mapNodes)
- {
- if (!node.IsActive) continue;
-
- var nodeDockingDirection = GetNodeDockingDirection(node);
- if (nodeDockingDirection == dockingDirection)
- {
- result.Add(node);
- }
- }
-
- return result;
- }
-
- ///
- /// 노드의 도킹 방향 결정
- ///
- /// 노드
- /// 도킹 방향
- public DockingDirection GetNodeDockingDirection(MapNode node)
- {
- switch (node.Type)
- {
- case NodeType.Charging:
- return DockingDirection.Forward; // 충전기: 전진 도킹
- case NodeType.Docking:
- return DockingDirection.Backward; // 장비: 후진 도킹
- default:
- return DockingDirection.Forward; // 기본값: 전진
- }
- }
-
- ///
- /// 도킹 방향과 장비 타입에 맞는 노드들로의 경로 검색
- ///
- /// 시작 RFID
- /// 필요한 도킹 방향
- /// 장비 타입 (선택사항)
- /// 경로 계산 결과 목록 (거리 순 정렬)
- public List FindPathsByDockingCondition(string startRfidId, DockingDirection dockingDirection, StationType? stationType = null)
- {
- var targetNodes = GetNodesByDockingDirection(dockingDirection);
- var results = new List();
-
- // 장비 타입 필터링 (필요시)
- if (stationType.HasValue && dockingDirection == DockingDirection.Backward)
- {
- // 후진 도킹이면서 특정 장비 타입이 지정된 경우
- // 이 부분은 추후 StationMapping 정보가 있을 때 구현
- // 현재는 모든 도킹 노드를 대상으로 함
- }
-
- foreach (var targetNode in targetNodes)
- {
- if (!targetNode.HasRfid()) continue;
-
- try
- {
- var pathResult = _rfidPathfinder.FindAGVPath(startRfidId, targetNode.RfidId);
- if (pathResult.Success)
- {
- results.Add(pathResult);
- }
- }
- catch (Exception ex)
- {
- // 개별 경로 계산 실패는 무시하고 계속 진행
- System.Diagnostics.Debug.WriteLine($"Path calculation failed from {startRfidId} to {targetNode.RfidId}: {ex.Message}");
- }
- }
-
- // 거리 순으로 정렬
- return results.OrderBy(r => r.TotalDistance).ToList();
- }
-
- ///
- /// 가장 가까운 충전기 경로 찾기 (전진 도킹)
- ///
- /// 시작 RFID
- /// 가장 가까운 충전기로의 경로
- public RfidPathResult FindNearestChargingStationPath(string startRfidId)
- {
- var chargingPaths = FindPathsByDockingCondition(startRfidId, DockingDirection.Forward);
- var chargingNodes = chargingPaths.Where(p => p.Success).ToList();
-
- return chargingNodes.FirstOrDefault() ?? new RfidPathResult
- {
- Success = false,
- ErrorMessage = "충전 가능한 충전기를 찾을 수 없습니다."
- };
- }
-
- ///
- /// 가장 가까운 장비 도킹 경로 찾기 (후진 도킹)
- ///
- /// 시작 RFID
- /// 장비 타입 (선택사항)
- /// 가장 가까운 장비로의 경로
- public RfidPathResult FindNearestEquipmentPath(string startRfidId, StationType? stationType = null)
- {
- var equipmentPaths = FindPathsByDockingCondition(startRfidId, DockingDirection.Backward, stationType);
- var equipmentNodes = equipmentPaths.Where(p => p.Success).ToList();
-
- return equipmentNodes.FirstOrDefault() ?? new RfidPathResult
- {
- Success = false,
- ErrorMessage = $"도킹 가능한 장비를 찾을 수 없습니다. ({stationType?.ToString() ?? "모든 타입"})"
- };
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj b/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
index bfb8a39..57102aa 100644
--- a/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
+++ b/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
@@ -64,36 +64,31 @@
+
+ UserControl
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
UserControl
UnifiedAGVCanvas.cs
-
- UnifiedAGVCanvas.cs
- UserControl
-
UnifiedAGVCanvas.cs
UserControl
+
diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
index 85f7618..87b9399 100644
--- a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
+++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
@@ -1,10 +1,11 @@
+using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding;
+using AGVNavigationCore.PathFinding.Core;
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
-using AGVNavigationCore.Models;
-using AGVNavigationCore.PathFinding;
namespace AGVNavigationCore.Controls
{
@@ -194,7 +195,7 @@ namespace AGVNavigationCore.Controls
}
}
- private void DrawPath(Graphics g, PathResult path, Color color)
+ private void DrawPath(Graphics g, AGVPathResult path, Color color)
{
if (path?.Path == null || path.Path.Count < 2) return;
diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
index 3a55ae6..78c3f8e 100644
--- a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
+++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Windows.Forms;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
+using AGVNavigationCore.PathFinding.Core;
namespace AGVNavigationCore.Controls
{
@@ -75,8 +76,11 @@ namespace AGVNavigationCore.Controls
private Dictionary _agvStates;
// 경로 관련
- private PathResult _currentPath;
- private List _allPaths;
+ private AGVPathResult _currentPath;
+ private List _allPaths;
+
+ // 도킹 검증 관련
+ private Dictionary _dockingErrors;
// UI 요소들
private Image _companyLogo;
@@ -255,7 +259,7 @@ namespace AGVNavigationCore.Controls
///
/// 현재 표시할 경로
///
- public PathResult CurrentPath
+ public AGVPathResult CurrentPath
{
get => _currentPath;
set
@@ -269,12 +273,12 @@ namespace AGVNavigationCore.Controls
///
/// 모든 경로 목록 (다중 AGV 경로 표시용)
///
- public List AllPaths
+ public List AllPaths
{
- get => _allPaths ?? new List();
+ get => _allPaths ?? new List();
set
{
- _allPaths = value ?? new List();
+ _allPaths = value ?? new List();
Invalidate();
}
}
@@ -371,7 +375,8 @@ namespace AGVNavigationCore.Controls
_agvPositions = new Dictionary();
_agvDirections = new Dictionary();
_agvStates = new Dictionary();
- _allPaths = new List();
+ _allPaths = new List();
+ _dockingErrors = new Dictionary();
InitializeBrushesAndPens();
CreateContextMenu();
@@ -653,6 +658,47 @@ namespace AGVNavigationCore.Controls
_nodeCounter = maxNumber + 1;
}
+ ///
+ /// 특정 노드에 도킹 오류 표시를 설정/해제합니다.
+ ///
+ /// 노드 ID
+ /// 오류 여부
+ public void SetDockingError(string nodeId, bool hasError)
+ {
+ if (string.IsNullOrEmpty(nodeId))
+ return;
+
+ if (hasError)
+ {
+ _dockingErrors[nodeId] = true;
+ }
+ else
+ {
+ _dockingErrors.Remove(nodeId);
+ }
+
+ Invalidate(); // 화면 다시 그리기
+ }
+
+ ///
+ /// 특정 노드에 도킹 오류가 있는지 확인합니다.
+ ///
+ /// 노드 ID
+ /// 도킹 오류 여부
+ public bool HasDockingError(string nodeId)
+ {
+ return _dockingErrors.ContainsKey(nodeId) && _dockingErrors[nodeId];
+ }
+
+ ///
+ /// 모든 도킹 오류를 초기화합니다.
+ ///
+ public void ClearDockingErrors()
+ {
+ _dockingErrors.Clear();
+ Invalidate();
+ }
+
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Models/Enums.cs b/Cs_HMI/AGVNavigationCore/Models/Enums.cs
index 7afe428..fb37ed2 100644
--- a/Cs_HMI/AGVNavigationCore/Models/Enums.cs
+++ b/Cs_HMI/AGVNavigationCore/Models/Enums.cs
@@ -2,6 +2,8 @@ using System;
namespace AGVNavigationCore.Models
{
+
+
///
/// 노드 타입 열거형
///
diff --git a/Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs b/Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs
deleted file mode 100644
index 169c0ab..0000000
--- a/Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System;
-
-namespace AGVNavigationCore.Models
-{
- ///
- /// RFID와 논리적 노드 ID를 매핑하는 클래스
- /// 물리적 RFID는 의미없는 고유값, 논리적 노드는 맵 에디터에서 관리
- ///
- public class RfidMapping
- {
- ///
- /// 물리적 RFID 값 (의미 없는 고유 식별자)
- /// 예: "1234567890", "ABCDEF1234" 등
- ///
- public string RfidId { get; set; } = string.Empty;
-
- ///
- /// 논리적 노드 ID (맵 에디터에서 관리)
- /// 예: "N001", "N002", "LOADER1", "CHARGER1" 등
- ///
- public string LogicalNodeId { get; set; } = string.Empty;
-
- ///
- /// 매핑 생성 일자
- ///
- public DateTime CreatedDate { get; set; } = DateTime.Now;
-
- ///
- /// 마지막 수정 일자
- ///
- public DateTime ModifiedDate { get; set; } = DateTime.Now;
-
- ///
- /// 설치 위치 설명 (현장 작업자용)
- /// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
- ///
- public string Description { get; set; } = string.Empty;
-
- ///
- /// RFID 상태 (정상, 손상, 교체예정 등)
- ///
- public string Status { get; set; } = "정상";
-
- ///
- /// 매핑 활성화 여부
- ///
- public bool IsActive { get; set; } = true;
-
- ///
- /// 기본 생성자
- ///
- public RfidMapping()
- {
- }
-
- ///
- /// 매개변수 생성자
- ///
- /// 물리적 RFID ID
- /// 논리적 노드 ID
- /// 설치 위치 설명
- public RfidMapping(string rfidId, string logicalNodeId, string description = "")
- {
- RfidId = rfidId;
- LogicalNodeId = logicalNodeId;
- Description = description;
- CreatedDate = DateTime.Now;
- ModifiedDate = DateTime.Now;
- }
-
- ///
- /// 문자열 표현
- ///
- public override string ToString()
- {
- return $"{RfidId} → {LogicalNodeId} ({Description})";
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
deleted file mode 100644
index 81e318e..0000000
--- a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
+++ /dev/null
@@ -1,862 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using AGVNavigationCore.Models;
-
-namespace AGVNavigationCore.PathFinding
-{
- ///
- /// AGV 특화 경로 탐색기 (방향성 및 도킹 제약 고려)
- ///
- public class AGVPathfinder
- {
- private AStarPathfinder _pathfinder;
- private Dictionary _nodeMap;
-
- ///
- /// AGV 현재 방향
- ///
- public AgvDirection CurrentDirection { get; set; } = AgvDirection.Forward;
-
- ///
- /// 회전 비용 가중치 (회전이 비싼 동작임을 반영)
- ///
- public float RotationCostWeight { get; set; } = 50.0f;
-
- ///
- /// 도킹 접근 거리 (픽셀 단위)
- ///
- public float DockingApproachDistance { get; set; } = 100.0f;
-
- ///
- /// 경로 탐색 옵션
- ///
- public PathfindingOptions Options { get; set; } = PathfindingOptions.Default;
-
- ///
- /// 생성자
- ///
- public AGVPathfinder()
- {
- _pathfinder = new AStarPathfinder();
- _nodeMap = new Dictionary();
- }
-
- ///
- /// 맵 노드 설정
- ///
- /// 맵 노드 목록
- public void SetMapNodes(List mapNodes)
- {
- _pathfinder.SetMapNodes(mapNodes);
- _nodeMap.Clear();
-
- foreach (var node in mapNodes ?? new List())
- {
- _nodeMap[node.NodeId] = node;
- }
- }
-
- ///
- /// AGV 경로 계산 (방향성 및 도킹 제약 고려)
- ///
- /// 시작 노드 ID
- /// 목적지 노드 ID
- /// 목적지 도착 방향 (null이면 자동 결정)
- /// AGV 경로 계산 결과
- public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection = null)
- {
- return FindAGVPath(startNodeId, endNodeId, targetDirection, Options);
- }
-
- ///
- /// AGV 경로 계산 (현재 방향 및 옵션 지정 가능)
- ///
- /// 시작 노드 ID
- /// 목적지 노드 ID
- /// 현재 AGV 방향
- /// 목적지 도착 방향 (null이면 자동 결정)
- /// 경로 탐색 옵션
- /// AGV 경로 계산 결과
- public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? currentDirection, AgvDirection? targetDirection, PathfindingOptions options)
- {
- var result = FindAGVPath(startNodeId, endNodeId, targetDirection, options);
-
- if (!result.Success || currentDirection == null || result.Commands.Count == 0)
- return result;
-
- // 경로의 첫 번째 방향과 현재 방향 비교
- var firstDirection = result.Commands[0];
-
- if (RequiresDirectionChange(currentDirection.Value, firstDirection))
- {
- return InsertDirectionChangeCommands(result, currentDirection.Value, firstDirection);
- }
-
- return result;
- }
-
- ///
- /// AGV 경로 계산 (옵션 지정 가능)
- ///
- /// 시작 노드 ID
- /// 목적지 노드 ID
- /// 목적지 도착 방향 (null이면 자동 결정)
- /// 경로 탐색 옵션
- /// AGV 경로 계산 결과
- public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, PathfindingOptions options)
- {
- var stopwatch = System.Diagnostics.Stopwatch.StartNew();
-
- try
- {
- if (!_nodeMap.ContainsKey(startNodeId))
- {
- return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds);
- }
-
- if (!_nodeMap.ContainsKey(endNodeId))
- {
- return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds);
- }
-
- var endNode = _nodeMap[endNodeId];
-
- if (IsSpecialNode(endNode))
- {
- return FindPathToSpecialNode(startNodeId, endNode, targetDirection, options, stopwatch);
- }
- else
- {
- return FindNormalPath(startNodeId, endNodeId, targetDirection, options, stopwatch);
- }
- }
- catch (Exception ex)
- {
- return AGVPathResult.CreateFailure($"AGV 경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds);
- }
- }
-
- ///
- /// 충전 스테이션으로의 경로 찾기
- ///
- /// 시작 노드 ID
- /// AGV 경로 계산 결과
- public AGVPathResult FindPathToChargingStation(string startNodeId)
- {
- var chargingStations = _nodeMap.Values
- .Where(n => n.Type == NodeType.Charging && n.IsActive)
- .Select(n => n.NodeId)
- .ToList();
-
- if (chargingStations.Count == 0)
- {
- return AGVPathResult.CreateFailure("사용 가능한 충전 스테이션이 없습니다", 0);
- }
-
- var nearestResult = _pathfinder.FindNearestPath(startNodeId, chargingStations);
- if (!nearestResult.Success)
- {
- return AGVPathResult.CreateFailure("충전 스테이션으로의 경로를 찾을 수 없습니다", nearestResult.CalculationTimeMs);
- }
-
- var targetNodeId = nearestResult.Path.Last();
- return FindAGVPath(startNodeId, targetNodeId, AgvDirection.Forward);
- }
-
- ///
- /// 특정 타입의 도킹 스테이션으로의 경로 찾기
- ///
- /// 시작 노드 ID
- /// 장비 타입
- /// AGV 경로 계산 결과
- public AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
- {
- var dockingStations = _nodeMap.Values
- .Where(n => n.Type == NodeType.Docking && n.StationType == stationType && n.IsActive)
- .Select(n => n.NodeId)
- .ToList();
-
- if (dockingStations.Count == 0)
- {
- return AGVPathResult.CreateFailure($"{stationType} 타입의 사용 가능한 도킹 스테이션이 없습니다", 0);
- }
-
- var nearestResult = _pathfinder.FindNearestPath(startNodeId, dockingStations);
- if (!nearestResult.Success)
- {
- return AGVPathResult.CreateFailure($"{stationType} 도킹 스테이션으로의 경로를 찾을 수 없습니다", nearestResult.CalculationTimeMs);
- }
-
- var targetNodeId = nearestResult.Path.Last();
- return FindAGVPath(startNodeId, targetNodeId, AgvDirection.Backward);
- }
-
- ///
- /// 일반 노드로의 경로 계산
- ///
- private AGVPathResult FindNormalPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, PathfindingOptions options, System.Diagnostics.Stopwatch stopwatch)
- {
- PathResult result;
-
- // 회전 회피 옵션이 활성화되어 있으면 회전 노드 회피 시도
- if (options.AvoidRotationNodes)
- {
- result = FindPathAvoidingRotation(startNodeId, endNodeId, options);
-
- // 회전 회피 경로를 찾지 못하면 일반 경로 계산
- if (!result.Success)
- {
- result = _pathfinder.FindPath(startNodeId, endNodeId);
- }
- }
- else
- {
- result = _pathfinder.FindPath(startNodeId, endNodeId);
- }
-
- if (!result.Success)
- {
- return AGVPathResult.CreateFailure(result.ErrorMessage, stopwatch.ElapsedMilliseconds);
- }
-
- var agvCommands = GenerateAGVCommands(result.Path, targetDirection ?? AgvDirection.Forward);
- var nodeMotorInfos = GenerateNodeMotorInfos(result.Path);
- return AGVPathResult.CreateSuccess(result.Path, agvCommands, nodeMotorInfos, result.TotalDistance, stopwatch.ElapsedMilliseconds);
- }
-
- ///
- /// 특수 노드(도킹/충전)로의 경로 계산
- ///
- private AGVPathResult FindPathToSpecialNode(string startNodeId, MapNode endNode, AgvDirection? targetDirection, PathfindingOptions options, System.Diagnostics.Stopwatch stopwatch)
- {
- var requiredDirection = GetRequiredDirectionForNode(endNode);
- var actualTargetDirection = targetDirection ?? requiredDirection;
-
- PathResult result;
-
- // 회전 회피 옵션이 활성화되어 있으면 회전 노드 회피 시도
- if (options.AvoidRotationNodes)
- {
- result = FindPathAvoidingRotation(startNodeId, endNode.NodeId, options);
-
- // 회전 회피 경로를 찾지 못하면 일반 경로 계산
- if (!result.Success)
- {
- result = _pathfinder.FindPath(startNodeId, endNode.NodeId);
- }
- }
- else
- {
- result = _pathfinder.FindPath(startNodeId, endNode.NodeId);
- }
-
- if (!result.Success)
- {
- return AGVPathResult.CreateFailure(result.ErrorMessage, stopwatch.ElapsedMilliseconds);
- }
-
- if (actualTargetDirection != requiredDirection)
- {
- return AGVPathResult.CreateFailure($"{endNode.NodeId}는 {requiredDirection} 방향으로만 접근 가능합니다", stopwatch.ElapsedMilliseconds);
- }
-
- var agvCommands = GenerateAGVCommands(result.Path, actualTargetDirection);
- var nodeMotorInfos = GenerateNodeMotorInfos(result.Path);
- return AGVPathResult.CreateSuccess(result.Path, agvCommands, nodeMotorInfos, result.TotalDistance, stopwatch.ElapsedMilliseconds);
- }
-
- ///
- /// 노드가 특수 노드(도킹/충전)인지 확인
- ///
- private bool IsSpecialNode(MapNode node)
- {
- return node.Type == NodeType.Docking || node.Type == NodeType.Charging;
- }
-
- ///
- /// 노드에 필요한 접근 방향 반환
- ///
- private AgvDirection GetRequiredDirectionForNode(MapNode node)
- {
- switch (node.Type)
- {
- case NodeType.Charging:
- return AgvDirection.Forward;
- case NodeType.Docking:
- return node.DockDirection == DockingDirection.Forward ? AgvDirection.Forward : AgvDirection.Backward;
- default:
- return AgvDirection.Forward;
- }
- }
-
- ///
- /// 경로에서 AGV 명령어 생성
- ///
- private List GenerateAGVCommands(List path, AgvDirection targetDirection)
- {
- var commands = new List();
- if (path.Count < 2) return commands;
-
- var currentDir = CurrentDirection;
-
- for (int i = 0; i < path.Count - 1; i++)
- {
- var currentNodeId = path[i];
- var nextNodeId = path[i + 1];
-
- if (_nodeMap.ContainsKey(currentNodeId) && _nodeMap.ContainsKey(nextNodeId))
- {
- var currentNode = _nodeMap[currentNodeId];
- var nextNode = _nodeMap[nextNodeId];
-
- if (currentNode.CanRotate && ShouldRotate(currentDir, targetDirection))
- {
- commands.Add(GetRotationCommand(currentDir, targetDirection));
- currentDir = targetDirection;
- }
-
- commands.Add(currentDir);
- }
- }
-
- return commands;
- }
-
- ///
- /// 회전이 필요한지 판단
- ///
- private bool ShouldRotate(AgvDirection current, AgvDirection target)
- {
- return current != target && (current == AgvDirection.Forward && target == AgvDirection.Backward ||
- current == AgvDirection.Backward && target == AgvDirection.Forward);
- }
-
- ///
- /// 회전 명령어 반환
- ///
- private AgvDirection GetRotationCommand(AgvDirection from, AgvDirection to)
- {
- if (from == AgvDirection.Forward && to == AgvDirection.Backward)
- return AgvDirection.Right;
- if (from == AgvDirection.Backward && to == AgvDirection.Forward)
- return AgvDirection.Right;
-
- return AgvDirection.Right;
- }
-
- ///
- /// 노드별 모터방향 정보 생성 (방향 전환 로직 개선)
- ///
- /// 경로 노드 목록
- /// 노드별 모터방향 정보 목록
- private List GenerateNodeMotorInfos(List path)
- {
- var nodeMotorInfos = new List();
- if (path.Count < 2) return nodeMotorInfos;
-
- // 전체 경로에 대한 방향 전환 계획 수립
- var directionPlan = PlanDirectionChanges(path);
-
- for (int i = 0; i < path.Count; i++)
- {
- var currentNodeId = path[i];
- string nextNodeId = i < path.Count - 1 ? path[i + 1] : null;
-
- // 계획된 방향 사용
- var motorDirection = directionPlan.ContainsKey(currentNodeId)
- ? directionPlan[currentNodeId]
- : AgvDirection.Forward;
-
- // 노드 특성 정보 수집
- bool canRotate = false;
- bool isDirectionChangePoint = false;
- bool requiresSpecialAction = false;
- string specialActionDescription = "";
-
- if (_nodeMap.ContainsKey(currentNodeId))
- {
- var currentNode = _nodeMap[currentNodeId];
- canRotate = currentNode.CanRotate;
-
- // 방향 전환 감지
- if (i > 0 && directionPlan.ContainsKey(path[i - 1]))
- {
- var prevDirection = directionPlan[path[i - 1]];
- isDirectionChangePoint = prevDirection != motorDirection;
- }
-
- // 특수 동작 필요 여부 감지
- if (!canRotate && isDirectionChangePoint)
- {
- requiresSpecialAction = true;
- specialActionDescription = "갈림길 전진/후진 반복";
- }
- else if (canRotate && isDirectionChangePoint)
- {
- specialActionDescription = "회전 노드 방향전환";
- }
- }
-
- var nodeMotorInfo = new NodeMotorInfo(currentNodeId, motorDirection, nextNodeId,
- canRotate, isDirectionChangePoint, MagnetDirection.Straight, requiresSpecialAction, specialActionDescription);
- nodeMotorInfos.Add(nodeMotorInfo);
- }
-
- return nodeMotorInfos;
- }
-
- ///
- /// 경로 전체에 대한 방향 전환 계획 수립
- ///
- /// 경로 노드 목록
- /// 노드별 모터 방향 계획
- private Dictionary PlanDirectionChanges(List path)
- {
- var directionPlan = new Dictionary();
- if (path.Count < 2) return directionPlan;
-
- // 1단계: 목적지 노드의 요구사항 분석
- var targetNodeId = path[path.Count - 1];
- var targetDirection = AgvDirection.Forward;
-
- if (_nodeMap.ContainsKey(targetNodeId))
- {
- var targetNode = _nodeMap[targetNodeId];
- targetDirection = GetRequiredDirectionForNode(targetNode);
- }
-
- // 2단계: 역방향으로 방향 전환점 찾기
- var currentDirection = targetDirection;
- directionPlan[targetNodeId] = currentDirection;
-
- // 마지막에서 두 번째부터 역순으로 처리
- for (int i = path.Count - 2; i >= 0; i--)
- {
- var currentNodeId = path[i];
- var nextNodeId = path[i + 1];
-
- if (_nodeMap.ContainsKey(currentNodeId))
- {
- var currentNode = _nodeMap[currentNodeId];
-
- // 방법1: 회전 가능 노드에서 방향 전환
- if (currentNode.CanRotate && NeedDirectionChange(currentNodeId, nextNodeId, currentDirection))
- {
- // 회전 가능한 노드에서 방향 전환 수행
- var optimalDirection = CalculateOptimalDirection(currentNodeId, nextNodeId, path, i);
- directionPlan[currentNodeId] = optimalDirection;
- currentDirection = optimalDirection;
- }
- else
- {
- // 일반 노드: 연속성 유지
- directionPlan[currentNodeId] = currentDirection;
- }
- }
- else
- {
- directionPlan[currentNodeId] = currentDirection;
- }
- }
-
- // 3단계: 방법2 적용 - 불가능한 방향 전환 감지 및 수정
- ApplyDirectionChangeCorrection(path, directionPlan);
-
- return directionPlan;
- }
-
- ///
- /// 방향 전환이 필요한지 판단
- ///
- private bool NeedDirectionChange(string currentNodeId, string nextNodeId, AgvDirection currentDirection)
- {
- if (!_nodeMap.ContainsKey(nextNodeId)) return false;
-
- var nextNode = _nodeMap[nextNodeId];
- var requiredDirection = GetRequiredDirectionForNode(nextNode);
-
- return currentDirection != requiredDirection;
- }
-
- ///
- /// 회전 가능 노드에서 최적 방향 계산
- ///
- private AgvDirection CalculateOptimalDirection(string nodeId, string nextNodeId, List path, int nodeIndex)
- {
- if (!_nodeMap.ContainsKey(nextNodeId)) return AgvDirection.Forward;
-
- var nextNode = _nodeMap[nextNodeId];
-
- // 목적지까지의 경로를 고려한 최적 방향 결정
- if (nextNode.Type == NodeType.Charging)
- {
- return AgvDirection.Forward; // 충전기는 전진 접근
- }
- else if (nextNode.Type == NodeType.Docking)
- {
- return AgvDirection.Backward; // 도킹은 후진 접근
- }
- else
- {
- // 경로 패턴 분석을 통한 방향 결정
- return AnalyzePathPattern(path, nodeIndex);
- }
- }
-
- ///
- /// 경로 패턴 분석을 통한 방향 결정
- ///
- private AgvDirection AnalyzePathPattern(List path, int startIndex)
- {
- // 남은 경로에서 도킹/충전 스테이션이 있는지 확인
- for (int i = startIndex + 1; i < path.Count; i++)
- {
- if (_nodeMap.ContainsKey(path[i]))
- {
- var node = _nodeMap[path[i]];
- if (node.Type == NodeType.Docking)
- {
- return AgvDirection.Backward; // 도킹 준비를 위해 후진 방향
- }
- else if (node.Type == NodeType.Charging)
- {
- return AgvDirection.Forward; // 충전 준비를 위해 전진 방향
- }
- }
- }
-
- return AgvDirection.Forward; // 기본값
- }
-
- ///
- /// 방법2: 불가능한 방향 전환 감지 및 보정 (갈림길 전진/후진 반복)
- ///
- private void ApplyDirectionChangeCorrection(List path, Dictionary directionPlan)
- {
- for (int i = 0; i < path.Count - 1; i++)
- {
- var currentNodeId = path[i];
- var nextNodeId = path[i + 1];
-
- if (directionPlan.ContainsKey(currentNodeId) && directionPlan.ContainsKey(nextNodeId))
- {
- var currentDir = directionPlan[currentNodeId];
- var nextDir = directionPlan[nextNodeId];
-
- // 급격한 방향 전환 감지 (전진→후진, 후진→전진)
- if (IsImpossibleDirectionChange(currentDir, nextDir))
- {
- var currentNode = _nodeMap.ContainsKey(currentNodeId) ? _nodeMap[currentNodeId] : null;
-
- if (currentNode != null && !currentNode.CanRotate)
- {
- // 회전 불가능한 노드에서 방향 전환 시도 → 특수 동작 추가
- AddTurnAroundSequence(currentNodeId, directionPlan);
- }
- }
- }
- }
- }
-
- ///
- /// 불가능한 방향 전환인지 확인
- ///
- private bool IsImpossibleDirectionChange(AgvDirection current, AgvDirection next)
- {
- return (current == AgvDirection.Forward && next == AgvDirection.Backward) ||
- (current == AgvDirection.Backward && next == AgvDirection.Forward);
- }
-
- ///
- /// 회전 불가능한 노드에서 방향 전환을 위한 특수 동작 시퀀스 추가
- ///
- private void AddTurnAroundSequence(string nodeId, Dictionary directionPlan)
- {
- // 갈림길에서 전진/후진 반복 작업으로 방향 변경
- // 실제 구현시에는 NodeMotorInfo에 특수 플래그나 추가 동작 정보 포함 필요
- // 현재는 안전한 방향으로 보정
- if (directionPlan.ContainsKey(nodeId))
- {
- // 일단 연속성을 유지하도록 보정 (실제로는 특수 회전 동작 필요)
- directionPlan[nodeId] = AgvDirection.Forward;
- }
- }
-
- ///
- /// 현재 노드에서 다음 노드로 이동할 때의 모터방향 계산 (레거시 - 새로운 PlanDirectionChanges 사용)
- ///
- /// 현재 노드 ID
- /// 다음 노드 ID
- /// 모터방향
- private AgvDirection CalculateMotorDirection(string currentNodeId, string nextNodeId)
- {
- if (!_nodeMap.ContainsKey(currentNodeId) || !_nodeMap.ContainsKey(nextNodeId))
- {
- return AgvDirection.Forward;
- }
-
- var nextNode = _nodeMap[nextNodeId];
-
- // 다음 노드가 특수 노드인지 확인
- if (nextNode.Type == NodeType.Charging)
- {
- // 충전기: 전진으로 도킹
- return AgvDirection.Forward;
- }
- else if (nextNode.Type == NodeType.Docking)
- {
- // 도킹 스테이션: 후진으로 도킹
- return AgvDirection.Backward;
- }
- else
- {
- // 일반 이동: 기본적으로 전진 (실제로는 PlanDirectionChanges에서 결정됨)
- return AgvDirection.Forward;
- }
- }
-
- ///
- /// 회전 노드를 회피하는 경로 탐색
- ///
- /// 시작 노드 ID
- /// 목적지 노드 ID
- /// 경로 탐색 옵션
- /// 회전 회피 경로 결과
- private PathResult FindPathAvoidingRotation(string startNodeId, string endNodeId, PathfindingOptions options)
- {
- try
- {
- // 회전 가능한 노드들을 필터링하여 임시로 비활성화
- var rotationNodes = _nodeMap.Values.Where(n => n.CanRotate).Select(n => n.NodeId).ToList();
- var originalConnections = new Dictionary>();
-
- // 시작과 끝 노드가 회전 노드인 경우는 제외
- var nodesToDisable = rotationNodes.Where(nodeId => nodeId != startNodeId && nodeId != endNodeId).ToList();
-
- foreach (var nodeId in nodesToDisable)
- {
- // 임시로 연결 해제 (실제로는 pathfinder에서 해당 노드를 높은 비용으로 설정해야 함)
- originalConnections[nodeId] = _nodeMap[nodeId].ConnectedNodes.ToList();
- }
-
- // 회전 노드 회피 경로 계산 시도
- var result = _pathfinder.FindPath(startNodeId, endNodeId);
-
- // 결과에서 회전 노드가 포함되어 있으면 실패로 간주
- if (result.Success && result.Path != null)
- {
- var pathContainsRotation = result.Path.Any(nodeId => nodesToDisable.Contains(nodeId));
- if (pathContainsRotation)
- {
- return PathResult.CreateFailure("회전 노드를 회피하는 경로를 찾을 수 없습니다.", 0, 0);
- }
- }
-
- return result;
- }
- catch (Exception ex)
- {
- return PathResult.CreateFailure($"회전 회피 경로 계산 중 오류: {ex.Message}", 0, 0);
- }
- }
-
- ///
- /// 경로 유효성 검증
- ///
- /// 검증할 경로
- /// 유효성 검증 결과
- public bool ValidatePath(List path)
- {
- if (path == null || path.Count < 2) return true;
-
- for (int i = 0; i < path.Count - 1; i++)
- {
- if (!_pathfinder.AreNodesConnected(path[i], path[i + 1]))
- {
- return false;
- }
- }
-
- return true;
- }
-
- ///
- /// 현재 방향에서 목표 방향으로 변경하기 위해 방향 전환이 필요한지 판단
- ///
- /// 현재 방향
- /// 목표 방향
- /// 방향 전환 필요 여부
- private bool RequiresDirectionChange(AgvDirection currentDirection, AgvDirection targetDirection)
- {
- // 같은 방향이면 변경 불필요
- if (currentDirection == targetDirection)
- return false;
-
- // 전진 <-> 후진 전환은 방향 전환 필요
- return (currentDirection == AgvDirection.Forward && targetDirection == AgvDirection.Backward) ||
- (currentDirection == AgvDirection.Backward && targetDirection == AgvDirection.Forward);
- }
-
- ///
- /// 방향 전환을 위한 명령어를 기존 경로에 삽입
- ///
- /// 원래 경로 결과
- /// 현재 AGV 방향
- /// 목표 방향
- /// 방향 전환 명령이 포함된 경로 결과
- private AGVPathResult InsertDirectionChangeCommands(AGVPathResult originalResult, AgvDirection currentDirection, AgvDirection targetDirection)
- {
- if (originalResult.Path.Count < 1)
- return originalResult;
-
- // 시작 노드를 찾아서 회전 가능한지 확인
- var startNodeId = originalResult.Path[0];
- if (!_nodeMap.ContainsKey(startNodeId))
- return originalResult;
-
- var startNode = _nodeMap[startNodeId];
-
- // 방향 전환을 위한 가장 가까운 교차로 찾기
- var targetNodeId = originalResult.Path.LastOrDefault();
- var junctionNode = FindNearestJunctionForDirectionChange(startNodeId, targetNodeId);
- if (junctionNode == null)
- {
- return AGVPathResult.CreateFailure("방향 전환을 위한 교차로를 찾을 수 없습니다.", originalResult.CalculationTimeMs);
- }
-
- // 교차로로의 경로 추가
- var pathToJunction = _pathfinder.FindPath(startNodeId, junctionNode.NodeId);
- if (!pathToJunction.Success)
- {
- return AGVPathResult.CreateFailure("방향 전환 교차로로의 경로를 찾을 수 없습니다.", originalResult.CalculationTimeMs);
- }
-
- // 교차로에서 원래 목적지로의 경로 계산
- var pathFromJunction = _pathfinder.FindPath(junctionNode.NodeId, originalResult.Path.Last());
- if (!pathFromJunction.Success)
- {
- return AGVPathResult.CreateFailure("방향 전환 후 목적지로의 경로를 찾을 수 없습니다.", originalResult.CalculationTimeMs);
- }
-
- // 전체 경로 조합
- var combinedPath = new List();
- combinedPath.AddRange(pathToJunction.Path);
- combinedPath.AddRange(pathFromJunction.Path.Skip(1)); // 중복 노드 제거
-
- var combinedDistance = pathToJunction.TotalDistance + pathFromJunction.TotalDistance;
- var combinedCommands = GenerateAGVCommandsWithDirectionChange(combinedPath, currentDirection, targetDirection, junctionNode.NodeId);
- var nodeMotorInfos = GenerateNodeMotorInfos(combinedPath);
-
- return AGVPathResult.CreateSuccess(combinedPath, combinedCommands, nodeMotorInfos, combinedDistance, originalResult.CalculationTimeMs);
- }
-
- ///
- /// 방향 전환을 위한 가장 가까운 교차로 찾기
- ///
- /// 시작 노드 ID
- /// 목적지 노드 ID (경로상 교차로 우선 검색용)
- /// 가장 가까운 교차로 노드
- private MapNode FindNearestJunctionForDirectionChange(string fromNodeId, string targetNodeId = null)
- {
- // 1단계: 시작점에서 목적지까지 직접 경로상의 교차로 우선 검색
- if (!string.IsNullOrEmpty(targetNodeId))
- {
- var directPath = _pathfinder.FindPath(fromNodeId, targetNodeId);
- if (directPath.Success)
- {
- foreach (var nodeId in directPath.Path)
- {
- var node = _nodeMap.ContainsKey(nodeId) ? _nodeMap[nodeId] : null;
- if (node != null && IsJunction(node))
- {
- return node;
- }
- }
- }
- }
-
- // 2단계: 경로상에 교차로가 없으면 거리가 가장 가까운 교차로 찾기
- var junctionNodes = _nodeMap.Values.Where(n => n.IsActive && IsJunction(n)).ToList();
-
- MapNode nearestNode = null;
- var shortestDistance = float.MaxValue;
-
- foreach (var junctionNode in junctionNodes)
- {
- var pathResult = _pathfinder.FindPath(fromNodeId, junctionNode.NodeId);
- if (pathResult.Success && pathResult.TotalDistance < shortestDistance)
- {
- shortestDistance = pathResult.TotalDistance;
- nearestNode = junctionNode;
- }
- }
-
- return nearestNode;
- }
-
- ///
- /// 노드가 교차로인지 판단 (3개 이상 연결된 노드)
- ///
- /// 검사할 노드
- /// 교차로이면 true
- private bool IsJunction(MapNode node)
- {
- // 연결된 노드 수 계산 (단방향 + 양방향)
- var connectedCount = node.ConnectedNodes.Count;
-
- // 역방향 연결도 고려
- foreach (var otherNode in _nodeMap.Values)
- {
- if (otherNode.NodeId != node.NodeId && otherNode.ConnectedNodes.Contains(node.NodeId))
- {
- connectedCount++;
- }
- }
-
- // 3개 이상 연결되어 있으면 교차로
- return connectedCount >= 3;
- }
-
- ///
- /// 방향 전환을 포함한 AGV 명령어 생성
- ///
- /// 경로
- /// 현재 방향
- /// 목표 방향
- /// 방향 전환 노드 ID
- /// AGV 명령어 목록
- private List GenerateAGVCommandsWithDirectionChange(List path, AgvDirection currentDirection, AgvDirection targetDirection, string rotationNodeId)
- {
- var commands = new List();
-
- int rotationIndex = path.IndexOf(rotationNodeId);
-
- // 회전 노드까지는 현재 방향으로 이동
- for (int i = 0; i < rotationIndex; i++)
- {
- commands.Add(currentDirection);
- }
-
- // 회전 명령 추가 (전진->후진 또는 후진->전진)
- if (currentDirection == AgvDirection.Forward && targetDirection == AgvDirection.Backward)
- {
- commands.Add(AgvDirection.Left);
- commands.Add(AgvDirection.Left); // 180도 회전
- }
- else if (currentDirection == AgvDirection.Backward && targetDirection == AgvDirection.Forward)
- {
- commands.Add(AgvDirection.Right);
- commands.Add(AgvDirection.Right); // 180도 회전
- }
-
- // 회전 노드부터 목적지까지는 목표 방향으로 이동
- for (int i = rotationIndex; i < path.Count - 1; i++)
- {
- commands.Add(targetDirection);
- }
-
- return commands;
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/JunctionAnalyzer.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Analysis/JunctionAnalyzer.cs
similarity index 99%
rename from Cs_HMI/AGVNavigationCore/PathFinding/JunctionAnalyzer.cs
rename to Cs_HMI/AGVNavigationCore/PathFinding/Analysis/JunctionAnalyzer.cs
index 5e24115..f9eb669 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/JunctionAnalyzer.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/Analysis/JunctionAnalyzer.cs
@@ -3,8 +3,9 @@ using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding.Planning;
-namespace AGVNavigationCore.PathFinding
+namespace AGVNavigationCore.PathFinding.Analysis
{
///
/// AGV 갈림길 분석 및 마그넷 센서 방향 계산 시스템
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs
similarity index 68%
rename from Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
rename to Cs_HMI/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs
index 1bd268d..bc8ceaa 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs
@@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding.Planning;
+using AGVNavigationCore.PathFinding.Validation;
-namespace AGVNavigationCore.PathFinding
+namespace AGVNavigationCore.PathFinding.Core
{
///
/// AGV 경로 계산 결과 (방향성 및 명령어 포함)
@@ -43,7 +45,16 @@ namespace AGVNavigationCore.PathFinding
///
/// 탐색된 노드 수
///
- public int ExploredNodes { get; set; }
+ public int ExploredNodeCount { get; set; }
+
+ ///
+ /// 탐색된 노드 수 (호환성용)
+ ///
+ public int ExploredNodes
+ {
+ get => ExploredNodeCount;
+ set => ExploredNodeCount = value;
+ }
///
/// 예상 소요 시간 (초)
@@ -60,6 +71,31 @@ namespace AGVNavigationCore.PathFinding
///
public string ErrorMessage { get; set; }
+ ///
+ /// 도킹 검증 결과
+ ///
+ public DockingValidationResult DockingValidation { get; set; }
+
+ ///
+ /// 상세 경로 정보 (NodeMotorInfo 목록)
+ ///
+ public List DetailedPath { get; set; }
+
+ ///
+ /// 계획 설명
+ ///
+ public string PlanDescription { get; set; }
+
+ ///
+ /// 방향 전환 필요 여부
+ ///
+ public bool RequiredDirectionChange { get; set; }
+
+ ///
+ /// 방향 전환 노드 ID
+ ///
+ public string DirectionChangeNode { get; set; }
+
///
/// 기본 생성자
///
@@ -69,12 +105,17 @@ namespace AGVNavigationCore.PathFinding
Path = new List();
Commands = new List();
NodeMotorInfos = new List();
+ DetailedPath = new List();
TotalDistance = 0;
CalculationTimeMs = 0;
ExploredNodes = 0;
EstimatedTimeSeconds = 0;
RotationCount = 0;
ErrorMessage = string.Empty;
+ PlanDescription = string.Empty;
+ RequiredDirectionChange = false;
+ DirectionChangeNode = string.Empty;
+ DockingValidation = DockingValidationResult.CreateNotRequired();
}
///
@@ -141,6 +182,56 @@ namespace AGVNavigationCore.PathFinding
};
}
+ ///
+ /// 실패 결과 생성 (확장)
+ ///
+ /// 오류 메시지
+ /// 계산 시간
+ /// 탐색된 노드 수
+ /// 실패 결과
+ public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs, int exploredNodes)
+ {
+ return new AGVPathResult
+ {
+ Success = false,
+ ErrorMessage = errorMessage,
+ CalculationTimeMs = calculationTimeMs,
+ ExploredNodes = exploredNodes
+ };
+ }
+
+ ///
+ /// 성공 결과 생성 (상세 경로용)
+ ///
+ /// 상세 경로
+ /// 총 거리
+ /// 계산 시간
+ /// 탐색된 노드 수
+ /// 계획 설명
+ /// 방향 전환 여부
+ /// 방향 전환 노드
+ /// 성공 결과
+ public static AGVPathResult CreateSuccess(List detailedPath, float totalDistance, long calculationTimeMs, int exploredNodes, string planDescription, bool directionChange = false, string changeNode = null)
+ {
+ var path = detailedPath?.Select(n => n.NodeId).ToList() ?? new List();
+
+ var result = new AGVPathResult
+ {
+ Success = true,
+ Path = path,
+ DetailedPath = detailedPath ?? new List(),
+ TotalDistance = totalDistance,
+ CalculationTimeMs = calculationTimeMs,
+ ExploredNodes = exploredNodes,
+ PlanDescription = planDescription ?? string.Empty,
+ RequiredDirectionChange = directionChange,
+ DirectionChangeNode = changeNode ?? string.Empty
+ };
+
+ result.CalculateMetrics();
+ return result;
+ }
+
///
/// 경로 메트릭 계산
///
@@ -248,20 +339,18 @@ namespace AGVNavigationCore.PathFinding
$"계산시간: {CalculationTimeMs}ms";
}
+
///
- /// PathResult로 변환 (호환성을 위해)
+ /// 단순 경로 목록 반환 (호환성용)
///
- /// PathResult 객체
- public PathResult ToPathResult()
+ /// 노드 ID 목록
+ public List GetSimplePath()
{
- if (Success)
+ if (DetailedPath != null && DetailedPath.Count > 0)
{
- return PathResult.CreateSuccess(Path, TotalDistance, CalculationTimeMs, 0);
- }
- else
- {
- return PathResult.CreateFailure(ErrorMessage, CalculationTimeMs, 0);
+ return DetailedPath.Select(n => n.NodeId).ToList();
}
+ return Path ?? new List();
}
///
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs
similarity index 87%
rename from Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs
rename to Cs_HMI/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs
index 3bb1547..2c59887 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs
@@ -4,7 +4,7 @@ using System.Drawing;
using System.Linq;
using AGVNavigationCore.Models;
-namespace AGVNavigationCore.PathFinding
+namespace AGVNavigationCore.PathFinding.Core
{
///
/// A* 알고리즘 기반 경로 탐색기
@@ -87,7 +87,7 @@ namespace AGVNavigationCore.PathFinding
/// 시작 노드 ID
/// 목적지 노드 ID
/// 경로 계산 결과
- public PathResult FindPath(string startNodeId, string endNodeId)
+ public AGVPathResult FindPath(string startNodeId, string endNodeId)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -95,17 +95,18 @@ namespace AGVNavigationCore.PathFinding
{
if (!_nodeMap.ContainsKey(startNodeId))
{
- return PathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds, 0);
+ return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds, 0);
}
if (!_nodeMap.ContainsKey(endNodeId))
{
- return PathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds, 0);
+ return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds, 0);
}
if (startNodeId == endNodeId)
{
- return PathResult.CreateSuccess(new List { startNodeId }, 0, stopwatch.ElapsedMilliseconds, 1);
+ var singlePath = new List { startNodeId };
+ return AGVPathResult.CreateSuccess(singlePath, new List(), 0, stopwatch.ElapsedMilliseconds);
}
var startNode = _nodeMap[startNodeId];
@@ -131,7 +132,7 @@ namespace AGVNavigationCore.PathFinding
{
var path = ReconstructPath(currentNode);
var totalDistance = CalculatePathDistance(path);
- return PathResult.CreateSuccess(path, totalDistance, stopwatch.ElapsedMilliseconds, exploredCount);
+ return AGVPathResult.CreateSuccess(path, new List(), totalDistance, stopwatch.ElapsedMilliseconds);
}
foreach (var neighborId in currentNode.ConnectedNodes)
@@ -157,11 +158,11 @@ namespace AGVNavigationCore.PathFinding
}
}
- return PathResult.CreateFailure("경로를 찾을 수 없습니다", stopwatch.ElapsedMilliseconds, exploredCount);
+ return AGVPathResult.CreateFailure("경로를 찾을 수 없습니다", stopwatch.ElapsedMilliseconds, exploredCount);
}
catch (Exception ex)
{
- return PathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
+ return AGVPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
}
}
@@ -171,14 +172,14 @@ namespace AGVNavigationCore.PathFinding
/// 시작 노드 ID
/// 목적지 후보 노드 ID 목록
/// 경로 계산 결과
- public PathResult FindNearestPath(string startNodeId, List targetNodeIds)
+ public AGVPathResult FindNearestPath(string startNodeId, List targetNodeIds)
{
if (targetNodeIds == null || targetNodeIds.Count == 0)
{
- return PathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0);
+ return AGVPathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0);
}
- PathResult bestResult = null;
+ AGVPathResult bestResult = null;
foreach (var targetId in targetNodeIds)
{
var result = FindPath(startNodeId, targetId);
@@ -188,7 +189,7 @@ namespace AGVNavigationCore.PathFinding
}
}
- return bestResult ?? PathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0);
+ return bestResult ?? AGVPathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0);
}
///
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Core/PathNode.cs
similarity index 98%
rename from Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs
rename to Cs_HMI/AGVNavigationCore/PathFinding/Core/PathNode.cs
index 0e668d2..29ccb0a 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/Core/PathNode.cs
@@ -1,7 +1,7 @@
using System;
using System.Drawing;
-namespace AGVNavigationCore.PathFinding
+namespace AGVNavigationCore.PathFinding.Core
{
///
/// A* 알고리즘에서 사용하는 경로 노드
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs
deleted file mode 100644
index aa58593..0000000
--- a/Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace AGVNavigationCore.PathFinding
-{
- ///
- /// 경로 계산 결과
- ///
- public class PathResult
- {
- ///
- /// 경로 찾기 성공 여부
- ///
- public bool Success { get; set; }
-
- ///
- /// 경로 노드 ID 목록 (시작 → 목적지 순서)
- ///
- public List Path { get; set; }
-
- ///
- /// 총 거리
- ///
- public float TotalDistance { get; set; }
-
- ///
- /// 계산 소요 시간 (밀리초)
- ///
- public long CalculationTimeMs { get; set; }
-
- ///
- /// 탐색한 노드 수
- ///
- public int ExploredNodeCount { get; set; }
-
- ///
- /// 오류 메시지 (실패시)
- ///
- public string ErrorMessage { get; set; }
-
- ///
- /// 기본 생성자
- ///
- public PathResult()
- {
- Success = false;
- Path = new List();
- TotalDistance = 0;
- CalculationTimeMs = 0;
- ExploredNodeCount = 0;
- ErrorMessage = string.Empty;
- }
-
- ///
- /// 성공 결과 생성
- ///
- /// 경로
- /// 총 거리
- /// 계산 시간
- /// 탐색 노드 수
- /// 성공 결과
- public static PathResult CreateSuccess(List path, float totalDistance, long calculationTimeMs, int exploredNodeCount)
- {
- return new PathResult
- {
- Success = true,
- Path = new List(path),
- TotalDistance = totalDistance,
- CalculationTimeMs = calculationTimeMs,
- ExploredNodeCount = exploredNodeCount
- };
- }
-
- ///
- /// 실패 결과 생성
- ///
- /// 오류 메시지
- /// 계산 시간
- /// 탐색 노드 수
- /// 실패 결과
- public static PathResult CreateFailure(string errorMessage, long calculationTimeMs, int exploredNodeCount)
- {
- return new PathResult
- {
- Success = false,
- ErrorMessage = errorMessage,
- CalculationTimeMs = calculationTimeMs,
- ExploredNodeCount = exploredNodeCount
- };
- }
-
- ///
- /// 문자열 표현
- ///
- public override string ToString()
- {
- if (Success)
- {
- return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {CalculationTimeMs}ms";
- }
- else
- {
- return $"Failed: {ErrorMessage}, {CalculationTimeMs}ms";
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/PathfindingOptions.cs b/Cs_HMI/AGVNavigationCore/PathFinding/PathfindingOptions.cs
deleted file mode 100644
index 6399874..0000000
--- a/Cs_HMI/AGVNavigationCore/PathFinding/PathfindingOptions.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using System;
-
-namespace AGVNavigationCore.PathFinding
-{
- ///
- /// 경로 탐색 옵션 설정
- ///
- public class PathfindingOptions
- {
- ///
- /// 회전 가능 노드 회피 여부 (기본값: false - 회전 허용)
- ///
- public bool AvoidRotationNodes { get; set; } = false;
-
- ///
- /// 회전 회피 시 추가 비용 가중치 (회전 노드를 완전 차단하지 않고 높은 비용으로 설정)
- ///
- public float RotationAvoidanceCost { get; set; } = 1000.0f;
-
- ///
- /// 회전 비용 가중치 (기존 회전 비용)
- ///
- public float RotationCostWeight { get; set; } = 50.0f;
-
- ///
- /// 도킹 접근 거리
- ///
- public float DockingApproachDistance { get; set; } = 100.0f;
-
- ///
- /// 기본 옵션 생성
- ///
- public static PathfindingOptions Default => new PathfindingOptions();
-
- ///
- /// 회전 회피 옵션 생성
- ///
- public static PathfindingOptions AvoidRotation => new PathfindingOptions
- {
- AvoidRotationNodes = true
- };
-
- ///
- /// 옵션 복사
- ///
- public PathfindingOptions Clone()
- {
- return new PathfindingOptions
- {
- AvoidRotationNodes = this.AvoidRotationNodes,
- RotationAvoidanceCost = this.RotationAvoidanceCost,
- RotationCostWeight = this.RotationCostWeight,
- DockingApproachDistance = this.DockingApproachDistance
- };
- }
-
- ///
- /// 설정 정보 문자열
- ///
- public override string ToString()
- {
- return $"회전회피: {(AvoidRotationNodes ? "ON" : "OFF")}, " +
- $"회전비용: {RotationCostWeight}, " +
- $"회피비용: {RotationAvoidanceCost}";
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AdvancedAGVPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs
similarity index 76%
rename from Cs_HMI/AGVNavigationCore/PathFinding/AdvancedAGVPathfinder.cs
rename to Cs_HMI/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs
index ab3d79a..0636805 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/AdvancedAGVPathfinder.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs
@@ -2,78 +2,25 @@ using System;
using System.Collections.Generic;
using System.Linq;
using AGVNavigationCore.Models;
+using AGVNavigationCore.Utils;
+using AGVNavigationCore.PathFinding.Core;
+using AGVNavigationCore.PathFinding.Analysis;
-namespace AGVNavigationCore.PathFinding
+namespace AGVNavigationCore.PathFinding.Planning
{
///
- /// 고급 AGV 경로 계획기
+ /// AGV 경로 계획기
/// 물리적 제약사항과 마그넷 센서를 고려한 실제 AGV 경로 생성
///
- public class AdvancedAGVPathfinder
+ public class AGVPathfinder
{
- ///
- /// 고급 AGV 경로 계산 결과
- ///
- public class AdvancedPathResult
- {
- public bool Success { get; set; }
- public List DetailedPath { get; set; }
- public float TotalDistance { get; set; }
- public long CalculationTimeMs { get; set; }
- public int ExploredNodeCount { get; set; }
- public string ErrorMessage { get; set; }
- public string PlanDescription { get; set; }
- public bool RequiredDirectionChange { get; set; }
- public string DirectionChangeNode { get; set; }
-
- public AdvancedPathResult()
- {
- DetailedPath = new List();
- ErrorMessage = string.Empty;
- PlanDescription = string.Empty;
- }
-
- public static AdvancedPathResult CreateSuccess(List path, float distance, long time, int explored, string description, bool directionChange = false, string changeNode = null)
- {
- return new AdvancedPathResult
- {
- Success = true,
- DetailedPath = path,
- TotalDistance = distance,
- CalculationTimeMs = time,
- ExploredNodeCount = explored,
- PlanDescription = description,
- RequiredDirectionChange = directionChange,
- DirectionChangeNode = changeNode
- };
- }
-
- public static AdvancedPathResult CreateFailure(string error, long time, int explored)
- {
- return new AdvancedPathResult
- {
- Success = false,
- ErrorMessage = error,
- CalculationTimeMs = time,
- ExploredNodeCount = explored
- };
- }
-
- ///
- /// 단순 경로 목록 반환 (호환성용)
- ///
- public List GetSimplePath()
- {
- return DetailedPath.Select(n => n.NodeId).ToList();
- }
- }
private readonly List _mapNodes;
private readonly AStarPathfinder _basicPathfinder;
private readonly JunctionAnalyzer _junctionAnalyzer;
private readonly DirectionChangePlanner _directionChangePlanner;
- public AdvancedAGVPathfinder(List mapNodes)
+ public AGVPathfinder(List mapNodes)
{
_mapNodes = mapNodes ?? new List();
_basicPathfinder = new AStarPathfinder();
@@ -83,9 +30,9 @@ namespace AGVNavigationCore.PathFinding
}
///
- /// 고급 AGV 경로 계산
+ /// AGV 경로 계산
///
- public AdvancedPathResult FindAdvancedPath(string startNodeId, string targetNodeId, AgvDirection currentDirection = AgvDirection.Forward)
+ public AGVPathResult FindPath(string startNodeId, string targetNodeId, AgvDirection currentDirection = AgvDirection.Forward)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -97,7 +44,7 @@ namespace AGVNavigationCore.PathFinding
// 2. 방향 전환이 필요한지 확인
bool needDirectionChange = (currentDirection != requiredDirection);
- AdvancedPathResult result;
+ AGVPathResult result;
if (needDirectionChange)
{
// 방향 전환이 필요한 경우
@@ -110,30 +57,37 @@ namespace AGVNavigationCore.PathFinding
}
result.CalculationTimeMs = stopwatch.ElapsedMilliseconds;
+
+ // 도킹 검증 수행
+ if (result.Success && _mapNodes != null)
+ {
+ result.DockingValidation = DockingValidator.ValidateDockingDirection(result, _mapNodes, currentDirection);
+ }
+
return result;
}
catch (Exception ex)
{
- return AdvancedPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
+ return AGVPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
}
}
///
/// 직접 경로 계획
///
- private AdvancedPathResult PlanDirectPath(string startNodeId, string targetNodeId, AgvDirection currentDirection)
+ private AGVPathResult PlanDirectPath(string startNodeId, string targetNodeId, AgvDirection currentDirection)
{
var basicResult = _basicPathfinder.FindPath(startNodeId, targetNodeId);
if (!basicResult.Success)
{
- return AdvancedPathResult.CreateFailure(basicResult.ErrorMessage, basicResult.CalculationTimeMs, basicResult.ExploredNodeCount);
+ return AGVPathResult.CreateFailure(basicResult.ErrorMessage, basicResult.CalculationTimeMs, basicResult.ExploredNodeCount);
}
// 기본 경로를 상세 경로로 변환
var detailedPath = ConvertToDetailedPath(basicResult.Path, currentDirection);
- return AdvancedPathResult.CreateSuccess(
+ return AGVPathResult.CreateSuccess(
detailedPath,
basicResult.TotalDistance,
basicResult.CalculationTimeMs,
@@ -145,13 +99,13 @@ namespace AGVNavigationCore.PathFinding
///
/// 방향 전환을 포함한 경로 계획
///
- private AdvancedPathResult PlanPathWithDirectionChange(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
+ private AGVPathResult PlanPathWithDirectionChange(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
{
var directionChangePlan = _directionChangePlanner.PlanDirectionChange(startNodeId, targetNodeId, currentDirection, requiredDirection);
if (!directionChangePlan.Success)
{
- return AdvancedPathResult.CreateFailure(directionChangePlan.ErrorMessage, 0, 0);
+ return AGVPathResult.CreateFailure(directionChangePlan.ErrorMessage, 0, 0);
}
// 방향 전환 경로를 상세 경로로 변환
@@ -160,7 +114,7 @@ namespace AGVNavigationCore.PathFinding
// 거리 계산
float totalDistance = CalculatePathDistance(detailedPath);
- return AdvancedPathResult.CreateSuccess(
+ return AGVPathResult.CreateSuccess(
detailedPath,
totalDistance,
0,
@@ -347,7 +301,7 @@ namespace AGVNavigationCore.PathFinding
///
/// 경로 최적화
///
- public AdvancedPathResult OptimizePath(AdvancedPathResult originalResult)
+ public AGVPathResult OptimizePath(AGVPathResult originalResult)
{
if (!originalResult.Success)
return originalResult;
@@ -363,7 +317,7 @@ namespace AGVNavigationCore.PathFinding
///
/// 디버깅용 경로 정보
///
- public string GetPathSummary(AdvancedPathResult result)
+ public string GetPathSummary(AGVPathResult result)
{
if (!result.Success)
return $"경로 계산 실패: {result.ErrorMessage}";
@@ -387,5 +341,6 @@ namespace AGVNavigationCore.PathFinding
return string.Join("\n", summary);
}
+
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/DirectionChangePlanner.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs
similarity index 99%
rename from Cs_HMI/AGVNavigationCore/PathFinding/DirectionChangePlanner.cs
rename to Cs_HMI/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs
index 9d4478e..2135abc 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/DirectionChangePlanner.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs
@@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding.Core;
+using AGVNavigationCore.PathFinding.Analysis;
-namespace AGVNavigationCore.PathFinding
+namespace AGVNavigationCore.PathFinding.Planning
{
///
/// AGV 방향 전환 경로 계획 시스템
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/NodeMotorInfo.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs
similarity index 98%
rename from Cs_HMI/AGVNavigationCore/PathFinding/NodeMotorInfo.cs
rename to Cs_HMI/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs
index 4ffedab..7cbbc9f 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/NodeMotorInfo.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs
@@ -1,6 +1,6 @@
using AGVNavigationCore.Models;
-namespace AGVNavigationCore.PathFinding
+namespace AGVNavigationCore.PathFinding.Planning
{
///
/// AGV 마그넷 센서 방향 제어
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs
deleted file mode 100644
index b4d6880..0000000
--- a/Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs
+++ /dev/null
@@ -1,275 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using AGVNavigationCore.Models;
-
-namespace AGVNavigationCore.PathFinding
-{
- ///
- /// RFID 기반 AGV 경로 탐색기
- /// 실제 현장에서 AGV가 RFID를 읽어서 위치를 파악하는 방식에 맞춤
- ///
- public class RfidBasedPathfinder
- {
- private AGVPathfinder _agvPathfinder;
- private AStarPathfinder _astarPathfinder;
- private Dictionary _rfidToNodeMap; // RFID -> NodeId
- private Dictionary _nodeToRfidMap; // NodeId -> RFID
- private List _mapNodes;
-
- ///
- /// AGV 현재 방향
- ///
- public AgvDirection CurrentDirection
- {
- get => _agvPathfinder.CurrentDirection;
- set => _agvPathfinder.CurrentDirection = value;
- }
-
- ///
- /// 회전 비용 가중치
- ///
- public float RotationCostWeight
- {
- get => _agvPathfinder.RotationCostWeight;
- set => _agvPathfinder.RotationCostWeight = value;
- }
-
- ///
- /// 생성자
- ///
- public RfidBasedPathfinder()
- {
- _agvPathfinder = new AGVPathfinder();
- _astarPathfinder = new AStarPathfinder();
- _rfidToNodeMap = new Dictionary();
- _nodeToRfidMap = new Dictionary();
- _mapNodes = new List();
- }
-
- ///
- /// 맵 노드 설정 (MapNode의 RFID 정보 직접 사용)
- ///
- /// 맵 노드 목록
- public void SetMapNodes(List mapNodes)
- {
- // 기존 pathfinder에 맵 노드 설정
- _agvPathfinder.SetMapNodes(mapNodes);
- _astarPathfinder.SetMapNodes(mapNodes);
-
- // MapNode의 RFID 정보로 매핑 구성
- _mapNodes = mapNodes ?? new List();
- _rfidToNodeMap.Clear();
- _nodeToRfidMap.Clear();
-
- foreach (var node in _mapNodes.Where(n => n.IsActive && n.HasRfid()))
- {
- _rfidToNodeMap[node.RfidId] = node.NodeId;
- _nodeToRfidMap[node.NodeId] = node.RfidId;
- }
- }
-
- ///
- /// RFID 기반 AGV 경로 계산
- ///
- /// 시작 RFID
- /// 목적지 RFID
- /// 목적지 도착 방향
- /// RFID 기반 AGV 경로 계산 결과
- public RfidPathResult FindAGVPath(string startRfidId, string endRfidId, AgvDirection? targetDirection = null)
- {
- try
- {
- // RFID를 NodeId로 변환
- if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
- {
- return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
- }
-
- if (!_rfidToNodeMap.TryGetValue(endRfidId, out string endNodeId))
- {
- return RfidPathResult.CreateFailure($"목적지 RFID를 찾을 수 없습니다: {endRfidId}", 0);
- }
-
- // NodeId 기반으로 경로 계산
- var nodeResult = _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
-
- // 결과를 RFID 기반으로 변환
- return ConvertToRfidResult(nodeResult, startRfidId, endRfidId);
- }
- catch (Exception ex)
- {
- return RfidPathResult.CreateFailure($"RFID 기반 경로 계산 중 오류: {ex.Message}", 0);
- }
- }
-
- ///
- /// 가장 가까운 충전소로의 RFID 기반 경로 찾기
- ///
- /// 시작 RFID
- /// RFID 기반 경로 계산 결과
- public RfidPathResult FindPathToChargingStation(string startRfidId)
- {
- try
- {
- if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
- {
- return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
- }
-
- var nodeResult = _agvPathfinder.FindPathToChargingStation(startNodeId);
- return ConvertToRfidResult(nodeResult, startRfidId, null);
- }
- catch (Exception ex)
- {
- return RfidPathResult.CreateFailure($"충전소 경로 계산 중 오류: {ex.Message}", 0);
- }
- }
-
- ///
- /// 특정 장비 타입의 도킹 스테이션으로의 RFID 기반 경로 찾기
- ///
- /// 시작 RFID
- /// 장비 타입
- /// RFID 기반 경로 계산 결과
- public RfidPathResult FindPathToDockingStation(string startRfidId, StationType stationType)
- {
- try
- {
- if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
- {
- return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
- }
-
- var nodeResult = _agvPathfinder.FindPathToDockingStation(startNodeId, stationType);
- return ConvertToRfidResult(nodeResult, startRfidId, null);
- }
- catch (Exception ex)
- {
- return RfidPathResult.CreateFailure($"도킹 스테이션 경로 계산 중 오류: {ex.Message}", 0);
- }
- }
-
- ///
- /// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
- ///
- /// 시작 RFID
- /// 목적지 후보 RFID 목록
- /// RFID 기반 경로 계산 결과
- public RfidPathResult FindNearestPath(string startRfidId, List targetRfidIds)
- {
- try
- {
- if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
- {
- return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
- }
-
- // RFID 목록을 NodeId 목록으로 변환
- var targetNodeIds = new List();
- foreach (var rfidId in targetRfidIds)
- {
- if (_rfidToNodeMap.TryGetValue(rfidId, out string nodeId))
- {
- targetNodeIds.Add(nodeId);
- }
- }
-
- if (targetNodeIds.Count == 0)
- {
- return RfidPathResult.CreateFailure("유효한 목적지 RFID가 없습니다", 0);
- }
-
- var pathResult = _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
- if (!pathResult.Success)
- {
- return RfidPathResult.CreateFailure(pathResult.ErrorMessage, pathResult.CalculationTimeMs);
- }
-
- // AGV 명령어 생성을 위해 AGV pathfinder 사용
- var endNodeId = pathResult.Path.Last();
- var agvResult = _agvPathfinder.FindAGVPath(startNodeId, endNodeId);
-
- return ConvertToRfidResult(agvResult, startRfidId, null);
- }
- catch (Exception ex)
- {
- return RfidPathResult.CreateFailure($"최근접 경로 계산 중 오류: {ex.Message}", 0);
- }
- }
-
- ///
- /// RFID 매핑 상태 확인 (MapNode 기반)
- ///
- /// 확인할 RFID
- /// MapNode 또는 null
- public MapNode GetRfidMapping(string rfidId)
- {
- return _mapNodes.FirstOrDefault(n => n.RfidId == rfidId && n.IsActive && n.HasRfid());
- }
-
- ///
- /// RFID로 NodeId 조회
- ///
- /// RFID
- /// NodeId 또는 null
- public string GetNodeIdByRfid(string rfidId)
- {
- return _rfidToNodeMap.TryGetValue(rfidId, out string nodeId) ? nodeId : null;
- }
-
- ///
- /// NodeId로 RFID 조회
- ///
- /// NodeId
- /// RFID 또는 null
- public string GetRfidByNodeId(string nodeId)
- {
- return _nodeToRfidMap.TryGetValue(nodeId, out string rfidId) ? rfidId : null;
- }
-
- ///
- /// 활성화된 RFID 목록 반환
- ///
- /// 활성화된 RFID 목록
- public List GetActiveRfidList()
- {
- return _mapNodes.Where(n => n.IsActive && n.HasRfid()).Select(n => n.RfidId).ToList();
- }
-
- ///
- /// NodeId 기반 결과를 RFID 기반 결과로 변환
- ///
- private RfidPathResult ConvertToRfidResult(AGVPathResult nodeResult, string startRfidId, string endRfidId)
- {
- if (!nodeResult.Success)
- {
- return RfidPathResult.CreateFailure(nodeResult.ErrorMessage, nodeResult.CalculationTimeMs);
- }
-
- // NodeId 경로를 RFID 경로로 변환
- var rfidPath = new List();
- foreach (var nodeId in nodeResult.Path)
- {
- if (_nodeToRfidMap.TryGetValue(nodeId, out string rfidId))
- {
- rfidPath.Add(rfidId);
- }
- else
- {
- // 매핑이 없는 경우 NodeId를 그대로 사용 (경고 로그 필요)
- rfidPath.Add($"[{nodeId}]");
- }
- }
-
- return RfidPathResult.CreateSuccess(
- rfidPath,
- nodeResult.Commands,
- nodeResult.TotalDistance,
- nodeResult.CalculationTimeMs,
- nodeResult.EstimatedTimeSeconds,
- nodeResult.RotationCount
- );
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs
deleted file mode 100644
index 49fb6af..0000000
--- a/Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs
+++ /dev/null
@@ -1,229 +0,0 @@
-using System;
-using System.Collections.Generic;
-using AGVNavigationCore.Models;
-
-namespace AGVNavigationCore.PathFinding
-{
- ///
- /// RFID 기반 AGV 경로 계산 결과
- /// 실제 현장에서 AGV가 RFID를 기준으로 이동하는 방식에 맞춤
- ///
- public class RfidPathResult
- {
- ///
- /// 경로 찾기 성공 여부
- ///
- public bool Success { get; set; }
-
- ///
- /// RFID 경로 목록 (시작 → 목적지 순서)
- ///
- public List RfidPath { get; set; }
-
- ///
- /// AGV 명령어 목록 (이동 방향 시퀀스)
- ///
- public List Commands { get; set; }
-
- ///
- /// 총 거리
- ///
- public float TotalDistance { get; set; }
-
- ///
- /// 계산 소요 시간 (밀리초)
- ///
- public long CalculationTimeMs { get; set; }
-
- ///
- /// 예상 소요 시간 (초)
- ///
- public float EstimatedTimeSeconds { get; set; }
-
- ///
- /// 회전 횟수
- ///
- public int RotationCount { get; set; }
-
- ///
- /// 오류 메시지 (실패시)
- ///
- public string ErrorMessage { get; set; }
-
- ///
- /// 기본 생성자
- ///
- public RfidPathResult()
- {
- Success = false;
- RfidPath = new List();
- Commands = new List();
- TotalDistance = 0;
- CalculationTimeMs = 0;
- EstimatedTimeSeconds = 0;
- RotationCount = 0;
- ErrorMessage = string.Empty;
- }
-
- ///
- /// 성공 결과 생성
- ///
- /// RFID 경로
- /// AGV 명령어 목록
- /// 총 거리
- /// 계산 시간
- /// 예상 소요 시간
- /// 회전 횟수
- /// 성공 결과
- public static RfidPathResult CreateSuccess(
- List rfidPath,
- List commands,
- float totalDistance,
- long calculationTimeMs,
- float estimatedTimeSeconds,
- int rotationCount)
- {
- return new RfidPathResult
- {
- Success = true,
- RfidPath = new List(rfidPath),
- Commands = new List(commands),
- TotalDistance = totalDistance,
- CalculationTimeMs = calculationTimeMs,
- EstimatedTimeSeconds = estimatedTimeSeconds,
- RotationCount = rotationCount
- };
- }
-
- ///
- /// 실패 결과 생성
- ///
- /// 오류 메시지
- /// 계산 시간
- /// 실패 결과
- public static RfidPathResult CreateFailure(string errorMessage, long calculationTimeMs)
- {
- return new RfidPathResult
- {
- Success = false,
- ErrorMessage = errorMessage,
- CalculationTimeMs = calculationTimeMs
- };
- }
-
- ///
- /// 명령어 요약 생성
- ///
- /// 명령어 요약 문자열
- public string GetCommandSummary()
- {
- if (!Success) return "실패";
-
- var summary = new List();
- var currentCommand = AgvDirection.Stop;
- var count = 0;
-
- foreach (var command in Commands)
- {
- if (command == currentCommand)
- {
- count++;
- }
- else
- {
- if (count > 0)
- {
- summary.Add($"{GetCommandText(currentCommand)}×{count}");
- }
- currentCommand = command;
- count = 1;
- }
- }
-
- if (count > 0)
- {
- summary.Add($"{GetCommandText(currentCommand)}×{count}");
- }
-
- return string.Join(" → ", summary);
- }
-
- ///
- /// 명령어 텍스트 반환
- ///
- private string GetCommandText(AgvDirection command)
- {
- switch (command)
- {
- case AgvDirection.Forward: return "전진";
- case AgvDirection.Backward: return "후진";
- case AgvDirection.Left: return "좌회전";
- case AgvDirection.Right: return "우회전";
- case AgvDirection.Stop: return "정지";
- default: return command.ToString();
- }
- }
-
- ///
- /// RFID 경로 요약 생성
- ///
- /// RFID 경로 요약 문자열
- public string GetRfidPathSummary()
- {
- if (!Success || RfidPath.Count == 0) return "경로 없음";
-
- if (RfidPath.Count <= 3)
- {
- return string.Join(" → ", RfidPath);
- }
- else
- {
- return $"{RfidPath[0]} → ... ({RfidPath.Count - 2}개 경유) → {RfidPath[RfidPath.Count - 1]}";
- }
- }
-
- ///
- /// 상세 경로 정보 반환
- ///
- /// 상세 정보 문자열
- public string GetDetailedInfo()
- {
- if (!Success)
- {
- return $"RFID 경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
- }
-
- return $"RFID 경로: {RfidPath.Count}개 지점, 거리: {TotalDistance:F1}px, " +
- $"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
- $"계산시간: {CalculationTimeMs}ms";
- }
-
- ///
- /// AGV 운영자용 실행 정보 반환
- ///
- /// 실행 정보 문자열
- public string GetExecutionInfo()
- {
- if (!Success) return $"실행 불가: {ErrorMessage}";
-
- return $"[실행준비] {GetRfidPathSummary()}\n" +
- $"[명령어] {GetCommandSummary()}\n" +
- $"[예상시간] {EstimatedTimeSeconds:F1}초";
- }
-
- ///
- /// 문자열 표현
- ///
- public override string ToString()
- {
- if (Success)
- {
- return $"Success: {RfidPath.Count} RFIDs, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
- }
- else
- {
- return $"Failed: {ErrorMessage}";
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/Validation/DockingValidationResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Validation/DockingValidationResult.cs
new file mode 100644
index 0000000..ceac513
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/Validation/DockingValidationResult.cs
@@ -0,0 +1,103 @@
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.PathFinding.Validation
+{
+ ///
+ /// 도킹 검증 결과
+ ///
+ public class DockingValidationResult
+ {
+ ///
+ /// 도킹 검증이 필요한지 여부 (목적지가 도킹 대상인 경우)
+ ///
+ public bool IsValidationRequired { get; set; }
+
+ ///
+ /// 도킹 검증 통과 여부
+ ///
+ public bool IsValid { get; set; }
+
+ ///
+ /// 목적지 노드 ID
+ ///
+ public string TargetNodeId { get; set; }
+
+ ///
+ /// 목적지 노드 타입
+ ///
+ public NodeType TargetNodeType { get; set; }
+
+ ///
+ /// 필요한 도킹 방향
+ ///
+ public AgvDirection RequiredDockingDirection { get; set; }
+
+ ///
+ /// 계산된 경로의 마지막 방향
+ ///
+ public AgvDirection CalculatedFinalDirection { get; set; }
+
+ ///
+ /// 검증 오류 메시지 (실패시)
+ ///
+ public string ValidationError { get; set; }
+
+ ///
+ /// 기본 생성자
+ ///
+ public DockingValidationResult()
+ {
+ IsValidationRequired = false;
+ IsValid = true;
+ TargetNodeId = string.Empty;
+ RequiredDockingDirection = AgvDirection.Forward;
+ CalculatedFinalDirection = AgvDirection.Forward;
+ ValidationError = string.Empty;
+ }
+
+ ///
+ /// 검증 불필요한 경우 생성
+ ///
+ public static DockingValidationResult CreateNotRequired()
+ {
+ return new DockingValidationResult
+ {
+ IsValidationRequired = false,
+ IsValid = true
+ };
+ }
+
+ ///
+ /// 검증 성공 결과 생성
+ ///
+ public static DockingValidationResult CreateValid(string targetNodeId, NodeType nodeType, AgvDirection requiredDirection, AgvDirection calculatedDirection)
+ {
+ return new DockingValidationResult
+ {
+ IsValidationRequired = true,
+ IsValid = true,
+ TargetNodeId = targetNodeId,
+ TargetNodeType = nodeType,
+ RequiredDockingDirection = requiredDirection,
+ CalculatedFinalDirection = calculatedDirection
+ };
+ }
+
+ ///
+ /// 검증 실패 결과 생성
+ ///
+ public static DockingValidationResult CreateInvalid(string targetNodeId, NodeType nodeType, AgvDirection requiredDirection, AgvDirection calculatedDirection, string error)
+ {
+ return new DockingValidationResult
+ {
+ IsValidationRequired = true,
+ IsValid = false,
+ TargetNodeId = targetNodeId,
+ TargetNodeType = nodeType,
+ RequiredDockingDirection = requiredDirection,
+ CalculatedFinalDirection = calculatedDirection,
+ ValidationError = error
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/README.md b/Cs_HMI/AGVNavigationCore/README.md
index ae2efe3..3731f88 100644
--- a/Cs_HMI/AGVNavigationCore/README.md
+++ b/Cs_HMI/AGVNavigationCore/README.md
@@ -1,225 +1,155 @@
-# AGVNavigationCore 프로젝트 기능 설명
+# AGVNavigationCore
+
+ENIG AGV 시스템을 위한 핵심 네비게이션 및 경로 탐색 라이브러리
## 📋 개요
-AGVNavigationCore는 AGV(Automated Guided Vehicle) 시스템을 위한 전문적인 경로 계산 및 네비게이션 라이브러리입니다. 실제 AGV의 물리적 제약사항과 운영 요구사항을 고려한 지능형 경로 탐색 기능을 제공합니다.
-## 🏗️ 핵심 구조
+AGVNavigationCore는 자동 유도 차량(AGV) 시스템의 경로 계획, 맵 편집, 시뮬레이션, 실시간 모니터링 기능을 제공하는 .NET Framework 4.8 라이브러리입니다.
-### **Models 패키지**
-- **MapNode**: 맵의 논리적 노드 정보 (위치, 타입, 연결 정보, RFID 매핑)
-- **RfidMapping**: RFID와 논리적 노드 ID 간의 매핑 정보
-- **Enums**: 노드 타입, AGV 방향, 도킹 방향, 장비 타입 등 열거형 정의
+## 🏗️ 프로젝트 구조
-### **PathFinding 패키지**
-경로 계산의 핵심 엔진들이 포함된 패키지
+### 📁 Controls/
+**AGV 관련 사용자 인터페이스 컨트롤 및 AGV 추상화 계층**
+
+- **AGVState.cs** - AGV 상태 열거형 (Idle, Moving, Rotating, Docking, Charging, Error)
+- **IAGV.cs** - AGV 인터페이스 정의 (가상/실제 AGV 통합)
+- **UnifiedAGVCanvas.cs** - 통합 AGV 캔버스 컨트롤 메인 클래스
+- **UnifiedAGVCanvas.Events.cs** - 그리기 및 렌더링 로직 (AGV, 노드, 경로 시각화)
+- **UnifiedAGVCanvas.Mouse.cs** - 마우스 이벤트 처리 (클릭, 드래그, 줌, 팬)
+
+### 📁 Models/
+**데이터 모델 및 핵심 비즈니스 엔티티 정의**
+
+- **Enums.cs** - 핵심 열거형 정의 (NodeType, DockingDirection, AgvDirection, StationType)
+- **MapNode.cs** - 맵 노드 엔티티 클래스 (논리적 노드 ID, 위치, 타입, 연결 정보, RFID 정보)
+- **MapLoader.cs** - 맵 파일 로딩/저장 유틸리티 (JSON 직렬화, 데이터 마이그레이션, 검증)
+
+### 📁 PathFinding/
+**AGV 경로 탐색 및 계산 알고리즘**
+
+#### 🟢 활발히 사용되는 클래스
+- **AGVPathfinder.cs** - 메인 AGV 경로 계획기 (물리적 제약사항 고려)
+- **AGVPathResult.cs** - 경로 계산 결과 데이터 클래스
+- **DockingValidationResult.cs** - 도킹 검증 결과 데이터 클래스
+
+#### 🟡 내부 구현 클래스
+- **AStarPathfinder.cs** - A* 알고리즘 기반 기본 경로 탐색
+- **DirectionChangePlanner.cs** - AGV 방향 전환 경로 계획 시스템
+- **JunctionAnalyzer.cs** - 교차점 분석 및 마그넷 센서 방향 계산
+- **NodeMotorInfo.cs** - 노드별 모터방향 정보 (방향 전환 지원 포함)
+- **PathNode.cs** - A* 알고리즘용 경로 노드
+
+### 📁 Utils/
+**유틸리티 및 계산 헬퍼 클래스**
+
+- **DockingValidator.cs** - AGV 도킹 방향 검증 유틸리티
+- **LiftCalculator.cs** - AGV 리프트 방향 계산 유틸리티
+
+### 📁 Properties/
+- **AssemblyInfo.cs** - 어셈블리 정보 및 버전 관리
## 🎯 주요 기능
-### 1. **기본 경로 탐색 (A* 알고리즘)**
+### 🗺️ 맵 관리
+- **논리적 노드 시스템**: 물리적 RFID와 분리된 논리적 노드 ID 관리
+- **노드 타입**: Normal, Rotation, Docking, Charging 등 다양한 노드 타입 지원
+- **연결 관리**: 노드 간 방향성 연결 관리
+- **JSON 저장/로드**: 표준 JSON 형식으로 맵 데이터 관리
-**AStarPathfinder 클래스**:
-- 표준 A* 알고리즘 구현으로 최적 경로 탐색
-- 유클리드 거리 기반 휴리스틱 사용
-- 설정 가능한 파라미터:
- - `HeuristicWeight`: 휴리스틱 가중치 (기본 1.0)
- - `MaxSearchNodes`: 최대 탐색 노드 수 (기본 1000개)
+### 🧭 경로 탐색
+- **A* 알고리즘**: 효율적인 최단 경로 탐색
+- **AGV 물리적 제약**: 전진/후진 모터 방향, 회전 제약 고려
+- **방향 전환 계획**: 마그넷 센서 위치에서의 방향 전환 최적화
+- **도킹 검증**: 목적지 타입에 따른 도킹 방향 검증
-**제공 기능**:
-```csharp
-// 단일 경로 탐색
-PathResult FindPath(string startNodeId, string endNodeId)
+### 🎮 시각화 및 편집
+- **통합 캔버스**: 맵 편집, 시뮬레이션, 모니터링 모드 지원
+- **실시간 렌더링**: AGV 위치, 경로, 상태 실시간 표시
+- **인터랙티브 편집**: 드래그앤드롭 노드 편집, 연결 관리
+- **줌/팬**: 대형 맵 탐색을 위한 줌/팬 기능
-// 다중 목표 중 최단 경로 탐색
-PathResult FindNearestPath(string startNodeId, List targetNodeIds)
+## 🔧 아키텍처 특징
-// 노드 연결 상태 확인
-bool AreNodesConnected(string nodeId1, string nodeId2)
-```
+### ✅ 장점
+- **계층화 아키텍처**: Models → Utils → PathFinding → Controls 의존성 구조
+- **관심사 분리**: 각 폴더별 명확한 책임 분담
+- **인터페이스 기반**: IAGV 인터페이스로 가상/실제 AGV 통합
+- **확장성**: 새로운 알고리즘, AGV 타입 추가 용이
-### 2. **AGV 전용 지능형 경로 계산**
-
-**AGVPathfinder 클래스**:
-AGV의 실제 움직임 제약사항을 고려한 전문 경로 계산기
-
-**AGV 제약사항 고려**:
-- **방향성 제약**: 전진/후진만 가능, 좌우 이동 불가
-- **회전 제약**: 특정 노드(회전 가능 지점)에서만 180도 회전 가능
-- **도킹 방향**:
- - 충전기: 전진 도킹만 가능
- - 장비 (로더, 클리너 등): 후진 도킹만 가능
-
-**전용 기능**:
-```csharp
-// AGV 경로 계산 (방향성 고려)
-AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection)
-
-// 가장 가까운 충전소 경로
-AGVPathResult FindPathToChargingStation(string startNodeId)
-
-// 특정 장비 타입 도킹 스테이션 경로
-AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
-```
-
-### 3. **RFID 기반 경로 계산 (실제 운영용)**
-
-**RfidBasedPathfinder 클래스**:
-실제 AGV가 RFID를 읽어서 위치를 파악하는 현장 운영 방식에 최적화
-
-**RFID 기반 제약사항**:
-- **물리적 RFID**: 의미 없는 고유 식별자 (현장 유지보수 편의성)
-- **논리적 매핑**: RFID ↔ NodeId 분리로 맵 변경 시 유연성 확보
-- **실시간 변환**: RFID 입력을 내부적으로 NodeId로 변환하여 처리
-
-**RFID 전용 기능**:
-```csharp
-// RFID 기반 AGV 경로 계산
-RfidPathResult FindAGVPath(string startRfidId, string endRfidId, AgvDirection? targetDirection)
-
-// RFID 기반 충전소 경로
-RfidPathResult FindPathToChargingStation(string startRfidId)
-
-// RFID 기반 도킹 스테이션 경로
-RfidPathResult FindPathToDockingStation(string startRfidId, StationType stationType)
-
-// RFID 매핑 관리
-RfidMapping GetRfidMapping(string rfidId)
-string GetNodeIdByRfid(string rfidId)
-string GetRfidByNodeId(string nodeId)
-```
-
-### 4. **상세한 결과 분석**
-
-**PathResult (기본 결과)**:
-- 성공/실패 여부
-- 노드 ID 시퀀스
-- 총 거리 및 계산 시간
-- 탐색한 노드 수
-
-**AGVPathResult (AGV 전용 결과)**:
-- **실행 가능한 명령어 시퀀스**: `[전진, 전진, 우회전, 후진, 정지]`
-- **상세 메트릭**:
- - 회전 횟수 계산
- - 예상 소요 시간 (이동 + 회전 시간)
- - 명령어 요약 (`전진×3 → 우회전×1 → 후진×2`)
-
-**RfidPathResult (RFID 기반 결과)**:
-- **RFID 경로 시퀀스**: `[RFID001, RFID045, RFID067, RFID123]`
-- **AGV 명령어**: NodeId 기반 결과와 동일한 명령어 시퀀스
-- **현장 친화적 정보**:
- - RFID 경로 요약 (`RFID001 → ... (2개 경유) → RFID123`)
- - 실행 정보 (`[실행준비] → [명령어] → [예상시간]`)
- - 운영자용 상세 정보
-
-### 5. **실시간 검증 및 최적화**
-
-**경로 검증**:
-```csharp
-// 경로 유효성 실시간 검증
-bool ValidatePath(List path)
-
-// 네비게이션 가능 노드 필터링 (라벨/이미지 노드 제외)
-List GetNavigationNodes()
-```
-
-**성능 최적화**:
-- 메모리 효율적인 노드 관리
-- 조기 종료 조건으로 불필요한 탐색 방지
-- 캐시된 거리 계산
-
-## 🔧 설정 가능한 파라미터
-
-**AGV 동작 파라미터**:
-- `CurrentDirection`: AGV 현재 방향
-- `RotationCostWeight`: 회전 비용 가중치 (기본 50.0)
-- `DockingApproachDistance`: 도킹 접근 거리 (기본 100픽셀)
-
-**알고리즘 파라미터**:
-- `HeuristicWeight`: A* 휴리스틱 강도
-- `MaxSearchNodes`: 탐색 제한으로 무한루프 방지
-
-**RFID 매핑 파라미터**:
-- `RfidMappings`: RFID ↔ NodeId 매핑 테이블
-- `IsActive`: 매핑 활성화 상태
-- `Status`: RFID 상태 (정상, 손상, 교체예정)
-
-## 🎯 실제 활용 시나리오
-
-### **시나리오 1: 일반 이동 (NodeId 기반)**
-```
-현재위치(N001) → 목적지(N010)
-결과: [N001, N003, N007, N010] + [전진, 전진, 전진]
-```
-
-### **시나리오 2: 일반 이동 (RFID 기반)**
-```
-현재위치(RFID123) → 목적지(RFID789)
-결과: [RFID123, RFID456, RFID789] + [전진, 우회전, 전진]
-AGV는 RFID를 읽으면서 실제 위치 확인 후 이동
-```
-
-### **시나리오 3: 충전 필요**
-```
-배터리 부족 → 가장 가까운 충전소 자동 탐색
-결과: 충전소까지 최단경로 + 전진 도킹 명령어
-```
-
-### **시나리오 4: 화물 적재**
-```
-로더 스테이션 접근 → 후진 도킹 필수
-결과: 로더까지 경로 + 후진 도킹 시퀀스
-```
-
-## 🌟 차별화 포인트
-
-1. **실제 AGV 제약사항 반영**: 이론적 경로가 아닌 실행 가능한 경로 제공
-2. **명령어 레벨 출력**: 경로뿐만 아니라 실제 AGV 제어 명령어 생성
-3. **RFID 기반 현장 운영**: 물리적 RFID와 논리적 노드 분리로 현장 유지보수성 향상
-4. **다양한 장비 지원**: 충전기, 로더, 클리너, 버퍼 등 각각의 도킹 요구사항 처리
-5. **이중 API 제공**: NodeId 기반(개발용) + RFID 기반(운영용) 동시 지원
-6. **확장성**: 새로운 AGV 타입이나 제약사항 쉽게 추가 가능
-7. **성능 최적화**: 실시간 운영에 적합한 빠른 응답속도
+### ⚠️ 개선 영역
+- **코드 크기**: 일부 클래스가 과도하게 큼 (UnifiedAGVCanvas.Events.cs: 1,699행)
+- **복잡도**: DirectionChangePlanner 등 복잡한 로직 포함
+- **분할 필요**: UnifiedAGVCanvas의 다중 책임 분리 필요
## 🚀 사용 방법
-### 기본 사용법
+### 기본 맵 로딩
+```csharp
+var mapLoader = new MapLoader();
+var mapNodes = mapLoader.LoadMap("path/to/map.json");
+```
+
+### 경로 계산
```csharp
-// 1. 경로 탐색기 초기화
var pathfinder = new AGVPathfinder();
pathfinder.SetMapNodes(mapNodes);
-// 2. AGV 경로 계산
-var result = pathfinder.FindAGVPath("N001", "N010");
-
-// 3. 결과 확인
+var result = pathfinder.FindPath("START_NODE", "TARGET_NODE", AgvDirection.Forward);
if (result.Success)
{
- Console.WriteLine($"경로: {string.Join(" → ", result.Path)}");
- Console.WriteLine($"명령어: {result.GetCommandSummary()}");
- Console.WriteLine($"예상시간: {result.EstimatedTimeSeconds}초");
+ Console.WriteLine($"경로: {string.Join(" -> ", result.Path)}");
+ Console.WriteLine($"거리: {result.TotalDistance:F1}px");
}
```
-### 충전소 경로 탐색
+### 캔버스 사용
```csharp
-var chargingResult = pathfinder.FindPathToChargingStation("N001");
-if (chargingResult.Success)
-{
- // 충전소까지 자동 이동
- ExecuteAGVCommands(chargingResult.Commands);
-}
+var canvas = new UnifiedAGVCanvas();
+canvas.Nodes = mapNodes;
+canvas.CurrentPath = result;
+canvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
```
+## 📈 최근 업데이트 (2024.12)
+
+### ✅ 완료된 개선사항
+- **중복 코드 정리**: PathResult, RfidPathResult 등 중복 클래스 제거
+- **아키텍처 통합**: AdvancedAGVPathfinder → AGVPathfinder 통합
+- **좌표 정확성**: 줌/팬 시 노드 선택 정확도 개선
+- **미사용 코드 제거**: PathfindingOptions 등 미사용 클래스 삭제
+
+### 🔄 진행 중인 개선사항
+- **방향 계산 최적화**: 리프트 방향 계산 로직 개선
+- **도킹 검증**: 도킹 방향 검증 시스템 강화
+- **성능 최적화**: 대형 맵 처리 성능 개선
+
+## 🏃♂️ 향후 계획
+
+### 우선순위 1 (즉시)
+- UnifiedAGVCanvas 분할 (Rendering, Editing, Simulation 분리)
+- [완료] PathFinding 폴더 세분화 (Core, Validation, Planning, Analysis)
+
+### 우선순위 2 (중기)
+- 인터페이스 표준화 (I접두사 통일)
+- Utils 폴더 확장 (Calculations, Validators, Converters)
+
+### 우선순위 3 (장기)
+- 의존성 주입 도입
+- 성능 모니터링 시스템
+- 단위 테스트 확충
+
## 📦 의존성
- .NET Framework 4.8
- Newtonsoft.Json 13.0.3
- System.Drawing
+- System.Windows.Forms
-## 🔗 통합 프로젝트
-이 라이브러리는 다음 프로젝트에서 사용됩니다:
-- **AGVMapEditor**: 맵 편집 및 경로 시뮬레이션
-- **AGV4**: 메인 AGV 제어 시스템
-- **AGVSimulator**: AGV 동작 시뮬레이터
+## 🔗 관련 프로젝트
----
+- **AGVMapEditor**: 맵 편집 전용 애플리케이션
+- **AGVSimulator**: AGV 시뮬레이션 애플리케이션
+- **AGVCSharp**: 메인 AGV 제어 시스템
-*AGVNavigationCore는 ENIG AGV 시스템의 핵심 네비게이션 엔진입니다.*
\ No newline at end of file
+## 📞 연락처
+
+ENIG AGV 개발팀 - 2024년 12월 업데이트
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Utils/DockingValidator.cs b/Cs_HMI/AGVNavigationCore/Utils/DockingValidator.cs
new file mode 100644
index 0000000..82c10f8
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Utils/DockingValidator.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding.Core;
+using AGVNavigationCore.PathFinding.Validation;
+
+namespace AGVNavigationCore.Utils
+{
+ ///
+ /// AGV 도킹 방향 검증 유틸리티
+ /// 경로 계산 후 마지막 도킹 방향이 올바른지 검증
+ ///
+ public static class DockingValidator
+ {
+ ///
+ /// 경로의 도킹 방향 검증
+ ///
+ /// 경로 계산 결과
+ /// 맵 노드 목록
+ /// AGV 현재 방향
+ /// 도킹 검증 결과
+ public static DockingValidationResult ValidateDockingDirection(AGVPathResult pathResult, List mapNodes, AgvDirection currentDirection)
+ {
+ // 경로가 없거나 실패한 경우
+ if (pathResult == null || !pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0)
+ {
+ return DockingValidationResult.CreateNotRequired();
+ }
+
+ // 목적지 노드 찾기
+ string targetNodeId = pathResult.Path[pathResult.Path.Count - 1];
+ var targetNode = mapNodes?.FirstOrDefault(n => n.NodeId == targetNodeId);
+
+ if (targetNode == null)
+ {
+ return DockingValidationResult.CreateNotRequired();
+ }
+
+ // 도킹이 필요한 노드 타입인지 확인
+ if (!IsDockingRequired(targetNode.Type))
+ {
+ return DockingValidationResult.CreateNotRequired();
+ }
+
+ // 필요한 도킹 방향 확인
+ var requiredDirection = GetRequiredDockingDirection(targetNode.Type);
+
+ // 경로 기반 최종 방향 계산
+ var calculatedDirection = CalculateFinalDirection(pathResult.Path, mapNodes, currentDirection);
+
+ // 검증 수행
+ if (calculatedDirection == requiredDirection)
+ {
+ return DockingValidationResult.CreateValid(
+ targetNodeId,
+ targetNode.Type,
+ requiredDirection,
+ calculatedDirection);
+ }
+ else
+ {
+ string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(calculatedDirection)}";
+ return DockingValidationResult.CreateInvalid(
+ targetNodeId,
+ targetNode.Type,
+ requiredDirection,
+ calculatedDirection,
+ error);
+ }
+ }
+
+ ///
+ /// 도킹이 필요한 노드 타입인지 확인
+ ///
+ private static bool IsDockingRequired(NodeType nodeType)
+ {
+ return nodeType == NodeType.Charging || nodeType == NodeType.Docking;
+ }
+
+ ///
+ /// 노드 타입에 따른 필요한 도킹 방향 반환
+ ///
+ private static AgvDirection GetRequiredDockingDirection(NodeType nodeType)
+ {
+ switch (nodeType)
+ {
+ case NodeType.Charging:
+ return AgvDirection.Forward; // 충전기는 전진 도킹
+ case NodeType.Docking:
+ return AgvDirection.Backward; // 일반 도킹은 후진 도킹
+ default:
+ return AgvDirection.Forward; // 기본값
+ }
+ }
+
+ ///
+ /// 경로 기반 최종 방향 계산
+ /// 현재 구현: 간단한 추정 (향후 고도화 가능)
+ ///
+ private static AgvDirection CalculateFinalDirection(List path, List mapNodes, AgvDirection currentDirection)
+ {
+ // 경로가 2개 이상일 때만 방향 변화 추정
+ if (path.Count < 2)
+ {
+ return currentDirection;
+ }
+
+ // 마지막 구간의 노드들 찾기
+ var secondLastNodeId = path[path.Count - 2];
+ var lastNodeId = path[path.Count - 1];
+
+ var secondLastNode = mapNodes?.FirstOrDefault(n => n.NodeId == secondLastNodeId);
+ var lastNode = mapNodes?.FirstOrDefault(n => n.NodeId == lastNodeId);
+
+ if (secondLastNode == null || lastNode == null)
+ {
+ return currentDirection;
+ }
+
+ // 마지막 구간의 이동 방향 분석
+ var deltaX = lastNode.Position.X - secondLastNode.Position.X;
+ var deltaY = lastNode.Position.Y - secondLastNode.Position.Y;
+
+ // 이동 거리가 매우 작으면 현재 방향 유지
+ var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
+ if (distance < 1.0)
+ {
+ return currentDirection;
+ }
+
+ // 간단한 방향 추정 (향후 더 정교한 로직으로 개선 가능)
+ // 현재는 현재 방향을 유지한다고 가정
+ return currentDirection;
+ }
+
+ ///
+ /// 방향을 텍스트로 변환
+ ///
+ private static string GetDirectionText(AgvDirection direction)
+ {
+ switch (direction)
+ {
+ case AgvDirection.Forward:
+ return "전진";
+ case AgvDirection.Backward:
+ return "후진";
+ default:
+ return direction.ToString();
+ }
+ }
+
+ ///
+ /// 도킹 검증 결과를 문자열로 변환 (디버깅용)
+ ///
+ public static string GetValidationSummary(DockingValidationResult validation)
+ {
+ if (validation == null)
+ return "검증 결과 없음";
+
+ if (!validation.IsValidationRequired)
+ return "도킹 검증 불필요";
+
+ if (validation.IsValid)
+ {
+ return $"도킹 검증 통과: {validation.TargetNodeId}({validation.TargetNodeType}) - {GetDirectionText(validation.RequiredDockingDirection)} 도킹";
+ }
+ else
+ {
+ return $"도킹 검증 실패: {validation.TargetNodeId}({validation.TargetNodeType}) - {validation.ValidationError}";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Utils/LiftCalculator.cs b/Cs_HMI/AGVNavigationCore/Utils/LiftCalculator.cs
index 640b043..0029c53 100644
--- a/Cs_HMI/AGVNavigationCore/Utils/LiftCalculator.cs
+++ b/Cs_HMI/AGVNavigationCore/Utils/LiftCalculator.cs
@@ -31,9 +31,10 @@ namespace AGVNavigationCore.Utils
}
else if (motorDirection == AgvDirection.Backward)
{
- // 후진 모터: AGV가 리프트 쪽으로 이동 (현재 → 타겟 방향이 리프트 방향)
- var dx = targetPos.X - currentPos.X;
- var dy = targetPos.Y - currentPos.Y;
+ // 후진 모터: AGV가 리프트 쪽으로 이동하므로 리프트는 AGV 이동 방향에 위치
+ // 007→006 후진시: 리프트는 006방향(이동방향)을 향해야 함 (타겟→현재 반대방향)
+ var dx = currentPos.X - targetPos.X;
+ var dy = currentPos.Y - targetPos.Y;
return Math.Atan2(dy, dx);
}
else
@@ -131,7 +132,7 @@ namespace AGVNavigationCore.Utils
if (motorDirection == AgvDirection.Forward)
calculationMethod = "이동방향 + 180도 (전진모터)";
else if (motorDirection == AgvDirection.Backward)
- calculationMethod = "이동방향과 동일 (후진모터)";
+ calculationMethod = "이동방향과 동일 (후진모터 - 리프트는 이동방향에 위치)";
else
calculationMethod = "기본값 (전진모터)";
diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
index 0647d58..844856a 100644
--- a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
+++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
@@ -86,7 +86,7 @@ namespace AGVSimulator.Forms
this._clearPathButton = new System.Windows.Forms.Button();
this._startPathButton = new System.Windows.Forms.Button();
this._calculatePathButton = new System.Windows.Forms.Button();
- this._selectTargetButton = new System.Windows.Forms.Button();
+ this._targetCalcButton = new System.Windows.Forms.Button();
this._avoidRotationCheckBox = new System.Windows.Forms.CheckBox();
this._targetNodeCombo = new System.Windows.Forms.ComboBox();
this.targetNodeLabel = new System.Windows.Forms.Label();
@@ -412,7 +412,7 @@ namespace AGVSimulator.Forms
this._statusGroup.Controls.Add(this._agvCountLabel);
this._statusGroup.Controls.Add(this._simulationStatusLabel);
this._statusGroup.Dock = System.Windows.Forms.DockStyle.Top;
- this._statusGroup.Location = new System.Drawing.Point(0, 404);
+ this._statusGroup.Location = new System.Drawing.Point(0, 446);
this._statusGroup.Name = "_statusGroup";
this._statusGroup.Size = new System.Drawing.Size(233, 100);
this._statusGroup.TabIndex = 3;
@@ -451,7 +451,7 @@ namespace AGVSimulator.Forms
this._pathGroup.Controls.Add(this._clearPathButton);
this._pathGroup.Controls.Add(this._startPathButton);
this._pathGroup.Controls.Add(this._calculatePathButton);
- this._pathGroup.Controls.Add(this._selectTargetButton);
+ this._pathGroup.Controls.Add(this._targetCalcButton);
this._pathGroup.Controls.Add(this._avoidRotationCheckBox);
this._pathGroup.Controls.Add(this._targetNodeCombo);
this._pathGroup.Controls.Add(this.targetNodeLabel);
@@ -460,14 +460,14 @@ namespace AGVSimulator.Forms
this._pathGroup.Dock = System.Windows.Forms.DockStyle.Top;
this._pathGroup.Location = new System.Drawing.Point(0, 214);
this._pathGroup.Name = "_pathGroup";
- this._pathGroup.Size = new System.Drawing.Size(233, 190);
+ this._pathGroup.Size = new System.Drawing.Size(233, 232);
this._pathGroup.TabIndex = 1;
this._pathGroup.TabStop = false;
this._pathGroup.Text = "경로 제어";
//
// _clearPathButton
//
- this._clearPathButton.Location = new System.Drawing.Point(150, 148);
+ this._clearPathButton.Location = new System.Drawing.Point(150, 177);
this._clearPathButton.Name = "_clearPathButton";
this._clearPathButton.Size = new System.Drawing.Size(70, 25);
this._clearPathButton.TabIndex = 6;
@@ -477,7 +477,7 @@ namespace AGVSimulator.Forms
//
// _startPathButton
//
- this._startPathButton.Location = new System.Drawing.Point(80, 148);
+ this._startPathButton.Location = new System.Drawing.Point(80, 177);
this._startPathButton.Name = "_startPathButton";
this._startPathButton.Size = new System.Drawing.Size(65, 25);
this._startPathButton.TabIndex = 5;
@@ -487,24 +487,25 @@ namespace AGVSimulator.Forms
//
// _calculatePathButton
//
- this._calculatePathButton.Location = new System.Drawing.Point(10, 148);
+ this._calculatePathButton.Location = new System.Drawing.Point(10, 177);
this._calculatePathButton.Name = "_calculatePathButton";
this._calculatePathButton.Size = new System.Drawing.Size(65, 25);
this._calculatePathButton.TabIndex = 4;
this._calculatePathButton.Text = "경로 계산";
this._calculatePathButton.UseVisualStyleBackColor = true;
this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
+ //
//
- // _selectTargetButton
- //
- this._selectTargetButton.Location = new System.Drawing.Point(80, 148);
- this._selectTargetButton.Name = "_selectTargetButton";
- this._selectTargetButton.Size = new System.Drawing.Size(70, 25);
- this._selectTargetButton.TabIndex = 8;
- this._selectTargetButton.Text = "목적지 선택";
- this._selectTargetButton.UseVisualStyleBackColor = true;
- this._selectTargetButton.Click += new System.EventHandler(this.OnSelectTarget_Click);
- //
+ // _targetCalcButton
+ //
+ this._targetCalcButton.Location = new System.Drawing.Point(10, 148);
+ this._targetCalcButton.Name = "_targetCalcButton";
+ this._targetCalcButton.Size = new System.Drawing.Size(70, 25);
+ this._targetCalcButton.TabIndex = 9;
+ this._targetCalcButton.Text = "타겟계산";
+ this._targetCalcButton.UseVisualStyleBackColor = true;
+ this._targetCalcButton.Click += new System.EventHandler(this.OnTargetCalc_Click);
+ //
// _avoidRotationCheckBox
//
this._avoidRotationCheckBox.AutoSize = true;
@@ -805,7 +806,7 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.Button _calculatePathButton;
private System.Windows.Forms.Button _startPathButton;
private System.Windows.Forms.Button _clearPathButton;
- private System.Windows.Forms.Button _selectTargetButton;
+ private System.Windows.Forms.Button _targetCalcButton;
private System.Windows.Forms.CheckBox _avoidRotationCheckBox;
private System.Windows.Forms.GroupBox _statusGroup;
private System.Windows.Forms.Label _simulationStatusLabel;
diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
index 7cbd0d0..c89716c 100644
--- a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
+++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
@@ -5,12 +5,14 @@ using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
-using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.Controls;
using AGVNavigationCore.PathFinding;
+using AGVNavigationCore.Utils;
using AGVSimulator.Models;
using Newtonsoft.Json;
+using AGVNavigationCore.PathFinding.Planning;
+using AGVNavigationCore.PathFinding.Core;
namespace AGVSimulator.Forms
{
@@ -23,15 +25,13 @@ namespace AGVSimulator.Forms
private UnifiedAGVCanvas _simulatorCanvas;
private List _mapNodes;
- private NodeResolver _nodeResolver;
- private PathCalculator _pathCalculator;
- private AdvancedAGVPathfinder _advancedPathfinder;
+ private AGVPathfinder _advancedPathfinder;
private List _agvList;
private SimulationState _simulationState;
private Timer _simulationTimer;
private SimulatorConfig _config;
private string _currentMapFilePath;
- private bool _isSelectingTarget; // 목적지 선택 모드 상태
+ private bool _isTargetCalcMode; // 타겟계산 모드 상태
// UI Controls - Designer에서 생성됨
@@ -89,13 +89,10 @@ namespace AGVSimulator.Forms
// 마지막 맵 파일 자동 로드 확인은 Form_Load에서 수행
}
-
-
private void CreateSimulatorCanvas()
{
_simulatorCanvas = new UnifiedAGVCanvas();
_simulatorCanvas.Dock = DockStyle.Fill;
- _simulatorCanvas.Mode = UnifiedAGVCanvas.CanvasMode.ViewOnly;
// 목적지 선택 이벤트 구독
_simulatorCanvas.TargetNodeSelected += OnTargetNodeSelected;
@@ -272,6 +269,12 @@ namespace AGVSimulator.Forms
private void OnCalculatePath_Click(object sender, EventArgs e)
{
+ // 시작 RFID가 없으면 AGV 현재 위치로 설정
+ if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요")
+ {
+ SetStartNodeFromAGVPosition();
+ }
+
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
{
MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -289,61 +292,40 @@ namespace AGVSimulator.Forms
return;
}
- if (_pathCalculator == null)
- {
- _pathCalculator = new PathCalculator();
- _pathCalculator.SetMapData(_mapNodes);
- }
if (_advancedPathfinder == null)
{
- _advancedPathfinder = new AdvancedAGVPathfinder(_mapNodes);
+ _advancedPathfinder = new AGVPathfinder(_mapNodes);
}
// 현재 AGV 방향 가져오기
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
var currentDirection = selectedAGV?.CurrentDirection ?? AgvDirection.Forward;
- // 고급 경로 계획 사용
- var advancedResult = _advancedPathfinder.FindAdvancedPath(startNode.NodeId, targetNode.NodeId, currentDirection);
+ // 고급 경로 계획 사용 (단일 경로 계산 방식)
+ var advancedResult = _advancedPathfinder.FindPath(startNode.NodeId, targetNode.NodeId, currentDirection);
if (advancedResult.Success)
{
- // 고급 경로 결과를 기존 AGVPathResult 형태로 변환
- var agvResult1 = ConvertToAGVPathResult(advancedResult);
+ // 고급 경로 결과를 AGVPathResult 형태로 변환 (도킹 검증 포함)
+ var agvResult = ConvertToAGVPathResult(advancedResult, currentDirection);
- _simulatorCanvas.CurrentPath = agvResult1.ToPathResult();
+ _simulatorCanvas.CurrentPath = agvResult;
_pathLengthLabel.Text = $"경로 길이: {advancedResult.TotalDistance:F1}";
- _statusLabel.Text = $"고급 경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)";
+ _statusLabel.Text = $"경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)";
+
+ // 도킹 검증 결과 확인 및 UI 표시
+ CheckAndDisplayDockingValidation(agvResult);
// 고급 경로 디버깅 정보 표시
UpdateAdvancedPathDebugInfo(advancedResult);
- return;
- }
-
- // 고급 경로 실패시 기존 방식으로 fallback
- // 회전 회피 옵션 설정
- var options = _avoidRotationCheckBox.Checked
- ? PathfindingOptions.AvoidRotation
- : PathfindingOptions.Default;
-
- var agvResult = _pathCalculator.FindAGVPath(startNode.NodeId, targetNode.NodeId, null, options);
-
- if (agvResult.Success)
- {
- _simulatorCanvas.CurrentPath = agvResult.ToPathResult();
- _pathLengthLabel.Text = $"경로 길이: {agvResult.TotalDistance:F1}";
- _statusLabel.Text = $"경로 계산 완료 ({agvResult.CalculationTimeMs}ms)";
-
- // 경로 디버깅 정보 표시
- UpdatePathDebugInfo(agvResult);
}
else
{
// 경로 실패시 디버깅 정보 초기화
- _pathDebugLabel.Text = $"경로: 실패 - {agvResult.ErrorMessage}";
-
- MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
+ _pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
+
+ MessageBox.Show($"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}", "경로 계산 실패",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
@@ -374,25 +356,25 @@ namespace AGVSimulator.Forms
_statusLabel.Text = "경로 지움";
}
- private void OnSelectTarget_Click(object sender, EventArgs e)
+ private void OnTargetCalc_Click(object sender, EventArgs e)
{
- if (_isSelectingTarget)
+ if (_isTargetCalcMode)
{
- // 목적지 선택 모드 해제
- _isSelectingTarget = false;
- _selectTargetButton.Text = "목적지 선택";
- _selectTargetButton.BackColor = SystemColors.Control;
+ // 타겟계산 모드 해제
+ _isTargetCalcMode = false;
+ _targetCalcButton.Text = "타겟계산";
+ _targetCalcButton.BackColor = SystemColors.Control;
_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
- _statusLabel.Text = "목적지 선택 모드 해제";
+ _statusLabel.Text = "타겟계산 모드 해제";
}
else
{
- // 목적지 선택 모드 활성화
- _isSelectingTarget = true;
- _selectTargetButton.Text = "선택 취소";
- _selectTargetButton.BackColor = Color.LightBlue;
+ // 타겟계산 모드 활성화
+ _isTargetCalcMode = true;
+ _targetCalcButton.Text = "계산 취소";
+ _targetCalcButton.BackColor = Color.LightGreen;
_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.SelectTarget;
- _statusLabel.Text = "맵에서 목적지 노드를 클릭하세요";
+ _statusLabel.Text = "목적지 노드를 클릭하세요 (자동으로 경로 계산됨)";
}
}
@@ -400,35 +382,123 @@ namespace AGVSimulator.Forms
{
try
{
- // 목적지 선택 모드 해제
- _isSelectingTarget = false;
- _selectTargetButton.Text = "목적지 선택";
- _selectTargetButton.BackColor = SystemColors.Control;
- _simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
-
- // 목적지 콤보박스에 선택된 노드 설정
- var displayText = GetDisplayName(selectedNode.NodeId);
- for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
+ // 타겟계산 모드에서만 처리
+ if (_isTargetCalcMode)
{
- var item = _targetNodeCombo.Items[i].ToString();
- if (item.Contains($"[{selectedNode.NodeId}]"))
- {
- _targetNodeCombo.SelectedIndex = i;
- break;
- }
+ // 타겟계산 모드 해제
+ //_isTargetCalcMode = false;
+ //_targetCalcButton.Text = "타겟계산";
+ //_targetCalcButton.BackColor = SystemColors.Control;
+ //_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
+
+ // 목적지를 선택된 노드로 설정
+ SetTargetNodeInCombo(selectedNode.NodeId);
+
+ var displayText = GetDisplayName(selectedNode.NodeId);
+ _statusLabel.Text = $"타겟계산 - 목적지: {displayText}";
+
+ // 자동으로 경로 계산 수행
+ OnCalculatePath_Click(this, EventArgs.Empty);
}
-
- _statusLabel.Text = $"목적지 설정: {displayText}";
-
- // 자동으로 경로 계산 수행
- OnCalculatePath_Click(this, EventArgs.Empty);
}
catch (Exception ex)
{
- _statusLabel.Text = $"목적지 선택 오류: {ex.Message}";
+ _statusLabel.Text = $"노드 선택 오류: {ex.Message}";
}
}
+ ///
+ /// 목적지 콤보박스에 노드 설정
+ ///
+ private void SetTargetNodeInCombo(string nodeId)
+ {
+ for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
+ {
+ var item = _targetNodeCombo.Items[i].ToString();
+ if (item.Contains($"[{nodeId}]"))
+ {
+ _targetNodeCombo.SelectedIndex = i;
+ break;
+ }
+ }
+ }
+
+ ///
+ /// AGV 현재 노드로 시작 노드 설정
+ ///
+ private void SetStartNodeFromAGVPosition()
+ {
+ try
+ {
+ if (_agvList.Count > 0)
+ {
+ var agv = _agvList[0]; // 첫 번째 AGV 사용
+ var currentNodeId = agv.CurrentNodeId;
+
+ // AGV가 현재 노드 정보를 가지고 있는 경우 직접 사용
+ if (!string.IsNullOrEmpty(currentNodeId))
+ {
+ // 시작 노드 콤보박스에 설정
+ for (int i = 0; i < _startNodeCombo.Items.Count; i++)
+ {
+ var item = _startNodeCombo.Items[i].ToString();
+ if (item.Contains($"[{currentNodeId}]"))
+ {
+ _startNodeCombo.SelectedIndex = i;
+ return; // 성공적으로 설정됨
+ }
+ }
+ }
+
+ // CurrentNodeId가 없거나 콤보박스에서 찾지 못한 경우 위치 기반으로 폴백
+ var currentPos = agv.CurrentPosition;
+ var closestNode = FindClosestNode(currentPos);
+ if (closestNode != null)
+ {
+ // 시작 노드 콤보박스에 설정
+ for (int i = 0; i < _startNodeCombo.Items.Count; i++)
+ {
+ var item = _startNodeCombo.Items[i].ToString();
+ if (item.Contains($"[{closestNode.NodeId}]"))
+ {
+ _startNodeCombo.SelectedIndex = i;
+ break;
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _statusLabel.Text = $"시작 노드 설정 오류: {ex.Message}";
+ }
+ }
+
+ ///
+ /// 위치에서 가장 가까운 노드 찾기
+ ///
+ private MapNode FindClosestNode(Point position)
+ {
+ if (_mapNodes == null || _mapNodes.Count == 0)
+ return null;
+
+ MapNode closestNode = null;
+ double closestDistance = double.MaxValue;
+
+ foreach (var node in _mapNodes)
+ {
+ var distance = Math.Sqrt(Math.Pow(node.Position.X - position.X, 2) +
+ Math.Pow(node.Position.Y - position.Y, 2));
+ if (distance < closestDistance)
+ {
+ closestDistance = distance;
+ closestNode = node;
+ }
+ }
+
+ return closestNode;
+ }
+
private void OnSetPosition_Click(object sender, EventArgs e)
{
SetAGVPositionByRfid();
@@ -859,9 +929,66 @@ namespace AGVSimulator.Forms
}
///
- /// 고급 경로 결과를 기존 AGVPathResult 형태로 변환
+ /// 도킹 검증 결과 확인 및 UI 표시
///
- private AGVPathResult ConvertToAGVPathResult(AdvancedAGVPathfinder.AdvancedPathResult advancedResult)
+ private void CheckAndDisplayDockingValidation(AGVPathResult agvResult)
+ {
+ if (agvResult?.DockingValidation == null)
+ return;
+
+ var validation = agvResult.DockingValidation;
+
+ // 도킹 검증이 필요하지 않은 경우
+ if (!validation.IsValidationRequired)
+ return;
+
+ // 도킹 검증 실패시 UI에 표시
+ if (!validation.IsValid)
+ {
+ // 상태바에 경고 메시지 표시
+ _statusLabel.Text = $"⚠️ 도킹 방향 오류: {validation.ValidationError}";
+ _statusLabel.ForeColor = Color.Red;
+
+ // 경로는 표시하되, 목적지 노드에 X 마크 표시 요청
+ _simulatorCanvas.SetDockingError(validation.TargetNodeId, true);
+
+ // 사용자에게 알림
+ MessageBox.Show($"도킹 방향 검증 실패!\n\n" +
+ $"노드: {validation.TargetNodeId} ({validation.TargetNodeType})\n" +
+ $"필요 방향: {GetDirectionText(validation.RequiredDockingDirection)}\n" +
+ $"계산 방향: {GetDirectionText(validation.CalculatedFinalDirection)}\n\n" +
+ $"오류: {validation.ValidationError}",
+ "도킹 검증 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ }
+ else
+ {
+ // 도킹 검증 성공시 정상 표시
+ if (_statusLabel.ForeColor == Color.Red)
+ _statusLabel.ForeColor = Color.Black;
+ _simulatorCanvas.SetDockingError(validation.TargetNodeId, false);
+ }
+ }
+
+ ///
+ /// AGV 방향을 한글 텍스트로 변환
+ ///
+ private string GetDirectionText(AgvDirection direction)
+ {
+ switch (direction)
+ {
+ case AgvDirection.Forward: return "전진";
+ case AgvDirection.Backward: return "후진";
+ case AgvDirection.Left: return "좌회전";
+ case AgvDirection.Right: return "우회전";
+ case AgvDirection.Stop: return "정지";
+ default: return direction.ToString();
+ }
+ }
+
+ ///
+ /// 고급 경로 결과를 기존 AGVPathResult 형태로 변환 (도킹 검증 포함)
+ ///
+ private AGVPathResult ConvertToAGVPathResult(AGVPathResult advancedResult, AgvDirection? currentDirection = null)
{
var agvResult = new AGVPathResult();
agvResult.Success = advancedResult.Success;
@@ -871,13 +998,25 @@ namespace AGVSimulator.Forms
agvResult.CalculationTimeMs = advancedResult.CalculationTimeMs;
agvResult.ExploredNodes = advancedResult.ExploredNodeCount;
agvResult.ErrorMessage = advancedResult.ErrorMessage;
+
+ // 도킹 검증 수행 (AdvancedPathResult에서 이미 수행되었다면 그 결과 사용)
+ if (advancedResult.DockingValidation != null && advancedResult.DockingValidation.IsValidationRequired)
+ {
+ agvResult.DockingValidation = advancedResult.DockingValidation;
+ }
+ else if (agvResult.Success && _mapNodes != null && currentDirection.HasValue)
+ {
+ agvResult.DockingValidation = DockingValidator.ValidateDockingDirection(agvResult, _mapNodes, currentDirection.Value);
+ }
+
return agvResult;
}
+
///
/// 고급 경로 디버깅 정보 업데이트
///
- private void UpdateAdvancedPathDebugInfo(AdvancedAGVPathfinder.AdvancedPathResult advancedResult)
+ private void UpdateAdvancedPathDebugInfo(AGVPathResult advancedResult)
{
if (advancedResult == null || !advancedResult.Success)
{
diff --git a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
index 64318b7..50dc579 100644
--- a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
+++ b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
@@ -5,6 +5,7 @@ using System.Linq;
using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
+using AGVNavigationCore.PathFinding.Core;
using AGVNavigationCore.Controls;
namespace AGVSimulator.Models
@@ -36,7 +37,7 @@ namespace AGVSimulator.Models
///
/// 경로 완료 이벤트
///
- public event EventHandler PathCompleted;
+ public event EventHandler PathCompleted;
///
/// 오류 발생 이벤트
@@ -55,7 +56,7 @@ namespace AGVSimulator.Models
private float _currentSpeed;
// 경로 관련
- private PathResult _currentPath;
+ private AGVPathResult _currentPath;
private List _remainingNodes;
private int _currentNodeIndex;
private string _currentNodeId;
@@ -108,7 +109,7 @@ namespace AGVSimulator.Models
///
/// 현재 경로
///
- public PathResult CurrentPath => _currentPath;
+ public AGVPathResult CurrentPath => _currentPath;
///
/// 현재 노드 ID
@@ -180,7 +181,7 @@ namespace AGVSimulator.Models
///
/// 실행할 경로
/// 맵 노드 목록
- public void StartPath(PathResult path, List mapNodes)
+ public void StartPath(AGVPathResult path, List mapNodes)
{
if (path == null || !path.Success)
{
@@ -287,7 +288,7 @@ namespace AGVSimulator.Models
///
/// AGV 위치 직접 설정 (시뮬레이터용)
- /// 이전 위치를 TargetPosition으로 저장하여 리프트 방향 계산이 가능하도록 함
+ /// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
///
/// 새로운 위치
public void SetPosition(Point newPosition)
@@ -295,12 +296,12 @@ namespace AGVSimulator.Models
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
if (_currentPosition != Point.Empty)
{
- _targetPosition = _currentPosition;
+ _targetPosition = _currentPosition; // 이전 위치 (previousPos 역할)
}
-
+
// 새로운 위치 설정
_currentPosition = newPosition;
-
+
// 위치 변경 이벤트 발생
PositionChanged?.Invoke(this, _currentPosition);
}
@@ -341,16 +342,15 @@ namespace AGVSimulator.Models
///
/// 현재 RFID 시뮬레이션 (현재 위치 기준)
///
- public string SimulateRfidReading(List mapNodes, List rfidMappings)
+ public string SimulateRfidReading(List mapNodes)
{
// 현재 위치에서 가장 가까운 노드 찾기
var closestNode = FindClosestNode(_currentPosition, mapNodes);
if (closestNode == null)
return null;
- // 해당 노드의 RFID 매핑 찾기
- var mapping = rfidMappings.FirstOrDefault(m => m.LogicalNodeId == closestNode.NodeId);
- return mapping?.RfidId;
+ // 해당 노드의 RFID 정보 반환 (MapNode에 RFID 정보 포함)
+ return closestNode.HasRfid() ? closestNode.RfidId : null;
}
#endregion
diff --git a/Cs_HMI/CHANGELOG.md b/Cs_HMI/CHANGELOG.md
new file mode 100644
index 0000000..b87d286
--- /dev/null
+++ b/Cs_HMI/CHANGELOG.md
@@ -0,0 +1,95 @@
+# AGV HMI 시스템 변경 로그
+
+이 파일은 AGV HMI 시스템의 주요 변경 사항을 기록합니다.
+
+## [2024.12.16] - AGV 방향 표시 수정 및 타겟계산 기능 추가
+
+### 🐛 버그 수정
+- **AGV 후진 모터 시 리프트 방향 표시 오류 수정**
+ - **문제**: 008→007→006 후진 이동 시 리프트가 동남쪽으로 잘못 표시
+ - **원인**: LiftCalculator에서 후진 모터 방향 계산 로직 오류
+ - **수정파일**: `AGVNavigationCore\Utils\LiftCalculator.cs`
+ - **수정메서드**: `CalculateLiftAngleRadians()` :32-39행
+ - **수정내용**:
+ ```csharp
+ // 이전 (잘못된 계산)
+ var dx = targetPos.X - currentPos.X;
+ var dy = targetPos.Y - currentPos.Y;
+
+ // 수정 (올바른 계산)
+ var dx = currentPos.X - targetPos.X;
+ var dy = currentPos.Y - targetPos.Y;
+ ```
+ - **결과**: 후진 시 리프트가 AGV 이동 방향(북서쪽)을 올바르게 표시
+
+### ✨ 새로운 기능
+- **타겟계산 버튼 추가**
+ - **위치**: AGV 시뮬레이터 → 경로 제어 그룹박스
+ - **기능**:
+ - 버튼 클릭 후 맵에서 노드 클릭 시 자동 경로 계산
+ - 시작 RFID가 비어있으면 AGV 현재 위치로 자동 설정
+ - 연속 테스트 지원
+ - **구현파일**:
+ - `AGVSimulator\Forms\SimulatorForm.Designer.cs` - UI 컨트롤 추가
+ - `AGVSimulator\Forms\SimulatorForm.cs` - 이벤트 핸들러 구현
+ - **주요메서드**:
+ - `OnTargetCalc_Click()` - 타겟계산 버튼 이벤트
+ - `SetStartNodeFromAGVPosition()` - AGV 위치에서 시작 노드 자동 설정
+ - `SetTargetNodeInCombo()` - 목적지 콤보박스 설정
+ - `FindClosestNode()` - 가장 가까운 노드 찾기
+
+### 📝 문서 업데이트
+- **CLAUDE.md 문서 개선**
+ - AGV 하드웨어 구조 및 방향 체계 설명 추가
+ - 방향 계산 관련 주요 파일 및 함수 매핑 정보 추가
+ - 해결된 문제점과 새로운 기능 기록
+ - 다음 세션에서의 빠른 참조를 위한 구조화
+
+### 🔧 기술적 개선사항
+- **AGV 방향 시스템 이해도 향상**
+ - `LIFT --- AGV --- MONITOR` 하드웨어 구조 명확화
+ - 모터 방향과 리프트 위치 관계 정립
+ - 전진/후진에 따른 리프트 방향 로직 통일
+
+- **마우스 클릭 감지 개선**
+ - **문제**: 줌/팬 상태에서 노드 클릭 시 좌표 오차로 감지되지 않는 문제
+ - **원인**: 렌더링 Matrix 변환과 마우스 좌표 변환이 불일치
+ - **수정파일**: `AGVNavigationCore\\Controls\\UnifiedAGVCanvas.Mouse.cs`
+ - **수정메서드**: `ScreenToWorld()` 좌표 변환 함수
+ - **수정내용**:
+ ```csharp
+ // 수정 전: 수동 계산
+ var worldX = (int)((screenPoint.X - _panOffset.X) / _zoomFactor);
+
+ // 수정 후: Matrix 역변환 사용
+ transform.Scale(_zoomFactor, _zoomFactor);
+ transform.Translate(_panOffset.X, _panOffset.Y);
+ transform.Invert();
+ transform.TransformPoints(points);
+ ```
+ - **결과**: 줌/팬 상태에서도 정확한 노드 클릭 감지 가능
+
+---
+
+## 변경 로그 형식
+
+### 버전 표기
+- `[YYYY.MM.DD]` - 날짜 기반 버전 관리
+
+### 변경 유형
+- 🐛 **버그 수정**: 기존 기능의 오류 수정
+- ✨ **새로운 기능**: 새로 추가된 기능
+- 🔧 **기술적 개선**: 성능 향상, 리팩토링 등
+- 📝 **문서**: 문서 추가/수정
+- 🗑️ **제거**: 기능 또는 파일 제거
+- ⚠️ **중요 변경**: 호환성에 영향을 주는 변경
+
+### 기록 원칙
+- 사용자에게 영향을 주는 모든 변경사항 기록
+- 수정된 파일과 메서드명 명시
+- 문제의 원인과 해결 방법 설명
+- 테스트 결과나 검증 방법 포함
+
+---
+
+*이 체인지로그는 [Keep a Changelog](https://keepachangelog.com/) 형식을 따릅니다.*
\ No newline at end of file
diff --git a/Cs_HMI/CLAUDE.md b/Cs_HMI/CLAUDE.md
index ab0b25f..1b0d3e0 100644
--- a/Cs_HMI/CLAUDE.md
+++ b/Cs_HMI/CLAUDE.md
@@ -134,8 +134,107 @@ SubProject 내의 GitUpdate.bat을 사용하여 모든 하위 프로젝트를
### RFID 매핑 아키텍처 원칙
**중요**: 물리적 RFID ID는 의미없는 고유값으로 관리하고, 논리적 노드 ID와 별도 매핑 테이블로 분리하여 현장 유지보수성 향상
+## AGV 방향 제어 및 도킹 시스템 (2024.12.16)
+
+### 🔄 AGV 방향 체계 및 하드웨어 구조
+
+#### 하드웨어 레이아웃
+```
+LIFT --- AGV --- MONITOR
+ ↑ ↑ ↑
+후진시 AGV본체 전진시
+도달위치 도달위치
+```
+
+#### 모터 방향과 이동 방향
+- **전진 모터 (Forward)**: AGV가 모니터 방향으로 이동 (→)
+- **후진 모터 (Backward)**: AGV가 리프트 방향으로 이동 (←)
+
+#### 도킹 방향 규칙
+- **충전기 (Charging)**: 전진 도킹 (Forward) - 모니터가 충전기 면
+- **장비 (Docking)**: 후진 도킹 (Backward) - 리프트가 장비 면
+
+### 🧭 방향 계산 핵심 파일 및 함수
+
+#### 1. 리프트 방향 계산
+**파일**: `AGVNavigationCore\Utils\LiftCalculator.cs`
+- **핵심 메서드**: `CalculateLiftAngleRadians(Point currentPos, Point targetPos, AgvDirection motorDirection)`
+- **수정된 로직** (2024.12.16):
+ ```csharp
+ // 후진 모터: 현재→목표 벡터 (리프트가 이동 방향 향함)
+ var dx = currentPos.X - targetPos.X;
+ var dy = currentPos.Y - targetPos.Y;
+ ```
+
+#### 2. 도킹 방향 결정
+**파일**: `AGVNavigationCore\PathFinding\DirectionChangePlanner.cs`
+- **핵심 메서드**: `GetRequiredDockingDirection(string targetNodeId)`
+- **로직**:
+ ```csharp
+ case NodeType.Charging: return AgvDirection.Forward; // 충전기
+ case NodeType.Docking: return AgvDirection.Backward; // 장비
+ ```
+
+#### 3. 경로 계산 시 방향 고려
+**파일**: `AGVNavigationCore\PathFinding\AdvancedAGVPathfinder.cs`
+- **핵심 메서드**: `FindAdvancedPath(string startNodeId, string targetNodeId, AgvDirection currentDirection)`
+- **로직**:
+ ```csharp
+ var requiredDirection = _directionChangePlanner.GetRequiredDockingDirection(targetNodeId);
+ bool needDirectionChange = (currentDirection != requiredDirection);
+ ```
+
+### ⚠️ 현재 발견된 문제점 (2024.12.16)
+
+#### 문제 상황
+AGV가 후진 상태로 006 → 005 → 004 이동 중, 037(버퍼)에 도킹 필요한 상황에서:
+- **기대**: 006 → 005 → 037 (후진 직진 도킹)
+- **실제**: 006 → ... → 041 → 037 (전진으로 잘못 계산)
+
+#### 문제 원인 분석
+1. **도킹 방향 계산**: 037은 `NodeType.Docking`이므로 후진 도킹 필요 ✅
+2. **현재 방향**: AGV가 후진(Backward) 상태 ✅
+3. **경로 계산 오류**: 알고리즘이 현재 후진 상태를 제대로 고려하지 못함 ❌
+
+#### 필요한 수정사항
+1. **경로 검증 시스템**: 계산된 경로의 마지막 도킹 방향이 올바른지 검증
+2. **UI 피드백**: 잘못된 도킹 방향 시 목적지 노드에 X 표시
+3. **알고리즘 개선**: 현재 방향을 유지하며 직진 가능한 경로 우선 탐색
+
+### 🎯 향후 구현 계획
+1. **도킹 방향 검증 시스템** 구현
+2. **UI 시각적 피드백** (X 표시) 추가
+3. **경로 계산 알고리즘** 개선
+4. **통합 테스트** 및 검증
+
+## AGV 방향 계산 관련 주요 파일 및 함수 (Legacy)
+
+### 핵심 방향 계산 함수들
+1. **LiftCalculator.cs** - 리프트 방향 계산
+ - `CalculateLiftAngleRadians(Point currentPos, Point targetPos, AgvDirection motorDirection)` :1022행
+ - `CalculateLiftInfoWithPathPrediction()` :1024행
+ - 파일 위치: `AGVNavigationCore\Utils\LiftCalculator.cs`
+
+2. **AGVPathfinder.cs** - 모터 방향 결정
+ - `PlanDirectionChanges(List path)` :414행 - 경로별 모터방향 계획
+ - `GetRequiredDirectionForNode(MapNode node)` :280행 - 노드타입별 요구방향
+ - 파일 위치: `AGVNavigationCore\PathFinding\AGVPathfinder.cs`
+
+3. **VirtualAGV.cs** - 시뮬레이터 AGV 위치/방향 관리
+ - `SetPosition(Point newPosition)` :293행 - ⚠️ TargetPosition 설정 문제 위치
+ - 파일 위치: `AGVSimulator\Models\VirtualAGV.cs`
+
+4. **UnifiedAGVCanvas.Events.cs** - AGV 시각적 표현
+ - `DrawAGVLiftAdvanced(Graphics g, IAGV agv)` :1011행 - 리프트 그리기
+ - 파일 위치: `AGVNavigationCore\Controls\UnifiedAGVCanvas.Events.cs`
+
+### 📋 변경 내역
+최신 패치 및 개발 내역은 **[CHANGELOG.md](./CHANGELOG.md)** 파일을 참조하세요.
+
+- **최근 업데이트**: 2024.12.16 - AGV 방향 표시 수정 및 타겟계산 기능 추가
+
## 개발시 주의사항
-- 메인 애플리케이션은 Windows Forms 기반의 터치 인터페이스로 설계됨
+- 메인 애플리케이션은 Windows .NetFramework Forms 기반의 터치 인터페이스로 설계됨
- 실시간 AGV 제어 시스템이므로 상태 머신 로직 수정시 신중히 접근
- 통신 관련 코드 변경시 하드웨어 호환성 고려 필요
- **맵 에디터/시뮬레이터**: AGVMapEditor 프로젝트에 의존성이 있으므로 먼저 빌드 필요
diff --git a/Cs_HMI/SubProject/CommUtil b/Cs_HMI/SubProject/CommUtil
new file mode 160000
index 0000000..632b087
--- /dev/null
+++ b/Cs_HMI/SubProject/CommUtil
@@ -0,0 +1 @@
+Subproject commit 632b087c5be6b94ee953a374a25992aa54d56c7c
diff --git a/Cs_HMI/SubProject/EnigProtocol b/Cs_HMI/SubProject/EnigProtocol
new file mode 160000
index 0000000..77a9d40
--- /dev/null
+++ b/Cs_HMI/SubProject/EnigProtocol
@@ -0,0 +1 @@
+Subproject commit 77a9d4066214342ca31118c01039fd23f7e57f53
diff --git a/Cs_HMI/SubProject/arCtl b/Cs_HMI/SubProject/arCtl
new file mode 160000
index 0000000..768d71e
--- /dev/null
+++ b/Cs_HMI/SubProject/arCtl
@@ -0,0 +1 @@
+Subproject commit 768d71ebcad77e9035f35e6b75f2402166bcfa89
diff --git a/Cs_HMI/build.bat b/Cs_HMI/build.bat
index c219d05..4ce74ec 100644
--- a/Cs_HMI/build.bat
+++ b/Cs_HMI/build.bat
@@ -12,7 +12,7 @@ if not exist "C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild
REM Set MSBuild path
set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
-REM Build Debug x64 configuration
-%MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo
+REM Rebuild Debug x86 configuration (VS-style Rebuild)
+%MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild
pause
\ No newline at end of file
diff --git a/Cs_HMI/run.bat b/Cs_HMI/run.bat
deleted file mode 100644
index f6db54e..0000000
--- a/Cs_HMI/run.bat
+++ /dev/null
@@ -1,2 +0,0 @@
-dotnet build
-dotnet run --project .\Project\AGV4.csproj
\ No newline at end of file