From ef72b77f1cf248bb447d0bf436753e9377de0b54 Mon Sep 17 00:00:00 2001 From: ChiKyun Kim Date: Tue, 16 Sep 2025 14:46:53 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20PathFinding=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EB=8F=84?= =?UTF-8?q?=ED=82=B9=20=EC=97=90=EB=9F=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PathFinding 폴더를 Core, Validation, Planning, Analysis로 세분화 - 네임스페이스 정리 및 using 문 업데이트 - UnifiedAGVCanvas에 SetDockingError 메서드 추가 - 도킹 검증 시스템 인프라 구축 - DockingValidator 유틸리티 클래스 추가 - 빌드 오류 수정 및 안정성 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Cs_HMI/.claude/settings.local.json | 16 + Cs_HMI/AGVCSharp.sln | 1 + Cs_HMI/AGVMapEditor/AGVMapEditor.csproj | 2 - Cs_HMI/AGVMapEditor/Forms/MainForm.cs | 1 - Cs_HMI/AGVMapEditor/Models/NodeResolver.cs | 309 ------- Cs_HMI/AGVMapEditor/Models/PathCalculator.cs | 432 --------- .../AGVNavigationCore.csproj | 29 +- .../Controls/UnifiedAGVCanvas.Events.cs | 7 +- .../Controls/UnifiedAGVCanvas.cs | 60 +- Cs_HMI/AGVNavigationCore/Models/Enums.cs | 2 + .../AGVNavigationCore/Models/RfidMapping.cs | 79 -- .../PathFinding/AGVPathfinder.cs | 862 ------------------ .../{ => Analysis}/JunctionAnalyzer.cs | 3 +- .../PathFinding/{ => Core}/AGVPathResult.cs | 111 ++- .../PathFinding/{ => Core}/AStarPathfinder.cs | 25 +- .../PathFinding/{ => Core}/PathNode.cs | 2 +- .../PathFinding/PathResult.cs | 107 --- .../PathFinding/PathfindingOptions.cs | 67 -- .../AGVPathfinder.cs} | 99 +- .../{ => Planning}/DirectionChangePlanner.cs | 4 +- .../{ => Planning}/NodeMotorInfo.cs | 2 +- .../PathFinding/RfidBasedPathfinder.cs | 275 ------ .../PathFinding/RfidPathResult.cs | 229 ----- .../Validation/DockingValidationResult.cs | 103 +++ Cs_HMI/AGVNavigationCore/README.md | 310 +++---- .../Utils/DockingValidator.cs | 174 ++++ .../AGVNavigationCore/Utils/LiftCalculator.cs | 9 +- .../Forms/SimulatorForm.Designer.cs | 37 +- Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs | 295 ++++-- Cs_HMI/AGVSimulator/Models/VirtualAGV.cs | 24 +- Cs_HMI/CHANGELOG.md | 95 ++ Cs_HMI/CLAUDE.md | 101 +- Cs_HMI/SubProject/CommUtil | 1 + Cs_HMI/SubProject/EnigProtocol | 1 + Cs_HMI/SubProject/arCtl | 1 + Cs_HMI/build.bat | 4 +- Cs_HMI/run.bat | 2 - 37 files changed, 1085 insertions(+), 2796 deletions(-) create mode 100644 Cs_HMI/.claude/settings.local.json delete mode 100644 Cs_HMI/AGVMapEditor/Models/NodeResolver.cs delete mode 100644 Cs_HMI/AGVMapEditor/Models/PathCalculator.cs delete mode 100644 Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs delete mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs rename Cs_HMI/AGVNavigationCore/PathFinding/{ => Analysis}/JunctionAnalyzer.cs (99%) rename Cs_HMI/AGVNavigationCore/PathFinding/{ => Core}/AGVPathResult.cs (68%) rename Cs_HMI/AGVNavigationCore/PathFinding/{ => Core}/AStarPathfinder.cs (87%) rename Cs_HMI/AGVNavigationCore/PathFinding/{ => Core}/PathNode.cs (98%) delete mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs delete mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/PathfindingOptions.cs rename Cs_HMI/AGVNavigationCore/PathFinding/{AdvancedAGVPathfinder.cs => Planning/AGVPathfinder.cs} (76%) rename Cs_HMI/AGVNavigationCore/PathFinding/{ => Planning}/DirectionChangePlanner.cs (99%) rename Cs_HMI/AGVNavigationCore/PathFinding/{ => Planning}/NodeMotorInfo.cs (98%) delete mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs delete mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs create mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/Validation/DockingValidationResult.cs create mode 100644 Cs_HMI/AGVNavigationCore/Utils/DockingValidator.cs create mode 100644 Cs_HMI/CHANGELOG.md create mode 160000 Cs_HMI/SubProject/CommUtil create mode 160000 Cs_HMI/SubProject/EnigProtocol create mode 160000 Cs_HMI/SubProject/arCtl delete mode 100644 Cs_HMI/run.bat 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