This commit is contained in:
backuppc
2026-01-09 17:25:53 +09:00
parent efab3d042c
commit 880dc526da
9 changed files with 749 additions and 206 deletions

View File

@@ -421,7 +421,7 @@ namespace AGVNavigationCore.Controls
(int)(point.Y + arrowSize * Math.Sin(angle + 4 * Math.PI / 3))
);
var arrowColor = direction == AgvDirection.Forward ? Color.Blue : Color.Red;
var arrowColor = direction == AgvDirection.Forward ? Color.Blue : Color.Yellow;
var arrowBrush = new SolidBrush(arrowColor);
// 정삼각형으로 화살표 그리기 (내부 채움)
@@ -525,7 +525,15 @@ namespace AGVNavigationCore.Controls
var angle = Math.Atan2(nextNode.Position.Y - currentNode.Position.Y,
nextNode.Position.X - currentNode.Position.X);
DrawDirectionArrow(g, midPoint, angle, AgvDirection.Forward);
// 상세 경로 정보가 있으면 해당 방향 사용, 없으면 Forward
AgvDirection arrowDir = AgvDirection.Forward;
if (path.DetailedPath != null && i < path.DetailedPath.Count)
{
arrowDir = path.DetailedPath[i].MotorDirection;
}
DrawDirectionArrow(g, midPoint, angle, arrowDir);
}
}
@@ -535,57 +543,85 @@ namespace AGVNavigationCore.Controls
/// <summary>
/// 경로에 포함된 교차로(3개 이상의 노드가 연결된 노드)를 파란색으로 강조 표시
/// </summary>
/// <summary>
/// 경로에 포함된 특정 노드(Gateway 등)를 강조 표시
/// HighlightNodeId가 설정된 경우 해당 노드만 표시하고, 없으면 기존대로 교차로 표시(또는 표시 안함)
/// 사용자가 "교차로 대신 게이트웨이만 강조"를 원하므로 우선순위 적용
/// </summary>
private void HighlightJunctionsInPath(Graphics g, AGVPathResult path)
{
if (path?.Path == null || _nodes == null || _nodes.Count == 0)
return;
const int JUNCTION_CONNECTIONS = 3; // 교차로 판정 기준: 3개 이상의 연결
// 1. HighlightNodeId가 설정되어 있다면 해당 노드만 강조
if (!string.IsNullOrEmpty(HighlightNodeId))
{
var targetNode = path.Path.FirstOrDefault(n => n.Id == HighlightNodeId);
if (targetNode != null)
{
DrawJunctionHighlight(g, targetNode, true); // true = Gateway 강조 색상 사용
}
// HighlightNodeId가 설정된 경우 다른 교차로는 표시하지 않음 (사용자 요청)
return;
}
// 2. 설정이 없다면 기존 로직 (교차로 표시) 유지 여부 결정
// 사용자가 "게이트웨이만 강조해줘"라고 했으므로, 혼란을 피하기 위해
// HighlightNodeId가 없을 때는 아무것도 표시하지 않거나, 필요한 경우 복구.
// 현재는 사용자 요청에 따라 Gateway 지정이 안된 경우(일반 경로)에는 교차로 강조를 끄는 것이 맞아 보임.
// 하지만 일반 주행시에도 교차로 정보가 필요할 수 있으니 일단 둡니다.
// 단, Gateway 로직을 타는 경우(HighlightNodeId가 Set됨)에는 위에서 return 되므로 OK.
/*
const int JUNCTION_CONNECTIONS = 3;
foreach (var node in path.Path)
{
if (node == null) continue;
// 교차로 판정: 3개 이상의 노드가 연결된 경우
if (node.ConnectedMapNodes != null && node.ConnectedMapNodes.Count >= JUNCTION_CONNECTIONS)
{
DrawJunctionHighlight(g, node);
DrawJunctionHighlight(g, node, false);
}
}
*/
}
/// <summary>
/// 교차로 노드를 파란색 반투명 배경으로 강조 표시
/// 노드 강조 표시
/// </summary>
private void DrawJunctionHighlight(Graphics g, MapNode junctionNode)
private void DrawJunctionHighlight(Graphics g, MapNode junctionNode, bool isGateway)
{
if (junctionNode == null) return;
const int JUNCTION_HIGHLIGHT_RADIUS = 25; // 강조 표시 반경
int radius = isGateway ? 35 : 25; // 게이트웨이는 좀 더 크게
// 파란색 반투명 브러시로 배경 원 그리기
using (var highlightBrush = new SolidBrush(Color.FromArgb(80, 70, 130, 200))) // 파란색 (70, 130, 200) 알파 80
using (var highlightPen = new Pen(Color.FromArgb(150, 100, 150, 220), 2)) // 파란 테두리
// 색상 결정: Gateway=진한 주황/골드, 일반 교차로=기존 파랑
Color fillColor = isGateway ? Color.FromArgb(100, 255, 140, 0) : Color.FromArgb(80, 70, 130, 200);
Color penColor = isGateway ? Color.OrangeRed : Color.FromArgb(150, 100, 150, 220);
using (var highlightBrush = new SolidBrush(fillColor))
using (var highlightPen = new Pen(penColor, 3))
{
g.FillEllipse(
highlightBrush,
junctionNode.Position.X - JUNCTION_HIGHLIGHT_RADIUS,
junctionNode.Position.Y - JUNCTION_HIGHLIGHT_RADIUS,
JUNCTION_HIGHLIGHT_RADIUS * 2,
JUNCTION_HIGHLIGHT_RADIUS * 2
junctionNode.Position.X - radius,
junctionNode.Position.Y - radius,
radius * 2,
radius * 2
);
// 테두리 점선 효과 (Gateway 인 경우)
if (isGateway) highlightPen.DashStyle = DashStyle.Dot;
g.DrawEllipse(
highlightPen,
junctionNode.Position.X - JUNCTION_HIGHLIGHT_RADIUS,
junctionNode.Position.Y - JUNCTION_HIGHLIGHT_RADIUS,
JUNCTION_HIGHLIGHT_RADIUS * 2,
JUNCTION_HIGHLIGHT_RADIUS * 2
junctionNode.Position.X - radius,
junctionNode.Position.Y - radius,
radius * 2,
radius * 2
);
}
// 교차로 라벨 추가
//DrawJunctionLabel(g, junctionNode);
}
/// <summary>

