..
This commit is contained in:
@@ -80,13 +80,16 @@ namespace AGVNavigationCore.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPass { get; set; }
|
public bool IsPass { get; set; }
|
||||||
|
|
||||||
|
public bool IsTurn { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 특수 동작 설명
|
/// 특수 동작 설명
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SpecialActionDescription { get; set; }
|
public string SpecialActionDescription { get; set; }
|
||||||
|
|
||||||
public NodeMotorInfo(int seqno,string nodeId,ushort rfid, AgvDirection motorDirection, MapNode nextNodeId = null, MagnetDirection magnetDirection = MagnetDirection.Straight)
|
public NodeMotorInfo(int seqno,string nodeId,ushort rfid,
|
||||||
|
AgvDirection motorDirection, MapNode nextNodeId = null, MagnetDirection magnetDirection = MagnetDirection.Straight,bool turn=false)
|
||||||
{
|
{
|
||||||
|
IsTurn = turn;
|
||||||
seq = seqno;
|
seq = seqno;
|
||||||
NodeId = nodeId;
|
NodeId = nodeId;
|
||||||
RfidId = rfid;
|
RfidId = rfid;
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// 초기 상태 설정
|
// 초기 상태 설정
|
||||||
var openSet = new List<SearchState>();
|
var openSet = new List<SearchState>();
|
||||||
var closedSet = new HashSet<string>(); // Key: "CurrentID_PrevID"
|
var closedSet = new HashSet<string>(); // Key: "CurrentID_PrevID"
|
||||||
|
|
||||||
// 시작 상태 생성
|
// 시작 상태 생성
|
||||||
var startState = new SearchState
|
var startState = new SearchState
|
||||||
{
|
{
|
||||||
@@ -271,7 +271,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// 이미 방문한 더 나은 경로가 있는지 확인
|
// 이미 방문한 더 나은 경로가 있는지 확인
|
||||||
// (여기서는 ClosedSet만 체크하고 OpenSet 내 중복 처리는 생략 - 간단 구현)
|
// (여기서는 ClosedSet만 체크하고 OpenSet 내 중복 처리는 생략 - 간단 구현)
|
||||||
// A* 최적화를 위해 OpenSet 내 동일 상태(Key)가 있고 G Cost가 더 낮다면 Skip해야 함.
|
// A* 최적화를 위해 OpenSet 내 동일 상태(Key)가 있고 G Cost가 더 낮다면 Skip해야 함.
|
||||||
|
|
||||||
string newStateKey = GetStateKey(newState);
|
string newStateKey = GetStateKey(newState);
|
||||||
if (closedSet.Contains(newStateKey)) continue;
|
if (closedSet.Contains(newStateKey)) continue;
|
||||||
|
|
||||||
@@ -302,11 +302,11 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
public MapNode CurrentNode { get; set; }
|
public MapNode CurrentNode { get; set; }
|
||||||
public MapNode PreviousNode { get; set; }
|
public MapNode PreviousNode { get; set; }
|
||||||
public AgvDirection CurrentDirection { get; set; } // 현재 모터 방향 (Forward/Backward)
|
public AgvDirection CurrentDirection { get; set; } // 현재 모터 방향 (Forward/Backward)
|
||||||
|
|
||||||
public float GCost { get; set; }
|
public float GCost { get; set; }
|
||||||
public float HCost { get; set; }
|
public float HCost { get; set; }
|
||||||
public float FCost => GCost + HCost;
|
public float FCost => GCost + HCost;
|
||||||
|
|
||||||
public SearchState Parent { get; set; }
|
public SearchState Parent { get; set; }
|
||||||
public string TurnType { get; set; } // Debug info
|
public string TurnType { get; set; } // Debug info
|
||||||
}
|
}
|
||||||
@@ -351,11 +351,11 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
// 1. 기본 거리 비용
|
// 1. 기본 거리 비용
|
||||||
float dist = (float)Math.Sqrt(Math.Pow(current.CurrentNode.Position.X - next.Position.X, 2) + Math.Pow(current.CurrentNode.Position.Y - next.Position.Y, 2));
|
float dist = (float)Math.Sqrt(Math.Pow(current.CurrentNode.Position.X - next.Position.X, 2) + Math.Pow(current.CurrentNode.Position.Y - next.Position.Y, 2));
|
||||||
|
|
||||||
// 2. 회전 각도 계산
|
// 2. 회전 각도 계산
|
||||||
// Vector Prev -> Curr
|
// Vector Prev -> Curr
|
||||||
// Vector Curr -> Next
|
// Vector Curr -> Next
|
||||||
|
|
||||||
double angle = 180.0; // Straight
|
double angle = 180.0; // Straight
|
||||||
if (current.PreviousNode != null)
|
if (current.PreviousNode != null)
|
||||||
{
|
{
|
||||||
@@ -379,7 +379,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// 부동소수점 오차 보정
|
// 부동소수점 오차 보정
|
||||||
if (cosTheta > 1.0) cosTheta = 1.0;
|
if (cosTheta > 1.0) cosTheta = 1.0;
|
||||||
if (cosTheta < -1.0) cosTheta = -1.0;
|
if (cosTheta < -1.0) cosTheta = -1.0;
|
||||||
|
|
||||||
double rad = Math.Acos(cosTheta);
|
double rad = Math.Acos(cosTheta);
|
||||||
// 외적을 이용해 좌/우 판별이 가능하지만, 여기서는 "꺾인 정도"만 중요하므로 내적각(0~180)만 사용
|
// 외적을 이용해 좌/우 판별이 가능하지만, 여기서는 "꺾인 정도"만 중요하므로 내적각(0~180)만 사용
|
||||||
// 180도 = 직진, 90도 = 직각, 0도 = U턴(완전 뒤로)
|
// 180도 = 직진, 90도 = 직각, 0도 = U턴(완전 뒤로)
|
||||||
@@ -387,14 +387,14 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// Prev->Curr 가 (1,0) 이고 Curr->Next가 (1,0) 이면 각도 0도? 아니면 180?
|
// Prev->Curr 가 (1,0) 이고 Curr->Next가 (1,0) 이면 각도 0도? 아니면 180?
|
||||||
// 위 내적 계산에서 같은 방향이면 cos=1 -> acos=0. 즉 0도가 직진임.
|
// 위 내적 계산에서 같은 방향이면 cos=1 -> acos=0. 즉 0도가 직진임.
|
||||||
// 180도가 U턴(역방향).
|
// 180도가 U턴(역방향).
|
||||||
|
|
||||||
// 변환: 사용자가 "120도 이상 완만해야" 라고 했음.
|
// 변환: 사용자가 "120도 이상 완만해야" 라고 했음.
|
||||||
// 그림상 11->3->4는 예각(Sharp turn).
|
// 그림상 11->3->4는 예각(Sharp turn).
|
||||||
// 직선 주행시 각도 변화량(Deviation)으로 생각하면:
|
// 직선 주행시 각도 변화량(Deviation)으로 생각하면:
|
||||||
// 직진 = 0도 변화.
|
// 직진 = 0도 변화.
|
||||||
// 90도 턴 = 90도 변화.
|
// 90도 턴 = 90도 변화.
|
||||||
// U턴 = 180도 변화.
|
// U턴 = 180도 변화.
|
||||||
|
|
||||||
angle = rad * (180.0 / Math.PI); // 0(직진) ~ 180(U턴)
|
angle = rad * (180.0 / Math.PI); // 0(직진) ~ 180(U턴)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,15 +411,15 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// 즉, "진행 방향의 꺾임각"이 크다(예: 150도 꺾임) = "내각"이 작다(30도).
|
// 즉, "진행 방향의 꺾임각"이 크다(예: 150도 꺾임) = "내각"이 작다(30도).
|
||||||
// "120도 이상 완만해야" -> 내각이 120도 이상(점잖은 턴)이어야 한다.
|
// "120도 이상 완만해야" -> 내각이 120도 이상(점잖은 턴)이어야 한다.
|
||||||
// 즉, 꺾임각(Deviation)은 60도 이하여야 한다.
|
// 즉, 꺾임각(Deviation)은 60도 이하여야 한다.
|
||||||
|
|
||||||
// 내 알고리즘의 `angle`은 "꺾임각(Deviation)"임. (0=직진, 180=U턴)
|
// 내 알고리즘의 `angle`은 "꺾임각(Deviation)"임. (0=직진, 180=U턴)
|
||||||
// 따라서 "내각 120도 이상" == "꺾임각 60도 이하".
|
// 따라서 "내각 120도 이상" == "꺾임각 60도 이하".
|
||||||
// 그러므로 angle <= 60 이어야 Normal Turn 가능.
|
// 그러므로 angle <= 60 이어야 Normal Turn 가능.
|
||||||
|
|
||||||
// 3번 노드 (RFID 3) 특수성:
|
// 3번 노드 (RFID 3) 특수성:
|
||||||
// "3번 노드에서만 180도 턴 가능"
|
// "3번 노드에서만 180도 턴 가능"
|
||||||
// "3->4 처럼 각이 좁은 경우 3번으로 가서 모터방향 바꿔서 4번으로 감"
|
// "3->4 처럼 각이 좁은 경우 3번으로 가서 모터방향 바꿔서 4번으로 감"
|
||||||
|
|
||||||
bool isRfid3 = current.CurrentNode.RfidId == 3;
|
bool isRfid3 = current.CurrentNode.RfidId == 3;
|
||||||
bool isSharpTurn = angle > 60.0; // 60도보다 많이 꺾이면 Sharp Turn (내각 120도 미만)
|
bool isSharpTurn = angle > 60.0; // 60도보다 많이 꺾이면 Sharp Turn (내각 120도 미만)
|
||||||
|
|
||||||
@@ -443,8 +443,8 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// 비용 패널티 추가 (멈추고 바꾸는 시간 등)
|
// 비용 패널티 추가 (멈추고 바꾸는 시간 등)
|
||||||
res.IsPossible = true;
|
res.IsPossible = true;
|
||||||
res.Cost = dist + 5000; // 큰 비용 (Switch Penalty)
|
res.Cost = dist + 5000; // 큰 비용 (Switch Penalty)
|
||||||
res.NextDirection = (current.CurrentDirection == AgvDirection.Forward)
|
res.NextDirection = (current.CurrentDirection == AgvDirection.Forward)
|
||||||
? AgvDirection.Backward
|
? AgvDirection.Backward
|
||||||
: AgvDirection.Forward;
|
: AgvDirection.Forward;
|
||||||
res.TurnType = "SwitchBack";
|
res.TurnType = "SwitchBack";
|
||||||
return res;
|
return res;
|
||||||
@@ -466,7 +466,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
// 도착 시 모터 방향 확인
|
// 도착 시 모터 방향 확인
|
||||||
// SearchState에 저장된 CurrentDirection이 도착 시 모터 방향임.
|
// SearchState에 저장된 CurrentDirection이 도착 시 모터 방향임.
|
||||||
|
|
||||||
if (target.DockDirection == DockingDirection.Forward)
|
if (target.DockDirection == DockingDirection.Forward)
|
||||||
return state.CurrentDirection == AgvDirection.Forward;
|
return state.CurrentDirection == AgvDirection.Forward;
|
||||||
|
|
||||||
@@ -484,7 +484,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
var current = endState;
|
var current = endState;
|
||||||
var pathStack = new Stack<SearchState>();
|
var pathStack = new Stack<SearchState>();
|
||||||
|
|
||||||
while (current != null)
|
while (current != null)
|
||||||
{
|
{
|
||||||
pathStack.Push(current);
|
pathStack.Push(current);
|
||||||
@@ -497,10 +497,10 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
{
|
{
|
||||||
var step = pathStack.Pop();
|
var step = pathStack.Pop();
|
||||||
pathList.Add(step.CurrentNode);
|
pathList.Add(step.CurrentNode);
|
||||||
|
|
||||||
// Detailed Info (마그넷 방향 계산 등은 후처리 필요할 수도 있으나 여기선 단순화)
|
// Detailed Info (마그넷 방향 계산 등은 후처리 필요할 수도 있으나 여기선 단순화)
|
||||||
// SearchState에는 "어떤 방향으로 왔는지"가 저장되어 있음.
|
// SearchState에는 "어떤 방향으로 왔는지"가 저장되어 있음.
|
||||||
|
|
||||||
// 마그넷 방향 계산 (다음 노드가 있을 때)
|
// 마그넷 방향 계산 (다음 노드가 있을 때)
|
||||||
MagnetDirection magDir = MagnetDirection.Straight;
|
MagnetDirection magDir = MagnetDirection.Straight;
|
||||||
if (pathStack.Count > 0)
|
if (pathStack.Count > 0)
|
||||||
@@ -510,33 +510,37 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
if (step.CurrentNode.MagnetDirections.ContainsKey(nextStep.CurrentNode.Id))
|
if (step.CurrentNode.MagnetDirections.ContainsKey(nextStep.CurrentNode.Id))
|
||||||
{
|
{
|
||||||
var magPos = step.CurrentNode.MagnetDirections[nextStep.CurrentNode.Id];
|
var magPos = step.CurrentNode.MagnetDirections[nextStep.CurrentNode.Id];
|
||||||
if (magPos == MagnetPosition.L) magDir = MagnetDirection.Left;
|
if (magPos == MagnetPosition.L) magDir = MagnetDirection.Left;
|
||||||
else if (magPos == MagnetPosition.R) magDir = MagnetDirection.Right;
|
else if (magPos == MagnetPosition.R) magDir = MagnetDirection.Right;
|
||||||
|
|
||||||
// 만약 SwitchBack 상황이라면?
|
// 만약 SwitchBack 상황이라면?
|
||||||
// 모터 방향이 바뀌었다면 마그넷 방향도 그에 맞춰야 하나?
|
// 모터 방향이 바뀌었다면 마그넷 방향도 그에 맞춰야 하나?
|
||||||
// 기존 로직 참고: MagnetDirection은 "진행 방향 기준" 좌/우 인가? 아님 절대적?
|
// 기존 로직 참고: MagnetDirection은 "진행 방향 기준" 좌/우 인가? 아님 절대적?
|
||||||
// 보통 "갈림길에서 어느 쪽" 인지 나타냄.
|
// 보통 "갈림길에서 어느 쪽" 인지 나타냄.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detailedList.Add(new NodeMotorInfo(seq++, step.CurrentNode.Id, step.CurrentNode.RfidId, step.CurrentDirection, null, magDir)); // NextNode는 일단 null
|
detailedList.Add(new NodeMotorInfo(seq++, step.CurrentNode.Id, step.CurrentNode.RfidId, step.CurrentDirection, null, magDir)); // NextNode는 일단 null
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextNode 정보 채우기
|
// NextNode 정보 채우기
|
||||||
for (int i = 0; i < detailedList.Count - 1; i++)
|
for (int i = 0; i < detailedList.Count - 1; i++)
|
||||||
{
|
{
|
||||||
detailedList[i].NextNode = _mapNodes.FirstOrDefault(n => n.Id == detailedList[i+1].NodeId);
|
detailedList[i].NextNode = _mapNodes.FirstOrDefault(n => n.Id == detailedList[i + 1].NodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Path = pathList;
|
result.Path = pathList;
|
||||||
result.DetailedPath = detailedList;
|
result.DetailedPath = detailedList;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
public enum MapZoneMonitor
|
||||||
|
{
|
||||||
|
LeftTop,
|
||||||
|
RightBtm,
|
||||||
|
}
|
||||||
public enum MapZone
|
public enum MapZone
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
@@ -545,91 +549,261 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
Plating, // 72 ~ 05
|
Plating, // 72 ~ 05
|
||||||
Loader, // 71 ~ 04
|
Loader, // 71 ~ 04
|
||||||
Cleaner, // 70 ~ 01
|
Cleaner, // 70 ~ 01
|
||||||
Junction // Hub (11, 12, etc)
|
Junction, // Hub (11, 12, etc)
|
||||||
|
Turn,
|
||||||
|
}
|
||||||
|
public class MapZonePathData
|
||||||
|
{
|
||||||
|
public StationType NodeSta { get; set; }
|
||||||
|
public StationType NodeEnd { get; set; }
|
||||||
|
public MapZoneMonitor Monitor { get; set; }
|
||||||
|
public List<string> Path { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<MapZonePathData> GetMapZonePathData()
|
||||||
|
{
|
||||||
|
var retval = new List<MapZonePathData>();
|
||||||
|
|
||||||
|
// Buffer -> ...
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "36F", "35F", "31F", "32F", "33F", "34F", "7F", "12F", "11B", "5B", "6B", "73B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "36B", "35B", "31B", "32B", "33B", "34B", "7B", "10B", "6B", "73B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "36F", "35F", "31F", "32F", "33F", "34F", "7F", "12F", "11B", "5B", "72B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "36B", "35B", "31B", "32B", "33B", "34B", "7B", "12B", "11B", "3T", "3B", "11B", "5B", "72B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "36F", "35F", "31F", "32F", "33F", "34F", "7F", "12F", "11F", "3B", "4B", "71B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "36B", "35B", "31B", "32B", "33B", "34B", "7B", "12B", "11B", "3T", "3B", "4B", "71B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "36F", "35F", "31F", "32F", "33F", "34F", "7F", "12F", "11F", "3T", "3B", "2B", "1B", "70B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Buffer, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "36B", "35B", "31B", "32B", "33B", "34B", "7B", "12B", "11B", "3B", "2B", "1B", "70B" } });
|
||||||
|
|
||||||
|
// Loader -> ...
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "71B", "4B", "3B", "2B", "1B", "70B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "71F", "4F", "3T", "3B", "2B", "1B", "70B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "71B", "4B", "3T", "3B", "11B", "5B", "6B", "73B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "71F", "4F", "3B", "11B", "5B", "6B", "73B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "71B", "4B", "3T", "3B", "11B", "5B", "72B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "71F", "4F", "3B", "11B", "5B", "72B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "71B", "4B", "3T", "3B", "11B", "12B", "7B", "8B", "9B", "34B", "33B", "32B", "31B", "35B", "36B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Loader, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "71F", "4F", "3B", "11B", "12B", "7B", "8B", "9B", "34B", "33B", "32B", "31B", "35B", "36B" } });
|
||||||
|
|
||||||
|
// Cleaner -> ...
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "70F", "1F", "2F", "3T", "3B", "4B", "71B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "70B", "1B", "2B", "3B", "4B", "71B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "70F", "1F", "2F", "3T", "3B", "11B", "5B", "6B", "73B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "70B", "1B", "2B", "3B", "11B", "5B", "6B", "73B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "70F", "1F", "2F", "3T", "3B", "11B", "5B", "72B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Plating, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "70B", "1B", "2B", "3B", "11B", "5B", "72B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "70F", "1F", "2F", "3T", "3B", "11B", "12B", "7B", "8B", "9B", "34B", "33B", "32B", "31B", "35B", "36B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Cleaner, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "70B", "1B", "2B", "3B", "11B", "12B", "7B", "8B", "9B", "34B", "33B", "32B", "31B", "35B", "36B" } });
|
||||||
|
|
||||||
|
// Plating -> ...
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "72F", "5F", "11F", "3B", "4B", "71B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Loader, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "72B", "5B", "11B", "3T", "3B", "4B", "71B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "72F", "5F", "11B", "5B", "6B", "73B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Charger, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "72B", "5B", "11B", "3T", "3B", "11B", "5B", "6B", "73B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "72F", "5F", "11F", "3T", "3B", "2B", "1B", "70B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Cleaner, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "72B", "5B", "11B", "3B", "2B", "1B", "70B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.RightBtm, Path = new List<string> { "72F", "5F", "11B", "12B", "7B", "8B", "9B", "34B", "33B", "32B", "31B", "35B", "36B" } });
|
||||||
|
retval.Add(new MapZonePathData { NodeSta = StationType.Plating, NodeEnd = StationType.Buffer, Monitor = MapZoneMonitor.LeftTop, Path = new List<string> { "72B", "5B", "11B", "3T", "3B", "11B", "12B", "7B", "8B", "9B", "34B", "33B", "32B", "31B", "35B", "36B" } });
|
||||||
|
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 해당 노드가 속하는 존을 반환한다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public MapZone GetMapZone(MapNode node)
|
public MapZone GetMapZone(MapNode node)
|
||||||
{
|
{
|
||||||
if (node == null) return MapZone.None;
|
if (node == null) return MapZone.None;
|
||||||
int rfid = node.RfidId;
|
int rfid = node.RfidId;
|
||||||
|
|
||||||
// Buffer: 91~07 (Linear)
|
Dictionary<MapZone, int[]> ZoneList = GetMapZoneNodeList();
|
||||||
// Assuming 91 is start, 07 is end.
|
var zone = ZoneList.Where(t => t.Value.Contains(rfid)).FirstOrDefault();
|
||||||
// Range check might be tricky if IDs are not sequential.
|
if (zone.Value == null) return MapZone.None;
|
||||||
// Using precise list based on map description if possible, acts as a catch-all for now.
|
|
||||||
if (rfid == 91 || (rfid >= 31 && rfid <= 36) || (rfid >= 7 && rfid <= 9)) return MapZone.Buffer;
|
|
||||||
|
|
||||||
// Charger: 73~10
|
|
||||||
if (rfid == 73 || rfid == 6 || rfid == 10) return MapZone.Charger;
|
|
||||||
|
|
||||||
// Plating: 72~5
|
return zone.Key;
|
||||||
if (rfid == 72 || rfid == 5) return MapZone.Plating;
|
|
||||||
|
|
||||||
// Loader: 71~4
|
|
||||||
if (rfid == 71 || rfid == 4) return MapZone.Loader;
|
|
||||||
|
|
||||||
// Cleaner: 70~1
|
|
||||||
if (rfid == 70 || rfid == 1 || rfid == 2 || rfid == 3) return MapZone.Cleaner;
|
|
||||||
|
|
||||||
// Junction (Hub)
|
|
||||||
if (rfid == 11 || rfid == 12) return MapZone.Junction;
|
|
||||||
|
|
||||||
return MapZone.None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Dictionary<MapZone, int[]> GetMapZoneNodeList()
|
||||||
|
{
|
||||||
|
Dictionary<MapZone, int[]> ZoneList = new Dictionary<MapZone, int[]>();
|
||||||
|
ZoneList.Add(MapZone.Turn, new int[] { 3 });
|
||||||
|
ZoneList.Add(MapZone.Buffer, new int[] { 91, 36, 35, 31, 32, 33, 34, 9, 8, 7 });
|
||||||
|
ZoneList.Add(MapZone.Charger, new int[] { 73, 6, 10 });
|
||||||
|
ZoneList.Add(MapZone.Junction, new int[] { 12, 11 });
|
||||||
|
ZoneList.Add(MapZone.Plating, new int[] { 72, 5 });
|
||||||
|
ZoneList.Add(MapZone.Loader, new int[] { 71, 4 });
|
||||||
|
ZoneList.Add(MapZone.Cleaner, new int[] { 70, 1, 2 });
|
||||||
|
return ZoneList;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public AGVPathResult CalculateScriptedPath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection prevDir)
|
public AGVPathResult CalculateScriptedPath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection prevDir)
|
||||||
{
|
{
|
||||||
var startZone = GetMapZone(startNode);
|
var startZone = GetMapZone(startNode);
|
||||||
var targetZone = GetMapZone(targetNode);
|
var targetZone = GetMapZone(targetNode);
|
||||||
|
|
||||||
// 1. Same Zone or Trivial Case -> Use CalculatePath_new
|
// 존이 확인되지 않는다면 오류
|
||||||
if (startZone == targetZone && startZone != MapZone.None && startZone != MapZone.Junction)
|
if (startZone == MapZone.None || targetZone == MapZone.None)
|
||||||
{
|
{
|
||||||
return CalculatePath_new(startNode, targetNode, prevNode, prevDir);
|
// return AGVPathResult.CreateFailure($"Zone not found: {startNode.ID2}->{targetNode.ID2}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Hub Logic (Buffer -> Hub -> Target, etc.)
|
var monitorMode = GetMonitorMode(startNode, prevNode, prevDir);
|
||||||
// Logic: Start -> ExitNode -> Hub -> EntryNode -> Target
|
var motDir = prevDir == AgvDirection.Forward ? 'F' : 'B';
|
||||||
|
|
||||||
|
// 시작 태그 검색용 (예: "91F")
|
||||||
|
string startTag = $"{startNode.RfidId}{motDir}";
|
||||||
|
|
||||||
MapNode exitNode = GetZoneExitNode(startZone);
|
// 모니터방향이 일치하고 대상노드가 동일한 경로를 찾는다
|
||||||
MapNode entryNode = GetZoneEntryNode(targetZone);
|
var zonepath = GetMapZonePathData();
|
||||||
|
|
||||||
|
// 모든 후보 경로 검색
|
||||||
|
var candidates = zonepath.Where(d =>
|
||||||
|
d.Monitor == monitorMode &&
|
||||||
|
d.NodeEnd == targetNode.StationType &&
|
||||||
|
d.Path.Any(p => p.StartsWith(startNode.RfidId.ToString())) && // 시작 포인트 포함
|
||||||
|
d.Path.Any(p => p.StartsWith(targetNode.RfidId.ToString())) // 끝 포인트 포함
|
||||||
|
).ToList();
|
||||||
|
|
||||||
// If Start/Target are in Junction or Unknown, handle gracefully
|
if (candidates.Any())
|
||||||
if (startZone == MapZone.Junction) exitNode = startNode;
|
|
||||||
if (targetZone == MapZone.Junction) entryNode = targetNode;
|
|
||||||
|
|
||||||
if (exitNode == null || entryNode == null)
|
|
||||||
{
|
{
|
||||||
// Fallback to normal search if zone logic fails
|
MapZonePathData bestPath = null;
|
||||||
return CalculatePath_new(startNode, targetNode, prevNode, prevDir);
|
int bestStartIndex = -1;
|
||||||
|
int bestEndIndex = -1;
|
||||||
|
int minPathLength = int.MaxValue;
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
// 시작 태그와 가장 일치하는 인덱스 찾기 (방향까지 고려 "91F")
|
||||||
|
int startIndex = candidate.Path.FindIndex(p => p == startTag);
|
||||||
|
if (startIndex == -1) // 방향이 안 맞으면 그냥 RFID로만 찾기
|
||||||
|
startIndex = candidate.Path.FindIndex(p => p.StartsWith(startNode.RfidId.ToString()));
|
||||||
|
|
||||||
|
// 끝 태그 인덱스 (뒤에서부터 찾기)
|
||||||
|
int endIndex = candidate.Path.FindLastIndex(p => p.StartsWith(targetNode.RfidId.ToString()));
|
||||||
|
|
||||||
|
if (startIndex != -1 && endIndex != -1 && startIndex < endIndex)
|
||||||
|
{
|
||||||
|
int length = endIndex - startIndex;
|
||||||
|
if (length < minPathLength)
|
||||||
|
{
|
||||||
|
minPathLength = length;
|
||||||
|
bestPath = candidate;
|
||||||
|
bestStartIndex = startIndex;
|
||||||
|
bestEndIndex = endIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestPath != null)
|
||||||
|
{
|
||||||
|
// 추출된 경로 조각
|
||||||
|
var slicedPath = bestPath.Path.Skip(bestStartIndex).Take(bestEndIndex - bestStartIndex + 1).ToList();
|
||||||
|
var a = ConvertHardcodedPathToResult(slicedPath, startNode, prevNode, prevDir);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path 1: Start -> Exit
|
// 하드코딩된 경로가 없으면 기존 A* 로직으로 대체 또는 실패 반환
|
||||||
var path1 = CalculatePath_new(startNode, exitNode, prevNode, prevDir);
|
return null;
|
||||||
if (!path1.Success) return AGVPathResult.CreateFailure($"Zone Exit Failure: {startNode.ID2}->{exitNode.ID2}");
|
}
|
||||||
|
|
||||||
// Path 2: Exit -> Entry (Hub Crossing)
|
private MapZoneMonitor GetMonitorMode(MapNode startNode, MapNode prevNode, AgvDirection prevDir)
|
||||||
// Use CalculatePath_new for Hub crossing relative to Arrival Direction
|
{
|
||||||
var lastNode1 = path1.Path.Last();
|
if (prevNode == null) return MapZoneMonitor.RightBtm;
|
||||||
var lastDir1 = path1.DetailedPath.Last().MotorDirection;
|
|
||||||
var prevNode1 = path1.Path.Count > 1 ? path1.Path[path1.Path.Count - 2] : prevNode;
|
|
||||||
|
|
||||||
var path2 = CalculatePath_new(exitNode, entryNode, prevNode1, lastDir1);
|
int dx = startNode.Position.X - prevNode.Position.X;
|
||||||
if (!path2.Success) return AGVPathResult.CreateFailure($"Hub Crossing Failure: {exitNode.ID2}->{entryNode.ID2}");
|
int dy = startNode.Position.Y - prevNode.Position.Y;
|
||||||
|
|
||||||
// Path 3: Entry -> Target
|
bool isMonitorLeft = false;
|
||||||
var lastNode2 = path2.Path.Last();
|
if (Math.Abs(dx) > Math.Abs(dy)) // Horizontal
|
||||||
var lastDir2 = path2.DetailedPath.Last().MotorDirection;
|
{
|
||||||
var prevNode2 = path2.Path.Count > 1 ? path2.Path[path2.Path.Count - 2] : lastNode1;
|
isMonitorLeft = (prevDir == AgvDirection.Backward);
|
||||||
|
}
|
||||||
|
else // Vertical
|
||||||
|
{
|
||||||
|
isMonitorLeft = (prevDir == AgvDirection.Forward);
|
||||||
|
}
|
||||||
|
|
||||||
var path3 = CalculatePath_new(entryNode, targetNode, prevNode2, lastDir2);
|
return isMonitorLeft ? MapZoneMonitor.LeftTop : MapZoneMonitor.RightBtm;
|
||||||
if (!path3.Success) return AGVPathResult.CreateFailure($"Zone Entry Failure: {entryNode.ID2}->{targetNode.ID2}");
|
}
|
||||||
|
|
||||||
// Merge Paths
|
private AGVPathResult ConvertHardcodedPathToResult(List<string> pathStrings, MapNode startNode, MapNode prevNode, AgvDirection prevDir)
|
||||||
var merged = Utility.CombineResults(path1, path2);
|
{
|
||||||
merged = Utility.CombineResults(merged, path3);
|
var result = new AGVPathResult { Success = true };
|
||||||
|
var pathList = new List<MapNode>();
|
||||||
|
var detailedList = new List<NodeMotorInfo>();
|
||||||
|
|
||||||
return merged;
|
int seq = 1;
|
||||||
|
for (int i = 0; i < pathStrings.Count; i++)
|
||||||
|
{
|
||||||
|
string s = pathStrings[i];
|
||||||
|
if (string.IsNullOrEmpty(s)) continue;
|
||||||
|
|
||||||
|
string rfIdStr = "";
|
||||||
|
char flag = ' ';
|
||||||
|
|
||||||
|
foreach (char c in s)
|
||||||
|
{
|
||||||
|
if (char.IsDigit(c)) rfIdStr += c;
|
||||||
|
else flag = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = _mapNodes.FirstOrDefault(n => n.RfidId.ToString() == rfIdStr);
|
||||||
|
if (node == null) continue;
|
||||||
|
|
||||||
|
// Determine Motor Direction from Flag or Maintain Previous
|
||||||
|
AgvDirection motorDir = detailedList.Count > 0 ? detailedList.Last().MotorDirection : prevDir;
|
||||||
|
bool isTurn = false;
|
||||||
|
|
||||||
|
if (flag == 'F') motorDir = AgvDirection.Forward;
|
||||||
|
else if (flag == 'B') motorDir = AgvDirection.Backward;
|
||||||
|
else if (flag == 'T') isTurn = true;
|
||||||
|
|
||||||
|
pathList.Add(node);
|
||||||
|
|
||||||
|
// Magnet direction lookup
|
||||||
|
MagnetDirection magDir = MagnetDirection.Straight;
|
||||||
|
if (i + 1 < pathStrings.Count)
|
||||||
|
{
|
||||||
|
var nextTag = pathStrings[i + 1];
|
||||||
|
string nextRfidStr = "";
|
||||||
|
foreach (char c in nextTag) if (char.IsDigit(c)) nextRfidStr += c;
|
||||||
|
|
||||||
|
var nextNode = _mapNodes.FirstOrDefault(n => n.RfidId.ToString() == nextRfidStr);
|
||||||
|
if (nextNode != null && node.MagnetDirections.ContainsKey(nextNode.Id))
|
||||||
|
{
|
||||||
|
var magPos = node.MagnetDirections[nextNode.Id];
|
||||||
|
if (magPos == MagnetPosition.R) magDir = MagnetDirection.Right;
|
||||||
|
else if (magPos == MagnetPosition.L) magDir = MagnetDirection.Left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = new NodeMotorInfo(seq++, node.Id, node.RfidId, motorDir, null, magDir, isTurn);
|
||||||
|
detailedList.Add(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect NextNode pointers
|
||||||
|
for (int i = 0; i < detailedList.Count - 1; i++)
|
||||||
|
{
|
||||||
|
detailedList[i].NextNode = pathList[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Path = pathList;
|
||||||
|
result.DetailedPath = detailedList;
|
||||||
|
result.TotalDistance = CalculatePathDistance(pathList);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculatePathDistance(List<MapNode> path)
|
||||||
|
{
|
||||||
|
float dist = 0;
|
||||||
|
for (int i = 0; i < path.Count - 1; i++)
|
||||||
|
{
|
||||||
|
dist += (float)Math.Sqrt(Math.Pow(path[i].Position.X - path[i + 1].Position.X, 2) + Math.Pow(path[i].Position.Y - path[i + 1].Position.Y, 2));
|
||||||
|
}
|
||||||
|
return dist;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MapNode GetZoneExitNode(MapZone zone)
|
private MapNode GetZoneExitNode(MapZone zone)
|
||||||
@@ -649,7 +823,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
private MapNode GetZoneEntryNode(MapZone zone)
|
private MapNode GetZoneEntryNode(MapZone zone)
|
||||||
{
|
{
|
||||||
int entryRfid = -1;
|
int entryRfid = -1;
|
||||||
switch (zone)
|
switch (zone)
|
||||||
{
|
{
|
||||||
case MapZone.Buffer: entryRfid = 7; break; // Bi-directional entry/exit?
|
case MapZone.Buffer: entryRfid = 7; break; // Bi-directional entry/exit?
|
||||||
@@ -736,7 +910,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
if (!fixpath)
|
if (!fixpath)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
if (targetNode.StationType == StationType.Limit || targetNode.StationType == StationType.Normal)
|
if (targetNode.StationType == StationType.Limit || targetNode.StationType == StationType.Normal)
|
||||||
{
|
{
|
||||||
@@ -790,7 +964,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
if (pathToGateway.Path.Count > 1)
|
if (pathToGateway.Path.Count > 1)
|
||||||
{
|
{
|
||||||
//다음이동방향이 이전노드와 동일하다면? 되돌아가야한다는것이다
|
//다음이동방향이 이전노드와 동일하다면? 되돌아가야한다는것이다
|
||||||
predictNext = pathToGateway.Path[1];
|
predictNext = pathToGateway.Path[1];
|
||||||
if (predictNext.Id == prevNode.Id)
|
if (predictNext.Id == prevNode.Id)
|
||||||
{
|
{
|
||||||
var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||||
|
|||||||
@@ -1291,7 +1291,7 @@ namespace AGVSimulator.Forms
|
|||||||
|
|
||||||
|
|
||||||
var flags = new List<string>();
|
var flags = new List<string>();
|
||||||
if (info.CanRotate) flags.Add("회전가능");
|
if (info.IsTurn) flags.Add("회전");
|
||||||
if (info.IsDirectionChangePoint) flags.Add("방향전환");
|
if (info.IsDirectionChangePoint) flags.Add("방향전환");
|
||||||
if (info.RequiresSpecialAction) flags.Add($"특수동작:{info.SpecialActionDescription}");
|
if (info.RequiresSpecialAction) flags.Add($"특수동작:{info.SpecialActionDescription}");
|
||||||
if (info.MagnetDirection != MagnetDirection.Straight) flags.Add($"마그넷:{info.MagnetDirection}");
|
if (info.MagnetDirection != MagnetDirection.Straight) flags.Add($"마그넷:{info.MagnetDirection}");
|
||||||
@@ -1322,6 +1322,8 @@ namespace AGVSimulator.Forms
|
|||||||
else if (motorInfo.IsDirectionChangePoint && motorInfo.CanRotate)
|
else if (motorInfo.IsDirectionChangePoint && motorInfo.CanRotate)
|
||||||
motorSymbol += "[↻]";
|
motorSymbol += "[↻]";
|
||||||
|
|
||||||
|
if (motorInfo.IsTurn) motorSymbol = "[TURN]";
|
||||||
|
|
||||||
pathWithDetails.Add($"{rfidId}{motorSymbol}");
|
pathWithDetails.Add($"{rfidId}{motorSymbol}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1596,7 +1598,7 @@ namespace AGVSimulator.Forms
|
|||||||
MotorDirection = directionName,
|
MotorDirection = directionName,
|
||||||
CurrentPosition = GetNodeDisplayName(currentNode),
|
CurrentPosition = GetNodeDisplayName(currentNode),
|
||||||
TargetPosition = GetNodeDisplayName(targetNode),
|
TargetPosition = GetNodeDisplayName(targetNode),
|
||||||
DockingPosition = (targetNode.StationType == StationType.Charger ) ? "충전기" : "장비"
|
DockingPosition = (targetNode.StationType == StationType.Charger) ? "충전기" : "장비"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (calcResult.Success)
|
if (calcResult.Success)
|
||||||
|
|||||||
Reference in New Issue
Block a user