feat: Add AGV Map Editor and Simulator tools
- Add AGVMapEditor: Visual map editing with drag-and-drop node placement * RFID mapping separation (physical ID ↔ logical node mapping) * A* pathfinding algorithm with AGV directional constraints * JSON map data persistence with structured format * Interactive map canvas with zoom/pan functionality - Add AGVSimulator: Real-time AGV movement simulation * Virtual AGV with state machine (Idle, Moving, Rotating, Docking, Charging, Error) * Path execution and visualization from calculated routes * Real-time position tracking and battery simulation * Integration with map editor data format - Update solution structure and build configuration - Add comprehensive documentation in CLAUDE.md - Implement AGV-specific constraints (forward/backward docking, rotation limits) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Express 15 for Windows Desktop
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 15.0.28307.1000
|
VisualStudioVersion = 17.14.36310.24
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sub", "Sub", "{C423C39A-44E7-4F09-B2F7-7943975FF948}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sub", "Sub", "{C423C39A-44E7-4F09-B2F7-7943975FF948}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -27,6 +27,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ENIGProtocol", "SubProject\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGV4", "Project\AGV4.csproj", "{D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGV4", "Project\AGV4.csproj", "{D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}.Release|x86.ActiveCfg = Release|x86
|
{D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}.Release|x86.ActiveCfg = Release|x86
|
||||||
{D6B3880D-7D5C-44E2-B6A5-CF6D881A8A38}.Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
84
Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
Normal file
84
Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<RootNamespace>AGVMapEditor</RootNamespace>
|
||||||
|
<AssemblyName>AGVMapEditor</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Drawing" />
|
||||||
|
<Reference Include="System.Windows.Forms" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
|
||||||
|
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Microsoft.VisualBasic" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Models\MapNode.cs" />
|
||||||
|
<Compile Include="Models\RfidMapping.cs" />
|
||||||
|
<Compile Include="Models\NodeResolver.cs" />
|
||||||
|
<Compile Include="Models\Enums.cs" />
|
||||||
|
<Compile Include="Models\PathNode.cs" />
|
||||||
|
<Compile Include="Models\PathResult.cs" />
|
||||||
|
<Compile Include="Models\PathCalculator.cs" />
|
||||||
|
<Compile Include="Forms\MainForm.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Forms\MainForm.Designer.cs">
|
||||||
|
<DependentUpon>MainForm.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Controls\MapCanvas.cs">
|
||||||
|
<SubType>UserControl</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Controls\MapCanvas.Designer.cs">
|
||||||
|
<DependentUpon>MapCanvas.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Forms\MainForm.resx">
|
||||||
|
<DependentUpon>MainForm.cs</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Controls\MapCanvas.resx">
|
||||||
|
<DependentUpon>MapCanvas.cs</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Data\" />
|
||||||
|
<Folder Include="Utils\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
59
Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs
generated
Normal file
59
Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs
generated
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
namespace AGVMapEditor.Controls
|
||||||
|
{
|
||||||
|
partial class MapCanvas
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 필수 디자이너 변수입니다.
|
||||||
|
/// </summary>
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사용 중인 모든 리소스를 정리합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
|
||||||
|
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 구성 요소 디자이너에서 생성한 코드
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 디자이너 지원에 필요한 메서드입니다.
|
||||||
|
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
608
Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs
Normal file
608
Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 편집을 위한 그래픽 캔버스 컨트롤
|
||||||
|
/// </summary>
|
||||||
|
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<MapNode> _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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드가 선택되었을 때 발생하는 이벤트
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<MapNode> NodeSelected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드가 이동되었을 때 발생하는 이벤트
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<MapNode> NodeMoved;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 배경이 클릭되었을 때 발생하는 이벤트
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<Point> BackgroundClicked;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
public MapCanvas() : this(new List<MapNode>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MapCanvas(List<MapNode> nodes)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
InitializeGraphics();
|
||||||
|
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true);
|
||||||
|
|
||||||
|
_nodes = nodes ?? new List<MapNode>();
|
||||||
|
|
||||||
|
// 이벤트 연결
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 그리드 표시 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowGrid
|
||||||
|
{
|
||||||
|
get { return _showGrid; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_showGrid = value;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 줌 팩터
|
||||||
|
/// </summary>
|
||||||
|
public float ZoomFactor
|
||||||
|
{
|
||||||
|
get { return _zoomFactor; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 선택된 노드
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 스크린 좌표를 월드 좌표로 변환
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 월드 좌표를 스크린 좌표로 변환
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 위치의 노드 검색
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 선택
|
||||||
|
/// </summary>
|
||||||
|
private void SelectNode(MapNode node)
|
||||||
|
{
|
||||||
|
if (_selectedNode != node)
|
||||||
|
{
|
||||||
|
_selectedNode = node;
|
||||||
|
NodeSelected?.Invoke(this, node);
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 우클릭 컨텍스트 메뉴 표시
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 타입 변경
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 보이는 월드 영역 계산
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx
Normal file
61
Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
||||||
422
Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
generated
Normal file
422
Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
generated
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
namespace AGVMapEditor.Forms
|
||||||
|
{
|
||||||
|
partial class MainForm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 필수 디자이너 변수입니다.
|
||||||
|
/// </summary>
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사용 중인 모든 리소스를 정리합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form 디자이너에서 생성한 코드
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 디자이너 지원에 필요한 메서드입니다.
|
||||||
|
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
586
Cs_HMI/AGVMapEditor/Forms/MainForm.cs
Normal file
586
Cs_HMI/AGVMapEditor/Forms/MainForm.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 맵 에디터 메인 폼
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainForm : Form
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private NodeResolver _nodeResolver;
|
||||||
|
private List<MapNode> _mapNodes;
|
||||||
|
private List<RfidMapping> _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<MapNode>();
|
||||||
|
_rfidMappings = new List<RfidMapping>();
|
||||||
|
_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<MapData>(json);
|
||||||
|
|
||||||
|
_mapNodes = mapData.Nodes ?? new List<MapNode>();
|
||||||
|
_rfidMappings = mapData.RfidMappings ?? new List<RfidMapping>();
|
||||||
|
_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<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||||
|
public List<RfidMapping> RfidMappings { get; set; } = new List<RfidMapping>();
|
||||||
|
public DateTime CreatedDate { get; set; }
|
||||||
|
public string Version { get; set; } = "1.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
67
Cs_HMI/AGVMapEditor/Forms/MainForm.resx
Normal file
67
Cs_HMI/AGVMapEditor/Forms/MainForm.resx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||||
|
<value>17, 17</value>
|
||||||
|
</metadata>
|
||||||
|
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||||
|
<value>132, 17</value>
|
||||||
|
</metadata>
|
||||||
|
</root>
|
||||||
64
Cs_HMI/AGVMapEditor/Models/Enums.cs
Normal file
64
Cs_HMI/AGVMapEditor/Models/Enums.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AGVMapEditor.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 타입 열거형
|
||||||
|
/// </summary>
|
||||||
|
public enum NodeType
|
||||||
|
{
|
||||||
|
/// <summary>일반 경로 노드</summary>
|
||||||
|
Normal,
|
||||||
|
/// <summary>회전 가능 지점</summary>
|
||||||
|
Rotation,
|
||||||
|
/// <summary>도킹 스테이션</summary>
|
||||||
|
Docking,
|
||||||
|
/// <summary>충전 스테이션</summary>
|
||||||
|
Charging
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 도킹 방향 열거형
|
||||||
|
/// </summary>
|
||||||
|
public enum DockingDirection
|
||||||
|
{
|
||||||
|
/// <summary>전진 도킹 (충전기)</summary>
|
||||||
|
Forward,
|
||||||
|
/// <summary>후진 도킹 (로더, 클리너, 오프로더, 버퍼)</summary>
|
||||||
|
Backward
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 이동 방향 열거형
|
||||||
|
/// </summary>
|
||||||
|
public enum AgvDirection
|
||||||
|
{
|
||||||
|
/// <summary>전진 (모니터 방향)</summary>
|
||||||
|
Forward,
|
||||||
|
/// <summary>후진 (리프트 방향)</summary>
|
||||||
|
Backward,
|
||||||
|
/// <summary>좌회전</summary>
|
||||||
|
Left,
|
||||||
|
/// <summary>우회전</summary>
|
||||||
|
Right,
|
||||||
|
/// <summary>정지</summary>
|
||||||
|
Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 장비 타입 열거형
|
||||||
|
/// </summary>
|
||||||
|
public enum StationType
|
||||||
|
{
|
||||||
|
/// <summary>로더</summary>
|
||||||
|
Loader,
|
||||||
|
/// <summary>클리너</summary>
|
||||||
|
Cleaner,
|
||||||
|
/// <summary>오프로더</summary>
|
||||||
|
Offloader,
|
||||||
|
/// <summary>버퍼</summary>
|
||||||
|
Buffer,
|
||||||
|
/// <summary>충전기</summary>
|
||||||
|
Charger
|
||||||
|
}
|
||||||
|
}
|
||||||
225
Cs_HMI/AGVMapEditor/Models/MapNode.cs
Normal file
225
Cs_HMI/AGVMapEditor/Models/MapNode.cs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace AGVMapEditor.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 노드 정보를 관리하는 클래스
|
||||||
|
/// 논리적 노드로서 실제 맵의 위치와 속성을 정의
|
||||||
|
/// </summary>
|
||||||
|
public class MapNode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID)
|
||||||
|
/// 예: "N001", "N002", "LOADER1", "CHARGER1"
|
||||||
|
/// </summary>
|
||||||
|
public string NodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 표시 이름 (사용자 친화적)
|
||||||
|
/// 예: "로더1", "충전기1", "교차점A", "회전지점1"
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 상의 위치 좌표 (픽셀 단위)
|
||||||
|
/// </summary>
|
||||||
|
public Point Position { get; set; } = Point.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 타입
|
||||||
|
/// </summary>
|
||||||
|
public NodeType Type { get; set; } = NodeType.Normal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 도킹 방향 (도킹/충전 노드인 경우만 사용)
|
||||||
|
/// </summary>
|
||||||
|
public DockingDirection? DockDirection { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 연결된 노드 ID 목록 (경로 정보)
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
||||||
|
/// </summary>
|
||||||
|
public bool CanRotate { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 장비 ID (도킹/충전 스테이션인 경우)
|
||||||
|
/// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1"
|
||||||
|
/// </summary>
|
||||||
|
public string StationId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 장비 타입 (도킹/충전 스테이션인 경우)
|
||||||
|
/// </summary>
|
||||||
|
public StationType? StationType { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 생성 일자
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 수정 일자
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 설명 (추가 정보)
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 활성화 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 색상 (맵 에디터 표시용)
|
||||||
|
/// </summary>
|
||||||
|
public Color DisplayColor { get; set; } = Color.Blue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기본 생성자
|
||||||
|
/// </summary>
|
||||||
|
public MapNode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매개변수 생성자
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">노드 ID</param>
|
||||||
|
/// <param name="name">노드 이름</param>
|
||||||
|
/// <param name="position">위치</param>
|
||||||
|
/// <param name="type">노드 타입</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 타입에 따른 기본 색상 설정
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">노드 타입</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 다른 노드와의 연결 추가
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">연결할 노드 ID</param>
|
||||||
|
public void AddConnection(string nodeId)
|
||||||
|
{
|
||||||
|
if (!ConnectedNodes.Contains(nodeId))
|
||||||
|
{
|
||||||
|
ConnectedNodes.Add(nodeId);
|
||||||
|
ModifiedDate = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 다른 노드와의 연결 제거
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">연결 해제할 노드 ID</param>
|
||||||
|
public void RemoveConnection(string nodeId)
|
||||||
|
{
|
||||||
|
if (ConnectedNodes.Remove(nodeId))
|
||||||
|
{
|
||||||
|
ModifiedDate = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 도킹 스테이션 설정
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stationId">장비 ID</param>
|
||||||
|
/// <param name="stationType">장비 타입</param>
|
||||||
|
/// <param name="dockDirection">도킹 방향</param>
|
||||||
|
public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection)
|
||||||
|
{
|
||||||
|
Type = NodeType.Docking;
|
||||||
|
StationId = stationId;
|
||||||
|
StationType = stationType;
|
||||||
|
DockDirection = dockDirection;
|
||||||
|
SetDefaultColorByType(NodeType.Docking);
|
||||||
|
ModifiedDate = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 충전 스테이션 설정
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stationId">충전기 ID</param>
|
||||||
|
public void SetChargingStation(string stationId)
|
||||||
|
{
|
||||||
|
Type = NodeType.Charging;
|
||||||
|
StationId = stationId;
|
||||||
|
StationType = Models.StationType.Charger;
|
||||||
|
DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹
|
||||||
|
SetDefaultColorByType(NodeType.Charging);
|
||||||
|
ModifiedDate = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 문자열 표현
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 복사
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>복사된 노드</returns>
|
||||||
|
public MapNode Clone()
|
||||||
|
{
|
||||||
|
var clone = new MapNode
|
||||||
|
{
|
||||||
|
NodeId = NodeId,
|
||||||
|
Name = Name,
|
||||||
|
Position = Position,
|
||||||
|
Type = Type,
|
||||||
|
DockDirection = DockDirection,
|
||||||
|
ConnectedNodes = new List<string>(ConnectedNodes),
|
||||||
|
CanRotate = CanRotate,
|
||||||
|
StationId = StationId,
|
||||||
|
StationType = StationType,
|
||||||
|
CreatedDate = CreatedDate,
|
||||||
|
ModifiedDate = ModifiedDate,
|
||||||
|
Description = Description,
|
||||||
|
IsActive = IsActive,
|
||||||
|
DisplayColor = DisplayColor
|
||||||
|
};
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
308
Cs_HMI/AGVMapEditor/Models/NodeResolver.cs
Normal file
308
Cs_HMI/AGVMapEditor/Models/NodeResolver.cs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AGVMapEditor.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// RFID 값을 논리적 노드로 변환하는 클래스
|
||||||
|
/// 실제 AGV 시스템에서 RFID 리더가 읽은 값을 맵 노드 정보로 변환
|
||||||
|
/// </summary>
|
||||||
|
public class NodeResolver
|
||||||
|
{
|
||||||
|
private List<RfidMapping> _rfidMappings;
|
||||||
|
private List<MapNode> _mapNodes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기본 생성자
|
||||||
|
/// </summary>
|
||||||
|
public NodeResolver()
|
||||||
|
{
|
||||||
|
_rfidMappings = new List<RfidMapping>();
|
||||||
|
_mapNodes = new List<MapNode>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매개변수 생성자
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rfidMappings">RFID 매핑 목록</param>
|
||||||
|
/// <param name="mapNodes">맵 노드 목록</param>
|
||||||
|
public NodeResolver(List<RfidMapping> rfidMappings, List<MapNode> mapNodes)
|
||||||
|
{
|
||||||
|
_rfidMappings = rfidMappings ?? new List<RfidMapping>();
|
||||||
|
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFID 값으로 맵 노드 검색
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rfidValue">RFID 리더에서 읽은 값</param>
|
||||||
|
/// <returns>해당하는 맵 노드, 없으면 null</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 논리적 노드 ID로 맵 노드 검색
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">논리적 노드 ID</param>
|
||||||
|
/// <returns>해당하는 맵 노드, 없으면 null</returns>
|
||||||
|
public MapNode GetNodeById(string nodeId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(nodeId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return _mapNodes.FirstOrDefault(n =>
|
||||||
|
n.NodeId.Equals(nodeId, StringComparison.OrdinalIgnoreCase) && n.IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 노드로 연결된 RFID 값 검색
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">논리적 노드 ID</param>
|
||||||
|
/// <returns>연결된 RFID 값, 없으면 null</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFID 매핑 정보 검색
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rfidValue">RFID 값</param>
|
||||||
|
/// <returns>매핑 정보, 없으면 null</returns>
|
||||||
|
public RfidMapping GetRfidMapping(string rfidValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(rfidValue))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return _rfidMappings.FirstOrDefault(m =>
|
||||||
|
m.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase) && m.IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 새로운 RFID 매핑 추가
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rfidId">RFID 값</param>
|
||||||
|
/// <param name="nodeId">논리적 노드 ID</param>
|
||||||
|
/// <param name="description">설명</param>
|
||||||
|
/// <returns>추가 성공 여부</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFID 매핑 제거
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rfidId">제거할 RFID 값</param>
|
||||||
|
/// <returns>제거 성공 여부</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 노드 추가
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">추가할 맵 노드</param>
|
||||||
|
/// <returns>추가 성공 여부</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 노드 제거
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">제거할 노드 ID</param>
|
||||||
|
/// <returns>제거 성공 여부</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 특정 타입의 노드들 검색
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeType">노드 타입</param>
|
||||||
|
/// <returns>해당 타입의 노드 목록</returns>
|
||||||
|
public List<MapNode> GetNodesByType(NodeType nodeType)
|
||||||
|
{
|
||||||
|
return _mapNodes.Where(n => n.Type == nodeType && n.IsActive).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 장비 ID로 노드 검색
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stationId">장비 ID</param>
|
||||||
|
/// <returns>해당 장비의 노드, 없으면 null</returns>
|
||||||
|
public MapNode GetNodeByStationId(string stationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(stationId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return _mapNodes.FirstOrDefault(n =>
|
||||||
|
n.StationId.Equals(stationId, StringComparison.OrdinalIgnoreCase) && n.IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매핑되지 않은 노드들 검색 (RFID가 연결되지 않은 노드)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>매핑되지 않은 노드 목록</returns>
|
||||||
|
public List<MapNode> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사용되지 않는 RFID 매핑들 검색 (노드가 삭제된 매핑)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>사용되지 않는 매핑 목록</returns>
|
||||||
|
public List<RfidMapping> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터 초기화
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_rfidMappings.Clear();
|
||||||
|
_mapNodes.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터 유효성 검증
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>검증 결과 메시지 목록</returns>
|
||||||
|
public List<string> ValidateData()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
// 중복 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
469
Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
Normal file
469
Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AGVMapEditor.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 전용 경로 계산기 (A* 알고리즘 기반)
|
||||||
|
/// AGV의 방향성, 도킹 제약, 회전 제약을 고려한 경로 계산
|
||||||
|
/// </summary>
|
||||||
|
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<MapNode> _mapNodes;
|
||||||
|
private NodeResolver _nodeResolver;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 생성자
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mapNodes">맵 노드 목록</param>
|
||||||
|
/// <param name="nodeResolver">노드 해결기</param>
|
||||||
|
public PathCalculator(List<MapNode> mapNodes, NodeResolver nodeResolver)
|
||||||
|
{
|
||||||
|
_mapNodes = mapNodes ?? throw new ArgumentNullException(nameof(mapNodes));
|
||||||
|
_nodeResolver = nodeResolver ?? throw new ArgumentNullException(nameof(nodeResolver));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 계산 (메인 메서드)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startNodeId">시작 노드 ID</param>
|
||||||
|
/// <param name="targetNodeId">목표 노드 ID</param>
|
||||||
|
/// <param name="currentDirection">현재 AGV 방향</param>
|
||||||
|
/// <returns>경로 계산 결과</returns>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 유효성 검증 (RFID 이탈 감지시 사용)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentPath">현재 경로</param>
|
||||||
|
/// <param name="currentRfidId">현재 감지된 RFID</param>
|
||||||
|
/// <returns>경로 유효성 여부</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 동적 경로 재계산 (경로 이탈시 사용)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentRfidId">현재 RFID 위치</param>
|
||||||
|
/// <param name="targetNodeId">목표 노드 ID</param>
|
||||||
|
/// <param name="currentDirection">현재 방향</param>
|
||||||
|
/// <param name="originalPath">원래 경로 (참고용)</param>
|
||||||
|
/// <returns>새로운 경로</returns>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 입력 값 검증
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A* 알고리즘 실행
|
||||||
|
/// </summary>
|
||||||
|
private PathResult ExecuteAStar(string startNodeId, string targetNodeId, AgvDirection currentDirection)
|
||||||
|
{
|
||||||
|
var openSet = new SortedSet<PathNode>();
|
||||||
|
var closedSet = new HashSet<string>();
|
||||||
|
var gScore = new Dictionary<string, float>();
|
||||||
|
|
||||||
|
// 시작 노드 설정
|
||||||
|
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("경로를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 인접 노드들 처리
|
||||||
|
/// </summary>
|
||||||
|
private void ProcessNeighbors(PathNode current, string targetNodeId,
|
||||||
|
SortedSet<PathNode> openSet, HashSet<string> closedSet,
|
||||||
|
Dictionary<string, float> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 가능한 방향들 계산
|
||||||
|
/// </summary>
|
||||||
|
private List<AgvDirection> GetPossibleDirections(PathNode current, MapNode neighborNode)
|
||||||
|
{
|
||||||
|
var directions = new List<AgvDirection>();
|
||||||
|
|
||||||
|
// 기본적으로 전진/후진 가능
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이동 비용 계산
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 휴리스틱 함수 (목표까지의 추정 거리)
|
||||||
|
/// </summary>
|
||||||
|
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; // 좌표 단위 조정
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 두 점 사이의 거리 계산
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 회전 가능한 위치인지 확인
|
||||||
|
/// </summary>
|
||||||
|
private bool CanRotateAt(string nodeId)
|
||||||
|
{
|
||||||
|
var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||||
|
return node != null && (node.CanRotate || node.Type == NodeType.Rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 도킹 접근 방향이 유효한지 확인
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이동 명령 시퀀스 생성
|
||||||
|
/// </summary>
|
||||||
|
private List<AgvDirection> GenerateMovementSequence(PathNode from, PathNode to)
|
||||||
|
{
|
||||||
|
var sequence = new List<AgvDirection>();
|
||||||
|
|
||||||
|
// 방향 전환이 필요한 경우
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 재구성
|
||||||
|
/// </summary>
|
||||||
|
private PathResult ReconstructPath(PathNode goalNode, string startNodeId, string targetNodeId, AgvDirection startDirection)
|
||||||
|
{
|
||||||
|
var path = new List<PathNode>();
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 간 직선 거리 계산
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 특정 노드에서 가능한 다음 노드들 조회
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetPossibleNextNodes(string currentNodeId, AgvDirection currentDirection)
|
||||||
|
{
|
||||||
|
var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId);
|
||||||
|
if (currentNode == null)
|
||||||
|
return new List<string>();
|
||||||
|
|
||||||
|
return currentNode.ConnectedNodes.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 최적화 (선택적 기능)
|
||||||
|
/// </summary>
|
||||||
|
public PathResult OptimizePath(PathResult originalPath)
|
||||||
|
{
|
||||||
|
if (originalPath == null || !originalPath.Success)
|
||||||
|
return originalPath;
|
||||||
|
|
||||||
|
// TODO: 경로 최적화 로직 구현
|
||||||
|
// - 불필요한 중간 정지점 제거
|
||||||
|
// - 회전 최소화
|
||||||
|
// - 경로 단순화
|
||||||
|
|
||||||
|
return originalPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
144
Cs_HMI/AGVMapEditor/Models/PathNode.cs
Normal file
144
Cs_HMI/AGVMapEditor/Models/PathNode.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AGVMapEditor.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A* 알고리즘에서 사용되는 경로 노드
|
||||||
|
/// </summary>
|
||||||
|
public class PathNode : IComparable<PathNode>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 노드 ID
|
||||||
|
/// </summary>
|
||||||
|
public string NodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV의 현재 방향 (이 노드에 도달했을 때의 방향)
|
||||||
|
/// </summary>
|
||||||
|
public AgvDirection Direction { get; set; } = AgvDirection.Forward;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시작점에서 이 노드까지의 실제 비용 (G)
|
||||||
|
/// </summary>
|
||||||
|
public float GCost { get; set; } = float.MaxValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이 노드에서 목표까지의 추정 비용 (H)
|
||||||
|
/// </summary>
|
||||||
|
public float HCost { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 총 비용 (F = G + H)
|
||||||
|
/// </summary>
|
||||||
|
public float FCost => GCost + HCost;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이전 노드 (경로 추적용)
|
||||||
|
/// </summary>
|
||||||
|
public PathNode Parent { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 회전 횟수 (방향 전환 비용 계산용)
|
||||||
|
/// </summary>
|
||||||
|
public int RotationCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이 노드에 도달하기 위한 이동 명령 시퀀스
|
||||||
|
/// </summary>
|
||||||
|
public List<AgvDirection> MovementSequence { get; set; } = new List<AgvDirection>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기본 생성자
|
||||||
|
/// </summary>
|
||||||
|
public PathNode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매개변수 생성자
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">노드 ID</param>
|
||||||
|
/// <param name="direction">AGV 방향</param>
|
||||||
|
public PathNode(string nodeId, AgvDirection direction)
|
||||||
|
{
|
||||||
|
NodeId = nodeId;
|
||||||
|
Direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 우선순위 큐를 위한 비교 (FCost 기준)
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 노드 상태 복사
|
||||||
|
/// </summary>
|
||||||
|
public PathNode Clone()
|
||||||
|
{
|
||||||
|
return new PathNode
|
||||||
|
{
|
||||||
|
NodeId = NodeId,
|
||||||
|
Direction = Direction,
|
||||||
|
GCost = GCost,
|
||||||
|
HCost = HCost,
|
||||||
|
Parent = Parent,
|
||||||
|
RotationCount = RotationCount,
|
||||||
|
MovementSequence = new List<AgvDirection>(MovementSequence)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 고유 키 생성 (노드ID + 방향)
|
||||||
|
/// </summary>
|
||||||
|
public string GetKey()
|
||||||
|
{
|
||||||
|
return $"{NodeId}_{Direction}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 문자열 표현
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{NodeId}({Direction}) F:{FCost:F1} G:{GCost:F1} H:{HCost:F1} R:{RotationCount}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 해시코드 (딕셔너리 키용)
|
||||||
|
/// </summary>
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return GetKey().GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 동등성 비교
|
||||||
|
/// </summary>
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj is PathNode other)
|
||||||
|
{
|
||||||
|
return NodeId == other.NodeId && Direction == other.Direction;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
277
Cs_HMI/AGVMapEditor/Models/PathResult.cs
Normal file
277
Cs_HMI/AGVMapEditor/Models/PathResult.cs
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AGVMapEditor.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 계산 결과
|
||||||
|
/// </summary>
|
||||||
|
public class PathResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 계산 성공 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로상의 노드 ID 시퀀스
|
||||||
|
/// </summary>
|
||||||
|
public List<string> NodeSequence { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 이동 명령 시퀀스
|
||||||
|
/// </summary>
|
||||||
|
public List<AgvDirection> MovementSequence { get; set; } = new List<AgvDirection>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 총 이동 거리 (비용)
|
||||||
|
/// </summary>
|
||||||
|
public float TotalDistance { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 총 회전 횟수
|
||||||
|
/// </summary>
|
||||||
|
public int TotalRotations { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 예상 소요 시간 (초)
|
||||||
|
/// </summary>
|
||||||
|
public float EstimatedTime { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시작 노드 ID
|
||||||
|
/// </summary>
|
||||||
|
public string StartNodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 목표 노드 ID
|
||||||
|
/// </summary>
|
||||||
|
public string TargetNodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시작시 AGV 방향
|
||||||
|
/// </summary>
|
||||||
|
public AgvDirection StartDirection { get; set; } = AgvDirection.Forward;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 도착시 AGV 방향
|
||||||
|
/// </summary>
|
||||||
|
public AgvDirection EndDirection { get; set; } = AgvDirection.Forward;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 계산에 걸린 시간 (밀리초)
|
||||||
|
/// </summary>
|
||||||
|
public long CalculationTime { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 오류 메시지 (실패시)
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로상의 상세 정보 (디버깅용)
|
||||||
|
/// </summary>
|
||||||
|
public List<PathNode> DetailedPath { get; set; } = new List<PathNode>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 회전이 발생하는 노드들
|
||||||
|
/// </summary>
|
||||||
|
public List<string> RotationNodes { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기본 생성자
|
||||||
|
/// </summary>
|
||||||
|
public PathResult()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 성공 결과 생성자
|
||||||
|
/// </summary>
|
||||||
|
public PathResult(List<PathNode> 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<PathNode>(path);
|
||||||
|
|
||||||
|
// 노드 시퀀스 구성
|
||||||
|
NodeSequence = path.Select(p => p.NodeId).ToList();
|
||||||
|
|
||||||
|
// 이동 명령 시퀀스 구성
|
||||||
|
MovementSequence = new List<AgvDirection>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 실패 결과 생성자
|
||||||
|
/// </summary>
|
||||||
|
public PathResult(string errorMessage)
|
||||||
|
{
|
||||||
|
Success = false;
|
||||||
|
ErrorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 예상 소요 시간 계산
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 요약 정보
|
||||||
|
/// </summary>
|
||||||
|
public string GetSummary()
|
||||||
|
{
|
||||||
|
if (!Success)
|
||||||
|
{
|
||||||
|
return $"경로 계산 실패: {ErrorMessage}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"경로: {NodeSequence.Count}개 노드, " +
|
||||||
|
$"거리: {TotalDistance:F1}, " +
|
||||||
|
$"회전: {TotalRotations}회, " +
|
||||||
|
$"예상시간: {EstimatedTime:F1}초";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 상세 경로 정보
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetDetailedSteps()
|
||||||
|
{
|
||||||
|
var steps = new List<string>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFID 시퀀스 추출 (실제 AGV 제어용)
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetRfidSequence(NodeResolver nodeResolver)
|
||||||
|
{
|
||||||
|
var rfidSequence = new List<string>();
|
||||||
|
|
||||||
|
foreach (var nodeId in NodeSequence)
|
||||||
|
{
|
||||||
|
var rfidId = nodeResolver.GetRfidByNodeId(nodeId);
|
||||||
|
if (!string.IsNullOrEmpty(rfidId))
|
||||||
|
{
|
||||||
|
rfidSequence.Add(rfidId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rfidSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 유효성 검증
|
||||||
|
/// </summary>
|
||||||
|
public bool ValidatePath(List<MapNode> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 직렬화를 위한 문자열 변환
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return GetSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
Cs_HMI/AGVMapEditor/Models/RfidMapping.cs
Normal file
79
Cs_HMI/AGVMapEditor/Models/RfidMapping.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AGVMapEditor.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// RFID와 논리적 노드 ID를 매핑하는 클래스
|
||||||
|
/// 물리적 RFID는 의미없는 고유값, 논리적 노드는 맵 에디터에서 관리
|
||||||
|
/// </summary>
|
||||||
|
public class RfidMapping
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 물리적 RFID 값 (의미 없는 고유 식별자)
|
||||||
|
/// 예: "1234567890", "ABCDEF1234" 등
|
||||||
|
/// </summary>
|
||||||
|
public string RfidId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 논리적 노드 ID (맵 에디터에서 관리)
|
||||||
|
/// 예: "N001", "N002", "LOADER1", "CHARGER1" 등
|
||||||
|
/// </summary>
|
||||||
|
public string LogicalNodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매핑 생성 일자
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 마지막 수정 일자
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 설치 위치 설명 (현장 작업자용)
|
||||||
|
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFID 상태 (정상, 손상, 교체예정 등)
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "정상";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매핑 활성화 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기본 생성자
|
||||||
|
/// </summary>
|
||||||
|
public RfidMapping()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 매개변수 생성자
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rfidId">물리적 RFID ID</param>
|
||||||
|
/// <param name="logicalNodeId">논리적 노드 ID</param>
|
||||||
|
/// <param name="description">설치 위치 설명</param>
|
||||||
|
public RfidMapping(string rfidId, string logicalNodeId, string description = "")
|
||||||
|
{
|
||||||
|
RfidId = rfidId;
|
||||||
|
LogicalNodeId = logicalNodeId;
|
||||||
|
Description = description;
|
||||||
|
CreatedDate = DateTime.Now;
|
||||||
|
ModifiedDate = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 문자열 표현
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{RfidId} → {LogicalNodeId} ({Description})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Cs_HMI/AGVMapEditor/Program.cs
Normal file
26
Cs_HMI/AGVMapEditor/Program.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using AGVMapEditor.Forms;
|
||||||
|
|
||||||
|
namespace AGVMapEditor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 애플리케이션 진입점
|
||||||
|
/// </summary>
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 애플리케이션의 기본 진입점입니다.
|
||||||
|
/// </summary>
|
||||||
|
[STAThread]
|
||||||
|
static void Main()
|
||||||
|
{
|
||||||
|
// Windows Forms 애플리케이션 초기화
|
||||||
|
Application.EnableVisualStyles();
|
||||||
|
Application.SetCompatibleTextRenderingDefault(false);
|
||||||
|
|
||||||
|
// 메인 폼 실행
|
||||||
|
Application.Run(new MainForm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Cs_HMI/AGVMapEditor/Properties/AssemblyInfo.cs
Normal file
36
Cs_HMI/AGVMapEditor/Properties/AssemblyInfo.cs
Normal file
@@ -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")]
|
||||||
4
Cs_HMI/AGVMapEditor/packages.config
Normal file
4
Cs_HMI/AGVMapEditor/packages.config
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||||
|
</packages>
|
||||||
80
Cs_HMI/AGVSimulator/AGVSimulator.csproj
Normal file
80
Cs_HMI/AGVSimulator/AGVSimulator.csproj
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{B2C3D4E5-F6G7-8901-BCDE-F23456789012}</ProjectGuid>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<RootNamespace>AGVSimulator</RootNamespace>
|
||||||
|
<AssemblyName>AGVSimulator</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Drawing" />
|
||||||
|
<Reference Include="System.Windows.Forms" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
|
||||||
|
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Models\VirtualAGV.cs" />
|
||||||
|
<Compile Include="Models\SimulationState.cs" />
|
||||||
|
<Compile Include="Controls\SimulatorCanvas.cs">
|
||||||
|
<SubType>UserControl</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Controls\SimulatorCanvas.Designer.cs">
|
||||||
|
<DependentUpon>SimulatorCanvas.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Forms\SimulatorForm.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Forms\SimulatorForm.Designer.cs">
|
||||||
|
<DependentUpon>SimulatorForm.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Controls\SimulatorCanvas.resx">
|
||||||
|
<DependentUpon>SimulatorCanvas.cs</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Forms\SimulatorForm.resx">
|
||||||
|
<DependentUpon>SimulatorForm.cs</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AGVMapEditor\AGVMapEditor.csproj">
|
||||||
|
<Project>{a1b2c3d4-e5f6-7890-abcd-ef1234567890}</Project>
|
||||||
|
<Name>AGVMapEditor</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
52
Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.Designer.cs
generated
Normal file
52
Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.Designer.cs
generated
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
namespace AGVSimulator.Controls
|
||||||
|
{
|
||||||
|
partial class SimulatorCanvas
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 필수 디자이너 변수입니다.
|
||||||
|
/// </summary>
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사용 중인 모든 리소스를 정리합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
if (components != null)
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 커스텀 리소스 정리
|
||||||
|
CleanupResources();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 구성 요소 디자이너에서 생성한 코드
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 디자이너 지원에 필요한 메서드입니다.
|
||||||
|
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
620
Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs
Normal file
620
Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 시뮬레이션 시각화 캔버스
|
||||||
|
/// </summary>
|
||||||
|
public partial class SimulatorCanvas : UserControl
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private List<MapNode> _mapNodes;
|
||||||
|
private List<VirtualAGV> _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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 노드 목록
|
||||||
|
/// </summary>
|
||||||
|
public List<MapNode> MapNodes
|
||||||
|
{
|
||||||
|
get => _mapNodes;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_mapNodes = value;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 목록
|
||||||
|
/// </summary>
|
||||||
|
public List<VirtualAGV> AGVList
|
||||||
|
{
|
||||||
|
get => _agvList;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_agvList = value;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 경로
|
||||||
|
/// </summary>
|
||||||
|
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<MapNode>();
|
||||||
|
_agvList = new List<VirtualAGV>();
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 추가
|
||||||
|
/// </summary>
|
||||||
|
public void AddAGV(VirtualAGV agv)
|
||||||
|
{
|
||||||
|
if (_agvList == null)
|
||||||
|
_agvList = new List<VirtualAGV>();
|
||||||
|
|
||||||
|
_agvList.Add(agv);
|
||||||
|
|
||||||
|
// AGV 이벤트 연결
|
||||||
|
agv.PositionChanged += OnAGVPositionChanged;
|
||||||
|
agv.StateChanged += OnAGVStateChanged;
|
||||||
|
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 제거
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 모든 AGV 제거
|
||||||
|
/// </summary>
|
||||||
|
public void ClearAGVs()
|
||||||
|
{
|
||||||
|
if (_agvList != null)
|
||||||
|
{
|
||||||
|
foreach (var agv in _agvList)
|
||||||
|
{
|
||||||
|
agv.PositionChanged -= OnAGVPositionChanged;
|
||||||
|
agv.StateChanged -= OnAGVStateChanged;
|
||||||
|
}
|
||||||
|
_agvList.Clear();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 확대/축소 초기화
|
||||||
|
/// </summary>
|
||||||
|
public void ResetZoom()
|
||||||
|
{
|
||||||
|
_zoom = 1.0f;
|
||||||
|
_panOffset = Point.Empty;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 전체 맞춤
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
119
Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx
Normal file
119
Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion. Classes that don't support this are
|
||||||
|
serialized and stored with the mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
||||||
64
Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
Normal file
64
Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
namespace AGVSimulator.Forms
|
||||||
|
{
|
||||||
|
partial class SimulatorForm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 필수 디자이너 변수입니다.
|
||||||
|
/// </summary>
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사용 중인 모든 리소스를 정리합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시뮬레이션 정지
|
||||||
|
if (_simulationTimer != null)
|
||||||
|
{
|
||||||
|
_simulationTimer.Stop();
|
||||||
|
_simulationTimer.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// AGV 정리
|
||||||
|
if (_agvList != null)
|
||||||
|
{
|
||||||
|
foreach (var agv in _agvList)
|
||||||
|
{
|
||||||
|
agv.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form 디자이너에서 생성한 코드
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 디자이너 지원에 필요한 메서드입니다.
|
||||||
|
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||||
|
/// </summary>
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
this.SuspendLayout();
|
||||||
|
//
|
||||||
|
// SimulatorForm
|
||||||
|
//
|
||||||
|
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||||
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
|
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||||
|
this.Name = "SimulatorForm";
|
||||||
|
this.Text = "AGV 시뮬레이터";
|
||||||
|
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
|
||||||
|
this.ResumeLayout(false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
688
Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
Normal file
688
Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using AGVMapEditor.Models;
|
||||||
|
using AGVSimulator.Controls;
|
||||||
|
using AGVSimulator.Models;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace AGVSimulator.Forms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 시뮬레이터 메인 폼
|
||||||
|
/// </summary>
|
||||||
|
public partial class SimulatorForm : Form
|
||||||
|
{
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private SimulatorCanvas _simulatorCanvas;
|
||||||
|
private List<MapNode> _mapNodes;
|
||||||
|
private List<RfidMapping> _rfidMappings;
|
||||||
|
private NodeResolver _nodeResolver;
|
||||||
|
private PathCalculator _pathCalculator;
|
||||||
|
private List<VirtualAGV> _agvList;
|
||||||
|
private SimulationState _simulationState;
|
||||||
|
private Timer _simulationTimer;
|
||||||
|
|
||||||
|
// UI Controls
|
||||||
|
private MenuStrip _menuStrip;
|
||||||
|
private ToolStrip _toolStrip;
|
||||||
|
private StatusStrip _statusStrip;
|
||||||
|
private Panel _controlPanel;
|
||||||
|
private Panel _canvasPanel;
|
||||||
|
|
||||||
|
// Control Panel Controls
|
||||||
|
private GroupBox _agvControlGroup;
|
||||||
|
private ComboBox _agvListCombo;
|
||||||
|
private Button _addAgvButton;
|
||||||
|
private Button _removeAgvButton;
|
||||||
|
private Button _startSimulationButton;
|
||||||
|
private Button _stopSimulationButton;
|
||||||
|
private Button _resetButton;
|
||||||
|
|
||||||
|
private GroupBox _pathGroup;
|
||||||
|
private ComboBox _startNodeCombo;
|
||||||
|
private ComboBox _targetNodeCombo;
|
||||||
|
private Button _calculatePathButton;
|
||||||
|
private Button _startPathButton;
|
||||||
|
private Button _clearPathButton;
|
||||||
|
|
||||||
|
private GroupBox _viewGroup;
|
||||||
|
private Button _fitToMapButton;
|
||||||
|
private Button _resetZoomButton;
|
||||||
|
|
||||||
|
private GroupBox _statusGroup;
|
||||||
|
private Label _simulationStatusLabel;
|
||||||
|
private Label _agvCountLabel;
|
||||||
|
private Label _pathLengthLabel;
|
||||||
|
|
||||||
|
// Status Labels
|
||||||
|
private ToolStripStatusLabel _statusLabel;
|
||||||
|
private ToolStripStatusLabel _coordLabel;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 상태
|
||||||
|
/// </summary>
|
||||||
|
public SimulationState SimulationState => _simulationState;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
public SimulatorForm()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
InitializeForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Initialization
|
||||||
|
|
||||||
|
private void InitializeForm()
|
||||||
|
{
|
||||||
|
// 폼 설정
|
||||||
|
Text = "AGV 시뮬레이터";
|
||||||
|
Size = new Size(1200, 800);
|
||||||
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
|
||||||
|
// 데이터 초기화
|
||||||
|
_mapNodes = new List<MapNode>();
|
||||||
|
_rfidMappings = new List<RfidMapping>();
|
||||||
|
_agvList = new List<VirtualAGV>();
|
||||||
|
_simulationState = new SimulationState();
|
||||||
|
|
||||||
|
// UI 컨트롤 생성
|
||||||
|
CreateMenuStrip();
|
||||||
|
CreateToolStrip();
|
||||||
|
CreateStatusStrip();
|
||||||
|
CreateControlPanel();
|
||||||
|
CreateSimulatorCanvas();
|
||||||
|
|
||||||
|
// 레이아웃 설정
|
||||||
|
SetupLayout();
|
||||||
|
|
||||||
|
// 타이머 초기화
|
||||||
|
_simulationTimer = new Timer();
|
||||||
|
_simulationTimer.Interval = 100; // 100ms 간격
|
||||||
|
_simulationTimer.Tick += OnSimulationTimer_Tick;
|
||||||
|
|
||||||
|
// 초기 상태 설정
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateMenuStrip()
|
||||||
|
{
|
||||||
|
_menuStrip = new MenuStrip();
|
||||||
|
|
||||||
|
// 파일 메뉴
|
||||||
|
var fileMenu = new ToolStripMenuItem("파일(&F)");
|
||||||
|
fileMenu.DropDownItems.Add(new ToolStripMenuItem("맵 열기(&O)...", null, OnOpenMap_Click) { ShortcutKeys = Keys.Control | Keys.O });
|
||||||
|
fileMenu.DropDownItems.Add(new ToolStripSeparator());
|
||||||
|
fileMenu.DropDownItems.Add(new ToolStripMenuItem("종료(&X)", null, OnExit_Click) { ShortcutKeys = Keys.Alt | Keys.F4 });
|
||||||
|
|
||||||
|
// 시뮬레이션 메뉴
|
||||||
|
var simMenu = new ToolStripMenuItem("시뮬레이션(&S)");
|
||||||
|
simMenu.DropDownItems.Add(new ToolStripMenuItem("시작(&S)", null, OnStartSimulation_Click) { ShortcutKeys = Keys.F5 });
|
||||||
|
simMenu.DropDownItems.Add(new ToolStripMenuItem("정지(&T)", null, OnStopSimulation_Click) { ShortcutKeys = Keys.F6 });
|
||||||
|
simMenu.DropDownItems.Add(new ToolStripMenuItem("초기화(&R)", null, OnReset_Click) { ShortcutKeys = Keys.F7 });
|
||||||
|
|
||||||
|
// 보기 메뉴
|
||||||
|
var viewMenu = new ToolStripMenuItem("보기(&V)");
|
||||||
|
viewMenu.DropDownItems.Add(new ToolStripMenuItem("맵 맞춤(&F)", null, OnFitToMap_Click) { ShortcutKeys = Keys.Control | Keys.F });
|
||||||
|
viewMenu.DropDownItems.Add(new ToolStripMenuItem("줌 초기화(&Z)", null, OnResetZoom_Click) { ShortcutKeys = Keys.Control | Keys.D0 });
|
||||||
|
|
||||||
|
// 도움말 메뉴
|
||||||
|
var helpMenu = new ToolStripMenuItem("도움말(&H)");
|
||||||
|
helpMenu.DropDownItems.Add(new ToolStripMenuItem("정보(&A)...", null, OnAbout_Click));
|
||||||
|
|
||||||
|
_menuStrip.Items.AddRange(new ToolStripItem[] { fileMenu, simMenu, viewMenu, helpMenu });
|
||||||
|
Controls.Add(_menuStrip);
|
||||||
|
MainMenuStrip = _menuStrip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateToolStrip()
|
||||||
|
{
|
||||||
|
_toolStrip = new ToolStrip();
|
||||||
|
|
||||||
|
_toolStrip.Items.Add(new ToolStripButton("맵 열기", null, OnOpenMap_Click) { ToolTipText = "맵 파일을 엽니다" });
|
||||||
|
_toolStrip.Items.Add(new ToolStripSeparator());
|
||||||
|
_toolStrip.Items.Add(new ToolStripButton("시뮬레이션 시작", null, OnStartSimulation_Click) { ToolTipText = "시뮬레이션을 시작합니다" });
|
||||||
|
_toolStrip.Items.Add(new ToolStripButton("시뮬레이션 정지", null, OnStopSimulation_Click) { ToolTipText = "시뮬레이션을 정지합니다" });
|
||||||
|
_toolStrip.Items.Add(new ToolStripButton("초기화", null, OnReset_Click) { ToolTipText = "시뮬레이션을 초기화합니다" });
|
||||||
|
_toolStrip.Items.Add(new ToolStripSeparator());
|
||||||
|
_toolStrip.Items.Add(new ToolStripButton("맵 맞춤", null, OnFitToMap_Click) { ToolTipText = "맵 전체를 화면에 맞춥니다" });
|
||||||
|
_toolStrip.Items.Add(new ToolStripButton("줌 초기화", null, OnResetZoom_Click) { ToolTipText = "줌을 초기화합니다" });
|
||||||
|
|
||||||
|
Controls.Add(_toolStrip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateStatusStrip()
|
||||||
|
{
|
||||||
|
_statusStrip = new StatusStrip();
|
||||||
|
|
||||||
|
_statusLabel = new ToolStripStatusLabel("준비");
|
||||||
|
_coordLabel = new ToolStripStatusLabel();
|
||||||
|
|
||||||
|
_statusStrip.Items.AddRange(new ToolStripItem[] { _statusLabel, _coordLabel });
|
||||||
|
Controls.Add(_statusStrip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateControlPanel()
|
||||||
|
{
|
||||||
|
_controlPanel = new Panel();
|
||||||
|
_controlPanel.Width = 250;
|
||||||
|
_controlPanel.Dock = DockStyle.Right;
|
||||||
|
_controlPanel.BackColor = SystemColors.Control;
|
||||||
|
|
||||||
|
// AGV 제어 그룹
|
||||||
|
CreateAGVControlGroup();
|
||||||
|
|
||||||
|
// 경로 제어 그룹
|
||||||
|
CreatePathControlGroup();
|
||||||
|
|
||||||
|
// 뷰 제어 그룹
|
||||||
|
CreateViewControlGroup();
|
||||||
|
|
||||||
|
// 상태 그룹
|
||||||
|
CreateStatusGroup();
|
||||||
|
|
||||||
|
Controls.Add(_controlPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateAGVControlGroup()
|
||||||
|
{
|
||||||
|
_agvControlGroup = new GroupBox();
|
||||||
|
_agvControlGroup.Text = "AGV 제어";
|
||||||
|
_agvControlGroup.Location = new Point(10, 10);
|
||||||
|
_agvControlGroup.Size = new Size(230, 120);
|
||||||
|
|
||||||
|
_agvListCombo = new ComboBox();
|
||||||
|
_agvListCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||||
|
_agvListCombo.Location = new Point(10, 25);
|
||||||
|
_agvListCombo.Size = new Size(210, 21);
|
||||||
|
_agvListCombo.SelectedIndexChanged += OnAGVList_SelectedIndexChanged;
|
||||||
|
|
||||||
|
_addAgvButton = new Button();
|
||||||
|
_addAgvButton.Text = "AGV 추가";
|
||||||
|
_addAgvButton.Location = new Point(10, 55);
|
||||||
|
_addAgvButton.Size = new Size(100, 25);
|
||||||
|
_addAgvButton.Click += OnAddAGV_Click;
|
||||||
|
|
||||||
|
_removeAgvButton = new Button();
|
||||||
|
_removeAgvButton.Text = "AGV 제거";
|
||||||
|
_removeAgvButton.Location = new Point(120, 55);
|
||||||
|
_removeAgvButton.Size = new Size(100, 25);
|
||||||
|
_removeAgvButton.Click += OnRemoveAGV_Click;
|
||||||
|
|
||||||
|
_startSimulationButton = new Button();
|
||||||
|
_startSimulationButton.Text = "시뮬레이션 시작";
|
||||||
|
_startSimulationButton.Location = new Point(10, 85);
|
||||||
|
_startSimulationButton.Size = new Size(100, 25);
|
||||||
|
_startSimulationButton.Click += OnStartSimulation_Click;
|
||||||
|
|
||||||
|
_stopSimulationButton = new Button();
|
||||||
|
_stopSimulationButton.Text = "시뮬레이션 정지";
|
||||||
|
_stopSimulationButton.Location = new Point(120, 85);
|
||||||
|
_stopSimulationButton.Size = new Size(100, 25);
|
||||||
|
_stopSimulationButton.Click += OnStopSimulation_Click;
|
||||||
|
|
||||||
|
_agvControlGroup.Controls.AddRange(new Control[] {
|
||||||
|
_agvListCombo, _addAgvButton, _removeAgvButton, _startSimulationButton, _stopSimulationButton
|
||||||
|
});
|
||||||
|
|
||||||
|
_controlPanel.Controls.Add(_agvControlGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreatePathControlGroup()
|
||||||
|
{
|
||||||
|
_pathGroup = new GroupBox();
|
||||||
|
_pathGroup.Text = "경로 제어";
|
||||||
|
_pathGroup.Location = new Point(10, 140);
|
||||||
|
_pathGroup.Size = new Size(230, 150);
|
||||||
|
|
||||||
|
var startLabel = new Label();
|
||||||
|
startLabel.Text = "시작 노드:";
|
||||||
|
startLabel.Location = new Point(10, 25);
|
||||||
|
startLabel.Size = new Size(70, 15);
|
||||||
|
|
||||||
|
_startNodeCombo = new ComboBox();
|
||||||
|
_startNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||||
|
_startNodeCombo.Location = new Point(10, 45);
|
||||||
|
_startNodeCombo.Size = new Size(210, 21);
|
||||||
|
|
||||||
|
var targetLabel = new Label();
|
||||||
|
targetLabel.Text = "목표 노드:";
|
||||||
|
targetLabel.Location = new Point(10, 75);
|
||||||
|
targetLabel.Size = new Size(70, 15);
|
||||||
|
|
||||||
|
_targetNodeCombo = new ComboBox();
|
||||||
|
_targetNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||||
|
_targetNodeCombo.Location = new Point(10, 95);
|
||||||
|
_targetNodeCombo.Size = new Size(210, 21);
|
||||||
|
|
||||||
|
_calculatePathButton = new Button();
|
||||||
|
_calculatePathButton.Text = "경로 계산";
|
||||||
|
_calculatePathButton.Location = new Point(10, 120);
|
||||||
|
_calculatePathButton.Size = new Size(65, 25);
|
||||||
|
_calculatePathButton.Click += OnCalculatePath_Click;
|
||||||
|
|
||||||
|
_startPathButton = new Button();
|
||||||
|
_startPathButton.Text = "경로 시작";
|
||||||
|
_startPathButton.Location = new Point(80, 120);
|
||||||
|
_startPathButton.Size = new Size(65, 25);
|
||||||
|
_startPathButton.Click += OnStartPath_Click;
|
||||||
|
|
||||||
|
_clearPathButton = new Button();
|
||||||
|
_clearPathButton.Text = "경로 지우기";
|
||||||
|
_clearPathButton.Location = new Point(150, 120);
|
||||||
|
_clearPathButton.Size = new Size(70, 25);
|
||||||
|
_clearPathButton.Click += OnClearPath_Click;
|
||||||
|
|
||||||
|
_pathGroup.Controls.AddRange(new Control[] {
|
||||||
|
startLabel, _startNodeCombo, targetLabel, _targetNodeCombo,
|
||||||
|
_calculatePathButton, _startPathButton, _clearPathButton
|
||||||
|
});
|
||||||
|
|
||||||
|
_controlPanel.Controls.Add(_pathGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateViewControlGroup()
|
||||||
|
{
|
||||||
|
_viewGroup = new GroupBox();
|
||||||
|
_viewGroup.Text = "화면 제어";
|
||||||
|
_viewGroup.Location = new Point(10, 300);
|
||||||
|
_viewGroup.Size = new Size(230, 60);
|
||||||
|
|
||||||
|
_fitToMapButton = new Button();
|
||||||
|
_fitToMapButton.Text = "맵 맞춤";
|
||||||
|
_fitToMapButton.Location = new Point(10, 25);
|
||||||
|
_fitToMapButton.Size = new Size(100, 25);
|
||||||
|
_fitToMapButton.Click += OnFitToMap_Click;
|
||||||
|
|
||||||
|
_resetZoomButton = new Button();
|
||||||
|
_resetZoomButton.Text = "줌 초기화";
|
||||||
|
_resetZoomButton.Location = new Point(120, 25);
|
||||||
|
_resetZoomButton.Size = new Size(100, 25);
|
||||||
|
_resetZoomButton.Click += OnResetZoom_Click;
|
||||||
|
|
||||||
|
_resetButton = new Button();
|
||||||
|
_resetButton.Text = "전체 초기화";
|
||||||
|
_resetButton.Location = new Point(65, 55);
|
||||||
|
_resetButton.Size = new Size(100, 25);
|
||||||
|
_resetButton.Click += OnReset_Click;
|
||||||
|
|
||||||
|
_viewGroup.Controls.AddRange(new Control[] { _fitToMapButton, _resetZoomButton });
|
||||||
|
|
||||||
|
_controlPanel.Controls.Add(_viewGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateStatusGroup()
|
||||||
|
{
|
||||||
|
_statusGroup = new GroupBox();
|
||||||
|
_statusGroup.Text = "상태 정보";
|
||||||
|
_statusGroup.Location = new Point(10, 370);
|
||||||
|
_statusGroup.Size = new Size(230, 100);
|
||||||
|
|
||||||
|
_simulationStatusLabel = new Label();
|
||||||
|
_simulationStatusLabel.Text = "시뮬레이션: 정지";
|
||||||
|
_simulationStatusLabel.Location = new Point(10, 25);
|
||||||
|
_simulationStatusLabel.Size = new Size(210, 15);
|
||||||
|
|
||||||
|
_agvCountLabel = new Label();
|
||||||
|
_agvCountLabel.Text = "AGV 수: 0";
|
||||||
|
_agvCountLabel.Location = new Point(10, 45);
|
||||||
|
_agvCountLabel.Size = new Size(210, 15);
|
||||||
|
|
||||||
|
_pathLengthLabel = new Label();
|
||||||
|
_pathLengthLabel.Text = "경로 길이: -";
|
||||||
|
_pathLengthLabel.Location = new Point(10, 65);
|
||||||
|
_pathLengthLabel.Size = new Size(210, 15);
|
||||||
|
|
||||||
|
_statusGroup.Controls.AddRange(new Control[] {
|
||||||
|
_simulationStatusLabel, _agvCountLabel, _pathLengthLabel
|
||||||
|
});
|
||||||
|
|
||||||
|
_controlPanel.Controls.Add(_statusGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateSimulatorCanvas()
|
||||||
|
{
|
||||||
|
_canvasPanel = new Panel();
|
||||||
|
_canvasPanel.Dock = DockStyle.Fill;
|
||||||
|
|
||||||
|
_simulatorCanvas = new SimulatorCanvas();
|
||||||
|
_simulatorCanvas.Dock = DockStyle.Fill;
|
||||||
|
|
||||||
|
_canvasPanel.Controls.Add(_simulatorCanvas);
|
||||||
|
Controls.Add(_canvasPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupLayout()
|
||||||
|
{
|
||||||
|
// Z-Order 설정
|
||||||
|
_canvasPanel.BringToFront();
|
||||||
|
_controlPanel.BringToFront();
|
||||||
|
_toolStrip.BringToFront();
|
||||||
|
_menuStrip.BringToFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Event Handlers
|
||||||
|
|
||||||
|
private void OnOpenMap_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
using (var openDialog = new OpenFileDialog())
|
||||||
|
{
|
||||||
|
openDialog.Filter = "맵 파일 (*.json)|*.json|모든 파일 (*.*)|*.*";
|
||||||
|
openDialog.Title = "맵 파일 열기";
|
||||||
|
|
||||||
|
if (openDialog.ShowDialog() == DialogResult.OK)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LoadMapFile(openDialog.FileName);
|
||||||
|
_statusLabel.Text = $"맵 로드 완료: {Path.GetFileName(openDialog.FileName)}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"맵 파일을 로드할 수 없습니다:\n{ex.Message}", "오류",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExit_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStartSimulation_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_simulationState.IsRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_simulationState.IsRunning = true;
|
||||||
|
_simulationTimer.Start();
|
||||||
|
_statusLabel.Text = "시뮬레이션 실행 중";
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStopSimulation_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!_simulationState.IsRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_simulationState.IsRunning = false;
|
||||||
|
_simulationTimer.Stop();
|
||||||
|
_statusLabel.Text = "시뮬레이션 정지";
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReset_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// 시뮬레이션 정지
|
||||||
|
if (_simulationState.IsRunning)
|
||||||
|
{
|
||||||
|
OnStopSimulation_Click(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AGV 초기화
|
||||||
|
_simulatorCanvas.ClearAGVs();
|
||||||
|
_agvList.Clear();
|
||||||
|
|
||||||
|
// 경로 초기화
|
||||||
|
_simulatorCanvas.CurrentPath = null;
|
||||||
|
|
||||||
|
// UI 업데이트
|
||||||
|
UpdateAGVComboBox();
|
||||||
|
UpdateNodeComboBoxes();
|
||||||
|
UpdateUI();
|
||||||
|
|
||||||
|
_statusLabel.Text = "초기화 완료";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFitToMap_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_simulatorCanvas.FitToMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResetZoom_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_simulatorCanvas.ResetZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAbout_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
MessageBox.Show("AGV 시뮬레이터 v1.0\n\nENIG AGV 시스템용 시뮬레이터", "정보",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAddAGV_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_mapNodes == null || _mapNodes.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show("먼저 맵을 로드해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var agvId = $"AGV{_agvList.Count + 1:D2}";
|
||||||
|
var startPosition = _mapNodes.First().Position; // 첫 번째 노드에서 시작
|
||||||
|
|
||||||
|
var newAGV = new VirtualAGV(agvId, startPosition);
|
||||||
|
_agvList.Add(newAGV);
|
||||||
|
_simulatorCanvas.AddAGV(newAGV);
|
||||||
|
|
||||||
|
UpdateAGVComboBox();
|
||||||
|
UpdateUI();
|
||||||
|
|
||||||
|
_statusLabel.Text = $"{agvId} 추가됨";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRemoveAGV_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_agvListCombo.SelectedItem == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||||
|
if (selectedAGV != null)
|
||||||
|
{
|
||||||
|
_simulatorCanvas.RemoveAGV(selectedAGV.AgvId);
|
||||||
|
_agvList.Remove(selectedAGV);
|
||||||
|
|
||||||
|
UpdateAGVComboBox();
|
||||||
|
UpdateUI();
|
||||||
|
|
||||||
|
_statusLabel.Text = $"{selectedAGV.AgvId} 제거됨";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAGVList_SelectedIndexChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCalculatePath_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show("시작 노드와 목표 노드를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startNode = _startNodeCombo.SelectedItem as MapNode;
|
||||||
|
var targetNode = _targetNodeCombo.SelectedItem as MapNode;
|
||||||
|
|
||||||
|
if (_pathCalculator == null)
|
||||||
|
{
|
||||||
|
_pathCalculator = new PathCalculator(_mapNodes, _nodeResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = _pathCalculator.CalculatePath(startNode.NodeId, targetNode.NodeId, AgvDirection.Forward);
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
_simulatorCanvas.CurrentPath = result;
|
||||||
|
_pathLengthLabel.Text = $"경로 길이: {result.TotalDistance:F1}";
|
||||||
|
_statusLabel.Text = $"경로 계산 완료 ({result.CalculationTime}ms)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MessageBox.Show($"경로를 찾을 수 없습니다:\n{result.ErrorMessage}", "경로 계산 실패",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStartPath_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
|
||||||
|
if (selectedAGV == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show("AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_simulatorCanvas.CurrentPath == null || !_simulatorCanvas.CurrentPath.Success)
|
||||||
|
{
|
||||||
|
MessageBox.Show("먼저 경로를 계산해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedAGV.StartPath(_simulatorCanvas.CurrentPath, _mapNodes);
|
||||||
|
_statusLabel.Text = $"{selectedAGV.AgvId} 경로 시작";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClearPath_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_simulatorCanvas.CurrentPath = null;
|
||||||
|
_pathLengthLabel.Text = "경로 길이: -";
|
||||||
|
_statusLabel.Text = "경로 지움";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSimulationTimer_Tick(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// 시뮬레이션 업데이트는 각 AGV의 내부 타이머에서 처리됨
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Methods
|
||||||
|
|
||||||
|
private void LoadMapFile(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
|
||||||
|
// 구조체로 직접 역직렬화
|
||||||
|
var mapData = JsonConvert.DeserializeObject<MapFileData>(json);
|
||||||
|
|
||||||
|
if (mapData != null)
|
||||||
|
{
|
||||||
|
_mapNodes = mapData.MapNodes ?? new List<MapNode>();
|
||||||
|
_rfidMappings = mapData.RfidMappings ?? new List<RfidMapping>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_mapNodes = new List<MapNode>();
|
||||||
|
_rfidMappings = new List<RfidMapping>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeResolver 초기화
|
||||||
|
_nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
|
||||||
|
|
||||||
|
// 시뮬레이터 캔버스에 맵 설정
|
||||||
|
_simulatorCanvas.MapNodes = _mapNodes;
|
||||||
|
|
||||||
|
// UI 업데이트
|
||||||
|
UpdateNodeComboBoxes();
|
||||||
|
UpdateUI();
|
||||||
|
|
||||||
|
// 맵에 맞춤
|
||||||
|
_simulatorCanvas.FitToMap();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"맵 파일 로드 실패: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 맵 파일 데이터 구조체
|
||||||
|
private class MapFileData
|
||||||
|
{
|
||||||
|
public List<MapNode> MapNodes { get; set; }
|
||||||
|
public List<RfidMapping> RfidMappings { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateNodeComboBoxes()
|
||||||
|
{
|
||||||
|
_startNodeCombo.Items.Clear();
|
||||||
|
_targetNodeCombo.Items.Clear();
|
||||||
|
|
||||||
|
if (_mapNodes != null)
|
||||||
|
{
|
||||||
|
foreach (var node in _mapNodes)
|
||||||
|
{
|
||||||
|
_startNodeCombo.Items.Add(node);
|
||||||
|
_targetNodeCombo.Items.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_startNodeCombo.DisplayMember = "NodeId";
|
||||||
|
_targetNodeCombo.DisplayMember = "NodeId";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAGVComboBox()
|
||||||
|
{
|
||||||
|
_agvListCombo.Items.Clear();
|
||||||
|
|
||||||
|
if (_agvList != null)
|
||||||
|
{
|
||||||
|
foreach (var agv in _agvList)
|
||||||
|
{
|
||||||
|
_agvListCombo.Items.Add(agv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_agvListCombo.DisplayMember = "AgvId";
|
||||||
|
|
||||||
|
if (_agvListCombo.Items.Count > 0)
|
||||||
|
{
|
||||||
|
_agvListCombo.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUI()
|
||||||
|
{
|
||||||
|
// 시뮬레이션 상태
|
||||||
|
_simulationStatusLabel.Text = _simulationState.IsRunning ? "시뮬레이션: 실행 중" : "시뮬레이션: 정지";
|
||||||
|
|
||||||
|
// AGV 수
|
||||||
|
_agvCountLabel.Text = $"AGV 수: {_agvList?.Count ?? 0}";
|
||||||
|
|
||||||
|
// 버튼 상태
|
||||||
|
_startSimulationButton.Enabled = !_simulationState.IsRunning && _agvList?.Count > 0;
|
||||||
|
_stopSimulationButton.Enabled = _simulationState.IsRunning;
|
||||||
|
|
||||||
|
_removeAgvButton.Enabled = _agvListCombo.SelectedItem != null;
|
||||||
|
_startPathButton.Enabled = _agvListCombo.SelectedItem != null &&
|
||||||
|
_simulatorCanvas.CurrentPath != null &&
|
||||||
|
_simulatorCanvas.CurrentPath.Success;
|
||||||
|
|
||||||
|
_calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
|
||||||
|
_targetNodeCombo.SelectedItem != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx
Normal file
61
Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
||||||
135
Cs_HMI/AGVSimulator/Models/SimulationState.cs
Normal file
135
Cs_HMI/AGVSimulator/Models/SimulationState.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AGVSimulator.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 상태 관리 클래스
|
||||||
|
/// </summary>
|
||||||
|
public class SimulationState
|
||||||
|
{
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 실행 중 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRunning { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 시작 시간
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 경과 시간
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ElapsedTime => StartTime.HasValue ? DateTime.Now - StartTime.Value : TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 속도 배율 (1.0 = 실시간, 2.0 = 2배속)
|
||||||
|
/// </summary>
|
||||||
|
public float SpeedMultiplier { get; set; } = 1.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 총 처리된 이벤트 수
|
||||||
|
/// </summary>
|
||||||
|
public int TotalEvents { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 총 이동 거리 (모든 AGV 합계)
|
||||||
|
/// </summary>
|
||||||
|
public float TotalDistance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 발생한 오류 수
|
||||||
|
/// </summary>
|
||||||
|
public int ErrorCount { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기본 생성자
|
||||||
|
/// </summary>
|
||||||
|
public SimulationState()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 시작
|
||||||
|
/// </summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (!IsRunning)
|
||||||
|
{
|
||||||
|
IsRunning = true;
|
||||||
|
StartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 정지
|
||||||
|
/// </summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시뮬레이션 상태 초기화
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
StartTime = null;
|
||||||
|
SpeedMultiplier = 1.0f;
|
||||||
|
TotalEvents = 0;
|
||||||
|
TotalDistance = 0;
|
||||||
|
ErrorCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이벤트 발생 시 호출
|
||||||
|
/// </summary>
|
||||||
|
public void RecordEvent()
|
||||||
|
{
|
||||||
|
TotalEvents++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이동 거리 추가
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="distance">이동한 거리</param>
|
||||||
|
public void AddDistance(float distance)
|
||||||
|
{
|
||||||
|
TotalDistance += distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 오류 발생 시 호출
|
||||||
|
/// </summary>
|
||||||
|
public void RecordError()
|
||||||
|
{
|
||||||
|
ErrorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 통계 정보 조회
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>통계 정보 문자열</returns>
|
||||||
|
public string GetStatistics()
|
||||||
|
{
|
||||||
|
return $"실행시간: {ElapsedTime:hh\\:mm\\:ss}, " +
|
||||||
|
$"이벤트: {TotalEvents}, " +
|
||||||
|
$"총거리: {TotalDistance:F1}, " +
|
||||||
|
$"오류: {ErrorCount}";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
482
Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
Normal file
482
Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
using AGVMapEditor.Models;
|
||||||
|
|
||||||
|
namespace AGVSimulator.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 가상 AGV 상태
|
||||||
|
/// </summary>
|
||||||
|
public enum AGVState
|
||||||
|
{
|
||||||
|
Idle, // 대기
|
||||||
|
Moving, // 이동 중
|
||||||
|
Rotating, // 회전 중
|
||||||
|
Docking, // 도킹 중
|
||||||
|
Charging, // 충전 중
|
||||||
|
Error // 오류
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 가상 AGV 클래스
|
||||||
|
/// 실제 AGV의 동작을 시뮬레이션
|
||||||
|
/// </summary>
|
||||||
|
public class VirtualAGV
|
||||||
|
{
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 상태 변경 이벤트
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<AGVState> StateChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 위치 변경 이벤트
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<Point> PositionChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFID 감지 이벤트
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<string> RfidDetected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 완료 이벤트
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<PathResult> PathCompleted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 오류 발생 이벤트
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<string> 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<string> _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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV ID
|
||||||
|
/// </summary>
|
||||||
|
public string AgvId => _agvId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 위치
|
||||||
|
/// </summary>
|
||||||
|
public Point CurrentPosition => _currentPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 방향
|
||||||
|
/// </summary>
|
||||||
|
public AgvDirection CurrentDirection => _currentDirection;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 상태
|
||||||
|
/// </summary>
|
||||||
|
public AGVState CurrentState => _currentState;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 속도
|
||||||
|
/// </summary>
|
||||||
|
public float CurrentSpeed => _currentSpeed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 경로
|
||||||
|
/// </summary>
|
||||||
|
public PathResult CurrentPath => _currentPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 노드 ID
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentNodeId => _currentNodeId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 목표 위치
|
||||||
|
/// </summary>
|
||||||
|
public Point TargetPosition => _targetPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 배터리 레벨 (시뮬레이션)
|
||||||
|
/// </summary>
|
||||||
|
public float BatteryLevel { get; set; } = 100.0f;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 생성자
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="agvId">AGV ID</param>
|
||||||
|
/// <param name="startPosition">시작 위치</param>
|
||||||
|
/// <param name="startDirection">시작 방향</param>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 실행 시작
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">실행할 경로</param>
|
||||||
|
/// <param name="mapNodes">맵 노드 목록</param>
|
||||||
|
public void StartPath(PathResult path, List<MapNode> mapNodes)
|
||||||
|
{
|
||||||
|
if (path == null || !path.Success)
|
||||||
|
{
|
||||||
|
OnError("유효하지 않은 경로입니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPath = path;
|
||||||
|
_remainingNodes = new List<string>(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]}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 경로 정지
|
||||||
|
/// </summary>
|
||||||
|
public void StopPath()
|
||||||
|
{
|
||||||
|
_moveTimer.Stop();
|
||||||
|
_currentPath = null;
|
||||||
|
_remainingNodes?.Clear();
|
||||||
|
SetState(AGVState.Idle);
|
||||||
|
_currentSpeed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 긴급 정지
|
||||||
|
/// </summary>
|
||||||
|
public void EmergencyStop()
|
||||||
|
{
|
||||||
|
StopPath();
|
||||||
|
OnError("긴급 정지가 실행되었습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 수동 이동 (테스트용)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetPosition">목표 위치</param>
|
||||||
|
public void MoveTo(Point targetPosition)
|
||||||
|
{
|
||||||
|
_targetPosition = targetPosition;
|
||||||
|
_moveStartPosition = _currentPosition;
|
||||||
|
_moveTargetPosition = targetPosition;
|
||||||
|
_moveProgress = 0;
|
||||||
|
|
||||||
|
SetState(AGVState.Moving);
|
||||||
|
_moveTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 수동 회전 (테스트용)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="direction">회전 방향</param>
|
||||||
|
public void Rotate(AgvDirection direction)
|
||||||
|
{
|
||||||
|
if (_currentState != AGVState.Idle)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetState(AGVState.Rotating);
|
||||||
|
|
||||||
|
// 시뮬레이션: 즉시 방향 변경 (실제로는 시간이 걸림)
|
||||||
|
_currentDirection = direction;
|
||||||
|
|
||||||
|
System.Threading.Thread.Sleep(500); // 회전 시간 시뮬레이션
|
||||||
|
SetState(AGVState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 충전 시작 (시뮬레이션)
|
||||||
|
/// </summary>
|
||||||
|
public void StartCharging()
|
||||||
|
{
|
||||||
|
if (_currentState == AGVState.Idle)
|
||||||
|
{
|
||||||
|
SetState(AGVState.Charging);
|
||||||
|
// 충전 시뮬레이션 시작
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 충전 종료
|
||||||
|
/// </summary>
|
||||||
|
public void StopCharging()
|
||||||
|
{
|
||||||
|
if (_currentState == AGVState.Charging)
|
||||||
|
{
|
||||||
|
SetState(AGVState.Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 정보 조회
|
||||||
|
/// </summary>
|
||||||
|
public string GetStatus()
|
||||||
|
{
|
||||||
|
return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " +
|
||||||
|
$"방향:{_currentDirection} 상태:{_currentState} " +
|
||||||
|
$"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 RFID 시뮬레이션 (현재 위치 기준)
|
||||||
|
/// </summary>
|
||||||
|
public string SimulateRfidReading(List<MapNode> mapNodes, List<RfidMapping> 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<MapNode> 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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 리소스 정리
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_moveTimer?.Stop();
|
||||||
|
_moveTimer?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Cs_HMI/AGVSimulator/Program.cs
Normal file
32
Cs_HMI/AGVSimulator/Program.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using AGVSimulator.Forms;
|
||||||
|
|
||||||
|
namespace AGVSimulator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AGV 시뮬레이터 프로그램 진입점
|
||||||
|
/// </summary>
|
||||||
|
static class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 애플리케이션의 주 진입점입니다.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Cs_HMI/AGVSimulator/Properties/AssemblyInfo.cs
Normal file
36
Cs_HMI/AGVSimulator/Properties/AssemblyInfo.cs
Normal file
@@ -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")]
|
||||||
4
Cs_HMI/AGVSimulator/packages.config
Normal file
4
Cs_HMI/AGVSimulator/packages.config
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||||
|
</packages>
|
||||||
141
Cs_HMI/CLAUDE.md
Normal file
141
Cs_HMI/CLAUDE.md
Normal file
@@ -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 두 섹션으로 구성
|
||||||
217
Cs_HMI/TODO.md
Normal file
217
Cs_HMI/TODO.md
Normal file
@@ -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<string> 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 설치 → 소프트웨어에서 매핑만 변경 → 완료
|
||||||
|
```
|
||||||
18
Cs_HMI/build.bat
Normal file
18
Cs_HMI/build.bat
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user