View File

@@ -211,6 +211,12 @@ namespace AGVNavigationCore.Controls
}
}
/// <summary>
/// 강조해서 표시할 특정 노드 ID (예: Gateway)
/// 이 값이 설정되면 해당 노드만 강조 표시됩니다.
/// </summary>
public string HighlightNodeId { get; set; }
public void RemoveItem(NodeBase item)
{
if (item is MapImage img) RemoveImage(img);

View File

@@ -116,7 +116,11 @@ namespace AGVNavigationCore.PathFinding.Planning
// 기본값으로 경로 탐색 (이전 위치 = 현재 위치, 방향 = 전진)
return FindPath(startNode, targetNode, startNode, AgvDirection.Forward, AgvDirection.Forward, false);
}
public AGVPathResult FindPathAStar(MapNode startNode, MapNode targetNode)
{
// 기본값으로 경로 탐색 (이전 위치 = 현재 위치, 방향 = 전진)
return _basicPathfinder.FindPathAStar(startNode.Id, targetNode.Id);
}
public AGVPathResult FindPath(MapNode startNode, MapNode targetNode,
MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection, bool crossignore = false)
{
@@ -434,6 +438,48 @@ namespace AGVNavigationCore.PathFinding.Planning
/// <summary>
/// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요
/// </summary>
/// <summary>
/// 단순 경로 찾기 (복잡한 제약조건/방향전환 로직 없이 A* 결과만 반환)
/// </summary>
public AGVPathResult FindBasicPath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection prevDirection)
{
// 1. 입력 검증
if (startNode == null || targetNode == null)
return AGVPathResult.CreateFailure("노드 정보 오류", 0, 0);
// 2. A* 경로 탐색
var pathResult = _basicPathfinder.FindPathAStar(startNode.Id, targetNode.Id);
pathResult.PrevNode = prevNode;
pathResult.PrevDirection = prevDirection;
if (!pathResult.Success)
return AGVPathResult.CreateFailure(pathResult.ErrorMessage ?? "경로 없음", 0, 0);
// 3. 상세 데이터 생성 (단순화: 방향 전환 없이 현재 방향 유지)
if (pathResult.Path != null && pathResult.Path.Count > 0)
{
var detailedPath = new List<NodeMotorInfo>();
for (int i = 0; i < pathResult.Path.Count; i++)
{
var node = pathResult.Path[i];
string nextNodeId = (i + 1 < pathResult.Path.Count) ? pathResult.Path[i + 1].Id : null;
// 단순화: 입력된 현재 방향을 그대로 사용
var nodeInfo = new NodeMotorInfo(i + 1, node.Id, node.RfidId, prevDirection, nextNodeId, MagnetDirection.Straight);
// 속도 설정
var mapNode = _mapNodes.FirstOrDefault(n => n.Id == node.Id);
if (mapNode != null) nodeInfo.Speed = mapNode.SpeedLimit;
detailedPath.Add(nodeInfo);
}
pathResult.DetailedPath = detailedPath;
}
return pathResult;
}
/// <summary>
/// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요
/// </summary>

View File

@@ -45,6 +45,8 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Forms\ComboBoxItem.cs" />
<Compile Include="Forms\DirectionItem.cs" />
<Compile Include="Forms\PathTestLogItem.cs" />
<Compile Include="Forms\ProgressLogForm.cs">
<SubType>Form</SubType>

View File

