2626 lines
102 KiB
C#
2626 lines
102 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using AGVNavigationCore.Models;
|
|
using AGVNavigationCore.Controls;
|
|
using AGVNavigationCore.PathFinding;
|
|
using AGVNavigationCore.Utils;
|
|
using Newtonsoft.Json;
|
|
using AGVNavigationCore.PathFinding.Planning;
|
|
using AGVNavigationCore.PathFinding.Core;
|
|
using AGVSimulator.Models;
|
|
using System.IO.Ports;
|
|
using System.Text;
|
|
|
|
namespace AGVSimulator.Forms
|
|
{
|
|
/// <summary>
|
|
/// AGV 시뮬레이터 메인 폼
|
|
/// </summary>
|
|
public partial class SimulatorForm : Form
|
|
{
|
|
#region Fields
|
|
|
|
// Emulator Fields
|
|
private SerialPort _emulatorPort;
|
|
private ComboBox _portCombo;
|
|
private Button _connectButton;
|
|
private bool _isEmulatorConnected = false;
|
|
|
|
// Emulator State Fields
|
|
private UInt16 _emu_system0 = 0;
|
|
private UInt16 _emu_system1 = 0;
|
|
private UInt16 _emu_error = 0;
|
|
private byte _emu_signal = 0;
|
|
private char _emu_sts_bunki = 'S';
|
|
private char _emu_sts_speed = 'L';
|
|
private char _emu_sts_dir = 'F';
|
|
private char _emu_sts_sensor = '1';
|
|
private string _lastSentNodeId = null;
|
|
|
|
public enum esystemflag0
|
|
{
|
|
Memory_RW_State = 5,
|
|
EXT_IO_Conn_State,
|
|
RFID_Conn_State,
|
|
M5E_Module_Run_State = 8,
|
|
Front_Ultrasonic_Conn_State,
|
|
Front_Untrasonic_Sensor_State,
|
|
Side_Ultrasonic_Conn_State,
|
|
Side_Ultrasonic_Sensor_State = 12,
|
|
Front_Guide_Sensor_State,
|
|
Rear_Guide_Sensor_State,
|
|
Battery_Level_Check
|
|
}
|
|
public enum esystemflag1
|
|
{
|
|
Side_Detect_Ignore = 3,
|
|
Melody_check,
|
|
Mark2_check,
|
|
Mark1_check,
|
|
gateout_check,
|
|
Battery_charging = 8,
|
|
re_Start,
|
|
front_detect_ignore,
|
|
front_detect_check,
|
|
stop_by_front_detect = 12,
|
|
stop_by_cross_in,
|
|
agv_stop,
|
|
agv_run
|
|
}
|
|
public enum eerror
|
|
{
|
|
Emergency = 0,
|
|
Overcurrent,
|
|
Charger_run_error,
|
|
Charger_pos_error,
|
|
line_out_error = 4,
|
|
runerror_by_no_magent_line,
|
|
controller_comm_error = 11,
|
|
arrive_ctl_comm_error,
|
|
door_ctl_comm_error,
|
|
charger_comm_error,
|
|
cross_ctrl_comm_error,
|
|
}
|
|
public enum esignal
|
|
{
|
|
front_gate_out = 0,
|
|
rear_sensor_out,
|
|
mark_sensor_1,
|
|
mark_sensor_2,
|
|
front_left_sensor,
|
|
front_right_sensor,
|
|
front_center_sensor,
|
|
charger_align_sensor,
|
|
}
|
|
public enum estsvaluetype
|
|
{
|
|
bunki,
|
|
speed,
|
|
direction,
|
|
sensor
|
|
}
|
|
|
|
private UnifiedAGVCanvas _simulatorCanvas;
|
|
// private AGVPathfinder _advancedPathfinder;
|
|
private List<VirtualAGV> _agvList;
|
|
private SimulationState _simulationState;
|
|
private System.Windows.Forms.Timer _simulationTimer;
|
|
private SimulatorConfig _config;
|
|
private string _currentMapFilePath;
|
|
private bool _isTargetCalcMode; // 타겟계산 모드 상태
|
|
|
|
// 맵 스캔 모드 관련
|
|
private bool _isMapScanMode; // 맵 스캔 모드 상태
|
|
private DateTime _lastNodeAddTime; // 마지막 노드 추가 시간
|
|
private MapNode _lastScannedNode; // 마지막으로 스캔된 노드
|
|
private int _scanNodeCounter; // 스캔 노드 카운터
|
|
private AgvDirection _lastScanDirection; // 마지막 스캔 방향
|
|
|
|
// UI Controls - Designer에서 생성됨
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// 시뮬레이션 상태
|
|
/// </summary>
|
|
public SimulationState SimulationState => _simulationState;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public SimulatorForm()
|
|
{
|
|
InitializeComponent();
|
|
InitializeForm();
|
|
|
|
// Load 이벤트 연결
|
|
this.Load += SimulatorForm_Load;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Initialization
|
|
|
|
private void InitializeForm()
|
|
{
|
|
// 설정 로드
|
|
_config = SimulatorConfig.Load();
|
|
|
|
// 데이터 초기화
|
|
|
|
_agvList = new List<VirtualAGV>();
|
|
_simulationState = new SimulationState();
|
|
_currentMapFilePath = string.Empty;
|
|
|
|
// 시뮬레이터 캔버스 생성 (중앙 패널에만)
|
|
CreateSimulatorCanvas();
|
|
|
|
// 타이머 초기화
|
|
_simulationTimer = new System.Windows.Forms.Timer();
|
|
_simulationTimer.Interval = 100; // 100ms 간격
|
|
_simulationTimer.Tick += OnSimulationTimer_Tick;
|
|
|
|
// 방향 콤보박스 초기화
|
|
InitializeDirectionCombo();
|
|
|
|
// 에뮬레이터 UI 초기화
|
|
InitializeEmulatorUI();
|
|
|
|
// 초기 상태 설정
|
|
UpdateUI();
|
|
|
|
// 마지막 맵 파일 자동 로드 확인은 Form_Load에서 수행
|
|
}
|
|
|
|
private void CreateSimulatorCanvas()
|
|
{
|
|
_simulatorCanvas = new UnifiedAGVCanvas();
|
|
_simulatorCanvas.Dock = DockStyle.Fill;
|
|
_simulatorCanvas.Mode = UnifiedAGVCanvas.CanvasMode.Emulator;
|
|
|
|
// 목적지 선택 이벤트 구독
|
|
_simulatorCanvas.NodesSelected += OnTargetNodeSelected;
|
|
|
|
_canvasPanel.Controls.Add(_simulatorCanvas);
|
|
}
|
|
|
|
private void SetupLayout()
|
|
{
|
|
// Z-Order 설정 - 모든 컨트롤이 디자이너에 구현되어 자동 관리됨
|
|
_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
|
|
|
|
private void SimulatorForm_Load(object sender, EventArgs e)
|
|
{
|
|
// 폼이 완전히 로드된 후 마지막 맵 파일 자동 로드 확인
|
|
CheckAndLoadLastMapFile();
|
|
}
|
|
|
|
private void OnOpenMap_Click(object sender, EventArgs e)
|
|
{
|
|
using (var openDialog = new OpenFileDialog())
|
|
{
|
|
openDialog.Filter = "AGV Map Files (*.json)|*.json|모든 파일 (*.*)|*.*";
|
|
openDialog.Title = "맵 파일 열기";
|
|
|
|
if (openDialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
try
|
|
{
|
|
LoadMapFile(openDialog.FileName);
|
|
_statusLabel.Text = $"맵 로드 완료: {Path.GetFileName(openDialog.FileName)}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"맵 파일을 로드할 수 없습니다:\n{ex.Message}", "오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnExit_Click(object sender, EventArgs e)
|
|
{
|
|
Close();
|
|
}
|
|
|
|
private void OnStartSimulation_Click(object sender, EventArgs e)
|
|
{
|
|
if (_simulationState.IsRunning)
|
|
return;
|
|
|
|
_simulationState.IsRunning = true;
|
|
_simulationTimer.Start();
|
|
_statusLabel.Text = "시뮬레이션 실행 중";
|
|
Console.WriteLine("시뮬레이션 실행");
|
|
UpdateUI();
|
|
|
|
timer1.Start();
|
|
}
|
|
|
|
private void OnStopSimulation_Click(object sender, EventArgs e)
|
|
{
|
|
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)
|
|
{
|
|
_simulatorCanvas.FitToNodes();
|
|
}
|
|
|
|
private void OnResetZoom_Click(object sender, EventArgs e)
|
|
{
|
|
_simulatorCanvas.ResetZoom();
|
|
}
|
|
|
|
private void OnAbout_Click(object sender, EventArgs e)
|
|
{
|
|
MessageBox.Show("AGV 시뮬레이터 v1.0\n\nENIG AGV 시스템용 시뮬레이터", "정보",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
|
|
private void OnAddAGV_Click(object sender, EventArgs e)
|
|
{
|
|
if (_simulatorCanvas.Nodes == null || _simulatorCanvas.Nodes.Count == 0)
|
|
{
|
|
MessageBox.Show("먼저 맵을 로드해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
|
|
var agvId = $"AGV{_agvList.Count + 1:D2}";
|
|
var startPosition = _simulatorCanvas.Nodes.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} 추가됨";
|
|
_simulatorCanvas.FitToNodes();
|
|
}
|
|
|
|
private void OnRemoveAGV_Click(object sender, EventArgs e)
|
|
{
|
|
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} 제거됨";
|
|
}
|
|
}
|
|
|
|
private void OnAGVList_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
UpdateUI();
|
|
}
|
|
|
|
|
|
|
|
private void OnClearPath_Click(object sender, EventArgs e)
|
|
{
|
|
_simulatorCanvas.CurrentPath = null;
|
|
_pathLengthLabel.Text = "경로 길이: -";
|
|
_statusLabel.Text = "경로 지움";
|
|
|
|
// 🔥 VirtualAGV의 경로도 정지
|
|
if (_agvList != null && _agvList.Count > 0)
|
|
{
|
|
_agvList[0].StopPath();
|
|
}
|
|
}
|
|
|
|
private void OnTargetCalc_Click(object sender, EventArgs e)
|
|
{
|
|
if (_isTargetCalcMode)
|
|
{
|
|
// 타겟계산 모드 해제
|
|
_isTargetCalcMode = false;
|
|
_targetCalcButton.Text = "타겟계산";
|
|
_targetCalcButton.BackColor = SystemColors.Control;
|
|
_statusLabel.Text = "타겟계산 모드 해제";
|
|
}
|
|
else
|
|
{
|
|
// 타겟계산 모드 활성화
|
|
_isTargetCalcMode = true;
|
|
_targetCalcButton.Text = "계산 취소";
|
|
_targetCalcButton.BackColor = Color.LightGreen;
|
|
_statusLabel.Text = "목적지 노드를 클릭하세요 (자동으로 경로 계산됨)";
|
|
}
|
|
}
|
|
|
|
private void OnTargetNodeSelected(object sender, List<NodeBase> selectedNodes)
|
|
{
|
|
try
|
|
{
|
|
// PropertyGrid 업데이트 (항상 수행)
|
|
propertyNode.SelectedObject = selectedNodes.FirstOrDefault();
|
|
|
|
// 타겟계산 모드에서만 처리
|
|
if (_isTargetCalcMode)
|
|
{
|
|
// 타겟계산 모드 해제
|
|
//_isTargetCalcMode = false;
|
|
//_targetCalcButton.Text = "타겟계산";
|
|
//_targetCalcButton.BackColor = SystemColors.Control;
|
|
//_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
|
|
var selectedNode = selectedNodes.FirstOrDefault();
|
|
if (selectedNode == null) return;
|
|
|
|
// 목적지를 선택된 노드로 설정
|
|
SetTargetNodeInCombo(selectedNode.Id);
|
|
|
|
var displayText = GetDisplayName(selectedNode.Id);
|
|
_statusLabel.Text = $"타겟계산 - 목적지: {displayText}";
|
|
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_statusLabel.Text = $"노드 선택 오류: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 목적지 콤보박스에 노드 설정
|
|
/// </summary>
|
|
private void SetTargetNodeInCombo(string nodeId)
|
|
{
|
|
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
|
|
{
|
|
var item = _targetNodeCombo.Items[i].ToString();
|
|
if (item.Contains($"[{nodeId}]"))
|
|
{
|
|
_targetNodeCombo.SelectedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 현재 노드로 시작 노드 설정
|
|
/// </summary>
|
|
private void SetStartNodeFromAGVPosition()
|
|
{
|
|
try
|
|
{
|
|
if (_agvList.Count > 0)
|
|
{
|
|
var agv = _agvList[0]; // 첫 번째 AGV 사용
|
|
var currentNodeId = agv.CurrentNodeId;
|
|
|
|
// AGV가 현재 노드 정보를 가지고 있는 경우 직접 사용
|
|
if (!string.IsNullOrEmpty(currentNodeId))
|
|
{
|
|
// 시작 노드 콤보박스에 설정
|
|
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
|
|
{
|
|
var item = _startNodeCombo.Items[i].ToString();
|
|
if (item.Contains($"[{currentNodeId}]"))
|
|
{
|
|
_startNodeCombo.SelectedIndex = i;
|
|
return; // 성공적으로 설정됨
|
|
}
|
|
}
|
|
}
|
|
|
|
// CurrentNodeId가 없거나 콤보박스에서 찾지 못한 경우 위치 기반으로 폴백
|
|
var currentPos = agv.CurrentPosition;
|
|
var closestNode = FindClosestNode(currentPos);
|
|
if (closestNode != null)
|
|
{
|
|
// 시작 노드 콤보박스에 설정
|
|
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
|
|
{
|
|
var item = _startNodeCombo.Items[i].ToString();
|
|
if (item.Contains($"[{closestNode.Id}]"))
|
|
{
|
|
_startNodeCombo.SelectedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_statusLabel.Text = $"시작 노드 설정 오류: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 위치에서 가장 가까운 노드 찾기
|
|
/// </summary>
|
|
private MapNode FindClosestNode(Point position)
|
|
{
|
|
if (_simulatorCanvas.Nodes == null || _simulatorCanvas.Nodes.Count == 0)
|
|
return null;
|
|
|
|
MapNode closestNode = null;
|
|
double closestDistance = double.MaxValue;
|
|
|
|
foreach (var node in _simulatorCanvas.Nodes)
|
|
{
|
|
var distance = Math.Sqrt(Math.Pow(node.Position.X - position.X, 2) +
|
|
Math.Pow(node.Position.Y - position.Y, 2));
|
|
if (distance < closestDistance)
|
|
{
|
|
closestDistance = distance;
|
|
closestNode = node;
|
|
}
|
|
}
|
|
|
|
return closestNode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 방향을 기호로 변환
|
|
/// </summary>
|
|
private string GetDirectionSymbol(AgvDirection direction)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case AgvDirection.Forward: return "→";
|
|
case AgvDirection.Backward: return "←";
|
|
case AgvDirection.Left: return "↺";
|
|
case AgvDirection.Right: return "↻";
|
|
default: return "-";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 맵 스캔 모드에서 RFID로부터 노드 생성
|
|
/// </summary>
|
|
private void CreateNodeFromRfidScan(ushort rfidId, VirtualAGV selectedAGV)
|
|
{
|
|
try
|
|
{
|
|
// 현재 선택된 방향 확인 (최상단에서 먼저 확인)
|
|
var directionItem = _directionCombo.SelectedItem as DirectionItem;
|
|
var currentDirection = directionItem?.Direction ?? AgvDirection.Forward;
|
|
|
|
// 중복 RFID 확인
|
|
var existingNode = _simulatorCanvas.Nodes?.FirstOrDefault(n => n.RfidId == rfidId);
|
|
if (existingNode != null)
|
|
{
|
|
// 이미 존재하는 노드로 이동
|
|
Program.WriteLine($"[맵 스캔] RFID '{rfidId}'는 이미 존재합니다 (노드: {existingNode.Id})");
|
|
|
|
// 기존 노드로 AGV 위치 설정
|
|
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, existingNode, currentDirection);
|
|
selectedAGV.SetPosition(existingNode, currentDirection);
|
|
|
|
_lastScannedNode = existingNode;
|
|
_lastNodeAddTime = DateTime.Now;
|
|
_lastScanDirection = currentDirection; // 방향 업데이트
|
|
|
|
_statusLabel.Text = $"기존 노드로 이동: {existingNode.Id} [{GetDirectionSymbol(currentDirection)}]";
|
|
_rfidTextBox.Text = "";
|
|
return;
|
|
}
|
|
|
|
// 새 노드 생성 위치 계산
|
|
int newX = 100; // 기본 시작 X 위치
|
|
int newY = 300; // 기본 시작 Y 위치
|
|
|
|
if (_lastScannedNode != null)
|
|
{
|
|
// 시간차 기반 X축 거리 계산
|
|
var timeDiff = (DateTime.Now - _lastNodeAddTime).TotalSeconds;
|
|
|
|
// 10초당 10px, 최소 50px, 최대 100px
|
|
int distanceX = Math.Max(50, Math.Min(100, (int)(timeDiff * 10)));
|
|
|
|
// 방향 전환 확인
|
|
bool directionChanged = (_lastScanDirection != currentDirection);
|
|
|
|
if (directionChanged)
|
|
{
|
|
// 방향이 바뀌면 Y축을 50px 증가시켜서 겹치지 않게 함
|
|
newY = _lastScannedNode.Position.Y + 50;
|
|
newX = _lastScannedNode.Position.X; // X는 같은 위치에서 시작
|
|
|
|
Program.WriteLine($"[맵 스캔] 방향 전환: {_lastScanDirection} → {currentDirection}, Y축 +50px");
|
|
}
|
|
else
|
|
{
|
|
// 방향이 같으면 Y축 유지
|
|
newY = _lastScannedNode.Position.Y;
|
|
|
|
// 모터 방향에 따라 X축 증가/감소
|
|
if (currentDirection == AgvDirection.Forward)
|
|
{
|
|
// 전진: X축 증가
|
|
newX = _lastScannedNode.Position.X + distanceX;
|
|
Program.WriteLine($"[맵 스캔] 전진 모드: X축 +{distanceX}px");
|
|
}
|
|
else if (currentDirection == AgvDirection.Backward)
|
|
{
|
|
// 후진: X축 감소
|
|
newX = _lastScannedNode.Position.X - distanceX;
|
|
Program.WriteLine($"[맵 스캔] 후진 모드: X축 -{distanceX}px");
|
|
}
|
|
else
|
|
{
|
|
// 그 외(회전 등): 기본적으로 전진 방향 사용
|
|
newX = _lastScannedNode.Position.X + distanceX;
|
|
Program.WriteLine($"[맵 스캔] 기타 방향({currentDirection}): X축 +{distanceX}px");
|
|
}
|
|
}
|
|
|
|
Program.WriteLine($"[맵 스캔] 시간차: {timeDiff:F1}초 → 거리: {distanceX}px");
|
|
}
|
|
|
|
// 새 노드 생성
|
|
var newNodeId = $"{_scanNodeCounter:D3}";
|
|
var newNode = new MapNode
|
|
{
|
|
Id = newNodeId,
|
|
RfidId = rfidId,
|
|
Position = new Point(newX, newY),
|
|
IsActive = true
|
|
};
|
|
|
|
// 맵에 추가
|
|
if (_simulatorCanvas.Nodes == null)
|
|
_simulatorCanvas.Nodes = new List<MapNode>();
|
|
|
|
_simulatorCanvas.Nodes.Add(newNode);
|
|
|
|
// 이전 노드와 연결 생성
|
|
if (_lastScannedNode != null)
|
|
{
|
|
// 양방향 연결 (ConnectedNodes에 추가 - JSON 저장됨)
|
|
_lastScannedNode.AddConnection(newNode.Id);
|
|
newNode.AddConnection(_lastScannedNode.Id);
|
|
|
|
Program.WriteLine($"[맵 스캔] 연결 생성: {_lastScannedNode.Id} ↔ {newNode.Id}");
|
|
}
|
|
|
|
// AGV 위치 설정
|
|
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, newNode, currentDirection);
|
|
selectedAGV.SetPosition(newNode, currentDirection);
|
|
|
|
// 캔버스 업데이트
|
|
_simulatorCanvas.Nodes = _simulatorCanvas.Nodes;
|
|
|
|
// 화면을 새 노드 위치로 이동
|
|
_simulatorCanvas.PanToNode(newNode.Id);
|
|
_simulatorCanvas.Invalidate();
|
|
|
|
// 상태 업데이트
|
|
_lastScannedNode = newNode;
|
|
_lastNodeAddTime = DateTime.Now;
|
|
_lastScanDirection = currentDirection; // 현재 방향 저장
|
|
_scanNodeCounter++;
|
|
|
|
// UI 업데이트
|
|
UpdateNodeComboBoxes();
|
|
|
|
_statusLabel.Text = $"노드 생성: {newNode.Id} (RFID: {rfidId}) [{GetDirectionSymbol(currentDirection)}] - 총 {_simulatorCanvas.Nodes.Count}개";
|
|
_rfidTextBox.Text = "";
|
|
|
|
Program.WriteLine($"[맵 스캔] 노드 생성 완료: {newNode.Id} (RFID: {rfidId}) at ({newX}, {newY}), 방향: {currentDirection}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"노드 생성 중 오류 발생:\n{ex.Message}", "오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
Program.WriteLine($"[맵 스캔 오류] {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void OnSetPosition_Click(object sender, EventArgs e)
|
|
{
|
|
SetAGVPositionByRfid();
|
|
_simulatorCanvas.FitToNodes();
|
|
}
|
|
|
|
private void OnRfidTextBox_KeyPress(object sender, KeyPressEventArgs e)
|
|
{
|
|
if (e.KeyChar == (char)Keys.Enter)
|
|
{
|
|
SetAGVPositionByRfid();
|
|
_simulatorCanvas.FitToNodes();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private void OnSimulationTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
// 모든 AGV의 업데이트 메서드 호출 (100ms 간격)
|
|
if (_agvList != null)
|
|
{
|
|
foreach (var agv in _agvList)
|
|
{
|
|
agv.Update(100); // 100ms 간격으로 업데이트
|
|
|
|
// Emulator Tag Logic
|
|
if (_isEmulatorConnected && agv == _agvList.FirstOrDefault())
|
|
{
|
|
if (agv.CurrentNodeId != null && agv.CurrentNodeId != _lastSentNodeId)
|
|
{
|
|
var rfid = GetRfidByNodeId(agv.CurrentNodeId);
|
|
if (rfid > 0)
|
|
{
|
|
SendTag(rfid);
|
|
_lastSentNodeId = agv.CurrentNodeId;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// UI 업데이트
|
|
UpdateUI();
|
|
_simulatorCanvas.Invalidate(); // 화면 다시 그리기
|
|
|
|
// 에뮬레이터 상태 전송
|
|
if (_isEmulatorConnected)
|
|
{
|
|
SendEmulatorStatus();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
|
|
private void SetAGVPositionByRfid()
|
|
{
|
|
// 선택된 AGV 확인
|
|
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
|
if (selectedAGV == null)
|
|
{
|
|
MessageBox.Show("먼저 AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
|
|
// RFID 값 확인
|
|
var rfidId = _rfidTextBox.Text.Trim();
|
|
if (ushort.TryParse(rfidId, out ushort rfidvalue) == false)
|
|
{
|
|
MessageBox.Show("RFID 값을 입력해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
|
|
// 선택된 방향 확인
|
|
var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
|
|
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
|
|
|
|
|
|
// 맵 스캔 모드일 때: 노드 자동 생성
|
|
if (_isMapScanMode)
|
|
{
|
|
CreateNodeFromRfidScan(rfidvalue, selectedAGV);
|
|
this._simulatorCanvas.FitToNodes();
|
|
return;
|
|
}
|
|
|
|
// RFID에 해당하는 노드 직접 찾기
|
|
var targetNode = _simulatorCanvas.Nodes?.FirstOrDefault(n => n.RfidId == rfidvalue);
|
|
if (targetNode == null)
|
|
{
|
|
MessageBox.Show($"RFID '{rfidId}'에 해당하는 노드를 찾을 수 없습니다.\n\n사용 가능한 RFID 목록:\n{GetAvailableRfidList()}",
|
|
"RFID 찾기 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
|
|
//이전위치와 동일한지 체크한다.
|
|
if (selectedAGV.CurrentNodeId == targetNode.Id && selectedAGV.CurrentDirection == selectedDirection)
|
|
{
|
|
Program.WriteLine($"이전 노드위치와 모터의 방향이 동일하여 현재 위치 변경이 취소됩니다(NODE:{targetNode.Id},RFID:{targetNode.RfidId},DIR:{selectedDirection})");
|
|
return;
|
|
}
|
|
|
|
// 콘솔 출력 (상세한 리프트 방향 계산 과정)
|
|
Program.WriteLine($"[AGV-{selectedAGV.AgvId}] 위치 설정:");
|
|
Program.WriteLine($" RFID: {rfidId} → 노드: {targetNode.Id}");
|
|
Program.WriteLine($" 위치: ({targetNode.Position.X}, {targetNode.Position.Y})");
|
|
Program.WriteLine($" 방향: {selectedDirectionItem?.DisplayText ?? "전진"} ({selectedDirection})");
|
|
|
|
// SetPosition 호출 전 상태
|
|
var PrevNodeID = selectedAGV.CurrentNodeId;
|
|
var PrevDir = selectedAGV.CurrentDirection;
|
|
var PrevPosition = selectedAGV.CurrentPosition;
|
|
Program.WriteLine($" [BEFORE] Node:{PrevNodeID}, Dir:{PrevDir},Pos X:{PrevPosition.X},{PrevPosition.Y}");
|
|
|
|
|
|
// AGV 위치 및 방향 설정
|
|
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, targetNode, selectedDirection);
|
|
|
|
// VirtualAGV 객체의 위치와 방향 업데이트
|
|
selectedAGV.SetPosition(targetNode, selectedDirection); // 이전 위치 기억하도록
|
|
|
|
// SetPosition 호출 후 상태 확인 및 리프트 계산
|
|
|
|
var newPrevPos = selectedAGV.PrevPosition;
|
|
var newCurrentPos = selectedAGV.CurrentPosition;
|
|
Program.WriteLine($" [AFTER] 새로운 CurrentPosition: ({newCurrentPos.X}, {newCurrentPos.Y})");
|
|
Program.WriteLine($" [AFTER] 새로운 PrevPosition: {(newPrevPos.HasValue ? $"({newPrevPos.Value.X}, {newPrevPos.Value.Y})" : "None")}");
|
|
|
|
// 리프트 방향 계산 과정 상세 출력
|
|
Program.WriteLine($" [LIFT] 리프트 방향 계산:");
|
|
CalculateLiftDirectionDetailed(selectedAGV);
|
|
Program.WriteLine("");
|
|
|
|
_statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.Id}), 방향: {selectedDirectionItem?.DisplayText ?? "전진"}로 설정했습니다.";
|
|
_rfidTextBox.Text = ""; // 입력 필드 초기화
|
|
|
|
// 시뮬레이터 캔버스의 해당 노드로 이동
|
|
//_simulatorCanvas.PanToNode(targetNode.NodeId);
|
|
|
|
// 시작 노드 콤보박스를 현재 위치로 자동 선택
|
|
SetStartNodeToCombo(targetNode.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 시작 노드 콤보박스에 노드를 설정
|
|
/// </summary>
|
|
private void SetStartNodeToCombo(string nodeId)
|
|
{
|
|
try
|
|
{
|
|
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
|
|
{
|
|
var item = _startNodeCombo.Items[i] as ComboBoxItem<MapNode>;//.ToString();
|
|
if (item.Value.Id.Equals(nodeId))
|
|
{
|
|
_startNodeCombo.SelectedIndex = i;
|
|
Program.WriteLine($"[SYSTEM] 시작 노드를 '{nodeId}'로 자동 선택했습니다.");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Program.WriteLine($"[ERROR] 시작 노드 자동 선택 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private string GetAvailableRfidList()
|
|
{
|
|
if (_simulatorCanvas.Nodes == null || _simulatorCanvas.Nodes.Count == 0)
|
|
return "매핑된 RFID가 없습니다.";
|
|
|
|
var nodesWithRfid = _simulatorCanvas.Nodes.Where(n => n.HasRfid()).ToList();
|
|
if (nodesWithRfid.Count == 0)
|
|
return "RFID가 할당된 노드가 없습니다.";
|
|
|
|
// 처음 10개의 RFID만 표시 (노드 이름 포함)
|
|
var rfidList = nodesWithRfid.Take(10).Select((Func<MapNode, string>)(n =>
|
|
{
|
|
return $"- {n.RfidId} → {n.Id}";
|
|
}));
|
|
var result = string.Join("\n", rfidList);
|
|
|
|
if (nodesWithRfid.Count > 10)
|
|
result += $"\n... 외 {nodesWithRfid.Count - 10}개";
|
|
|
|
return result;
|
|
}
|
|
|
|
private void LoadMapFile(string filePath)
|
|
{
|
|
try
|
|
{
|
|
var result = MapLoader.LoadMapFromFile(filePath);
|
|
sbFile.Text = filePath;
|
|
if (result.Success)
|
|
{
|
|
Console.WriteLine($"Map File Load : {filePath}");
|
|
|
|
_simulatorCanvas.Nodes = result.Nodes;
|
|
_currentMapFilePath = filePath;
|
|
|
|
// RFID 자동 할당 제거 - 에디터에서 설정한 값 그대로 사용
|
|
|
|
// 시뮬레이터 캔버스에 맵 설정
|
|
_simulatorCanvas.SetMapLoadResult(result);//.Nodes = _simulatorCanvas.Nodes;
|
|
|
|
// 맵 설정 적용 (배경색, 그리드 표시)
|
|
if (result.Settings != null)
|
|
{
|
|
_simulatorCanvas.BackColor = System.Drawing.Color.FromArgb(result.Settings.BackgroundColorArgb);
|
|
_simulatorCanvas.ShowGrid = result.Settings.ShowGrid;
|
|
}
|
|
|
|
// 설정에 마지막 맵 파일 경로 저장
|
|
_config.LastMapFilePath = filePath;
|
|
if (_config.AutoSave)
|
|
{
|
|
_config.Save();
|
|
}
|
|
|
|
// UI 업데이트
|
|
UpdateNodeComboBoxes();
|
|
UpdateUI();
|
|
|
|
// 맵에 맞춤
|
|
_simulatorCanvas.FitToNodes();
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException($"맵 파일 로드 실패: {result.ErrorMessage}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException($"맵 파일 로드 실패: {ex.Message}", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 마지막 맵 파일이 있는지 확인하고 사용자에게 로드할지 물어봄
|
|
/// </summary>
|
|
private void CheckAndLoadLastMapFile()
|
|
{
|
|
if (_config.AutoLoadLastMapFile && _config.HasValidLastMapFile())
|
|
{
|
|
string fileName = Path.GetFileName(_config.LastMapFilePath);
|
|
var result = MessageBox.Show(
|
|
$"마지막으로 사용한 맵 파일을 찾았습니다:\n\n{fileName}\n\n이 파일을 열까요?",
|
|
"마지막 맵 파일 로드",
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Question);
|
|
|
|
if (result == DialogResult.Yes)
|
|
{
|
|
try
|
|
{
|
|
LoadMapFile(_config.LastMapFilePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"맵 파일 로드 중 오류가 발생했습니다:\n{ex.Message}",
|
|
"맵 파일 로드 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateNodeComboBoxes()
|
|
{
|
|
_startNodeCombo.Items.Clear();
|
|
_targetNodeCombo.Items.Clear();
|
|
|
|
if (_simulatorCanvas.Nodes != null)
|
|
{
|
|
foreach (var node in _simulatorCanvas.Nodes)
|
|
{
|
|
if (node.IsActive)
|
|
{
|
|
// {rfid} - [{node}] {name} 형식으로 ComboBoxItem 생성
|
|
var displayText = $"{node.StationType.ToString().PadRight(7)} | {node.ID2}";
|
|
var item = new ComboBoxItem<MapNode>(node, displayText);
|
|
|
|
_startNodeCombo.Items.Add(item);
|
|
_targetNodeCombo.Items.Add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
_startNodeCombo.DisplayMember = "DisplayText";
|
|
_targetNodeCombo.DisplayMember = "DisplayText";
|
|
}
|
|
|
|
private void UpdateAGVComboBox()
|
|
{
|
|
_agvListCombo.Items.Clear();
|
|
|
|
if (_agvList != null)
|
|
{
|
|
foreach (var agv in _agvList)
|
|
{
|
|
_agvListCombo.Items.Add(agv);
|
|
}
|
|
}
|
|
|
|
_agvListCombo.DisplayMember = "AgvId";
|
|
|
|
if (_agvListCombo.Items.Count > 0)
|
|
{
|
|
_agvListCombo.SelectedIndex = 0;
|
|
}
|
|
}
|
|
|
|
private void UpdateUI()
|
|
{
|
|
// 시뮬레이션 상태
|
|
_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;
|
|
// btPath1.Enabled = _startNodeCombo.SelectedItem != null &&
|
|
// _targetNodeCombo.SelectedItem != null;
|
|
|
|
// RFID 위치 설정 관련
|
|
var hasSelectedAGV = _agvListCombo.SelectedItem != null;
|
|
var hasRfidNodes = _simulatorCanvas.Nodes != null && _simulatorCanvas.Nodes.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 prevPos = agv.PrevPosition;
|
|
var dockingDirection = agv.DockingDirection;
|
|
|
|
Program.WriteLine($" 입력값: CurrentPos=({currentPos.X}, {currentPos.Y})");
|
|
Program.WriteLine($" 입력값: prevPos={(!prevPos.HasValue ? "None" : $"({prevPos.Value.X}, {prevPos.Value.Y})")}");
|
|
Program.WriteLine($" 입력값: DockingDirection={dockingDirection}");
|
|
|
|
if (!prevPos.HasValue || prevPos.Value == currentPos)
|
|
{
|
|
Program.WriteLine($" 결과: 방향을 알 수 없음 (이전 위치값 없음 또는 같은 위치)");
|
|
return;
|
|
}
|
|
|
|
// 이동 방향 계산 (이전 → 현재 = TargetPos → CurrentPos)
|
|
var dx = currentPos.X - prevPos.Value.X;
|
|
var dy = currentPos.Y - prevPos.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, prevPos.Value, agv.CurrentDirection, _simulatorCanvas.Nodes);
|
|
|
|
// 이동 각도 계산 (표시용)
|
|
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.PrevPosition;
|
|
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, _simulatorCanvas.Nodes);
|
|
|
|
// 도킹 방향 정보 추가
|
|
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 ushort GetRfidByNodeId(string nodeId)
|
|
{
|
|
var node = _simulatorCanvas.Nodes?.FirstOrDefault(n => n.Id == nodeId);
|
|
if (node == null) return 0;
|
|
if (node.HasRfid()) return node.RfidId;
|
|
else return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드의 표시명 가져오기 (RFID 우선, 없으면 (NodeID) 형태)
|
|
/// </summary>
|
|
private string GetDisplayName(string nodeId)
|
|
{
|
|
var node = _simulatorCanvas.Nodes?.FirstOrDefault(n => n.Id == nodeId);
|
|
if (node != null && node.HasRfid())
|
|
{
|
|
return node.RfidId.ToString("0000");
|
|
}
|
|
return $"({nodeId})";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 도킹 검증 결과 확인 및 UI 표시
|
|
/// </summary>
|
|
private void CheckAndDisplayDockingValidation(AGVPathResult agvResult)
|
|
{
|
|
if (agvResult?.DockingValidation == null)
|
|
return;
|
|
|
|
var validation = agvResult.DockingValidation;
|
|
|
|
// 도킹 검증이 필요하지 않은 경우
|
|
if (!validation.IsValidationRequired)
|
|
return;
|
|
|
|
// 도킹 검증 실패시 UI에 표시
|
|
if (!validation.IsValid)
|
|
{
|
|
// 상태바에 경고 메시지 표시
|
|
_statusLabel.Text = $"⚠️ 도킹 방향 오류: {validation.ValidationError}";
|
|
_statusLabel.ForeColor = Color.Red;
|
|
|
|
// 경로는 표시하되, 목적지 노드에 X 마크 표시 요청
|
|
_simulatorCanvas.SetDockingError(validation.TargetNodeId, true);
|
|
|
|
// 사용자에게 알림
|
|
MessageBox.Show($"도킹 방향 검증 실패!\n\n" +
|
|
$"노드: {validation.TargetNodeId} ({validation.TargetNodeType})\n" +
|
|
$"필요 방향: {GetDirectionText(validation.RequiredDockingDirection)}\n" +
|
|
$"계산 방향: {GetDirectionText(validation.CalculatedFinalDirection)}\n\n" +
|
|
$"오류: {validation.ValidationError}",
|
|
"도킹 검증 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
}
|
|
else
|
|
{
|
|
// 도킹 검증 성공시 정상 표시
|
|
if (_statusLabel.ForeColor == Color.Red)
|
|
_statusLabel.ForeColor = Color.Black;
|
|
_simulatorCanvas.SetDockingError(validation.TargetNodeId, false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 방향을 한글 텍스트로 변환
|
|
/// </summary>
|
|
private string GetDirectionText(AgvDirection direction)
|
|
{
|
|
switch (direction)
|
|
{
|
|
case AgvDirection.Forward: return "전진";
|
|
case AgvDirection.Backward: return "후진";
|
|
case AgvDirection.Left: return "좌회전";
|
|
case AgvDirection.Right: return "우회전";
|
|
case AgvDirection.Stop: return "정지";
|
|
default: return direction.ToString();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 고급 경로 디버깅 정보 업데이트
|
|
/// </summary>
|
|
private void UpdateAdvancedPathDebugInfo(AGVPathResult advancedResult)
|
|
{
|
|
if (advancedResult == null || !advancedResult.Success)
|
|
{
|
|
_pathDebugLabel.Text = "고급 경로: 설정되지 않음";
|
|
return;
|
|
}
|
|
|
|
// 노드 ID를 RFID로 변환한 경로 생성
|
|
var pathWithRfid = advancedResult.GetSimplePath().Select(nodeId => GetRfidByNodeId(nodeId)).ToList();
|
|
|
|
// 콘솔 디버그 정보 출력
|
|
Program.WriteLine($"[ADVANCED DEBUG] 고급 경로 계산 완료:");
|
|
Program.WriteLine($" 전체 경로 (RFID): [{string.Join(" → ", pathWithRfid)}]");
|
|
Program.WriteLine($" 전체 경로 (NDID): [{string.Join(" → ", advancedResult.GetSimplePath())}]");
|
|
Program.WriteLine($" 경로 노드 수: {advancedResult.DetailedPath.Count}");
|
|
Program.WriteLine($" 방향 전환 필요: {advancedResult.RequiredDirectionChange}");
|
|
|
|
if (advancedResult.RequiredDirectionChange && !string.IsNullOrEmpty(advancedResult.DirectionChangeNode))
|
|
{
|
|
Program.WriteLine($" 방향 전환 노드: {GetDisplayName(advancedResult.DirectionChangeNode)}");
|
|
}
|
|
|
|
Program.WriteLine($" 설명: {advancedResult.PlanDescription}");
|
|
|
|
// 상세 경로 정보 출력
|
|
for (int i = 0; i < advancedResult.DetailedPath.Count; i++)
|
|
{
|
|
var info = advancedResult.DetailedPath[i];
|
|
var rfidId = GetRfidByNodeId(info.NodeId);
|
|
|
|
var nextRfidId = "";
|
|
if (info.NextNode != null && info.NextNode.HasRfid())
|
|
{
|
|
nextRfidId = info.NextNode.RfidId.ToString("0000");
|
|
}
|
|
else if (info.NextNode != null)
|
|
{
|
|
nextRfidId = info.NextNode.Id;
|
|
}
|
|
else
|
|
{
|
|
nextRfidId = "-END-";
|
|
}
|
|
|
|
|
|
var flags = new List<string>();
|
|
if (info.CanRotate) flags.Add("회전가능");
|
|
if (info.IsDirectionChangePoint) flags.Add("방향전환");
|
|
if (info.RequiresSpecialAction) flags.Add($"특수동작:{info.SpecialActionDescription}");
|
|
if (info.MagnetDirection != MagnetDirection.Straight) flags.Add($"마그넷:{info.MagnetDirection}");
|
|
|
|
var flagsStr = flags.Count > 0 ? $" [{string.Join(", ", flags)}]" : "";
|
|
Program.WriteLine($" {i}: {rfidId}({info.NodeId}) → {info.MotorDirection} → {nextRfidId}{flagsStr}");
|
|
}
|
|
|
|
// 경로 문자열 구성 (마그넷 방향 포함)
|
|
var pathWithDetails = new List<string>();
|
|
for (int i = 0; i < advancedResult.DetailedPath.Count; i++)
|
|
{
|
|
var motorInfo = advancedResult.DetailedPath[i];
|
|
var rfidId = GetRfidByNodeId(motorInfo.NodeId);
|
|
string motorSymbol = motorInfo.MotorDirection == AgvDirection.Forward ? "[F]" : "[B]";
|
|
|
|
// 마그넷 방향 표시
|
|
if (motorInfo.MagnetDirection != MagnetDirection.Straight)
|
|
{
|
|
string magnetSymbol = motorInfo.MagnetDirection == MagnetDirection.Left ? "[L]" : "[R]";
|
|
motorSymbol += magnetSymbol;
|
|
}
|
|
else motorSymbol += "[S]";
|
|
|
|
// 특수 동작 표시
|
|
if (motorInfo.RequiresSpecialAction)
|
|
motorSymbol += "[🔄]";
|
|
else if (motorInfo.IsDirectionChangePoint && motorInfo.CanRotate)
|
|
motorSymbol += "[↻]";
|
|
|
|
pathWithDetails.Add($"{rfidId}{motorSymbol}");
|
|
}
|
|
|
|
string pathString = string.Join(" → ", pathWithDetails);
|
|
|
|
// UI에 표시 (길이 제한)
|
|
//if (pathString.Length > 100)
|
|
//{
|
|
// pathString = pathString.Substring(0, 97) + "...";
|
|
//}
|
|
|
|
// 통계 정보
|
|
var forwardCount = advancedResult.DetailedPath.Count(m => m.MotorDirection == AgvDirection.Forward);
|
|
var backwardCount = advancedResult.DetailedPath.Count(m => m.MotorDirection == AgvDirection.Backward);
|
|
var magnetDirectionChanges = advancedResult.DetailedPath.Count(m => m.MagnetDirection != MagnetDirection.Straight);
|
|
|
|
string stats = $"전진: {forwardCount}, 후진: {backwardCount}";
|
|
if (magnetDirectionChanges > 0)
|
|
stats += $", 마그넷제어: {magnetDirectionChanges}";
|
|
|
|
_pathDebugLabel.Text = $"고급경로: {pathString} (총 {advancedResult.DetailedPath.Count}개 노드, {advancedResult.TotalDistance:F1}px, {stats})";
|
|
}
|
|
|
|
|
|
|
|
private void OnReloadMap_Click(object sender, EventArgs e)
|
|
{
|
|
if (string.IsNullOrEmpty(_currentMapFilePath))
|
|
{
|
|
MessageBox.Show("다시 로드할 맵 파일이 없습니다. 먼저 맵을 열어주세요.", "알림",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
|
|
if (!File.Exists(_currentMapFilePath))
|
|
{
|
|
MessageBox.Show($"맵 파일을 찾을 수 없습니다:\n{_currentMapFilePath}", "오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
LoadMapFile(_currentMapFilePath);
|
|
_statusLabel.Text = $"맵 다시 로드 완료: {Path.GetFileName(_currentMapFilePath)}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"맵 파일을 다시 로드할 수 없습니다:\n{ex.Message}", "오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
private void OnLaunchMapEditor_Click(object sender, EventArgs e)
|
|
{
|
|
try
|
|
{
|
|
// MapEditor 실행 파일 경로 확인
|
|
string mapEditorPath = _config.MapEditorExecutablePath;
|
|
|
|
// 경로가 설정되지 않았거나 파일이 없는 경우 사용자에게 선택을 요청
|
|
if (string.IsNullOrEmpty(mapEditorPath) || !File.Exists(mapEditorPath))
|
|
{
|
|
using (var openDialog = new OpenFileDialog())
|
|
{
|
|
openDialog.Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*";
|
|
openDialog.Title = "AGV MapEditor 실행 파일 선택";
|
|
openDialog.InitialDirectory = Application.StartupPath;
|
|
|
|
if (openDialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
mapEditorPath = openDialog.FileName;
|
|
|
|
// 설정에 저장
|
|
_config.MapEditorExecutablePath = mapEditorPath;
|
|
if (_config.AutoSave)
|
|
{
|
|
_config.Save();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return; // 사용자가 취소함
|
|
}
|
|
}
|
|
}
|
|
|
|
// MapEditor 실행
|
|
var startInfo = new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = mapEditorPath,
|
|
UseShellExecute = true
|
|
};
|
|
|
|
// 현재 로드된 맵 파일이 있으면 파라미터로 전달
|
|
if (!string.IsNullOrEmpty(_currentMapFilePath) && File.Exists(_currentMapFilePath))
|
|
{
|
|
startInfo.Arguments = $"\"{_currentMapFilePath}\"";
|
|
}
|
|
|
|
System.Diagnostics.Process.Start(startInfo);
|
|
_statusLabel.Text = "MapEditor 실행됨";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"MapEditor를 실행할 수 없습니다:\n{ex.Message}", "오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void btAllReset_Click(object sender, EventArgs e)
|
|
{
|
|
// 시뮬레이션 정지
|
|
if (_simulationState.IsRunning)
|
|
{
|
|
OnStopSimulation_Click(sender, e);
|
|
}
|
|
|
|
// AGV 초기화
|
|
_agvList.Clear();
|
|
_simulatorCanvas.AGVList = new List<IAGV>();
|
|
|
|
// 경로 초기화
|
|
_simulatorCanvas.CurrentPath = null;
|
|
|
|
// UI 업데이트
|
|
UpdateAGVComboBox();
|
|
UpdateNodeComboBoxes();
|
|
UpdateUI();
|
|
|
|
_statusLabel.Text = "초기화 완료";
|
|
}
|
|
|
|
|
|
private async void toolStripButton1_Click(object sender, EventArgs e)
|
|
{
|
|
// 맵과 AGV 확인
|
|
if (_simulatorCanvas.Nodes == null || _simulatorCanvas.Nodes.Count == 0)
|
|
{
|
|
MessageBox.Show("맵 데이터가 없습니다. 먼저 맵을 로드해주세요.", "알림",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
|
if (selectedAGV == null)
|
|
{
|
|
MessageBox.Show("테스트할 AGV를 선택해주세요.", "알림",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
// 도킹 타겟 노드 찾기
|
|
var dockingTargets = _simulatorCanvas.Nodes.Where(n => n.isDockingNode).ToList();
|
|
|
|
if (dockingTargets.Count == 0)
|
|
{
|
|
MessageBox.Show("도킹 타겟(충전기 또는 장비)이 없습니다.", "알림",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
// 연결된 노드 쌍 찾기 (사전 계산)
|
|
var nodePairs = GetConnectedNodePairs();
|
|
var testCount = nodePairs.Count * dockingTargets.Count * 2; // 노드 쌍 x 도킹 타겟 x 방향(2)
|
|
|
|
// 테스트 시작 확인
|
|
var result = MessageBox.Show(
|
|
$"경로 예측 테스트를 시작합니다.\n\n" +
|
|
$"※ 실제 사용자 시나리오 재현 방식:\n" +
|
|
$" 연결된 노드 쌍을 따라 AGV를 2번 이동시켜\n" +
|
|
$" 방향을 확정한 후 각 도킹 타겟으로 경로 계산\n\n" +
|
|
$"• 연결된 노드 쌍: {nodePairs.Count}개\n" +
|
|
$"• 도킹 타겟: {dockingTargets.Count}개 (충전기/장비)\n" +
|
|
$"• AGV 방향: 2가지 (정방향/역방향)\n" +
|
|
$"• 총 테스트 케이스: {testCount}개\n\n" +
|
|
$"※ UI가 실시간으로 업데이트됩니다.\n" +
|
|
$"계속하시겠습니까?",
|
|
"경로 예측 테스트 확인",
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Question);
|
|
|
|
if (result != DialogResult.Yes)
|
|
return;
|
|
|
|
// 로그 폼 생성 및 표시
|
|
var logForm = new ProgressLogForm();
|
|
logForm.Show(this);
|
|
logForm.UpdateProgress(0, testCount);
|
|
logForm.UpdateStatus("테스트 준비 중...");
|
|
|
|
// 비동기 테스트 시작
|
|
await Task.Run(() => RunPathPredictionTest(selectedAGV, dockingTargets, logForm));
|
|
|
|
// 완료
|
|
if (logForm.CancelRequested)
|
|
{
|
|
logForm.SetCancelled();
|
|
MessageBox.Show("테스트가 취소되었습니다.", "알림",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
else
|
|
{
|
|
logForm.SetCompleted();
|
|
MessageBox.Show("경로 예측 테스트가 완료되었습니다.", "완료",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드의 표시 이름 가져오기 (RFID 우선, 없으면 (NodeId))
|
|
/// </summary>
|
|
private string GetNodeDisplayName(MapNode node)
|
|
{
|
|
if (node == null) return "-";
|
|
var retval = "";
|
|
if (node.HasRfid()) retval = node.RfidId.ToString("0000");
|
|
else retval = $"({node.Id})";
|
|
if (node.DockDirection == DockingDirection.Forward)
|
|
retval += "(F)";
|
|
else if (node.DockDirection == DockingDirection.Backward)
|
|
retval += "(B)";
|
|
return retval;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 방향 콤보박스 선택 (테스트용)
|
|
/// </summary>
|
|
private void SetDirectionComboBox(AgvDirection direction)
|
|
{
|
|
for (int i = 0; i < _directionCombo.Items.Count; i++)
|
|
{
|
|
var item = _directionCombo.Items[i] as DirectionItem;
|
|
if (item != null && item.Direction == direction)
|
|
{
|
|
_directionCombo.SelectedIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 목표 노드 콤보박스 선택 (테스트용)
|
|
/// </summary>
|
|
private void SetTargetNodeComboBox(string nodeId)
|
|
{
|
|
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
|
|
{
|
|
var item = _targetNodeCombo.Items[i] as ComboBoxItem<MapNode>;
|
|
if (item?.Value?.Id == nodeId)
|
|
{
|
|
_targetNodeCombo.SelectedIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// UI 상태로부터 테스트 결과 생성 (테스트용)
|
|
/// </summary>
|
|
private PathTestLogItem CreateTestResultFromUI(MapNode prevNode, MapNode targetNode,
|
|
string directionName, AGVPathResult calcResult)
|
|
{
|
|
var currentNode = _simulatorCanvas.Nodes.FirstOrDefault(n => n.Id ==
|
|
(_agvListCombo.SelectedItem as VirtualAGV)?.CurrentNodeId);
|
|
|
|
var logItem = new PathTestLogItem
|
|
{
|
|
PreviousPosition = GetNodeDisplayName(prevNode),
|
|
MotorDirection = directionName,
|
|
CurrentPosition = GetNodeDisplayName(currentNode),
|
|
TargetPosition = GetNodeDisplayName(targetNode),
|
|
DockingPosition = (targetNode.StationType == StationType.Charger ) ? "충전기" : "장비"
|
|
};
|
|
|
|
if (calcResult.Success)
|
|
{
|
|
// 경로 계산 성공 - 현재 화면에 표시된 경로 정보 사용
|
|
var currentPath = calcResult;// _simulatorCanvas.CurrentPath;
|
|
if (currentPath != null && currentPath.Success)
|
|
{
|
|
// 도킹 검증
|
|
var dockingValidation = DockingValidator.ValidateDockingDirection(currentPath, _simulatorCanvas.Nodes);
|
|
|
|
if (dockingValidation.IsValid)
|
|
{
|
|
logItem.Success = true;
|
|
logItem.Message = "성공";
|
|
logItem.DetailedPath = currentPath.GetDetailedPathInfo(true);
|
|
}
|
|
else
|
|
{
|
|
logItem.Success = false;
|
|
logItem.Message = $"도킹 검증 실패: {dockingValidation.ValidationError}";
|
|
logItem.DetailedPath = currentPath.GetDetailedPathInfo(true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logItem.Success = true;
|
|
logItem.Message = "경로 계산 성공";
|
|
logItem.DetailedPath = "-";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 경로 계산 실패
|
|
logItem.Success = false;
|
|
logItem.Message = calcResult.Message;
|
|
logItem.DetailedPath = "-";
|
|
}
|
|
|
|
return logItem;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 연결된 노드 쌍 찾기 (A→B 형태)
|
|
/// </summary>
|
|
private List<(MapNode nodeA, MapNode nodeB)> GetConnectedNodePairs()
|
|
{
|
|
var pairs = new List<(MapNode, MapNode)>();
|
|
var processedPairs = new HashSet<string>();
|
|
|
|
foreach (var nodeA in _simulatorCanvas.Nodes)
|
|
{
|
|
if (nodeA.ConnectedMapNodes == null || nodeA.ConnectedMapNodes.Count == 0)
|
|
continue;
|
|
|
|
// 연결된 노드 객체 순회
|
|
foreach (var nodeB in nodeA.ConnectedMapNodes)
|
|
{
|
|
if (nodeB == null)
|
|
continue;
|
|
|
|
// 중복 방지 (A→B와 B→A를 같은 것으로 간주)
|
|
var pairKey1 = $"{nodeA.Id}→{nodeB.Id}";
|
|
var pairKey2 = $"{nodeB.Id}→{nodeA.Id}";
|
|
|
|
if (nodeA.HasRfid() && nodeB.HasRfid() && !processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
|
|
{
|
|
pairs.Add((nodeA, nodeB));
|
|
processedPairs.Add(pairKey1);
|
|
processedPairs.Add(pairKey2);
|
|
}
|
|
}
|
|
}
|
|
|
|
return pairs;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 경로 예측 테스트 실행 (실제 사용자 시나리오 재현)
|
|
/// </summary>
|
|
private void RunPathPredictionTest(VirtualAGV agv, List<MapNode> dockingTargets, ProgressLogForm logForm)
|
|
{
|
|
var directions = new[]
|
|
{
|
|
(AgvDirection.Forward, "정방향"),
|
|
(AgvDirection.Backward, "역방향")
|
|
};
|
|
|
|
// 연결된 노드 쌍 찾기
|
|
var nodePairs = GetConnectedNodePairs();
|
|
|
|
int totalTests = nodePairs.Count * dockingTargets.Count * 2;
|
|
int currentTest = 0;
|
|
int successCount = 0;
|
|
int failCount = 0;
|
|
|
|
logForm.UpdateStatus("경로 예측 테스트 진행 중...");
|
|
logForm.AppendLog($"테스트 시작: 총 {totalTests}개 케이스");
|
|
logForm.AppendLog($"연결된 노드 쌍: {nodePairs.Count}개");
|
|
logForm.AppendLog($"도킹 타겟: {dockingTargets.Count}개");
|
|
logForm.AppendLog("---");
|
|
|
|
// 각 연결된 노드 쌍에 대해 테스트
|
|
foreach (var (direction, directionName) in directions)
|
|
{
|
|
foreach (var (nodeA, nodeB) in nodePairs)
|
|
{
|
|
// 취소 확인
|
|
if (logForm.CancelRequested)
|
|
{
|
|
logForm.AppendLog($"테스트 취소됨 - {currentTest}/{totalTests} 완료");
|
|
return;
|
|
}
|
|
|
|
// === 실제 사용자 워크플로우 재현 ===
|
|
// 1단계: AGV를 nodeA로 이동 (실제 UI 조작)
|
|
this.Invoke((MethodInvoker)delegate
|
|
{
|
|
// RFID 텍스트박스에 값 입력
|
|
_rfidTextBox.Text = nodeA.RfidId.ToString();
|
|
|
|
// 방향 콤보박스 선택
|
|
SetDirectionComboBox(direction);
|
|
|
|
// 위치설정 버튼 클릭 (실제 사용자 동작)
|
|
SetAGVPositionByRfid();
|
|
|
|
Application.DoEvents(); // UI 업데이트
|
|
});
|
|
Thread.Sleep(100); // 시각적 효과
|
|
|
|
// 2단계: AGV를 nodeB로 이동 (방향 확정됨)
|
|
this.Invoke((MethodInvoker)delegate
|
|
{
|
|
// RFID 텍스트박스에 값 입력
|
|
_rfidTextBox.Text = nodeB.RfidId.ToString();
|
|
|
|
// 방향 콤보박스 선택
|
|
SetDirectionComboBox(direction);
|
|
|
|
// 위치설정 버튼 클릭 (실제 사용자 동작)
|
|
SetAGVPositionByRfid();
|
|
|
|
Application.DoEvents(); // UI 업데이트
|
|
});
|
|
Thread.Sleep(100); // 시각적 효과
|
|
|
|
// 3단계: nodeB 위치에서 모든 도킹 타겟으로 경로 예측
|
|
foreach (var dockingTarget in dockingTargets)
|
|
{
|
|
// 취소 확인
|
|
if (logForm.CancelRequested)
|
|
{
|
|
logForm.AppendLog($"테스트 취소됨 - {currentTest}/{totalTests} 완료");
|
|
return;
|
|
}
|
|
|
|
currentTest++;
|
|
|
|
// UI 스레드에서 경로 계산 및 테스트 (실제 UI 사용)
|
|
PathTestLogItem testResult = null;
|
|
this.Invoke((MethodInvoker)delegate
|
|
{
|
|
// 진행상황 업데이트
|
|
logForm.UpdateProgress(currentTest, totalTests);
|
|
logForm.UpdateStatus($"테스트 진행 중... ({currentTest}/{totalTests}) [{GetNodeDisplayName(nodeA)}→{GetNodeDisplayName(nodeB)}→{GetNodeDisplayName(dockingTarget)}]");
|
|
prb1.Value = (int)((double)currentTest / totalTests * 100);
|
|
|
|
// 목표 노드 콤보박스 선택
|
|
SetTargetNodeComboBox(dockingTarget.Id);
|
|
|
|
// 경로 계산 버튼 클릭 (실제 사용자 동작)
|
|
var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
|
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
|
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
|
var calcResult = CalcPath(startNode, targetNode, this._simulatorCanvas.Nodes, selectedAGV.PrevNode, selectedAGV.PrevDirection);
|
|
|
|
//// 테스트 결과 생성
|
|
testResult = CreateTestResultFromUI(nodeA, dockingTarget, directionName, calcResult);
|
|
|
|
//// 로그 추가
|
|
logForm.AddLogItem(testResult);
|
|
|
|
//// 실패한 경우에만 경로를 화면에 표시 (시각적 확인)
|
|
if (!testResult.Success && _simulatorCanvas.CurrentPath != null)
|
|
{
|
|
_simulatorCanvas.Invalidate();
|
|
}
|
|
|
|
Application.DoEvents();
|
|
});
|
|
|
|
if (testResult.Success)
|
|
successCount++;
|
|
else
|
|
failCount++;
|
|
|
|
// UI 반응성을 위한 짧은 대기
|
|
Thread.Sleep(50);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 최종 결과
|
|
logForm.AppendLog($"");
|
|
logForm.AppendLog($"=== 테스트 완료 ===");
|
|
logForm.AppendLog($"총 테스트: {totalTests}");
|
|
logForm.AppendLog($"성공: {successCount}");
|
|
logForm.AppendLog($"실패: {failCount}");
|
|
logForm.AppendLog($"성공률: {(double)successCount / totalTests * 100:F1}%");
|
|
}
|
|
|
|
private void btPredict_Click(object sender, EventArgs e)
|
|
{
|
|
// 다음 행동 예측
|
|
if (_agvList == null || _agvList.Count == 0)
|
|
{
|
|
MessageBox.Show("AGV가 없습니다.", "예측 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
// 첫 번째 AGV의 다음 행동 예측
|
|
var agv = _agvList[0];
|
|
var command = agv.Predict();
|
|
|
|
//this.lbPredict.Text = $"MOT:{command.Motor},MAG:{command.Magnet},SPD:{command.Speed}:{command.Reason}";
|
|
|
|
// 예측 결과 표시
|
|
var message = $"[다음 행동 예측]\n\n" +
|
|
$"모터: {command.Motor}\n" +
|
|
$"마그넷: {command.Magnet}\n" +
|
|
$"속도: {command.Speed}\n" +
|
|
$"이유: {command.Message}\n\n" +
|
|
$"---\n" +
|
|
$"현재 상태: {agv.CurrentState}\n" +
|
|
$"현재 방향: {agv.CurrentDirection}\n" +
|
|
$"위치 확정: {agv.IsPositionConfirmed} (RFID {agv.DetectedRfidCount}개)\n" +
|
|
$"현재 노드: {agv.CurrentNodeId ?? "없음"}";
|
|
|
|
Console.WriteLine(message);
|
|
}
|
|
|
|
private void timer1_Tick(object sender, EventArgs e)
|
|
{
|
|
if (_agvList == null || _agvList.Count == 0)
|
|
{
|
|
// MessageBox.Show("AGV가 없습니다.", "예측 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
// 첫 번째 AGV의 다음 행동 예측
|
|
var agv = _agvList[0];
|
|
var command = agv.Predict();
|
|
this.lbPredict.Text = $"Motor:{command.Motor},Magnet:{command.Magnet},Speed:{command.Speed} : {command.Message}";
|
|
}
|
|
|
|
|
|
private void btMakeMap_Click(object sender, EventArgs e)
|
|
{
|
|
if (!_isMapScanMode)
|
|
{
|
|
// 스캔 모드 시작
|
|
var result = MessageBox.Show(
|
|
"맵 스캔 모드를 시작합니다.\n\n" +
|
|
"RFID를 입력하면 자동으로 맵 노드가 생성되고\n" +
|
|
"이전 노드와 연결됩니다.\n\n" +
|
|
"기존 맵 데이터를 삭제하고 시작하시겠습니까?\n\n" +
|
|
"예: 새 맵 시작\n" +
|
|
"아니오: 기존 맵에 추가",
|
|
"맵 스캔 모드",
|
|
MessageBoxButtons.YesNoCancel,
|
|
MessageBoxIcon.Question);
|
|
|
|
if (result == DialogResult.Cancel)
|
|
return;
|
|
|
|
if (result == DialogResult.Yes)
|
|
{
|
|
// 기존 맵 데이터 삭제
|
|
_simulatorCanvas.Nodes?.Clear();
|
|
_simulatorCanvas.Nodes = new List<MapNode>();
|
|
_simulatorCanvas.Nodes = _simulatorCanvas.Nodes;
|
|
_currentMapFilePath = string.Empty;
|
|
UpdateNodeComboBoxes();
|
|
_statusLabel.Text = "맵 초기화 완료 - 스캔 모드 시작";
|
|
}
|
|
|
|
// 스캔 모드 활성화
|
|
_isMapScanMode = true;
|
|
_lastNodeAddTime = DateTime.Now;
|
|
_lastScannedNode = null;
|
|
_scanNodeCounter = 1;
|
|
_lastScanDirection = AgvDirection.Forward; // 기본 방향은 전진
|
|
btMakeMap.Text = "스캔 중지";
|
|
btMakeMap.BackColor = Color.LightCoral;
|
|
_statusLabel.Text = "맵 스캔 모드: RFID를 입력하여 노드를 생성하세요";
|
|
|
|
Program.WriteLine("[맵 스캔] 스캔 모드 시작");
|
|
}
|
|
else
|
|
{
|
|
// 스캔 모드 종료
|
|
_isMapScanMode = false;
|
|
btMakeMap.Text = "맵 생성";
|
|
btMakeMap.BackColor = SystemColors.Control;
|
|
_statusLabel.Text = $"맵 스캔 완료 - {_simulatorCanvas.Nodes?.Count ?? 0}개 노드 생성됨";
|
|
|
|
Program.WriteLine($"[맵 스캔] 스캔 모드 종료 - 총 {_simulatorCanvas.Nodes?.Count ?? 0}개 노드");
|
|
|
|
// 맵 저장 권장
|
|
if (_simulatorCanvas.Nodes != null && _simulatorCanvas.Nodes.Count > 0)
|
|
{
|
|
var saveResult = MessageBox.Show(
|
|
$"맵 스캔이 완료되었습니다.\n\n" +
|
|
$"생성된 노드: {_simulatorCanvas.Nodes.Count}개\n\n" +
|
|
"맵을 저장하시겠습니까?",
|
|
"맵 저장",
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Question);
|
|
|
|
if (saveResult == DialogResult.Yes)
|
|
{
|
|
btMapSaveAs_Click(sender, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 맵 데이터를 파일에 저장 (MapLoader 공통 저장 로직 사용)
|
|
/// </summary>
|
|
private void SaveMapToFile(string filePath)
|
|
{
|
|
try
|
|
{
|
|
// MapLoader의 표준 저장 메서드 사용 (AGVMapEditor와 동일한 형식)
|
|
bool success = MapLoader.SaveMapToFile(filePath, _simulatorCanvas.Nodes);
|
|
|
|
if (success)
|
|
{
|
|
Program.WriteLine($"[맵 저장] 파일 저장 완료: {filePath} ({_simulatorCanvas.Nodes.Count}개 노드)");
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("맵 저장에 실패했습니다.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Program.WriteLine($"[맵 저장 오류] {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void btMapSaveAs_Click(object sender, EventArgs e)
|
|
{
|
|
// 맵 데이터 확인
|
|
if (_simulatorCanvas.Nodes == null || _simulatorCanvas.Nodes.Count == 0)
|
|
{
|
|
MessageBox.Show("저장할 맵 데이터가 없습니다.", "알림",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
|
|
using (var saveDialog = new SaveFileDialog())
|
|
{
|
|
saveDialog.Filter = "AGV Map Files (*.json)|*.json|모든 파일 (*.*)|*.*";
|
|
saveDialog.Title = "맵 파일 저장";
|
|
saveDialog.DefaultExt = "json";
|
|
|
|
// 현재 파일이 있으면 기본 파일명으로 설정
|
|
if (!string.IsNullOrEmpty(_currentMapFilePath))
|
|
{
|
|
saveDialog.FileName = Path.GetFileName(_currentMapFilePath);
|
|
saveDialog.InitialDirectory = Path.GetDirectoryName(_currentMapFilePath);
|
|
}
|
|
else
|
|
{
|
|
// 기본 파일명: 날짜_시간 형식
|
|
saveDialog.FileName = $"ScanMap_{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
|
}
|
|
|
|
if (saveDialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
try
|
|
{
|
|
SaveMapToFile(saveDialog.FileName);
|
|
_currentMapFilePath = saveDialog.FileName;
|
|
|
|
// 설정에 마지막 맵 파일 경로 저장
|
|
_config.LastMapFilePath = _currentMapFilePath;
|
|
if (_config.AutoSave)
|
|
{
|
|
_config.Save();
|
|
}
|
|
|
|
_statusLabel.Text = $"맵 저장 완료: {Path.GetFileName(_currentMapFilePath)}";
|
|
MessageBox.Show($"맵이 저장되었습니다.\n\n파일: {_currentMapFilePath}", "저장 완료",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"맵 저장 중 오류 발생:\n{ex.Message}", "저장 오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void 맵저장SToolStripMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
// 현재 맵 파일 경로가 있는 경우 해당 파일에 저장
|
|
if (string.IsNullOrEmpty(_currentMapFilePath))
|
|
{
|
|
// 경로가 없으면 다른 이름으로 저장 다이얼로그 표시
|
|
btMapSaveAs_Click(sender, e);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
SaveMapToFile(_currentMapFilePath);
|
|
_statusLabel.Text = $"맵 저장 완료: {Path.GetFileName(_currentMapFilePath)}";
|
|
MessageBox.Show($"맵이 저장되었습니다.\n\n파일: {_currentMapFilePath}", "저장 완료",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"맵 저장 중 오류 발생:\n{ex.Message}", "저장 오류",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
#region Emulator Logic
|
|
|
|
private void InitializeEmulatorUI()
|
|
{
|
|
// 에뮬레이터 제어 패널 생성
|
|
var emulatorPanel = new GroupBox();
|
|
emulatorPanel.Text = "AGV Emulator (RS232)";
|
|
emulatorPanel.Height = 60;
|
|
emulatorPanel.Dock = DockStyle.Top;
|
|
|
|
var layout = new FlowLayoutPanel();
|
|
layout.Dock = DockStyle.Fill;
|
|
layout.Padding = new Padding(5);
|
|
layout.AutoSize = true;
|
|
|
|
// 포트 선택 콤보박스
|
|
_portCombo = new ComboBox();
|
|
_portCombo.Width = 100;
|
|
_portCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
|
_portCombo.DropDown += (s, e) =>
|
|
{
|
|
_portCombo.Items.Clear();
|
|
_portCombo.Items.AddRange(SerialPort.GetPortNames());
|
|
};
|
|
_portCombo.Items.AddRange(SerialPort.GetPortNames());
|
|
if (_portCombo.Items.Count > 0) _portCombo.SelectedIndex = 0;
|
|
|
|
// 연결 버튼
|
|
_connectButton = new Button();
|
|
_connectButton.Text = "Connect";
|
|
_connectButton.Click += OnConnectEmulator_Click;
|
|
|
|
layout.Controls.Add(new Label { Text = "Port:", AutoSize = true, Margin = new Padding(3, 8, 3, 3) });
|
|
layout.Controls.Add(_portCombo);
|
|
layout.Controls.Add(_connectButton);
|
|
|
|
emulatorPanel.Controls.Add(layout);
|
|
|
|
// 폼의 최상단에 추가
|
|
this.Controls.Add(emulatorPanel);
|
|
emulatorPanel.BringToFront();
|
|
}
|
|
|
|
private void OnConnectEmulator_Click(object sender, EventArgs e)
|
|
{
|
|
if (_isEmulatorConnected)
|
|
{
|
|
DisconnectEmulator();
|
|
}
|
|
else
|
|
{
|
|
if (_portCombo.SelectedItem == null)
|
|
{
|
|
MessageBox.Show("포트를 선택해주세요.");
|
|
return;
|
|
}
|
|
ConnectEmulator(_portCombo.SelectedItem.ToString());
|
|
}
|
|
}
|
|
|
|
private void ConnectEmulator(string portName)
|
|
{
|
|
try
|
|
{
|
|
_emulatorPort = new SerialPort(portName, 115200, Parity.None, 8, StopBits.One);
|
|
_emulatorPort.DataReceived += OnEmulatorDataReceived;
|
|
_emulatorPort.Open();
|
|
_isEmulatorConnected = true;
|
|
_connectButton.Text = "Disconnect";
|
|
_portCombo.Enabled = false;
|
|
MessageBox.Show($"에뮬레이터 시작: {portName}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"연결 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void DisconnectEmulator()
|
|
{
|
|
try
|
|
{
|
|
if (_emulatorPort != null)
|
|
{
|
|
// 이벤트 핸들러 해제 (중복 호출 방지)
|
|
_emulatorPort.DataReceived -= OnEmulatorDataReceived;
|
|
|
|
if (_emulatorPort.IsOpen)
|
|
_emulatorPort.Close();
|
|
}
|
|
_isEmulatorConnected = false;
|
|
_connectButton.Text = "Connect";
|
|
_portCombo.Enabled = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"해제 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private StringBuilder _recvBuffer = new StringBuilder();
|
|
|
|
private void OnEmulatorDataReceived(object sender, SerialDataReceivedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
if (_emulatorPort == null || !_emulatorPort.IsOpen) return;
|
|
|
|
string data = _emulatorPort.ReadExisting();
|
|
_recvBuffer.Append(data);
|
|
|
|
string buffer = _recvBuffer.ToString();
|
|
int stxIndex = buffer.IndexOf((char)0x02);
|
|
|
|
while (stxIndex >= 0)
|
|
{
|
|
int etxIndex = buffer.IndexOf((char)0x03, stxIndex);
|
|
if (etxIndex > stxIndex)
|
|
{
|
|
string packet = buffer.Substring(stxIndex + 1, etxIndex - stxIndex - 1);
|
|
ProcessEmulatorPacket(packet);
|
|
|
|
// 처리된 패킷 제거
|
|
buffer = buffer.Substring(etxIndex + 1);
|
|
stxIndex = buffer.IndexOf((char)0x02);
|
|
}
|
|
else
|
|
{
|
|
break; // ETX 아직 안옴
|
|
}
|
|
}
|
|
|
|
_recvBuffer.Clear();
|
|
_recvBuffer.Append(buffer);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Emulator Recv Error: {ex.Message}");
|
|
// 수신 중 오류 발생 시 연결 해제 처리 (UI 스레드에서 실행)
|
|
this.Invoke(new Action(() =>
|
|
{
|
|
if (_isEmulatorConnected) DisconnectEmulator();
|
|
}));
|
|
}
|
|
}
|
|
|
|
#region Emulator Helpers
|
|
private bool GetBit(ref UInt16 _value, int idx)
|
|
{
|
|
var offset = (UInt16)(1 << idx);
|
|
return (_value & offset) != 0;
|
|
}
|
|
|
|
private bool SetBit(ref UInt16 _value, int idx, bool value)
|
|
{
|
|
var oldvalue = GetBit(ref _value, idx);
|
|
if (value)
|
|
{
|
|
var offset = (UInt16)(1 << idx);
|
|
_value = (UInt16)(_value | offset);
|
|
}
|
|
else
|
|
{
|
|
var offset = (UInt16)(~(1 << idx));
|
|
_value = (UInt16)(_value & offset);
|
|
}
|
|
return oldvalue != value;
|
|
}
|
|
|
|
private bool SetBit(ref byte _value, int idx, bool value)
|
|
{
|
|
var offset = (byte)(1 << idx);
|
|
if (value) _value |= offset;
|
|
else _value &= (byte)~offset;
|
|
return true;
|
|
}
|
|
|
|
private void SetAGV(esystemflag0 flag, bool value)
|
|
{
|
|
SetBit(ref _emu_system0, (int)flag, value);
|
|
}
|
|
private void SetAGV(esystemflag1 flag, bool value)
|
|
{
|
|
SetBit(ref _emu_system1, (int)flag, value);
|
|
}
|
|
private void SetAGV(eerror flag, bool value)
|
|
{
|
|
SetBit(ref _emu_error, (int)flag, value);
|
|
}
|
|
private void SetAGV(esignal flag, bool value)
|
|
{
|
|
SetBit(ref _emu_signal, (int)flag, value);
|
|
}
|
|
private void SetSTS(estsvaluetype target, char value)
|
|
{
|
|
switch (target)
|
|
{
|
|
case estsvaluetype.sensor: _emu_sts_sensor = value; break;
|
|
case estsvaluetype.direction: _emu_sts_dir = value; break;
|
|
case estsvaluetype.speed: _emu_sts_speed = value; break;
|
|
case estsvaluetype.bunki: _emu_sts_bunki = value; break;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
private void ProcessEmulatorPacket(string packet)
|
|
{
|
|
// Packet: CMD(3) + DATA(...) + Checksum(2)
|
|
// But here packet is substring between STX and ETX.
|
|
// Example: STS...Checksum
|
|
|
|
if (packet.Length < 3) return;
|
|
string cmd = packet.Substring(0, 3);
|
|
string data = "";
|
|
if (packet.Length > 5) // CMD + Checksum(2)
|
|
{
|
|
data = packet.Substring(3, packet.Length - 5);
|
|
}
|
|
|
|
// AGV 제어 (첫 번째 AGV 대상)
|
|
var agv = _agvList.FirstOrDefault();
|
|
|
|
this.Invoke(new Action(() =>
|
|
{
|
|
switch (cmd)
|
|
{
|
|
case "CRN": // 기동명령
|
|
if (data.Length > 0) SetSTS(estsvaluetype.direction, data[0]);
|
|
SetAGV(esystemflag1.agv_stop, false);
|
|
SetAGV(esystemflag1.agv_run, true);
|
|
if (agv != null) agv.Resume();
|
|
break;
|
|
|
|
case "CST": // 중지명령
|
|
if (data.StartsWith("M"))
|
|
{
|
|
// Mark Stop
|
|
// TODO: Implement Mark Stop logic in VirtualAGV if needed
|
|
// For now, just log it
|
|
Console.WriteLine("Mark Stop Command Received");
|
|
}
|
|
else
|
|
{
|
|
SetAGV(esystemflag1.agv_run, false);
|
|
SetAGV(esystemflag1.agv_stop, true);
|
|
if (agv != null) agv.Pause();
|
|
}
|
|
break;
|
|
|
|
case "CBR": // 분기명령
|
|
// FSL1
|
|
if (data.Length >= 4)
|
|
{
|
|
SetSTS(estsvaluetype.direction, data[0]);
|
|
SetSTS(estsvaluetype.bunki, data[1]);
|
|
SetSTS(estsvaluetype.speed, data[2]);
|
|
SetSTS(estsvaluetype.sensor, data[3]);
|
|
}
|
|
break;
|
|
|
|
case "CRT": // 수동제어
|
|
if (data.Length >= 4)
|
|
{
|
|
_emu_sts_dir = data[0];
|
|
_emu_sts_bunki = data[1];
|
|
_emu_sts_speed = data[2];
|
|
_emu_sts_sensor = data[3];
|
|
SetAGV(esystemflag1.agv_stop, false);
|
|
SetAGV(esystemflag1.agv_run, true);
|
|
if (agv != null) agv.Resume();
|
|
}
|
|
break;
|
|
|
|
case "SFR": // Reset
|
|
SetAGV(eerror.Emergency, false);
|
|
SetAGV(eerror.line_out_error, false);
|
|
SetAGV(eerror.Overcurrent, false);
|
|
SetAGV(esystemflag1.agv_run, false);
|
|
SetAGV(esystemflag1.agv_stop, true);
|
|
if (agv != null) agv.Pause();
|
|
break;
|
|
|
|
case "CBT": // 충전
|
|
SetAGV(esystemflag1.Battery_charging, true);
|
|
if (data.Length >= 5)
|
|
{
|
|
var cmdChar = data[4];
|
|
if (cmdChar == 'I') SetAGV(esystemflag1.Battery_charging, true);
|
|
else SetAGV(esystemflag1.Battery_charging, false);
|
|
}
|
|
break;
|
|
|
|
case "ACK":
|
|
// Log ACK
|
|
break;
|
|
|
|
default:
|
|
// Send ACK for other commands
|
|
SendCmd("ACK", cmd);
|
|
break;
|
|
}
|
|
}));
|
|
}
|
|
|
|
private void SendCmd(string cmd, string value)
|
|
{
|
|
if (_emulatorPort == null || !_emulatorPort.IsOpen) return;
|
|
|
|
var barr = new List<byte>();
|
|
barr.Add(0x02);
|
|
barr.AddRange(System.Text.Encoding.Default.GetBytes(cmd));
|
|
barr.AddRange(System.Text.Encoding.Default.GetBytes(value));
|
|
barr.Add((byte)'*');
|
|
barr.Add((byte)'*');
|
|
barr.Add(0x03);
|
|
|
|
try
|
|
{
|
|
_emulatorPort.Write(barr.ToArray(), 0, barr.Count);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
public void SendTag(ushort tagno)
|
|
{
|
|
if (_emulatorPort == null || !_emulatorPort.IsOpen) return;
|
|
|
|
var tagnostr = tagno.ToString("000000");
|
|
|
|
var barr = new List<byte>();
|
|
barr.Add(0x02);
|
|
barr.Add((byte)'T');
|
|
barr.Add((byte)'A');
|
|
barr.Add((byte)'G');
|
|
barr.AddRange(System.Text.Encoding.Default.GetBytes(tagnostr));
|
|
barr.Add((byte)'*');
|
|
barr.Add((byte)'*');
|
|
barr.Add(0x03);
|
|
|
|
try
|
|
{
|
|
_emulatorPort.Write(barr.ToArray(), 0, barr.Count);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private void SendEmulatorStatus()
|
|
{
|
|
if (_emulatorPort == null || !_emulatorPort.IsOpen)
|
|
{
|
|
if (_isEmulatorConnected) DisconnectEmulator();
|
|
return;
|
|
}
|
|
|
|
var agv = _agvList.FirstOrDefault();
|
|
|
|
// Sync state from VirtualAGV
|
|
if (agv != null)
|
|
{
|
|
// Update Battery
|
|
// Update Position/Tag?
|
|
}
|
|
|
|
// STS Packet Construction
|
|
// STS(3) + Volt(3) + Sys0(4) + Sys1(4) + Err(4) + Spd(1) + Bunki(1) + Dir(1) + Sensor(1) + Signal(2) + Checksum(2)
|
|
|
|
// Default buffer
|
|
var sample = "02 53 54 53 32 35 38 46 46 46 46 34 30 30 30 30 30 30 30 4C 53 46 30 30 30 30 30 30 33 41 03";
|
|
var barr = sample.Split(' ').Select(t => Convert.ToByte(t, 16)).ToArray();
|
|
|
|
// Volt (255 for now)
|
|
var voltstr = "255";
|
|
var bufarr = System.Text.Encoding.Default.GetBytes(voltstr);
|
|
Array.Copy(bufarr, 0, barr, 4, bufarr.Length);
|
|
|
|
// System0
|
|
bufarr = System.Text.Encoding.Default.GetBytes(_emu_system0.ToString("X2").PadLeft(4, '0'));
|
|
Array.Copy(bufarr, 0, barr, 7, bufarr.Length);
|
|
|
|
// System1
|
|
bufarr = System.Text.Encoding.Default.GetBytes(_emu_system1.ToString("X2").PadLeft(4, '0'));
|
|
Array.Copy(bufarr, 0, barr, 11, bufarr.Length);
|
|
|
|
// Error
|
|
bufarr = System.Text.Encoding.Default.GetBytes(_emu_error.ToString("X2").PadLeft(4, '0'));
|
|
Array.Copy(bufarr, 0, barr, 15, bufarr.Length);
|
|
|
|
// Status Chars
|
|
barr[19] = (byte)_emu_sts_speed;
|
|
barr[20] = (byte)_emu_sts_bunki;
|
|
barr[21] = (byte)_emu_sts_dir;
|
|
barr[22] = (byte)_emu_sts_sensor;
|
|
|
|
// Signal
|
|
bufarr = System.Text.Encoding.Default.GetBytes(_emu_signal.ToString("X2").PadLeft(2, '0'));
|
|
Array.Copy(bufarr, 0, barr, 23, bufarr.Length);
|
|
|
|
// Checksum (**)
|
|
barr[barr.Length - 3] = (byte)'*';
|
|
barr[barr.Length - 2] = (byte)'*';
|
|
|
|
try
|
|
{
|
|
_emulatorPort.Write(barr, 0, barr.Length);
|
|
}
|
|
catch
|
|
{
|
|
if (_isEmulatorConnected) DisconnectEmulator();
|
|
}
|
|
}
|
|
|
|
private string CalculateChecksum(string data)
|
|
{
|
|
int sum = 0;
|
|
foreach (char c in data) sum += c;
|
|
|
|
// 16진수 변환 후 뒤 2자리
|
|
string hex = sum.ToString("X");
|
|
if (hex.Length >= 2) return hex.Substring(hex.Length - 2);
|
|
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)
|
|
{
|
|
// 1. 기본 정보 획득
|
|
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요") SetStartNodeFromAGVPosition();
|
|
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
|
|
{
|
|
MessageBox.Show("시작/목표 노드를 확인하세요");
|
|
return;
|
|
}
|
|
|
|
//var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
|
//if (selectedAGV == null) return AGVPathResult.CreateFailure("Virtual AGV 없음");
|
|
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
|
|
|
// 경로계산2 (Gateway Logic)
|
|
var startNode = (_startNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
|
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem<MapNode>)?.Value;
|
|
var rlt = CalcPath(startNode, targetNode, this._simulatorCanvas.Nodes, selectedAGV.PrevNode, selectedAGV.PrevDirection);
|
|
if (rlt.Success == false) MessageBox.Show(rlt.Message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
else
|
|
{
|
|
// 8. 적용
|
|
|
|
ApplyResultToSimulator(rlt, selectedAGV);
|
|
UpdateAdvancedPathDebugInfo(rlt);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// 길목(Gateway) 기반 경로 계산
|
|
/// 버퍼-버퍼 상태에서는 별도의 추가 로직을 적용합니다
|
|
/// </summary>
|
|
public AGVPathResult CalcPath(MapNode startNode, MapNode targetNode, List<MapNode> nodes,
|
|
MapNode prevNode, AgvDirection prevDir)
|
|
{
|
|
// Core Logic으로 이관됨
|
|
var pathFinder = new AGVPathfinder(nodes);
|
|
var result = pathFinder.CalculatePath(startNode, targetNode, prevNode, prevDir);
|
|
|
|
//게이트웨이노드를 하이라이트강조 한단
|
|
this._simulatorCanvas.HighlightNodeId = (result.Gateway?.Id ?? string.Empty);
|
|
return result;
|
|
}
|
|
|
|
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 btSelectMapEditor_Click(object sender, EventArgs e)
|
|
{
|
|
using (var openDialog = new OpenFileDialog())
|
|
{
|
|
openDialog.Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*";
|
|
openDialog.Title = "MapEditor 실행 파일 선택";
|
|
|
|
if (!string.IsNullOrEmpty(_config.MapEditorExecutablePath) && File.Exists(_config.MapEditorExecutablePath))
|
|
{
|
|
openDialog.InitialDirectory = Path.GetDirectoryName(_config.MapEditorExecutablePath);
|
|
openDialog.FileName = Path.GetFileName(_config.MapEditorExecutablePath);
|
|
}
|
|
|
|
if (openDialog.ShowDialog() == DialogResult.OK)
|
|
{
|
|
_config.MapEditorExecutablePath = openDialog.FileName;
|
|
_config.Save();
|
|
|
|
_statusLabel.Text = $"MapEditor 경로 설정: {Path.GetFileName(openDialog.FileName)}";
|
|
MessageBox.Show($"MapEditor 실행 파일이 설정되었습니다:\n{openDialog.FileName}",
|
|
"경로 설정 완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |