enhance: Display RFID values in path UI and improve AGV lift direction visualization

- Replace NodeID with RFID values in path display for better field mapping
- Add ComboBoxItem<T> class for {rfid} - [{node}] format in combo boxes
- Implement GetRfidByNodeId helper method for NodeID to RFID conversion
- Enhanced UpdatePathDebugInfo to show both RFID and NodeID information
- Improved path visualization with RFID-based route display
- Users can now easily match displayed paths with physical RFID tags

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-09-12 15:36:01 +09:00
parent de0e39e030
commit 1add9ed59a
9 changed files with 1626 additions and 98 deletions

View File

@@ -8,6 +8,7 @@ using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.Controls;
using AGVNavigationCore.PathFinding;
using AGVSimulator.Models;
using Newtonsoft.Json;
@@ -59,21 +60,24 @@ namespace AGVSimulator.Forms
{
// 설정 로드
_config = SimulatorConfig.Load();
// 데이터 초기화
_mapNodes = new List<MapNode>();
_agvList = new List<VirtualAGV>();
_simulationState = new SimulationState();
_currentMapFilePath = string.Empty;
// 시뮬레이터 캔버스 생성 (중앙 패널에만)
CreateSimulatorCanvas();
// 타이머 초기화
_simulationTimer = new Timer();
_simulationTimer.Interval = 100; // 100ms 간격
_simulationTimer.Tick += OnSimulationTimer_Tick;
// 방향 콤보박스 초기화
InitializeDirectionCombo();
// 초기 상태 설정
UpdateUI();
}
@@ -85,7 +89,7 @@ namespace AGVSimulator.Forms
_simulatorCanvas = new UnifiedAGVCanvas();
_simulatorCanvas.Dock = DockStyle.Fill;
_simulatorCanvas.Mode = UnifiedAGVCanvas.CanvasMode.ViewOnly;
_canvasPanel.Controls.Add(_simulatorCanvas);
}
@@ -95,6 +99,24 @@ namespace AGVSimulator.Forms
_canvasPanel.BringToFront();
}
/// <summary>
/// 모터 구동방향 콤보박스 초기화
/// </summary>
private void InitializeDirectionCombo()
{
_directionCombo.Items.Clear();
// AgvDirection enum 값들을 콤보박스에 추가
_directionCombo.Items.Add(new DirectionItem(AgvDirection.Forward, "전진 (모니터쪽)"));
_directionCombo.Items.Add(new DirectionItem(AgvDirection.Backward, "후진 (리프트쪽)"));
_directionCombo.Items.Add(new DirectionItem(AgvDirection.Left, "좌회전"));
_directionCombo.Items.Add(new DirectionItem(AgvDirection.Right, "우회전"));
_directionCombo.Items.Add(new DirectionItem(AgvDirection.Stop, "정지"));
// 기본 선택: 전진
_directionCombo.SelectedIndex = 0;
}
#endregion
#region Event Handlers
@@ -105,7 +127,7 @@ namespace AGVSimulator.Forms
{
openDialog.Filter = "AGV Map Files (*.agvmap)|*.agvmap|모든 파일 (*.*)|*.*";
openDialog.Title = "맵 파일 열기";
if (openDialog.ShowDialog() == DialogResult.OK)
{
try
@@ -115,7 +137,7 @@ namespace AGVSimulator.Forms
}
catch (Exception ex)
{
MessageBox.Show($"맵 파일을 로드할 수 없습니다:\n{ex.Message}", "오류",
MessageBox.Show($"맵 파일을 로드할 수 없습니다:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -131,10 +153,11 @@ namespace AGVSimulator.Forms
{
if (_simulationState.IsRunning)
return;
_simulationState.IsRunning = true;
_simulationTimer.Start();
_statusLabel.Text = "시뮬레이션 실행 중";
Console.WriteLine("시뮬레이션 실행");
UpdateUI();
}
@@ -142,16 +165,17 @@ namespace AGVSimulator.Forms
{
if (!_simulationState.IsRunning)
return;
_simulationState.IsRunning = false;
_simulationTimer.Stop();
_statusLabel.Text = "시뮬레이션 정지";
Console.WriteLine("시뮬레이션 정지");
UpdateUI();
}
private void OnReset_Click(object sender, EventArgs e)
{
}
private void OnFitToMap_Click(object sender, EventArgs e)
@@ -166,7 +190,7 @@ namespace AGVSimulator.Forms
private void OnAbout_Click(object sender, EventArgs e)
{
MessageBox.Show("AGV 시뮬레이터 v1.0\n\nENIG AGV 시스템용 시뮬레이터", "정보",
MessageBox.Show("AGV 시뮬레이터 v1.0\n\nENIG AGV 시스템용 시뮬레이터", "정보",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
@@ -177,17 +201,26 @@ namespace AGVSimulator.Forms
MessageBox.Show("먼저 맵을 로드해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var agvId = $"AGV{_agvList.Count + 1:D2}";
var startPosition = _mapNodes.First().Position; // 첫 번째 노드에서 시작
var newAGV = new VirtualAGV(agvId, startPosition);
_agvList.Add(newAGV);
_simulatorCanvas.AGVList = new List<IAGV>(_agvList.Cast<IAGV>());
// 콘솔 출력
Program.WriteLine($"[SYSTEM] AGV 추가:");
Program.WriteLine($" AGV ID: {agvId}");
Program.WriteLine($" 시작 위치: ({startPosition.X}, {startPosition.Y})");
Program.WriteLine($" 총 AGV 수: {_agvList.Count}");
Program.WriteLine("");
UpdateAGVComboBox();
UpdateUI();
_statusLabel.Text = $"{agvId} 추가됨";
}
@@ -195,16 +228,23 @@ namespace AGVSimulator.Forms
{
if (_agvListCombo.SelectedItem == null)
return;
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV != null)
{
_agvList.Remove(selectedAGV);
_simulatorCanvas.AGVList = new List<IAGV>(_agvList.Cast<IAGV>());
// 콘솔 출력
Console.WriteLine($"[SYSTEM] AGV 제거:");
Console.WriteLine($" AGV ID: {selectedAGV.AgvId}");
Console.WriteLine($" 남은 AGV 수: {_agvList.Count}");
Console.WriteLine("");
UpdateAGVComboBox();
UpdateUI();
_statusLabel.Text = $"{selectedAGV.AgvId} 제거됨";
}
}
@@ -221,27 +261,41 @@ namespace AGVSimulator.Forms
MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var startItem = _startNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
var targetItem = _targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
var startNode = startItem?.Value;
var targetNode = targetItem?.Value;
var startNode = _startNodeCombo.SelectedItem as MapNode;
var targetNode = _targetNodeCombo.SelectedItem as MapNode;
if (startNode == null || targetNode == null)
{
MessageBox.Show("선택한 노드 정보가 올바르지 않습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (_pathCalculator == null)
{
_pathCalculator = new PathCalculator();
_pathCalculator.SetMapData(_mapNodes);
}
var agvResult = _pathCalculator.FindAGVPath(startNode.NodeId, targetNode.NodeId);
if (agvResult.Success)
{
_simulatorCanvas.CurrentPath = agvResult.ToPathResult();
_pathLengthLabel.Text = $"경로 길이: {agvResult.TotalDistance:F1}";
_statusLabel.Text = $"경로 계산 완료 ({agvResult.CalculationTimeMs}ms)";
// 경로 디버깅 정보 표시
UpdatePathDebugInfo(agvResult);
}
else
{
MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
// 경로 실패시 디버깅 정보 초기화
_pathDebugLabel.Text = $"경로: 실패 - {agvResult.ErrorMessage}";
MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
@@ -254,13 +308,13 @@ namespace AGVSimulator.Forms
MessageBox.Show("AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (_simulatorCanvas.CurrentPath == null || !_simulatorCanvas.CurrentPath.Success)
{
MessageBox.Show("먼저 경로를 계산해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
selectedAGV.StartPath(_simulatorCanvas.CurrentPath, _mapNodes);
_statusLabel.Text = $"{selectedAGV.AgvId} 경로 시작";
}
@@ -275,6 +329,7 @@ namespace AGVSimulator.Forms
private void OnSetPosition_Click(object sender, EventArgs e)
{
SetAGVPositionByRfid();
_simulatorCanvas.FitToNodes();
}
private void OnRfidTextBox_KeyPress(object sender, KeyPressEventArgs e)
@@ -282,6 +337,7 @@ namespace AGVSimulator.Forms
if (e.KeyChar == (char)Keys.Enter)
{
SetAGVPositionByRfid();
_simulatorCanvas.FitToNodes();
e.Handled = true;
}
}
@@ -318,17 +374,53 @@ namespace AGVSimulator.Forms
var targetNode = _mapNodes?.FirstOrDefault(n => n.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
if (targetNode == null)
{
MessageBox.Show($"RFID '{rfidId}'에 해당하는 노드를 찾을 수 없습니다.\n\n사용 가능한 RFID 목록:\n{GetAvailableRfidList()}",
MessageBox.Show($"RFID '{rfidId}'에 해당하는 노드를 찾을 수 없습니다.\n\n사용 가능한 RFID 목록:\n{GetAvailableRfidList()}",
"RFID 찾기 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// AGV 위치 설정
// 선택된 방향 확인
var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
// 콘솔 출력 (상세한 리프트 방향 계산 과정)
Program.WriteLine($"[AGV-{selectedAGV.AgvId}] 위치 설정:");
Program.WriteLine($" RFID: {rfidId} → 노드: {targetNode.NodeId}");
Program.WriteLine($" 새로운 위치: ({targetNode.Position.X}, {targetNode.Position.Y})");
Program.WriteLine($" 모터 방향: {selectedDirectionItem?.DisplayText ?? ""} ({selectedDirection})");
// SetPosition 호출 전 상태
var oldTargetPos = selectedAGV.TargetPosition;
var oldCurrentPos = selectedAGV.CurrentPosition;
Program.WriteLine($" [BEFORE] 현재 CurrentPosition: ({oldCurrentPos.X}, {oldCurrentPos.Y})");
Program.WriteLine($" [BEFORE] 이전 TargetPosition: {(oldTargetPos.HasValue ? $"({oldTargetPos.Value.X}, {oldTargetPos.Value.Y})" : "None")}");
// AGV 위치 및 방향 설정
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, targetNode.Position);
_statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId})로 설정했습니다.";
_simulatorCanvas.UpdateAGVDirection(selectedAGV.AgvId, selectedDirection);
// VirtualAGV 객체의 위치와 방향 업데이트
selectedAGV.SetPosition(targetNode.Position); // 이전 위치 기억하도록
selectedAGV.SetDirection(selectedDirection);
// SetPosition 호출 후 상태 확인 및 리프트 계산
var newTargetPos = selectedAGV.TargetPosition;
var newCurrentPos = selectedAGV.CurrentPosition;
Program.WriteLine($" [AFTER] 새로운 CurrentPosition: ({newCurrentPos.X}, {newCurrentPos.Y})");
Program.WriteLine($" [AFTER] 새로운 TargetPosition: {(newTargetPos.HasValue ? $"({newTargetPos.Value.X}, {newTargetPos.Value.Y})" : "None")}");
// 리프트 방향 계산 과정 상세 출력
Program.WriteLine($" [LIFT] 리프트 방향 계산:");
CalculateLiftDirectionDetailed(selectedAGV);
Program.WriteLine("");
_statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId}), 방향: {selectedDirectionItem?.DisplayText ?? ""}로 설정했습니다.";
_rfidTextBox.Text = ""; // 입력 필드 초기화
// 시뮬레이터 캔버스의 해당 노드로 이동
_simulatorCanvas.PanToNode(targetNode.NodeId);
}
@@ -345,10 +437,10 @@ namespace AGVSimulator.Forms
// 처음 10개의 RFID만 표시
var rfidList = nodesWithRfid.Take(10).Select(n => $"- {n.RfidId} → {n.NodeId}");
var result = string.Join("\n", rfidList);
if (nodesWithRfid.Count > 10)
result += $"\n... 외 {nodesWithRfid.Count - 10}개";
return result;
}
@@ -357,29 +449,31 @@ namespace AGVSimulator.Forms
try
{
var result = MapLoader.LoadMapFromFile(filePath);
if (result.Success)
{
Console.WriteLine($"Map File Load : {filePath}");
_mapNodes = result.Nodes;
_currentMapFilePath = filePath;
// RFID가 없는 노드들에 자동 할당
MapLoader.AssignAutoRfidIds(_mapNodes);
// 시뮬레이터 캔버스에 맵 설정
_simulatorCanvas.Nodes = _mapNodes;
// 설정에 마지막 맵 파일 경로 저장
_config.LastMapFilePath = filePath;
if (_config.AutoSave)
{
_config.Save();
}
// UI 업데이트
UpdateNodeComboBoxes();
UpdateUI();
// 맵에 맞춤
_simulatorCanvas.FitToNodes();
}
@@ -398,27 +492,31 @@ namespace AGVSimulator.Forms
{
_startNodeCombo.Items.Clear();
_targetNodeCombo.Items.Clear();
if (_mapNodes != null)
{
foreach (var node in _mapNodes)
{
if (node.IsActive && node.HasRfid())
{
_startNodeCombo.Items.Add(node);
_targetNodeCombo.Items.Add(node);
// {rfid} - [{node}] 형식으로 ComboBoxItem 생성
var displayText = $"{node.RfidId} - [{node.NodeId}]";
var item = new ComboBoxItem<MapNode>(node, displayText);
_startNodeCombo.Items.Add(item);
_targetNodeCombo.Items.Add(item);
}
}
}
_startNodeCombo.DisplayMember = "RfidId";
_targetNodeCombo.DisplayMember = "RfidId";
_startNodeCombo.DisplayMember = "DisplayText";
_targetNodeCombo.DisplayMember = "DisplayText";
}
private void UpdateAGVComboBox()
{
_agvListCombo.Items.Clear();
if (_agvList != null)
{
foreach (var agv in _agvList)
@@ -426,9 +524,9 @@ namespace AGVSimulator.Forms
_agvListCombo.Items.Add(agv);
}
}
_agvListCombo.DisplayMember = "AgvId";
if (_agvListCombo.Items.Count > 0)
{
_agvListCombo.SelectedIndex = 0;
@@ -439,47 +537,260 @@ namespace AGVSimulator.Forms
{
// 시뮬레이션 상태
_simulationStatusLabel.Text = _simulationState.IsRunning ? "시뮬레이션: 실행 중" : "시뮬레이션: 정지";
// AGV 수
_agvCountLabel.Text = $"AGV 수: {_agvList?.Count ?? 0}";
// 버튼 상태
_startSimulationButton.Enabled = !_simulationState.IsRunning && _agvList?.Count > 0;
_stopSimulationButton.Enabled = _simulationState.IsRunning;
_removeAgvButton.Enabled = _agvListCombo.SelectedItem != null;
_startPathButton.Enabled = _agvListCombo.SelectedItem != null &&
_simulatorCanvas.CurrentPath != null &&
_startPathButton.Enabled = _agvListCombo.SelectedItem != null &&
_simulatorCanvas.CurrentPath != null &&
_simulatorCanvas.CurrentPath.Success;
_calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
_calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
_targetNodeCombo.SelectedItem != null;
// RFID 위치 설정 관련
var hasSelectedAGV = _agvListCombo.SelectedItem != null;
var hasRfidNodes = _mapNodes != null && _mapNodes.Any(n => n.HasRfid());
_setPositionButton.Enabled = hasSelectedAGV && hasRfidNodes;
_rfidTextBox.Enabled = hasSelectedAGV && hasRfidNodes;
// AGV 정보 패널 업데이트
UpdateAGVInfoPanel();
// 맵 다시열기 버튼
var hasCurrentMap = !string.IsNullOrEmpty(_currentMapFilePath);
reloadMapToolStripMenuItem.Enabled = hasCurrentMap;
reloadMapToolStripButton.Enabled = hasCurrentMap;
}
/// <summary>
/// AGV 정보 패널 업데이트
/// </summary>
private void UpdateAGVInfoPanel()
{
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV == null)
{
_liftDirectionLabel.Text = "리프트 방향: -";
_motorDirectionLabel.Text = "모터 방향: -";
_agvInfoTitleLabel.Text = "AGV 상태 정보: (AGV 선택 안됨)";
return;
}
// AGV 선택됨
_agvInfoTitleLabel.Text = $"AGV 상태 정보: {selectedAGV.AgvId}";
// 리프트 방향 계산
var liftDirection = CalculateLiftDirection(selectedAGV);
_liftDirectionLabel.Text = $"리프트 방향: {liftDirection}";
// 모터 방향
var motorDirection = GetMotorDirectionString(selectedAGV.CurrentDirection);
_motorDirectionLabel.Text = $"모터 방향: {motorDirection}";
}
/// <summary>
/// AGV의 리프트 방향 계산 (상세 출력 버전)
/// </summary>
private void CalculateLiftDirectionDetailed(VirtualAGV agv)
{
var currentPos = agv.CurrentPosition;
var targetPos = agv.TargetPosition;
var dockingDirection = agv.DockingDirection;
Program.WriteLine($" 입력값: CurrentPos=({currentPos.X}, {currentPos.Y})");
Program.WriteLine($" 입력값: TargetPos={(!targetPos.HasValue ? "None" : $"({targetPos.Value.X}, {targetPos.Value.Y})")}");
Program.WriteLine($" 입력값: DockingDirection={dockingDirection}");
if (!targetPos.HasValue || targetPos.Value == currentPos)
{
Program.WriteLine($" 결과: 방향을 알 수 없음 (TargetPos 없음 또는 같은 위치)");
return;
}
// 이동 방향 계산 (이전 → 현재 = TargetPos → CurrentPos)
var dx = currentPos.X - targetPos.Value.X;
var dy = currentPos.Y - targetPos.Value.Y;
Program.WriteLine($" 이동 벡터: dx={dx}, dy={dy}");
if (Math.Abs(dx) < 1 && Math.Abs(dy) < 1)
{
Program.WriteLine($" 결과: 정지 상태 (이동거리 < 1픽셀)");
return;
}
// 경로 예측 기반 LiftCalculator를 사용하여 리프트 방향 계산
var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction(
currentPos, targetPos.Value, agv.CurrentDirection, _mapNodes);
// 이동 각도 계산 (표시용)
var moveAngleRad = Math.Atan2(dy, dx);
var moveAngleDeg = moveAngleRad * 180.0 / Math.PI;
while (moveAngleDeg < 0) moveAngleDeg += 360;
while (moveAngleDeg >= 360) moveAngleDeg -= 360;
Program.WriteLine($" 이동 각도: {moveAngleDeg:F1}도 (라디안: {moveAngleRad:F3})");
Program.WriteLine($" 모터 방향: {GetMotorDirectionString(liftInfo.MotorDirection)}");
Program.WriteLine($" 리프트 각도: {liftInfo.AngleDegrees:F1}도 ({liftInfo.CalculationMethod})");
// 도킹 방향 정보 추가
string dockingInfo = dockingDirection == DockingDirection.Forward ? "전진도킹" : "후진도킹";
Program.WriteLine($" 최종 결과: {liftInfo.DirectionString} ({liftInfo.AngleDegrees:F0}°) [{dockingInfo}]");
}
/// <summary>
/// AGV의 리프트 방향 계산 (간단한 버전)
/// </summary>
private string CalculateLiftDirection(VirtualAGV agv)
{
var currentPos = agv.CurrentPosition;
var targetPos = agv.TargetPosition;
var dockingDirection = agv.DockingDirection;
if (!targetPos.HasValue || targetPos.Value == currentPos)
{
// 방향을 알 수 없는 경우
return "알 수 없음 (?)";
}
// 이동 방향 계산 (현재 → 타겟, 실제로는 이전 → 현재)
var dx = currentPos.X - targetPos.Value.X;
var dy = currentPos.Y - targetPos.Value.Y;
if (Math.Abs(dx) < 1 && Math.Abs(dy) < 1)
{
return "정지 상태";
}
// 경로 예측 기반 LiftCalculator를 사용하여 리프트 방향 계산
var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction(
currentPos, targetPos.Value, agv.CurrentDirection, _mapNodes);
// 도킹 방향 정보 추가
string dockingInfo = dockingDirection == DockingDirection.Forward ? "전진도킹" : "후진도킹";
return $"{liftInfo.DirectionString} ({liftInfo.AngleDegrees:F0}°) [{dockingInfo}]";
}
/// <summary>
/// 모터 방향을 문자열로 변환
/// </summary>
private string GetMotorDirectionString(AgvDirection direction)
{
switch (direction)
{
case AgvDirection.Forward:
return "전진 (F)";
case AgvDirection.Backward:
return "후진 (B)";
case AgvDirection.Left:
return "좌회전 (L)";
case AgvDirection.Right:
return "우회전 (R)";
default:
return "알 수 없음";
}
}
/// <summary>
/// 노드 ID를 RFID 값으로 변환 (NodeResolver 사용)
/// </summary>
private string GetRfidByNodeId(string nodeId)
{
var node = _mapNodes?.FirstOrDefault(n => n.NodeId == nodeId);
return node?.HasRfid() == true ? node.RfidId : nodeId;
}
/// <summary>
/// 경로 디버깅 정보 업데이트 (RFID 값 표시, 모터방향 정보 포함)
/// </summary>
private void UpdatePathDebugInfo(AGVPathResult agvResult)
{
if (agvResult == null || !agvResult.Success)
{
_pathDebugLabel.Text = "경로: 설정되지 않음";
return;
}
// 노드 ID를 RFID로 변환한 경로 생성
var pathWithRfid = agvResult.Path.Select(nodeId => GetRfidByNodeId(nodeId)).ToList();
// 콘솔 디버그 정보 출력 (RFID 기준)
Program.WriteLine($"[DEBUG] 경로 계산 완료:");
Program.WriteLine($" 전체 경로 (RFID): [{string.Join(" ", pathWithRfid)}]");
Program.WriteLine($" 전체 경로 (NodeID): [{string.Join(" ", agvResult.Path)}]");
Program.WriteLine($" 경로 노드 수: {agvResult.Path.Count}");
if (agvResult.NodeMotorInfos != null)
{
Program.WriteLine($" 모터정보 수: {agvResult.NodeMotorInfos.Count}");
for (int i = 0; i < agvResult.NodeMotorInfos.Count; i++)
{
var info = agvResult.NodeMotorInfos[i];
var rfidId = GetRfidByNodeId(info.NodeId);
var nextRfidId = info.NextNodeId != null ? GetRfidByNodeId(info.NextNodeId) : "END";
Program.WriteLine($" {i}: {rfidId}({info.NodeId}) → {info.MotorDirection} → {nextRfidId}");
}
}
// 모터방향 정보가 있다면 이를 포함하여 경로 문자열 구성 (RFID 기준)
string pathString;
if (agvResult.NodeMotorInfos != null && agvResult.NodeMotorInfos.Count > 0)
{
// RFID별 모터방향 정보를 포함한 경로 문자열 생성
var pathWithMotorInfo = new List<string>();
for (int i = 0; i < agvResult.NodeMotorInfos.Count; i++)
{
var motorInfo = agvResult.NodeMotorInfos[i];
var rfidId = GetRfidByNodeId(motorInfo.NodeId);
string motorSymbol = motorInfo.MotorDirection == AgvDirection.Forward ? "[전진]" : "[후진]";
pathWithMotorInfo.Add($"{rfidId}{motorSymbol}");
}
pathString = string.Join(" → ", pathWithMotorInfo);
}
else
{
// 기본 경로 정보만 표시 (RFID 기준)
pathString = string.Join(" → ", pathWithRfid);
}
// UI에 표시 (길이 제한)
if (pathString.Length > 120)
{
pathString = pathString.Substring(0, 117) + "...";
}
// 모터방향 통계 추가
string motorStats = "";
if (agvResult.NodeMotorInfos != null && agvResult.NodeMotorInfos.Count > 0)
{
var forwardCount = agvResult.NodeMotorInfos.Count(m => m.MotorDirection == AgvDirection.Forward);
var backwardCount = agvResult.NodeMotorInfos.Count(m => m.MotorDirection == AgvDirection.Backward);
motorStats = $", 전진: {forwardCount}, 후진: {backwardCount}";
}
_pathDebugLabel.Text = $"경로: {pathString} (총 {agvResult.Path.Count}개 노드, {agvResult.TotalDistance:F1}px{motorStats})";
}
private void OnReloadMap_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_currentMapFilePath))
{
MessageBox.Show("다시 로드할 맵 파일이 없습니다. 먼저 맵을 열어주세요.", "알림",
MessageBox.Show("다시 로드할 맵 파일이 없습니다. 먼저 맵을 열어주세요.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (!File.Exists(_currentMapFilePath))
{
MessageBox.Show($"맵 파일을 찾을 수 없습니다:\n{_currentMapFilePath}", "오류",
MessageBox.Show($"맵 파일을 찾을 수 없습니다:\n{_currentMapFilePath}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
@@ -491,7 +802,7 @@ namespace AGVSimulator.Forms
}
catch (Exception ex)
{
MessageBox.Show($"맵 파일을 다시 로드할 수 없습니다:\n{ex.Message}", "오류",
MessageBox.Show($"맵 파일을 다시 로드할 수 없습니다:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -515,7 +826,7 @@ namespace AGVSimulator.Forms
if (openDialog.ShowDialog() == DialogResult.OK)
{
mapEditorPath = openDialog.FileName;
// 설정에 저장
_config.MapEditorExecutablePath = mapEditorPath;
if (_config.AutoSave)
@@ -548,7 +859,7 @@ namespace AGVSimulator.Forms
}
catch (Exception ex)
{
MessageBox.Show($"MapEditor를 실행할 수 없습니다:\n{ex.Message}", "오류",
MessageBox.Show($"MapEditor를 실행할 수 없습니다:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -578,4 +889,45 @@ namespace AGVSimulator.Forms
_statusLabel.Text = "초기화 완료";
}
}
/// <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;
}
}
/// <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

@@ -59,6 +59,7 @@ namespace AGVSimulator.Models
private List<string> _remainingNodes;
private int _currentNodeIndex;
private string _currentNodeId;
private string _targetNodeId;
// 이동 관련
private System.Windows.Forms.Timer _moveTimer;
@@ -67,6 +68,9 @@ namespace AGVSimulator.Models
private Point _moveTargetPosition;
private float _moveProgress;
// 도킹 관련
private DockingDirection _dockingDirection;
// 시뮬레이션 설정
private readonly float _moveSpeed = 50.0f; // 픽셀/초
private readonly float _rotationSpeed = 90.0f; // 도/초
@@ -114,13 +118,23 @@ namespace AGVSimulator.Models
/// <summary>
/// 목표 위치
/// </summary>
public Point TargetPosition => _targetPosition;
public Point? TargetPosition => _targetPosition;
/// <summary>
/// 배터리 레벨 (시뮬레이션)
/// </summary>
public float BatteryLevel { get; set; } = 100.0f;
/// <summary>
/// 목표 노드 ID
/// </summary>
public string TargetNodeId => _targetNodeId;
/// <summary>
/// 도킹 방향
/// </summary>
public DockingDirection DockingDirection => _dockingDirection;
#endregion
#region Constructor
@@ -138,6 +152,9 @@ namespace AGVSimulator.Models
_currentDirection = startDirection;
_currentState = AGVState.Idle;
_currentSpeed = 0;
_dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹
_currentNodeId = string.Empty;
_targetNodeId = string.Empty;
InitializeTimer();
}
@@ -175,13 +192,27 @@ namespace AGVSimulator.Models
_remainingNodes = new List<string>(path.Path);
_currentNodeIndex = 0;
// 시작 노드 위치로 이동
// 시작 노드와 목표 노드 설정
if (_remainingNodes.Count > 0)
{
var startNode = mapNodes.FirstOrDefault(n => n.NodeId == _remainingNodes[0]);
if (startNode != null)
{
_currentNodeId = startNode.NodeId;
// 목표 노드 설정 (경로의 마지막 노드)
if (_remainingNodes.Count > 1)
{
_targetNodeId = _remainingNodes[_remainingNodes.Count - 1];
var targetNode = mapNodes.FirstOrDefault(n => n.NodeId == _targetNodeId);
// 목표 노드의 타입에 따라 도킹 방향 결정
if (targetNode != null)
{
_dockingDirection = GetDockingDirection(targetNode.Type);
}
}
StartMovement();
}
else
@@ -245,6 +276,35 @@ namespace AGVSimulator.Models
SetState(AGVState.Idle);
}
/// <summary>
/// AGV 방향 직접 설정 (시뮬레이터용)
/// </summary>
/// <param name="direction">설정할 방향</param>
public void SetDirection(AgvDirection direction)
{
_currentDirection = direction;
}
/// <summary>
/// AGV 위치 직접 설정 (시뮬레이터용)
/// 이전 위치를 TargetPosition으로 저장하여 리프트 방향 계산이 가능하도록 함
/// </summary>
/// <param name="newPosition">새로운 위치</param>
public void SetPosition(Point newPosition)
{
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
if (_currentPosition != Point.Empty)
{
_targetPosition = _currentPosition;
}
// 새로운 위치 설정
_currentPosition = newPosition;
// 위치 변경 이벤트 발생
PositionChanged?.Invoke(this, _currentPosition);
}
/// <summary>
/// 충전 시작 (시뮬레이션)
/// </summary>
@@ -449,6 +509,19 @@ namespace AGVSimulator.Models
}
}
private DockingDirection GetDockingDirection(NodeType nodeType)
{
switch (nodeType)
{
case NodeType.Charging:
return DockingDirection.Forward; // 충전기: 전진 도킹
case NodeType.Docking:
return DockingDirection.Backward; // 장비 (로더, 클리너, 오프로더, 버퍼): 후진 도킹
default:
return DockingDirection.Forward; // 기본값: 전진
}
}
private void OnError(string message)
{
SetState(AGVState.Error);