diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs
index 4f518ba..f200ce6 100644
--- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs
+++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs
@@ -559,253 +559,7 @@ namespace AGVNavigationCore.Models
#endregion
- #region Directional Navigation
-
- ///
- /// 현재 이전/현재 위치와 이동 방향을 기반으로 다음 노드 ID를 반환
- ///
- /// 사용 예시:
- /// - 001에서 002로 이동 후, Forward 선택 → 003 반환
- /// - 003에서 004로 이동 후, Right 선택 → 030 반환
- /// - 004에서 003으로 Backward 이동 중, GetNextNodeId(Backward) → 002 반환
- ///
- /// 전제조건: SetPosition이 최소 2번 이상 호출되어 _prevPosition이 설정되어야 함
- ///
- /// 이동 방향 (Forward/Backward/Left/Right)
- /// 맵의 모든 노드
- /// 다음 노드 ID (또는 null)
- public MapNode GetNextNodeId(AgvDirection direction, List allNodes)
- {
- // 전제조건 검증: 2개 위치 히스토리 필요
- if ( _prevNode == null)
- {
- return null;
- }
-
- if (_currentNode == null || allNodes == null || allNodes.Count == 0)
- {
- return null;
- }
-
- // 현재 노드에 연결된 노드들 가져오기
- var connectedNodeIds = _currentNode.ConnectedNodes;
- if (connectedNodeIds == null || connectedNodeIds.Count == 0)
- {
- return null;
- }
-
- // 연결된 노드 중 현재 노드가 아닌 것들만 필터링
- var candidateNodes = allNodes.Where(n =>
- connectedNodeIds.Contains(n.NodeId) && n.NodeId != _currentNode.NodeId
- ).ToList();
-
- if (candidateNodes.Count == 0)
- {
- return null;
- }
-
- // 이전→현재 벡터 계산 (진행 방향 벡터)
- var movementVector = new PointF(
- _currentPosition.X - _prevPosition.X,
- _currentPosition.Y - _prevPosition.Y
- );
-
- // 벡터 정규화
- var movementLength = (float)Math.Sqrt(
- movementVector.X * movementVector.X +
- movementVector.Y * movementVector.Y
- );
-
- if (movementLength < 0.001f) // 거의 이동하지 않음
- {
- return candidateNodes[0]; // 첫 번째 연결 노드 반환
- }
-
- var normalizedMovement = new PointF(
- movementVector.X / movementLength,
- movementVector.Y / movementLength
- );
-
- // 각 후보 노드에 대해 방향 점수 계산
- var bestCandidate = (node: (MapNode)null, score: 0.0f);
-
- foreach (var candidate in candidateNodes)
- {
- var toNextVector = new PointF(
- candidate.Position.X - _currentPosition.X,
- candidate.Position.Y - _currentPosition.Y
- );
-
- var toNextLength = (float)Math.Sqrt(
- toNextVector.X * toNextVector.X +
- toNextVector.Y * toNextVector.Y
- );
-
- if (toNextLength < 0.001f)
- {
- continue;
- }
-
- var normalizedToNext = new PointF(
- toNextVector.X / toNextLength,
- toNextVector.Y / toNextLength
- );
-
- // 진행 방향 기반 점수 계산
- float score = CalculateDirectionalScore(
- normalizedMovement,
- normalizedToNext,
- direction
- );
-
- if (score > bestCandidate.score)
- {
- bestCandidate = (candidate, score);
- }
- }
-
- return bestCandidate.node;
- }
-
- ///
- /// 이동 방향을 기반으로 방향 점수를 계산
- /// 높은 점수 = 더 나은 선택지
- ///
- private float CalculateDirectionalScore(
- PointF movementDirection, // 정규화된 이전→현재 벡터
- PointF nextDirection, // 정규화된 현재→다음 벡터
- AgvDirection requestedDir) // 요청된 이동 방향
- {
- // 벡터 간 내적 계산 (유사도: -1 ~ 1)
- float dotProduct = (movementDirection.X * nextDirection.X) +
- (movementDirection.Y * nextDirection.Y);
-
- // 벡터 간 외적 계산 (좌우 판별: Z 성분)
- // 양수 = 좌측, 음수 = 우측
- float crossProduct = (movementDirection.X * nextDirection.Y) -
- (movementDirection.Y * nextDirection.X);
-
- float baseScore = 0;
-
- switch (requestedDir)
- {
- case AgvDirection.Forward:
- // Forward: 현재 모터 상태에 따라 다름
- // 1) 현재 Forward 모터 상태라면 → 같은 경로 (계속 전진)
- // 2) 현재 Backward 모터 상태라면 → 반대 경로 (모터 방향 전환)
- if (_currentDirection == AgvDirection.Forward)
- {
- // 이미 Forward 상태, 계속 Forward → 같은 경로
- if (dotProduct > 0.9f)
- baseScore = 100.0f;
- else if (dotProduct > 0.5f)
- baseScore = 80.0f;
- else if (dotProduct > 0.0f)
- baseScore = 50.0f;
- else if (dotProduct > -0.5f)
- baseScore = 20.0f;
- }
- else
- {
- // Backward 상태에서 Forward로 → 반대 경로
- if (dotProduct < -0.9f)
- baseScore = 100.0f;
- else if (dotProduct < -0.5f)
- baseScore = 80.0f;
- else if (dotProduct < 0.0f)
- baseScore = 50.0f;
- else if (dotProduct < 0.5f)
- baseScore = 20.0f;
- }
- break;
-
- case AgvDirection.Backward:
- // Backward: 현재 모터 상태에 따라 다름
- // 1) 현재 Backward 모터 상태라면 → 같은 경로 (Forward와 동일)
- // 2) 현재 Forward 모터 상태라면 → 반대 경로 (현재의 Backward와 반대)
- if (_currentDirection == AgvDirection.Backward)
- {
- // 이미 Backward 상태, 계속 Backward → 같은 경로
- if (dotProduct > 0.9f)
- baseScore = 100.0f;
- else if (dotProduct > 0.5f)
- baseScore = 80.0f;
- else if (dotProduct > 0.0f)
- baseScore = 50.0f;
- else if (dotProduct > -0.5f)
- baseScore = 20.0f;
- }
- else
- {
- // Forward 상태에서 Backward로 → 반대 경로
- if (dotProduct < -0.9f)
- baseScore = 100.0f;
- else if (dotProduct < -0.5f)
- baseScore = 80.0f;
- else if (dotProduct < 0.0f)
- baseScore = 50.0f;
- else if (dotProduct < 0.5f)
- baseScore = 20.0f;
- }
- break;
-
- case AgvDirection.Left:
- // Left: 좌측 방향 선호
- if (dotProduct > 0.0f) // Forward 상태
- {
- if (crossProduct > 0.5f)
- baseScore = 100.0f;
- else if (crossProduct > 0.0f)
- baseScore = 70.0f;
- else if (crossProduct > -0.5f)
- baseScore = 50.0f;
- else
- baseScore = 30.0f;
- }
- else // Backward 상태 - 좌우 반전
- {
- if (crossProduct < -0.5f)
- baseScore = 100.0f;
- else if (crossProduct < 0.0f)
- baseScore = 70.0f;
- else if (crossProduct < 0.5f)
- baseScore = 50.0f;
- else
- baseScore = 30.0f;
- }
- break;
-
- case AgvDirection.Right:
- // Right: 우측 방향 선호
- if (dotProduct > 0.0f) // Forward 상태
- {
- if (crossProduct < -0.5f)
- baseScore = 100.0f;
- else if (crossProduct < 0.0f)
- baseScore = 70.0f;
- else if (crossProduct < 0.5f)
- baseScore = 50.0f;
- else
- baseScore = 30.0f;
- }
- else // Backward 상태 - 좌우 반전
- {
- if (crossProduct > 0.5f)
- baseScore = 100.0f;
- else if (crossProduct > 0.0f)
- baseScore = 70.0f;
- else if (crossProduct > -0.5f)
- baseScore = 50.0f;
- else
- baseScore = 30.0f;
- }
- break;
- }
-
- return baseScore;
- }
-
- #endregion
+
#region Cleanup
diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs
index e7adebf..ed8105e 100644
--- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs
+++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs
@@ -66,17 +66,17 @@ namespace AGVNavigationCore.PathFinding.Core
{
var pathNode = _nodeMap[mapNode.NodeId];
- foreach (var connectedNodeId in mapNode.ConnectedNodes)
+ foreach (var connectedNode in mapNode.ConnectedNodes)
{
- if (_nodeMap.ContainsKey(connectedNodeId))
+ if (_nodeMap.ContainsKey(connectedNode))
{
// 양방향 연결 생성 (단일 연결이 양방향을 의미)
- if (!pathNode.ConnectedNodes.Contains(connectedNodeId))
+ if (!pathNode.ConnectedNodes.Contains(connectedNode))
{
- pathNode.ConnectedNodes.Add(connectedNodeId);
+ pathNode.ConnectedNodes.Add(connectedNode);
}
- var connectedPathNode = _nodeMap[connectedNodeId];
+ var connectedPathNode = _nodeMap[connectedNode];
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId))
{
connectedPathNode.ConnectedNodes.Add(mapNode.NodeId);
@@ -594,13 +594,13 @@ namespace AGVNavigationCore.PathFinding.Core
foreach (var connectedNodeId in junctionNode.ConnectedNodes)
{
+ if (connectedNodeId == null) continue;
+
// 조건 1: 왔던 길이 아님
- if (connectedNodeId == previousNodeId)
- continue;
+ if (connectedNodeId == previousNodeId) continue;
// 조건 2: 갈 길이 아님
- if (connectedNodeId == targetNodeId)
- continue;
+ if (connectedNodeId == targetNodeId) continue;
// 조건 3, 4, 5: 존재하고, 활성 상태이고, 네비게이션 가능
var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs
index afb699c..aebb94c 100644
--- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs
+++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs
@@ -22,6 +22,8 @@ namespace AGVNavigationCore.PathFinding.Planning
private readonly JunctionAnalyzer _junctionAnalyzer;
private readonly DirectionChangePlanner _directionChangePlanner;
+
+
public AGVPathfinder(List mapNodes)
{
_mapNodes = mapNodes ?? new List();
@@ -114,7 +116,7 @@ namespace AGVNavigationCore.PathFinding.Planning
return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0);
if (prevNode == null)
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
- if (startNode == targetNode)
+ if (startNode.NodeId == targetNode.NodeId && targetNode.DockDirection.MatchAGVDirection(prevDirection))
return AGVPathResult.CreateFailure("목적지와 현재위치가 동일합니다.", 0, 0);
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
@@ -258,77 +260,101 @@ namespace AGVNavigationCore.PathFinding.Planning
MakeDetailData(path2, currentDirection);
}
+ MapNode tempNode = null;
+
+
//3.방향전환을 위환 대체 노드찾기
- var tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
- path1.Path[path1.Path.Count - 2].NodeId,
- path2.Path[1].NodeId);
+ tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
+ path1.Path[path1.Path.Count - 2].NodeId,
+ path2.Path[1].NodeId);
//4. path1 + tempnode + path2 가 최종 위치가 된다.
if (tempNode == null)
return AGVPathResult.CreateFailure("방향 전환을 위한 대체 노드를 찾을 수 없습니다.", 0, 0);
+
// path1 (시작 → 교차로)
var combinedResult = path1;
- // 교차로 → 대체노드 경로 계산
- var pathToTemp = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode.NodeId);
- pathToTemp.PrevNode = JunctionInPath;
- pathToTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
- if (!pathToTemp.Success)
- return AGVPathResult.CreateFailure("교차로에서 대체 노드까지의 경로를 찾을 수 없습니다.", 0, 0);
- if (ReverseCheck) MakeDetailData(pathToTemp, ReverseDirection);
- else MakeDetailData(pathToTemp, currentDirection);
-
- //교차로찍고 원래방향으로 돌어가야한다.
- if (pathToTemp.DetailedPath.Count > 1)
- pathToTemp.DetailedPath[pathToTemp.DetailedPath.Count - 1].MotorDirection = currentDirection;
-
- // path1 + pathToTemp 합치기
- combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp);
-
- // 대체노드 → 교차로 경로 계산 (역방향)
- var pathFromTemp = _basicPathfinder.FindPath(tempNode.NodeId, JunctionInPath.NodeId);
- pathFromTemp.PrevNode = JunctionInPath;
- pathFromTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
- if (!pathFromTemp.Success)
- return AGVPathResult.CreateFailure("대체 노드에서 교차로까지의 경로를 찾을 수 없습니다.", 0, 0);
-
- if (ReverseCheck) MakeDetailData(pathFromTemp, currentDirection);
- else MakeDetailData(pathFromTemp, ReverseDirection);
-
- // (path1 + pathToTemp) + pathFromTemp 합치기
- combinedResult = _basicPathfinder.CombineResults(combinedResult, pathFromTemp);
-
- //대체노드에서 최종 목적지를 다시 확인한다.
- if ((currentDirection == AgvDirection.Forward && targetNode.DockDirection != DockingDirection.Forward) ||
- (currentDirection == AgvDirection.Backward && targetNode.DockDirection != DockingDirection.Backward))
+ //교차로 대체노드를 사용한 경우
+ //if (tempNode != null)
{
- //목적지와 방향이 맞지 않다. 그러므로 대체노드를 추가로 더 찾아야한다.
- var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
- combinedResult.Path[combinedResult.Path.Count - 2].NodeId,
- path2.Path[1].NodeId);
-
- var pathToTemp2 = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode2.NodeId);
- if (ReverseCheck) MakeDetailData(pathToTemp2, currentDirection);
- else MakeDetailData(pathToTemp2, ReverseDirection);
-
- combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp2);
+ // 교차로 → 대체노드 경로 계산
+ var pathToTemp = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode.NodeId);
+ pathToTemp.PrevNode = JunctionInPath;
+ pathToTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
+ if (!pathToTemp.Success)
+ return AGVPathResult.CreateFailure("교차로에서 대체 노드까지의 경로를 찾을 수 없습니다.", 0, 0);
+ if (ReverseCheck) MakeDetailData(pathToTemp, ReverseDirection);
+ else MakeDetailData(pathToTemp, currentDirection);
//교차로찍고 원래방향으로 돌어가야한다.
- if (combinedResult.DetailedPath.Count > 1)
+ if (pathToTemp.DetailedPath.Count > 1)
+ pathToTemp.DetailedPath[pathToTemp.DetailedPath.Count - 1].MotorDirection = currentDirection;
+
+ // path1 + pathToTemp 합치기
+ combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp);
+
+ // 대체노드 → 교차로 경로 계산 (역방향)
+ var pathFromTemp = _basicPathfinder.FindPath(tempNode.NodeId, JunctionInPath.NodeId);
+ pathFromTemp.PrevNode = JunctionInPath;
+ pathFromTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
+ if (!pathFromTemp.Success)
+ return AGVPathResult.CreateFailure("대체 노드에서 교차로까지의 경로를 찾을 수 없습니다.", 0, 0);
+
+ if (ReverseCheck) MakeDetailData(pathFromTemp, currentDirection);
+ else MakeDetailData(pathFromTemp, ReverseDirection);
+
+ // (path1 + pathToTemp) + pathFromTemp 합치기
+ combinedResult = _basicPathfinder.CombineResults(combinedResult, pathFromTemp);
+
+ //현재까지 노드에서 목적지까지의 방향이 일치하면 그대로 사용한다.
+ bool temp3ok = false;
+ var TempCheck3 = _basicPathfinder.FindPath(combinedResult.Path.Last().NodeId, targetNode.NodeId);
+ if (TempCheck3.Path.First().NodeId.Equals(combinedResult.Path.Last().NodeId))
{
- if (ReverseCheck)
- combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = ReverseDirection;
- else
- combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = currentDirection;
+ if (targetNode.DockDirection == DockingDirection.Forward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Forward)
+ {
+ temp3ok = true;
+ }
+ else if (targetNode.DockDirection == DockingDirection.Backward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Backward)
+ {
+ temp3ok = true;
+ }
}
- var pathToTemp3 = _basicPathfinder.FindPath(tempNode2.NodeId, JunctionInPath.NodeId);
- if (ReverseCheck) MakeDetailData(pathToTemp3, ReverseDirection);
- else MakeDetailData(pathToTemp3, currentDirection);
- combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp3);
+
+ //대체노드에서 최종 목적지를 다시 확인한다.
+ if (temp3ok == false)
+ {
+ //목적지와 방향이 맞지 않다. 그러므로 대체노드를 추가로 더 찾아야한다.
+ var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
+ combinedResult.Path[combinedResult.Path.Count - 2].NodeId,
+ path2.Path[1].NodeId);
+
+ var pathToTemp2 = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode2.NodeId);
+ if (ReverseCheck) MakeDetailData(pathToTemp2, currentDirection);
+ else MakeDetailData(pathToTemp2, ReverseDirection);
+
+ combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp2);
+
+ //교차로찍고 원래방향으로 돌어가야한다.
+ if (combinedResult.DetailedPath.Count > 1)
+ {
+ if (ReverseCheck)
+ combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = ReverseDirection;
+ else
+ combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = currentDirection;
+ }
+
+ var pathToTemp3 = _basicPathfinder.FindPath(tempNode2.NodeId, JunctionInPath.NodeId);
+ if (ReverseCheck) MakeDetailData(pathToTemp3, ReverseDirection);
+ else MakeDetailData(pathToTemp3, currentDirection);
+
+ combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp3);
+ }
}
// (path1 + pathToTemp + pathFromTemp) + path2 합치기
diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs
index 7ee251e..389b94e 100644
--- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs
+++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs
@@ -15,6 +15,21 @@ namespace AGVNavigationCore.Utils
///
public static class DirectionalHelper
{
+
+ ///
+ /// AGV방향과 일치하는지 확인한다. 단 원본위치에서 dock 위치가 Don't Care 라면 true가 반환 됩니다.
+ ///
+ ///
+ ///
+ ///
+ public static bool MatchAGVDirection(this DockingDirection dock, AgvDirection agvdirection)
+ {
+ if (dock == DockingDirection.DontCare) return true;
+ if (dock == DockingDirection.Forward && agvdirection == AgvDirection.Forward) return true;
+ if (dock == DockingDirection.Backward && agvdirection == AgvDirection.Backward) return true;
+ return false;
+ }
+
private static JunctionAnalyzer _junctionAnalyzer;
///
diff --git a/Cs_HMI/AGVLogic/AGVSimulator/AGVSimulator.csproj b/Cs_HMI/AGVLogic/AGVSimulator/AGVSimulator.csproj
index 2b0e79b..9088fe2 100644
--- a/Cs_HMI/AGVLogic/AGVSimulator/AGVSimulator.csproj
+++ b/Cs_HMI/AGVLogic/AGVSimulator/AGVSimulator.csproj
@@ -45,6 +45,7 @@
+
diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/ProgressLogForm.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/ProgressLogForm.cs
new file mode 100644
index 0000000..c06eac7
--- /dev/null
+++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/ProgressLogForm.cs
@@ -0,0 +1,356 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Text;
+using System.Windows.Forms;
+
+namespace AGVSimulator.Forms
+{
+ ///
+ /// 경로 예측 테스트 결과 로그 항목
+ ///
+ public class PathTestLogItem
+ {
+ public string PreviousPosition { get; set; }
+ public string MotorDirection { get; set; } // 정방향 or 역방향
+ public string CurrentPosition { get; set; }
+ public string TargetPosition { get; set; }
+ public string DockingPosition { get; set; } // 도킹위치
+ public bool Success { get; set; }
+ public string Message { get; set; }
+ public string DetailedPath { get; set; }
+ public DateTime Timestamp { get; set; }
+
+ public PathTestLogItem()
+ {
+ Timestamp = DateTime.Now;
+ }
+ }
+
+ ///
+ /// 경로 예측 테스트 진행 상황 로그 표시 폼
+ ///
+ public partial class ProgressLogForm : Form
+ {
+ private ListView _logListView;
+ private Button _closeButton;
+ private Button _cancelButton;
+ private Button _saveCSVButton;
+ private ProgressBar _progressBar;
+ private Label _statusLabel;
+ private List _logItems;
+
+ ///
+ /// 취소 요청 여부
+ ///
+ public bool CancelRequested { get; private set; }
+
+ public ProgressLogForm()
+ {
+ InitializeComponent();
+ CancelRequested = false;
+ _logItems = new List();
+ }
+
+ private void InitializeComponent()
+ {
+ this.Text = "경로 예측 테스트 진행 상황";
+ this.Size = new Size(1200, 600);
+ this.StartPosition = FormStartPosition.CenterParent;
+ this.FormBorderStyle = FormBorderStyle.Sizable;
+ this.MinimumSize = new Size(800, 400);
+
+ // 상태 레이블
+ _statusLabel = new Label
+ {
+ Text = "준비 중...",
+ Dock = DockStyle.Top,
+ Height = 30,
+ TextAlign = ContentAlignment.MiddleLeft,
+ Padding = new Padding(10, 5, 10, 5),
+ Font = new Font("맑은 고딕", 10F, FontStyle.Bold)
+ };
+
+ // 프로그레스바
+ _progressBar = new ProgressBar
+ {
+ Dock = DockStyle.Top,
+ Height = 25,
+ Style = ProgressBarStyle.Continuous,
+ Minimum = 0,
+ Maximum = 100
+ };
+
+ // 로그 리스트뷰
+ _logListView = new ListView
+ {
+ Dock = DockStyle.Fill,
+ View = View.Details,
+ FullRowSelect = true,
+ GridLines = true,
+ Font = new Font("맑은 고딕", 9F),
+ BackColor = Color.White
+ };
+
+ // 컬럼 추가
+ _logListView.Columns.Add("이전위치", 80);
+ _logListView.Columns.Add("모터방향", 80);
+ _logListView.Columns.Add("현재위치", 80);
+ _logListView.Columns.Add("대상위치", 80);
+ _logListView.Columns.Add("도킹위치", 80);
+ _logListView.Columns.Add("성공", 50);
+ _logListView.Columns.Add("메세지", 180);
+ _logListView.Columns.Add("상세경로", 350);
+ _logListView.Columns.Add("시간", 90);
+
+ // 버튼 패널
+ var buttonPanel = new Panel
+ {
+ Dock = DockStyle.Bottom,
+ Height = 50
+ };
+
+ _cancelButton = new Button
+ {
+ Text = "취소",
+ Size = new Size(100, 30),
+ Location = new Point(10, 10),
+ Enabled = true
+ };
+ _cancelButton.Click += OnCancel_Click;
+
+ _closeButton = new Button
+ {
+ Text = "닫기",
+ Size = new Size(100, 30),
+ Location = new Point(120, 10),
+ Enabled = false
+ };
+ _closeButton.Click += OnClose_Click;
+
+ _saveCSVButton = new Button
+ {
+ Text = "CSV 저장",
+ Size = new Size(100, 30),
+ Location = new Point(230, 10),
+ Enabled = true
+ };
+ _saveCSVButton.Click += OnSaveCSV_Click;
+
+ buttonPanel.Controls.Add(_cancelButton);
+ buttonPanel.Controls.Add(_closeButton);
+ buttonPanel.Controls.Add(_saveCSVButton);
+
+ this.Controls.Add(_logListView);
+ this.Controls.Add(_progressBar);
+ this.Controls.Add(_statusLabel);
+ this.Controls.Add(buttonPanel);
+ }
+
+ ///
+ /// 로그 추가 (PathTestLogItem)
+ ///
+ public void AddLogItem(PathTestLogItem item)
+ {
+ if (InvokeRequired)
+ {
+ Invoke(new Action(AddLogItem), item);
+ return;
+ }
+
+ _logItems.Add(item);
+
+ var listItem = new ListViewItem(item.PreviousPosition ?? "-");
+ listItem.SubItems.Add(item.MotorDirection ?? "-");
+ listItem.SubItems.Add(item.CurrentPosition ?? "-");
+ listItem.SubItems.Add(item.TargetPosition ?? "-");
+ listItem.SubItems.Add(item.DockingPosition ?? "-");
+ listItem.SubItems.Add(item.Success ? "O" : "X");
+ listItem.SubItems.Add(item.Message ?? "-");
+ listItem.SubItems.Add(item.DetailedPath ?? "-");
+ listItem.SubItems.Add(item.Timestamp.ToString("HH:mm:ss"));
+
+ // 성공 여부에 따라 색상 설정
+ if (!item.Success)
+ {
+ listItem.BackColor = Color.LightPink;
+ }
+
+ _logListView.Items.Add(listItem);
+ _logListView.EnsureVisible(_logListView.Items.Count - 1);
+ }
+
+ ///
+ /// 간단한 텍스트 로그 추가 (상태 메시지용)
+ ///
+ public void AppendLog(string message)
+ {
+ var item = new PathTestLogItem
+ {
+ Message = message,
+ Success = true
+ };
+ AddLogItem(item);
+ }
+
+ ///
+ /// 상태 메시지 업데이트
+ ///
+ public void UpdateStatus(string status)
+ {
+ if (InvokeRequired)
+ {
+ Invoke(new Action(UpdateStatus), status);
+ return;
+ }
+
+ _statusLabel.Text = status;
+ }
+
+ ///
+ /// 프로그레스바 업데이트
+ ///
+ public void UpdateProgress(int value, int maximum)
+ {
+ if (InvokeRequired)
+ {
+ Invoke(new Action(UpdateProgress), value, maximum);
+ return;
+ }
+
+ _progressBar.Maximum = maximum;
+ _progressBar.Value = Math.Min(value, maximum);
+ }
+
+ ///
+ /// 작업 완료 시 호출
+ ///
+ public void SetCompleted()
+ {
+ if (InvokeRequired)
+ {
+ Invoke(new Action(SetCompleted));
+ return;
+ }
+
+ _cancelButton.Enabled = false;
+ _closeButton.Enabled = true;
+ UpdateStatus("작업 완료");
+ }
+
+ ///
+ /// 작업 취소 시 호출
+ ///
+ public void SetCancelled()
+ {
+ if (InvokeRequired)
+ {
+ Invoke(new Action(SetCancelled));
+ return;
+ }
+
+ _cancelButton.Enabled = false;
+ _closeButton.Enabled = true;
+ UpdateStatus("작업 취소됨");
+ }
+
+ private void OnCancel_Click(object sender, EventArgs e)
+ {
+ var result = MessageBox.Show(
+ "진행 중인 작업을 취소하시겠습니까?",
+ "취소 확인",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question);
+
+ if (result == DialogResult.Yes)
+ {
+ CancelRequested = true;
+ _cancelButton.Enabled = false;
+ UpdateStatus("취소 요청됨...");
+ AppendLog("사용자가 취소를 요청했습니다.");
+ }
+ }
+
+ private void OnSaveCSV_Click(object sender, EventArgs e)
+ {
+ if (_logItems.Count == 0)
+ {
+ MessageBox.Show("저장할 데이터가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ using (var saveDialog = new SaveFileDialog())
+ {
+ saveDialog.Filter = "CSV 파일 (*.csv)|*.csv|모든 파일 (*.*)|*.*";
+ saveDialog.DefaultExt = "csv";
+ saveDialog.FileName = $"경로예측테스트_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
+
+ if (saveDialog.ShowDialog() == DialogResult.OK)
+ {
+ try
+ {
+ SaveToCSV(saveDialog.FileName);
+ MessageBox.Show($"CSV 파일이 저장되었습니다.\n{saveDialog.FileName}",
+ "저장 완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"CSV 저장 중 오류가 발생했습니다:\n{ex.Message}",
+ "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+ }
+ }
+
+ ///
+ /// CSV 파일로 저장
+ ///
+ private void SaveToCSV(string filePath)
+ {
+ using (var writer = new StreamWriter(filePath, false, Encoding.UTF8))
+ {
+ // 헤더 작성
+ writer.WriteLine("이전위치,모터방향,현재위치,대상위치,도킹위치,성공,메세지,상세경로,시간");
+
+ // 데이터 작성
+ foreach (var item in _logItems)
+ {
+ var line = $"{EscapeCSV(item.PreviousPosition)}," +
+ $"{EscapeCSV(item.MotorDirection)}," +
+ $"{EscapeCSV(item.CurrentPosition)}," +
+ $"{EscapeCSV(item.TargetPosition)}," +
+ $"{EscapeCSV(item.DockingPosition)}," +
+ $"{(item.Success ? "O" : "X")}," +
+ $"{EscapeCSV(item.Message)}," +
+ $"{EscapeCSV(item.DetailedPath)}," +
+ $"{item.Timestamp:yyyy-MM-dd HH:mm:ss}";
+
+ writer.WriteLine(line);
+ }
+ }
+ }
+
+ ///
+ /// CSV 셀 데이터 이스케이프 처리
+ ///
+ private string EscapeCSV(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return "";
+
+ // 쉼표, 큰따옴표, 줄바꿈이 있으면 큰따옴표로 감싸고 내부 큰따옴표는 두 개로
+ if (value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r"))
+ {
+ return "\"" + value.Replace("\"", "\"\"") + "\"";
+ }
+
+ return value;
+ }
+
+ private void OnClose_Click(object sender, EventArgs e)
+ {
+ this.Close();
+ }
+ }
+}
diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs
index 20f5d69..0ebf626 100644
--- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs
+++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs
@@ -45,6 +45,7 @@ namespace AGVSimulator.Forms
///
private void InitializeComponent()
{
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SimulatorForm));
this._menuStrip = new System.Windows.Forms.MenuStrip();
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.openMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -74,9 +75,12 @@ namespace AGVSimulator.Forms
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
this.resetZoomToolStripButton = new System.Windows.Forms.ToolStripButton();
+ this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator();
+ this.toolStripButton1 = new System.Windows.Forms.ToolStripButton();
this._statusStrip = new System.Windows.Forms.StatusStrip();
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
+ this.prb1 = new System.Windows.Forms.ToolStripProgressBar();
this._controlPanel = new System.Windows.Forms.Panel();
this._statusGroup = new System.Windows.Forms.GroupBox();
this._pathLengthLabel = new System.Windows.Forms.Label();
@@ -92,7 +96,6 @@ namespace AGVSimulator.Forms
this._startNodeCombo = new System.Windows.Forms.ComboBox();
this.startNodeLabel = new System.Windows.Forms.Label();
this._agvControlGroup = new System.Windows.Forms.GroupBox();
- this.btNextNode = new System.Windows.Forms.Button();
this._setPositionButton = new System.Windows.Forms.Button();
this._rfidTextBox = new System.Windows.Forms.TextBox();
this._rfidLabel = new System.Windows.Forms.Label();
@@ -105,10 +108,10 @@ namespace AGVSimulator.Forms
this._agvListCombo = new System.Windows.Forms.ComboBox();
this._canvasPanel = new System.Windows.Forms.Panel();
this._agvInfoPanel = new System.Windows.Forms.Panel();
+ this._pathDebugLabel = new System.Windows.Forms.TextBox();
this._agvInfoTitleLabel = new System.Windows.Forms.Label();
this._liftDirectionLabel = new System.Windows.Forms.Label();
this._motorDirectionLabel = new System.Windows.Forms.Label();
- this._pathDebugLabel = new System.Windows.Forms.TextBox();
this._menuStrip.SuspendLayout();
this._toolStrip.SuspendLayout();
this._statusStrip.SuspendLayout();
@@ -274,7 +277,9 @@ namespace AGVSimulator.Forms
this.btAllReset,
this.toolStripSeparator3,
this.fitToMapToolStripButton,
- this.resetZoomToolStripButton});
+ this.resetZoomToolStripButton,
+ this.toolStripSeparator5,
+ this.toolStripButton1});
this._toolStrip.Location = new System.Drawing.Point(0, 24);
this._toolStrip.Name = "_toolStrip";
this._toolStrip.Size = new System.Drawing.Size(1200, 25);
@@ -372,11 +377,26 @@ namespace AGVSimulator.Forms
this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
this.resetZoomToolStripButton.Click += new System.EventHandler(this.OnResetZoom_Click);
//
+ // toolStripSeparator5
+ //
+ this.toolStripSeparator5.Name = "toolStripSeparator5";
+ this.toolStripSeparator5.Size = new System.Drawing.Size(6, 25);
+ //
+ // toolStripButton1
+ //
+ this.toolStripButton1.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButton1.Image")));
+ this.toolStripButton1.ImageTransparentColor = System.Drawing.Color.Magenta;
+ this.toolStripButton1.Name = "toolStripButton1";
+ this.toolStripButton1.Size = new System.Drawing.Size(75, 22);
+ this.toolStripButton1.Text = "경로예측";
+ this.toolStripButton1.Click += new System.EventHandler(this.toolStripButton1_Click);
+ //
// _statusStrip
//
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this._statusLabel,
- this._coordLabel});
+ this._coordLabel,
+ this.prb1});
this._statusStrip.Location = new System.Drawing.Point(0, 778);
this._statusStrip.Name = "_statusStrip";
this._statusStrip.Size = new System.Drawing.Size(1200, 22);
@@ -394,6 +414,11 @@ namespace AGVSimulator.Forms
this._coordLabel.Name = "_coordLabel";
this._coordLabel.Size = new System.Drawing.Size(0, 17);
//
+ // prb1
+ //
+ this.prb1.Name = "prb1";
+ this.prb1.Size = new System.Drawing.Size(200, 16);
+ //
// _controlPanel
//
this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
@@ -540,7 +565,6 @@ namespace AGVSimulator.Forms
//
// _agvControlGroup
//
- this._agvControlGroup.Controls.Add(this.btNextNode);
this._agvControlGroup.Controls.Add(this._setPositionButton);
this._agvControlGroup.Controls.Add(this._rfidTextBox);
this._agvControlGroup.Controls.Add(this._rfidLabel);
@@ -559,16 +583,6 @@ namespace AGVSimulator.Forms
this._agvControlGroup.TabStop = false;
this._agvControlGroup.Text = "AGV 제어";
//
- // btNextNode
- //
- this.btNextNode.Location = new System.Drawing.Point(160, 183);
- this.btNextNode.Name = "btNextNode";
- this.btNextNode.Size = new System.Drawing.Size(60, 21);
- this.btNextNode.TabIndex = 10;
- this.btNextNode.Text = "다음";
- this.btNextNode.UseVisualStyleBackColor = true;
- this.btNextNode.Click += new System.EventHandler(this.btNextNode_Click);
- //
// _setPositionButton
//
this._setPositionButton.Location = new System.Drawing.Point(160, 138);
@@ -685,6 +699,18 @@ namespace AGVSimulator.Forms
this._agvInfoPanel.Size = new System.Drawing.Size(967, 80);
this._agvInfoPanel.TabIndex = 5;
//
+ // _pathDebugLabel
+ //
+ this._pathDebugLabel.BackColor = System.Drawing.Color.LightBlue;
+ this._pathDebugLabel.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ this._pathDebugLabel.Font = new System.Drawing.Font("굴림", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
+ this._pathDebugLabel.Location = new System.Drawing.Point(10, 30);
+ this._pathDebugLabel.Multiline = true;
+ this._pathDebugLabel.Name = "_pathDebugLabel";
+ this._pathDebugLabel.Size = new System.Drawing.Size(947, 43);
+ this._pathDebugLabel.TabIndex = 4;
+ this._pathDebugLabel.Text = "경로: 설정되지 않음";
+ //
// _agvInfoTitleLabel
//
this._agvInfoTitleLabel.AutoSize = true;
@@ -715,18 +741,6 @@ namespace AGVSimulator.Forms
this._motorDirectionLabel.TabIndex = 2;
this._motorDirectionLabel.Text = "모터 방향: -";
//
- // _pathDebugLabel
- //
- this._pathDebugLabel.BackColor = System.Drawing.Color.LightBlue;
- this._pathDebugLabel.BorderStyle = System.Windows.Forms.BorderStyle.None;
- this._pathDebugLabel.Font = new System.Drawing.Font("굴림", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
- this._pathDebugLabel.Location = new System.Drawing.Point(10, 30);
- this._pathDebugLabel.Multiline = true;
- this._pathDebugLabel.Name = "_pathDebugLabel";
- this._pathDebugLabel.Size = new System.Drawing.Size(947, 43);
- this._pathDebugLabel.TabIndex = 4;
- this._pathDebugLabel.Text = "경로: 설정되지 않음";
- //
// SimulatorForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
@@ -827,7 +841,9 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.Label _liftDirectionLabel;
private System.Windows.Forms.Label _motorDirectionLabel;
private System.Windows.Forms.Label _agvInfoTitleLabel;
- private System.Windows.Forms.Button btNextNode;
private System.Windows.Forms.TextBox _pathDebugLabel;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator5;
+ private System.Windows.Forms.ToolStripButton toolStripButton1;
+ private System.Windows.Forms.ToolStripProgressBar prb1;
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs
index 899c527..adb7be7 100644
--- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs
+++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs
@@ -4,6 +4,8 @@ using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using System.Windows.Forms;
using AGVNavigationCore.Models;
using AGVNavigationCore.Controls;
@@ -28,7 +30,7 @@ namespace AGVSimulator.Forms
private AGVPathfinder _advancedPathfinder;
private List _agvList;
private SimulationState _simulationState;
- private Timer _simulationTimer;
+ private System.Windows.Forms.Timer _simulationTimer;
private SimulatorConfig _config;
private string _currentMapFilePath;
private bool _isTargetCalcMode; // 타겟계산 모드 상태
@@ -76,7 +78,7 @@ namespace AGVSimulator.Forms
CreateSimulatorCanvas();
// 타이머 초기화
- _simulationTimer = new Timer();
+ _simulationTimer = new System.Windows.Forms.Timer();
_simulationTimer.Interval = 100; // 100ms 간격
_simulationTimer.Tick += OnSimulationTimer_Tick;
@@ -268,6 +270,12 @@ namespace AGVSimulator.Forms
}
private void OnCalculatePath_Click(object sender, EventArgs e)
+ {
+ var rlt = CalcPath();
+ if (rlt.result == false) MessageBox.Show(rlt.message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
+
+ (bool result, string message) CalcPath()
{
// 시작 RFID가 없으면 AGV 현재 위치로 설정
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요")
@@ -277,19 +285,17 @@ namespace AGVSimulator.Forms
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
{
- MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
- return;
+ return (false, "시작 RFID와 목표 RFID를 선택해주세요.");
}
var startItem = _startNodeCombo.SelectedItem as ComboBoxItem;
var targetItem = _targetNodeCombo.SelectedItem as ComboBoxItem;
var startNode = startItem?.Value;
var targetNode = targetItem?.Value;
-
+
if (startNode == null || targetNode == null)
{
- MessageBox.Show("선택한 노드 정보가 올바르지 않습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
- return;
+ return (false, "선택한 노드 정보가 올바르지 않습니다.");
}
@@ -300,10 +306,9 @@ namespace AGVSimulator.Forms
// 현재 AGV 방향 가져오기
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
- if(selectedAGV == null)
+ if (selectedAGV == null)
{
- MessageBox.Show("Virtual AGV 가 없습니다", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
- return;
+ return (false, "Virtual AGV 가 없습니다");
}
var currentDirection = selectedAGV.CurrentDirection;
@@ -331,14 +336,14 @@ namespace AGVSimulator.Forms
// 고급 경로 디버깅 정보 표시
UpdateAdvancedPathDebugInfo(advancedResult);
+ return (true, string.Empty);
}
else
{
// 경로 실패시 디버깅 정보 초기화
_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
- MessageBox.Show($"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}", "경로 계산 실패",
- MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return (false, $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}");
}
}
@@ -559,7 +564,7 @@ namespace AGVSimulator.Forms
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
//이전위치와 동일한지 체크한다.
- if(selectedAGV.CurrentNodeId == targetNode.NodeId && selectedAGV.CurrentDirection == selectedDirection)
+ if (selectedAGV.CurrentNodeId == targetNode.NodeId && selectedAGV.CurrentDirection == selectedDirection)
{
Program.WriteLine($"이전 노드위치와 모터의 방향이 동일하여 현재 위치 변경이 취소됩니다(NODE:{targetNode.NodeId},RFID:{targetNode.RfidId},DIR:{selectedDirection})");
return;
@@ -741,7 +746,7 @@ namespace AGVSimulator.Forms
var nodeNamePart = !string.IsNullOrEmpty(node.Name) ? $" {node.Name}" : "";
var displayText = $"{node.RfidId} - [{node.NodeId}]{nodeNamePart}";
var item = new ComboBoxItem(node, displayText);
-
+
_startNodeCombo.Items.Add(item);
_targetNodeCombo.Items.Add(item);
}
@@ -1106,7 +1111,7 @@ namespace AGVSimulator.Forms
_pathDebugLabel.Text = $"고급경로: {pathString} (총 {advancedResult.DetailedPath.Count}개 노드, {advancedResult.TotalDistance:F1}px, {stats})";
}
-
+
private void OnReloadMap_Click(object sender, EventArgs e)
{
@@ -1218,26 +1223,355 @@ namespace AGVSimulator.Forms
_statusLabel.Text = "초기화 완료";
}
- private void btNextNode_Click(object sender, EventArgs e)
+
+ private async void toolStripButton1_Click(object sender, EventArgs e)
{
- //get next node
-
- // 선택된 AGV 확인
- var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
- if (selectedAGV == null)
+ // 맵과 AGV 확인
+ if (_mapNodes == null || _mapNodes.Count == 0)
{
- MessageBox.Show("먼저 AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ MessageBox.Show("맵 데이터가 없습니다. 먼저 맵을 로드해주세요.", "알림",
+ MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
- // 선택된 방향 확인
- var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
- var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
+ var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
+ if (selectedAGV == null)
+ {
+ MessageBox.Show("테스트할 AGV를 선택해주세요.", "알림",
+ MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
- var nextNode = selectedAGV.GetNextNodeId(selectedDirection, this._mapNodes);
- MessageBox.Show($"Node:{nextNode.NodeId},RFID:{nextNode.RfidId}");
+ // 도킹 타겟 노드 찾기
+ var dockingTargets = _mapNodes.Where(n =>
+ n.Type == NodeType.Charging || n.Type == NodeType.Docking).ToList();
+ if (dockingTargets.Count == 0)
+ {
+ MessageBox.Show("도킹 타겟(충전기 또는 장비)이 없습니다.", "알림",
+ MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+
+ // 연결된 노드 쌍 찾기 (사전 계산)
+ var nodePairs = GetConnectedNodePairs();
+ var testCount = nodePairs.Count * dockingTargets.Count * 2; // 노드 쌍 x 도킹 타겟 x 방향(2)
+
+ // 테스트 시작 확인
+ var result = MessageBox.Show(
+ $"경로 예측 테스트를 시작합니다.\n\n" +
+ $"※ 실제 사용자 시나리오 재현 방식:\n" +
+ $" 연결된 노드 쌍을 따라 AGV를 2번 이동시켜\n" +
+ $" 방향을 확정한 후 각 도킹 타겟으로 경로 계산\n\n" +
+ $"• 연결된 노드 쌍: {nodePairs.Count}개\n" +
+ $"• 도킹 타겟: {dockingTargets.Count}개 (충전기/장비)\n" +
+ $"• AGV 방향: 2가지 (정방향/역방향)\n" +
+ $"• 총 테스트 케이스: {testCount}개\n\n" +
+ $"※ UI가 실시간으로 업데이트됩니다.\n" +
+ $"계속하시겠습니까?",
+ "경로 예측 테스트 확인",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question);
+
+ if (result != DialogResult.Yes)
+ return;
+
+ // 로그 폼 생성 및 표시
+ var logForm = new ProgressLogForm();
+ logForm.Show(this);
+ logForm.UpdateProgress(0, testCount);
+ logForm.UpdateStatus("테스트 준비 중...");
+
+ // 비동기 테스트 시작
+ await Task.Run(() => RunPathPredictionTest(selectedAGV, dockingTargets, logForm));
+
+ // 완료
+ if (logForm.CancelRequested)
+ {
+ logForm.SetCancelled();
+ MessageBox.Show("테스트가 취소되었습니다.", "알림",
+ MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
+ else
+ {
+ logForm.SetCompleted();
+ MessageBox.Show("경로 예측 테스트가 완료되었습니다.", "완료",
+ MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
}
+
+ ///
+ /// 노드의 표시 이름 가져오기 (RFID 우선, 없으면 (NodeId))
+ ///
+ private string GetNodeDisplayName(MapNode node)
+ {
+ if (node == null)
+ return "-";
+
+ if (!string.IsNullOrEmpty(node.RfidId))
+ return node.RfidId;
+
+ return $"({node.NodeId})";
+ }
+
+ ///
+ /// 방향 콤보박스 선택 (테스트용)
+ ///
+ private void SetDirectionComboBox(AgvDirection direction)
+ {
+ for (int i = 0; i < _directionCombo.Items.Count; i++)
+ {
+ var item = _directionCombo.Items[i] as DirectionItem;
+ if (item != null && item.Direction == direction)
+ {
+ _directionCombo.SelectedIndex = i;
+ return;
+ }
+ }
+ }
+
+ ///
+ /// 목표 노드 콤보박스 선택 (테스트용)
+ ///
+ private void SetTargetNodeComboBox(string nodeId)
+ {
+ for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
+ {
+ var item = _targetNodeCombo.Items[i] as ComboBoxItem;
+ if (item?.Value?.NodeId == nodeId)
+ {
+ _targetNodeCombo.SelectedIndex = i;
+ return;
+ }
+ }
+ }
+
+ ///
+ /// UI 상태로부터 테스트 결과 생성 (테스트용)
+ ///
+ private PathTestLogItem CreateTestResultFromUI(MapNode prevNode, MapNode targetNode,
+ string directionName, (bool result, string message) calcResult)
+ {
+ var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId ==
+ (_agvListCombo.SelectedItem as VirtualAGV)?.CurrentNodeId);
+
+ var logItem = new PathTestLogItem
+ {
+ PreviousPosition = GetNodeDisplayName(prevNode),
+ MotorDirection = directionName,
+ CurrentPosition = GetNodeDisplayName(currentNode),
+ TargetPosition = GetNodeDisplayName(targetNode),
+ DockingPosition = targetNode.Type == NodeType.Charging ? "충전기" : "장비"
+ };
+
+ if (calcResult.result)
+ {
+ // 경로 계산 성공 - 현재 화면에 표시된 경로 정보 사용
+ var currentPath = _simulatorCanvas.CurrentPath;
+ if (currentPath != null && currentPath.Success)
+ {
+ // 도킹 검증
+ var dockingValidation = DockingValidator.ValidateDockingDirection(currentPath, _mapNodes);
+
+ if (dockingValidation.IsValid)
+ {
+ logItem.Success = true;
+ logItem.Message = "성공";
+ logItem.DetailedPath = string.Join(" → ", currentPath.GetDetailedInfo());
+ }
+ else
+ {
+ logItem.Success = false;
+ logItem.Message = $"도킹 검증 실패: {dockingValidation.ValidationError}";
+ logItem.DetailedPath = string.Join(" → ", currentPath.GetDetailedInfo());
+ }
+ }
+ else
+ {
+ logItem.Success = true;
+ logItem.Message = "경로 계산 성공";
+ logItem.DetailedPath = "-";
+ }
+ }
+ else
+ {
+ // 경로 계산 실패
+ logItem.Success = false;
+ logItem.Message = calcResult.message;
+ logItem.DetailedPath = "-";
+ }
+
+ return logItem;
+ }
+
+ ///
+ /// 연결된 노드 쌍 찾기 (A→B 형태)
+ ///
+ private List<(MapNode nodeA, MapNode nodeB)> GetConnectedNodePairs()
+ {
+ var pairs = new List<(MapNode, MapNode)>();
+ var processedPairs = new HashSet();
+
+ foreach (var nodeA in _mapNodes)
+ {
+ if (nodeA.ConnectedNodes == null || nodeA.ConnectedNodes.Count == 0)
+ continue;
+
+ // 연결된 노드 ID를 실제 MapNode 객체로 변환
+ foreach (var connectedNodeId in nodeA.ConnectedNodes)
+ {
+ var nodeB = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
+ if (nodeB == null)
+ continue;
+
+ // 중복 방지 (A→B와 B→A를 같은 것으로 간주)
+ var pairKey1 = $"{nodeA.NodeId}→{nodeB.NodeId}";
+ var pairKey2 = $"{nodeB.NodeId}→{nodeA.NodeId}";
+
+ if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
+ {
+ pairs.Add((nodeA, nodeB));
+ processedPairs.Add(pairKey1);
+ processedPairs.Add(pairKey2);
+ }
+ }
+ }
+
+ return pairs;
+ }
+
+ ///
+ /// 경로 예측 테스트 실행 (실제 사용자 시나리오 재현)
+ ///
+ private void RunPathPredictionTest(VirtualAGV agv, List dockingTargets, ProgressLogForm logForm)
+ {
+ var directions = new[]
+ {
+ (AgvDirection.Forward, "정방향"),
+ (AgvDirection.Backward, "역방향")
+ };
+
+ // 연결된 노드 쌍 찾기
+ var nodePairs = GetConnectedNodePairs();
+
+ int totalTests = nodePairs.Count * dockingTargets.Count * 2;
+ int currentTest = 0;
+ int successCount = 0;
+ int failCount = 0;
+
+ logForm.UpdateStatus("경로 예측 테스트 진행 중...");
+ logForm.AppendLog($"테스트 시작: 총 {totalTests}개 케이스");
+ logForm.AppendLog($"연결된 노드 쌍: {nodePairs.Count}개");
+ logForm.AppendLog($"도킹 타겟: {dockingTargets.Count}개");
+ logForm.AppendLog("---");
+
+ // 각 연결된 노드 쌍에 대해 테스트
+ foreach (var (nodeA, nodeB) in nodePairs)
+ {
+ foreach (var (direction, directionName) in directions)
+ {
+ // 취소 확인
+ if (logForm.CancelRequested)
+ {
+ logForm.AppendLog($"테스트 취소됨 - {currentTest}/{totalTests} 완료");
+ return;
+ }
+
+ // === 실제 사용자 워크플로우 재현 ===
+ // 1단계: AGV를 nodeA로 이동 (실제 UI 조작)
+ this.Invoke((MethodInvoker)delegate
+ {
+ // RFID 텍스트박스에 값 입력
+ _rfidTextBox.Text = nodeA.RfidId;
+
+ // 방향 콤보박스 선택
+ SetDirectionComboBox(direction);
+
+ // 위치설정 버튼 클릭 (실제 사용자 동작)
+ SetAGVPositionByRfid();
+
+ Application.DoEvents(); // UI 업데이트
+ });
+ Thread.Sleep(100); // 시각적 효과
+
+ // 2단계: AGV를 nodeB로 이동 (방향 확정됨)
+ this.Invoke((MethodInvoker)delegate
+ {
+ // RFID 텍스트박스에 값 입력
+ _rfidTextBox.Text = nodeB.RfidId;
+
+ // 방향 콤보박스 선택
+ SetDirectionComboBox(direction);
+
+ // 위치설정 버튼 클릭 (실제 사용자 동작)
+ SetAGVPositionByRfid();
+
+ Application.DoEvents(); // UI 업데이트
+ });
+ Thread.Sleep(100); // 시각적 효과
+
+ // 3단계: nodeB 위치에서 모든 도킹 타겟으로 경로 예측
+ foreach (var dockingTarget in dockingTargets)
+ {
+ // 취소 확인
+ if (logForm.CancelRequested)
+ {
+ logForm.AppendLog($"테스트 취소됨 - {currentTest}/{totalTests} 완료");
+ return;
+ }
+
+ currentTest++;
+
+ // UI 스레드에서 경로 계산 및 테스트 (실제 UI 사용)
+ PathTestLogItem testResult = null;
+ this.Invoke((MethodInvoker)delegate
+ {
+ // 진행상황 업데이트
+ logForm.UpdateProgress(currentTest, totalTests);
+ logForm.UpdateStatus($"테스트 진행 중... ({currentTest}/{totalTests}) [{GetNodeDisplayName(nodeA)}→{GetNodeDisplayName(nodeB)}→{GetNodeDisplayName(dockingTarget)}]");
+ prb1.Value = (int)((double)currentTest / totalTests * 100);
+
+ // 목표 노드 콤보박스 선택
+ SetTargetNodeComboBox(dockingTarget.NodeId);
+
+ // 경로 계산 버튼 클릭 (실제 사용자 동작)
+ var calcResult = CalcPath();
+
+ // 테스트 결과 생성
+ testResult = CreateTestResultFromUI(nodeA, dockingTarget, directionName, calcResult);
+
+ // 로그 추가
+ logForm.AddLogItem(testResult);
+
+ // 실패한 경우에만 경로를 화면에 표시 (시각적 확인)
+ if (!testResult.Success && _simulatorCanvas.CurrentPath != null)
+ {
+ _simulatorCanvas.Invalidate();
+ }
+
+ Application.DoEvents();
+ });
+
+ if (testResult.Success)
+ successCount++;
+ else
+ failCount++;
+
+ // UI 반응성을 위한 짧은 대기
+ Thread.Sleep(50);
+ }
+ }
+ }
+
+ // 최종 결과
+ logForm.AppendLog($"");
+ logForm.AppendLog($"=== 테스트 완료 ===");
+ logForm.AppendLog($"총 테스트: {totalTests}");
+ logForm.AppendLog($"성공: {successCount}");
+ logForm.AppendLog($"실패: {failCount}");
+ logForm.AppendLog($"성공률: {(double)successCount / totalTests * 100:F1}%");
+ }
+
}
///
diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.resx b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.resx
index 35cb4b7..3d64678 100644
--- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.resx
+++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.resx
@@ -123,6 +123,22 @@
132, 17
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
+ YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
+ YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
+ /aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
+ I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
+ cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
+ 6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
+ lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
+ HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
+ 1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
+ nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
+
+
237, 17