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.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(); 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(); // 경로 초기화 _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 == 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; } /// /// 연결된 노드 쌍 찾기 (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]; 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(); _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); } } } }