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:
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>
|
||||
Reference in New Issue
Block a user