From debbf712d4ab8d75ea51e0e670d146d97e6ea495 Mon Sep 17 00:00:00 2001 From: ChiKyun Kim Date: Mon, 15 Sep 2025 17:36:46 +0900 Subject: [PATCH] add files --- Cs_HMI/AGVCSharp.sln | 15 +++ .../PathFinding/AGVPathfinder.cs | 122 +++++++++++------- Cs_HMI/Project/AGV4.csproj | 4 +- Cs_HMI/SubProject/AGVControl/Models/AGV.cs | 95 ++++++++++++++ Cs_HMI/TestProject/Test_BMS/Form1.cs | 2 +- Cs_HMI/TestProject/Test_BMS/Test_BMS.csproj | 4 +- Cs_HMI/build.bat | 2 +- 7 files changed, 194 insertions(+), 50 deletions(-) diff --git a/Cs_HMI/AGVCSharp.sln b/Cs_HMI/AGVCSharp.sln index 4cfe026..d7dd581 100644 --- a/Cs_HMI/AGVCSharp.sln +++ b/Cs_HMI/AGVCSharp.sln @@ -35,8 +35,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "솔루션 항목", "솔루 ProjectSection(SolutionItems) = preProject build.bat = build.bat CLAUDE.md = CLAUDE.md + TODO.md = TODO.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVNavigationCore", "AGVNavigationCore\AGVNavigationCore.csproj", "{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -191,6 +194,18 @@ Global {B2C3D4E5-0000-0000-0000-000000000000}.Release|x64.Build.0 = Release|Any CPU {B2C3D4E5-0000-0000-0000-000000000000}.Release|x86.ActiveCfg = Release|Any CPU {B2C3D4E5-0000-0000-0000-000000000000}.Release|x86.Build.0 = Release|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Debug|x64.ActiveCfg = Debug|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Debug|x64.Build.0 = Debug|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Debug|x86.ActiveCfg = Debug|x86 + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Debug|x86.Build.0 = Debug|x86 + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|Any CPU.Build.0 = Release|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|x64.ActiveCfg = Release|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|x64.Build.0 = Release|Any CPU + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|x86.ActiveCfg = Release|x86 + {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs index afea6f5..81e318e 100644 --- a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs +++ b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs @@ -717,73 +717,107 @@ namespace AGVNavigationCore.PathFinding var startNode = _nodeMap[startNodeId]; - // 시작 노드에서 회전이 불가능하면 회전 가능한 가까운 노드 찾기 - if (!startNode.CanRotate) + // 방향 전환을 위한 가장 가까운 교차로 찾기 + var targetNodeId = originalResult.Path.LastOrDefault(); + var junctionNode = FindNearestJunctionForDirectionChange(startNodeId, targetNodeId); + if (junctionNode == null) { - var rotationNode = FindNearestRotationNode(startNodeId); - if (rotationNode == null) - { - return AGVPathResult.CreateFailure("방향 전환을 위한 회전 가능 노드를 찾을 수 없습니다.", originalResult.CalculationTimeMs); - } - - // 회전 노드로의 경로 추가 - var pathToRotationNode = _pathfinder.FindPath(startNodeId, rotationNode.NodeId); - if (!pathToRotationNode.Success) - { - return AGVPathResult.CreateFailure("방향 전환 노드로의 경로를 찾을 수 없습니다.", originalResult.CalculationTimeMs); - } - - // 회전 노드에서 원래 목적지로의 경로 계산 - var pathFromRotationNode = _pathfinder.FindPath(rotationNode.NodeId, originalResult.Path.Last()); - if (!pathFromRotationNode.Success) - { - return AGVPathResult.CreateFailure("방향 전환 후 목적지로의 경로를 찾을 수 없습니다.", originalResult.CalculationTimeMs); - } - - // 전체 경로 조합 - var combinedPath = new List(); - combinedPath.AddRange(pathToRotationNode.Path); - combinedPath.AddRange(pathFromRotationNode.Path.Skip(1)); // 중복 노드 제거 - - var combinedDistance = pathToRotationNode.TotalDistance + pathFromRotationNode.TotalDistance; - var combinedCommands = GenerateAGVCommandsWithDirectionChange(combinedPath, currentDirection, targetDirection, rotationNode.NodeId); - var nodeMotorInfos = GenerateNodeMotorInfos(combinedPath); - - return AGVPathResult.CreateSuccess(combinedPath, combinedCommands, nodeMotorInfos, combinedDistance, originalResult.CalculationTimeMs); + return AGVPathResult.CreateFailure("방향 전환을 위한 교차로를 찾을 수 없습니다.", originalResult.CalculationTimeMs); } - else + + // 교차로로의 경로 추가 + var pathToJunction = _pathfinder.FindPath(startNodeId, junctionNode.NodeId); + if (!pathToJunction.Success) { - // 시작 노드에서 바로 방향 전환 - var commandsWithRotation = GenerateAGVCommandsWithDirectionChange(originalResult.Path, currentDirection, targetDirection, startNodeId); - return AGVPathResult.CreateSuccess(originalResult.Path, commandsWithRotation, originalResult.NodeMotorInfos, originalResult.TotalDistance, originalResult.CalculationTimeMs); + 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 - /// 가장 가까운 회전 가능 노드 - private MapNode FindNearestRotationNode(string fromNodeId) + /// 목적지 노드 ID (경로상 교차로 우선 검색용) + /// 가장 가까운 교차로 노드 + private MapNode FindNearestJunctionForDirectionChange(string fromNodeId, string targetNodeId = null) { - var rotationNodes = _nodeMap.Values.Where(n => n.CanRotate && n.IsActive).ToList(); - + // 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 rotationNode in rotationNodes) + foreach (var junctionNode in junctionNodes) { - var pathResult = _pathfinder.FindPath(fromNodeId, rotationNode.NodeId); + var pathResult = _pathfinder.FindPath(fromNodeId, junctionNode.NodeId); if (pathResult.Success && pathResult.TotalDistance < shortestDistance) { shortestDistance = pathResult.TotalDistance; - nearestNode = rotationNode; + 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 명령어 생성 /// diff --git a/Cs_HMI/Project/AGV4.csproj b/Cs_HMI/Project/AGV4.csproj index 2c5af28..190b825 100644 --- a/Cs_HMI/Project/AGV4.csproj +++ b/Cs_HMI/Project/AGV4.csproj @@ -553,7 +553,7 @@ - rem xcopy "$(TargetDir)*.exe" "\\192.168.1.80\Amkor\AGV2" /Y -rem xcopy "$(TargetDir)*.dll" "\\192.168.1.80\Amkor\AGV2" /Y + + \ No newline at end of file diff --git a/Cs_HMI/SubProject/AGVControl/Models/AGV.cs b/Cs_HMI/SubProject/AGVControl/Models/AGV.cs index 07e2b26..8bbb410 100644 --- a/Cs_HMI/SubProject/AGVControl/Models/AGV.cs +++ b/Cs_HMI/SubProject/AGVControl/Models/AGV.cs @@ -146,6 +146,9 @@ namespace AGVControl.Models MovementHistory.Clear(); MovementHistory.AddRange(lastTwo); } + + // 위치 업데이트 시 자동으로 히스토리 파일에 저장 + SaveHistoryOnUpdate(); } // 연결 정보 기반 실제 이동 방향 계산 @@ -233,6 +236,98 @@ namespace AGVControl.Models } } + // 위치 히스토리 파일 저장 (최근 3개만 저장) + public void SavePositionHistory(string filePath) + { + try + { + // 최근 3개의 히스토리만 저장 + var recentHistory = MovementHistory.Skip(Math.Max(0, MovementHistory.Count - 3)).ToList(); + + var lines = new List(); + lines.Add($"# AGV Position History - {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + lines.Add("# Format: RFID,Direction,X,Y,Timestamp"); + + foreach (var history in recentHistory) + { + lines.Add($"{history.Value},{history.Direction},{history.Location.X},{history.Location.Y},{DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + } + + System.IO.File.WriteAllLines(filePath, lines); + } + catch (Exception ex) + { + // 로그 기록 (실제 환경에서는 로깅 시스템 사용) + System.Diagnostics.Debug.WriteLine($"SavePositionHistory Error: {ex.Message}"); + } + } + + // 위치 히스토리 파일 로드 + public bool LoadPositionHistory(string filePath) + { + try + { + if (!System.IO.File.Exists(filePath)) + return false; + + var lines = System.IO.File.ReadAllLines(filePath); + MovementHistory.Clear(); + + foreach (var line in lines) + { + // 주석 라인 건너뛰기 + if (line.StartsWith("#") || string.IsNullOrWhiteSpace(line)) + continue; + + var parts = line.Split(','); + if (parts.Length >= 4) + { + if (UInt16.TryParse(parts[0], out UInt16 rfidValue) && + Enum.TryParse(parts[1], out AgvDir direction) && + int.TryParse(parts[2], out int x) && + int.TryParse(parts[3], out int y)) + { + MovementHistory.Add(new movehistorydata + { + Value = rfidValue, + Direction = direction, + Location = new Point(x, y) + }); + } + } + } + + return MovementHistory.Count > 0; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"LoadPositionHistory Error: {ex.Message}"); + return false; + } + } + + // 시작 시 위치 히스토리 자동 로드 + public void LoadHistoryOnStartup() + { + string historyFilePath = System.IO.Path.Combine( + System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), + "agv_position_history.dat" + ); + + LoadPositionHistory(historyFilePath); + } + + // 위치 업데이트 시 자동 저장 + public void SaveHistoryOnUpdate() + { + string historyFilePath = System.IO.Path.Combine( + System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), + "agv_position_history.dat" + ); + + SavePositionHistory(historyFilePath); + } + } diff --git a/Cs_HMI/TestProject/Test_BMS/Form1.cs b/Cs_HMI/TestProject/Test_BMS/Form1.cs index e6a6518..dbcb383 100644 --- a/Cs_HMI/TestProject/Test_BMS/Form1.cs +++ b/Cs_HMI/TestProject/Test_BMS/Form1.cs @@ -17,7 +17,7 @@ namespace Test_BMS InitializeComponent(); bms = new arDev.BMS(); bms.BMSDataReceive += Bms_BMSDataReceive; - bms.Message += Bms_Message; + //bms.Message += Bms_Message; } private void Bms_Message(object sender, arDev.arRS232.MessageEventArgs e) diff --git a/Cs_HMI/TestProject/Test_BMS/Test_BMS.csproj b/Cs_HMI/TestProject/Test_BMS/Test_BMS.csproj index 9bfe482..d150d22 100644 --- a/Cs_HMI/TestProject/Test_BMS/Test_BMS.csproj +++ b/Cs_HMI/TestProject/Test_BMS/Test_BMS.csproj @@ -97,7 +97,7 @@ - xcopy "$(TargetDir)*.exe" "\\192.168.1.80\Amkor\AGV2\Test\BMS" /Y -xcopy "$(TargetDir)*.dll" "\\192.168.1.80\Amkor\AGV2\Test\BMS" /Y + + \ No newline at end of file diff --git a/Cs_HMI/build.bat b/Cs_HMI/build.bat index de49611..c219d05 100644 --- a/Cs_HMI/build.bat +++ b/Cs_HMI/build.bat @@ -1,5 +1,5 @@ @echo off -echo Building V2GDecoder VC++ Project... +echo Building AGV C# HMI Project... REM Check if Visual Studio 2022 is installed if not exist "C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" (