@@ -0,0 +1,23 @@
namespace AGVSimulator.Forms
{
/// <summary>
/// 제네릭 콤보박스 아이템 클래스
/// </summary>
/// <typeparam name="T">값의 타입</typeparam>
public class ComboBoxItem<T>
{
public T Value { get; }
public string DisplayText { get; }
public ComboBoxItem(T value, string displayText)
{
Value = value;
DisplayText = displayText;
}
public override string ToString()
{
return DisplayText;
}
}
}

View File

@@ -0,0 +1,24 @@
using AGVNavigationCore.Models;
namespace AGVSimulator.Forms
{
/// <summary>
/// 방향 콤보박스용 아이템 클래스
/// </summary>
public class DirectionItem
{
public AgvDirection Direction { get; }
public string DisplayText { get; }
public DirectionItem(AgvDirection direction, string displayText)
{
Direction = direction;
DisplayText = displayText;
}
public override string ToString()
{
return DisplayText;
}
}
}

View File

@@ -86,13 +86,15 @@ namespace AGVSimulator.Forms
this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
this.prb1 = new System.Windows.Forms.ToolStripProgressBar();
this._controlPanel = new System.Windows.Forms.Panel();
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.propertyNode = new System.Windows.Forms.PropertyGrid();
this._statusGroup = new System.Windows.Forms.GroupBox();
this._pathLengthLabel = new System.Windows.Forms.Label();
this._agvCountLabel = new System.Windows.Forms.Label();
this._simulationStatusLabel = new System.Windows.Forms.Label();
this._pathGroup = new System.Windows.Forms.GroupBox();
this._clearPathButton = new System.Windows.Forms.Button();
this._calculatePathButton = new System.Windows.Forms.Button();
this.btPath1 = new System.Windows.Forms.Button();
this._targetCalcButton = new System.Windows.Forms.Button();
this._avoidRotationCheckBox = new System.Windows.Forms.CheckBox();
this._targetNodeCombo = new System.Windows.Forms.ComboBox();
@@ -118,18 +120,17 @@ namespace AGVSimulator.Forms
this._liftDirectionLabel = new System.Windows.Forms.Label();
this._motorDirectionLabel = new System.Windows.Forms.Label();
this.timer1 = new System.Windows.Forms.Timer(this.components);
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.propertyNode = new System.Windows.Forms.PropertyGrid();
this.btPath2 = new System.Windows.Forms.Button();
this._menuStrip.SuspendLayout();
this._toolStrip.SuspendLayout();
this._statusStrip.SuspendLayout();
this._controlPanel.SuspendLayout();
this.groupBox1.SuspendLayout();
this._statusGroup.SuspendLayout();
this._pathGroup.SuspendLayout();
this._agvControlGroup.SuspendLayout();
this._canvasPanel.SuspendLayout();
this._agvInfoPanel.SuspendLayout();
this.groupBox1.SuspendLayout();
this.SuspendLayout();
//
// _menuStrip
@@ -468,6 +469,25 @@ namespace AGVSimulator.Forms
this._controlPanel.Size = new System.Drawing.Size(233, 640);
this._controlPanel.TabIndex = 3;
//
// groupBox1
//
this.groupBox1.Controls.Add(this.propertyNode);
this.groupBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.groupBox1.Location = new System.Drawing.Point(0, 546);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(233, 94);
this.groupBox1.TabIndex = 4;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "노드 정보";
//
// propertyNode
//
this.propertyNode.Dock = System.Windows.Forms.DockStyle.Fill;
this.propertyNode.Location = new System.Drawing.Point(3, 17);
this.propertyNode.Name = "propertyNode";
this.propertyNode.Size = new System.Drawing.Size(227, 74);
this.propertyNode.TabIndex = 0;
//
// _statusGroup
//
this._statusGroup.Controls.Add(this._pathLengthLabel);
@@ -510,8 +530,9 @@ namespace AGVSimulator.Forms
//
// _pathGroup
//
this._pathGroup.Controls.Add(this.btPath2);
this._pathGroup.Controls.Add(this._clearPathButton);
this._pathGroup.Controls.Add(this._calculatePathButton);
this._pathGroup.Controls.Add(this.btPath1);
this._pathGroup.Controls.Add(this._targetCalcButton);
this._pathGroup.Controls.Add(this._avoidRotationCheckBox);
this._pathGroup.Controls.Add(this._targetNodeCombo);
@@ -528,23 +549,23 @@ namespace AGVSimulator.Forms
//
// _clearPathButton
//
this._clearPathButton.Location = new System.Drawing.Point(150, 177);
this._clearPathButton.Location = new System.Drawing.Point(121, 177);
this._clearPathButton.Name = "_clearPathButton";
this._clearPathButton.Size = new System.Drawing.Size(70, 25);
this._clearPathButton.Size = new System.Drawing.Size(111, 25);
this._clearPathButton.TabIndex = 6;
this._clearPathButton.Text = "경로 지우기";
this._clearPathButton.UseVisualStyleBackColor = true;
this._clearPathButton.Click += new System.EventHandler(this.OnClearPath_Click);
//
// _calculatePathButton
// btPath1
//
this._calculatePathButton.Location = new System.Drawing.Point(10, 177);
this._calculatePathButton.Name = "_calculatePathButton";
this._calculatePathButton.Size = new System.Drawing.Size(65, 25);
this._calculatePathButton.TabIndex = 4;
this._calculatePathButton.Text = "경로 계산";
this._calculatePathButton.UseVisualStyleBackColor = true;
this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
this.btPath1.Location = new System.Drawing.Point(12, 174);
this.btPath1.Name = "btPath1";
this.btPath1.Size = new System.Drawing.Size(106, 25);
this.btPath1.TabIndex = 4;
this.btPath1.Text = "경로 계산";
this.btPath1.UseVisualStyleBackColor = true;
this.btPath1.Click += new System.EventHandler(this.OnCalculatePath_Click);
//
// _targetCalcButton
//
@@ -793,24 +814,15 @@ namespace AGVSimulator.Forms
this.timer1.Interval = 500;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
//
// groupBox1
// btPath2
//
this.groupBox1.Controls.Add(this.propertyNode);
this.groupBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.groupBox1.Location = new System.Drawing.Point(0, 546);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(233, 94);
this.groupBox1.TabIndex = 4;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "노드 정보";
//
// propertyNode
//
this.propertyNode.Dock = System.Windows.Forms.DockStyle.Fill;
this.propertyNode.Location = new System.Drawing.Point(3, 17);
this.propertyNode.Name = "propertyNode";
this.propertyNode.Size = new System.Drawing.Size(227, 74);
this.propertyNode.TabIndex = 0;
this.btPath2.Location = new System.Drawing.Point(12, 201);
this.btPath2.Name = "btPath2";
this.btPath2.Size = new System.Drawing.Size(106, 25);
this.btPath2.TabIndex = 10;
this.btPath2.Text = "경로 계산2";
this.btPath2.UseVisualStyleBackColor = true;
this.btPath2.Click += new System.EventHandler(this.btPath2_Click);
//
// SimulatorForm
//
@@ -835,6 +847,7 @@ namespace AGVSimulator.Forms
this._statusStrip.ResumeLayout(false);
this._statusStrip.PerformLayout();
this._controlPanel.ResumeLayout(false);
this.groupBox1.ResumeLayout(false);
this._statusGroup.ResumeLayout(false);
this._statusGroup.PerformLayout();
this._pathGroup.ResumeLayout(false);
@@ -844,7 +857,6 @@ namespace AGVSimulator.Forms
this._canvasPanel.ResumeLayout(false);
this._agvInfoPanel.ResumeLayout(false);
this._agvInfoPanel.PerformLayout();
this.groupBox1.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
@@ -889,7 +901,7 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.ComboBox _startNodeCombo;
private System.Windows.Forms.Label targetNodeLabel;
private System.Windows.Forms.ComboBox _targetNodeCombo;
private System.Windows.Forms.Button _calculatePathButton;
private System.Windows.Forms.Button btPath1;
private System.Windows.Forms.Button _clearPathButton;
private System.Windows.Forms.Button _targetCalcButton;
private System.Windows.Forms.CheckBox _avoidRotationCheckBox;
@@ -925,5 +937,6 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.ToolStripMenuItem ToolStripMenuItem;
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.PropertyGrid propertyNode;
private System.Windows.Forms.Button btPath2;
}
}

