feat: Add comprehensive path prediction test with ProgressLogForm
- Add ProgressLogForm.cs for test result logging with ListView - Implement real UI workflow simulation in path prediction test - Test all connected node pairs to all docking targets - Support CSV export for test results - Keep List<string> ConnectedNodes structure (reverted List<MapNode> changes) - Display RFID values in log for better readability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -559,253 +559,7 @@ namespace AGVNavigationCore.Models
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Directional Navigation
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 현재 이전/현재 위치와 이동 방향을 기반으로 다음 노드 ID를 반환
|
|
||||||
///
|
|
||||||
/// 사용 예시:
|
|
||||||
/// - 001에서 002로 이동 후, Forward 선택 → 003 반환
|
|
||||||
/// - 003에서 004로 이동 후, Right 선택 → 030 반환
|
|
||||||
/// - 004에서 003으로 Backward 이동 중, GetNextNodeId(Backward) → 002 반환
|
|
||||||
///
|
|
||||||
/// 전제조건: SetPosition이 최소 2번 이상 호출되어 _prevPosition이 설정되어야 함
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="direction">이동 방향 (Forward/Backward/Left/Right)</param>
|
|
||||||
/// <param name="allNodes">맵의 모든 노드</param>
|
|
||||||
/// <returns>다음 노드 ID (또는 null)</returns>
|
|
||||||
public MapNode GetNextNodeId(AgvDirection direction, List<MapNode> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이동 방향을 기반으로 방향 점수를 계산
|
|
||||||
/// 높은 점수 = 더 나은 선택지
|
|
||||||
/// </summary>
|
|
||||||
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
|
#region Cleanup
|
||||||
|
|
||||||
|
|||||||
@@ -66,17 +66,17 @@ namespace AGVNavigationCore.PathFinding.Core
|
|||||||
{
|
{
|
||||||
var pathNode = _nodeMap[mapNode.NodeId];
|
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))
|
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId))
|
||||||
{
|
{
|
||||||
connectedPathNode.ConnectedNodes.Add(mapNode.NodeId);
|
connectedPathNode.ConnectedNodes.Add(mapNode.NodeId);
|
||||||
@@ -594,13 +594,13 @@ namespace AGVNavigationCore.PathFinding.Core
|
|||||||
|
|
||||||
foreach (var connectedNodeId in junctionNode.ConnectedNodes)
|
foreach (var connectedNodeId in junctionNode.ConnectedNodes)
|
||||||
{
|
{
|
||||||
|
if (connectedNodeId == null) continue;
|
||||||
|
|
||||||
// 조건 1: 왔던 길이 아님
|
// 조건 1: 왔던 길이 아님
|
||||||
if (connectedNodeId == previousNodeId)
|
if (connectedNodeId == previousNodeId) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
// 조건 2: 갈 길이 아님
|
// 조건 2: 갈 길이 아님
|
||||||
if (connectedNodeId == targetNodeId)
|
if (connectedNodeId == targetNodeId) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
// 조건 3, 4, 5: 존재하고, 활성 상태이고, 네비게이션 가능
|
// 조건 3, 4, 5: 존재하고, 활성 상태이고, 네비게이션 가능
|
||||||
var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
private readonly JunctionAnalyzer _junctionAnalyzer;
|
private readonly JunctionAnalyzer _junctionAnalyzer;
|
||||||
private readonly DirectionChangePlanner _directionChangePlanner;
|
private readonly DirectionChangePlanner _directionChangePlanner;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public AGVPathfinder(List<MapNode> mapNodes)
|
public AGVPathfinder(List<MapNode> mapNodes)
|
||||||
{
|
{
|
||||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||||
@@ -114,7 +116,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0);
|
return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0);
|
||||||
if (prevNode == null)
|
if (prevNode == null)
|
||||||
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
|
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
|
||||||
if (startNode == targetNode)
|
if (startNode.NodeId == targetNode.NodeId && targetNode.DockDirection.MatchAGVDirection(prevDirection))
|
||||||
return AGVPathResult.CreateFailure("목적지와 현재위치가 동일합니다.", 0, 0);
|
return AGVPathResult.CreateFailure("목적지와 현재위치가 동일합니다.", 0, 0);
|
||||||
|
|
||||||
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
||||||
@@ -258,77 +260,101 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
MakeDetailData(path2, currentDirection);
|
MakeDetailData(path2, currentDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MapNode tempNode = null;
|
||||||
|
|
||||||
|
|
||||||
//3.방향전환을 위환 대체 노드찾기
|
//3.방향전환을 위환 대체 노드찾기
|
||||||
var tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
|
tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
|
||||||
path1.Path[path1.Path.Count - 2].NodeId,
|
path1.Path[path1.Path.Count - 2].NodeId,
|
||||||
path2.Path[1].NodeId);
|
path2.Path[1].NodeId);
|
||||||
|
|
||||||
//4. path1 + tempnode + path2 가 최종 위치가 된다.
|
//4. path1 + tempnode + path2 가 최종 위치가 된다.
|
||||||
if (tempNode == null)
|
if (tempNode == null)
|
||||||
return AGVPathResult.CreateFailure("방향 전환을 위한 대체 노드를 찾을 수 없습니다.", 0, 0);
|
return AGVPathResult.CreateFailure("방향 전환을 위한 대체 노드를 찾을 수 없습니다.", 0, 0);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// path1 (시작 → 교차로)
|
// path1 (시작 → 교차로)
|
||||||
var combinedResult = path1;
|
var combinedResult = path1;
|
||||||
|
|
||||||
// 교차로 → 대체노드 경로 계산
|
//교차로 대체노드를 사용한 경우
|
||||||
var pathToTemp = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode.NodeId);
|
//if (tempNode != null)
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
//목적지와 방향이 맞지 않다. 그러므로 대체노드를 추가로 더 찾아야한다.
|
// 교차로 → 대체노드 경로 계산
|
||||||
var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
|
var pathToTemp = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode.NodeId);
|
||||||
combinedResult.Path[combinedResult.Path.Count - 2].NodeId,
|
pathToTemp.PrevNode = JunctionInPath;
|
||||||
path2.Path[1].NodeId);
|
pathToTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
|
||||||
|
if (!pathToTemp.Success)
|
||||||
var pathToTemp2 = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode2.NodeId);
|
return AGVPathResult.CreateFailure("교차로에서 대체 노드까지의 경로를 찾을 수 없습니다.", 0, 0);
|
||||||
if (ReverseCheck) MakeDetailData(pathToTemp2, currentDirection);
|
if (ReverseCheck) MakeDetailData(pathToTemp, ReverseDirection);
|
||||||
else MakeDetailData(pathToTemp2, ReverseDirection);
|
else MakeDetailData(pathToTemp, currentDirection);
|
||||||
|
|
||||||
combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp2);
|
|
||||||
|
|
||||||
//교차로찍고 원래방향으로 돌어가야한다.
|
//교차로찍고 원래방향으로 돌어가야한다.
|
||||||
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)
|
if (targetNode.DockDirection == DockingDirection.Forward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Forward)
|
||||||
combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = ReverseDirection;
|
{
|
||||||
else
|
temp3ok = true;
|
||||||
combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = currentDirection;
|
}
|
||||||
|
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 합치기
|
// (path1 + pathToTemp + pathFromTemp) + path2 합치기
|
||||||
|
|||||||
@@ -15,6 +15,21 @@ namespace AGVNavigationCore.Utils
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DirectionalHelper
|
public static class DirectionalHelper
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV방향과 일치하는지 확인한다. 단 원본위치에서 dock 위치가 Don't Care 라면 true가 반환 됩니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dock"></param>
|
||||||
|
/// <param name="agvdirection"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
private static JunctionAnalyzer _junctionAnalyzer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Forms\ProgressLogForm.cs" />
|
||||||
<Compile Include="Models\SimulatorConfig.cs" />
|
<Compile Include="Models\SimulatorConfig.cs" />
|
||||||
<Compile Include="Models\SimulationState.cs" />
|
<Compile Include="Models\SimulationState.cs" />
|
||||||
<Compile Include="Forms\SimulatorForm.cs">
|
<Compile Include="Forms\SimulatorForm.cs">
|
||||||
|
|||||||
356
Cs_HMI/AGVLogic/AGVSimulator/Forms/ProgressLogForm.cs
Normal file
356
Cs_HMI/AGVLogic/AGVSimulator/Forms/ProgressLogForm.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 예측 테스트 결과 로그 항목
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 예측 테스트 진행 상황 로그 표시 폼
|
||||||
|
/// </summary>
|
||||||
|
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<PathTestLogItem> _logItems;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 취소 요청 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool CancelRequested { get; private set; }
|
||||||
|
|
||||||
|
public ProgressLogForm()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
CancelRequested = false;
|
||||||
|
_logItems = new List<PathTestLogItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 로그 추가 (PathTestLogItem)
|
||||||
|
/// </summary>
|
||||||
|
public void AddLogItem(PathTestLogItem item)
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
Invoke(new Action<PathTestLogItem>(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 간단한 텍스트 로그 추가 (상태 메시지용)
|
||||||
|
/// </summary>
|
||||||
|
public void AppendLog(string message)
|
||||||
|
{
|
||||||
|
var item = new PathTestLogItem
|
||||||
|
{
|
||||||
|
Message = message,
|
||||||
|
Success = true
|
||||||
|
};
|
||||||
|
AddLogItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 상태 메시지 업데이트
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateStatus(string status)
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
Invoke(new Action<string>(UpdateStatus), status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_statusLabel.Text = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 프로그레스바 업데이트
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateProgress(int value, int maximum)
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
Invoke(new Action<int, int>(UpdateProgress), value, maximum);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_progressBar.Maximum = maximum;
|
||||||
|
_progressBar.Value = Math.Min(value, maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 작업 완료 시 호출
|
||||||
|
/// </summary>
|
||||||
|
public void SetCompleted()
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
Invoke(new Action(SetCompleted));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cancelButton.Enabled = false;
|
||||||
|
_closeButton.Enabled = true;
|
||||||
|
UpdateStatus("작업 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 작업 취소 시 호출
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CSV 파일로 저장
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CSV 셀 데이터 이스케이프 처리
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ namespace AGVSimulator.Forms
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
|
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SimulatorForm));
|
||||||
this._menuStrip = new System.Windows.Forms.MenuStrip();
|
this._menuStrip = new System.Windows.Forms.MenuStrip();
|
||||||
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
this.openMapToolStripMenuItem = 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.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||||
this.resetZoomToolStripButton = 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._statusStrip = new System.Windows.Forms.StatusStrip();
|
||||||
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||||
this._coordLabel = 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._controlPanel = new System.Windows.Forms.Panel();
|
||||||
this._statusGroup = new System.Windows.Forms.GroupBox();
|
this._statusGroup = new System.Windows.Forms.GroupBox();
|
||||||
this._pathLengthLabel = new System.Windows.Forms.Label();
|
this._pathLengthLabel = new System.Windows.Forms.Label();
|
||||||
@@ -92,7 +96,6 @@ namespace AGVSimulator.Forms
|
|||||||
this._startNodeCombo = new System.Windows.Forms.ComboBox();
|
this._startNodeCombo = new System.Windows.Forms.ComboBox();
|
||||||
this.startNodeLabel = new System.Windows.Forms.Label();
|
this.startNodeLabel = new System.Windows.Forms.Label();
|
||||||
this._agvControlGroup = new System.Windows.Forms.GroupBox();
|
this._agvControlGroup = new System.Windows.Forms.GroupBox();
|
||||||
this.btNextNode = new System.Windows.Forms.Button();
|
|
||||||
this._setPositionButton = new System.Windows.Forms.Button();
|
this._setPositionButton = new System.Windows.Forms.Button();
|
||||||
this._rfidTextBox = new System.Windows.Forms.TextBox();
|
this._rfidTextBox = new System.Windows.Forms.TextBox();
|
||||||
this._rfidLabel = new System.Windows.Forms.Label();
|
this._rfidLabel = new System.Windows.Forms.Label();
|
||||||
@@ -105,10 +108,10 @@ namespace AGVSimulator.Forms
|
|||||||
this._agvListCombo = new System.Windows.Forms.ComboBox();
|
this._agvListCombo = new System.Windows.Forms.ComboBox();
|
||||||
this._canvasPanel = new System.Windows.Forms.Panel();
|
this._canvasPanel = new System.Windows.Forms.Panel();
|
||||||
this._agvInfoPanel = 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._agvInfoTitleLabel = new System.Windows.Forms.Label();
|
||||||
this._liftDirectionLabel = new System.Windows.Forms.Label();
|
this._liftDirectionLabel = new System.Windows.Forms.Label();
|
||||||
this._motorDirectionLabel = new System.Windows.Forms.Label();
|
this._motorDirectionLabel = new System.Windows.Forms.Label();
|
||||||
this._pathDebugLabel = new System.Windows.Forms.TextBox();
|
|
||||||
this._menuStrip.SuspendLayout();
|
this._menuStrip.SuspendLayout();
|
||||||
this._toolStrip.SuspendLayout();
|
this._toolStrip.SuspendLayout();
|
||||||
this._statusStrip.SuspendLayout();
|
this._statusStrip.SuspendLayout();
|
||||||
@@ -274,7 +277,9 @@ namespace AGVSimulator.Forms
|
|||||||
this.btAllReset,
|
this.btAllReset,
|
||||||
this.toolStripSeparator3,
|
this.toolStripSeparator3,
|
||||||
this.fitToMapToolStripButton,
|
this.fitToMapToolStripButton,
|
||||||
this.resetZoomToolStripButton});
|
this.resetZoomToolStripButton,
|
||||||
|
this.toolStripSeparator5,
|
||||||
|
this.toolStripButton1});
|
||||||
this._toolStrip.Location = new System.Drawing.Point(0, 24);
|
this._toolStrip.Location = new System.Drawing.Point(0, 24);
|
||||||
this._toolStrip.Name = "_toolStrip";
|
this._toolStrip.Name = "_toolStrip";
|
||||||
this._toolStrip.Size = new System.Drawing.Size(1200, 25);
|
this._toolStrip.Size = new System.Drawing.Size(1200, 25);
|
||||||
@@ -372,11 +377,26 @@ namespace AGVSimulator.Forms
|
|||||||
this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
|
this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
|
||||||
this.resetZoomToolStripButton.Click += new System.EventHandler(this.OnResetZoom_Click);
|
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
|
// _statusStrip
|
||||||
//
|
//
|
||||||
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||||
this._statusLabel,
|
this._statusLabel,
|
||||||
this._coordLabel});
|
this._coordLabel,
|
||||||
|
this.prb1});
|
||||||
this._statusStrip.Location = new System.Drawing.Point(0, 778);
|
this._statusStrip.Location = new System.Drawing.Point(0, 778);
|
||||||
this._statusStrip.Name = "_statusStrip";
|
this._statusStrip.Name = "_statusStrip";
|
||||||
this._statusStrip.Size = new System.Drawing.Size(1200, 22);
|
this._statusStrip.Size = new System.Drawing.Size(1200, 22);
|
||||||
@@ -394,6 +414,11 @@ namespace AGVSimulator.Forms
|
|||||||
this._coordLabel.Name = "_coordLabel";
|
this._coordLabel.Name = "_coordLabel";
|
||||||
this._coordLabel.Size = new System.Drawing.Size(0, 17);
|
this._coordLabel.Size = new System.Drawing.Size(0, 17);
|
||||||
//
|
//
|
||||||
|
// prb1
|
||||||
|
//
|
||||||
|
this.prb1.Name = "prb1";
|
||||||
|
this.prb1.Size = new System.Drawing.Size(200, 16);
|
||||||
|
//
|
||||||
// _controlPanel
|
// _controlPanel
|
||||||
//
|
//
|
||||||
this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
|
this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
|
||||||
@@ -540,7 +565,6 @@ namespace AGVSimulator.Forms
|
|||||||
//
|
//
|
||||||
// _agvControlGroup
|
// _agvControlGroup
|
||||||
//
|
//
|
||||||
this._agvControlGroup.Controls.Add(this.btNextNode);
|
|
||||||
this._agvControlGroup.Controls.Add(this._setPositionButton);
|
this._agvControlGroup.Controls.Add(this._setPositionButton);
|
||||||
this._agvControlGroup.Controls.Add(this._rfidTextBox);
|
this._agvControlGroup.Controls.Add(this._rfidTextBox);
|
||||||
this._agvControlGroup.Controls.Add(this._rfidLabel);
|
this._agvControlGroup.Controls.Add(this._rfidLabel);
|
||||||
@@ -559,16 +583,6 @@ namespace AGVSimulator.Forms
|
|||||||
this._agvControlGroup.TabStop = false;
|
this._agvControlGroup.TabStop = false;
|
||||||
this._agvControlGroup.Text = "AGV 제어";
|
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
|
// _setPositionButton
|
||||||
//
|
//
|
||||||
this._setPositionButton.Location = new System.Drawing.Point(160, 138);
|
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.Size = new System.Drawing.Size(967, 80);
|
||||||
this._agvInfoPanel.TabIndex = 5;
|
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
|
// _agvInfoTitleLabel
|
||||||
//
|
//
|
||||||
this._agvInfoTitleLabel.AutoSize = true;
|
this._agvInfoTitleLabel.AutoSize = true;
|
||||||
@@ -715,18 +741,6 @@ namespace AGVSimulator.Forms
|
|||||||
this._motorDirectionLabel.TabIndex = 2;
|
this._motorDirectionLabel.TabIndex = 2;
|
||||||
this._motorDirectionLabel.Text = "모터 방향: -";
|
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
|
// SimulatorForm
|
||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
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 _liftDirectionLabel;
|
||||||
private System.Windows.Forms.Label _motorDirectionLabel;
|
private System.Windows.Forms.Label _motorDirectionLabel;
|
||||||
private System.Windows.Forms.Label _agvInfoTitleLabel;
|
private System.Windows.Forms.Label _agvInfoTitleLabel;
|
||||||
private System.Windows.Forms.Button btNextNode;
|
|
||||||
private System.Windows.Forms.TextBox _pathDebugLabel;
|
private System.Windows.Forms.TextBox _pathDebugLabel;
|
||||||
|
private System.Windows.Forms.ToolStripSeparator toolStripSeparator5;
|
||||||
|
private System.Windows.Forms.ToolStripButton toolStripButton1;
|
||||||
|
private System.Windows.Forms.ToolStripProgressBar prb1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ using System.Diagnostics;
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using AGVNavigationCore.Models;
|
using AGVNavigationCore.Models;
|
||||||
using AGVNavigationCore.Controls;
|
using AGVNavigationCore.Controls;
|
||||||
@@ -28,7 +30,7 @@ namespace AGVSimulator.Forms
|
|||||||
private AGVPathfinder _advancedPathfinder;
|
private AGVPathfinder _advancedPathfinder;
|
||||||
private List<VirtualAGV> _agvList;
|
private List<VirtualAGV> _agvList;
|
||||||
private SimulationState _simulationState;
|
private SimulationState _simulationState;
|
||||||
private Timer _simulationTimer;
|
private System.Windows.Forms.Timer _simulationTimer;
|
||||||
private SimulatorConfig _config;
|
private SimulatorConfig _config;
|
||||||
private string _currentMapFilePath;
|
private string _currentMapFilePath;
|
||||||
private bool _isTargetCalcMode; // 타겟계산 모드 상태
|
private bool _isTargetCalcMode; // 타겟계산 모드 상태
|
||||||
@@ -76,7 +78,7 @@ namespace AGVSimulator.Forms
|
|||||||
CreateSimulatorCanvas();
|
CreateSimulatorCanvas();
|
||||||
|
|
||||||
// 타이머 초기화
|
// 타이머 초기화
|
||||||
_simulationTimer = new Timer();
|
_simulationTimer = new System.Windows.Forms.Timer();
|
||||||
_simulationTimer.Interval = 100; // 100ms 간격
|
_simulationTimer.Interval = 100; // 100ms 간격
|
||||||
_simulationTimer.Tick += OnSimulationTimer_Tick;
|
_simulationTimer.Tick += OnSimulationTimer_Tick;
|
||||||
|
|
||||||
@@ -268,6 +270,12 @@ namespace AGVSimulator.Forms
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void OnCalculatePath_Click(object sender, EventArgs e)
|
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 현재 위치로 설정
|
// 시작 RFID가 없으면 AGV 현재 위치로 설정
|
||||||
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요")
|
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요")
|
||||||
@@ -277,8 +285,7 @@ namespace AGVSimulator.Forms
|
|||||||
|
|
||||||
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
|
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
|
||||||
{
|
{
|
||||||
MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
return (false, "시작 RFID와 목표 RFID를 선택해주세요.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var startItem = _startNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
|
var startItem = _startNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
|
||||||
@@ -288,8 +295,7 @@ namespace AGVSimulator.Forms
|
|||||||
|
|
||||||
if (startNode == null || targetNode == null)
|
if (startNode == null || targetNode == null)
|
||||||
{
|
{
|
||||||
MessageBox.Show("선택한 노드 정보가 올바르지 않습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
return (false, "선택한 노드 정보가 올바르지 않습니다.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -300,10 +306,9 @@ namespace AGVSimulator.Forms
|
|||||||
|
|
||||||
// 현재 AGV 방향 가져오기
|
// 현재 AGV 방향 가져오기
|
||||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||||
if(selectedAGV == null)
|
if (selectedAGV == null)
|
||||||
{
|
{
|
||||||
MessageBox.Show("Virtual AGV 가 없습니다", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
return (false, "Virtual AGV 가 없습니다");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
var currentDirection = selectedAGV.CurrentDirection;
|
var currentDirection = selectedAGV.CurrentDirection;
|
||||||
|
|
||||||
@@ -331,14 +336,14 @@ namespace AGVSimulator.Forms
|
|||||||
|
|
||||||
// 고급 경로 디버깅 정보 표시
|
// 고급 경로 디버깅 정보 표시
|
||||||
UpdateAdvancedPathDebugInfo(advancedResult);
|
UpdateAdvancedPathDebugInfo(advancedResult);
|
||||||
|
return (true, string.Empty);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 경로 실패시 디버깅 정보 초기화
|
// 경로 실패시 디버깅 정보 초기화
|
||||||
_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
|
_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
|
||||||
|
|
||||||
MessageBox.Show($"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}", "경로 계산 실패",
|
return (false, $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}");
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,7 +564,7 @@ namespace AGVSimulator.Forms
|
|||||||
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
|
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})");
|
Program.WriteLine($"이전 노드위치와 모터의 방향이 동일하여 현재 위치 변경이 취소됩니다(NODE:{targetNode.NodeId},RFID:{targetNode.RfidId},DIR:{selectedDirection})");
|
||||||
return;
|
return;
|
||||||
@@ -1218,26 +1223,355 @@ namespace AGVSimulator.Forms
|
|||||||
_statusLabel.Text = "초기화 완료";
|
_statusLabel.Text = "초기화 완료";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btNextNode_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
//get next node
|
|
||||||
|
|
||||||
// 선택된 AGV 확인
|
private async void toolStripButton1_Click(object sender, EventArgs e)
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 방향 확인
|
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||||
var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
|
if (selectedAGV == null)
|
||||||
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드의 표시 이름 가져오기 (RFID 우선, 없으면 (NodeId))
|
||||||
|
/// </summary>
|
||||||
|
private string GetNodeDisplayName(MapNode node)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
return "-";
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(node.RfidId))
|
||||||
|
return node.RfidId;
|
||||||
|
|
||||||
|
return $"({node.NodeId})";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 방향 콤보박스 선택 (테스트용)
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 목표 노드 콤보박스 선택 (테스트용)
|
||||||
|
/// </summary>
|
||||||
|
private void SetTargetNodeComboBox(string nodeId)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
|
||||||
|
{
|
||||||
|
var item = _targetNodeCombo.Items[i] as ComboBoxItem<MapNode>;
|
||||||
|
if (item?.Value?.NodeId == nodeId)
|
||||||
|
{
|
||||||
|
_targetNodeCombo.SelectedIndex = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI 상태로부터 테스트 결과 생성 (테스트용)
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 연결된 노드 쌍 찾기 (A→B 형태)
|
||||||
|
/// </summary>
|
||||||
|
private List<(MapNode nodeA, MapNode nodeB)> GetConnectedNodePairs()
|
||||||
|
{
|
||||||
|
var pairs = new List<(MapNode, MapNode)>();
|
||||||
|
var processedPairs = new HashSet<string>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 예측 테스트 실행 (실제 사용자 시나리오 재현)
|
||||||
|
/// </summary>
|
||||||
|
private void RunPathPredictionTest(VirtualAGV agv, List<MapNode> 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}%");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -123,6 +123,22 @@
|
|||||||
<metadata name="_toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
<metadata name="_toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||||
<value>132, 17</value>
|
<value>132, 17</value>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||||
|
<data name="toolStripButton1.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>
|
||||||
|
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
|
||||||
|
</value>
|
||||||
|
</data>
|
||||||
<metadata name="_statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
<metadata name="_statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||||
<value>237, 17</value>
|
<value>237, 17</value>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
|||||||
Reference in New Issue
Block a user