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
{
///
/// AGV 시뮬레이터 메인 폼
///
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 _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
///
/// 시뮬레이션 상태
///
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();
_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();
}
///
/// 모터 구동방향 콤보박스 초기화
///
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(_agvList.Cast());
// 콘솔 출력
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(_agvList.Cast());
// 콘솔 출력
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 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}";
}
}
///
/// 목적지 콤보박스에 노드 설정
///
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;
}
}
}
///
/// AGV 현재 노드로 시작 노드 설정
///
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}";
}
}
///
/// 위치에서 가장 가까운 노드 찾기
///
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;
}
///
/// 방향을 기호로 변환
///
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 "-";
}
}
///
/// 맵 스캔 모드에서 RFID로부터 노드 생성
///
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();
_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);
}
///
/// 시작 노드 콤보박스에 노드를 설정
///
private void SetStartNodeToCombo(string nodeId)
{
try
{
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
{
var item = _startNodeCombo.Items[i] as ComboBoxItem;//.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)(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);
}
}
///
/// 마지막 맵 파일이 있는지 확인하고 사용자에게 로드할지 물어봄
///
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(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;
}
///
/// AGV 정보 패널 업데이트
///
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}";
}
///
/// AGV의 리프트 방향 계산 (상세 출력 버전)
///
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}]");
}
///
/// AGV의 리프트 방향 계산 (간단한 버전)
///
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}]";
}
///
/// 모터 방향을 문자열로 변환
///
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 "알 수 없음";
}
}
///
/// 노드 ID를 RFID 값으로 변환 (NodeResolver 사용)
///
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;
}
///
/// 노드의 표시명 가져오기 (RFID 우선, 없으면 (NodeID) 형태)
///
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})";
}
///
/// 도킹 검증 결과 확인 및 UI 표시
///
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);
}
}
///
/// AGV 방향을 한글 텍스트로 변환
///
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();
}
}
///
/// 고급 경로 디버깅 정보 업데이트
///
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();
if (info.IsTurn) 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();
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 += "[↻]";
if (motorInfo.IsTurn) motorSymbol = "[TURN]";
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();
// 경로 초기화
_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);
}
}
///
/// 노드의 표시 이름 가져오기 (RFID 우선, 없으면 (NodeId))
///
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;
}
///
/// 방향 콤보박스 선택 (테스트용)
///
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;
}
}
}
///
/// 목표 노드 콤보박스 선택 (테스트용)
///
private void SetTargetNodeComboBox(string nodeId)
{
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
{
var item = _targetNodeCombo.Items[i] as ComboBoxItem;
if (item?.Value?.Id == nodeId)
{
_targetNodeCombo.SelectedIndex = i;
return;
}
}
}
///
/// UI 상태로부터 테스트 결과 생성 (테스트용)
///
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 == Station.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;
}
///
/// 연결된 노드 쌍 찾기 (A→B 형태)
///
private List<(MapNode nodeA, MapNode nodeB)> GetConnectedNodePairs()
{
var pairs = new List<(MapNode, MapNode)>();
var processedPairs = new HashSet();
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;
}
///
/// 경로 예측 테스트 실행 (실제 사용자 시나리오 재현)
///
private void RunPathPredictionTest(VirtualAGV agv, List 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)?.Value;
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem)?.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];
try
{
var command = agv.Predict();
this.lbPredict.Text = $"Motor:{command.Motor},Magnet:{command.Magnet},Speed:{command.Speed} : {command.Message}";
}catch ( Exception ex)
{
lbPredict.Text = "예측오류" + ex.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();
_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);
}
}
}
}
///
/// 맵 데이터를 파일에 저장 (MapLoader 공통 저장 로직 사용)
///
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();
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();
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;
// var targetItem = _targetNodeCombo.SelectedItem as ComboBoxItem;
// 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)?.Value;
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem)?.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);
}
}
///
/// 길목(Gateway) 기반 경로 계산
/// 버퍼-버퍼 상태에서는 별도의 추가 로직을 적용합니다
///
public AGVPathResult CalcPath(MapNode startNode, MapNode targetNode, List 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;
}
///
/// 길목(Gateway) 기반 경로 계산
/// 버퍼-버퍼 상태에서는 별도의 추가 로직을 적용합니다
///
public AGVPathResult CalcPath_New(MapNode startNode, MapNode targetNode, List nodes,
MapNode prevNode, AgvDirection prevDir)
{
// Core Logic으로 이관됨
var pathFinder = new AGVPathfinder(nodes);
var result = pathFinder.CalculateScriptedPath(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);
}
}
}
private void button1_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)?.Value;
var targetNode = (_targetNodeCombo.SelectedItem as ComboBoxItem)?.Value;
var rlt = CalcPath_New(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);
}
}
}
}