From 7567602479284e8ae0be80b79f515b5dcc041992 Mon Sep 17 00:00:00 2001 From: ChiKyun Kim Date: Wed, 10 Sep 2025 17:39:23 +0900 Subject: [PATCH] feat: Add AGV Map Editor and Simulator tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Cs_HMI/AGVCSharp.sln | 38 +- Cs_HMI/AGVMapEditor/AGVMapEditor.csproj | 84 +++ .../Controls/MapCanvas.Designer.cs | 59 ++ Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs | 608 ++++++++++++++++ Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx | 61 ++ .../AGVMapEditor/Forms/MainForm.Designer.cs | 422 +++++++++++ Cs_HMI/AGVMapEditor/Forms/MainForm.cs | 586 +++++++++++++++ Cs_HMI/AGVMapEditor/Forms/MainForm.resx | 67 ++ Cs_HMI/AGVMapEditor/Models/Enums.cs | 64 ++ Cs_HMI/AGVMapEditor/Models/MapNode.cs | 225 ++++++ Cs_HMI/AGVMapEditor/Models/NodeResolver.cs | 308 ++++++++ Cs_HMI/AGVMapEditor/Models/PathCalculator.cs | 469 ++++++++++++ Cs_HMI/AGVMapEditor/Models/PathNode.cs | 144 ++++ Cs_HMI/AGVMapEditor/Models/PathResult.cs | 277 +++++++ Cs_HMI/AGVMapEditor/Models/RfidMapping.cs | 79 ++ Cs_HMI/AGVMapEditor/Program.cs | 26 + .../AGVMapEditor/Properties/AssemblyInfo.cs | 36 + Cs_HMI/AGVMapEditor/packages.config | 4 + Cs_HMI/AGVSimulator/AGVSimulator.csproj | 80 ++ .../Controls/SimulatorCanvas.Designer.cs | 52 ++ .../AGVSimulator/Controls/SimulatorCanvas.cs | 620 ++++++++++++++++ .../Controls/SimulatorCanvas.resx | 119 +++ .../Forms/SimulatorForm.Designer.cs | 64 ++ Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs | 688 ++++++++++++++++++ Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx | 61 ++ Cs_HMI/AGVSimulator/Models/SimulationState.cs | 135 ++++ Cs_HMI/AGVSimulator/Models/VirtualAGV.cs | 482 ++++++++++++ Cs_HMI/AGVSimulator/Program.cs | 32 + .../AGVSimulator/Properties/AssemblyInfo.cs | 36 + Cs_HMI/AGVSimulator/packages.config | 4 + Cs_HMI/CLAUDE.md | 141 ++++ Cs_HMI/TODO.md | 217 ++++++ Cs_HMI/build.bat | 18 + 33 files changed, 6304 insertions(+), 2 deletions(-) create mode 100644 Cs_HMI/AGVMapEditor/AGVMapEditor.csproj create mode 100644 Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs create mode 100644 Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs create mode 100644 Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx create mode 100644 Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs create mode 100644 Cs_HMI/AGVMapEditor/Forms/MainForm.cs create mode 100644 Cs_HMI/AGVMapEditor/Forms/MainForm.resx create mode 100644 Cs_HMI/AGVMapEditor/Models/Enums.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/MapNode.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/NodeResolver.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/PathCalculator.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/PathNode.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/PathResult.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/RfidMapping.cs create mode 100644 Cs_HMI/AGVMapEditor/Program.cs create mode 100644 Cs_HMI/AGVMapEditor/Properties/AssemblyInfo.cs create mode 100644 Cs_HMI/AGVMapEditor/packages.config create mode 100644 Cs_HMI/AGVSimulator/AGVSimulator.csproj create mode 100644 Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.Designer.cs create mode 100644 Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs create mode 100644 Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx create mode 100644 Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs create mode 100644 Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs create mode 100644 Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx create mode 100644 Cs_HMI/AGVSimulator/Models/SimulationState.cs create mode 100644 Cs_HMI/AGVSimulator/Models/VirtualAGV.cs create mode 100644 Cs_HMI/AGVSimulator/Program.cs create mode 100644 Cs_HMI/AGVSimulator/Properties/AssemblyInfo.cs create mode 100644 Cs_HMI/AGVSimulator/packages.config create mode 100644 Cs_HMI/CLAUDE.md create mode 100644 Cs_HMI/TODO.md create mode 100644 Cs_HMI/build.bat diff --git a/Cs_HMI/AGVCSharp.sln b/Cs_HMI/AGVCSharp.sln index c4910f1..4cfe026 100644 --- a/Cs_HMI/AGVCSharp.sln +++ b/Cs_HMI/AGVCSharp.sln @@ -1,7 +1,7 @@ ๏ปฟ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Express 15 for Windows Desktop -VisualStudioVersion = 15.0.28307.1000 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36310.24 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sub", "Sub", "{C423C39A-44E7-4F09-B2F7-7943975FF948}" EndProject @@ -27,6 +27,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ENIGProtocol", "SubProject\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGV4", "Project\AGV4.csproj", "{D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVMapEditor", "AGVMapEditor\AGVMapEditor.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVSimulator", "AGVSimulator\AGVSimulator.csproj", "{B2C3D4E5-0000-0000-0000-000000000000}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "์†”๋ฃจ์…˜ ํ•ญ๋ชฉ", "์†”๋ฃจ์…˜ ํ•ญ๋ชฉ", "{2A3A057F-5D22-31FD-628C-DF5EF75AEF1E}" + ProjectSection(SolutionItems) = preProject + build.bat = build.bat + CLAUDE.md = CLAUDE.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -157,6 +167,30 @@ Global {D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}.Release|x64.Build.0 = Release|Any CPU {D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}.Release|x86.ActiveCfg = Release|x86 {D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}.Release|x86.Build.0 = Release|x86 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Debug|x64.Build.0 = Debug|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Debug|x86.Build.0 = Debug|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Release|x64.ActiveCfg = Release|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Release|x64.Build.0 = Release|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Release|x86.ActiveCfg = Release|Any CPU + {B2C3D4E5-0000-0000-0000-000000000000}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj b/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj new file mode 100644 index 0000000..3440faf --- /dev/null +++ b/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj @@ -0,0 +1,84 @@ + + + + + Debug + AnyCPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + WinExe + AGVMapEditor + AGVMapEditor + v4.8 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + Form + + + MainForm.cs + + + UserControl + + + MapCanvas.cs + + + + + + + MainForm.cs + + + MapCanvas.cs + + + + + + + + + + + \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs new file mode 100644 index 0000000..da7a559 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs @@ -0,0 +1,59 @@ +namespace AGVMapEditor.Controls +{ + partial class MapCanvas + { + /// + /// ํ•„์ˆ˜ ๋””์ž์ด๋„ˆ ๋ณ€์ˆ˜์ž…๋‹ˆ๋‹ค. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// ์‚ฌ์šฉ ์ค‘์ธ ๋ชจ๋“  ๋ฆฌ์†Œ์Šค๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + /// + /// ๊ด€๋ฆฌ๋˜๋Š” ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•˜๋ฉด true์ด๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false์ž…๋‹ˆ๋‹ค. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (components != null) + { + components.Dispose(); + } + + // MapCanvas์˜ ์ถ”๊ฐ€ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ + _normalNodeBrush?.Dispose(); + _rotationNodeBrush?.Dispose(); + _dockingNodeBrush?.Dispose(); + _chargingNodeBrush?.Dispose(); + _selectedNodeBrush?.Dispose(); + _hoveredNodeBrush?.Dispose(); + _connectionPen?.Dispose(); + _gridPen?.Dispose(); + } + base.Dispose(disposing); + } + + #region ๊ตฌ์„ฑ ์š”์†Œ ๋””์ž์ด๋„ˆ์—์„œ ์ƒ์„ฑํ•œ ์ฝ”๋“œ + + /// + /// ๋””์ž์ด๋„ˆ ์ง€์›์— ํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + /// ์ด ๋ฉ”์„œ๋“œ์˜ ๋‚ด์šฉ์„ ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ๋กœ ์ˆ˜์ •ํ•˜์ง€ ๋งˆ์„ธ์š”. + /// + private void InitializeComponent() + { + this.SuspendLayout(); + // + // MapCanvas + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.White; + this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.Name = "MapCanvas"; + this.ResumeLayout(false); + + } + + #endregion + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs new file mode 100644 index 0000000..3769f22 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs @@ -0,0 +1,608 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Linq; +using System.Windows.Forms; +using AGVMapEditor.Models; + +namespace AGVMapEditor.Controls +{ + /// + /// ๋งต ํŽธ์ง‘์„ ์œ„ํ•œ ๊ทธ๋ž˜ํ”ฝ ์บ”๋ฒ„์Šค ์ปจํŠธ๋กค + /// + public partial class MapCanvas : UserControl + { + #region Constants + + private const int NODE_SIZE = 20; + private const int NODE_RADIUS = NODE_SIZE / 2; + private const int GRID_SIZE = 20; + private const float CONNECTION_WIDTH = 2.0f; + + #endregion + + #region Fields + + private List _nodes; + private MapNode _selectedNode; + private MapNode _hoveredNode; + private bool _isDragging; + private Point _dragOffset; + private Point _lastMousePosition; + + // ๊ทธ๋ฆฌ๋“œ ๋ฐ ์คŒ ๊ด€๋ จ + private bool _showGrid = true; + private float _zoomFactor = 1.0f; + private Point _panOffset = Point.Empty; + + // ๋ธŒ๋Ÿฌ์‰ฌ ๋ฐ ํŽœ + private Brush _normalNodeBrush; + private Brush _rotationNodeBrush; + private Brush _dockingNodeBrush; + private Brush _chargingNodeBrush; + private Brush _selectedNodeBrush; + private Brush _hoveredNodeBrush; + private Pen _connectionPen; + private Pen _gridPen; + + #endregion + + #region Events + + /// + /// ๋…ธ๋“œ๊ฐ€ ์„ ํƒ๋˜์—ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ + /// + public event EventHandler NodeSelected; + + /// + /// ๋…ธ๋“œ๊ฐ€ ์ด๋™๋˜์—ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ + /// + public event EventHandler NodeMoved; + + /// + /// ๋ฐฐ๊ฒฝ์ด ํด๋ฆญ๋˜์—ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ + /// + public event EventHandler BackgroundClicked; + + #endregion + + #region Constructor + + public MapCanvas() : this(new List()) + { + } + + public MapCanvas(List nodes) + { + InitializeComponent(); + InitializeGraphics(); + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); + + _nodes = nodes ?? new List(); + + // ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ + this.MouseDown += MapCanvas_MouseDown; + this.MouseMove += MapCanvas_MouseMove; + this.MouseUp += MapCanvas_MouseUp; + this.MouseWheel += MapCanvas_MouseWheel; + this.KeyDown += MapCanvas_KeyDown; + + // ํฌ์ปค์Šค๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ์„ค์ • + this.TabStop = true; + } + + #endregion + + #region Graphics Initialization + + private void InitializeGraphics() + { + _normalNodeBrush = new SolidBrush(Color.Blue); + _rotationNodeBrush = new SolidBrush(Color.Orange); + _dockingNodeBrush = new SolidBrush(Color.Green); + _chargingNodeBrush = new SolidBrush(Color.Red); + _selectedNodeBrush = new SolidBrush(Color.Yellow); + _hoveredNodeBrush = new SolidBrush(Color.LightBlue); + + _connectionPen = new Pen(Color.Gray, CONNECTION_WIDTH); + _gridPen = new Pen(Color.LightGray, 1.0f) { DashStyle = DashStyle.Dot }; + } + + #endregion + + #region Properties + + /// + /// ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ ์—ฌ๋ถ€ + /// + public bool ShowGrid + { + get { return _showGrid; } + set + { + _showGrid = value; + Invalidate(); + } + } + + /// + /// ์คŒ ํŒฉํ„ฐ + /// + public float ZoomFactor + { + get { return _zoomFactor; } + set + { + _zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value)); + Invalidate(); + } + } + + /// + /// ์„ ํƒ๋œ ๋…ธ๋“œ + /// + public MapNode SelectedNode + { + get { return _selectedNode; } + set + { + if (_selectedNode != value) + { + _selectedNode = value; + Invalidate(); + } + } + } + + #endregion + + #region Mouse Events + + private void MapCanvas_MouseDown(object sender, MouseEventArgs e) + { + this.Focus(); // ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๊ธฐ ์œ„ํ•ด ํฌ์ปค์Šค ์„ค์ • + + var worldPoint = ScreenToWorld(e.Location); + var hitNode = GetNodeAt(worldPoint); + + if (e.Button == MouseButtons.Left) + { + if (hitNode != null) + { + // ๋…ธ๋“œ ์„ ํƒ ๋ฐ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ + SelectNode(hitNode); + _isDragging = true; + _dragOffset = new Point(worldPoint.X - hitNode.Position.X, worldPoint.Y - hitNode.Position.Y); + } + else + { + // ๋ฐฐ๊ฒฝ ํด๋ฆญ + SelectNode(null); + BackgroundClicked?.Invoke(this, worldPoint); + } + } + else if (e.Button == MouseButtons.Right) + { + // ์šฐํด๋ฆญ ๋ฉ”๋‰ด (์ถ”ํ›„ ๊ตฌํ˜„) + ShowContextMenu(worldPoint, hitNode); + } + + _lastMousePosition = e.Location; + } + + private void MapCanvas_MouseMove(object sender, MouseEventArgs e) + { + var worldPoint = ScreenToWorld(e.Location); + + if (_isDragging && _selectedNode != null) + { + // ๋…ธ๋“œ ๋“œ๋ž˜๊ทธ + var newPosition = new Point(worldPoint.X - _dragOffset.X, worldPoint.Y - _dragOffset.Y); + + // ๊ทธ๋ฆฌ๋“œ์— ๋งž์ถค (Ctrl ํ‚ค๋ฅผ ๋ˆ„๋ฅด์ง€ ์•Š์€ ๊ฒฝ์šฐ) + if (!ModifierKeys.HasFlag(Keys.Control)) + { + newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE; + newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE; + } + + _selectedNode.Position = newPosition; + Invalidate(); + } + else if (e.Button == MouseButtons.Middle || (e.Button == MouseButtons.Left && ModifierKeys.HasFlag(Keys.Space))) + { + // ํŒฌ (ํ™”๋ฉด ์ด๋™) + var deltaX = e.X - _lastMousePosition.X; + var deltaY = e.Y - _lastMousePosition.Y; + _panOffset.X += deltaX; + _panOffset.Y += deltaY; + Invalidate(); + } + else + { + // ํ˜ธ๋ฒ„ ํšจ๊ณผ + var hitNode = GetNodeAt(worldPoint); + if (_hoveredNode != hitNode) + { + _hoveredNode = hitNode; + Invalidate(); + } + } + + _lastMousePosition = e.Location; + } + + private void MapCanvas_MouseUp(object sender, MouseEventArgs e) + { + if (_isDragging && _selectedNode != null) + { + NodeMoved?.Invoke(this, _selectedNode); + } + + _isDragging = false; + } + + private void MapCanvas_MouseWheel(object sender, MouseEventArgs e) + { + // ์คŒ + var delta = e.Delta > 0 ? 1.1f : 0.9f; + ZoomFactor *= delta; + } + + #endregion + + #region Keyboard Events + + private void MapCanvas_KeyDown(object sender, KeyEventArgs e) + { + switch (e.KeyCode) + { + case Keys.Delete: + // ์„ ํƒ๋œ ๋…ธ๋“œ ์‚ญ์ œ (๋ฉ”์ธ ํผ์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ด๋ฒคํŠธ ๋ฐœ์ƒ) + if (_selectedNode != null) + { + // ์‚ญ์ œ ํ™•์ธ ํ›„ ์ฒ˜๋ฆฌ๋Š” ๋ฉ”์ธ ํผ์—์„œ + } + break; + + case Keys.G: + // ๊ทธ๋ฆฌ๋“œ ํ† ๊ธ€ + ShowGrid = !ShowGrid; + break; + + case Keys.Home: + // ๋ทฐ ๋ฆฌ์…‹ + ZoomFactor = 1.0f; + _panOffset = Point.Empty; + Invalidate(); + break; + } + } + + #endregion + + #region Coordinate Conversion + + /// + /// ์Šคํฌ๋ฆฐ ์ขŒํ‘œ๋ฅผ ์›”๋“œ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ + /// + private Point ScreenToWorld(Point screenPoint) + { + var worldX = (int)((screenPoint.X - _panOffset.X) / _zoomFactor); + var worldY = (int)((screenPoint.Y - _panOffset.Y) / _zoomFactor); + return new Point(worldX, worldY); + } + + /// + /// ์›”๋“œ ์ขŒํ‘œ๋ฅผ ์Šคํฌ๋ฆฐ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ + /// + private Point WorldToScreen(Point worldPoint) + { + var screenX = (int)(worldPoint.X * _zoomFactor + _panOffset.X); + var screenY = (int)(worldPoint.Y * _zoomFactor + _panOffset.Y); + return new Point(screenX, screenY); + } + + #endregion + + #region Node Management + + /// + /// ์ง€์ •๋œ ์œ„์น˜์˜ ๋…ธ๋“œ ๊ฒ€์ƒ‰ + /// + private MapNode GetNodeAt(Point worldPoint) + { + foreach (var node in _nodes) + { + var distance = Math.Sqrt(Math.Pow(worldPoint.X - node.Position.X, 2) + Math.Pow(worldPoint.Y - node.Position.Y, 2)); + if (distance <= NODE_RADIUS) + { + return node; + } + } + return null; + } + + /// + /// ๋…ธ๋“œ ์„ ํƒ + /// + private void SelectNode(MapNode node) + { + if (_selectedNode != node) + { + _selectedNode = node; + NodeSelected?.Invoke(this, node); + Invalidate(); + } + } + + /// + /// ์šฐํด๋ฆญ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ํ‘œ์‹œ + /// + private void ShowContextMenu(Point worldPoint, MapNode node) + { + var contextMenu = new ContextMenuStrip(); + + if (node != null) + { + contextMenu.Items.Add("๋…ธ๋“œ ์‚ญ์ œ", null, (s, e) => { /* ์‚ญ์ œ ์ฒ˜๋ฆฌ */ }); + contextMenu.Items.Add(new ToolStripSeparator()); + contextMenu.Items.Add("์ผ๋ฐ˜ ๋…ธ๋“œ๋กœ ๋ณ€๊ฒฝ", null, (s, e) => ChangeNodeType(node, NodeType.Normal)); + contextMenu.Items.Add("ํšŒ์ „ ์ง€์ ์œผ๋กœ ๋ณ€๊ฒฝ", null, (s, e) => ChangeNodeType(node, NodeType.Rotation)); + contextMenu.Items.Add("๋„ํ‚น ์Šคํ…Œ์ด์…˜์œผ๋กœ ๋ณ€๊ฒฝ", null, (s, e) => ChangeNodeType(node, NodeType.Docking)); + contextMenu.Items.Add("์ถฉ์ „ ์Šคํ…Œ์ด์…˜์œผ๋กœ ๋ณ€๊ฒฝ", null, (s, e) => ChangeNodeType(node, NodeType.Charging)); + } + else + { + contextMenu.Items.Add("์ƒˆ ๋…ธ๋“œ ์ถ”๊ฐ€", null, (s, e) => { /* ๋…ธ๋“œ ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ */ }); + } + + var screenPoint = WorldToScreen(worldPoint); + contextMenu.Show(this, screenPoint); + } + + /// + /// ๋…ธ๋“œ ํƒ€์ž… ๋ณ€๊ฒฝ + /// + private void ChangeNodeType(MapNode node, NodeType newType) + { + node.Type = newType; + node.SetDefaultColorByType(newType); + Invalidate(); + } + + #endregion + + #region Painting + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + + // ๋ณ€ํ™˜ ๋งคํŠธ๋ฆญ์Šค ์ ์šฉ + g.ResetTransform(); + g.TranslateTransform(_panOffset.X, _panOffset.Y); + g.ScaleTransform(_zoomFactor, _zoomFactor); + + // ๋ฐฐ๊ฒฝ ๊ทธ๋ฆฌ๋“œ ๊ทธ๋ฆฌ๊ธฐ + if (_showGrid) + { + DrawGrid(g); + } + + // ์—ฐ๊ฒฐ์„  ๊ทธ๋ฆฌ๊ธฐ + DrawConnections(g); + + // ๋…ธ๋“œ ๊ทธ๋ฆฌ๊ธฐ + DrawNodes(g); + + // ์„ ํƒ๋œ ๋…ธ๋“œ์˜ ์—ฐ๊ฒฐ ์ •๋ณด ๊ฐ•์กฐ ํ‘œ์‹œ + if (_selectedNode != null) + { + DrawSelectedNodeConnections(g); + } + } + + private void DrawGrid(Graphics g) + { + var bounds = GetVisibleWorldBounds(); + + // ์ˆ˜์ง์„  + for (int x = (bounds.Left / GRID_SIZE) * GRID_SIZE; x <= bounds.Right; x += GRID_SIZE) + { + g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom); + } + + // ์ˆ˜ํ‰์„  + for (int y = (bounds.Top / GRID_SIZE) * GRID_SIZE; y <= bounds.Bottom; y += GRID_SIZE) + { + g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y); + } + } + + private void DrawConnections(Graphics g) + { + foreach (var node in _nodes) + { + foreach (var connectedNodeId in node.ConnectedNodes) + { + var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId); + if (connectedNode != null) + { + g.DrawLine(_connectionPen, node.Position, connectedNode.Position); + } + } + } + } + + private void DrawNodes(Graphics g) + { + foreach (var node in _nodes) + { + DrawNode(g, node); + } + } + + private void DrawNode(Graphics g, MapNode node) + { + var brush = GetNodeBrush(node); + var rect = new Rectangle( + node.Position.X - NODE_RADIUS, + node.Position.Y - NODE_RADIUS, + NODE_SIZE, + NODE_SIZE); + + // ๋…ธ๋“œ ๊ทธ๋ฆฌ๊ธฐ + switch (node.Type) + { + case NodeType.Normal: + g.FillEllipse(brush, rect); + break; + case NodeType.Rotation: + g.FillRectangle(brush, rect); + break; + case NodeType.Docking: + g.FillRectangle(brush, rect); + // ๋„ํ‚น ๋ฐฉํ–ฅ ํ‘œ์‹œ + DrawDockingDirection(g, node); + break; + case NodeType.Charging: + g.FillEllipse(brush, rect); + // ์ถฉ์ „ ํ‘œ์‹œ (+) + DrawChargingSymbol(g, node); + break; + } + + // ๋…ธ๋“œ ํ…Œ๋‘๋ฆฌ + g.DrawEllipse(Pens.Black, rect); + + // ๋…ธ๋“œ ์ด๋ฆ„ ํ‘œ์‹œ + if (_zoomFactor > 0.5f) // ์คŒ์ด ์ถฉ๋ถ„ํžˆ ํฐ ๊ฒฝ์šฐ๋งŒ ํ…์ŠคํŠธ ํ‘œ์‹œ + { + var font = new Font("Arial", 8 * _zoomFactor); + var textRect = new RectangleF( + node.Position.X - 30, + node.Position.Y + NODE_RADIUS + 2, + 60, 15); + + var format = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Near + }; + + g.DrawString(node.Name, font, Brushes.Black, textRect, format); + } + } + + private Brush GetNodeBrush(MapNode node) + { + if (node == _selectedNode) + return _selectedNodeBrush; + + if (node == _hoveredNode) + return _hoveredNodeBrush; + + switch (node.Type) + { + case NodeType.Normal: + return _normalNodeBrush; + case NodeType.Rotation: + return _rotationNodeBrush; + case NodeType.Docking: + return _dockingNodeBrush; + case NodeType.Charging: + return _chargingNodeBrush; + default: + return _normalNodeBrush; + } + } + + private void DrawDockingDirection(Graphics g, MapNode node) + { + if (node.DockDirection == null) + return; + + var arrowSize = 8; + var arrowPen = new Pen(Color.White, 2); + + Point arrowStart, arrowEnd; + + if (node.DockDirection == DockingDirection.Forward) + { + arrowStart = new Point(node.Position.X - arrowSize/2, node.Position.Y); + arrowEnd = new Point(node.Position.X + arrowSize/2, node.Position.Y); + } + else // Backward + { + arrowStart = new Point(node.Position.X + arrowSize/2, node.Position.Y); + arrowEnd = new Point(node.Position.X - arrowSize/2, node.Position.Y); + } + + g.DrawLine(arrowPen, arrowStart, arrowEnd); + + // ํ™”์‚ดํ‘œ ๋จธ๋ฆฌ + var headSize = 3; + var headPoints = new Point[] + { + arrowEnd, + new Point(arrowEnd.X - headSize, arrowEnd.Y - headSize), + new Point(arrowEnd.X - headSize, arrowEnd.Y + headSize) + }; + g.FillPolygon(Brushes.White, headPoints); + } + + private void DrawChargingSymbol(Graphics g, MapNode node) + { + var symbolSize = 6; + var symbolPen = new Pen(Color.White, 2); + + // + ๋ชจ์–‘ + g.DrawLine(symbolPen, + node.Position.X - symbolSize/2, node.Position.Y, + node.Position.X + symbolSize/2, node.Position.Y); + g.DrawLine(symbolPen, + node.Position.X, node.Position.Y - symbolSize/2, + node.Position.X, node.Position.Y + symbolSize/2); + } + + private void DrawSelectedNodeConnections(Graphics g) + { + var highlightPen = new Pen(Color.Yellow, CONNECTION_WIDTH + 2); + + foreach (var connectedNodeId in _selectedNode.ConnectedNodes) + { + var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId); + if (connectedNode != null) + { + g.DrawLine(highlightPen, _selectedNode.Position, connectedNode.Position); + } + } + } + + #endregion + + #region Helper Methods + + /// + /// ํ˜„์žฌ ๋ณด์ด๋Š” ์›”๋“œ ์˜์—ญ ๊ณ„์‚ฐ + /// + private Rectangle GetVisibleWorldBounds() + { + var topLeft = ScreenToWorld(new Point(0, 0)); + var bottomRight = ScreenToWorld(new Point(Width, Height)); + + return new Rectangle( + topLeft.X, topLeft.Y, + bottomRight.X - topLeft.X, + bottomRight.Y - topLeft.Y); + } + + #endregion + + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx new file mode 100644 index 0000000..b45c916 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs b/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs new file mode 100644 index 0000000..55ef0af --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs @@ -0,0 +1,422 @@ +namespace AGVMapEditor.Forms +{ + partial class MainForm + { + /// + /// ํ•„์ˆ˜ ๋””์ž์ด๋„ˆ ๋ณ€์ˆ˜์ž…๋‹ˆ๋‹ค. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// ์‚ฌ์šฉ ์ค‘์ธ ๋ชจ๋“  ๋ฆฌ์†Œ์Šค๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + /// + /// ๊ด€๋ฆฌ๋˜๋Š” ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•˜๋ฉด true์ด๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false์ž…๋‹ˆ๋‹ค. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form ๋””์ž์ด๋„ˆ์—์„œ ์ƒ์„ฑํ•œ ์ฝ”๋“œ + + /// + /// ๋””์ž์ด๋„ˆ ์ง€์›์— ํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + /// ์ด ๋ฉ”์„œ๋“œ์˜ ๋‚ด์šฉ์„ ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ๋กœ ์ˆ˜์ •ํ•˜์ง€ ๋งˆ์„ธ์š”. + /// + private void InitializeComponent() + { + this.menuStrip1 = new System.Windows.Forms.MenuStrip(); + this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.newToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.openToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); + this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); + this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.statusStrip1 = new System.Windows.Forms.StatusStrip(); + this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel(); + this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.tabControl1 = new System.Windows.Forms.TabControl(); + this.tabPageNodes = new System.Windows.Forms.TabPage(); + this.btnRemoveConnection = new System.Windows.Forms.Button(); + this.btnAddConnection = new System.Windows.Forms.Button(); + this.btnDeleteNode = new System.Windows.Forms.Button(); + this.btnAddNode = new System.Windows.Forms.Button(); + this.listBoxNodes = new System.Windows.Forms.ListBox(); + this.label1 = new System.Windows.Forms.Label(); + this.tabPageRfid = new System.Windows.Forms.TabPage(); + this.btnDeleteRfidMapping = new System.Windows.Forms.Button(); + this.btnAddRfidMapping = new System.Windows.Forms.Button(); + this.listBoxRfidMappings = new System.Windows.Forms.ListBox(); + this.label2 = new System.Windows.Forms.Label(); + this.tabPageProperties = new System.Windows.Forms.TabPage(); + this.labelSelectedNode = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); + this.menuStrip1.SuspendLayout(); + this.statusStrip1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + this.tabControl1.SuspendLayout(); + this.tabPageNodes.SuspendLayout(); + this.tabPageRfid.SuspendLayout(); + this.tabPageProperties.SuspendLayout(); + this.SuspendLayout(); + // + // menuStrip1 + // + this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.fileToolStripMenuItem}); + this.menuStrip1.Location = new System.Drawing.Point(0, 0); + this.menuStrip1.Name = "menuStrip1"; + this.menuStrip1.Size = new System.Drawing.Size(1200, 24); + this.menuStrip1.TabIndex = 0; + this.menuStrip1.Text = "menuStrip1"; + // + // fileToolStripMenuItem + // + this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.newToolStripMenuItem, + this.openToolStripMenuItem, + this.toolStripSeparator1, + this.saveToolStripMenuItem, + this.saveAsToolStripMenuItem, + this.toolStripSeparator2, + this.exitToolStripMenuItem}); + this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; + this.fileToolStripMenuItem.Size = new System.Drawing.Size(57, 20); + this.fileToolStripMenuItem.Text = "ํŒŒ์ผ(&F)"; + // + // newToolStripMenuItem + // + this.newToolStripMenuItem.Name = "newToolStripMenuItem"; + this.newToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.N))); + this.newToolStripMenuItem.Size = new System.Drawing.Size(180, 22); + this.newToolStripMenuItem.Text = "์ƒˆ๋กœ ๋งŒ๋“ค๊ธฐ(&N)"; + this.newToolStripMenuItem.Click += new System.EventHandler(this.newToolStripMenuItem_Click); + // + // openToolStripMenuItem + // + this.openToolStripMenuItem.Name = "openToolStripMenuItem"; + this.openToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O))); + this.openToolStripMenuItem.Size = new System.Drawing.Size(180, 22); + this.openToolStripMenuItem.Text = "์—ด๊ธฐ(&O)"; + this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click); + // + // toolStripSeparator1 + // + this.toolStripSeparator1.Name = "toolStripSeparator1"; + this.toolStripSeparator1.Size = new System.Drawing.Size(177, 6); + // + // saveToolStripMenuItem + // + this.saveToolStripMenuItem.Name = "saveToolStripMenuItem"; + this.saveToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S))); + this.saveToolStripMenuItem.Size = new System.Drawing.Size(180, 22); + this.saveToolStripMenuItem.Text = "์ €์žฅ(&S)"; + this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click); + // + // saveAsToolStripMenuItem + // + this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem"; + this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(180, 22); + this.saveAsToolStripMenuItem.Text = "๋‹ค๋ฅธ ์ด๋ฆ„์œผ๋กœ ์ €์žฅ(&A)"; + this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click); + // + // toolStripSeparator2 + // + this.toolStripSeparator2.Name = "toolStripSeparator2"; + this.toolStripSeparator2.Size = new System.Drawing.Size(177, 6); + // + // exitToolStripMenuItem + // + this.exitToolStripMenuItem.Name = "exitToolStripMenuItem"; + this.exitToolStripMenuItem.Size = new System.Drawing.Size(180, 22); + this.exitToolStripMenuItem.Text = "์ข…๋ฃŒ(&X)"; + this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click); + // + // statusStrip1 + // + this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.toolStripStatusLabel1}); + this.statusStrip1.Location = new System.Drawing.Point(0, 751); + this.statusStrip1.Name = "statusStrip1"; + this.statusStrip1.Size = new System.Drawing.Size(1200, 22); + this.statusStrip1.TabIndex = 1; + this.statusStrip1.Text = "statusStrip1"; + // + // toolStripStatusLabel1 + // + this.toolStripStatusLabel1.Name = "toolStripStatusLabel1"; + this.toolStripStatusLabel1.Size = new System.Drawing.Size(39, 17); + this.toolStripStatusLabel1.Text = "Ready"; + // + // splitContainer1 + // + this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer1.Location = new System.Drawing.Point(0, 24); + this.splitContainer1.Name = "splitContainer1"; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.tabControl1); + this.splitContainer1.Panel1MinSize = 300; + this.splitContainer1.Size = new System.Drawing.Size(1200, 727); + this.splitContainer1.SplitterDistance = 300; + this.splitContainer1.TabIndex = 2; + // + // tabControl1 + // + this.tabControl1.Controls.Add(this.tabPageNodes); + this.tabControl1.Controls.Add(this.tabPageRfid); + this.tabControl1.Controls.Add(this.tabPageProperties); + this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill; + this.tabControl1.Location = new System.Drawing.Point(0, 0); + this.tabControl1.Name = "tabControl1"; + this.tabControl1.SelectedIndex = 0; + this.tabControl1.Size = new System.Drawing.Size(300, 727); + this.tabControl1.TabIndex = 0; + // + // tabPageNodes + // + this.tabPageNodes.Controls.Add(this.btnRemoveConnection); + this.tabPageNodes.Controls.Add(this.btnAddConnection); + this.tabPageNodes.Controls.Add(this.btnDeleteNode); + this.tabPageNodes.Controls.Add(this.btnAddNode); + this.tabPageNodes.Controls.Add(this.listBoxNodes); + this.tabPageNodes.Controls.Add(this.label1); + this.tabPageNodes.Location = new System.Drawing.Point(4, 22); + this.tabPageNodes.Name = "tabPageNodes"; + this.tabPageNodes.Padding = new System.Windows.Forms.Padding(3); + this.tabPageNodes.Size = new System.Drawing.Size(292, 701); + this.tabPageNodes.TabIndex = 0; + this.tabPageNodes.Text = "๋…ธ๋“œ ๊ด€๋ฆฌ"; + this.tabPageNodes.UseVisualStyleBackColor = true; + // + // btnRemoveConnection + // + this.btnRemoveConnection.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.btnRemoveConnection.Location = new System.Drawing.Point(6, 637); + this.btnRemoveConnection.Name = "btnRemoveConnection"; + this.btnRemoveConnection.Size = new System.Drawing.Size(280, 25); + this.btnRemoveConnection.TabIndex = 5; + this.btnRemoveConnection.Text = "์—ฐ๊ฒฐ ์ œ๊ฑฐ"; + this.btnRemoveConnection.UseVisualStyleBackColor = true; + this.btnRemoveConnection.Click += new System.EventHandler(this.btnRemoveConnection_Click); + // + // btnAddConnection + // + this.btnAddConnection.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.btnAddConnection.Location = new System.Drawing.Point(6, 606); + this.btnAddConnection.Name = "btnAddConnection"; + this.btnAddConnection.Size = new System.Drawing.Size(280, 25); + this.btnAddConnection.TabIndex = 4; + this.btnAddConnection.Text = "์—ฐ๊ฒฐ ์ถ”๊ฐ€"; + this.btnAddConnection.UseVisualStyleBackColor = true; + this.btnAddConnection.Click += new System.EventHandler(this.btnAddConnection_Click); + // + // btnDeleteNode + // + this.btnDeleteNode.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.btnDeleteNode.Location = new System.Drawing.Point(148, 670); + this.btnDeleteNode.Name = "btnDeleteNode"; + this.btnDeleteNode.Size = new System.Drawing.Size(138, 25); + this.btnDeleteNode.TabIndex = 3; + this.btnDeleteNode.Text = "๋…ธ๋“œ ์‚ญ์ œ"; + this.btnDeleteNode.UseVisualStyleBackColor = true; + this.btnDeleteNode.Click += new System.EventHandler(this.btnDeleteNode_Click); + // + // btnAddNode + // + this.btnAddNode.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.btnAddNode.Location = new System.Drawing.Point(6, 670); + this.btnAddNode.Name = "btnAddNode"; + this.btnAddNode.Size = new System.Drawing.Size(138, 25); + this.btnAddNode.TabIndex = 2; + this.btnAddNode.Text = "๋…ธ๋“œ ์ถ”๊ฐ€"; + this.btnAddNode.UseVisualStyleBackColor = true; + this.btnAddNode.Click += new System.EventHandler(this.btnAddNode_Click); + // + // listBoxNodes + // + this.listBoxNodes.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.listBoxNodes.FormattingEnabled = true; + this.listBoxNodes.ItemHeight = 12; + this.listBoxNodes.Location = new System.Drawing.Point(6, 25); + this.listBoxNodes.Name = "listBoxNodes"; + this.listBoxNodes.Size = new System.Drawing.Size(280, 568); + this.listBoxNodes.TabIndex = 1; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(6, 6); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(53, 12); + this.label1.TabIndex = 0; + this.label1.Text = "๋…ธ๋“œ ๋ชฉ๋ก"; + // + // tabPageRfid + // + this.tabPageRfid.Controls.Add(this.btnDeleteRfidMapping); + this.tabPageRfid.Controls.Add(this.btnAddRfidMapping); + this.tabPageRfid.Controls.Add(this.listBoxRfidMappings); + this.tabPageRfid.Controls.Add(this.label2); + this.tabPageRfid.Location = new System.Drawing.Point(4, 22); + this.tabPageRfid.Name = "tabPageRfid"; + this.tabPageRfid.Padding = new System.Windows.Forms.Padding(3); + this.tabPageRfid.Size = new System.Drawing.Size(292, 701); + this.tabPageRfid.TabIndex = 1; + this.tabPageRfid.Text = "RFID ๋งคํ•‘"; + this.tabPageRfid.UseVisualStyleBackColor = true; + // + // btnDeleteRfidMapping + // + this.btnDeleteRfidMapping.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.btnDeleteRfidMapping.Location = new System.Drawing.Point(148, 670); + this.btnDeleteRfidMapping.Name = "btnDeleteRfidMapping"; + this.btnDeleteRfidMapping.Size = new System.Drawing.Size(138, 25); + this.btnDeleteRfidMapping.TabIndex = 3; + this.btnDeleteRfidMapping.Text = "๋งคํ•‘ ์‚ญ์ œ"; + this.btnDeleteRfidMapping.UseVisualStyleBackColor = true; + this.btnDeleteRfidMapping.Click += new System.EventHandler(this.btnDeleteRfidMapping_Click); + // + // btnAddRfidMapping + // + this.btnAddRfidMapping.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.btnAddRfidMapping.Location = new System.Drawing.Point(6, 670); + this.btnAddRfidMapping.Name = "btnAddRfidMapping"; + this.btnAddRfidMapping.Size = new System.Drawing.Size(138, 25); + this.btnAddRfidMapping.TabIndex = 2; + this.btnAddRfidMapping.Text = "๋งคํ•‘ ์ถ”๊ฐ€"; + this.btnAddRfidMapping.UseVisualStyleBackColor = true; + this.btnAddRfidMapping.Click += new System.EventHandler(this.btnAddRfidMapping_Click); + // + // listBoxRfidMappings + // + this.listBoxRfidMappings.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.listBoxRfidMappings.FormattingEnabled = true; + this.listBoxRfidMappings.ItemHeight = 12; + this.listBoxRfidMappings.Location = new System.Drawing.Point(6, 25); + this.listBoxRfidMappings.Name = "listBoxRfidMappings"; + this.listBoxRfidMappings.Size = new System.Drawing.Size(280, 628); + this.listBoxRfidMappings.TabIndex = 1; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(6, 6); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(77, 12); + this.label2.TabIndex = 0; + this.label2.Text = "RFID ๋งคํ•‘ ๋ชฉ๋ก"; + // + // tabPageProperties + // + this.tabPageProperties.Controls.Add(this.labelSelectedNode); + this.tabPageProperties.Controls.Add(this.label3); + this.tabPageProperties.Location = new System.Drawing.Point(4, 22); + this.tabPageProperties.Name = "tabPageProperties"; + this.tabPageProperties.Size = new System.Drawing.Size(292, 701); + this.tabPageProperties.TabIndex = 2; + this.tabPageProperties.Text = "์†์„ฑ"; + this.tabPageProperties.UseVisualStyleBackColor = true; + // + // labelSelectedNode + // + this.labelSelectedNode.AutoSize = true; + this.labelSelectedNode.Location = new System.Drawing.Point(8, 35); + this.labelSelectedNode.Name = "labelSelectedNode"; + this.labelSelectedNode.Size = new System.Drawing.Size(93, 12); + this.labelSelectedNode.TabIndex = 1; + this.labelSelectedNode.Text = "์„ ํƒ๋œ ๋…ธ๋“œ: ์—†์Œ"; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(8, 12); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(53, 12); + this.label3.TabIndex = 0; + this.label3.Text = "๋…ธ๋“œ ์†์„ฑ"; + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(1200, 773); + this.Controls.Add(this.splitContainer1); + this.Controls.Add(this.statusStrip1); + this.Controls.Add(this.menuStrip1); + this.MainMenuStrip = this.menuStrip1; + this.Name = "MainForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "AGV Map Editor"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); + this.Load += new System.EventHandler(this.MainForm_Load); + this.menuStrip1.ResumeLayout(false); + this.menuStrip1.PerformLayout(); + this.statusStrip1.ResumeLayout(false); + this.statusStrip1.PerformLayout(); + this.splitContainer1.Panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); + this.splitContainer1.ResumeLayout(false); + this.tabControl1.ResumeLayout(false); + this.tabPageNodes.ResumeLayout(false); + this.tabPageNodes.PerformLayout(); + this.tabPageRfid.ResumeLayout(false); + this.tabPageRfid.ResumeLayout(false); + this.tabPageRfid.PerformLayout(); + this.tabPageProperties.ResumeLayout(false); + this.tabPageProperties.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.MenuStrip menuStrip1; + private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem openToolStripMenuItem; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator1; + private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem saveAsToolStripMenuItem; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator2; + private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem; + private System.Windows.Forms.StatusStrip statusStrip1; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1; + private System.Windows.Forms.SplitContainer splitContainer1; + private System.Windows.Forms.TabControl tabControl1; + private System.Windows.Forms.TabPage tabPageNodes; + private System.Windows.Forms.TabPage tabPageRfid; + private System.Windows.Forms.Button btnDeleteNode; + private System.Windows.Forms.Button btnAddNode; + private System.Windows.Forms.ListBox listBoxNodes; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Button btnDeleteRfidMapping; + private System.Windows.Forms.Button btnAddRfidMapping; + private System.Windows.Forms.ListBox listBoxRfidMappings; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.TabPage tabPageProperties; + private System.Windows.Forms.Label labelSelectedNode; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Button btnRemoveConnection; + private System.Windows.Forms.Button btnAddConnection; + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.cs b/Cs_HMI/AGVMapEditor/Forms/MainForm.cs new file mode 100644 index 0000000..5c0ab43 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.cs @@ -0,0 +1,586 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms; +using AGVMapEditor.Models; +using AGVMapEditor.Controls; +using Newtonsoft.Json; + +namespace AGVMapEditor.Forms +{ + /// + /// AGV ๋งต ์—๋””ํ„ฐ ๋ฉ”์ธ ํผ + /// + public partial class MainForm : Form + { + #region Fields + + private NodeResolver _nodeResolver; + private List _mapNodes; + private List _rfidMappings; + private MapCanvas _mapCanvas; + + // ํ˜„์žฌ ์„ ํƒ๋œ ๋…ธ๋“œ + private MapNode _selectedNode; + + // ํŒŒ์ผ ๊ฒฝ๋กœ + private string _currentMapFile = string.Empty; + private bool _hasChanges = false; + + #endregion + + #region Constructor + + public MainForm() + { + InitializeComponent(); + InitializeData(); + InitializeMapCanvas(); + UpdateTitle(); + } + + #endregion + + #region Initialization + + private void InitializeData() + { + _mapNodes = new List(); + _rfidMappings = new List(); + _nodeResolver = new NodeResolver(_rfidMappings, _mapNodes); + } + + private void InitializeMapCanvas() + { + _mapCanvas = new MapCanvas(_mapNodes); + _mapCanvas.Dock = DockStyle.Fill; + _mapCanvas.NodeSelected += OnNodeSelected; + _mapCanvas.NodeMoved += OnNodeMoved; + _mapCanvas.BackgroundClicked += OnBackgroundClicked; + + // ์Šคํ”Œ๋ฆฌํ„ฐ ํŒจ๋„์— ๋งต ์บ”๋ฒ„์Šค ์ถ”๊ฐ€ + splitContainer1.Panel2.Controls.Add(_mapCanvas); + } + + #endregion + + #region Event Handlers + + private void MainForm_Load(object sender, EventArgs e) + { + RefreshNodeList(); + RefreshRfidMappingList(); + } + + private void OnNodeSelected(object sender, MapNode node) + { + _selectedNode = node; + UpdateNodeProperties(); + } + + private void OnNodeMoved(object sender, MapNode node) + { + _hasChanges = true; + UpdateTitle(); + RefreshNodeList(); + } + + private void OnBackgroundClicked(object sender, Point location) + { + _selectedNode = null; + ClearNodeProperties(); + } + + #endregion + + #region Menu Event Handlers + + private void newToolStripMenuItem_Click(object sender, EventArgs e) + { + if (CheckSaveChanges()) + { + NewMap(); + } + } + + private void openToolStripMenuItem_Click(object sender, EventArgs e) + { + if (CheckSaveChanges()) + { + OpenMap(); + } + } + + private void saveToolStripMenuItem_Click(object sender, EventArgs e) + { + SaveMap(); + } + + private void saveAsToolStripMenuItem_Click(object sender, EventArgs e) + { + SaveAsMap(); + } + + private void exitToolStripMenuItem_Click(object sender, EventArgs e) + { + this.Close(); + } + + #endregion + + #region Button Event Handlers + + private void btnAddNode_Click(object sender, EventArgs e) + { + AddNewNode(); + } + + private void btnDeleteNode_Click(object sender, EventArgs e) + { + DeleteSelectedNode(); + } + + private void btnAddConnection_Click(object sender, EventArgs e) + { + AddConnectionToSelectedNode(); + } + + private void btnRemoveConnection_Click(object sender, EventArgs e) + { + RemoveConnectionFromSelectedNode(); + } + + private void btnAddRfidMapping_Click(object sender, EventArgs e) + { + AddNewRfidMapping(); + } + + private void btnDeleteRfidMapping_Click(object sender, EventArgs e) + { + DeleteSelectedRfidMapping(); + } + + #endregion + + #region Node Management + + private void AddNewNode() + { + var nodeId = GenerateNodeId(); + var nodeName = $"๋…ธ๋“œ{_mapNodes.Count + 1}"; + var position = new Point(100 + _mapNodes.Count * 50, 100 + _mapNodes.Count * 50); + + var node = new MapNode(nodeId, nodeName, position, NodeType.Normal); + + _mapNodes.Add(node); + _hasChanges = true; + + RefreshNodeList(); + RefreshMapCanvas(); + UpdateTitle(); + } + + private void DeleteSelectedNode() + { + if (_selectedNode == null) + { + MessageBox.Show("์‚ญ์ œํ•  ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜์„ธ์š”.", "์•Œ๋ฆผ", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var result = MessageBox.Show($"๋…ธ๋“œ '{_selectedNode.Name}'๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\n์—ฐ๊ฒฐ๋œ RFID ๋งคํ•‘๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค.", + "์‚ญ์ œ ํ™•์ธ", MessageBoxButtons.YesNo, MessageBoxIcon.Question); + + if (result == DialogResult.Yes) + { + _nodeResolver.RemoveMapNode(_selectedNode.NodeId); + _selectedNode = null; + _hasChanges = true; + + RefreshNodeList(); + RefreshRfidMappingList(); + RefreshMapCanvas(); + ClearNodeProperties(); + UpdateTitle(); + } + } + + private void AddConnectionToSelectedNode() + { + if (_selectedNode == null) + { + MessageBox.Show("์—ฐ๊ฒฐ์„ ์ถ”๊ฐ€ํ•  ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜์„ธ์š”.", "์•Œ๋ฆผ", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // ๋‹ค๋ฅธ ๋…ธ๋“œ๋“ค ์ค‘์—์„œ ์„ ํƒ + var availableNodes = _mapNodes.Where(n => n.NodeId != _selectedNode.NodeId && + !_selectedNode.ConnectedNodes.Contains(n.NodeId)).ToList(); + + if (availableNodes.Count == 0) + { + MessageBox.Show("์—ฐ๊ฒฐ ๊ฐ€๋Šฅํ•œ ๋…ธ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", "์•Œ๋ฆผ", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // ๊ฐ„๋‹จํ•œ ์„ ํƒ ๋‹ค์ด์–ผ๋กœ๊ทธ (์‹ค์ œ๋กœ๋Š” ๋ณ„๋„ ํผ์„ ๋งŒ๋“ค์–ด์•ผ ํ•จ) + var nodeNames = availableNodes.Select(n => $"{n.NodeId}: {n.Name}").ToArray(); + var input = Microsoft.VisualBasic.Interaction.InputBox("์—ฐ๊ฒฐํ•  ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜์„ธ์š”:", "๋…ธ๋“œ ์—ฐ๊ฒฐ", nodeNames[0]); + + var targetNode = availableNodes.FirstOrDefault(n => input.StartsWith(n.NodeId)); + if (targetNode != null) + { + _selectedNode.AddConnection(targetNode.NodeId); + _hasChanges = true; + RefreshMapCanvas(); + UpdateNodeProperties(); + UpdateTitle(); + } + } + + private void RemoveConnectionFromSelectedNode() + { + if (_selectedNode == null || _selectedNode.ConnectedNodes.Count == 0) + { + MessageBox.Show("์—ฐ๊ฒฐ์„ ์ œ๊ฑฐํ•  ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜๊ฑฐ๋‚˜ ์—ฐ๊ฒฐ๋œ ๋…ธ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", "์•Œ๋ฆผ", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // ์—ฐ๊ฒฐ๋œ ๋…ธ๋“œ๋“ค ์ค‘์—์„œ ์„ ํƒ + var connectedNodeNames = _selectedNode.ConnectedNodes.Select(connectedNodeId => + { + var node = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); + return node != null ? $"{node.NodeId}: {node.Name}" : connectedNodeId; + }).ToArray(); + + var input = Microsoft.VisualBasic.Interaction.InputBox("์ œ๊ฑฐํ•  ์—ฐ๊ฒฐ์„ ์„ ํƒํ•˜์„ธ์š”:", "์—ฐ๊ฒฐ ์ œ๊ฑฐ", connectedNodeNames[0]); + + var targetNodeId = input.Split(':')[0]; + if (_selectedNode.ConnectedNodes.Contains(targetNodeId)) + { + _selectedNode.RemoveConnection(targetNodeId); + _hasChanges = true; + RefreshMapCanvas(); + UpdateNodeProperties(); + UpdateTitle(); + } + } + + private string GenerateNodeId() + { + int counter = 1; + string nodeId; + + do + { + nodeId = $"N{counter:D3}"; + counter++; + } while (_mapNodes.Any(n => n.NodeId == nodeId)); + + return nodeId; + } + + #endregion + + #region RFID Mapping Management + + private void AddNewRfidMapping() + { + if (_mapNodes.Count == 0) + { + MessageBox.Show("๋งคํ•‘ํ•  ๋…ธ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ๋…ธ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.", "์•Œ๋ฆผ", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var unmappedNodes = _nodeResolver.GetUnmappedNodes(); + if (unmappedNodes.Count == 0) + { + MessageBox.Show("๋ชจ๋“  ๋…ธ๋“œ๊ฐ€ ์ด๋ฏธ RFID์— ๋งคํ•‘๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.", "์•Œ๋ฆผ", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + // RFID ๊ฐ’ ์ž…๋ ฅ + var rfidValue = Microsoft.VisualBasic.Interaction.InputBox("RFID ๊ฐ’์„ ์ž…๋ ฅํ•˜์„ธ์š”:", "RFID ๋งคํ•‘ ์ถ”๊ฐ€"); + if (string.IsNullOrEmpty(rfidValue)) + return; + + // ๋…ธ๋“œ ์„ ํƒ + var nodeNames = unmappedNodes.Select(n => $"{n.NodeId}: {n.Name}").ToArray(); + var selectedNode = Microsoft.VisualBasic.Interaction.InputBox("๋งคํ•‘ํ•  ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜์„ธ์š”:", "๋…ธ๋“œ ์„ ํƒ", nodeNames[0]); + + var nodeId = selectedNode.Split(':')[0]; + var description = Microsoft.VisualBasic.Interaction.InputBox("์„ค๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š” (์„ ํƒ์‚ฌํ•ญ):", "์„ค๋ช…"); + + if (_nodeResolver.AddRfidMapping(rfidValue, nodeId, description)) + { + _hasChanges = true; + RefreshRfidMappingList(); + UpdateTitle(); + MessageBox.Show("RFID ๋งคํ•‘์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", "์„ฑ๊ณต", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + else + { + MessageBox.Show("RFID ๋งคํ•‘ ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ค‘๋ณต๋œ RFID์ด๊ฑฐ๋‚˜ ๋…ธ๋“œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", "์˜ค๋ฅ˜", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void DeleteSelectedRfidMapping() + { + if (listBoxRfidMappings.SelectedItem == null) + { + MessageBox.Show("์‚ญ์ œํ•  RFID ๋งคํ•‘์„ ์„ ํƒํ•˜์„ธ์š”.", "์•Œ๋ฆผ", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var mapping = listBoxRfidMappings.SelectedItem as RfidMapping; + var result = MessageBox.Show($"RFID ๋งคํ•‘ '{mapping.RfidId} โ†’ {mapping.LogicalNodeId}'๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", + "์‚ญ์ œ ํ™•์ธ", MessageBoxButtons.YesNo, MessageBoxIcon.Question); + + if (result == DialogResult.Yes) + { + if (_nodeResolver.RemoveRfidMapping(mapping.RfidId)) + { + _hasChanges = true; + RefreshRfidMappingList(); + UpdateTitle(); + MessageBox.Show("RFID ๋งคํ•‘์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", "์„ฑ๊ณต", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + else + { + MessageBox.Show("RFID ๋งคํ•‘ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", "์˜ค๋ฅ˜", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + #endregion + + #region File Operations + + private void NewMap() + { + _mapNodes.Clear(); + _rfidMappings.Clear(); + _nodeResolver = new NodeResolver(_rfidMappings, _mapNodes); + _selectedNode = null; + _currentMapFile = string.Empty; + _hasChanges = false; + + RefreshAll(); + UpdateTitle(); + } + + private void OpenMap() + { + var openFileDialog = new OpenFileDialog + { + Filter = "AGV Map Files (*.agvmap)|*.agvmap|JSON Files (*.json)|*.json|All Files (*.*)|*.*", + DefaultExt = "agvmap" + }; + + if (openFileDialog.ShowDialog() == DialogResult.OK) + { + try + { + LoadMapFromFile(openFileDialog.FileName); + _currentMapFile = openFileDialog.FileName; + _hasChanges = false; + RefreshAll(); + UpdateTitle(); + MessageBox.Show("๋งต์ด ์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", "์„ฑ๊ณต", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"๋งต ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {ex.Message}", "์˜ค๋ฅ˜", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void SaveMap() + { + if (string.IsNullOrEmpty(_currentMapFile)) + { + SaveAsMap(); + } + else + { + try + { + SaveMapToFile(_currentMapFile); + _hasChanges = false; + UpdateTitle(); + MessageBox.Show("๋งต์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", "์„ฑ๊ณต", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"๋งต ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {ex.Message}", "์˜ค๋ฅ˜", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void SaveAsMap() + { + var saveFileDialog = new SaveFileDialog + { + Filter = "AGV Map Files (*.agvmap)|*.agvmap|JSON Files (*.json)|*.json", + DefaultExt = "agvmap", + FileName = "NewMap.agvmap" + }; + + if (saveFileDialog.ShowDialog() == DialogResult.OK) + { + try + { + SaveMapToFile(saveFileDialog.FileName); + _currentMapFile = saveFileDialog.FileName; + _hasChanges = false; + UpdateTitle(); + MessageBox.Show("๋งต์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", "์„ฑ๊ณต", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + MessageBox.Show($"๋งต ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {ex.Message}", "์˜ค๋ฅ˜", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void LoadMapFromFile(string filePath) + { + var json = File.ReadAllText(filePath); + var mapData = JsonConvert.DeserializeObject(json); + + _mapNodes = mapData.Nodes ?? new List(); + _rfidMappings = mapData.RfidMappings ?? new List(); + _nodeResolver = new NodeResolver(_rfidMappings, _mapNodes); + } + + private void SaveMapToFile(string filePath) + { + var mapData = new MapData + { + Nodes = _mapNodes, + RfidMappings = _rfidMappings, + CreatedDate = DateTime.Now, + Version = "1.0" + }; + + var json = JsonConvert.SerializeObject(mapData, Formatting.Indented); + File.WriteAllText(filePath, json); + } + + private bool CheckSaveChanges() + { + if (_hasChanges) + { + var result = MessageBox.Show("๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", "๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ", + MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); + + if (result == DialogResult.Yes) + { + SaveMap(); + return !_hasChanges; // ์ €์žฅ์ด ์„ฑ๊ณตํ–ˆ์œผ๋ฉด true + } + else if (result == DialogResult.Cancel) + { + return false; + } + } + + return true; + } + + #endregion + + #region UI Updates + + private void RefreshAll() + { + RefreshNodeList(); + RefreshRfidMappingList(); + RefreshMapCanvas(); + ClearNodeProperties(); + } + + private void RefreshNodeList() + { + listBoxNodes.DataSource = null; + listBoxNodes.DataSource = _mapNodes; + listBoxNodes.DisplayMember = "Name"; + listBoxNodes.ValueMember = "NodeId"; + } + + private void RefreshRfidMappingList() + { + listBoxRfidMappings.DataSource = null; + listBoxRfidMappings.DataSource = _rfidMappings; + listBoxRfidMappings.DisplayMember = "ToString"; + } + + private void RefreshMapCanvas() + { + _mapCanvas?.Invalidate(); + } + + private void UpdateNodeProperties() + { + if (_selectedNode == null) + { + ClearNodeProperties(); + return; + } + + // ์„ ํƒ๋œ ๋…ธ๋“œ์˜ ์†์„ฑ์„ ํ”„๋กœํผํ‹ฐ ํŒจ๋„์— ํ‘œ์‹œ + // (์‹ค์ œ๋กœ๋Š” PropertyGrid๋‚˜ ๋ณ„๋„ ์ปจํŠธ๋กค ์‚ฌ์šฉ) + labelSelectedNode.Text = $"์„ ํƒ๋œ ๋…ธ๋“œ: {_selectedNode.Name} ({_selectedNode.NodeId})"; + } + + private void ClearNodeProperties() + { + labelSelectedNode.Text = "์„ ํƒ๋œ ๋…ธ๋“œ: ์—†์Œ"; + } + + private void UpdateTitle() + { + var title = "AGV Map Editor"; + + if (!string.IsNullOrEmpty(_currentMapFile)) + { + title += $" - {Path.GetFileName(_currentMapFile)}"; + } + + if (_hasChanges) + { + title += " *"; + } + + this.Text = title; + } + + #endregion + + #region Form Events + + private void MainForm_FormClosing(object sender, FormClosingEventArgs e) + { + if (!CheckSaveChanges()) + { + e.Cancel = true; + } + } + + #endregion + + #region Data Model for Serialization + + private class MapData + { + public List Nodes { get; set; } = new List(); + public List RfidMappings { get; set; } = new List(); + public DateTime CreatedDate { get; set; } + public string Version { get; set; } = "1.0"; + } + + #endregion + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.resx b/Cs_HMI/AGVMapEditor/Forms/MainForm.resx new file mode 100644 index 0000000..9564200 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.resx @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 132, 17 + + \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Models/Enums.cs b/Cs_HMI/AGVMapEditor/Models/Enums.cs new file mode 100644 index 0000000..741f2f8 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Models/Enums.cs @@ -0,0 +1,64 @@ +using System; + +namespace AGVMapEditor.Models +{ + /// + /// ๋…ธ๋“œ ํƒ€์ž… ์—ด๊ฑฐํ˜• + /// + public enum NodeType + { + /// ์ผ๋ฐ˜ ๊ฒฝ๋กœ ๋…ธ๋“œ + Normal, + /// ํšŒ์ „ ๊ฐ€๋Šฅ ์ง€์  + Rotation, + /// ๋„ํ‚น ์Šคํ…Œ์ด์…˜ + Docking, + /// ์ถฉ์ „ ์Šคํ…Œ์ด์…˜ + Charging + } + + /// + /// ๋„ํ‚น ๋ฐฉํ–ฅ ์—ด๊ฑฐํ˜• + /// + public enum DockingDirection + { + /// ์ „์ง„ ๋„ํ‚น (์ถฉ์ „๊ธฐ) + Forward, + /// ํ›„์ง„ ๋„ํ‚น (๋กœ๋”, ํด๋ฆฌ๋„ˆ, ์˜คํ”„๋กœ๋”, ๋ฒ„ํผ) + Backward + } + + /// + /// AGV ์ด๋™ ๋ฐฉํ–ฅ ์—ด๊ฑฐํ˜• + /// + public enum AgvDirection + { + /// ์ „์ง„ (๋ชจ๋‹ˆํ„ฐ ๋ฐฉํ–ฅ) + Forward, + /// ํ›„์ง„ (๋ฆฌํ”„ํŠธ ๋ฐฉํ–ฅ) + Backward, + /// ์ขŒํšŒ์ „ + Left, + /// ์šฐํšŒ์ „ + Right, + /// ์ •์ง€ + Stop + } + + /// + /// ์žฅ๋น„ ํƒ€์ž… ์—ด๊ฑฐํ˜• + /// + public enum StationType + { + /// ๋กœ๋” + Loader, + /// ํด๋ฆฌ๋„ˆ + Cleaner, + /// ์˜คํ”„๋กœ๋” + Offloader, + /// ๋ฒ„ํผ + Buffer, + /// ์ถฉ์ „๊ธฐ + Charger + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Models/MapNode.cs b/Cs_HMI/AGVMapEditor/Models/MapNode.cs new file mode 100644 index 0000000..267509b --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Models/MapNode.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace AGVMapEditor.Models +{ + /// + /// ๋งต ๋…ธ๋“œ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค + /// ๋…ผ๋ฆฌ์  ๋…ธ๋“œ๋กœ์„œ ์‹ค์ œ ๋งต์˜ ์œ„์น˜์™€ ์†์„ฑ์„ ์ •์˜ + /// + public class MapNode + { + /// + /// ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID (๋งต ์—๋””ํ„ฐ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ณ ์œ  ID) + /// ์˜ˆ: "N001", "N002", "LOADER1", "CHARGER1" + /// + public string NodeId { get; set; } = string.Empty; + + /// + /// ๋…ธ๋“œ ํ‘œ์‹œ ์ด๋ฆ„ (์‚ฌ์šฉ์ž ์นœํ™”์ ) + /// ์˜ˆ: "๋กœ๋”1", "์ถฉ์ „๊ธฐ1", "๊ต์ฐจ์ A", "ํšŒ์ „์ง€์ 1" + /// + public string Name { get; set; } = string.Empty; + + /// + /// ๋งต ์ƒ์˜ ์œ„์น˜ ์ขŒํ‘œ (ํ”ฝ์…€ ๋‹จ์œ„) + /// + public Point Position { get; set; } = Point.Empty; + + /// + /// ๋…ธ๋“œ ํƒ€์ž… + /// + public NodeType Type { get; set; } = NodeType.Normal; + + /// + /// ๋„ํ‚น ๋ฐฉํ–ฅ (๋„ํ‚น/์ถฉ์ „ ๋…ธ๋“œ์ธ ๊ฒฝ์šฐ๋งŒ ์‚ฌ์šฉ) + /// + public DockingDirection? DockDirection { get; set; } = null; + + /// + /// ์—ฐ๊ฒฐ๋œ ๋…ธ๋“œ ID ๋ชฉ๋ก (๊ฒฝ๋กœ ์ •๋ณด) + /// + public List ConnectedNodes { get; set; } = new List(); + + /// + /// ํšŒ์ „ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (180๋„ ํšŒ์ „ ๊ฐ€๋Šฅํ•œ ์ง€์ ) + /// + public bool CanRotate { get; set; } = false; + + /// + /// ์žฅ๋น„ ID (๋„ํ‚น/์ถฉ์ „ ์Šคํ…Œ์ด์…˜์ธ ๊ฒฝ์šฐ) + /// ์˜ˆ: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1" + /// + public string StationId { get; set; } = string.Empty; + + /// + /// ์žฅ๋น„ ํƒ€์ž… (๋„ํ‚น/์ถฉ์ „ ์Šคํ…Œ์ด์…˜์ธ ๊ฒฝ์šฐ) + /// + public StationType? StationType { get; set; } = null; + + /// + /// ๋…ธ๋“œ ์ƒ์„ฑ ์ผ์ž + /// + public DateTime CreatedDate { get; set; } = DateTime.Now; + + /// + /// ๋…ธ๋“œ ์ˆ˜์ • ์ผ์ž + /// + public DateTime ModifiedDate { get; set; } = DateTime.Now; + + /// + /// ๋…ธ๋“œ ์„ค๋ช… (์ถ”๊ฐ€ ์ •๋ณด) + /// + public string Description { get; set; } = string.Empty; + + /// + /// ๋…ธ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + /// + public bool IsActive { get; set; } = true; + + /// + /// ๋…ธ๋“œ ์ƒ‰์ƒ (๋งต ์—๋””ํ„ฐ ํ‘œ์‹œ์šฉ) + /// + public Color DisplayColor { get; set; } = Color.Blue; + + /// + /// ๊ธฐ๋ณธ ์ƒ์„ฑ์ž + /// + public MapNode() + { + } + + /// + /// ๋งค๊ฐœ๋ณ€์ˆ˜ ์ƒ์„ฑ์ž + /// + /// ๋…ธ๋“œ ID + /// ๋…ธ๋“œ ์ด๋ฆ„ + /// ์œ„์น˜ + /// ๋…ธ๋“œ ํƒ€์ž… + public MapNode(string nodeId, string name, Point position, NodeType type) + { + NodeId = nodeId; + Name = name; + Position = position; + Type = type; + CreatedDate = DateTime.Now; + ModifiedDate = DateTime.Now; + + // ํƒ€์ž…๋ณ„ ๊ธฐ๋ณธ ์ƒ‰์ƒ ์„ค์ • + SetDefaultColorByType(type); + } + + /// + /// ๋…ธ๋“œ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๊ธฐ๋ณธ ์ƒ‰์ƒ ์„ค์ • + /// + /// ๋…ธ๋“œ ํƒ€์ž… + public void SetDefaultColorByType(NodeType type) + { + switch (type) + { + case NodeType.Normal: + DisplayColor = Color.Blue; + break; + case NodeType.Rotation: + DisplayColor = Color.Orange; + break; + case NodeType.Docking: + DisplayColor = Color.Green; + break; + case NodeType.Charging: + DisplayColor = Color.Red; + break; + } + } + + /// + /// ๋‹ค๋ฅธ ๋…ธ๋“œ์™€์˜ ์—ฐ๊ฒฐ ์ถ”๊ฐ€ + /// + /// ์—ฐ๊ฒฐํ•  ๋…ธ๋“œ ID + public void AddConnection(string nodeId) + { + if (!ConnectedNodes.Contains(nodeId)) + { + ConnectedNodes.Add(nodeId); + ModifiedDate = DateTime.Now; + } + } + + /// + /// ๋‹ค๋ฅธ ๋…ธ๋“œ์™€์˜ ์—ฐ๊ฒฐ ์ œ๊ฑฐ + /// + /// ์—ฐ๊ฒฐ ํ•ด์ œํ•  ๋…ธ๋“œ ID + public void RemoveConnection(string nodeId) + { + if (ConnectedNodes.Remove(nodeId)) + { + ModifiedDate = DateTime.Now; + } + } + + /// + /// ๋„ํ‚น ์Šคํ…Œ์ด์…˜ ์„ค์ • + /// + /// ์žฅ๋น„ ID + /// ์žฅ๋น„ ํƒ€์ž… + /// ๋„ํ‚น ๋ฐฉํ–ฅ + public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection) + { + Type = NodeType.Docking; + StationId = stationId; + StationType = stationType; + DockDirection = dockDirection; + SetDefaultColorByType(NodeType.Docking); + ModifiedDate = DateTime.Now; + } + + /// + /// ์ถฉ์ „ ์Šคํ…Œ์ด์…˜ ์„ค์ • + /// + /// ์ถฉ์ „๊ธฐ ID + public void SetChargingStation(string stationId) + { + Type = NodeType.Charging; + StationId = stationId; + StationType = Models.StationType.Charger; + DockDirection = DockingDirection.Forward; // ์ถฉ์ „๊ธฐ๋Š” ํ•ญ์ƒ ์ „์ง„ ๋„ํ‚น + SetDefaultColorByType(NodeType.Charging); + ModifiedDate = DateTime.Now; + } + + /// + /// ๋ฌธ์ž์—ด ํ‘œํ˜„ + /// + public override string ToString() + { + return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})"; + } + + /// + /// ๋…ธ๋“œ ๋ณต์‚ฌ + /// + /// ๋ณต์‚ฌ๋œ ๋…ธ๋“œ + public MapNode Clone() + { + var clone = new MapNode + { + NodeId = NodeId, + Name = Name, + Position = Position, + Type = Type, + DockDirection = DockDirection, + ConnectedNodes = new List(ConnectedNodes), + CanRotate = CanRotate, + StationId = StationId, + StationType = StationType, + CreatedDate = CreatedDate, + ModifiedDate = ModifiedDate, + Description = Description, + IsActive = IsActive, + DisplayColor = DisplayColor + }; + return clone; + } + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs b/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs new file mode 100644 index 0000000..8b350d7 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AGVMapEditor.Models +{ + /// + /// RFID ๊ฐ’์„ ๋…ผ๋ฆฌ์  ๋…ธ๋“œ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํด๋ž˜์Šค + /// ์‹ค์ œ AGV ์‹œ์Šคํ…œ์—์„œ RFID ๋ฆฌ๋”๊ฐ€ ์ฝ์€ ๊ฐ’์„ ๋งต ๋…ธ๋“œ ์ •๋ณด๋กœ ๋ณ€ํ™˜ + /// + public class NodeResolver + { + private List _rfidMappings; + private List _mapNodes; + + /// + /// ๊ธฐ๋ณธ ์ƒ์„ฑ์ž + /// + public NodeResolver() + { + _rfidMappings = new List(); + _mapNodes = new List(); + } + + /// + /// ๋งค๊ฐœ๋ณ€์ˆ˜ ์ƒ์„ฑ์ž + /// + /// RFID ๋งคํ•‘ ๋ชฉ๋ก + /// ๋งต ๋…ธ๋“œ ๋ชฉ๋ก + public NodeResolver(List rfidMappings, List mapNodes) + { + _rfidMappings = rfidMappings ?? new List(); + _mapNodes = mapNodes ?? new List(); + } + + /// + /// RFID ๊ฐ’์œผ๋กœ ๋งต ๋…ธ๋“œ ๊ฒ€์ƒ‰ + /// + /// RFID ๋ฆฌ๋”์—์„œ ์ฝ์€ ๊ฐ’ + /// ํ•ด๋‹นํ•˜๋Š” ๋งต ๋…ธ๋“œ, ์—†์œผ๋ฉด null + public MapNode GetNodeByRfid(string rfidValue) + { + if (string.IsNullOrEmpty(rfidValue)) + return null; + + // 1. RFID ๋งคํ•‘์—์„œ ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID ์ฐพ๊ธฐ + var mapping = _rfidMappings.FirstOrDefault(m => + m.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase) && m.IsActive); + + if (mapping == null) + return null; + + // 2. ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID๋กœ ์‹ค์ œ ๋งต ๋…ธ๋“œ ์ฐพ๊ธฐ + var mapNode = _mapNodes.FirstOrDefault(n => + n.NodeId.Equals(mapping.LogicalNodeId, StringComparison.OrdinalIgnoreCase) && n.IsActive); + + return mapNode; + } + + /// + /// ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID๋กœ ๋งต ๋…ธ๋“œ ๊ฒ€์ƒ‰ + /// + /// ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID + /// ํ•ด๋‹นํ•˜๋Š” ๋งต ๋…ธ๋“œ, ์—†์œผ๋ฉด null + public MapNode GetNodeById(string nodeId) + { + if (string.IsNullOrEmpty(nodeId)) + return null; + + return _mapNodes.FirstOrDefault(n => + n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase) && n.IsActive); + } + + /// + /// ๋งต ๋…ธ๋“œ๋กœ ์—ฐ๊ฒฐ๋œ RFID ๊ฐ’ ๊ฒ€์ƒ‰ + /// + /// ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID + /// ์—ฐ๊ฒฐ๋œ RFID ๊ฐ’, ์—†์œผ๋ฉด null + public string GetRfidByNodeId(string nodeId) + { + if (string.IsNullOrEmpty(nodeId)) + return null; + + var mapping = _rfidMappings.FirstOrDefault(m => + m.LogicalNodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase) && m.IsActive); + + return mapping?.RfidId; + } + + /// + /// RFID ๋งคํ•‘ ์ •๋ณด ๊ฒ€์ƒ‰ + /// + /// RFID ๊ฐ’ + /// ๋งคํ•‘ ์ •๋ณด, ์—†์œผ๋ฉด null + public RfidMapping GetRfidMapping(string rfidValue) + { + if (string.IsNullOrEmpty(rfidValue)) + return null; + + return _rfidMappings.FirstOrDefault(m => + m.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase) && m.IsActive); + } + + /// + /// ์ƒˆ๋กœ์šด RFID ๋งคํ•‘ ์ถ”๊ฐ€ + /// + /// RFID ๊ฐ’ + /// ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID + /// ์„ค๋ช… + /// ์ถ”๊ฐ€ ์„ฑ๊ณต ์—ฌ๋ถ€ + public bool AddRfidMapping(string rfidId, string nodeId, string description = "") + { + if (string.IsNullOrEmpty(rfidId) || string.IsNullOrEmpty(nodeId)) + return false; + + // ์ค‘๋ณต RFID ์ฒดํฌ + if (_rfidMappings.Any(m => m.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase))) + return false; + + // ํ•ด๋‹น ๋…ธ๋“œ ์กด์žฌ ์ฒดํฌ + if (!_mapNodes.Any(n => n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase))) + return false; + + var mapping = new RfidMapping(rfidId, nodeId, description); + _rfidMappings.Add(mapping); + return true; + } + + /// + /// RFID ๋งคํ•‘ ์ œ๊ฑฐ + /// + /// ์ œ๊ฑฐํ•  RFID ๊ฐ’ + /// ์ œ๊ฑฐ ์„ฑ๊ณต ์—ฌ๋ถ€ + public bool RemoveRfidMapping(string rfidId) + { + if (string.IsNullOrEmpty(rfidId)) + return false; + + var mapping = _rfidMappings.FirstOrDefault(m => + m.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase)); + + if (mapping != null) + { + _rfidMappings.Remove(mapping); + return true; + } + + return false; + } + + /// + /// ๋งต ๋…ธ๋“œ ์ถ”๊ฐ€ + /// + /// ์ถ”๊ฐ€ํ•  ๋งต ๋…ธ๋“œ + /// ์ถ”๊ฐ€ ์„ฑ๊ณต ์—ฌ๋ถ€ + public bool AddMapNode(MapNode node) + { + if (node == null || string.IsNullOrEmpty(node.NodeId)) + return false; + + // ์ค‘๋ณต ๋…ธ๋“œ ID ์ฒดํฌ + if (_mapNodes.Any(n => n.NodeId.Equals(node.NodeId, StringComparison.OrdinalIgnoreCase))) + return false; + + _mapNodes.Add(node); + return true; + } + + /// + /// ๋งต ๋…ธ๋“œ ์ œ๊ฑฐ + /// + /// ์ œ๊ฑฐํ•  ๋…ธ๋“œ ID + /// ์ œ๊ฑฐ ์„ฑ๊ณต ์—ฌ๋ถ€ + public bool RemoveMapNode(string nodeId) + { + if (string.IsNullOrEmpty(nodeId)) + return false; + + var node = _mapNodes.FirstOrDefault(n => + n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase)); + + if (node != null) + { + // ์—ฐ๊ด€๋œ RFID ๋งคํ•‘๋„ ํ•จ๊ป˜ ์ œ๊ฑฐ + var associatedMappings = _rfidMappings.Where(m => + m.LogicalNodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase)).ToList(); + + foreach (var mapping in associatedMappings) + { + _rfidMappings.Remove(mapping); + } + + // ๋‹ค๋ฅธ ๋…ธ๋“œ์˜ ์—ฐ๊ฒฐ ์ •๋ณด์—์„œ๋„ ์ œ๊ฑฐ + foreach (var otherNode in _mapNodes.Where(n => n.ConnectedNodes.Contains(nodeId))) + { + otherNode.RemoveConnection(nodeId); + } + + _mapNodes.Remove(node); + return true; + } + + return false; + } + + /// + /// ํŠน์ • ํƒ€์ž…์˜ ๋…ธ๋“œ๋“ค ๊ฒ€์ƒ‰ + /// + /// ๋…ธ๋“œ ํƒ€์ž… + /// ํ•ด๋‹น ํƒ€์ž…์˜ ๋…ธ๋“œ ๋ชฉ๋ก + public List GetNodesByType(NodeType nodeType) + { + return _mapNodes.Where(n => n.Type == nodeType && n.IsActive).ToList(); + } + + /// + /// ์žฅ๋น„ ID๋กœ ๋…ธ๋“œ ๊ฒ€์ƒ‰ + /// + /// ์žฅ๋น„ ID + /// ํ•ด๋‹น ์žฅ๋น„์˜ ๋…ธ๋“œ, ์—†์œผ๋ฉด null + public MapNode GetNodeByStationId(string stationId) + { + if (string.IsNullOrEmpty(stationId)) + return null; + + return _mapNodes.FirstOrDefault(n => + n.StationId.Equals(stationId, StringComparison.OrdinalIgnoreCase) && n.IsActive); + } + + /// + /// ๋งคํ•‘๋˜์ง€ ์•Š์€ ๋…ธ๋“œ๋“ค ๊ฒ€์ƒ‰ (RFID๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๋…ธ๋“œ) + /// + /// ๋งคํ•‘๋˜์ง€ ์•Š์€ ๋…ธ๋“œ ๋ชฉ๋ก + public List GetUnmappedNodes() + { + var mappedNodeIds = _rfidMappings.Where(m => m.IsActive) + .Select(m => m.LogicalNodeId) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + return _mapNodes.Where(n => n.IsActive && !mappedNodeIds.Contains(n.NodeId)).ToList(); + } + + /// + /// ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” RFID ๋งคํ•‘๋“ค ๊ฒ€์ƒ‰ (๋…ธ๋“œ๊ฐ€ ์‚ญ์ œ๋œ ๋งคํ•‘) + /// + /// ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋งคํ•‘ ๋ชฉ๋ก + public List GetOrphanedMappings() + { + var activeNodeIds = _mapNodes.Where(n => n.IsActive) + .Select(n => n.NodeId) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + return _rfidMappings.Where(m => m.IsActive && !activeNodeIds.Contains(m.LogicalNodeId)).ToList(); + } + + /// + /// ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + /// + public void Clear() + { + _rfidMappings.Clear(); + _mapNodes.Clear(); + } + + /// + /// ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + /// + /// ๊ฒ€์ฆ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก + public List ValidateData() + { + var errors = new List(); + + // ์ค‘๋ณต RFID ์ฒดํฌ + var duplicateRfids = _rfidMappings.GroupBy(m => m.RfidId.ToLower()) + .Where(g => g.Count() > 1) + .Select(g => g.Key); + foreach (var rfid in duplicateRfids) + { + errors.Add($"์ค‘๋ณต๋œ RFID: {rfid}"); + } + + // ์ค‘๋ณต ๋…ธ๋“œ ID ์ฒดํฌ + var duplicateNodeIds = _mapNodes.GroupBy(n => n.NodeId.ToLower()) + .Where(g => g.Count() > 1) + .Select(g => g.Key); + foreach (var nodeId in duplicateNodeIds) + { + errors.Add($"์ค‘๋ณต๋œ ๋…ธ๋“œ ID: {nodeId}"); + } + + // ๊ณ ์•„ ๋งคํ•‘ ์ฒดํฌ + var orphanedMappings = GetOrphanedMappings(); + foreach (var mapping in orphanedMappings) + { + errors.Add($"์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋…ธ๋“œ๋ฅผ ์ฐธ์กฐํ•˜๋Š” RFID ๋งคํ•‘: {mapping.RfidId} โ†’ {mapping.LogicalNodeId}"); + } + + // ๋งคํ•‘๋˜์ง€ ์•Š์€ ๋…ธ๋“œ ๊ฒฝ๊ณ  (์—๋Ÿฌ๋Š” ์•„๋‹˜) + var unmappedNodes = GetUnmappedNodes(); + foreach (var node in unmappedNodes) + { + errors.Add($"RFID๊ฐ€ ๋งคํ•‘๋˜์ง€ ์•Š์€ ๋…ธ๋“œ: {node.NodeId} ({node.Name})"); + } + + return errors; + } + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs b/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs new file mode 100644 index 0000000..e702140 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; + +namespace AGVMapEditor.Models +{ + /// + /// AGV ์ „์šฉ ๊ฒฝ๋กœ ๊ณ„์‚ฐ๊ธฐ (A* ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ธฐ๋ฐ˜) + /// AGV์˜ ๋ฐฉํ–ฅ์„ฑ, ๋„ํ‚น ์ œ์•ฝ, ํšŒ์ „ ์ œ์•ฝ์„ ๊ณ ๋ คํ•œ ๊ฒฝ๋กœ ๊ณ„์‚ฐ + /// + public class PathCalculator + { + #region Constants + + private const float BASE_MOVE_COST = 1.0f; // ๊ธฐ๋ณธ ์ด๋™ ๋น„์šฉ + private const float ROTATION_COST = 0.5f; // ํšŒ์ „ ๋น„์šฉ + private const float DOCKING_APPROACH_COST = 0.2f; // ๋„ํ‚น ์ ‘๊ทผ ์ถ”๊ฐ€ ๋น„์šฉ + private const float HEURISTIC_WEIGHT = 1.0f; // ํœด๋ฆฌ์Šคํ‹ฑ ๊ฐ€์ค‘์น˜ + + #endregion + + #region Fields + + private List _mapNodes; + private NodeResolver _nodeResolver; + + #endregion + + #region Constructor + + /// + /// ์ƒ์„ฑ์ž + /// + /// ๋งต ๋…ธ๋“œ ๋ชฉ๋ก + /// ๋…ธ๋“œ ํ•ด๊ฒฐ๊ธฐ + public PathCalculator(List mapNodes, NodeResolver nodeResolver) + { + _mapNodes = mapNodes ?? throw new ArgumentNullException(nameof(mapNodes)); + _nodeResolver = nodeResolver ?? throw new ArgumentNullException(nameof(nodeResolver)); + } + + #endregion + + #region Public Methods + + /// + /// ๊ฒฝ๋กœ ๊ณ„์‚ฐ (๋ฉ”์ธ ๋ฉ”์„œ๋“œ) + /// + /// ์‹œ์ž‘ ๋…ธ๋“œ ID + /// ๋ชฉํ‘œ ๋…ธ๋“œ ID + /// ํ˜„์žฌ AGV ๋ฐฉํ–ฅ + /// ๊ฒฝ๋กœ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + public PathResult CalculatePath(string startNodeId, string targetNodeId, AgvDirection currentDirection) + { + var stopwatch = Stopwatch.StartNew(); + + try + { + // ์ž…๋ ฅ ๊ฒ€์ฆ + var validationResult = ValidateInput(startNodeId, targetNodeId, currentDirection); + if (!validationResult.Success) + { + stopwatch.Stop(); + validationResult.CalculationTime = stopwatch.ElapsedMilliseconds; + return validationResult; + } + + // A* ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‹คํ–‰ + var result = ExecuteAStar(startNodeId, targetNodeId, currentDirection); + + stopwatch.Stop(); + result.CalculationTime = stopwatch.ElapsedMilliseconds; + + return result; + } + catch (Exception ex) + { + stopwatch.Stop(); + return new PathResult($"๊ฒฝ๋กœ ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {ex.Message}") + { + CalculationTime = stopwatch.ElapsedMilliseconds + }; + } + } + + /// + /// ๊ฒฝ๋กœ ์œ ํšจ์„ฑ ๊ฒ€์ฆ (RFID ์ดํƒˆ ๊ฐ์ง€์‹œ ์‚ฌ์šฉ) + /// + /// ํ˜„์žฌ ๊ฒฝ๋กœ + /// ํ˜„์žฌ ๊ฐ์ง€๋œ RFID + /// ๊ฒฝ๋กœ ์œ ํšจ์„ฑ ์—ฌ๋ถ€ + public bool ValidateCurrentPath(PathResult currentPath, string currentRfidId) + { + if (currentPath == null || !currentPath.Success) + return false; + + var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId); + if (currentNode == null) + return false; + + // ํ˜„์žฌ ๋…ธ๋“œ๊ฐ€ ๊ณ„ํš๋œ ๊ฒฝ๋กœ์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + return currentPath.NodeSequence.Contains(currentNode.NodeId); + } + + /// + /// ๋™์  ๊ฒฝ๋กœ ์žฌ๊ณ„์‚ฐ (๊ฒฝ๋กœ ์ดํƒˆ์‹œ ์‚ฌ์šฉ) + /// + /// ํ˜„์žฌ RFID ์œ„์น˜ + /// ๋ชฉํ‘œ ๋…ธ๋“œ ID + /// ํ˜„์žฌ ๋ฐฉํ–ฅ + /// ์›๋ž˜ ๊ฒฝ๋กœ (์ฐธ๊ณ ์šฉ) + /// ์ƒˆ๋กœ์šด ๊ฒฝ๋กœ + public PathResult RecalculatePath(string currentRfidId, string targetNodeId, + AgvDirection currentDirection, PathResult originalPath = null) + { + var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId); + if (currentNode == null) + { + return new PathResult("ํ˜„์žฌ ์œ„์น˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + // ์ƒˆ๋กœ์šด ๊ฒฝ๋กœ ๊ณ„์‚ฐ + var result = CalculatePath(currentNode.NodeId, targetNodeId, currentDirection); + + // ์›๋ž˜ ๊ฒฝ๋กœ์™€ ๋น„๊ต (๋กœ๊ทธ์šฉ) + if (originalPath != null && result.Success) + { + // TODO: ๊ฒฝ๋กœ ๋ณ€๊ฒฝ ๋กœ๊ทธ ๊ธฐ๋ก + } + + return result; + } + + #endregion + + #region Private Methods - Input Validation + + /// + /// ์ž…๋ ฅ ๊ฐ’ ๊ฒ€์ฆ + /// + private PathResult ValidateInput(string startNodeId, string targetNodeId, AgvDirection currentDirection) + { + if (string.IsNullOrEmpty(startNodeId)) + return new PathResult("์‹œ์ž‘ ๋…ธ๋“œ๊ฐ€ ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); + + if (string.IsNullOrEmpty(targetNodeId)) + return new PathResult("๋ชฉํ‘œ ๋…ธ๋“œ๊ฐ€ ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); + + var startNode = _mapNodes.FirstOrDefault(n => n.NodeId == startNodeId); + if (startNode == null) + return new PathResult($"์‹œ์ž‘ ๋…ธ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {startNodeId}"); + + var targetNode = _mapNodes.FirstOrDefault(n => n.NodeId == targetNodeId); + if (targetNode == null) + return new PathResult($"๋ชฉํ‘œ ๋…ธ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {targetNodeId}"); + + if (startNodeId == targetNodeId) + return new PathResult("์‹œ์ž‘์ ๊ณผ ๋ชฉํ‘œ์ ์ด ๋™์ผํ•ฉ๋‹ˆ๋‹ค."); + + return new PathResult { Success = true }; + } + + #endregion + + #region Private Methods - A* Algorithm + + /// + /// A* ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‹คํ–‰ + /// + private PathResult ExecuteAStar(string startNodeId, string targetNodeId, AgvDirection currentDirection) + { + var openSet = new SortedSet(); + var closedSet = new HashSet(); + var gScore = new Dictionary(); + + // ์‹œ์ž‘ ๋…ธ๋“œ ์„ค์ • + var startPathNode = new PathNode(startNodeId, currentDirection) + { + GCost = 0, + HCost = CalculateHeuristic(startNodeId, targetNodeId) + }; + + openSet.Add(startPathNode); + gScore[startPathNode.GetKey()] = 0; + + while (openSet.Count > 0) + { + // ๊ฐ€์žฅ ๋‚ฎ์€ F ๋น„์šฉ์„ ๊ฐ€์ง„ ๋…ธ๋“œ ์„ ํƒ + var current = openSet.Min; + openSet.Remove(current); + + // ๋ชฉํ‘œ ๋„๋‹ฌ ํ™•์ธ + if (current.NodeId == targetNodeId) + { + // ๋„ํ‚น ๋ฐฉํ–ฅ ๊ฒ€์ฆ + if (IsValidDockingApproach(targetNodeId, current.Direction)) + { + return ReconstructPath(current, startNodeId, targetNodeId, currentDirection); + } + // ๋„ํ‚น ๋ฐฉํ–ฅ์ด ๋งž์ง€ ์•Š์œผ๋ฉด ๊ณ„์† ํƒ์ƒ‰ + } + + closedSet.Add(current.GetKey()); + + // ์ธ์ ‘ ๋…ธ๋“œ๋“ค ์ฒ˜๋ฆฌ + ProcessNeighbors(current, targetNodeId, openSet, closedSet, gScore); + } + + return new PathResult("๊ฒฝ๋กœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + /// + /// ์ธ์ ‘ ๋…ธ๋“œ๋“ค ์ฒ˜๋ฆฌ + /// + private void ProcessNeighbors(PathNode current, string targetNodeId, + SortedSet openSet, HashSet closedSet, + Dictionary gScore) + { + var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == current.NodeId); + if (currentMapNode == null) return; + + foreach (var neighborId in currentMapNode.ConnectedNodes) + { + var neighborMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == neighborId); + if (neighborMapNode == null) continue; + + // ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๋ฐฉํ–ฅ์œผ๋กœ ์ด์›ƒ ๋…ธ๋“œ ๋ฐฉ๋ฌธ + foreach (var direction in GetPossibleDirections(current, neighborMapNode)) + { + var neighborPathNode = new PathNode(neighborId, direction); + var neighborKey = neighborPathNode.GetKey(); + + if (closedSet.Contains(neighborKey)) + continue; + + // ์ด๋™ ๋น„์šฉ ๊ณ„์‚ฐ + var moveCost = CalculateMoveCost(current, neighborPathNode, neighborMapNode); + var tentativeGScore = current.GCost + moveCost; + + // ๋” ์ข‹์€ ๊ฒฝ๋กœ์ธ์ง€ ํ™•์ธ + if (!gScore.ContainsKey(neighborKey) || tentativeGScore < gScore[neighborKey]) + { + // ๊ฒฝ๋กœ ์ •๋ณด ์—…๋ฐ์ดํŠธ + neighborPathNode.Parent = current; + neighborPathNode.GCost = tentativeGScore; + neighborPathNode.HCost = CalculateHeuristic(neighborId, targetNodeId); + neighborPathNode.RotationCount = current.RotationCount + + (current.Direction != direction ? 1 : 0); + + // ์ด๋™ ๋ช…๋ น ์‹œํ€€์Šค ๊ตฌ์„ฑ + neighborPathNode.MovementSequence = GenerateMovementSequence(current, neighborPathNode); + + gScore[neighborKey] = tentativeGScore; + + // openSet์— ์ถ”๊ฐ€ (์ค‘๋ณต ์ œ๊ฑฐ) + openSet.RemoveWhere(n => n.GetKey() == neighborKey); + openSet.Add(neighborPathNode); + } + } + } + } + + /// + /// ๊ฐ€๋Šฅํ•œ ๋ฐฉํ–ฅ๋“ค ๊ณ„์‚ฐ + /// + private List GetPossibleDirections(PathNode current, MapNode neighborNode) + { + var directions = new List(); + + // ๊ธฐ๋ณธ์ ์œผ๋กœ ์ „์ง„/ํ›„์ง„ ๊ฐ€๋Šฅ + directions.Add(AgvDirection.Forward); + directions.Add(AgvDirection.Backward); + + // ํšŒ์ „ ๊ฐ€๋Šฅํ•œ ๋…ธ๋“œ์—์„œ๋งŒ ๋ฐฉํ–ฅ ์ „ํ™˜ ๊ฐ€๋Šฅ + if (CanRotateAt(current.NodeId)) + { + // ํ˜„์žฌ ๋ฐฉํ–ฅ๊ณผ ๋‹ค๋ฅธ ๋ฐฉํ–ฅ๋„ ๊ณ ๋ ค + if (current.Direction == AgvDirection.Forward) + directions.Add(AgvDirection.Backward); + else if (current.Direction == AgvDirection.Backward) + directions.Add(AgvDirection.Forward); + } + + return directions; + } + + /// + /// ์ด๋™ ๋น„์šฉ ๊ณ„์‚ฐ + /// + private float CalculateMoveCost(PathNode from, PathNode to, MapNode toMapNode) + { + float cost = BASE_MOVE_COST; + + // ๋ฐฉํ–ฅ ์ „ํ™˜ ๋น„์šฉ + if (from.Direction != to.Direction) + { + cost += ROTATION_COST; + } + + // ๋„ํ‚น ์Šคํ…Œ์ด์…˜ ์ ‘๊ทผ ๋น„์šฉ + if (toMapNode.Type == NodeType.Docking || toMapNode.Type == NodeType.Charging) + { + cost += DOCKING_APPROACH_COST; + } + + // ์‹ค์ œ ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ˜ ๋น„์šฉ (์ขŒํ‘œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) + var fromMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == from.NodeId); + if (fromMapNode != null && toMapNode != null) + { + var distance = CalculateDistance(fromMapNode.Position, toMapNode.Position); + cost *= (distance / 100.0f); // ์ขŒํ‘œ ๋‹จ์œ„๋ฅผ ๊ฑฐ๋ฆฌ ๋‹จ์œ„๋กœ ์กฐ์ • + } + + return cost; + } + + /// + /// ํœด๋ฆฌ์Šคํ‹ฑ ํ•จ์ˆ˜ (๋ชฉํ‘œ๊นŒ์ง€์˜ ์ถ”์ • ๊ฑฐ๋ฆฌ) + /// + private float CalculateHeuristic(string fromNodeId, string toNodeId) + { + var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId); + var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId); + + if (fromNode == null || toNode == null) + return 0; + + // ์œ ํด๋ฆฌ๋“œ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ + var distance = CalculateDistance(fromNode.Position, toNode.Position); + return distance * HEURISTIC_WEIGHT / 100.0f; // ์ขŒํ‘œ ๋‹จ์œ„ ์กฐ์ • + } + + /// + /// ๋‘ ์  ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ + /// + private float CalculateDistance(Point from, Point to) + { + var dx = to.X - from.X; + var dy = to.Y - from.Y; + return (float)Math.Sqrt(dx * dx + dy * dy); + } + + /// + /// ํšŒ์ „ ๊ฐ€๋Šฅํ•œ ์œ„์น˜์ธ์ง€ ํ™•์ธ + /// + private bool CanRotateAt(string nodeId) + { + var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + return node != null && (node.CanRotate || node.Type == NodeType.Rotation); + } + + /// + /// ๋„ํ‚น ์ ‘๊ทผ ๋ฐฉํ–ฅ์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ + /// + private bool IsValidDockingApproach(string nodeId, AgvDirection approachDirection) + { + var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + if (node == null) return true; + + // ๋„ํ‚น/์ถฉ์ „ ์Šคํ…Œ์ด์…˜์ด ์•„๋‹ˆ๋ฉด ๋ฐฉํ–ฅ ์ œ์•ฝ ์—†์Œ + if (node.Type != NodeType.Docking && node.Type != NodeType.Charging) + return true; + + // ๋„ํ‚น ๋ฐฉํ–ฅ ํ™•์ธ + if (node.DockDirection == null) + return true; + + // ์ถฉ์ „๊ธฐ๋Š” ์ „์ง„์œผ๋กœ๋งŒ, ๋‹ค๋ฅธ ์žฅ๋น„๋Š” ํ›„์ง„์œผ๋กœ๋งŒ + if (node.Type == NodeType.Charging) + return approachDirection == AgvDirection.Forward; + else + return approachDirection == AgvDirection.Backward; + } + + /// + /// ์ด๋™ ๋ช…๋ น ์‹œํ€€์Šค ์ƒ์„ฑ + /// + private List GenerateMovementSequence(PathNode from, PathNode to) + { + var sequence = new List(); + + // ๋ฐฉํ–ฅ ์ „ํ™˜์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ + if (from.Direction != to.Direction) + { + if (from.Direction == AgvDirection.Forward && to.Direction == AgvDirection.Backward) + { + sequence.Add(AgvDirection.Right); // 180๋„ ํšŒ์ „ (๋งˆํฌ์„ผ์„œ๊นŒ์ง€) + } + else if (from.Direction == AgvDirection.Backward && to.Direction == AgvDirection.Forward) + { + sequence.Add(AgvDirection.Right); // 180๋„ ํšŒ์ „ (๋งˆํฌ์„ผ์„œ๊นŒ์ง€) + } + } + + // ์ด๋™ ๋ช…๋ น + sequence.Add(to.Direction); + + return sequence; + } + + /// + /// ๊ฒฝ๋กœ ์žฌ๊ตฌ์„ฑ + /// + private PathResult ReconstructPath(PathNode goalNode, string startNodeId, string targetNodeId, AgvDirection startDirection) + { + var path = new List(); + var current = goalNode; + + // ์—ญ์ˆœ์œผ๋กœ ๊ฒฝ๋กœ ๊ตฌ์„ฑ + while (current != null) + { + path.Insert(0, current); + current = current.Parent; + } + + return new PathResult(path, startNodeId, targetNodeId, startDirection); + } + + #endregion + + #region Public Utility Methods + + /// + /// ๋…ธ๋“œ ๊ฐ„ ์ง์„  ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ + /// + public float GetDistance(string fromNodeId, string toNodeId) + { + var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId); + var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId); + + if (fromNode == null || toNode == null) + return float.MaxValue; + + return CalculateDistance(fromNode.Position, toNode.Position); + } + + /// + /// ํŠน์ • ๋…ธ๋“œ์—์„œ ๊ฐ€๋Šฅํ•œ ๋‹ค์Œ ๋…ธ๋“œ๋“ค ์กฐํšŒ + /// + public List GetPossibleNextNodes(string currentNodeId, AgvDirection currentDirection) + { + var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId); + if (currentNode == null) + return new List(); + + return currentNode.ConnectedNodes.ToList(); + } + + /// + /// ๊ฒฝ๋กœ ์ตœ์ ํ™” (์„ ํƒ์  ๊ธฐ๋Šฅ) + /// + public PathResult OptimizePath(PathResult originalPath) + { + if (originalPath == null || !originalPath.Success) + return originalPath; + + // TODO: ๊ฒฝ๋กœ ์ตœ์ ํ™” ๋กœ์ง ๊ตฌํ˜„ + // - ๋ถˆํ•„์š”ํ•œ ์ค‘๊ฐ„ ์ •์ง€์  ์ œ๊ฑฐ + // - ํšŒ์ „ ์ตœ์†Œํ™” + // - ๊ฒฝ๋กœ ๋‹จ์ˆœํ™” + + return originalPath; + } + + #endregion + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Models/PathNode.cs b/Cs_HMI/AGVMapEditor/Models/PathNode.cs new file mode 100644 index 0000000..983cde8 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Models/PathNode.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; + +namespace AGVMapEditor.Models +{ + /// + /// A* ์•Œ๊ณ ๋ฆฌ์ฆ˜์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ๋กœ ๋…ธ๋“œ + /// + public class PathNode : IComparable + { + /// + /// ๋งต ๋…ธ๋“œ ID + /// + public string NodeId { get; set; } = string.Empty; + + /// + /// AGV์˜ ํ˜„์žฌ ๋ฐฉํ–ฅ (์ด ๋…ธ๋“œ์— ๋„๋‹ฌํ–ˆ์„ ๋•Œ์˜ ๋ฐฉํ–ฅ) + /// + public AgvDirection Direction { get; set; } = AgvDirection.Forward; + + /// + /// ์‹œ์ž‘์ ์—์„œ ์ด ๋…ธ๋“œ๊นŒ์ง€์˜ ์‹ค์ œ ๋น„์šฉ (G) + /// + public float GCost { get; set; } = float.MaxValue; + + /// + /// ์ด ๋…ธ๋“œ์—์„œ ๋ชฉํ‘œ๊นŒ์ง€์˜ ์ถ”์ • ๋น„์šฉ (H) + /// + public float HCost { get; set; } = 0; + + /// + /// ์ด ๋น„์šฉ (F = G + H) + /// + public float FCost => GCost + HCost; + + /// + /// ์ด์ „ ๋…ธ๋“œ (๊ฒฝ๋กœ ์ถ”์ ์šฉ) + /// + public PathNode Parent { get; set; } = null; + + /// + /// ํšŒ์ „ ํšŸ์ˆ˜ (๋ฐฉํ–ฅ ์ „ํ™˜ ๋น„์šฉ ๊ณ„์‚ฐ์šฉ) + /// + public int RotationCount { get; set; } = 0; + + /// + /// ์ด ๋…ธ๋“œ์— ๋„๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ์ด๋™ ๋ช…๋ น ์‹œํ€€์Šค + /// + public List MovementSequence { get; set; } = new List(); + + /// + /// ๊ธฐ๋ณธ ์ƒ์„ฑ์ž + /// + public PathNode() + { + } + + /// + /// ๋งค๊ฐœ๋ณ€์ˆ˜ ์ƒ์„ฑ์ž + /// + /// ๋…ธ๋“œ ID + /// AGV ๋ฐฉํ–ฅ + public PathNode(string nodeId, AgvDirection direction) + { + NodeId = nodeId; + Direction = direction; + } + + /// + /// ์šฐ์„ ์ˆœ์œ„ ํ๋ฅผ ์œ„ํ•œ ๋น„๊ต (FCost ๊ธฐ์ค€) + /// + public int CompareTo(PathNode other) + { + if (other == null) return 1; + + int compare = FCost.CompareTo(other.FCost); + if (compare == 0) + { + // FCost๊ฐ€ ๊ฐ™์œผ๋ฉด HCost๊ฐ€ ๋‚ฎ์€ ๊ฒƒ์„ ์šฐ์„  + compare = HCost.CompareTo(other.HCost); + } + if (compare == 0) + { + // ๊ทธ๊ฒƒ๋„ ๊ฐ™์œผ๋ฉด ํšŒ์ „ ํšŸ์ˆ˜๊ฐ€ ์ ์€ ๊ฒƒ์„ ์šฐ์„  + compare = RotationCount.CompareTo(other.RotationCount); + } + + return compare; + } + + /// + /// ๋…ธ๋“œ ์ƒํƒœ ๋ณต์‚ฌ + /// + public PathNode Clone() + { + return new PathNode + { + NodeId = NodeId, + Direction = Direction, + GCost = GCost, + HCost = HCost, + Parent = Parent, + RotationCount = RotationCount, + MovementSequence = new List(MovementSequence) + }; + } + + /// + /// ๊ณ ์œ  ํ‚ค ์ƒ์„ฑ (๋…ธ๋“œID + ๋ฐฉํ–ฅ) + /// + public string GetKey() + { + return $"{NodeId}_{Direction}"; + } + + /// + /// ๋ฌธ์ž์—ด ํ‘œํ˜„ + /// + public override string ToString() + { + return $"{NodeId}({Direction}) F:{FCost:F1} G:{GCost:F1} H:{HCost:F1} R:{RotationCount}"; + } + + /// + /// ํ•ด์‹œ์ฝ”๋“œ (๋”•์…”๋„ˆ๋ฆฌ ํ‚ค์šฉ) + /// + public override int GetHashCode() + { + return GetKey().GetHashCode(); + } + + /// + /// ๋™๋“ฑ์„ฑ ๋น„๊ต + /// + public override bool Equals(object obj) + { + if (obj is PathNode other) + { + return NodeId == other.NodeId && Direction == other.Direction; + } + return false; + } + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Models/PathResult.cs b/Cs_HMI/AGVMapEditor/Models/PathResult.cs new file mode 100644 index 0000000..993c2c7 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Models/PathResult.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AGVMapEditor.Models +{ + /// + /// ๊ฒฝ๋กœ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + /// + public class PathResult + { + /// + /// ๊ฒฝ๋กœ ๊ณ„์‚ฐ ์„ฑ๊ณต ์—ฌ๋ถ€ + /// + public bool Success { get; set; } = false; + + /// + /// ๊ฒฝ๋กœ์ƒ์˜ ๋…ธ๋“œ ID ์‹œํ€€์Šค + /// + public List NodeSequence { get; set; } = new List(); + + /// + /// AGV ์ด๋™ ๋ช…๋ น ์‹œํ€€์Šค + /// + public List MovementSequence { get; set; } = new List(); + + /// + /// ์ด ์ด๋™ ๊ฑฐ๋ฆฌ (๋น„์šฉ) + /// + public float TotalDistance { get; set; } = 0; + + /// + /// ์ด ํšŒ์ „ ํšŸ์ˆ˜ + /// + public int TotalRotations { get; set; } = 0; + + /// + /// ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„ (์ดˆ) + /// + public float EstimatedTime { get; set; } = 0; + + /// + /// ์‹œ์ž‘ ๋…ธ๋“œ ID + /// + public string StartNodeId { get; set; } = string.Empty; + + /// + /// ๋ชฉํ‘œ ๋…ธ๋“œ ID + /// + public string TargetNodeId { get; set; } = string.Empty; + + /// + /// ์‹œ์ž‘์‹œ AGV ๋ฐฉํ–ฅ + /// + public AgvDirection StartDirection { get; set; } = AgvDirection.Forward; + + /// + /// ๋„์ฐฉ์‹œ AGV ๋ฐฉํ–ฅ + /// + public AgvDirection EndDirection { get; set; } = AgvDirection.Forward; + + /// + /// ๊ฒฝ๋กœ ๊ณ„์‚ฐ์— ๊ฑธ๋ฆฐ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + /// + public long CalculationTime { get; set; } = 0; + + /// + /// ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ (์‹คํŒจ์‹œ) + /// + public string ErrorMessage { get; set; } = string.Empty; + + /// + /// ๊ฒฝ๋กœ์ƒ์˜ ์ƒ์„ธ ์ •๋ณด (๋””๋ฒ„๊น…์šฉ) + /// + public List DetailedPath { get; set; } = new List(); + + /// + /// ํšŒ์ „์ด ๋ฐœ์ƒํ•˜๋Š” ๋…ธ๋“œ๋“ค + /// + public List RotationNodes { get; set; } = new List(); + + /// + /// ๊ธฐ๋ณธ ์ƒ์„ฑ์ž + /// + public PathResult() + { + } + + /// + /// ์„ฑ๊ณต ๊ฒฐ๊ณผ ์ƒ์„ฑ์ž + /// + public PathResult(List path, string startNodeId, string targetNodeId, AgvDirection startDirection) + { + if (path == null || path.Count == 0) + { + Success = false; + ErrorMessage = "๋นˆ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค."; + return; + } + + Success = true; + StartNodeId = startNodeId; + TargetNodeId = targetNodeId; + StartDirection = startDirection; + DetailedPath = new List(path); + + // ๋…ธ๋“œ ์‹œํ€€์Šค ๊ตฌ์„ฑ + NodeSequence = path.Select(p => p.NodeId).ToList(); + + // ์ด๋™ ๋ช…๋ น ์‹œํ€€์Šค ๊ตฌ์„ฑ + MovementSequence = new List(); + for (int i = 0; i < path.Count; i++) + { + MovementSequence.AddRange(path[i].MovementSequence); + } + + // ํ†ต๊ณ„ ๊ณ„์‚ฐ + if (path.Count > 0) + { + TotalDistance = path[path.Count - 1].GCost; + EndDirection = path[path.Count - 1].Direction; + } + + TotalRotations = MovementSequence.Count(cmd => + cmd == AgvDirection.Left || cmd == AgvDirection.Right); + + // ํšŒ์ „ ๋…ธ๋“œ ์ถ”์ถœ + var previousDirection = startDirection; + for (int i = 0; i < path.Count; i++) + { + if (path[i].Direction != previousDirection) + { + RotationNodes.Add(path[i].NodeId); + } + previousDirection = path[i].Direction; + } + + // ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„ ๊ณ„์‚ฐ (๋‹จ์ˆœ ์ถ”์ •) + EstimatedTime = CalculateEstimatedTime(); + } + + /// + /// ์‹คํŒจ ๊ฒฐ๊ณผ ์ƒ์„ฑ์ž + /// + public PathResult(string errorMessage) + { + Success = false; + ErrorMessage = errorMessage; + } + + /// + /// ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„ ๊ณ„์‚ฐ + /// + private float CalculateEstimatedTime() + { + // ๊ธฐ๋ณธ ์ด๋™ ์†๋„ ๋ฐ ํšŒ์ „ ์‹œ๊ฐ„ ๊ฐ€์ • + const float MOVE_SPEED = 1.0f; // ๋‹จ์œ„/์ดˆ + const float ROTATION_TIME = 2.0f; // ์ดˆ/ํšŒ์ „ + + float moveTime = TotalDistance / MOVE_SPEED; + float rotationTime = TotalRotations * ROTATION_TIME; + + return moveTime + rotationTime; + } + + /// + /// ๊ฒฝ๋กœ ์š”์•ฝ ์ •๋ณด + /// + public string GetSummary() + { + if (!Success) + { + return $"๊ฒฝ๋กœ ๊ณ„์‚ฐ ์‹คํŒจ: {ErrorMessage}"; + } + + return $"๊ฒฝ๋กœ: {NodeSequence.Count}๊ฐœ ๋…ธ๋“œ, " + + $"๊ฑฐ๋ฆฌ: {TotalDistance:F1}, " + + $"ํšŒ์ „: {TotalRotations}ํšŒ, " + + $"์˜ˆ์ƒ์‹œ๊ฐ„: {EstimatedTime:F1}์ดˆ"; + } + + /// + /// ์ƒ์„ธ ๊ฒฝ๋กœ ์ •๋ณด + /// + public List GetDetailedSteps() + { + var steps = new List(); + + if (!Success) + { + steps.Add($"๊ฒฝ๋กœ ๊ณ„์‚ฐ ์‹คํŒจ: {ErrorMessage}"); + return steps; + } + + steps.Add($"์‹œ์ž‘: {StartNodeId} (๋ฐฉํ–ฅ: {StartDirection})"); + + for (int i = 0; i < DetailedPath.Count; i++) + { + var node = DetailedPath[i]; + var step = $"{i + 1}. {node.NodeId}"; + + if (node.MovementSequence.Count > 0) + { + step += $" [๋ช…๋ น: {string.Join(",", node.MovementSequence)}]"; + } + + step += $" (F:{node.FCost:F1}, ๋ฐฉํ–ฅ:{node.Direction})"; + steps.Add(step); + } + + steps.Add($"๋„์ฐฉ: {TargetNodeId} (์ตœ์ข… ๋ฐฉํ–ฅ: {EndDirection})"); + + return steps; + } + + /// + /// RFID ์‹œํ€€์Šค ์ถ”์ถœ (์‹ค์ œ AGV ์ œ์–ด์šฉ) + /// + public List GetRfidSequence(NodeResolver nodeResolver) + { + var rfidSequence = new List(); + + foreach (var nodeId in NodeSequence) + { + var rfidId = nodeResolver.GetRfidByNodeId(nodeId); + if (!string.IsNullOrEmpty(rfidId)) + { + rfidSequence.Add(rfidId); + } + } + + return rfidSequence; + } + + /// + /// ๊ฒฝ๋กœ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + /// + public bool ValidatePath(List mapNodes) + { + if (!Success || NodeSequence.Count == 0) + return false; + + // ๋ชจ๋“  ๋…ธ๋“œ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + foreach (var nodeId in NodeSequence) + { + if (!mapNodes.Any(n => n.NodeId == nodeId)) + { + ErrorMessage = $"์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋…ธ๋“œ: {nodeId}"; + return false; + } + } + + // ์—ฐ๊ฒฐ์„ฑ ํ™•์ธ + for (int i = 0; i < NodeSequence.Count - 1; i++) + { + var currentNode = mapNodes.FirstOrDefault(n => n.NodeId == NodeSequence[i]); + var nextNodeId = NodeSequence[i + 1]; + + if (currentNode != null && !currentNode.ConnectedNodes.Contains(nextNodeId)) + { + ErrorMessage = $"์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๋…ธ๋“œ: {currentNode.NodeId} โ†’ {nextNodeId}"; + return false; + } + } + + return true; + } + + /// + /// JSON ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ ๋ฌธ์ž์—ด ๋ณ€ํ™˜ + /// + public override string ToString() + { + return GetSummary(); + } + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs b/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs new file mode 100644 index 0000000..9f09b5f --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs @@ -0,0 +1,79 @@ +using System; + +namespace AGVMapEditor.Models +{ + /// + /// RFID์™€ ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID๋ฅผ ๋งคํ•‘ํ•˜๋Š” ํด๋ž˜์Šค + /// ๋ฌผ๋ฆฌ์  RFID๋Š” ์˜๋ฏธ์—†๋Š” ๊ณ ์œ ๊ฐ’, ๋…ผ๋ฆฌ์  ๋…ธ๋“œ๋Š” ๋งต ์—๋””ํ„ฐ์—์„œ ๊ด€๋ฆฌ + /// + public class RfidMapping + { + /// + /// ๋ฌผ๋ฆฌ์  RFID ๊ฐ’ (์˜๋ฏธ ์—†๋Š” ๊ณ ์œ  ์‹๋ณ„์ž) + /// ์˜ˆ: "1234567890", "ABCDEF1234" ๋“ฑ + /// + public string RfidId { get; set; } = string.Empty; + + /// + /// ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID (๋งต ์—๋””ํ„ฐ์—์„œ ๊ด€๋ฆฌ) + /// ์˜ˆ: "N001", "N002", "LOADER1", "CHARGER1" ๋“ฑ + /// + public string LogicalNodeId { get; set; } = string.Empty; + + /// + /// ๋งคํ•‘ ์ƒ์„ฑ ์ผ์ž + /// + public DateTime CreatedDate { get; set; } = DateTime.Now; + + /// + /// ๋งˆ์ง€๋ง‰ ์ˆ˜์ • ์ผ์ž + /// + public DateTime ModifiedDate { get; set; } = DateTime.Now; + + /// + /// ์„ค์น˜ ์œ„์น˜ ์„ค๋ช… (ํ˜„์žฅ ์ž‘์—…์ž์šฉ) + /// ์˜ˆ: "๋กœ๋”1๋ฒˆ ์•ž", "์ถฉ์ „๊ธฐ2๋ฒˆ ์ž…๊ตฌ", "๋ณต๋„ ๊ต์ฐจ์ " ๋“ฑ + /// + public string Description { get; set; } = string.Empty; + + /// + /// RFID ์ƒํƒœ (์ •์ƒ, ์†์ƒ, ๊ต์ฒด์˜ˆ์ • ๋“ฑ) + /// + public string Status { get; set; } = "์ •์ƒ"; + + /// + /// ๋งคํ•‘ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + /// + public bool IsActive { get; set; } = true; + + /// + /// ๊ธฐ๋ณธ ์ƒ์„ฑ์ž + /// + public RfidMapping() + { + } + + /// + /// ๋งค๊ฐœ๋ณ€์ˆ˜ ์ƒ์„ฑ์ž + /// + /// ๋ฌผ๋ฆฌ์  RFID ID + /// ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID + /// ์„ค์น˜ ์œ„์น˜ ์„ค๋ช… + public RfidMapping(string rfidId, string logicalNodeId, string description = "") + { + RfidId = rfidId; + LogicalNodeId = logicalNodeId; + Description = description; + CreatedDate = DateTime.Now; + ModifiedDate = DateTime.Now; + } + + /// + /// ๋ฌธ์ž์—ด ํ‘œํ˜„ + /// + public override string ToString() + { + return $"{RfidId} โ†’ {LogicalNodeId} ({Description})"; + } + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Program.cs b/Cs_HMI/AGVMapEditor/Program.cs new file mode 100644 index 0000000..4d11b9e --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Windows.Forms; +using AGVMapEditor.Forms; + +namespace AGVMapEditor +{ + /// + /// ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์  + /// + internal static class Program + { + /// + /// ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ธฐ๋ณธ ์ง„์ž…์ ์ž…๋‹ˆ๋‹ค. + /// + [STAThread] + static void Main() + { + // Windows Forms ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐํ™” + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + // ๋ฉ”์ธ ํผ ์‹คํ–‰ + Application.Run(new MainForm()); + } + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/Properties/AssemblyInfo.cs b/Cs_HMI/AGVMapEditor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7ef9156 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// ์–ด์…ˆ๋ธ”๋ฆฌ์— ๋Œ€ํ•œ ์ผ๋ฐ˜ ์ •๋ณด๋Š” ๋‹ค์Œ ํŠน์„ฑ ์ง‘ํ•ฉ์„ ํ†ตํ•ด +// ์ œ์–ด๋ฉ๋‹ˆ๋‹ค. ์–ด์…ˆ๋ธ”๋ฆฌ์™€ ๊ด€๋ จ๋œ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•˜๋ ค๋ฉด +// ์ด๋Ÿฌํ•œ ํŠน์„ฑ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜์„ธ์š”. +[assembly: AssemblyTitle("AGV Map Editor")] +[assembly: AssemblyDescription("AGV Navigation Map Editor Tool")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("ENIG AGV")] +[assembly: AssemblyProduct("AGV Map Editor")] +[assembly: AssemblyCopyright("Copyright ยฉ ENIG AGV 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// ComVisible์„ false๋กœ ์„ค์ •ํ•˜๋ฉด ์ด ์–ด์…ˆ๋ธ”๋ฆฌ์˜ ํ˜•์‹์ด COM ๊ตฌ์„ฑ ์š”์†Œ์— +// ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. COM์—์„œ ์ด ์–ด์…ˆ๋ธ”๋ฆฌ์˜ ํ˜•์‹์— ์•ก์„ธ์Šคํ•˜๋ ค๋ฉด +// ํ•ด๋‹น ํ˜•์‹์— ๋Œ€ํ•ด ComVisible ํŠน์„ฑ์„ true๋กœ ์„ค์ •ํ•˜์„ธ์š”. +[assembly: ComVisible(false)] + +// ์ด ํ”„๋กœ์ ํŠธ๊ฐ€ COM์— ๋…ธ์ถœ๋˜๋Š” ๊ฒฝ์šฐ ๋‹ค์Œ GUID๋Š” typelib์˜ ID๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. +[assembly: Guid("a1b2c3d4-e5f6-7890-abcd-ef1234567890")] + +// ์–ด์…ˆ๋ธ”๋ฆฌ์˜ ๋ฒ„์ „ ์ •๋ณด๋Š” ๋‹ค์Œ ๋„ค ๊ฐœ์˜ ๊ฐ’์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. +// +// ์ฃผ ๋ฒ„์ „ +// ๋ถ€ ๋ฒ„์ „ +// ๋นŒ๋“œ ๋ฒˆํ˜ธ +// ์ˆ˜์ • ๋ฒ„์ „ +// +// ๋ชจ๋“  ๊ฐ’์„ ์ง€์ •ํ•˜๊ฑฐ๋‚˜ ์•„๋ž˜์™€ ๊ฐ™์ด '*'๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋นŒ๋“œ ๋ฒˆํ˜ธ ๋ฐ ์ˆ˜์ • ๋ฒˆํ˜ธ๋ฅผ +// ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/Cs_HMI/AGVMapEditor/packages.config b/Cs_HMI/AGVMapEditor/packages.config new file mode 100644 index 0000000..8b1a8d0 --- /dev/null +++ b/Cs_HMI/AGVMapEditor/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/AGVSimulator.csproj b/Cs_HMI/AGVSimulator/AGVSimulator.csproj new file mode 100644 index 0000000..c9f0cdc --- /dev/null +++ b/Cs_HMI/AGVSimulator/AGVSimulator.csproj @@ -0,0 +1,80 @@ + + + + + Debug + AnyCPU + {B2C3D4E5-F6G7-8901-BCDE-F23456789012} + WinExe + AGVSimulator + AGVSimulator + v4.8 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + + + + + + + UserControl + + + SimulatorCanvas.cs + + + Form + + + SimulatorForm.cs + + + + + + + SimulatorCanvas.cs + + + SimulatorForm.cs + + + + + + + + {a1b2c3d4-e5f6-7890-abcd-ef1234567890} + AGVMapEditor + + + + \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.Designer.cs b/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.Designer.cs new file mode 100644 index 0000000..1a38fbb --- /dev/null +++ b/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.Designer.cs @@ -0,0 +1,52 @@ +namespace AGVSimulator.Controls +{ + partial class SimulatorCanvas + { + /// + /// ํ•„์ˆ˜ ๋””์ž์ด๋„ˆ ๋ณ€์ˆ˜์ž…๋‹ˆ๋‹ค. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// ์‚ฌ์šฉ ์ค‘์ธ ๋ชจ๋“  ๋ฆฌ์†Œ์Šค๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + /// + /// ๊ด€๋ฆฌ๋˜๋Š” ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•˜๋ฉด true์ด๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false์ž…๋‹ˆ๋‹ค. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (components != null) + { + components.Dispose(); + } + + // ์ปค์Šคํ…€ ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ + CleanupResources(); + } + base.Dispose(disposing); + } + + #region ๊ตฌ์„ฑ ์š”์†Œ ๋””์ž์ด๋„ˆ์—์„œ ์ƒ์„ฑํ•œ ์ฝ”๋“œ + + /// + /// ๋””์ž์ด๋„ˆ ์ง€์›์— ํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + /// ์ด ๋ฉ”์„œ๋“œ์˜ ๋‚ด์šฉ์„ ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ๋กœ ์ˆ˜์ •ํ•˜์ง€ ๋งˆ์„ธ์š”. + /// + private void InitializeComponent() + { + this.SuspendLayout(); + // + // SimulatorCanvas + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.White; + this.Name = "SimulatorCanvas"; + this.Size = new System.Drawing.Size(800, 600); + this.ResumeLayout(false); + + } + + #endregion + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs b/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs new file mode 100644 index 0000000..dfd2402 --- /dev/null +++ b/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs @@ -0,0 +1,620 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using AGVMapEditor.Models; +using AGVSimulator.Models; + +namespace AGVSimulator.Controls +{ + /// + /// AGV ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ๊ฐํ™” ์บ”๋ฒ„์Šค + /// + public partial class SimulatorCanvas : UserControl + { + #region Fields + + private List _mapNodes; + private List _agvList; + private PathResult _currentPath; + + // ๊ทธ๋ž˜ํ”ฝ ์„ค์ • + private float _zoom = 1.0f; + private Point _panOffset = Point.Empty; + private bool _isPanning = false; + private Point _lastMousePos = Point.Empty; + + // ์ƒ‰์ƒ ์„ค์ • + private readonly Brush _normalNodeBrush = Brushes.LightBlue; + private readonly Brush _rotationNodeBrush = Brushes.Yellow; + private readonly Brush _dockingNodeBrush = Brushes.Orange; + private readonly Brush _chargingNodeBrush = Brushes.Green; + private readonly Brush _agvBrush = Brushes.Red; + private readonly Brush _pathBrush = Brushes.Purple; + + private readonly Pen _connectionPen = new Pen(Color.Gray, 2); + private readonly Pen _pathPen = new Pen(Color.Purple, 3); + private readonly Pen _agvPen = new Pen(Color.Red, 3); + + // ํฌ๊ธฐ ์„ค์ • + private const int NODE_SIZE = 20; + private const int AGV_SIZE = 30; + private const int CONNECTION_ARROW_SIZE = 8; + + #endregion + + #region Properties + + /// + /// ๋งต ๋…ธ๋“œ ๋ชฉ๋ก + /// + public List MapNodes + { + get => _mapNodes; + set + { + _mapNodes = value; + Invalidate(); + } + } + + /// + /// AGV ๋ชฉ๋ก + /// + public List AGVList + { + get => _agvList; + set + { + _agvList = value; + Invalidate(); + } + } + + /// + /// ํ˜„์žฌ ๊ฒฝ๋กœ + /// + public PathResult CurrentPath + { + get => _currentPath; + set + { + _currentPath = value; + Invalidate(); + } + } + + #endregion + + #region Constructor + + public SimulatorCanvas() + { + InitializeComponent(); + InitializeCanvas(); + } + + #endregion + + #region Initialization + + private void InitializeCanvas() + { + _mapNodes = new List(); + _agvList = new List(); + + SetStyle(ControlStyles.AllPaintingInWmPaint | + ControlStyles.UserPaint | + ControlStyles.DoubleBuffer | + ControlStyles.ResizeRedraw, true); + + BackColor = Color.White; + + // ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ + MouseDown += OnMouseDown; + MouseMove += OnMouseMove; + MouseUp += OnMouseUp; + MouseWheel += OnMouseWheel; + } + + #endregion + + #region Public Methods + + /// + /// AGV ์ถ”๊ฐ€ + /// + public void AddAGV(VirtualAGV agv) + { + if (_agvList == null) + _agvList = new List(); + + _agvList.Add(agv); + + // AGV ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ + agv.PositionChanged += OnAGVPositionChanged; + agv.StateChanged += OnAGVStateChanged; + + Invalidate(); + } + + /// + /// AGV ์ œ๊ฑฐ + /// + public void RemoveAGV(string agvId) + { + var agv = _agvList?.FirstOrDefault(a => a.AgvId == agvId); + if (agv != null) + { + // ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ ํ•ด์ œ + agv.PositionChanged -= OnAGVPositionChanged; + agv.StateChanged -= OnAGVStateChanged; + + _agvList.Remove(agv); + Invalidate(); + } + } + + /// + /// ๋ชจ๋“  AGV ์ œ๊ฑฐ + /// + public void ClearAGVs() + { + if (_agvList != null) + { + foreach (var agv in _agvList) + { + agv.PositionChanged -= OnAGVPositionChanged; + agv.StateChanged -= OnAGVStateChanged; + } + _agvList.Clear(); + Invalidate(); + } + } + + /// + /// ํ™•๋Œ€/์ถ•์†Œ ์ดˆ๊ธฐํ™” + /// + public void ResetZoom() + { + _zoom = 1.0f; + _panOffset = Point.Empty; + Invalidate(); + } + + /// + /// ๋งต ์ „์ฒด ๋งž์ถค + /// + public void FitToMap() + { + if (_mapNodes == null || _mapNodes.Count == 0) + return; + + var minX = _mapNodes.Min(n => n.Position.X); + var maxX = _mapNodes.Max(n => n.Position.X); + var minY = _mapNodes.Min(n => n.Position.Y); + var maxY = _mapNodes.Max(n => n.Position.Y); + + var mapWidth = maxX - minX + 100; // ์—ฌ๋ฐฑ ์ถ”๊ฐ€ + var mapHeight = maxY - minY + 100; + + var zoomX = (float)Width / mapWidth; + var zoomY = (float)Height / mapHeight; + _zoom = Math.Min(zoomX, zoomY) * 0.9f; // ์•ฝ๊ฐ„์˜ ์—ฌ๋ฐฑ + + _panOffset = new Point( + (int)((Width - mapWidth * _zoom) / 2 - minX * _zoom), + (int)((Height - mapHeight * _zoom) / 2 - minY * _zoom) + ); + + Invalidate(); + } + + #endregion + + #region Event Handlers + + private void OnAGVPositionChanged(object sender, Point newPosition) + { + Invalidate(); // AGV ์œ„์น˜ ๋ณ€๊ฒฝ์‹œ ํ™”๋ฉด ๊ฐฑ์‹  + } + + private void OnAGVStateChanged(object sender, AGVState newState) + { + Invalidate(); // AGV ์ƒํƒœ ๋ณ€๊ฒฝ์‹œ ํ™”๋ฉด ๊ฐฑ์‹  + } + + private void OnMouseDown(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Right) + { + _isPanning = true; + _lastMousePos = e.Location; + Cursor = Cursors.Hand; + } + } + + private void OnMouseMove(object sender, MouseEventArgs e) + { + if (_isPanning) + { + var deltaX = e.X - _lastMousePos.X; + var deltaY = e.Y - _lastMousePos.Y; + + _panOffset = new Point( + _panOffset.X + deltaX, + _panOffset.Y + deltaY + ); + + _lastMousePos = e.Location; + Invalidate(); + } + } + + private void OnMouseUp(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Right) + { + _isPanning = false; + Cursor = Cursors.Default; + } + } + + private void OnMouseWheel(object sender, MouseEventArgs e) + { + var zoomFactor = e.Delta > 0 ? 1.1f : 0.9f; + var newZoom = _zoom * zoomFactor; + + if (newZoom >= 0.1f && newZoom <= 10.0f) + { + // ๋งˆ์šฐ์Šค ์œ„์น˜ ๊ธฐ์ค€์œผ๋กœ ์คŒ + var mouseX = e.X - _panOffset.X; + var mouseY = e.Y - _panOffset.Y; + + _panOffset = new Point( + (int)(_panOffset.X - mouseX * (zoomFactor - 1)), + (int)(_panOffset.Y - mouseY * (zoomFactor - 1)) + ); + + _zoom = newZoom; + Invalidate(); + } + } + + #endregion + + #region Painting + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + var g = e.Graphics; + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + + // ๋ณ€ํ™˜ ํ–‰๋ ฌ ์„ค์ • + g.TranslateTransform(_panOffset.X, _panOffset.Y); + g.ScaleTransform(_zoom, _zoom); + + // ๋ฐฐ๊ฒฝ ๊ทธ๋ฆฌ๋“œ ๊ทธ๋ฆฌ๊ธฐ + DrawGrid(g); + + // ๋งต ๋…ธ๋“œ ์—ฐ๊ฒฐ์„  ๊ทธ๋ฆฌ๊ธฐ + DrawNodeConnections(g); + + // ๊ฒฝ๋กœ ๊ทธ๋ฆฌ๊ธฐ + if (_currentPath != null && _currentPath.Success) + { + DrawPath(g); + } + + // ๋งต ๋…ธ๋“œ ๊ทธ๋ฆฌ๊ธฐ + DrawMapNodes(g); + + // AGV ๊ทธ๋ฆฌ๊ธฐ + DrawAGVs(g); + + // ์ •๋ณด ํ‘œ์‹œ (๋ณ€ํ™˜ ํ•ด์ œ) + g.ResetTransform(); + DrawInfo(g); + } + + private void DrawGrid(Graphics g) + { + var gridSize = 50; + var pen = new Pen(Color.LightGray, 1); + + var startX = -(int)(_panOffset.X / _zoom / gridSize) * gridSize; + var startY = -(int)(_panOffset.Y / _zoom / gridSize) * gridSize; + var endX = startX + (int)(Width / _zoom) + gridSize; + var endY = startY + (int)(Height / _zoom) + gridSize; + + for (int x = startX; x <= endX; x += gridSize) + { + g.DrawLine(pen, x, startY, x, endY); + } + + for (int y = startY; y <= endY; y += gridSize) + { + g.DrawLine(pen, startX, y, endX, y); + } + + pen.Dispose(); + } + + private void DrawNodeConnections(Graphics g) + { + if (_mapNodes == null) return; + + foreach (var node in _mapNodes) + { + if (node.ConnectedNodes != null) + { + foreach (var connectedNodeId in node.ConnectedNodes) + { + var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); + if (connectedNode != null) + { + DrawConnection(g, node.Position, connectedNode.Position); + } + } + } + } + } + + private void DrawConnection(Graphics g, Point from, Point to) + { + g.DrawLine(_connectionPen, from, to); + + // ๋ฐฉํ–ฅ ํ™”์‚ดํ‘œ ๊ทธ๋ฆฌ๊ธฐ + var angle = Math.Atan2(to.Y - from.Y, to.X - from.X); + var arrowX = to.X - CONNECTION_ARROW_SIZE * Math.Cos(angle); + var arrowY = to.Y - CONNECTION_ARROW_SIZE * Math.Sin(angle); + + var arrowPoint1 = new PointF( + (float)(arrowX - CONNECTION_ARROW_SIZE * Math.Cos(angle - Math.PI / 6)), + (float)(arrowY - CONNECTION_ARROW_SIZE * Math.Sin(angle - Math.PI / 6)) + ); + + var arrowPoint2 = new PointF( + (float)(arrowX - CONNECTION_ARROW_SIZE * Math.Cos(angle + Math.PI / 6)), + (float)(arrowY - CONNECTION_ARROW_SIZE * Math.Sin(angle + Math.PI / 6)) + ); + + g.DrawLine(_connectionPen, to, arrowPoint1); + g.DrawLine(_connectionPen, to, arrowPoint2); + } + + private void DrawPath(Graphics g) + { + if (_currentPath?.NodeSequence == null || _currentPath.NodeSequence.Count < 2) + return; + + for (int i = 0; i < _currentPath.NodeSequence.Count - 1; i++) + { + var currentNodeId = _currentPath.NodeSequence[i]; + var nextNodeId = _currentPath.NodeSequence[i + 1]; + + var currentNode = _mapNodes?.FirstOrDefault(n => n.NodeId == currentNodeId); + var nextNode = _mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId); + + if (currentNode != null && nextNode != null) + { + g.DrawLine(_pathPen, currentNode.Position, nextNode.Position); + } + } + } + + private void DrawMapNodes(Graphics g) + { + if (_mapNodes == null) return; + + foreach (var node in _mapNodes) + { + DrawMapNode(g, node); + } + } + + private void DrawMapNode(Graphics g, MapNode node) + { + var brush = GetNodeBrush(node.Type); + var rect = new Rectangle( + node.Position.X - NODE_SIZE / 2, + node.Position.Y - NODE_SIZE / 2, + NODE_SIZE, + NODE_SIZE + ); + + // ๋…ธ๋“œ ๊ทธ๋ฆฌ๊ธฐ + if (node.Type == NodeType.Rotation) + { + g.FillEllipse(brush, rect); // ํšŒ์ „ ๋…ธ๋“œ๋Š” ์›ํ˜• + } + else + { + g.FillRectangle(brush, rect); // ์ผ๋ฐ˜ ๋…ธ๋“œ๋Š” ์‚ฌ๊ฐํ˜• + } + + g.DrawRectangle(Pens.Black, rect); + + // ๋…ธ๋“œ ID ํ‘œ์‹œ + var font = new Font("Arial", 8); + var textSize = g.MeasureString(node.NodeId, font); + var textPos = new PointF( + node.Position.X - textSize.Width / 2, + node.Position.Y + NODE_SIZE / 2 + 2 + ); + + g.DrawString(node.NodeId, font, Brushes.Black, textPos); + font.Dispose(); + } + + private Brush GetNodeBrush(NodeType nodeType) + { + switch (nodeType) + { + case NodeType.Rotation: return _rotationNodeBrush; + case NodeType.Docking: return _dockingNodeBrush; + case NodeType.Charging: return _chargingNodeBrush; + default: return _normalNodeBrush; + } + } + + private void DrawAGVs(Graphics g) + { + if (_agvList == null) return; + + foreach (var agv in _agvList) + { + DrawAGV(g, agv); + } + } + + private void DrawAGV(Graphics g, VirtualAGV agv) + { + var position = agv.CurrentPosition; + var rect = new Rectangle( + position.X - AGV_SIZE / 2, + position.Y - AGV_SIZE / 2, + AGV_SIZE, + AGV_SIZE + ); + + // AGV ์ƒํƒœ์— ๋”ฐ๋ฅธ ์ƒ‰์ƒ ๋ณ€๊ฒฝ + var brush = GetAGVBrush(agv.CurrentState); + + // AGV ๋ณธ์ฒด ๊ทธ๋ฆฌ๊ธฐ + g.FillEllipse(brush, rect); + g.DrawEllipse(_agvPen, rect); + + // ๋ฐฉํ–ฅ ํ‘œ์‹œ + DrawAGVDirection(g, position, agv.CurrentDirection); + + // AGV ID ํ‘œ์‹œ + var font = new Font("Arial", 10, FontStyle.Bold); + var textSize = g.MeasureString(agv.AgvId, font); + var textPos = new PointF( + position.X - textSize.Width / 2, + position.Y + AGV_SIZE / 2 + 5 + ); + + g.DrawString(agv.AgvId, font, Brushes.Black, textPos); + font.Dispose(); + } + + private Brush GetAGVBrush(AGVState state) + { + switch (state) + { + case AGVState.Moving: return Brushes.Blue; + case AGVState.Rotating: return Brushes.Yellow; + case AGVState.Docking: return Brushes.Orange; + case AGVState.Charging: return Brushes.Green; + case AGVState.Error: return Brushes.Red; + default: return Brushes.Gray; // Idle + } + } + + private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction) + { + var arrowSize = 10; + var pen = new Pen(Color.White, 2); + + switch (direction) + { + case AgvDirection.Forward: + // ์œ„์ชฝ ํ™”์‚ดํ‘œ + g.DrawLine(pen, position.X, position.Y - arrowSize, position.X, position.Y + arrowSize); + g.DrawLine(pen, position.X, position.Y - arrowSize, position.X - 5, position.Y - arrowSize + 5); + g.DrawLine(pen, position.X, position.Y - arrowSize, position.X + 5, position.Y - arrowSize + 5); + break; + + case AgvDirection.Backward: + // ์•„๋ž˜์ชฝ ํ™”์‚ดํ‘œ + g.DrawLine(pen, position.X, position.Y - arrowSize, position.X, position.Y + arrowSize); + g.DrawLine(pen, position.X, position.Y + arrowSize, position.X - 5, position.Y + arrowSize - 5); + g.DrawLine(pen, position.X, position.Y + arrowSize, position.X + 5, position.Y + arrowSize - 5); + break; + + case AgvDirection.Left: + // ์™ผ์ชฝ ํ™”์‚ดํ‘œ + g.DrawLine(pen, position.X - arrowSize, position.Y, position.X + arrowSize, position.Y); + g.DrawLine(pen, position.X - arrowSize, position.Y, position.X - arrowSize + 5, position.Y - 5); + g.DrawLine(pen, position.X - arrowSize, position.Y, position.X - arrowSize + 5, position.Y + 5); + break; + + case AgvDirection.Right: + // ์˜ค๋ฅธ์ชฝ ํ™”์‚ดํ‘œ + g.DrawLine(pen, position.X - arrowSize, position.Y, position.X + arrowSize, position.Y); + g.DrawLine(pen, position.X + arrowSize, position.Y, position.X + arrowSize - 5, position.Y - 5); + g.DrawLine(pen, position.X + arrowSize, position.Y, position.X + arrowSize - 5, position.Y + 5); + break; + } + + pen.Dispose(); + } + + private void DrawInfo(Graphics g) + { + var font = new Font("Arial", 10); + var brush = Brushes.Black; + var y = 10; + + // ์คŒ ๋ ˆ๋ฒจ ํ‘œ์‹œ + g.DrawString($"์คŒ: {_zoom:P0}", font, brush, new PointF(10, y)); + y += 20; + + // AGV ์ •๋ณด ํ‘œ์‹œ + if (_agvList != null) + { + g.DrawString($"AGV ์ˆ˜: {_agvList.Count}", font, brush, new PointF(10, y)); + y += 20; + + foreach (var agv in _agvList) + { + var info = $"{agv.AgvId}: {agv.CurrentState} ({agv.CurrentPosition.X},{agv.CurrentPosition.Y})"; + g.DrawString(info, font, brush, new PointF(10, y)); + y += 15; + } + } + + // ๊ฒฝ๋กœ ์ •๋ณด ํ‘œ์‹œ + if (_currentPath != null && _currentPath.Success) + { + y += 10; + g.DrawString($"๊ฒฝ๋กœ: {_currentPath.NodeSequence.Count}๊ฐœ ๋…ธ๋“œ", font, brush, new PointF(10, y)); + y += 15; + g.DrawString($"๊ฑฐ๋ฆฌ: {_currentPath.TotalDistance:F1}", font, brush, new PointF(10, y)); + y += 15; + g.DrawString($"๊ณ„์‚ฐ์‹œ๊ฐ„: {_currentPath.CalculationTime}ms", font, brush, new PointF(10, y)); + } + + font.Dispose(); + } + + #endregion + + #region Cleanup + + private void CleanupResources() + { + // AGV ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ ํ•ด์ œ + if (_agvList != null) + { + foreach (var agv in _agvList) + { + agv.PositionChanged -= OnAGVPositionChanged; + agv.StateChanged -= OnAGVStateChanged; + } + } + + // ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ + _connectionPen?.Dispose(); + _pathPen?.Dispose(); + _agvPen?.Dispose(); + } + + #endregion + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx b/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx new file mode 100644 index 0000000..c50400a --- /dev/null +++ b/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs new file mode 100644 index 0000000..89ce9bb --- /dev/null +++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs @@ -0,0 +1,64 @@ +namespace AGVSimulator.Forms +{ + partial class SimulatorForm + { + /// + /// ํ•„์ˆ˜ ๋””์ž์ด๋„ˆ ๋ณ€์ˆ˜์ž…๋‹ˆ๋‹ค. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// ์‚ฌ์šฉ ์ค‘์ธ ๋ชจ๋“  ๋ฆฌ์†Œ์Šค๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + /// + /// ๊ด€๋ฆฌ๋˜๋Š” ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•˜๋ฉด true์ด๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false์ž…๋‹ˆ๋‹ค. + 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 ๋””์ž์ด๋„ˆ์—์„œ ์ƒ์„ฑํ•œ ์ฝ”๋“œ + + /// + /// ๋””์ž์ด๋„ˆ ์ง€์›์— ํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + /// ์ด ๋ฉ”์„œ๋“œ์˜ ๋‚ด์šฉ์„ ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ๋กœ ์ˆ˜์ •ํ•˜์ง€ ๋งˆ์„ธ์š”. + /// + 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 + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs new file mode 100644 index 0000000..b0c3baa --- /dev/null +++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs @@ -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 +{ + /// + /// AGV ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋ฉ”์ธ ํผ + /// + public partial class SimulatorForm : Form + { + #region Fields + + private SimulatorCanvas _simulatorCanvas; + private List _mapNodes; + private List _rfidMappings; + private NodeResolver _nodeResolver; + private PathCalculator _pathCalculator; + private List _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 + + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒํƒœ + /// + 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(); + _rfidMappings = new List(); + _agvList = new List(); + _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(json); + + if (mapData != null) + { + _mapNodes = mapData.MapNodes ?? new List(); + _rfidMappings = mapData.RfidMappings ?? new List(); + } + else + { + _mapNodes = new List(); + _rfidMappings = new List(); + } + + // 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 MapNodes { get; set; } + public List 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 + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx new file mode 100644 index 0000000..b45c916 --- /dev/null +++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Models/SimulationState.cs b/Cs_HMI/AGVSimulator/Models/SimulationState.cs new file mode 100644 index 0000000..1c05bd3 --- /dev/null +++ b/Cs_HMI/AGVSimulator/Models/SimulationState.cs @@ -0,0 +1,135 @@ +using System; + +namespace AGVSimulator.Models +{ + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒํƒœ ๊ด€๋ฆฌ ํด๋ž˜์Šค + /// + public class SimulationState + { + #region Properties + + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰ ์ค‘ ์—ฌ๋ถ€ + /// + public bool IsRunning { get; set; } + + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘ ์‹œ๊ฐ„ + /// + public DateTime? StartTime { get; set; } + + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๊ฒฝ๊ณผ ์‹œ๊ฐ„ + /// + public TimeSpan ElapsedTime => StartTime.HasValue ? DateTime.Now - StartTime.Value : TimeSpan.Zero; + + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์†๋„ ๋ฐฐ์œจ (1.0 = ์‹ค์‹œ๊ฐ„, 2.0 = 2๋ฐฐ์†) + /// + public float SpeedMultiplier { get; set; } = 1.0f; + + /// + /// ์ด ์ฒ˜๋ฆฌ๋œ ์ด๋ฒคํŠธ ์ˆ˜ + /// + public int TotalEvents { get; set; } + + /// + /// ์ด ์ด๋™ ๊ฑฐ๋ฆฌ (๋ชจ๋“  AGV ํ•ฉ๊ณ„) + /// + public float TotalDistance { get; set; } + + /// + /// ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜ ์ˆ˜ + /// + public int ErrorCount { get; set; } + + #endregion + + #region Constructor + + /// + /// ๊ธฐ๋ณธ ์ƒ์„ฑ์ž + /// + public SimulationState() + { + Reset(); + } + + #endregion + + #region Public Methods + + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘ + /// + public void Start() + { + if (!IsRunning) + { + IsRunning = true; + StartTime = DateTime.Now; + } + } + + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ •์ง€ + /// + public void Stop() + { + IsRunning = false; + } + + /// + /// ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒํƒœ ์ดˆ๊ธฐํ™” + /// + public void Reset() + { + IsRunning = false; + StartTime = null; + SpeedMultiplier = 1.0f; + TotalEvents = 0; + TotalDistance = 0; + ErrorCount = 0; + } + + /// + /// ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ ํ˜ธ์ถœ + /// + public void RecordEvent() + { + TotalEvents++; + } + + /// + /// ์ด๋™ ๊ฑฐ๋ฆฌ ์ถ”๊ฐ€ + /// + /// ์ด๋™ํ•œ ๊ฑฐ๋ฆฌ + public void AddDistance(float distance) + { + TotalDistance += distance; + } + + /// + /// ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํ˜ธ์ถœ + /// + public void RecordError() + { + ErrorCount++; + } + + /// + /// ํ†ต๊ณ„ ์ •๋ณด ์กฐํšŒ + /// + /// ํ†ต๊ณ„ ์ •๋ณด ๋ฌธ์ž์—ด + public string GetStatistics() + { + return $"์‹คํ–‰์‹œ๊ฐ„: {ElapsedTime:hh\\:mm\\:ss}, " + + $"์ด๋ฒคํŠธ: {TotalEvents}, " + + $"์ด๊ฑฐ๋ฆฌ: {TotalDistance:F1}, " + + $"์˜ค๋ฅ˜: {ErrorCount}"; + } + + #endregion + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs new file mode 100644 index 0000000..ca3cfd8 --- /dev/null +++ b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs @@ -0,0 +1,482 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using AGVMapEditor.Models; + +namespace AGVSimulator.Models +{ + /// + /// ๊ฐ€์ƒ AGV ์ƒํƒœ + /// + public enum AGVState + { + Idle, // ๋Œ€๊ธฐ + Moving, // ์ด๋™ ์ค‘ + Rotating, // ํšŒ์ „ ์ค‘ + Docking, // ๋„ํ‚น ์ค‘ + Charging, // ์ถฉ์ „ ์ค‘ + Error // ์˜ค๋ฅ˜ + } + + /// + /// ๊ฐ€์ƒ AGV ํด๋ž˜์Šค + /// ์‹ค์ œ AGV์˜ ๋™์ž‘์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + /// + public class VirtualAGV + { + #region Events + + /// + /// AGV ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ + /// + public event EventHandler StateChanged; + + /// + /// ์œ„์น˜ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ + /// + public event EventHandler PositionChanged; + + /// + /// RFID ๊ฐ์ง€ ์ด๋ฒคํŠธ + /// + public event EventHandler RfidDetected; + + /// + /// ๊ฒฝ๋กœ ์™„๋ฃŒ ์ด๋ฒคํŠธ + /// + public event EventHandler PathCompleted; + + /// + /// ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์ด๋ฒคํŠธ + /// + public event EventHandler ErrorOccurred; + + #endregion + + #region Fields + + private string _agvId; + private Point _currentPosition; + private Point _targetPosition; + private AgvDirection _currentDirection; + private AGVState _currentState; + private float _currentSpeed; + + // ๊ฒฝ๋กœ ๊ด€๋ จ + private PathResult _currentPath; + private List _remainingNodes; + private int _currentNodeIndex; + private string _currentNodeId; + + // ์ด๋™ ๊ด€๋ จ + private System.Windows.Forms.Timer _moveTimer; + private DateTime _lastMoveTime; + private Point _moveStartPosition; + private Point _moveTargetPosition; + private float _moveProgress; + + // ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์„ค์ • + private readonly float _moveSpeed = 50.0f; // ํ”ฝ์…€/์ดˆ + private readonly float _rotationSpeed = 90.0f; // ๋„/์ดˆ + private readonly int _updateInterval = 50; // ms + + #endregion + + #region Properties + + /// + /// AGV ID + /// + public string AgvId => _agvId; + + /// + /// ํ˜„์žฌ ์œ„์น˜ + /// + public Point CurrentPosition => _currentPosition; + + /// + /// ํ˜„์žฌ ๋ฐฉํ–ฅ + /// + public AgvDirection CurrentDirection => _currentDirection; + + /// + /// ํ˜„์žฌ ์ƒํƒœ + /// + public AGVState CurrentState => _currentState; + + /// + /// ํ˜„์žฌ ์†๋„ + /// + public float CurrentSpeed => _currentSpeed; + + /// + /// ํ˜„์žฌ ๊ฒฝ๋กœ + /// + public PathResult CurrentPath => _currentPath; + + /// + /// ํ˜„์žฌ ๋…ธ๋“œ ID + /// + public string CurrentNodeId => _currentNodeId; + + /// + /// ๋ชฉํ‘œ ์œ„์น˜ + /// + public Point TargetPosition => _targetPosition; + + /// + /// ๋ฐฐํ„ฐ๋ฆฌ ๋ ˆ๋ฒจ (์‹œ๋ฎฌ๋ ˆ์ด์…˜) + /// + public float BatteryLevel { get; set; } = 100.0f; + + #endregion + + #region Constructor + + /// + /// ์ƒ์„ฑ์ž + /// + /// AGV ID + /// ์‹œ์ž‘ ์œ„์น˜ + /// ์‹œ์ž‘ ๋ฐฉํ–ฅ + public VirtualAGV(string agvId, Point startPosition, AgvDirection startDirection = AgvDirection.Forward) + { + _agvId = agvId; + _currentPosition = startPosition; + _currentDirection = startDirection; + _currentState = AGVState.Idle; + _currentSpeed = 0; + + InitializeTimer(); + } + + #endregion + + #region Initialization + + private void InitializeTimer() + { + _moveTimer = new System.Windows.Forms.Timer(); + _moveTimer.Interval = _updateInterval; + _moveTimer.Tick += OnMoveTimer_Tick; + _lastMoveTime = DateTime.Now; + } + + #endregion + + #region Public Methods + + /// + /// ๊ฒฝ๋กœ ์‹คํ–‰ ์‹œ์ž‘ + /// + /// ์‹คํ–‰ํ•  ๊ฒฝ๋กœ + /// ๋งต ๋…ธ๋“œ ๋ชฉ๋ก + public void StartPath(PathResult path, List mapNodes) + { + if (path == null || !path.Success) + { + OnError("์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค."); + return; + } + + _currentPath = path; + _remainingNodes = new List(path.NodeSequence); + _currentNodeIndex = 0; + + // ์‹œ์ž‘ ๋…ธ๋“œ ์œ„์น˜๋กœ ์ด๋™ + if (_remainingNodes.Count > 0) + { + var startNode = mapNodes.FirstOrDefault(n => n.NodeId == _remainingNodes[0]); + if (startNode != null) + { + _currentNodeId = startNode.NodeId; + StartMovement(); + } + else + { + OnError($"์‹œ์ž‘ ๋…ธ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {_remainingNodes[0]}"); + } + } + } + + /// + /// ๊ฒฝ๋กœ ์ •์ง€ + /// + public void StopPath() + { + _moveTimer.Stop(); + _currentPath = null; + _remainingNodes?.Clear(); + SetState(AGVState.Idle); + _currentSpeed = 0; + } + + /// + /// ๊ธด๊ธ‰ ์ •์ง€ + /// + public void EmergencyStop() + { + StopPath(); + OnError("๊ธด๊ธ‰ ์ •์ง€๊ฐ€ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + /// + /// ์ˆ˜๋™ ์ด๋™ (ํ…Œ์ŠคํŠธ์šฉ) + /// + /// ๋ชฉํ‘œ ์œ„์น˜ + public void MoveTo(Point targetPosition) + { + _targetPosition = targetPosition; + _moveStartPosition = _currentPosition; + _moveTargetPosition = targetPosition; + _moveProgress = 0; + + SetState(AGVState.Moving); + _moveTimer.Start(); + } + + /// + /// ์ˆ˜๋™ ํšŒ์ „ (ํ…Œ์ŠคํŠธ์šฉ) + /// + /// ํšŒ์ „ ๋ฐฉํ–ฅ + public void Rotate(AgvDirection direction) + { + if (_currentState != AGVState.Idle) + return; + + SetState(AGVState.Rotating); + + // ์‹œ๋ฎฌ๋ ˆ์ด์…˜: ์ฆ‰์‹œ ๋ฐฉํ–ฅ ๋ณ€๊ฒฝ (์‹ค์ œ๋กœ๋Š” ์‹œ๊ฐ„์ด ๊ฑธ๋ฆผ) + _currentDirection = direction; + + System.Threading.Thread.Sleep(500); // ํšŒ์ „ ์‹œ๊ฐ„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + SetState(AGVState.Idle); + } + + /// + /// ์ถฉ์ „ ์‹œ์ž‘ (์‹œ๋ฎฌ๋ ˆ์ด์…˜) + /// + public void StartCharging() + { + if (_currentState == AGVState.Idle) + { + SetState(AGVState.Charging); + // ์ถฉ์ „ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘ + } + } + + /// + /// ์ถฉ์ „ ์ข…๋ฃŒ + /// + public void StopCharging() + { + if (_currentState == AGVState.Charging) + { + SetState(AGVState.Idle); + } + } + + /// + /// AGV ์ •๋ณด ์กฐํšŒ + /// + public string GetStatus() + { + return $"AGV[{_agvId}] ์œ„์น˜:({_currentPosition.X},{_currentPosition.Y}) " + + $"๋ฐฉํ–ฅ:{_currentDirection} ์ƒํƒœ:{_currentState} " + + $"์†๋„:{_currentSpeed:F1} ๋ฐฐํ„ฐ๋ฆฌ:{BatteryLevel:F1}%"; + } + + /// + /// ํ˜„์žฌ RFID ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (ํ˜„์žฌ ์œ„์น˜ ๊ธฐ์ค€) + /// + public string SimulateRfidReading(List mapNodes, List rfidMappings) + { + // ํ˜„์žฌ ์œ„์น˜์—์„œ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋…ธ๋“œ ์ฐพ๊ธฐ + var closestNode = FindClosestNode(_currentPosition, mapNodes); + if (closestNode == null) + return null; + + // ํ•ด๋‹น ๋…ธ๋“œ์˜ RFID ๋งคํ•‘ ์ฐพ๊ธฐ + var mapping = rfidMappings.FirstOrDefault(m => m.LogicalNodeId == closestNode.NodeId); + return mapping?.RfidId; + } + + #endregion + + #region Private Methods + + private void StartMovement() + { + SetState(AGVState.Moving); + _moveTimer.Start(); + _lastMoveTime = DateTime.Now; + } + + private void OnMoveTimer_Tick(object sender, EventArgs e) + { + var now = DateTime.Now; + var deltaTime = (float)(now - _lastMoveTime).TotalSeconds; + _lastMoveTime = now; + + UpdateMovement(deltaTime); + UpdateBattery(deltaTime); + + // ์œ„์น˜ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + PositionChanged?.Invoke(this, _currentPosition); + } + + private void UpdateMovement(float deltaTime) + { + if (_currentState != AGVState.Moving) + return; + + // ๋ชฉํ‘œ ์œ„์น˜๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ + var distance = CalculateDistance(_currentPosition, _moveTargetPosition); + + if (distance < 5.0f) // ๋„๋‹ฌ ์ž„๊ณ„๊ฐ’ + { + // ๋ชฉํ‘œ ๋„๋‹ฌ + _currentPosition = _moveTargetPosition; + _currentSpeed = 0; + + // ๋‹ค์Œ ๋…ธ๋“œ๋กœ ์ด๋™ + ProcessNextNode(); + } + else + { + // ๊ณ„์† ์ด๋™ + var moveDistance = _moveSpeed * deltaTime; + var direction = new PointF( + _moveTargetPosition.X - _currentPosition.X, + _moveTargetPosition.Y - _currentPosition.Y + ); + + // ์ •๊ทœํ™” + var length = (float)Math.Sqrt(direction.X * direction.X + direction.Y * direction.Y); + if (length > 0) + { + direction.X /= length; + direction.Y /= length; + } + + // ์ƒˆ ์œ„์น˜ ๊ณ„์‚ฐ + _currentPosition = new Point( + (int)(_currentPosition.X + direction.X * moveDistance), + (int)(_currentPosition.Y + direction.Y * moveDistance) + ); + + _currentSpeed = _moveSpeed; + } + } + + private void UpdateBattery(float deltaTime) + { + // ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + if (_currentState == AGVState.Moving) + { + BatteryLevel -= 0.1f * deltaTime; // ์ด๋™์‹œ ์†Œ๋ชจ + } + else if (_currentState == AGVState.Charging) + { + BatteryLevel += 5.0f * deltaTime; // ์ถฉ์ „ + BatteryLevel = Math.Min(100.0f, BatteryLevel); + } + + BatteryLevel = Math.Max(0, BatteryLevel); + + // ๋ฐฐํ„ฐ๋ฆฌ ๋ถ€์กฑ ๊ฒฝ๊ณ  + if (BatteryLevel < 20.0f && _currentState != AGVState.Charging) + { + OnError($"๋ฐฐํ„ฐ๋ฆฌ ๋ถ€์กฑ: {BatteryLevel:F1}%"); + } + } + + private void ProcessNextNode() + { + if (_remainingNodes == null || _currentNodeIndex >= _remainingNodes.Count - 1) + { + // ๊ฒฝ๋กœ ์™„๋ฃŒ + _moveTimer.Stop(); + SetState(AGVState.Idle); + PathCompleted?.Invoke(this, _currentPath); + return; + } + + // ๋‹ค์Œ ๋…ธ๋“œ๋กœ ์ด๋™ + _currentNodeIndex++; + var nextNodeId = _remainingNodes[_currentNodeIndex]; + + // RFID ๊ฐ์ง€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + RfidDetected?.Invoke(this, $"RFID_{nextNodeId}"); + _currentNodeId = nextNodeId; + + // ๋‹ค์Œ ๋ชฉํ‘œ ์œ„์น˜ ์„ค์ • (์‹ค์ œ๋กœ๋Š” ๋งต์—์„œ ์ขŒํ‘œ ๊ฐ€์ ธ์™€์•ผ ํ•จ) + // ์—ฌ๊ธฐ์„œ๋Š” ๊ฐ„๋‹จํžˆ ํ˜„์žฌ ์œ„์น˜์—์„œ ๋žœ๋ค ์˜คํ”„์…‹์œผ๋กœ ์„ค์ • + var random = new Random(); + _moveTargetPosition = new Point( + _currentPosition.X + random.Next(-100, 100), + _currentPosition.Y + random.Next(-100, 100) + ); + } + + private MapNode FindClosestNode(Point position, List mapNodes) + { + if (mapNodes == null || mapNodes.Count == 0) + return null; + + MapNode closestNode = null; + float closestDistance = float.MaxValue; + + foreach (var node in mapNodes) + { + var distance = CalculateDistance(position, node.Position); + if (distance < closestDistance) + { + closestDistance = distance; + closestNode = node; + } + } + + // ์ผ์ • ๊ฑฐ๋ฆฌ ๋‚ด์— ์žˆ๋Š” ๋…ธ๋“œ๋งŒ ๋ฐ˜ํ™˜ + return closestDistance < 50.0f ? closestNode : null; + } + + private float CalculateDistance(Point from, Point to) + { + var dx = to.X - from.X; + var dy = to.Y - from.Y; + return (float)Math.Sqrt(dx * dx + dy * dy); + } + + private void SetState(AGVState newState) + { + if (_currentState != newState) + { + _currentState = newState; + StateChanged?.Invoke(this, newState); + } + } + + private void OnError(string message) + { + SetState(AGVState.Error); + ErrorOccurred?.Invoke(this, message); + } + + #endregion + + #region Cleanup + + /// + /// ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ + /// + public void Dispose() + { + _moveTimer?.Stop(); + _moveTimer?.Dispose(); + } + + #endregion + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Program.cs b/Cs_HMI/AGVSimulator/Program.cs new file mode 100644 index 0000000..5e86016 --- /dev/null +++ b/Cs_HMI/AGVSimulator/Program.cs @@ -0,0 +1,32 @@ +using System; +using System.Windows.Forms; +using AGVSimulator.Forms; + +namespace AGVSimulator +{ + /// + /// AGV ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ”„๋กœ๊ทธ๋žจ ์ง„์ž…์  + /// + static class Program + { + /// + /// ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ฃผ ์ง„์ž…์ ์ž…๋‹ˆ๋‹ค. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + try + { + Application.Run(new SimulatorForm()); + } + catch (Exception ex) + { + MessageBox.Show($"์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค:\n{ex.Message}", + "์‹œ์Šคํ…œ ์˜ค๋ฅ˜", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/Properties/AssemblyInfo.cs b/Cs_HMI/AGVSimulator/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c13094c --- /dev/null +++ b/Cs_HMI/AGVSimulator/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// ์–ด์…ˆ๋ธ”๋ฆฌ์— ๋Œ€ํ•œ ์ผ๋ฐ˜ ์ •๋ณด๋Š” ๋‹ค์Œ ํŠน์„ฑ ์ง‘ํ•ฉ์„ ํ†ตํ•ด +// ์ œ์–ด๋ฉ๋‹ˆ๋‹ค. ์–ด์…ˆ๋ธ”๋ฆฌ์™€ ๊ด€๋ จ๋œ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•˜๋ ค๋ฉด +// ์ด๋Ÿฌํ•œ ํŠน์„ฑ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜์„ธ์š”. +[assembly: AssemblyTitle("AGV Simulator")] +[assembly: AssemblyDescription("ENIG AGV System Simulator")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("ENIG")] +[assembly: AssemblyProduct("AGV HMI System")] +[assembly: AssemblyCopyright("Copyright ยฉ ENIG 2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// ComVisible์„ false๋กœ ์„ค์ •ํ•˜๋ฉด ์ด ์–ด์…ˆ๋ธ”๋ฆฌ์˜ ํ˜•์‹์ด COM ๊ตฌ์„ฑ ์š”์†Œ์— +// ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. COM์—์„œ ์ด ์–ด์…ˆ๋ธ”๋ฆฌ์˜ ํ˜•์‹์— ์•ก์„ธ์Šคํ•˜๋ ค๋ฉด +// ํ•ด๋‹น ํ˜•์‹์— ๋Œ€ํ•ด ComVisible ํŠน์„ฑ์„ true๋กœ ์„ค์ •ํ•˜์„ธ์š”. +[assembly: ComVisible(false)] + +// ์ด ํ”„๋กœ์ ํŠธ๊ฐ€ COM์— ๋…ธ์ถœ๋˜๋Š” ๊ฒฝ์šฐ ๋‹ค์Œ GUID๋Š” typelib์˜ ID๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. +[assembly: Guid("b2c3d4e5-f6a7-4901-bcde-f23456789012")] + +// ์–ด์…ˆ๋ธ”๋ฆฌ์˜ ๋ฒ„์ „ ์ •๋ณด๋Š” ๋‹ค์Œ ๋„ค ๊ฐœ์˜ ๊ฐ’์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. +// +// ์ฃผ ๋ฒ„์ „ +// ๋ถ€ ๋ฒ„์ „ +// ๋นŒ๋“œ ๋ฒˆํ˜ธ +// ์ˆ˜์ • ๋ฒ„์ „ +// +// ๋ชจ๋“  ๊ฐ’์„ ์ง€์ •ํ•˜๊ฑฐ๋‚˜ ์•„๋ž˜์™€ ๊ฐ™์ด '*'๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋นŒ๋“œ ๋ฒˆํ˜ธ ๋ฐ ์ˆ˜์ • ๋ฒˆํ˜ธ๋ฅผ +// ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/Cs_HMI/AGVSimulator/packages.config b/Cs_HMI/AGVSimulator/packages.config new file mode 100644 index 0000000..8b1a8d0 --- /dev/null +++ b/Cs_HMI/AGVSimulator/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Cs_HMI/CLAUDE.md b/Cs_HMI/CLAUDE.md new file mode 100644 index 0000000..3264393 --- /dev/null +++ b/Cs_HMI/CLAUDE.md @@ -0,0 +1,141 @@ +# CLAUDE.md + +์ด ํŒŒ์ผ์€ ์ด ์ €์žฅ์†Œ์˜ ์ฝ”๋“œ๋กœ ์ž‘์—…ํ•  ๋•Œ Claude Code (claude.ai/code)๋ฅผ ์œ„ํ•œ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +## ๋นŒ๋“œ ๋ฐ ๊ฐœ๋ฐœ ๋ช…๋ น์–ด + +### ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰ +- **๋ฉ”์ธ ๋นŒ๋“œ**: build.bat ํŒŒ์ผ ์ฐธ๊ณ  +- **๋นŒ๋“œํ›„ ์ด๋ฒคํŠธ ์˜ˆ์ œ) rem xcopy "$(TargetDir)*.exe" "\\192.168.1.80\Amkor\AGV2" /Y + +### ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๋ฐ ๋นŒ๋“œ ์„ค์ • +- **๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜**: `Project/AGV4.csproj` - "Amkor"๋ผ๋Š” ์‹คํ–‰ ํŒŒ์ผ๋ช…์œผ๋กœ ์ปดํŒŒ์ผ +- **ํƒ€๊ฒŸ ํ”Œ๋žซํผ**: .NET Framework 4.8, x86/x64 ์•„ํ‚คํ…์ฒ˜ +- **์ถœ๋ ฅ ๊ฒฝ๋กœ**: + - Debug: `..\..\..\..\..\Amkor\AGV4\` + - Release: `..\..\..\ManualMapEditor\` + +## ๊ณ ์ˆ˜์ค€ ์ฝ”๋“œ ์•„ํ‚คํ…์ฒ˜ + +### ์†”๋ฃจ์…˜ ๊ตฌ์กฐ +์ด ํ”„๋กœ์ ํŠธ๋Š” ENIG AGV (์ž๋™ ์œ ๋„ ์ฐจ๋Ÿ‰) ์‹œ์Šคํ…œ์„ ์œ„ํ•œ C# HMI (Human-Machine Interface) ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค. + +``` +AGVCSharp.sln (๋ฉ”์ธ ์†”๋ฃจ์…˜) +โ”œโ”€โ”€ Project/AGV4.csproj (๋ฉ”์ธ HMI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜) +โ”œโ”€โ”€ StateMachine/ (์ƒํƒœ ๋จธ์‹  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) +โ”œโ”€โ”€ AGVMapEditor/ (๋งต ์—๋””ํ„ฐ - 2024.09 ์ถ”๊ฐ€) +โ”œโ”€โ”€ AGVSimulator/ (AGV ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ - 2024.09 ์ถ”๊ฐ€) +โ””โ”€โ”€ SubProject/ (์„œ๋ธŒ ํ”„๋กœ์ ํŠธ ๋ชจ๋“ˆ๋“ค) + โ”œโ”€โ”€ AGVControl/ (AGV ์ œ์–ด) + โ”œโ”€โ”€ BMS/ (๋ฐฐํ„ฐ๋ฆฌ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ) + โ”œโ”€โ”€ NARUMI/ (AGV ํ•˜๋“œ์›จ์–ด) + โ”œโ”€โ”€ CommData/ (ํ†ต์‹  ๋ฐ์ดํ„ฐ) + โ”œโ”€โ”€ ENIGProtocol/ (ENIG ํ”„๋กœํ† ์ฝœ) + โ””โ”€โ”€ ๊ธฐํƒ€ ๋ชจ๋“ˆ๋“ค +``` + +### ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ฒ˜ (Project/) + +#### ํ•ต์‹ฌ ํผ ๊ตฌ์กฐ +- **fMain.cs**: ๋ฉ”์ธ UI ํผ - AGV ์‹œ์Šคํ…œ์˜ ์ค‘์•™ ์ œ์–ดํŒ +- **fSetup.cs**: ์„ค์ • ํ™”๋ฉด - ์‹œ์Šคํ…œ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ ๊ตฌ์„ฑ +- **ViewForm/**: ๊ฐ์ข… ๋ชจ๋‹ˆํ„ฐ๋ง ํ™”๋ฉด๋“ค + - `fAuto.cs` - ์ž๋™ ๋ชจ๋“œ ํ™”๋ฉด + - `fManual.cs` - ์ˆ˜๋™ ๋ชจ๋“œ ํ™”๋ฉด + - `fAgv.cs` - AGV ์ƒํƒœ ํ™”๋ฉด + - `fBms.cs` - ๋ฐฐํ„ฐ๋ฆฌ ์ƒํƒœ ํ™”๋ฉด + - `fIO.cs` - I/O ์ƒํƒœ ํ™”๋ฉด + +#### ์ƒํƒœ ๋จธ์‹  ์‹œ์Šคํ…œ (StateMachine/) +AGV์˜ ๋™์ž‘์„ ์ œ์–ดํ•˜๋Š” ์ƒํƒœ ๊ธฐ๋ฐ˜ ์‹œ์Šคํ…œ: +- **_Loop.cs**: ๋ฉ”์ธ ์ƒํƒœ ๋จธ์‹  ๋ฃจํ”„ +- **_AGV.cs**: AGV ์ œ์–ด ๋กœ์ง +- **_BMS.cs**: ๋ฐฐํ„ฐ๋ฆฌ ๊ด€๋ฆฌ ์ƒํƒœ +- **_SPS.cs**: SPS(Stored Program Sequencer) ์ œ์–ด +- **Step/**: ๊ฐ ์ƒํƒœ๋ณ„ ๊ตฌํ˜„ ํด๋ž˜์Šค๋“ค + - `_SM_RUN_*.cs` - ์‹คํ–‰ ์ƒํƒœ๋“ค (INIT, READY, GOTO, CHARGE ๋“ฑ) + +#### ํ•ต์‹ฌ ํด๋ž˜์Šค๋“ค +- **PUB.cs**: ์ „์—ญ ๋ณ€์ˆ˜ ๋ฐ ๊ณตํ†ต ํ•จ์ˆ˜ +- **CSetting.cs**: ์„ค์ • ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ +- **Manager/DataBaseManager.cs**: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ +- **Device/**: ํ•˜๋“œ์›จ์–ด ์ธํ„ฐํŽ˜์ด์Šค ํด๋ž˜์Šค๋“ค + +### ํ†ต์‹  ๋ฐ ํ”„๋กœํ† ์ฝœ +- **XBee ๋ฌด์„  ํ†ต์‹ **: call button ๋ฐ ์ถฉ์ „๊ธฐ ํ†ต์‹  +- **ENIG Protocol**: ์ž์ฒด ์ •์˜ ํ”„๋กœํ† ์ฝœ +- **Socket ํ†ต์‹ **: ๋„คํŠธ์›Œํฌ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๊ตํ™˜ +- **Database**: ์šด์˜ ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ + +### ์˜์กด์„ฑ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ +- **arCommUtil**: AR ํ†ต์‹  ์œ ํ‹ธ๋ฆฌํ‹ฐ +- **arControl.Net4**: AR ์ œ์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ +- **Newtonsoft.Json**: JSON ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ +- **Microsoft.Speech**: ์Œ์„ฑ ์ฒ˜๋ฆฌ +- **System.Management**: ์‹œ์Šคํ…œ ๊ด€๋ฆฌ + +### ๋นŒ๋“œ ํ›„ ์ฒ˜๋ฆฌ +ํ”„๋กœ์ ํŠธ๋Š” ๋นŒ๋“œ ํ›„ ๋„คํŠธ์›Œํฌ ์œ„์น˜๋กœ ํŒŒ์ผ์„ ์ž๋™ ๋ณต์‚ฌํ•˜๋Š” ์„ค์ •์ด ์žˆ์Šต๋‹ˆ๋‹ค (ํ˜„์žฌ ์ฃผ์„ ์ฒ˜๋ฆฌ๋จ). + +### Git ์—…๋ฐ์ดํŠธ +SubProject ๋‚ด์˜ GitUpdate.bat์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ํ•˜์œ„ ํ”„๋กœ์ ํŠธ๋ฅผ ์ผ๊ด„ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +## ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ AGV ๊ฐœ๋ฐœ ๋„๊ตฌ (2024.09) + +### AGVMapEditor (๋งต ์—๋””ํ„ฐ) +**์œ„์น˜**: `AGVMapEditor/AGVMapEditor.csproj` +**์‹คํ–‰ํŒŒ์ผ**: `AGVMapEditor/bin/Debug/AGVMapEditor.exe` + +#### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- **๋งต ๋…ธ๋“œ ๊ด€๋ฆฌ**: ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ์ƒ์„ฑ, ์—ฐ๊ฒฐ, ์†์„ฑ ์„ค์ • +- **RFID ๋งคํ•‘ ๋ถ„๋ฆฌ**: ๋ฌผ๋ฆฌ์  RFID ID โ†” ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID ๋งคํ•‘ ๊ด€๋ฆฌ +- **์‹œ๊ฐ์  ๋งต ํŽธ์ง‘**: ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ๋…ธ๋“œ ๋ฐฐ์น˜ ๋ฐ ์—ฐ๊ฒฐ +- **JSON ํŒŒ์ผ ์ €์žฅ**: ๋งต ๋ฐ์ดํ„ฐ๋ฅผ JSON ํ˜•์‹์œผ๋กœ ์ €์žฅ/๋กœ๋“œ + +#### ํ•ต์‹ฌ ํด๋ž˜์Šค +- **MapNode**: ๋…ผ๋ฆฌ์  ๋งต ๋…ธ๋“œ (NodeId, ์œ„์น˜, ํƒ€์ž…, ์—ฐ๊ฒฐ ์ •๋ณด) +- **RfidMapping**: RFID ๋ฌผ๋ฆฌ์  ID โ†” ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ๋งคํ•‘ +- **NodeResolver**: RFID ID๋ฅผ ํ†ตํ•œ ๋…ธ๋“œ ํ•ด์„๊ธฐ +- **PathCalculator**: A* ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ธฐ๋ฐ˜ AGV ๊ฒฝ๋กœ ๊ณ„์‚ฐ +- **MapCanvas**: ์‹œ๊ฐ์  ๋งต ํŽธ์ง‘ ์ปจํŠธ๋กค + +#### AGV ํŠนํ™” ์ œ์•ฝ์‚ฌํ•ญ +- **๋ฐฉํ–ฅ์„ฑ ์ œ์•ฝ**: ์ „์ง„/ํ›„์ง„, ํšŒ์ „์€ ๋งˆํฌ์„ผ์„œ ์œ„์น˜์—์„œ๋งŒ ๊ฐ€๋Šฅ +- **๋„ํ‚น ์ œ์•ฝ**: ์ถฉ์ „๊ธฐ(์ „์ง„ ๋„ํ‚น), ์žฅ๋น„(ํ›„์ง„ ๋„ํ‚น) +- **RFID ๊ธฐ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜**: ๋ฌผ๋ฆฌ์  RFID์™€ ๋…ผ๋ฆฌ์  ์˜๋ฏธ ๋ถ„๋ฆฌ + +### AGVSimulator (AGV ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ) +**์œ„์น˜**: `AGVSimulator/AGVSimulator.csproj` +**์‹คํ–‰ํŒŒ์ผ**: `AGVSimulator/bin/Debug/AGVSimulator.exe` + +#### ํ•ต์‹ฌ ๊ธฐ๋Šฅ +- **๊ฐ€์ƒ AGV ์‹œ๋ฎฌ๋ ˆ์ด์…˜**: ์‹ค์‹œ๊ฐ„ AGV ์›€์ง์ž„ ๋ฐ ์ƒํƒœ ๊ด€๋ฆฌ +- **๋งต ์‹œ๊ฐํ™”**: ๋งต ์—๋””ํ„ฐ์—์„œ ์ƒ์„ฑํ•œ ๋งต ํŒŒ์ผ ๋กœ๋“œ ๋ฐ ํ‘œ์‹œ +- **๊ฒฝ๋กœ ์‹คํ–‰**: ๊ณ„์‚ฐ๋œ ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ AGV ์‹œ๋ฎฌ๋ ˆ์ด์…˜ +- **์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง**: AGV ์ƒํƒœ, ์œ„์น˜, ๋ฐฐํ„ฐ๋ฆฌ ๋“ฑ ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง + +#### ํ•ต์‹ฌ ํด๋ž˜์Šค +- **VirtualAGV**: ๊ฐ€์ƒ AGV ๋™์ž‘ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (์ด๋™, ํšŒ์ „, ๋„ํ‚น, ์ถฉ์ „) +- **SimulatorCanvas**: AGV ๋ฐ ๋งต ์‹œ๊ฐํ™” ์บ”๋ฒ„์Šค +- **SimulatorForm**: ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋ฉ”์ธ ์ธํ„ฐํŽ˜์ด์Šค +- **SimulationState**: ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒํƒœ ๊ด€๋ฆฌ + +#### AGV ์ƒํƒœ +- **Idle**: ๋Œ€๊ธฐ, **Moving**: ์ด๋™ ์ค‘, **Rotating**: ํšŒ์ „ ์ค‘ +- **Docking**: ๋„ํ‚น ์ค‘, **Charging**: ์ถฉ์ „ ์ค‘, **Error**: ์˜ค๋ฅ˜ + +### ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ +1. **๋งต ์ƒ์„ฑ**: AGVMapEditor๋กœ ๋งต ๋…ธ๋“œ ๋ฐฐ์น˜ ๋ฐ RFID ๋งคํ•‘ ์„ค์ • +2. **๊ฒฝ๋กœ ํ…Œ์ŠคํŠธ**: AGVSimulator๋กœ AGV ๋™์ž‘ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฐ ๊ฒ€์ฆ +3. **์‹ค์ œ ์ ์šฉ**: ๊ฒ€์ฆ๋œ ๋งต ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ AGV ์‹œ์Šคํ…œ์— ์ ์šฉ + +### RFID ๋งคํ•‘ ์•„ํ‚คํ…์ฒ˜ ์›์น™ +**์ค‘์š”**: ๋ฌผ๋ฆฌ์  RFID ID๋Š” ์˜๋ฏธ์—†๋Š” ๊ณ ์œ ๊ฐ’์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID์™€ ๋ณ„๋„ ๋งคํ•‘ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ํ˜„์žฅ ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ + +## ๊ฐœ๋ฐœ์‹œ ์ฃผ์˜์‚ฌํ•ญ +- ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ Windows Forms ๊ธฐ๋ฐ˜์˜ ํ„ฐ์น˜ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์„ค๊ณ„๋จ +- ์‹ค์‹œ๊ฐ„ AGV ์ œ์–ด ์‹œ์Šคํ…œ์ด๋ฏ€๋กœ ์ƒํƒœ ๋จธ์‹  ๋กœ์ง ์ˆ˜์ •์‹œ ์‹ ์ค‘ํžˆ ์ ‘๊ทผ +- ํ†ต์‹  ๊ด€๋ จ ์ฝ”๋“œ ๋ณ€๊ฒฝ์‹œ ํ•˜๋“œ์›จ์–ด ํ˜ธํ™˜์„ฑ ๊ณ ๋ ค ํ•„์š” +- **๋งต ์—๋””ํ„ฐ/์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ**: AGVMapEditor ํ”„๋กœ์ ํŠธ์— ์˜์กด์„ฑ์ด ์žˆ์œผ๋ฏ€๋กœ ๋จผ์ € ๋นŒ๋“œ ํ•„์š” +- **JSON ํŒŒ์ผ ํ˜•์‹**: ๋งต ๋ฐ์ดํ„ฐ๋Š” MapNodes, RfidMappings ๋‘ ์„น์…˜์œผ๋กœ ๊ตฌ์„ฑ \ No newline at end of file diff --git a/Cs_HMI/TODO.md b/Cs_HMI/TODO.md new file mode 100644 index 0000000..e08e9fc --- /dev/null +++ b/Cs_HMI/TODO.md @@ -0,0 +1,217 @@ +# AGV ์ด๋™ ์‹œ์Šคํ…œ ๊ฐœ๋ฐœ TODO + +## ํ”„๋กœ์ ํŠธ ๊ฐœ์š” +AGV ์ด๋™ ์‹œ์Šคํ…œ ์„ค๊ณ„ ๋ฐ ๊ฐœ๋ฐœ - RFID ๊ธฐ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ์‹œ์Šคํ…œ + +--- + +## 1. ์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„ ๋ฐ ์‹œ์Šคํ…œ ๊ตฌ์„ฑ ์š”์†Œ + +### ๐Ÿ“ AGV ๊ธฐ๋ณธ ์‚ฌ์–‘ +- **์ด๋™ ๊ธฐ๋Šฅ**: ์ „์ง„, ํ›„์ง„, ์ขŒํšŒ์ „, ์šฐํšŒ์ „, ๋งˆํฌ์Šคํƒ‘, ์ •์ง€ +- **๋ฐฉํ–ฅ์„ฑ**: ์ „๋ฉด(๋ชจ๋‹ˆํ„ฐ), ํ›„๋ฉด(๋ฆฌํ”„ํŠธ) +- **์ฃผ์š” ๊ธฐ๋Šฅ**: ์นดํŠธ ์ด๋™ ๋ฐ ์žฅ๋น„ ๋„ํ‚น + +### ๐Ÿญ ์žฅ๋น„ ๊ตฌ์„ฑ (์ด 10๋Œ€) +``` +๋„ํ‚น ์žฅ๋น„ (8๋Œ€): +โ”œโ”€โ”€ 1๋ฒˆ ๋กœ๋” (ํ›„์ง„ ๋„ํ‚น) +โ”œโ”€โ”€ 2๋ฒˆ ํด๋ฆฌ๋„ˆ (ํ›„์ง„ ๋„ํ‚น) +โ”œโ”€โ”€ 3๋ฒˆ ์˜คํ”„๋กœ๋” (ํ›„์ง„ ๋„ํ‚น) +โ””โ”€โ”€ 4๋ฒˆ ๋ฒ„ํผ (ํ›„์ง„ ๋„ํ‚น) + โ”œโ”€โ”€ 4-1 ~ 4-5 (5๋Œ€) + +์ถฉ์ „ ์žฅ๋น„ (2๋Œ€): +โ”œโ”€โ”€ ์ถฉ์ „๊ธฐ 1 (์ „์ง„ ๋„ํ‚น) +โ””โ”€โ”€ ์ถฉ์ „๊ธฐ 2 (์ „์ง„ ๋„ํ‚น) +``` + +### ๐Ÿ—บ๏ธ ๋„ค๋น„๊ฒŒ์ด์…˜ ์‹œ์Šคํ…œ +- **์œ„์น˜ ์ธ์‹**: RFID ๊ธฐ๋ฐ˜ +- **๊ฒฝ๋กœ ๊ณ„์‚ฐ**: ๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŒ… +- **๋ฐฉํ–ฅ ์ œ์–ด**: ๋„ํ‚น ๋ฐฉํ–ฅ๋ณ„ ์ ‘๊ทผ ์ „๋žต + +--- + +## 2. ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ + +### ๐Ÿ“Š ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ +``` +AGV Navigation System +โ”œโ”€โ”€ Map Management +โ”‚ โ”œโ”€โ”€ RFID Node Manager +โ”‚ โ”œโ”€โ”€ Path Calculator +โ”‚ โ””โ”€โ”€ Route Optimizer +โ”œโ”€โ”€ Movement Control +โ”‚ โ”œโ”€โ”€ Motion Controller +โ”‚ โ”œโ”€โ”€ Direction Manager +โ”‚ โ””โ”€โ”€ Docking Controller +โ”œโ”€โ”€ Position Tracking +โ”‚ โ”œโ”€โ”€ RFID Reader Interface +โ”‚ โ”œโ”€โ”€ Position Monitor +โ”‚ โ””โ”€โ”€ Route Validator +โ””โ”€โ”€ Station Management + โ”œโ”€โ”€ Equipment Interface + โ”œโ”€โ”€ Charging Controller + โ””โ”€โ”€ Operation Scheduler +``` + +--- + +## 3. RFID ๊ธฐ๋ฐ˜ ๋งต ์‹œ์Šคํ…œ ์„ค๊ณ„ + +### ๐Ÿท๏ธ RFID ๊ด€๋ฆฌ ๋ฐฉ์‹ (์‹ค์šฉ์  ์ ‘๊ทผ) +```csharp +// RFID ID๋Š” ์ˆœ์ˆ˜ํ•œ ์‹๋ณ„์ž๋กœ๋งŒ ์‚ฌ์šฉ (์˜๋ฏธ ์—†๋Š” ๊ฐ’) +// ์˜ˆ์‹œ RFID ๊ฐ’๋“ค: +"1234567890", "9876543210", "5555666677" // ์ž„์˜์˜ ๊ณ ์œ ๊ฐ’ + +// ์‹ค์ œ ์˜๋ฏธ๋Š” ๋ณ„๋„ ๋งคํ•‘ ๋ฐ์ดํ„ฐ์—์„œ ๊ด€๋ฆฌ +// RFID Writer ์ž‘์—… ์ตœ์†Œํ™” - ๋ฌผ๋ฆฌ์  ๊ต์ฒด/์ถ”๊ฐ€๋งŒ ์ˆ˜ํ–‰ +``` + +### ๐Ÿ—บ๏ธ ๋งต ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ (RFID ๋งคํ•‘ ๋ถ„๋ฆฌ) +```csharp +// RFID-๋งต๋…ธ๋“œ ๋งคํ•‘ ํ…Œ์ด๋ธ” +public class RfidMapping +{ + public string RfidId { get; set; } // ๋ฌผ๋ฆฌ์  RFID ๊ฐ’ (์˜๋ฏธ ์—†์Œ) + public string LogicalNodeId { get; set; } // ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ID + public DateTime CreatedDate { get; set; } + public string Description { get; set; } // ์„ค์น˜ ์œ„์น˜ ์„ค๋ช… +} + +// ๋งต ๋…ธ๋“œ ์ •๋ณด (๋…ผ๋ฆฌ์ ) +public class MapNode +{ + public string NodeId { get; set; } // ๋…ผ๋ฆฌ์  ID (N001, N002...) + public string Name { get; set; } // ๋…ธ๋“œ ์ด๋ฆ„ (๋กœ๋”1, ์ถฉ์ „๊ธฐ1...) + public Point Position { get; set; } + public NodeType Type { get; set; } + public DockingDirection? DockDirection { get; set; } + public List ConnectedNodes { get; set; } + public bool CanRotate { get; set; } + public string StationId { get; set; } // ์žฅ๋น„ ID (LOADER1, CHARGER1...) +} + +// RFID ๋ฆฌ๋”์—์„œ ์ฝ์€ ๊ฐ’์„ ๋…ผ๋ฆฌ์  ๋…ธ๋“œ๋กœ ๋ณ€ํ™˜ +public class NodeResolver +{ + public MapNode GetNodeByRfid(string rfidValue) + { + var mapping = GetRfidMapping(rfidValue); + return GetNodeById(mapping?.LogicalNodeId); + } +} + +public enum NodeType { Normal, Rotation, Docking, Charging } +public enum DockingDirection { Forward, Backward } +public enum AgvDirection { Forward, Backward, Left, Right } +``` + +--- + +## 4. ๊ฐœ๋ฐœ ์ˆœ์„œ ๋ฐ ๋‹จ๊ณ„๋ณ„ ๊ณ„ํš + +### ๐ŸŽฏ Phase 1: ๊ธฐ๋ฐ˜ ์‹œ์Šคํ…œ ๊ฐœ๋ฐœ +1. **๋งต ์—๋””ํ„ฐ ๋„๊ตฌ** (.NET Framework 4.8 WinForms) + - [ ] RFID ๋…ธ๋“œ ๋ฐฐ์น˜ ๋ฐ ํŽธ์ง‘ + - [ ] ์—ฐ๊ฒฐ์„  ์„ค์ • ๋ฐ ์‹œ๊ฐํ™” + - [ ] ๋„ํ‚น ๋ฐฉํ–ฅ ์„ค์ • ์ธํ„ฐํŽ˜์ด์Šค + +2. **๊ฒฝ๋กœ ๊ณ„์‚ฐ ์—”์ง„** + - [ ] A* ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ธฐ๋ฐ˜ ์ตœ๋‹จ ๊ฒฝ๋กœ + - [ ] ๋ฐฉํ–ฅ์„ฑ ๊ณ ๋ ค ๋ผ์šฐํŒ… + - [ ] ๋™์  ๊ฒฝ๋กœ ์žฌ๊ณ„์‚ฐ + +### ๐ŸŽฏ Phase 2: ์ด๋™ ์ œ์–ด ์‹œ์Šคํ…œ +3. **AGV ๋ชจ์…˜ ์ปจํŠธ๋กค๋Ÿฌ** + - [ ] ๊ธฐ์กด AGV ์ปจํŠธ๋กค๋Ÿฌ ์ธํ„ฐํŽ˜์ด์Šค + - [ ] ๋ฐฉํ–ฅ ์ „ํ™˜ ๋กœ์ง + - [ ] ๋„ํ‚น ์‹œํ€€์Šค ์ œ์–ด + +4. **์œ„์น˜ ์ถ”์  ์‹œ์Šคํ…œ** + - [ ] RFID ๋ฆฌ๋” ์ธํ„ฐํŽ˜์ด์Šค + - [ ] ์‹ค์‹œ๊ฐ„ ์œ„์น˜ ๋ชจ๋‹ˆํ„ฐ๋ง + - [ ] ๊ฒฝ๋กœ ์ดํƒˆ ๊ฐ์ง€ ๋ฐ ๋ณด์ • + +### ๐ŸŽฏ Phase 3: ํ†ตํ•ฉ ๋ฐ ํ…Œ์ŠคํŠธ +5. **์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ…Œ์ŠคํŠธ ๋„๊ตฌ** + - [ ] ๊ฐ€์ƒ AGV ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ + - [ ] ๊ฒฝ๋กœ ์‹œ๊ฐํ™” ๋ฐ ๋””๋ฒ„๊น… + - [ ] ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ + +--- + +## 5. ํ…Œ์ŠคํŠธ ํ”„๋กœ๊ทธ๋žจ ์šฐ์„  ๊ฐœ๋ฐœ + +### ๐Ÿงช AGV Navigation Test Suite (.NET Framework 4.8) +``` +TestProgram Solution +โ”œโ”€โ”€ AGVNavigationCore // ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ +โ”œโ”€โ”€ MapEditorTool // ๋งต ํŽธ์ง‘ ๋„๊ตฌ +โ”œโ”€โ”€ SimulatorApp // AGV ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ +โ””โ”€โ”€ UnitTestProjects // ๋‹จ์œ„ ํ…Œ์ŠคํŠธ +``` + +### ๐Ÿ“‹ ์ฒซ ๋ฒˆ์งธ ๊ตฌํ˜„ ๋ชฉํ‘œ (์šฐ์„ ์ˆœ์œ„) +1. **๋งต ์—๋””ํ„ฐ**: RFID ๋…ธ๋“œ ๋ฐฐ์น˜ ๋ฐ ์—ฐ๊ฒฐ์„  ์„ค์ • +2. **๊ฒฝ๋กœ ๊ณ„์‚ฐ๊ธฐ**: A* ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ ๋ฐ ํ…Œ์ŠคํŠธ +3. **์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ**: ๊ฐ€์ƒ AGV๋กœ ๊ฒฝ๋กœ ์ถ”์  ํ…Œ์ŠคํŠธ +4. **RFID ๋งค๋‹ˆ์ €**: ๋…ธ๋“œ ์ •๋ณด ๊ด€๋ฆฌ ๋ฐ ๊ฒ€์ƒ‰ + +--- + +## ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ + +**ํ˜„์žฌ ๊ถŒ์žฅ์‚ฌํ•ญ**: ๋งต ์—๋””ํ„ฐ ํ…Œ์ŠคํŠธ ํ”„๋กœ๊ทธ๋žจ๋ถ€ํ„ฐ ์‹œ์ž‘ + +### ๋งต ์—๋””ํ„ฐ ์šฐ์„  ๊ฐœ๋ฐœ ์ด์œ : +1. **์‹œ๊ฐ์  ํ™•์ธ**: RFID ๋…ธ๋“œ์™€ ๊ฒฝ๋กœ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ๋ฐฐ์น˜ํ•˜๊ณ  ํ™•์ธ ๊ฐ€๋Šฅ +2. **๊ธฐ์ดˆ ๊ฒ€์ฆ**: ๊ฒฝ๋กœ ๊ณ„์‚ฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ฐ˜ ์ œ๊ณต +3. **์ ์ง„์  ๊ฐœ๋ฐœ**: ๋ณต์žกํ•œ AGV ์ œ์–ด ์—†์ด ํ•ต์‹ฌ ๋กœ์ง๋ถ€ํ„ฐ ๊ฒ€์ฆ + +### ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„: +``` +AGV MapEditor Test Program (.NET Framework 4.8) +โ”œโ”€โ”€ ๊ทธ๋ž˜ํ”ฝ ๋งต ํŽธ์ง‘๊ธฐ (WinForms) +โ”œโ”€โ”€ RFID ๋…ธ๋“œ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ +โ”œโ”€โ”€ ๊ฒฝ๋กœ ๊ณ„์‚ฐ ์—”์ง„ (A*) +โ””โ”€โ”€ ๋งต ๋ฐ์ดํ„ฐ ์ €์žฅ/๋กœ๋“œ (JSON/XML) +``` + +--- + +## ๐Ÿ“ ์ƒ์„ธ ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ + +### AGV ๋™์ž‘ ํŠน์„ฑ +- **์ „์ง„**: ๋ชจ๋‹ˆํ„ฐ ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™ +- **ํ›„์ง„**: ๋ฆฌํ”„ํŠธ(๋ชจ๋‹ˆํ„ฐ ๋ฐ˜๋Œ€) ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™ +- **๋„ํ‚น**: ์žฅ๋น„๋ณ„ ์ง€์ •๋œ ๋ฐฉํ–ฅ์œผ๋กœ ์ ‘๊ทผ ํ•„์š” + - ๋กœ๋”/ํด๋ฆฌ๋„ˆ/์˜คํ”„๋กœ๋”/๋ฒ„ํผ: ํ›„์ง„ ๋„ํ‚น + - ์ถฉ์ „๊ธฐ: ์ „์ง„ ๋„ํ‚น +- **ํšŒ์ „**: 180๋„ ํšŒ์ „, ๋งˆํฌ์„ผ์„œ ๊ฐ์ง€์‹œ ์ •์ง€ + +### ๊ฒฝ๋กœ ๊ด€๋ฆฌ ์š”๊ตฌ์‚ฌํ•ญ +- **๋™์  ๊ฒฝ๋กœ ๊ณ„์‚ฐ**: ํ˜„์žฌ ์œ„์น˜์—์„œ ๋ชฉ์ ์ง€๊นŒ์ง€ +- **๊ฒฝ๋กœ ๋ณด์ •**: RFID ๊ฐ์ง€์‹œ ๊ฒฝ๋กœ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ฐ ์žฌ๊ณ„์‚ฐ +- **๋ฐฉํ–ฅ ์ตœ์ ํ™”**: ๋ชฉ์ ์ง€ ๋„ํ‚น ๋ฐฉํ–ฅ ๊ณ ๋ คํ•œ ์ ‘๊ทผ ๊ฒฝ๋กœ +- **ํšŒ์ „ ์ง€์ **: ํŠน์ • RFID์—์„œ๋งŒ 180๋„ ํšŒ์ „ ๊ฐ€๋Šฅ + +### RFID ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ (๋งคํ•‘ ๋ถ„๋ฆฌ ๋ฐฉ์‹) +- **๋ฌผ๋ฆฌ์  RFID**: ์˜๋ฏธ ์—†๋Š” ๊ณ ์œ ๊ฐ’, Writer ์ž‘์—… ์ตœ์†Œํ™” +- **๋…ผ๋ฆฌ์  ๋งคํ•‘**: ์†Œํ”„ํŠธ์›จ์–ด์—์„œ RFID โ†” ๋…ธ๋“œ์ •๋ณด ๋งคํ•‘ ๊ด€๋ฆฌ +- **์œ ์ง€๋ณด์ˆ˜์„ฑ**: + - RFID ์†์ƒ์‹œ โ†’ ์ƒˆ RFID ์„ค์น˜ ํ›„ ๋งคํ•‘๋งŒ ๋ณ€๊ฒฝ + - ์žฅ๋น„ ์ถ”๊ฐ€์‹œ โ†’ ๋…ผ๋ฆฌ์  ๋…ธ๋“œ ์ถ”๊ฐ€, RFID ๋งคํ•‘ ์—ฐ๊ฒฐ +- **ํŽธ์ง‘ ์šฉ์ด์„ฑ**: ๋งต ์—๋””ํ„ฐ์—์„œ ์˜๋ฏธ์žˆ๋Š” ์ด๋ฆ„์œผ๋กœ ์ž‘์—… +- **ํ˜„์žฅ ์ž‘์—… ์ตœ์†Œํ™”**: RFID Writer ์‚ฌ์šฉ ๋นˆ๋„ ๋Œ€ํญ ๊ฐ์†Œ + +### ๐Ÿ’ก ์‹ค๋ฌด ์žฅ์  +``` +๊ธฐ์กด ๋ฐฉ์‹ (RFID์— ์˜๋ฏธ ๋ถ€์—ฌ): +RFID ์†์ƒ โ†’ Writer ๋“ค๊ณ ๊ฐ€์„œ โ†’ ์˜๋ฏธ์žˆ๋Š” ๊ฐ’ ์žฌ์ž‘์„ฑ โ†’ ํ…Œ์ŠคํŠธ + +์ƒˆ ๋ฐฉ์‹ (๋งคํ•‘ ๋ถ„๋ฆฌ): +RFID ์†์ƒ โ†’ ์ž„์˜ RFID ์„ค์น˜ โ†’ ์†Œํ”„ํŠธ์›จ์–ด์—์„œ ๋งคํ•‘๋งŒ ๋ณ€๊ฒฝ โ†’ ์™„๋ฃŒ +``` \ No newline at end of file diff --git a/Cs_HMI/build.bat b/Cs_HMI/build.bat new file mode 100644 index 0000000..de49611 --- /dev/null +++ b/Cs_HMI/build.bat @@ -0,0 +1,18 @@ +@echo off +echo Building V2GDecoder VC++ Project... + +REM Check if Visual Studio 2022 is installed +if not exist "C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" ( + echo Visual Studio 2022 Professional not found! + echo Please install Visual Studio 2022 Professional or update the MSBuild path. + pause + exit /b 1 +) + +REM Set MSBuild path +set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" + +REM Build Debug x64 configuration +%MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo + +pause \ No newline at end of file