..
This commit is contained in:
@@ -64,7 +64,7 @@ namespace AGVNavigationCore.PathFinding.Core
|
||||
/// <summary>
|
||||
/// 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 검증 결과
|
||||
@@ -116,7 +116,7 @@ namespace AGVNavigationCore.PathFinding.Core
|
||||
ExploredNodes = 0;
|
||||
EstimatedTimeSeconds = 0;
|
||||
RotationCount = 0;
|
||||
ErrorMessage = string.Empty;
|
||||
Message = string.Empty;
|
||||
PlanDescription = string.Empty;
|
||||
RequiredDirectionChange = false;
|
||||
DirectionChangeNode = string.Empty;
|
||||
@@ -149,36 +149,19 @@ namespace AGVNavigationCore.PathFinding.Core
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">오류 메시지</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <returns>실패 결과</returns>
|
||||
public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs)
|
||||
{
|
||||
return new AGVPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
CalculationTimeMs = calculationTimeMs
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성 (확장)
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">오류 메시지</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <param name="exploredNodes">탐색된 노드 수</param>
|
||||
/// <returns>실패 결과</returns>
|
||||
public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs, int exploredNodes)
|
||||
public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs = 0, int exploredNodes = 0)
|
||||
{
|
||||
return new AGVPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
Message = errorMessage,
|
||||
CalculationTimeMs = calculationTimeMs,
|
||||
ExploredNodes = exploredNodes
|
||||
};
|
||||
@@ -276,22 +259,6 @@ namespace AGVNavigationCore.PathFinding.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상세 경로 정보 반환
|
||||
/// </summary>
|
||||
/// <returns>상세 정보 문자열</returns>
|
||||
public string GetDetailedInfo()
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return $"경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
|
||||
}
|
||||
|
||||
return $"경로: {Path.Count}개 노드, 거리: {TotalDistance:F1}px, " +
|
||||
$"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
|
||||
$"계산시간: {CalculationTimeMs}ms";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로의 노드 정보를 포함
|
||||
/// </summary>
|
||||
@@ -300,13 +267,14 @@ namespace AGVNavigationCore.PathFinding.Core
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return $"경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
|
||||
return $"경로 계산 실패: {Message} (계산시간: {CalculationTimeMs}ms)";
|
||||
}
|
||||
|
||||
var data = DetailedPath.Select(t => {
|
||||
return $"{t.RfidId}[{t.NodeId}] {t.MotorDirection.ToString().Substring(0,1)}-{t.MagnetDirection.ToString().Substring(0,1)}";
|
||||
var data = DetailedPath.Select(t =>
|
||||
{
|
||||
return $"{t.RfidId}[{t.NodeId}] {t.MotorDirection.ToString().Substring(0, 1)}-{t.MagnetDirection.ToString().Substring(0, 1)}";
|
||||
});
|
||||
return string.Join(" → ",data);
|
||||
return string.Join(" → ", data);
|
||||
}
|
||||
|
||||
|
||||
@@ -334,7 +302,7 @@ namespace AGVNavigationCore.PathFinding.Core
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Failed: {ErrorMessage}";
|
||||
return $"Failed: {Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,12 +351,12 @@ namespace AGVNavigationCore.PathFinding.Core
|
||||
|
||||
if (!previousResult.Success)
|
||||
return AGVPathResult.CreateFailure(
|
||||
$"이전 경로 결과 실패: {previousResult.ErrorMessage}",
|
||||
$"이전 경로 결과 실패: {previousResult.Message}",
|
||||
previousResult.CalculationTimeMs);
|
||||
|
||||
if (!currentResult.Success)
|
||||
return AGVPathResult.CreateFailure(
|
||||
$"현재 경로 결과 실패: {currentResult.ErrorMessage}",
|
||||
$"현재 경로 결과 실패: {currentResult.Message}",
|
||||
currentResult.CalculationTimeMs);
|
||||
|
||||
// 경로가 비어있는 경우 처리
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
pathResult.PrevDirection = prevDirection;
|
||||
|
||||
if (!pathResult.Success)
|
||||
return AGVPathResult.CreateFailure(pathResult.ErrorMessage ?? "경로 없음", 0, 0);
|
||||
return AGVPathResult.CreateFailure(pathResult.Message ?? "경로 없음", 0, 0);
|
||||
|
||||
// 3. 상세 데이터 생성 (갈림길 마그넷 방향 계산 포함)
|
||||
// 3. 상세 데이터 생성 (갈림길 마그넷 방향 계산 포함)
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var result = $"R{RfidId}[N{NodeId}]:{MotorDirection}";
|
||||
var result = $"R{RfidId}[*{NodeId}]:{MotorDirection}";
|
||||
|
||||
// 마그넷 방향이 직진이 아닌 경우 표시
|
||||
if (MagnetDirection != MagnetDirection.Straight)
|
||||
|
||||
@@ -191,7 +191,7 @@ namespace AGVNavigationCore.Utils
|
||||
}
|
||||
|
||||
// 검증 수행
|
||||
if (LastNodeInfo.MotorDirection == requiredDirection && pathResult.DetailedPath[pathResult.DetailedPath.Count - 2].MotorDirection == requiredDirection)
|
||||
if (LastNodeInfo.MotorDirection == requiredDirection && pathResult.DetailedPath[pathResult.DetailedPath.Count - 1].MotorDirection == requiredDirection)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공");
|
||||
return DockingValidationResult.CreateValid(
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace AGVSimulator.Forms
|
||||
}
|
||||
|
||||
private UnifiedAGVCanvas _simulatorCanvas;
|
||||
private AGVPathfinder _advancedPathfinder;
|
||||
// private AGVPathfinder _advancedPathfinder;
|
||||
private List<VirtualAGV> _agvList;
|
||||
private SimulationState _simulationState;
|
||||
private System.Windows.Forms.Timer _simulationTimer;
|
||||
@@ -1579,7 +1579,7 @@ namespace AGVSimulator.Forms
|
||||
/// UI 상태로부터 테스트 결과 생성 (테스트용)
|
||||
/// </summary>
|
||||
private PathTestLogItem CreateTestResultFromUI(MapNode prevNode, MapNode targetNode,
|
||||
string directionName, (bool result, string message) calcResult)
|
||||
string directionName, AGVPathResult calcResult)
|
||||
{
|
||||
var currentNode = _simulatorCanvas.Nodes.FirstOrDefault(n => n.Id ==
|
||||
(_agvListCombo.SelectedItem as VirtualAGV)?.CurrentNodeId);
|
||||
@@ -1593,10 +1593,10 @@ namespace AGVSimulator.Forms
|
||||
DockingPosition = (targetNode.StationType == StationType.Charger1 || targetNode.StationType == StationType.Charger2) ? "충전기" : "장비"
|
||||
};
|
||||
|
||||
if (calcResult.result)
|
||||
if (calcResult.Success)
|
||||
{
|
||||
// 경로 계산 성공 - 현재 화면에 표시된 경로 정보 사용
|
||||
var currentPath = _simulatorCanvas.CurrentPath;
|
||||
var currentPath = calcResult;// _simulatorCanvas.CurrentPath;
|
||||
if (currentPath != null && currentPath.Success)
|
||||
{
|
||||
// 도킹 검증
|
||||
@@ -1626,7 +1626,7 @@ namespace AGVSimulator.Forms
|
||||
{
|
||||
// 경로 계산 실패
|
||||
logItem.Success = false;
|
||||
logItem.Message = calcResult.message;
|
||||
logItem.Message = calcResult.Message;
|
||||
logItem.DetailedPath = "-";
|
||||
}
|
||||
|
||||
@@ -1763,19 +1763,25 @@ namespace AGVSimulator.Forms
|
||||
SetTargetNodeComboBox(dockingTarget.Id);
|
||||
|
||||
// 경로 계산 버튼 클릭 (실제 사용자 동작)
|
||||
//var calcResult = CalcPath();
|
||||
var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
||||
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
var calcResult = CalcPath(startNode, targetNode, this._simulatorCanvas.Nodes,selectedAGV.PrevNode, selectedAGV.PrevDirection);
|
||||
|
||||
|
||||
|
||||
|
||||
//// 테스트 결과 생성
|
||||
//testResult = CreateTestResultFromUI(nodeA, dockingTarget, directionName, calcResult);
|
||||
testResult = CreateTestResultFromUI(nodeA, dockingTarget, directionName, calcResult);
|
||||
|
||||
//// 로그 추가
|
||||
//logForm.AddLogItem(testResult);
|
||||
logForm.AddLogItem(testResult);
|
||||
|
||||
//// 실패한 경우에만 경로를 화면에 표시 (시각적 확인)
|
||||
//if (!testResult.Success && _simulatorCanvas.CurrentPath != null)
|
||||
//{
|
||||
// _simulatorCanvas.Invalidate();
|
||||
//}
|
||||
if (!testResult.Success && _simulatorCanvas.CurrentPath != null)
|
||||
{
|
||||
_simulatorCanvas.Invalidate();
|
||||
}
|
||||
|
||||
Application.DoEvents();
|
||||
});
|
||||
@@ -2519,102 +2525,332 @@ namespace AGVSimulator.Forms
|
||||
|
||||
private void btPath2_Click(object sender, EventArgs e)
|
||||
{
|
||||
// 1. 기본 정보 획득
|
||||
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요") SetStartNodeFromAGVPosition();
|
||||
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
|
||||
{
|
||||
MessageBox.Show("시작/목표 노드를 확인하세요");
|
||||
return;
|
||||
}
|
||||
|
||||
//var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
//if (selectedAGV == null) return AGVPathResult.CreateFailure("Virtual AGV 없음");
|
||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
|
||||
// 경로계산2 (Gateway Logic)
|
||||
var rlt = CalcPathGateway();
|
||||
if (rlt.result == false) MessageBox.Show(rlt.message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
||||
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
||||
var rlt = CalcPath(startNode, targetNode, this._simulatorCanvas.Nodes, selectedAGV.PrevNode, selectedAGV.PrevDirection);
|
||||
if (rlt.Success == false) MessageBox.Show(rlt.Message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
else
|
||||
{
|
||||
// 8. 적용
|
||||
|
||||
ApplyResultToSimulator(rlt, selectedAGV);
|
||||
UpdateAdvancedPathDebugInfo(rlt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 길목(Gateway) 기반 경로 계산
|
||||
/// 버퍼-버퍼 상태에서는 별도의 추가 로직을 적용합니다
|
||||
/// </summary>
|
||||
private (bool result, string message) CalcPathGateway()
|
||||
public AGVPathResult CalcPath(MapNode startNode, MapNode targetNode, List<MapNode> nodes,
|
||||
MapNode prevNode, AgvDirection prevDir)
|
||||
{
|
||||
// 1. 기본 정보 획득
|
||||
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요") SetStartNodeFromAGVPosition();
|
||||
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null) return (false, "시작/목표 노드 선택 필요");
|
||||
AGVPathResult Retval;
|
||||
|
||||
var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
||||
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
||||
if (startNode == null || targetNode == null) return (false, "노드 정보 오류");
|
||||
if (startNode == null || targetNode == null) return AGVPathResult.CreateFailure("시작/종료노드가 지정되지 않음");
|
||||
|
||||
if (_advancedPathfinder == null) _advancedPathfinder = new AGVPathfinder(_simulatorCanvas.Nodes);
|
||||
|
||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
if (selectedAGV == null) return (false, "Virtual AGV 없음");
|
||||
|
||||
var currentAgvDir = selectedAGV.CurrentDirection;
|
||||
var prevNode = selectedAGV.PrevNode;
|
||||
var prevDir = selectedAGV.PrevDirection;
|
||||
|
||||
// 2. Buffer-to-Buffer 예외 처리
|
||||
|
||||
var node05 = FindNode(5); //05~31사이의 노드는 모두 버퍼이다.
|
||||
var node31 = FindNode(31);
|
||||
if (node05 == null || node31 == null) return (false, "버퍼구간 노드가 없습니다(05~31)");
|
||||
|
||||
var rlt = _advancedPathfinder.FindPathAStar(node05, node31);
|
||||
if (rlt.Success == false) return (false, "버퍼구간 노드경로 확인 실패(05~31)");
|
||||
|
||||
//버퍼구간내에 시작과 종료가 모두 포함되어있다
|
||||
if (rlt.Path.Contains(startNode) && rlt.Path.Contains(targetNode))
|
||||
try
|
||||
{
|
||||
return CalcPathBufferToBuffer(startNode, targetNode, prevNode, prevDir, currentAgvDir, selectedAGV);
|
||||
}
|
||||
var pathFinder = new AGVPathfinder(nodes);
|
||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
if (selectedAGV == null) return AGVPathResult.CreateFailure("Virtual AGV 없음");
|
||||
|
||||
|
||||
// 3. 목적지별 Gateway 및 진입 조건 확인
|
||||
var gatewayNode = GetGatewayNode(targetNode);
|
||||
if (gatewayNode == null)
|
||||
{
|
||||
//게이트웨이가 없는 경우라면 목적지가 도킹포인트가 아니므로, a*알골리즘으로 진행 방향만 맟춰서 이동한다
|
||||
var simplePath = _advancedPathfinder.FindBasicPath(startNode, targetNode, prevNode, prevDir);
|
||||
if (simplePath.Success)
|
||||
//종료노드라면 이전위치로 이동시켜야한다.
|
||||
AGVPathResult LimitPath = null;
|
||||
if (startNode.ConnectedMapNodes.Count == 1)
|
||||
{
|
||||
_simulatorCanvas.HighlightNodeId = null; // 일반 경로는 강조 없음
|
||||
ApplyResultToSimulator(simplePath, selectedAGV);
|
||||
UpdateAdvancedPathDebugInfo(simplePath);
|
||||
return (true, "일반 이동 경로(Gateway 없음)");
|
||||
LimitPath = pathFinder.FindPathAStar(startNode, targetNode);
|
||||
for(int i = 0; i < LimitPath.Path.Count;i++)
|
||||
{
|
||||
var nodeinfo = LimitPath.Path[i];
|
||||
var dir = (prevDir == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
||||
LimitPath.DetailedPath.Add(new NodeMotorInfo(i + 1, nodeinfo.Id, nodeinfo.RfidId, dir));
|
||||
}
|
||||
}
|
||||
return (false, $"일반 이동 경로 실패: {simplePath.ErrorMessage}");
|
||||
|
||||
|
||||
//var prevNode = selectedAGV.PrevNode;
|
||||
//var prevDir = selectedAGV.PrevDirection;
|
||||
|
||||
// 2. Buffer-to-Buffer 예외 처리
|
||||
var node05 = FindNode(5); //05~31사이의 노드는 모두 버퍼이다.
|
||||
var node31 = FindNode(31);
|
||||
if (node05 == null || node31 == null) return AGVPathResult.CreateFailure("버퍼구간 노드가 없습니다(05~31)");
|
||||
|
||||
var rlt = pathFinder.FindPathAStar(node05, node31);
|
||||
if (rlt.Success == false) return AGVPathResult.CreateFailure("버퍼구간 노드경로 확인 실패(05~31)");
|
||||
|
||||
//하이라이트노드 해제
|
||||
_simulatorCanvas.HighlightNodeId = null;
|
||||
|
||||
//버퍼구간내에 시작과 종료가 모두 포함되어있다
|
||||
if (rlt.Path.Contains(startNode) && rlt.Path.Contains(targetNode))
|
||||
{
|
||||
Retval = CalcPathBufferToBuffer(pathFinder, startNode, targetNode, prevNode, prevDir, selectedAGV);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 3. 목적지별 Gateway 및 진입 조건 확인
|
||||
var gatewayNode = GetGatewayNode(targetNode);
|
||||
if (gatewayNode == null)
|
||||
{
|
||||
//게이트웨이가 없는 경우라면 목적지가 도킹포인트가 아니므로, a*알골리즘으로 진행 방향만 맟춰서 이동한다
|
||||
Retval = pathFinder.FindBasicPath(startNode, targetNode, prevNode, prevDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Gateway Node 찾음
|
||||
_simulatorCanvas.HighlightNodeId = gatewayNode.Id; // Gateway 강조 설정
|
||||
|
||||
// 4. Start -> Gateway 경로 계산 (A*)
|
||||
var pathToGateway = pathFinder.FindBasicPath(startNode, gatewayNode, prevNode, prevDir);
|
||||
if (pathToGateway.Success == false)
|
||||
AGVPathResult.CreateFailure($"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.Message}");
|
||||
|
||||
//마지막경로는 게이트웨이이므로 제거하낟.(260113)
|
||||
if (pathToGateway.Path.Count > 1 && pathToGateway.Path.Last().Id == gatewayNode.Id)
|
||||
{
|
||||
pathToGateway.Path.RemoveAt(pathToGateway.Path.Count - 1);
|
||||
pathToGateway.DetailedPath.RemoveAt(pathToGateway.DetailedPath.Count - 1);
|
||||
}
|
||||
|
||||
// 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함)
|
||||
MapNode GateprevNode = pathToGateway.Path.Last();
|
||||
NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.Last();
|
||||
|
||||
var arrivalOrientation = GatePrevDetail.MotorDirection;
|
||||
var gatewayPathResult = GetPathFromGateway(pathFinder, gatewayNode, targetNode, GateprevNode, arrivalOrientation);
|
||||
if (!gatewayPathResult.Success) return AGVPathResult.CreateFailure($"{gatewayPathResult.Message}");
|
||||
Retval = CombinePaths(pathToGateway, gatewayPathResult);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//경로오류 검사
|
||||
if (Retval.Success == false) return Retval;
|
||||
|
||||
//해당경로와 대상의 도킹포인트의 방향을 검사합니다
|
||||
if (targetNode.DockDirection != DockingDirection.DontCare)
|
||||
{
|
||||
var lastPath = Retval.DetailedPath.Last();
|
||||
if (targetNode.DockDirection == DockingDirection.Forward && lastPath.MotorDirection != AgvDirection.Forward)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"생성된 경로와 목적지의 도킹방향이 일치하지 않습니다(FWD) Target:{targetNode.DockDirection}, Path:{Retval.GetDetailedPathInfo()}");
|
||||
}
|
||||
if (targetNode.DockDirection == DockingDirection.Backward && lastPath.MotorDirection != AgvDirection.Backward)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"생성된 경로와 목적지의 도킹방향이 일치하지 않습니다(BWD) Target:{targetNode.DockDirection}, Path:{Retval.GetDetailedPathInfo()}");
|
||||
}
|
||||
}
|
||||
|
||||
//6[F][R] → 13[B][L] → 6[F][L] 이런경우를 찾아서 경로를 최적화한다.
|
||||
//위 예제에서는 6FR 13BL 이 제자리로 오는 경로이므로 6FL만 남기면된다.
|
||||
//단순히 ID만 같은게 아니라, 실제로 갔다가 되돌아오는 패턴(역방향)인지 확인해야 함.
|
||||
while (true)
|
||||
{
|
||||
var updatecount = 0;
|
||||
for (int i = 0; i < Retval.DetailedPath.Count - 2; i++)
|
||||
{
|
||||
var n1 = Retval.DetailedPath[i];
|
||||
var n2 = Retval.DetailedPath[i + 1];
|
||||
var n3 = Retval.DetailedPath[i + 2];
|
||||
|
||||
if (n1.NodeId == n3.NodeId)
|
||||
{
|
||||
bool isInverse = false;
|
||||
|
||||
// 1. 모터 방향이 반대인가? (F <-> B)
|
||||
bool isMotorInverse = (n1.MotorDirection != n2.MotorDirection) &&
|
||||
(n1.MotorDirection == AgvDirection.Forward || n1.MotorDirection == AgvDirection.Backward) &&
|
||||
(n2.MotorDirection == AgvDirection.Forward || n2.MotorDirection == AgvDirection.Backward);
|
||||
|
||||
if (isMotorInverse)
|
||||
{
|
||||
// 2. 마그넷 방향이 반대인가? (L <-> R, S <-> S)
|
||||
bool isMagnetInverse = false;
|
||||
if (n1.MagnetDirection == MagnetDirection.Straight && n2.MagnetDirection == MagnetDirection.Straight) isMagnetInverse = true;
|
||||
else if (n1.MagnetDirection == MagnetDirection.Left && n2.MagnetDirection == MagnetDirection.Right) isMagnetInverse = true;
|
||||
else if (n1.MagnetDirection == MagnetDirection.Right && n2.MagnetDirection == MagnetDirection.Left) isMagnetInverse = true;
|
||||
|
||||
if (isMagnetInverse) isInverse = true;
|
||||
}
|
||||
|
||||
if (isInverse)
|
||||
{
|
||||
// 제자리 회귀 경로 발견 -> 앞의 두 노드(n1, n2)를 제거하여 n3만 남김
|
||||
Retval.DetailedPath.RemoveAt(i);
|
||||
Retval.DetailedPath.RemoveAt(i);
|
||||
|
||||
if (Retval.Path.Count > i + 1)
|
||||
{
|
||||
Retval.Path.RemoveAt(i);
|
||||
Retval.Path.RemoveAt(i);
|
||||
}
|
||||
i--; // 인덱스 재조정
|
||||
updatecount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updatecount == 0) break;
|
||||
}
|
||||
|
||||
|
||||
//detail 경로를 확인해서 왓던길에서 바로 방향이 전환되는 경우를 찾는다
|
||||
for (int i = 0; i < Retval.DetailedPath.Count - 2; i++)
|
||||
{
|
||||
|
||||
var n1 = Retval.DetailedPath[i];
|
||||
var n2 = Retval.DetailedPath[i + 1];
|
||||
var n3 = Retval.DetailedPath[i + 2];
|
||||
|
||||
if (n1.NodeId == n3.NodeId && n1.MotorDirection == n3.MotorDirection)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"불가능한 회전 경로가 포함되어있습니다. {n1.RfidId}->{n2.RfidId}->{n3.RfidId}\n{Retval.GetDetailedPathInfo()}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//6[F][R] → 13[B][L] → 6[F][L] 이런경우를 찾아서 경로를 최적화한다.
|
||||
//위 예제에서는 6FR 13BL 이 제자리로 오는 경로이므로 6FL만 남기면된다.
|
||||
|
||||
|
||||
//최종결과 반환
|
||||
return Retval;
|
||||
}
|
||||
|
||||
// Gateway Node 찾음
|
||||
_simulatorCanvas.HighlightNodeId = gatewayNode.Id; // Gateway 강조 설정
|
||||
|
||||
// 4. Start -> Gateway 경로 계산 (A*)
|
||||
var pathToGateway = _advancedPathfinder.FindBasicPath(startNode, gatewayNode, prevNode, prevDir);
|
||||
|
||||
if (!pathToGateway.Success) return (false, $"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.ErrorMessage}");
|
||||
|
||||
//마지막경로는 게이트웨이이므로 제거하낟.(260113)
|
||||
if (pathToGateway.Path.Count > 1 && pathToGateway.Path.Last().Id == gatewayNode.Id)
|
||||
catch (Exception ex)
|
||||
{
|
||||
pathToGateway.Path.RemoveAt(pathToGateway.Path.Count - 1);
|
||||
pathToGateway.DetailedPath.RemoveAt(pathToGateway.DetailedPath.Count - 1);
|
||||
return AGVPathResult.CreateFailure($"[계산오류] {ex.Message}");
|
||||
}
|
||||
|
||||
// 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함)
|
||||
}
|
||||
|
||||
private AGVPathResult CalcPathBufferToBuffer(AGVPathfinder pathfinder, MapNode start, MapNode target, MapNode prev, AgvDirection prevDir, VirtualAGV agv)
|
||||
{
|
||||
// Monitor Side 판단 로직
|
||||
// 현재 AGV의 물리적 방향(Monitor Side)이 "Right" 상태여야 버퍼 진입이 용이하다고 가정.
|
||||
// Monitor Left 상태(부적절한 방향)라면 Gateway로 탈출해야 함.
|
||||
|
||||
// 이동 벡터 X 변화량
|
||||
// prev가 없거나 start와 같으면 이동 방향을 알 수 없음 -> 이 경우 보수적으로 기존 로직(Backward면 탈출) 따름
|
||||
int deltaX = 0;
|
||||
if (prev == null) return AGVPathResult.CreateFailure("이전 노드 정보가 없습니다");
|
||||
else deltaX = start.Position.X - prev.Position.X;
|
||||
|
||||
bool isMonitorLeft = false;
|
||||
|
||||
if (deltaX > 0) // 오른쪽(Forward)으로 이동해 옴 (예: 2 -> 4)
|
||||
{
|
||||
// 이동방향(Right) + 전진(F) => Monitor Right (Good)
|
||||
// 이동방향(Right) + 후진(B) => Monitor Left (Bad)
|
||||
isMonitorLeft = (prevDir == AgvDirection.Backward);
|
||||
}
|
||||
else if (deltaX < 0) // 왼쪽(Reverse)으로 이동해 옴 (예: 4 -> 2)
|
||||
{
|
||||
// 이동방향(Left) + 전진(F) => Monitor Left (Bad)
|
||||
// 이동방향(Left) + 후진(B) => Monitor Right (Good)
|
||||
isMonitorLeft = (prevDir == AgvDirection.Forward);
|
||||
}
|
||||
else // 제자리 또는 수직 이동
|
||||
{
|
||||
// 판단 불가 시 기존 로직(Backward면 Gateway) 유지
|
||||
return AGVPathResult.CreateFailure("이전 노드와의 방향을 알 수 없습니다");
|
||||
}
|
||||
|
||||
if (isMonitorLeft)
|
||||
{
|
||||
// Monitor Left 상태 (방향 불일치) -> Gateway로 탈출
|
||||
var GateWayNode = FindNode(6);
|
||||
var escPath = pathfinder.FindBasicPath(start, GateWayNode, prev, prevDir);
|
||||
if (!escPath.Success) return AGVPathResult.CreateFailure("버퍼 탈출 경로 실패");
|
||||
|
||||
var lastNode = escPath.Path.Last(); // Should be GW6
|
||||
var lastPrev = escPath.Path[escPath.Path.Count - 2];
|
||||
var lastDir = escPath.DetailedPath.Last().MotorDirection;
|
||||
|
||||
// Gateway 도착 후, Target(Buffer)으로 이동
|
||||
// 여기서부터는 "Monitor Right" 로직(즉, 적절한 방향 진입)을 적용합니다.
|
||||
// 6번에서 Target이 왼쪽이면 Direct(Backward), 오른쪽이면 Overshoot(Forward->Backward)
|
||||
|
||||
|
||||
MapNode GateprevNode = pathToGateway.Path.Last();
|
||||
NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.Last();
|
||||
bool isTargetLeftOfGW = target.Position.X < GateWayNode.Position.X;
|
||||
AGVPathResult entryPath = null;
|
||||
|
||||
//게이트웨이까지 후진으로 이동했다면 모니터방향이 오른쪽이다 => 방향전환필요
|
||||
var gateToTarget = GetPathFromGateway(pathfinder, GateWayNode, target, lastPrev, lastDir);
|
||||
|
||||
escPath.Path.RemoveAt(escPath.Path.Count - 1);
|
||||
escPath.DetailedPath.RemoveAt(escPath.DetailedPath.Count - 1);
|
||||
|
||||
|
||||
var arrivalOrientation = GatePrevDetail.MotorDirection;
|
||||
var final = CombinePaths(escPath, gateToTarget);
|
||||
ApplyResultToSimulator(final, agv);
|
||||
UpdateAdvancedPathDebugInfo(final);
|
||||
return final;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Monitor Right 상태 (방향 일치) -> 직접 진입 또는 Overshoot
|
||||
bool isTargetLeft = target.Position.X < start.Position.X;
|
||||
|
||||
//아래코드오류발생함
|
||||
var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, GateprevNode, arrivalOrientation);
|
||||
if (isTargetLeft)
|
||||
{
|
||||
var directPath = pathfinder.FindBasicPath(start, target, prev, AgvDirection.Backward);
|
||||
ApplyResultToSimulator(directPath, agv);
|
||||
UpdateAdvancedPathDebugInfo(directPath);
|
||||
return directPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
//대상이 나보다 우측에 있으니 RFID값이 읽어지는 위치까지 이동후에 다시 반대방향으로 마크스탑 해야 함
|
||||
//그냥 대상 노드까지 이동을 한다.. RFID값이 실제 멈추는 위치 이전에 있으니 그곳까지 이동하고 역방향 마크스탑하면 동일한 위치이다. 위 식은 그 이전노드까지 확실하게 이동하는 코드이다
|
||||
//var overshootNode = target.ConnectedMapNodes.OrderByDescending(n => n.Position.X).FirstOrDefault();
|
||||
//if (overshootNode == null || overshootNode.Position.X <= target.Position.X)
|
||||
// return (false, "Overshoot 공간 부족");
|
||||
|
||||
if (!gatewayPathResult.Success) return (false, $"{gatewayPathResult.ErrorMessage}");
|
||||
AGVPathResult finalPath = pathToGateway;
|
||||
var path1 = pathfinder.FindBasicPath(start, target, prev, AgvDirection.Forward);
|
||||
if (path1.Path.Count < 2) return AGVPathResult.CreateFailure("Overshoot 경로 생성 실패");
|
||||
|
||||
finalPath = CombinePaths(finalPath, gatewayPathResult);
|
||||
//목표에서 방향바꿔 마크스탑을 해야한다
|
||||
path1.Path.Add(path1.Path.Last());
|
||||
|
||||
//디테일은 방향바꿔서 추가 필요(속도는 저속처리해준다)
|
||||
var lastDefailt = path1.DetailedPath.Last();
|
||||
path1.DetailedPath.Add(new NodeMotorInfo(lastDefailt.seq + 1, lastDefailt.NodeId, lastDefailt.RfidId, AgvDirection.Backward)
|
||||
{
|
||||
Speed = SpeedLevel.L,
|
||||
IsPass = false,
|
||||
});
|
||||
|
||||
// 8. 적용
|
||||
ApplyResultToSimulator(finalPath, selectedAGV);
|
||||
UpdateAdvancedPathDebugInfo(finalPath);
|
||||
return (true, "성공");
|
||||
//var p1Last = path1.Path.Last();
|
||||
//var p1Prev = path1.Path[path1.Path.Count > 1 ? path1.Path.Count - 2 : 0]; // Safety check
|
||||
//var p1Dir = path1.DetailedPath.Last().MotorDirection;
|
||||
|
||||
//var path2 = _advancedPathfinder.FindPath(start, target, p1Last, p1Dir, AgvDirection.Backward);
|
||||
|
||||
//if (path2.Success && path2.DetailedPath.Last().NodeId == target.Id)
|
||||
// path2.DetailedPath = path2.DetailedPath.Take(path2.DetailedPath.Count - 1).ToList();
|
||||
|
||||
//var final = CombinePaths(path1, path2);
|
||||
ApplyResultToSimulator(path1, agv);
|
||||
UpdateAdvancedPathDebugInfo(path1);
|
||||
return path1;// (true, path1, "버퍼 우측(Overshoot)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2649,7 +2885,7 @@ namespace AGVSimulator.Forms
|
||||
/// <param name="PrevNode">게이트웨이 진입 전 노드</param>
|
||||
/// <param name="PrevDirection">게이트웨이 진입 전 모터방향</param>
|
||||
/// <returns></returns>
|
||||
private AGVPathResult GetPathFromGateway(MapNode GTNode, MapNode targetNode, MapNode PrevNode, AgvDirection PrevDirection)
|
||||
private AGVPathResult GetPathFromGateway(AGVPathfinder pathFinder, MapNode GTNode, MapNode targetNode, MapNode PrevNode, AgvDirection PrevDirection)
|
||||
{
|
||||
AGVPathResult resultPath = null;
|
||||
|
||||
@@ -2693,13 +2929,14 @@ namespace AGVSimulator.Forms
|
||||
rlt1.Success = true;
|
||||
|
||||
//목적지까지 바로 계산한다
|
||||
var pathtarget = _advancedPathfinder.FindBasicPath(GTNode, targetNode, PrevNode, AgvDirection.Backward);
|
||||
var motdir = targetNode.DockDirection == DockingDirection.Backward ? AgvDirection.Backward : AgvDirection.Forward;
|
||||
var pathtarget = pathFinder.FindBasicPath(GTNode, targetNode, PrevNode, motdir);
|
||||
|
||||
if (targetNode.DockDirection == DockingDirection.Backward && isMonitorLeft)
|
||||
{
|
||||
//턴을 하는
|
||||
turnPatterns = GetTurnaroundPattern(GTNode, targetNode);
|
||||
if (turnPatterns == null || turnPatterns.Any() == false) return new AGVPathResult { Success = false, ErrorMessage = $"회차 패턴 없음: Dir {PrevDirection}" };
|
||||
if (turnPatterns == null || turnPatterns.Any() == false) return new AGVPathResult { Success = false, Message = $"회차 패턴 없음: Dir {PrevDirection}" };
|
||||
foreach (var item in turnPatterns)
|
||||
{
|
||||
var rfidvalue = ushort.Parse(item.Substring(0, 4));
|
||||
@@ -2724,7 +2961,7 @@ namespace AGVSimulator.Forms
|
||||
if (pathtarget.DetailedPath.First().NodeId != rlt1.DetailedPath.Last().NodeId ||
|
||||
pathtarget.DetailedPath.First().MotorDirection != rlt1.DetailedPath.Last().MotorDirection)
|
||||
{
|
||||
new AGVPathResult { Success = false, ErrorMessage = $"게이트웨이 턴 마지막 주소와, 이 후 주소의 시작 노드ID가 일치하지 않습니다" };
|
||||
new AGVPathResult { Success = false, Message = $"게이트웨이 턴 마지막 주소와, 이 후 주소의 시작 노드ID가 일치하지 않습니다" };
|
||||
}
|
||||
|
||||
pathtarget.Path.RemoveAt(0);
|
||||
@@ -2800,120 +3037,6 @@ namespace AGVSimulator.Forms
|
||||
}
|
||||
}
|
||||
|
||||
private (bool result, string message) CalcPathBufferToBuffer(MapNode start, MapNode target, MapNode prev, AgvDirection prevDir, AgvDirection currentDir, VirtualAGV agv)
|
||||
{
|
||||
// Monitor Side 판단 로직
|
||||
// 현재 AGV의 물리적 방향(Monitor Side)이 "Right" 상태여야 버퍼 진입이 용이하다고 가정.
|
||||
// Monitor Left 상태(부적절한 방향)라면 Gateway로 탈출해야 함.
|
||||
|
||||
// 이동 벡터 X 변화량
|
||||
// prev가 없거나 start와 같으면 이동 방향을 알 수 없음 -> 이 경우 보수적으로 기존 로직(Backward면 탈출) 따름
|
||||
int deltaX = 0;
|
||||
if (prev == null) return (false, "이전 노드 정보가 없습니다");
|
||||
else deltaX = start.Position.X - prev.Position.X;
|
||||
|
||||
bool isMonitorLeft = false;
|
||||
|
||||
if (deltaX > 0) // 오른쪽(Forward)으로 이동해 옴 (예: 2 -> 4)
|
||||
{
|
||||
// 이동방향(Right) + 전진(F) => Monitor Right (Good)
|
||||
// 이동방향(Right) + 후진(B) => Monitor Left (Bad)
|
||||
isMonitorLeft = (prevDir == AgvDirection.Backward);
|
||||
}
|
||||
else if (deltaX < 0) // 왼쪽(Reverse)으로 이동해 옴 (예: 4 -> 2)
|
||||
{
|
||||
// 이동방향(Left) + 전진(F) => Monitor Left (Bad)
|
||||
// 이동방향(Left) + 후진(B) => Monitor Right (Good)
|
||||
isMonitorLeft = (prevDir == AgvDirection.Forward);
|
||||
}
|
||||
else // 제자리 또는 수직 이동
|
||||
{
|
||||
// 판단 불가 시 기존 로직(Backward면 Gateway) 유지
|
||||
return (false, "이전 노드와의 방향을 알 수 없습니다");
|
||||
}
|
||||
|
||||
if (isMonitorLeft)
|
||||
{
|
||||
// Monitor Left 상태 (방향 불일치) -> Gateway로 탈출
|
||||
var GateWayNode = FindNode(6);
|
||||
var escPath = _advancedPathfinder.FindBasicPath(start, GateWayNode, prev, prevDir);
|
||||
if (!escPath.Success) return (false, "버퍼 탈출 경로 실패");
|
||||
|
||||
var lastNode = escPath.Path.Last(); // Should be GW6
|
||||
var lastPrev = escPath.Path[escPath.Path.Count - 2];
|
||||
var lastDir = escPath.DetailedPath.Last().MotorDirection;
|
||||
|
||||
// Gateway 도착 후, Target(Buffer)으로 이동
|
||||
// 여기서부터는 "Monitor Right" 로직(즉, 적절한 방향 진입)을 적용합니다.
|
||||
// 6번에서 Target이 왼쪽이면 Direct(Backward), 오른쪽이면 Overshoot(Forward->Backward)
|
||||
|
||||
|
||||
bool isTargetLeftOfGW = target.Position.X < GateWayNode.Position.X;
|
||||
AGVPathResult entryPath = null;
|
||||
|
||||
//게이트웨이까지 후진으로 이동했다면 모니터방향이 오른쪽이다 => 방향전환필요
|
||||
var gateToTarget = GetPathFromGateway(GateWayNode, target, lastPrev, lastDir);
|
||||
|
||||
escPath.Path.RemoveAt(escPath.Path.Count - 1);
|
||||
escPath.DetailedPath.RemoveAt(escPath.DetailedPath.Count - 1);
|
||||
|
||||
|
||||
var final = CombinePaths(escPath, gateToTarget);
|
||||
ApplyResultToSimulator(final, agv);
|
||||
UpdateAdvancedPathDebugInfo(final);
|
||||
return (true, "버퍼 재진입(탈출후)");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Monitor Right 상태 (방향 일치) -> 직접 진입 또는 Overshoot
|
||||
bool isTargetLeft = target.Position.X < start.Position.X;
|
||||
|
||||
if (isTargetLeft)
|
||||
{
|
||||
var directPath = _advancedPathfinder.FindBasicPath(start, target, prev, AgvDirection.Backward);
|
||||
ApplyResultToSimulator(directPath, agv);
|
||||
UpdateAdvancedPathDebugInfo(directPath);
|
||||
return (true, "버퍼 좌측이동(직접진입)");
|
||||
}
|
||||
else
|
||||
{
|
||||
//대상이 나보다 우측에 있으니 RFID값이 읽어지는 위치까지 이동후에 다시 반대방향으로 마크스탑 해야 함
|
||||
//그냥 대상 노드까지 이동을 한다.. RFID값이 실제 멈추는 위치 이전에 있으니 그곳까지 이동하고 역방향 마크스탑하면 동일한 위치이다. 위 식은 그 이전노드까지 확실하게 이동하는 코드이다
|
||||
//var overshootNode = target.ConnectedMapNodes.OrderByDescending(n => n.Position.X).FirstOrDefault();
|
||||
//if (overshootNode == null || overshootNode.Position.X <= target.Position.X)
|
||||
// return (false, "Overshoot 공간 부족");
|
||||
|
||||
var path1 = _advancedPathfinder.FindBasicPath(start, target, prev, AgvDirection.Forward);
|
||||
if (path1.Path.Count < 2) return (false, "Overshoot 경로 생성 실패");
|
||||
|
||||
//목표에서 방향바꿔 마크스탑을 해야한다
|
||||
path1.Path.Add(path1.Path.Last());
|
||||
|
||||
//디테일은 방향바꿔서 추가 필요(속도는 저속처리해준다)
|
||||
var lastDefailt = path1.DetailedPath.Last();
|
||||
path1.DetailedPath.Add(new NodeMotorInfo(lastDefailt.seq + 1, lastDefailt.NodeId, lastDefailt.RfidId, AgvDirection.Backward)
|
||||
{
|
||||
Speed = SpeedLevel.L,
|
||||
IsPass = false,
|
||||
});
|
||||
|
||||
//var p1Last = path1.Path.Last();
|
||||
//var p1Prev = path1.Path[path1.Path.Count > 1 ? path1.Path.Count - 2 : 0]; // Safety check
|
||||
//var p1Dir = path1.DetailedPath.Last().MotorDirection;
|
||||
|
||||
//var path2 = _advancedPathfinder.FindPath(start, target, p1Last, p1Dir, AgvDirection.Backward);
|
||||
|
||||
//if (path2.Success && path2.DetailedPath.Last().NodeId == target.Id)
|
||||
// path2.DetailedPath = path2.DetailedPath.Take(path2.DetailedPath.Count - 1).ToList();
|
||||
|
||||
//var final = CombinePaths(path1, path2);
|
||||
ApplyResultToSimulator(path1, agv);
|
||||
UpdateAdvancedPathDebugInfo(path1);
|
||||
return (true, "버퍼 우측(Overshoot)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// p1+p2
|
||||
/// </summary>
|
||||
|
||||
@@ -371,7 +371,7 @@ namespace Project
|
||||
{
|
||||
// 경로 실패시 디버깅 정보 초기화
|
||||
//_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
|
||||
Message = $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}";
|
||||
Message = $"경로를 찾을 수 없습니다:\n{advancedResult.Message}";
|
||||
advancedResult = null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user