View File

@@ -363,100 +363,7 @@ namespace AGVSimulator.Forms
UpdateUI();
}
private void OnCalculatePath_Click(object sender, EventArgs e)
{
var rlt = CalcPath();
if (rlt.result == false) MessageBox.Show(rlt.message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
(bool result, string message) CalcPath()
{
// 시작 RFID가 없으면 AGV 현재 위치로 설정
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요")
{
SetStartNodeFromAGVPosition();
}
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
{
return (false, "시작 RFID와 목표 RFID를 선택해주세요.");
}
var startItem = _startNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
var targetItem = _targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
var startNode = startItem?.Value;
var targetNode = targetItem?.Value;
if (startNode == null || targetNode == null)
{
return (false, "선택한 노드 정보가 올바르지 않습니다.");
}
if (_advancedPathfinder == null)
{
_advancedPathfinder = new AGVPathfinder(_simulatorCanvas.Nodes);
}
// 현재 AGV 방향 가져오기
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV == null)
{
return (false, "Virtual AGV 가 없습니다");
}
var currentDirection = selectedAGV.CurrentDirection;
// AGV의 이전 위치에서 가장 가까운 노드 찾기
var prevNode = selectedAGV.PrevNode;
var prevDir = selectedAGV.PrevDirection;
// 고급 경로 계획 사용 (노드 객체 직접 전달)
var advancedResult = _advancedPathfinder.FindPath(startNode, targetNode, prevNode, prevDir, currentDirection);
_simulatorCanvas.FitToNodes();
if (advancedResult.Success)
{
// 도킹 검증이 없는 경우 추가 검증 수행
if (advancedResult.DockingValidation == null || !advancedResult.DockingValidation.IsValidationRequired)
{
advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _simulatorCanvas.Nodes);
}
//마지막대상이 버퍼라면 시퀀스처리를 해야한다
if(targetNode.StationType == StationType.Buffer)
{
var lastDetailPath = advancedResult.DetailedPath.Last();
if(lastDetailPath.NodeId == targetNode.Id) //마지막노드 재확인
{
//버퍼에 도킹할때에는 마지막 노드에서 멈추고 시퀀스를 적용해야한다
advancedResult.DetailedPath = advancedResult.DetailedPath.Take(advancedResult.DetailedPath.Count - 1).ToList();
Console.WriteLine("최종위치가 버퍼이므로 마지막 RFID에서 멈추도록 합니다");
}
}
_simulatorCanvas.CurrentPath = advancedResult;
_pathLengthLabel.Text = $"경로 길이: {advancedResult.TotalDistance:F1}";
_statusLabel.Text = $"경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)";
// 🔥 VirtualAGV에도 경로 설정 (Predict()가 동작하려면 필요)
selectedAGV.SetPath(advancedResult);
// 도킹 검증 결과 확인 및 UI 표시
CheckAndDisplayDockingValidation(advancedResult);
// 고급 경로 디버깅 정보 표시
UpdateAdvancedPathDebugInfo(advancedResult);
return (true, string.Empty);
}
else
{
// 경로 실패시 디버깅 정보 초기화
_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
return (false, $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}");
}
}
private void OnClearPath_Click(object sender, EventArgs e)
{
@@ -1102,7 +1009,7 @@ namespace AGVSimulator.Forms
_stopSimulationButton.Enabled = _simulationState.IsRunning;
_removeAgvButton.Enabled = _agvListCombo.SelectedItem != null;
_calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
btPath1.Enabled = _startNodeCombo.SelectedItem != null &&
_targetNodeCombo.SelectedItem != null;
// RFID 위치 설정 관련
@@ -2122,7 +2029,8 @@ namespace AGVSimulator.Forms
_portCombo = new ComboBox();
_portCombo.Width = 100;
_portCombo.DropDownStyle = ComboBoxStyle.DropDownList;
_portCombo.DropDown += (s, e) => {
_portCombo.DropDown += (s, e) =>
{
_portCombo.Items.Clear();
_portCombo.Items.AddRange(SerialPort.GetPortNames());
};
@@ -2313,7 +2221,8 @@ namespace AGVSimulator.Forms
// AGV 제어 (첫 번째 AGV 대상)
var agv = _agvList.FirstOrDefault();
this.Invoke(new Action(() => {
this.Invoke(new Action(() =>
{
switch (cmd)
{
case "CRN": // 기동명령
@@ -2505,48 +2414,521 @@ namespace AGVSimulator.Forms
return hex.PadLeft(2, '0');
}
(bool result, string message) CalcPath()
{
// 시작 RFID가 없으면 AGV 현재 위치로 설정
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요")
{
SetStartNodeFromAGVPosition();
}
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
{
return (false, "시작 RFID와 목표 RFID를 선택해주세요.");
}
var startItem = _startNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
var targetItem = _targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
var startNode = startItem?.Value;
var targetNode = targetItem?.Value;
if (startNode == null || targetNode == null)
{
return (false, "선택한 노드 정보가 올바르지 않습니다.");
}
if (_advancedPathfinder == null)
{
_advancedPathfinder = new AGVPathfinder(_simulatorCanvas.Nodes);
}
// 현재 AGV 방향 가져오기
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV == null)
{
return (false, "Virtual AGV 가 없습니다");
}
var currentDirection = selectedAGV.CurrentDirection;
// AGV의 이전 위치에서 가장 가까운 노드 찾기
var prevNode = selectedAGV.PrevNode;
var prevDir = selectedAGV.PrevDirection;
// 고급 경로 계획 사용 (노드 객체 직접 전달)
var advancedResult = _advancedPathfinder.FindPath(startNode, targetNode, prevNode, prevDir, currentDirection);
_simulatorCanvas.FitToNodes();
if (advancedResult.Success)
{
// 도킹 검증이 없는 경우 추가 검증 수행
if (advancedResult.DockingValidation == null || !advancedResult.DockingValidation.IsValidationRequired)
{
advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _simulatorCanvas.Nodes);
}
//마지막대상이 버퍼라면 시퀀스처리를 해야한다
if (targetNode.StationType == StationType.Buffer)
{
var lastDetailPath = advancedResult.DetailedPath.Last();
if (lastDetailPath.NodeId == targetNode.Id) //마지막노드 재확인
{
//버퍼에 도킹할때에는 마지막 노드에서 멈추고 시퀀스를 적용해야한다
advancedResult.DetailedPath = advancedResult.DetailedPath.Take(advancedResult.DetailedPath.Count - 1).ToList();
Console.WriteLine("최종위치가 버퍼이므로 마지막 RFID에서 멈추도록 합니다");
}
}
_simulatorCanvas.CurrentPath = advancedResult;
_pathLengthLabel.Text = $"경로 길이: {advancedResult.TotalDistance:F1}";
_statusLabel.Text = $"경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)";
// 🔥 VirtualAGV에도 경로 설정 (Predict()가 동작하려면 필요)
selectedAGV.SetPath(advancedResult);
// 도킹 검증 결과 확인 및 UI 표시
CheckAndDisplayDockingValidation(advancedResult);
// 고급 경로 디버깅 정보 표시
UpdateAdvancedPathDebugInfo(advancedResult);
return (true, string.Empty);
}
else
{
// 경로 실패시 디버깅 정보 초기화
_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
return (false, $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}");
}
}
#endregion
private void btPath2_Click(object sender, EventArgs e)
{
// 경로계산2 (Gateway Logic)
var rlt = CalcPathGateway();
if (rlt.result == false) MessageBox.Show(rlt.message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
/// <summary>
/// 방향 콤보박스용 아이템 클래스
/// 길목(Gateway) 기반 경로 계산
/// </summary>
public class DirectionItem
private (bool result, string message) CalcPathGateway()
{
public AgvDirection Direction { get; }
public string DisplayText { get; }
// 1. 기본 정보 획득
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요") SetStartNodeFromAGVPosition();
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null) return (false, "시작/목표 노드 선택 필요");
public DirectionItem(AgvDirection direction, string displayText)
var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
if (startNode == null || targetNode == null) return (false, "노드 정보 오류");
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))
{
Direction = direction;
DisplayText = displayText;
return CalcPathBufferToBuffer(startNode, targetNode, prevNode, prevDir, currentAgvDir, selectedAGV);
}
public override string ToString()
// 3. 목적지별 Gateway 및 진입 조건 확인
var gatewayNode = GetGatewayNode(targetNode);
if (gatewayNode == null)
{
return DisplayText;
//게이트웨이가 없는 경우라면 목적지가 도킹포인트가 아니므로, a*알골리즘으로 진행 방향만 맟춰서 이동한다
var simplePath = _advancedPathfinder.FindBasicPath(startNode, targetNode, prevNode, prevDir);
if (simplePath.Success)
{
_simulatorCanvas.HighlightNodeId = null; // 일반 경로는 강조 없음
ApplyResultToSimulator(simplePath, selectedAGV);
UpdateAdvancedPathDebugInfo(simplePath);
return (true, "일반 이동 경로(Gateway 없음)");
}
return (false, $"일반 이동 경로 실패: {simplePath.ErrorMessage}");
}
// Gateway Node 찾음
_simulatorCanvas.HighlightNodeId = gatewayNode.Id; // Gateway 강조 설정
// 4. Start -> Gateway 경로 계산 (A*)
var pathToGateway = _advancedPathfinder.FindPath(startNode, gatewayNode, prevNode, prevDir, currentAgvDir);
if (!pathToGateway.Success) return (false, $"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.ErrorMessage}");
// 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함)
var arrivalOrientation = pathToGateway.DetailedPath.Last().MotorDirection;
AGVPathResult finalPath = pathToGateway;
var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, pathToGateway.Path.Last(), arrivalOrientation);
if (!gatewayPathResult.Success) return (false, $"{gatewayPathResult.ErrorMessage}");
finalPath = CombinePaths(finalPath, gatewayPathResult);
// 8. 적용
ApplyResultToSimulator(finalPath, selectedAGV);
UpdateAdvancedPathDebugInfo(finalPath);
return (true, "성공");
}
/// <summary>
/// 노드를 찾기위한 함수
/// </summary>
/// <param name="rfid"></param>
/// <returns></returns>
private MapNode FindNode(ushort rfid)
{
return _simulatorCanvas.Nodes.FirstOrDefault(n => n.RfidId == rfid);
}
/// <summary>
/// 노드를 찾기위한 함수
/// </summary>
/// <param name="nodeid"></param>
/// <returns></returns>
private MapNode FindNode(string nodeid)
{
return _simulatorCanvas.Nodes.FirstOrDefault(n => n.Id == nodeid);
}
/// <summary>
/// Gateway 도착 후, Target까지의 경로(회차 및 최종진입 포함)를 계산합니다.
/// </summary>
/// <param name="gatewayNode">게이트웨이 노드값</param>
/// <param name="targetNode">최종 목표값</param>
/// <param name="GTprevNode">게이트웨이 진입 전 노드</param>
/// <param name="GTprevDirection">게이트웨이 진입 전 모터방향</param>
/// <returns></returns>
private AGVPathResult GetPathFromGateway(MapNode gatewayNode, MapNode targetNode, MapNode GTprevNode, AgvDirection GTprevDirection)
{
AGVPathResult resultPath = null;
MapNode currentNode = gatewayNode;
MapNode currentPrev = GTprevNode; // Gateway 바로 이전 노드 (방향 계산용)
AgvDirection currentDir = GTprevDirection;
//게이트웨이 진입 한 방향을 보고. 목적지와 도킹방향이 일치하는지 결정한다.
var deltaX = gatewayNode.Position.X - GTprevNode.Position.X;
var isMonitorLeft = false;
bool requiredDir = false;
switch (targetNode.StationType)
{
case StationType.Buffer:
//버퍼는 게이트웨이가 6번이고 좌/우로 판단한다
if (deltaX > 0) //게이트웨이가 더 오른쪽에있으니 좌->우 이동을 한경우이다. 이떄 모터방향이 후진이라면 모니터는 왼쪽이고, 반대는 오른쪽이다
{
isMonitorLeft = GTprevDirection == AgvDirection.Backward;
}
else
{
isMonitorLeft = GTprevDirection == AgvDirection.Forward;
}
//버퍼는 모니터가 왼쪽에 있으면 안된다.
List<string> turnPatterns = new List<string>();
AGVPathResult rlt1 = new AGVPathResult();
rlt1.Success = true;
//목적지까지 바로 계산한다
var pathtarget = _advancedPathfinder.FindBasicPath(gatewayNode, targetNode, GTprevNode, AgvDirection.Backward);
if (isMonitorLeft)
{
//턴을 하는
turnPatterns = GetTurnaroundPattern(gatewayNode, targetNode);
if (turnPatterns == null || turnPatterns.Any() == false) return new AGVPathResult { Success = false, ErrorMessage = $"회차 패턴 없음: Dir {currentDir}" };
foreach (var item in turnPatterns)
{
var rfidvalue = ushort.Parse(item.Substring(0, 4));
var node = _simulatorCanvas.Nodes.FirstOrDefault(t => t.RfidId == rfidvalue);
//경로노드추가
rlt1.Path.Add(node);
//Detail 정보도 추가한다.
AgvDirection nodedir = item.Substring(4, 1) == "F" ? AgvDirection.Forward : AgvDirection.Backward;
MagnetDirection magnet = MagnetDirection.Straight;
var magchar = item.Substring(5, 1);
if (magchar == "L") magnet = MagnetDirection.Left;
else if (magchar == "R") magnet = MagnetDirection.Right;
rlt1.DetailedPath.Add(new NodeMotorInfo(rlt1.DetailedPath.Count, node.Id, node.RfidId, nodedir, null, magnet)
{
Speed = SpeedLevel.L,
});
}
//시작위치가 겹치므로 제거해줘야하낟.
if (pathtarget.DetailedPath.First().NodeId != rlt1.DetailedPath.Last().NodeId ||
pathtarget.DetailedPath.First().MotorDirection != rlt1.DetailedPath.Last().MotorDirection )
{
new AGVPathResult { Success = false, ErrorMessage = $"게이트웨이 턴 마지막 주소와, 이 후 주소의 시작 노드ID가 일치하지 않습니다" };
}
pathtarget.Path.RemoveAt(0);
pathtarget.DetailedPath.RemoveAt(0);
}
var lastpath = CombinePaths(rlt1, pathtarget);
return lastpath;
default:
throw new Exception("ASdf");
}
return null;
}
/// <summary>
/// 지정한 노드의 게이트웨이를 반환합니다.
/// 도킹노드가 아닐경우 NULL이 반환됩니다.
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
private MapNode GetGatewayNode(MapNode node)
{
var rfid = 0;
if (node.RfidId == 1) rfid = 10;
if (node.RfidId == 15) rfid = 9;
if (node.RfidId == 11) rfid = 6;
if (node.RfidId == 8) rfid = 13;
if (node.RfidId == 19) rfid = 13;
if (node.StationType == StationType.Buffer) rfid = 6;
if (rfid == 0) return null;
return this._simulatorCanvas.Nodes.FirstOrDefault(t => t.RfidId == rfid);
}
private AgvDirection GetRequiredGatewayDirection(string gatewayLogId)
{
switch (gatewayLogId)
{
case "0010": return AgvDirection.Backward;
case "0009": return AgvDirection.Forward;
case "0006": return AgvDirection.Backward;
case "0013": return AgvDirection.Backward;
default: return AgvDirection.Forward;
}
}
/// <summary>
/// 제네릭 콤보박스 아이템 클래스
/// 대상노드와 게이트웨이노드를 가지고 턴 노드값을 반환합니다
/// (이 값은 하드코딩 되어있음)
/// </summary>
/// <typeparam name="T">값의 타입</typeparam>
public class ComboBoxItem<T>
/// <param name="gatewayNode"></param>
/// <param name="targetNode"></param>
/// <returns></returns>
private List<string> GetTurnaroundPattern(MapNode gatewayNode, MapNode targetNode)
{
public T Value { get; }
public string DisplayText { get; }
public ComboBoxItem(T value, string displayText)
switch (gatewayNode.RfidId)
{
Value = value;
DisplayText = displayText;
case 6:
//버퍼와 11을 다르게하낟.
if (targetNode.StationType == StationType.Buffer)
{
return new List<string> { "0006BL", "0007FS", "0013BL", "0006BL" };
}
else
{
return new List<string> { "0006BL", "0007FS", "0013BL", "0006BS" };
}
case 9: return new List<string> { "0009FL", "0010BS", "0007FL", "0009FS" };
case 10: return new List<string> { "0010BR", "0009FR", "0007BS", "0010BS" };
case 13: return new List<string> { "0013BL", "0006FL", "0007BS", "0013BS" };
default: return null;
}
}
public override string ToString()
private (bool result, string message) CalcPathBufferToBuffer(MapNode start, MapNode target, MapNode prev, AgvDirection prevDir, AgvDirection currentDir, VirtualAGV agv)
{
return DisplayText;
// 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>
/// <param name="p1"></param>
/// <param name="p2"></param>
/// <returns></returns>
private AGVPathResult CombinePaths(AGVPathResult p1, AGVPathResult p2)
{
var res = new AGVPathResult();
res.Success = true;
foreach(var item in p1.Path)
{
res.Path.Add(item);
}
foreach(var item in p2.Path)
{
res.Path.Add(item);
}
foreach (var item in p1.DetailedPath)
{
var maxseq = res.DetailedPath.Count == 0 ? 0 : res.DetailedPath.Max(t => t.seq);
item.seq = maxseq + 1;
res.DetailedPath.Add(item);
}
foreach (var item in p2.DetailedPath)
{
var maxseq = res.DetailedPath.Count == 0 ? 0 : res.DetailedPath.Max(t => t.seq);
item.seq = maxseq + 1;
res.DetailedPath.Add(item);
}
return res;
}
private void ApplyResultToSimulator(AGVPathResult result, VirtualAGV agv)
{
_simulatorCanvas.CurrentPath = result;
_pathLengthLabel.Text = $"Gateway경로: {result.TotalDistance:F1}";
agv.SetPath(result);
//_simulatorCanvas.CheckAndDisplayDockingValidation(result); // Optional/Needs access
_simulatorCanvas.FitToNodes();
}
private void OnCalculatePath_Click(object sender, EventArgs e)
{
var rlt = CalcPath();
if (rlt.result == false) MessageBox.Show(rlt.message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}

View File

@@ -6,6 +6,7 @@
0011 06 역방향 : 0006(BS) 0011(BS), 정방향(회전필요) : [ 0006(BL) 0007(FS) 00013(BL) 0006(BS) ] - 동일
버퍼 06 역방향 : 0006(BL) 0005(BS) ~ 나머지 0031까지는 BS상태로 계속 이동, 정방향(회전필요) : [ 0006(BL) 0007(FS) 0013(BL) 0006(BL) ] - 동일
0008 13 역방향 : 0013(BS) 0019(BS) 0008(BS), 정방향(회전필요) : [ 0013(BL) 0006(FL) 0007(BS) 0013(BS) ] - 동일
0019 13 역방향 : 0013(BS) 0019(BS), 정방향(회전필요) : [ 0013(BL) 0006(FL) 0007(BS) 0013(BS) ] - 동일
나머지 모든 경유는 각 경유지로 해당 방향으로 그대로 길을 찾는다
목적지를 보고 목적지의 경유지까지 경로로를 계산한다 (A*)
@@ -15,5 +16,15 @@
도킹방향이 맞고 마크센서가 OFF되어있다면 반대방향으로 한번 이동하고, 다시 역방향 이동하면서 MARK STOP 처리한다.
도킹방향이 맞지 않는 경우 일반 노드 검색처럼 경유지 처리한다
버퍼에서 버퍼로 이동할때에만 추가 코드를 적용한다
버퍼에서 버퍼로 이동하는 경우에는 별도 로직으로 처리한다
모니터가 좌측에 있는 경우라면 도킹방향이 맞지 않으므로 0006까지 이동을 완료 한 후 기존 로직을 적용
모니터가 우측에 있는 방향이라면 도킹방향이 맞는 경우이다
목표가 현재 기준 좌측이라면?
BACK으로 이동하면 좌측이동이므로 목적지 노드까지 A*로 계산하여 이동 후 MARK STOP 한다
목표가 현재 기준 우측이라면?
위치 정밀도를 높이기 위해서 목표 지점을 벗어난 후 다시 좌측으로 마크스탑한다
A* -> Back -> MARKSTOP
* 인식된 위치가 전체 경로상에 존재하지 않는 노드라면 모든 경로계산을 다시 계산 한다
* 경로상 노드에 포함되어있다 해도 진입방향등을 고려하여 일치하지 않으면 경로 이탈로 간주하고 다시 계산 한다