feat: Add AGV Map Editor and Simulator tools
- Add AGVMapEditor: Visual map editing with drag-and-drop node placement * RFID mapping separation (physical ID ↔ logical node mapping) * A* pathfinding algorithm with AGV directional constraints * JSON map data persistence with structured format * Interactive map canvas with zoom/pan functionality - Add AGVSimulator: Real-time AGV movement simulation * Virtual AGV with state machine (Idle, Moving, Rotating, Docking, Charging, Error) * Path execution and visualization from calculated routes * Real-time position tracking and battery simulation * Integration with map editor data format - Update solution structure and build configuration - Add comprehensive documentation in CLAUDE.md - Implement AGV-specific constraints (forward/backward docking, rotation limits) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
64
Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
Normal file
64
Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
Normal file
@@ -0,0 +1,64 @@
|
||||
namespace AGVSimulator.Forms
|
||||
{
|
||||
partial class SimulatorForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 필수 디자이너 변수입니다.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 사용 중인 모든 리소스를 정리합니다.
|
||||
/// </summary>
|
||||
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
|
||||
// 시뮬레이션 정지
|
||||
if (_simulationTimer != null)
|
||||
{
|
||||
_simulationTimer.Stop();
|
||||
_simulationTimer.Dispose();
|
||||
}
|
||||
|
||||
// AGV 정리
|
||||
if (_agvList != null)
|
||||
{
|
||||
foreach (var agv in _agvList)
|
||||
{
|
||||
agv.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form 디자이너에서 생성한 코드
|
||||
|
||||
/// <summary>
|
||||
/// 디자이너 지원에 필요한 메서드입니다.
|
||||
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// SimulatorForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||
this.Name = "SimulatorForm";
|
||||
this.Text = "AGV 시뮬레이터";
|
||||
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
688
Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
Normal file
688
Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
Normal file
@@ -0,0 +1,688 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AGVMapEditor.Models;
|
||||
using AGVSimulator.Controls;
|
||||
using AGVSimulator.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVSimulator.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 시뮬레이터 메인 폼
|
||||
/// </summary>
|
||||
public partial class SimulatorForm : Form
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private SimulatorCanvas _simulatorCanvas;
|
||||
private List<MapNode> _mapNodes;
|
||||
private List<RfidMapping> _rfidMappings;
|
||||
private NodeResolver _nodeResolver;
|
||||
private PathCalculator _pathCalculator;
|
||||
private List<VirtualAGV> _agvList;
|
||||
private SimulationState _simulationState;
|
||||
private Timer _simulationTimer;
|
||||
|
||||
// UI Controls
|
||||
private MenuStrip _menuStrip;
|
||||
private ToolStrip _toolStrip;
|
||||
private StatusStrip _statusStrip;
|
||||
private Panel _controlPanel;
|
||||
private Panel _canvasPanel;
|
||||
|
||||
// Control Panel Controls
|
||||
private GroupBox _agvControlGroup;
|
||||
private ComboBox _agvListCombo;
|
||||
private Button _addAgvButton;
|
||||
private Button _removeAgvButton;
|
||||
private Button _startSimulationButton;
|
||||
private Button _stopSimulationButton;
|
||||
private Button _resetButton;
|
||||
|
||||
private GroupBox _pathGroup;
|
||||
private ComboBox _startNodeCombo;
|
||||
private ComboBox _targetNodeCombo;
|
||||
private Button _calculatePathButton;
|
||||
private Button _startPathButton;
|
||||
private Button _clearPathButton;
|
||||
|
||||
private GroupBox _viewGroup;
|
||||
private Button _fitToMapButton;
|
||||
private Button _resetZoomButton;
|
||||
|
||||
private GroupBox _statusGroup;
|
||||
private Label _simulationStatusLabel;
|
||||
private Label _agvCountLabel;
|
||||
private Label _pathLengthLabel;
|
||||
|
||||
// Status Labels
|
||||
private ToolStripStatusLabel _statusLabel;
|
||||
private ToolStripStatusLabel _coordLabel;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// 시뮬레이션 상태
|
||||
/// </summary>
|
||||
public SimulationState SimulationState => _simulationState;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public SimulatorForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeForm();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
private void InitializeForm()
|
||||
{
|
||||
// 폼 설정
|
||||
Text = "AGV 시뮬레이터";
|
||||
Size = new Size(1200, 800);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
// 데이터 초기화
|
||||
_mapNodes = new List<MapNode>();
|
||||
_rfidMappings = new List<RfidMapping>();
|
||||
_agvList = new List<VirtualAGV>();
|
||||
_simulationState = new SimulationState();
|
||||
|
||||
// UI 컨트롤 생성
|
||||
CreateMenuStrip();
|
||||
CreateToolStrip();
|
||||
CreateStatusStrip();
|
||||
CreateControlPanel();
|
||||
CreateSimulatorCanvas();
|
||||
|
||||
// 레이아웃 설정
|
||||
SetupLayout();
|
||||
|
||||
// 타이머 초기화
|
||||
_simulationTimer = new Timer();
|
||||
_simulationTimer.Interval = 100; // 100ms 간격
|
||||
_simulationTimer.Tick += OnSimulationTimer_Tick;
|
||||
|
||||
// 초기 상태 설정
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void CreateMenuStrip()
|
||||
{
|
||||
_menuStrip = new MenuStrip();
|
||||
|
||||
// 파일 메뉴
|
||||
var fileMenu = new ToolStripMenuItem("파일(&F)");
|
||||
fileMenu.DropDownItems.Add(new ToolStripMenuItem("맵 열기(&O)...", null, OnOpenMap_Click) { ShortcutKeys = Keys.Control | Keys.O });
|
||||
fileMenu.DropDownItems.Add(new ToolStripSeparator());
|
||||
fileMenu.DropDownItems.Add(new ToolStripMenuItem("종료(&X)", null, OnExit_Click) { ShortcutKeys = Keys.Alt | Keys.F4 });
|
||||
|
||||
// 시뮬레이션 메뉴
|
||||
var simMenu = new ToolStripMenuItem("시뮬레이션(&S)");
|
||||
simMenu.DropDownItems.Add(new ToolStripMenuItem("시작(&S)", null, OnStartSimulation_Click) { ShortcutKeys = Keys.F5 });
|
||||
simMenu.DropDownItems.Add(new ToolStripMenuItem("정지(&T)", null, OnStopSimulation_Click) { ShortcutKeys = Keys.F6 });
|
||||
simMenu.DropDownItems.Add(new ToolStripMenuItem("초기화(&R)", null, OnReset_Click) { ShortcutKeys = Keys.F7 });
|
||||
|
||||
// 보기 메뉴
|
||||
var viewMenu = new ToolStripMenuItem("보기(&V)");
|
||||
viewMenu.DropDownItems.Add(new ToolStripMenuItem("맵 맞춤(&F)", null, OnFitToMap_Click) { ShortcutKeys = Keys.Control | Keys.F });
|
||||
viewMenu.DropDownItems.Add(new ToolStripMenuItem("줌 초기화(&Z)", null, OnResetZoom_Click) { ShortcutKeys = Keys.Control | Keys.D0 });
|
||||
|
||||
// 도움말 메뉴
|
||||
var helpMenu = new ToolStripMenuItem("도움말(&H)");
|
||||
helpMenu.DropDownItems.Add(new ToolStripMenuItem("정보(&A)...", null, OnAbout_Click));
|
||||
|
||||
_menuStrip.Items.AddRange(new ToolStripItem[] { fileMenu, simMenu, viewMenu, helpMenu });
|
||||
Controls.Add(_menuStrip);
|
||||
MainMenuStrip = _menuStrip;
|
||||
}
|
||||
|
||||
private void CreateToolStrip()
|
||||
{
|
||||
_toolStrip = new ToolStrip();
|
||||
|
||||
_toolStrip.Items.Add(new ToolStripButton("맵 열기", null, OnOpenMap_Click) { ToolTipText = "맵 파일을 엽니다" });
|
||||
_toolStrip.Items.Add(new ToolStripSeparator());
|
||||
_toolStrip.Items.Add(new ToolStripButton("시뮬레이션 시작", null, OnStartSimulation_Click) { ToolTipText = "시뮬레이션을 시작합니다" });
|
||||
_toolStrip.Items.Add(new ToolStripButton("시뮬레이션 정지", null, OnStopSimulation_Click) { ToolTipText = "시뮬레이션을 정지합니다" });
|
||||
_toolStrip.Items.Add(new ToolStripButton("초기화", null, OnReset_Click) { ToolTipText = "시뮬레이션을 초기화합니다" });
|
||||
_toolStrip.Items.Add(new ToolStripSeparator());
|
||||
_toolStrip.Items.Add(new ToolStripButton("맵 맞춤", null, OnFitToMap_Click) { ToolTipText = "맵 전체를 화면에 맞춥니다" });
|
||||
_toolStrip.Items.Add(new ToolStripButton("줌 초기화", null, OnResetZoom_Click) { ToolTipText = "줌을 초기화합니다" });
|
||||
|
||||
Controls.Add(_toolStrip);
|
||||
}
|
||||
|
||||
private void CreateStatusStrip()
|
||||
{
|
||||
_statusStrip = new StatusStrip();
|
||||
|
||||
_statusLabel = new ToolStripStatusLabel("준비");
|
||||
_coordLabel = new ToolStripStatusLabel();
|
||||
|
||||
_statusStrip.Items.AddRange(new ToolStripItem[] { _statusLabel, _coordLabel });
|
||||
Controls.Add(_statusStrip);
|
||||
}
|
||||
|
||||
private void CreateControlPanel()
|
||||
{
|
||||
_controlPanel = new Panel();
|
||||
_controlPanel.Width = 250;
|
||||
_controlPanel.Dock = DockStyle.Right;
|
||||
_controlPanel.BackColor = SystemColors.Control;
|
||||
|
||||
// AGV 제어 그룹
|
||||
CreateAGVControlGroup();
|
||||
|
||||
// 경로 제어 그룹
|
||||
CreatePathControlGroup();
|
||||
|
||||
// 뷰 제어 그룹
|
||||
CreateViewControlGroup();
|
||||
|
||||
// 상태 그룹
|
||||
CreateStatusGroup();
|
||||
|
||||
Controls.Add(_controlPanel);
|
||||
}
|
||||
|
||||
private void CreateAGVControlGroup()
|
||||
{
|
||||
_agvControlGroup = new GroupBox();
|
||||
_agvControlGroup.Text = "AGV 제어";
|
||||
_agvControlGroup.Location = new Point(10, 10);
|
||||
_agvControlGroup.Size = new Size(230, 120);
|
||||
|
||||
_agvListCombo = new ComboBox();
|
||||
_agvListCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
_agvListCombo.Location = new Point(10, 25);
|
||||
_agvListCombo.Size = new Size(210, 21);
|
||||
_agvListCombo.SelectedIndexChanged += OnAGVList_SelectedIndexChanged;
|
||||
|
||||
_addAgvButton = new Button();
|
||||
_addAgvButton.Text = "AGV 추가";
|
||||
_addAgvButton.Location = new Point(10, 55);
|
||||
_addAgvButton.Size = new Size(100, 25);
|
||||
_addAgvButton.Click += OnAddAGV_Click;
|
||||
|
||||
_removeAgvButton = new Button();
|
||||
_removeAgvButton.Text = "AGV 제거";
|
||||
_removeAgvButton.Location = new Point(120, 55);
|
||||
_removeAgvButton.Size = new Size(100, 25);
|
||||
_removeAgvButton.Click += OnRemoveAGV_Click;
|
||||
|
||||
_startSimulationButton = new Button();
|
||||
_startSimulationButton.Text = "시뮬레이션 시작";
|
||||
_startSimulationButton.Location = new Point(10, 85);
|
||||
_startSimulationButton.Size = new Size(100, 25);
|
||||
_startSimulationButton.Click += OnStartSimulation_Click;
|
||||
|
||||
_stopSimulationButton = new Button();
|
||||
_stopSimulationButton.Text = "시뮬레이션 정지";
|
||||
_stopSimulationButton.Location = new Point(120, 85);
|
||||
_stopSimulationButton.Size = new Size(100, 25);
|
||||
_stopSimulationButton.Click += OnStopSimulation_Click;
|
||||
|
||||
_agvControlGroup.Controls.AddRange(new Control[] {
|
||||
_agvListCombo, _addAgvButton, _removeAgvButton, _startSimulationButton, _stopSimulationButton
|
||||
});
|
||||
|
||||
_controlPanel.Controls.Add(_agvControlGroup);
|
||||
}
|
||||
|
||||
private void CreatePathControlGroup()
|
||||
{
|
||||
_pathGroup = new GroupBox();
|
||||
_pathGroup.Text = "경로 제어";
|
||||
_pathGroup.Location = new Point(10, 140);
|
||||
_pathGroup.Size = new Size(230, 150);
|
||||
|
||||
var startLabel = new Label();
|
||||
startLabel.Text = "시작 노드:";
|
||||
startLabel.Location = new Point(10, 25);
|
||||
startLabel.Size = new Size(70, 15);
|
||||
|
||||
_startNodeCombo = new ComboBox();
|
||||
_startNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
_startNodeCombo.Location = new Point(10, 45);
|
||||
_startNodeCombo.Size = new Size(210, 21);
|
||||
|
||||
var targetLabel = new Label();
|
||||
targetLabel.Text = "목표 노드:";
|
||||
targetLabel.Location = new Point(10, 75);
|
||||
targetLabel.Size = new Size(70, 15);
|
||||
|
||||
_targetNodeCombo = new ComboBox();
|
||||
_targetNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
_targetNodeCombo.Location = new Point(10, 95);
|
||||
_targetNodeCombo.Size = new Size(210, 21);
|
||||
|
||||
_calculatePathButton = new Button();
|
||||
_calculatePathButton.Text = "경로 계산";
|
||||
_calculatePathButton.Location = new Point(10, 120);
|
||||
_calculatePathButton.Size = new Size(65, 25);
|
||||
_calculatePathButton.Click += OnCalculatePath_Click;
|
||||
|
||||
_startPathButton = new Button();
|
||||
_startPathButton.Text = "경로 시작";
|
||||
_startPathButton.Location = new Point(80, 120);
|
||||
_startPathButton.Size = new Size(65, 25);
|
||||
_startPathButton.Click += OnStartPath_Click;
|
||||
|
||||
_clearPathButton = new Button();
|
||||
_clearPathButton.Text = "경로 지우기";
|
||||
_clearPathButton.Location = new Point(150, 120);
|
||||
_clearPathButton.Size = new Size(70, 25);
|
||||
_clearPathButton.Click += OnClearPath_Click;
|
||||
|
||||
_pathGroup.Controls.AddRange(new Control[] {
|
||||
startLabel, _startNodeCombo, targetLabel, _targetNodeCombo,
|
||||
_calculatePathButton, _startPathButton, _clearPathButton
|
||||
});
|
||||
|
||||
_controlPanel.Controls.Add(_pathGroup);
|
||||
}
|
||||
|
||||
private void CreateViewControlGroup()
|
||||
{
|
||||
_viewGroup = new GroupBox();
|
||||
_viewGroup.Text = "화면 제어";
|
||||
_viewGroup.Location = new Point(10, 300);
|
||||
_viewGroup.Size = new Size(230, 60);
|
||||
|
||||
_fitToMapButton = new Button();
|
||||
_fitToMapButton.Text = "맵 맞춤";
|
||||
_fitToMapButton.Location = new Point(10, 25);
|
||||
_fitToMapButton.Size = new Size(100, 25);
|
||||
_fitToMapButton.Click += OnFitToMap_Click;
|
||||
|
||||
_resetZoomButton = new Button();
|
||||
_resetZoomButton.Text = "줌 초기화";
|
||||
_resetZoomButton.Location = new Point(120, 25);
|
||||
_resetZoomButton.Size = new Size(100, 25);
|
||||
_resetZoomButton.Click += OnResetZoom_Click;
|
||||
|
||||
_resetButton = new Button();
|
||||
_resetButton.Text = "전체 초기화";
|
||||
_resetButton.Location = new Point(65, 55);
|
||||
_resetButton.Size = new Size(100, 25);
|
||||
_resetButton.Click += OnReset_Click;
|
||||
|
||||
_viewGroup.Controls.AddRange(new Control[] { _fitToMapButton, _resetZoomButton });
|
||||
|
||||
_controlPanel.Controls.Add(_viewGroup);
|
||||
}
|
||||
|
||||
private void CreateStatusGroup()
|
||||
{
|
||||
_statusGroup = new GroupBox();
|
||||
_statusGroup.Text = "상태 정보";
|
||||
_statusGroup.Location = new Point(10, 370);
|
||||
_statusGroup.Size = new Size(230, 100);
|
||||
|
||||
_simulationStatusLabel = new Label();
|
||||
_simulationStatusLabel.Text = "시뮬레이션: 정지";
|
||||
_simulationStatusLabel.Location = new Point(10, 25);
|
||||
_simulationStatusLabel.Size = new Size(210, 15);
|
||||
|
||||
_agvCountLabel = new Label();
|
||||
_agvCountLabel.Text = "AGV 수: 0";
|
||||
_agvCountLabel.Location = new Point(10, 45);
|
||||
_agvCountLabel.Size = new Size(210, 15);
|
||||
|
||||
_pathLengthLabel = new Label();
|
||||
_pathLengthLabel.Text = "경로 길이: -";
|
||||
_pathLengthLabel.Location = new Point(10, 65);
|
||||
_pathLengthLabel.Size = new Size(210, 15);
|
||||
|
||||
_statusGroup.Controls.AddRange(new Control[] {
|
||||
_simulationStatusLabel, _agvCountLabel, _pathLengthLabel
|
||||
});
|
||||
|
||||
_controlPanel.Controls.Add(_statusGroup);
|
||||
}
|
||||
|
||||
private void CreateSimulatorCanvas()
|
||||
{
|
||||
_canvasPanel = new Panel();
|
||||
_canvasPanel.Dock = DockStyle.Fill;
|
||||
|
||||
_simulatorCanvas = new SimulatorCanvas();
|
||||
_simulatorCanvas.Dock = DockStyle.Fill;
|
||||
|
||||
_canvasPanel.Controls.Add(_simulatorCanvas);
|
||||
Controls.Add(_canvasPanel);
|
||||
}
|
||||
|
||||
private void SetupLayout()
|
||||
{
|
||||
// Z-Order 설정
|
||||
_canvasPanel.BringToFront();
|
||||
_controlPanel.BringToFront();
|
||||
_toolStrip.BringToFront();
|
||||
_menuStrip.BringToFront();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnOpenMap_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (var openDialog = new OpenFileDialog())
|
||||
{
|
||||
openDialog.Filter = "맵 파일 (*.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 = "시뮬레이션 실행 중";
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnStopSimulation_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!_simulationState.IsRunning)
|
||||
return;
|
||||
|
||||
_simulationState.IsRunning = false;
|
||||
_simulationTimer.Stop();
|
||||
_statusLabel.Text = "시뮬레이션 정지";
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnReset_Click(object sender, EventArgs e)
|
||||
{
|
||||
// 시뮬레이션 정지
|
||||
if (_simulationState.IsRunning)
|
||||
{
|
||||
OnStopSimulation_Click(sender, e);
|
||||
}
|
||||
|
||||
// AGV 초기화
|
||||
_simulatorCanvas.ClearAGVs();
|
||||
_agvList.Clear();
|
||||
|
||||
// 경로 초기화
|
||||
_simulatorCanvas.CurrentPath = null;
|
||||
|
||||
// UI 업데이트
|
||||
UpdateAGVComboBox();
|
||||
UpdateNodeComboBoxes();
|
||||
UpdateUI();
|
||||
|
||||
_statusLabel.Text = "초기화 완료";
|
||||
}
|
||||
|
||||
private void OnFitToMap_Click(object sender, EventArgs e)
|
||||
{
|
||||
_simulatorCanvas.FitToMap();
|
||||
}
|
||||
|
||||
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 (_mapNodes == null || _mapNodes.Count == 0)
|
||||
{
|
||||
MessageBox.Show("먼저 맵을 로드해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var agvId = $"AGV{_agvList.Count + 1:D2}";
|
||||
var startPosition = _mapNodes.First().Position; // 첫 번째 노드에서 시작
|
||||
|
||||
var newAGV = new VirtualAGV(agvId, startPosition);
|
||||
_agvList.Add(newAGV);
|
||||
_simulatorCanvas.AddAGV(newAGV);
|
||||
|
||||
UpdateAGVComboBox();
|
||||
UpdateUI();
|
||||
|
||||
_statusLabel.Text = $"{agvId} 추가됨";
|
||||
}
|
||||
|
||||
private void OnRemoveAGV_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_agvListCombo.SelectedItem == null)
|
||||
return;
|
||||
|
||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
if (selectedAGV != null)
|
||||
{
|
||||
_simulatorCanvas.RemoveAGV(selectedAGV.AgvId);
|
||||
_agvList.Remove(selectedAGV);
|
||||
|
||||
UpdateAGVComboBox();
|
||||
UpdateUI();
|
||||
|
||||
_statusLabel.Text = $"{selectedAGV.AgvId} 제거됨";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAGVList_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnCalculatePath_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
|
||||
{
|
||||
MessageBox.Show("시작 노드와 목표 노드를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var startNode = _startNodeCombo.SelectedItem as MapNode;
|
||||
var targetNode = _targetNodeCombo.SelectedItem as MapNode;
|
||||
|
||||
if (_pathCalculator == null)
|
||||
{
|
||||
_pathCalculator = new PathCalculator(_mapNodes, _nodeResolver);
|
||||
}
|
||||
|
||||
var result = _pathCalculator.CalculatePath(startNode.NodeId, targetNode.NodeId, AgvDirection.Forward);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_simulatorCanvas.CurrentPath = result;
|
||||
_pathLengthLabel.Text = $"경로 길이: {result.TotalDistance:F1}";
|
||||
_statusLabel.Text = $"경로 계산 완료 ({result.CalculationTime}ms)";
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show($"경로를 찾을 수 없습니다:\n{result.ErrorMessage}", "경로 계산 실패",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartPath_Click(object sender, EventArgs e)
|
||||
{
|
||||
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||
if (selectedAGV == null)
|
||||
{
|
||||
MessageBox.Show("AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_simulatorCanvas.CurrentPath == null || !_simulatorCanvas.CurrentPath.Success)
|
||||
{
|
||||
MessageBox.Show("먼저 경로를 계산해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedAGV.StartPath(_simulatorCanvas.CurrentPath, _mapNodes);
|
||||
_statusLabel.Text = $"{selectedAGV.AgvId} 경로 시작";
|
||||
}
|
||||
|
||||
private void OnClearPath_Click(object sender, EventArgs e)
|
||||
{
|
||||
_simulatorCanvas.CurrentPath = null;
|
||||
_pathLengthLabel.Text = "경로 길이: -";
|
||||
_statusLabel.Text = "경로 지움";
|
||||
}
|
||||
|
||||
private void OnSimulationTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
// 시뮬레이션 업데이트는 각 AGV의 내부 타이머에서 처리됨
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void LoadMapFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
|
||||
// 구조체로 직접 역직렬화
|
||||
var mapData = JsonConvert.DeserializeObject<MapFileData>(json);
|
||||
|
||||
if (mapData != null)
|
||||
{
|
||||
_mapNodes = mapData.MapNodes ?? new List<MapNode>();
|
||||
_rfidMappings = mapData.RfidMappings ?? new List<RfidMapping>();
|
||||
}
|
||||
else
|
||||
{
|
||||
_mapNodes = new List<MapNode>();
|
||||
_rfidMappings = new List<RfidMapping>();
|
||||
}
|
||||
|
||||
// NodeResolver 초기화
|
||||
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
|
||||
|
||||
// 시뮬레이터 캔버스에 맵 설정
|
||||
_simulatorCanvas.MapNodes = _mapNodes;
|
||||
|
||||
// UI 업데이트
|
||||
UpdateNodeComboBoxes();
|
||||
UpdateUI();
|
||||
|
||||
// 맵에 맞춤
|
||||
_simulatorCanvas.FitToMap();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"맵 파일 로드 실패: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 맵 파일 데이터 구조체
|
||||
private class MapFileData
|
||||
{
|
||||
public List<MapNode> MapNodes { get; set; }
|
||||
public List<RfidMapping> RfidMappings { get; set; }
|
||||
}
|
||||
|
||||
private void UpdateNodeComboBoxes()
|
||||
{
|
||||
_startNodeCombo.Items.Clear();
|
||||
_targetNodeCombo.Items.Clear();
|
||||
|
||||
if (_mapNodes != null)
|
||||
{
|
||||
foreach (var node in _mapNodes)
|
||||
{
|
||||
_startNodeCombo.Items.Add(node);
|
||||
_targetNodeCombo.Items.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
_startNodeCombo.DisplayMember = "NodeId";
|
||||
_targetNodeCombo.DisplayMember = "NodeId";
|
||||
}
|
||||
|
||||
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;
|
||||
_startPathButton.Enabled = _agvListCombo.SelectedItem != null &&
|
||||
_simulatorCanvas.CurrentPath != null &&
|
||||
_simulatorCanvas.CurrentPath.Success;
|
||||
|
||||
_calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
|
||||
_targetNodeCombo.SelectedItem != null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
61
Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx
Normal file
61
Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
Reference in New Issue
Block a user