파일정리
This commit is contained in:
43
AGVLogic/AGVLogic.sln
Normal file
43
AGVLogic/AGVLogic.sln
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36310.24
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVMapEditor", "AGVMapEditor\AGVMapEditor.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVNavigationCore", "AGVNavigationCore\AGVNavigationCore.csproj", "{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVSimulator", "AGVSimulator\AGVSimulator.csproj", "{B2C3D4E5-0000-0000-0000-000000000000}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ENIGProtocol", "EnigProtocol\enigprotocol\ENIGProtocol.csproj", "{9365803B-933D-4237-93C7-B502C855A71C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-0000-0000-0000-000000000000}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9365803B-933D-4237-93C7-B502C855A71C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9365803B-933D-4237-93C7-B502C855A71C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9365803B-933D-4237-93C7-B502C855A71C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9365803B-933D-4237-93C7-B502C855A71C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {638744DA-A7C8-43E2-A98E-0DE9BDB1DA35}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
96
AGVLogic/AGVMapEditor/AGVMapEditor.csproj
Normal file
96
AGVLogic/AGVMapEditor/AGVMapEditor.csproj
Normal file
@@ -0,0 +1,96 @@
|
||||
<?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>
|
||||
<ProjectReference Include="..\AGVNavigationCore\AGVNavigationCore.csproj">
|
||||
<Project>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</Project>
|
||||
<Name>AGVNavigationCore</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Controls\ImageEditorCanvas.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Forms\ImageEditorForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Forms\ImageEditorForm.Designer.cs">
|
||||
<DependentUpon>ImageEditorForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Models\EditorSettings.cs" />
|
||||
<Compile Include="Models\ImagePathEditor.cs" />
|
||||
<Compile Include="Models\MapImage.cs" />
|
||||
<Compile Include="Models\MapLabel.cs" />
|
||||
<Compile Include="Forms\MainForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Forms\MainForm.Designer.cs">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Forms\MainForm.resx">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="build.bat" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
415
AGVLogic/AGVMapEditor/Controls/ImageEditorCanvas.cs
Normal file
415
AGVLogic/AGVMapEditor/Controls/ImageEditorCanvas.cs
Normal file
@@ -0,0 +1,415 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AGVMapEditor.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 이미지 편집용 사용자 정의 캔버스 컨트롤
|
||||
/// 이미지 중앙 정렬, 크기 조정 핸들, 브러시 그리기 기능 제공
|
||||
/// </summary>
|
||||
public class ImageEditorCanvas : UserControl
|
||||
{
|
||||
private Bitmap _editingImage;
|
||||
private Graphics _imageGraphics;
|
||||
private Rectangle _imageRect = Rectangle.Empty;
|
||||
private float _imageDisplayWidth = 0;
|
||||
private float _imageDisplayHeight = 0;
|
||||
|
||||
// 브러시 그리기
|
||||
private bool _isDrawing = false;
|
||||
private Point _lastDrawPoint = Point.Empty;
|
||||
private Color _drawColor = Color.Black;
|
||||
private int _brushSize = 3;
|
||||
private bool _brushModeEnabled = false;
|
||||
|
||||
// 크기 조정
|
||||
private bool _isResizing = false;
|
||||
private ResizeHandle _activeHandle = ResizeHandle.None;
|
||||
private Point _resizeStartPoint = Point.Empty;
|
||||
private float _resizeStartWidth = 0;
|
||||
private float _resizeStartHeight = 0;
|
||||
private const int HANDLE_SIZE = 8;
|
||||
|
||||
private enum ResizeHandle
|
||||
{
|
||||
None,
|
||||
TopLeft, Top, TopRight,
|
||||
Right, BottomRight, Bottom,
|
||||
BottomLeft, Left
|
||||
}
|
||||
|
||||
public ImageEditorCanvas()
|
||||
{
|
||||
this.DoubleBuffered = true;
|
||||
this.BackColor = Color.White;
|
||||
this.AutoScroll = true;
|
||||
|
||||
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
public Bitmap EditingImage
|
||||
{
|
||||
get => _editingImage;
|
||||
set
|
||||
{
|
||||
_editingImage = value;
|
||||
if (_editingImage != null)
|
||||
{
|
||||
_imageGraphics?.Dispose();
|
||||
_imageGraphics = Graphics.FromImage(_editingImage);
|
||||
_imageDisplayWidth = _editingImage.Width;
|
||||
_imageDisplayHeight = _editingImage.Height;
|
||||
UpdateImageRect();
|
||||
}
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public Color DrawColor
|
||||
{
|
||||
get => _drawColor;
|
||||
set => _drawColor = value;
|
||||
}
|
||||
|
||||
public int BrushSize
|
||||
{
|
||||
get => _brushSize;
|
||||
set => _brushSize = value;
|
||||
}
|
||||
|
||||
public bool BrushModeEnabled
|
||||
{
|
||||
get => _brushModeEnabled;
|
||||
set => _brushModeEnabled = value;
|
||||
}
|
||||
|
||||
public Size ImageDisplaySize => new Size((int)_imageDisplayWidth, (int)_imageDisplayHeight);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Paint
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
base.OnPaint(e);
|
||||
|
||||
if (_editingImage == null)
|
||||
{
|
||||
e.Graphics.Clear(BackColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// 배경 채우기
|
||||
e.Graphics.Clear(BackColor);
|
||||
|
||||
// 이미지 영역 업데이트
|
||||
UpdateImageRect();
|
||||
|
||||
// 이미지 그리기 (고품질)
|
||||
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
e.Graphics.DrawImage(_editingImage, _imageRect);
|
||||
|
||||
// 크기 조정 핸들 그리기
|
||||
DrawResizeHandles(e.Graphics);
|
||||
}
|
||||
|
||||
private void UpdateImageRect()
|
||||
{
|
||||
if (_editingImage == null || Width == 0 || Height == 0)
|
||||
{
|
||||
_imageRect = Rectangle.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
// 이미지를 중앙 정렬
|
||||
float x = (Width - _imageDisplayWidth) / 2f;
|
||||
float y = (Height - _imageDisplayHeight) / 2f;
|
||||
|
||||
// 음수 방지
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
|
||||
_imageRect = new Rectangle((int)x, (int)y, (int)_imageDisplayWidth, (int)_imageDisplayHeight);
|
||||
}
|
||||
|
||||
private void DrawResizeHandles(Graphics g)
|
||||
{
|
||||
if (_imageRect.IsEmpty)
|
||||
return;
|
||||
|
||||
var handles = GetResizeHandles();
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
g.FillRectangle(Brushes.Blue, handle);
|
||||
g.DrawRectangle(Pens.White, handle);
|
||||
}
|
||||
}
|
||||
|
||||
private Rectangle[] GetResizeHandles()
|
||||
{
|
||||
int x = _imageRect.X;
|
||||
int y = _imageRect.Y;
|
||||
int w = _imageRect.Width;
|
||||
int h = _imageRect.Height;
|
||||
int hs = HANDLE_SIZE;
|
||||
|
||||
return new Rectangle[]
|
||||
{
|
||||
new Rectangle(x - hs/2, y - hs/2, hs, hs), // TopLeft
|
||||
new Rectangle(x + w/2 - hs/2, y - hs/2, hs, hs), // Top
|
||||
new Rectangle(x + w - hs/2, y - hs/2, hs, hs), // TopRight
|
||||
new Rectangle(x + w - hs/2, y + h/2 - hs/2, hs, hs), // Right
|
||||
new Rectangle(x + w - hs/2, y + h - hs/2, hs, hs), // BottomRight
|
||||
new Rectangle(x + w/2 - hs/2, y + h - hs/2, hs, hs), // Bottom
|
||||
new Rectangle(x - hs/2, y + h - hs/2, hs, hs), // BottomLeft
|
||||
new Rectangle(x - hs/2, y + h/2 - hs/2, hs, hs) // Left
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mouse Events
|
||||
|
||||
protected override void OnMouseDown(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseDown(e);
|
||||
|
||||
if (_editingImage == null || e.Button != MouseButtons.Left)
|
||||
return;
|
||||
|
||||
// 크기 조정 핸들 확인
|
||||
_activeHandle = GetHandleAtPoint(e.Location);
|
||||
if (_activeHandle != ResizeHandle.None)
|
||||
{
|
||||
_isResizing = true;
|
||||
_resizeStartPoint = e.Location;
|
||||
_resizeStartWidth = _imageDisplayWidth;
|
||||
_resizeStartHeight = _imageDisplayHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
// 브러시 모드: 그리기
|
||||
if (_brushModeEnabled && _imageRect.Contains(e.Location))
|
||||
{
|
||||
_isDrawing = true;
|
||||
_lastDrawPoint = ImagePointFromScreen(e.Location);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseMove(e);
|
||||
|
||||
if (_editingImage == null)
|
||||
return;
|
||||
|
||||
// 크기 조정 중
|
||||
if (_isResizing && _activeHandle != ResizeHandle.None)
|
||||
{
|
||||
ResizeImageDisplay(e.Location);
|
||||
return;
|
||||
}
|
||||
|
||||
// 크기 조정 핸들 위에 마우스가 있으면 커서 변경
|
||||
var handle = GetHandleAtPoint(e.Location);
|
||||
if (handle != ResizeHandle.None)
|
||||
{
|
||||
Cursor = GetCursorForHandle(handle);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Cursor = Cursors.Default;
|
||||
}
|
||||
|
||||
// 브러시 모드: 그리기
|
||||
if (_isDrawing && _lastDrawPoint != Point.Empty && _brushModeEnabled && _imageRect.Contains(e.Location))
|
||||
{
|
||||
Point currentImagePoint = ImagePointFromScreen(e.Location);
|
||||
_imageGraphics.DrawLine(new Pen(_drawColor, _brushSize), _lastDrawPoint, currentImagePoint);
|
||||
_lastDrawPoint = currentImagePoint;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
|
||||
if (_isResizing)
|
||||
{
|
||||
_isResizing = false;
|
||||
_activeHandle = ResizeHandle.None;
|
||||
}
|
||||
|
||||
if (_isDrawing)
|
||||
{
|
||||
_isDrawing = false;
|
||||
_lastDrawPoint = Point.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnResize(EventArgs e)
|
||||
{
|
||||
base.OnResize(e);
|
||||
UpdateImageRect();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 화면 좌표를 이미지 좌표로 변환
|
||||
/// </summary>
|
||||
private Point ImagePointFromScreen(Point screenPoint)
|
||||
{
|
||||
if (_imageRect.IsEmpty || _editingImage == null)
|
||||
return Point.Empty;
|
||||
|
||||
// 화면 좌표를 이미지 비율로 변환
|
||||
float scaleX = (float)_editingImage.Width / _imageRect.Width;
|
||||
float scaleY = (float)_editingImage.Height / _imageRect.Height;
|
||||
|
||||
int imageX = (int)((screenPoint.X - _imageRect.X) * scaleX);
|
||||
int imageY = (int)((screenPoint.Y - _imageRect.Y) * scaleY);
|
||||
|
||||
return new Point(imageX, imageY);
|
||||
}
|
||||
|
||||
private ResizeHandle GetHandleAtPoint(Point pt)
|
||||
{
|
||||
var handles = GetResizeHandles();
|
||||
var handleTypes = new[]
|
||||
{
|
||||
ResizeHandle.TopLeft, ResizeHandle.Top, ResizeHandle.TopRight,
|
||||
ResizeHandle.Right, ResizeHandle.BottomRight, ResizeHandle.Bottom,
|
||||
ResizeHandle.BottomLeft, ResizeHandle.Left
|
||||
};
|
||||
|
||||
for (int i = 0; i < handles.Length; i++)
|
||||
{
|
||||
if (handles[i].Contains(pt))
|
||||
return handleTypes[i];
|
||||
}
|
||||
|
||||
return ResizeHandle.None;
|
||||
}
|
||||
|
||||
private Cursor GetCursorForHandle(ResizeHandle handle)
|
||||
{
|
||||
switch (handle)
|
||||
{
|
||||
case ResizeHandle.TopLeft:
|
||||
case ResizeHandle.BottomRight:
|
||||
return Cursors.SizeNWSE;
|
||||
case ResizeHandle.TopRight:
|
||||
case ResizeHandle.BottomLeft:
|
||||
return Cursors.SizeNESW;
|
||||
case ResizeHandle.Top:
|
||||
case ResizeHandle.Bottom:
|
||||
return Cursors.SizeNS;
|
||||
case ResizeHandle.Left:
|
||||
case ResizeHandle.Right:
|
||||
return Cursors.SizeWE;
|
||||
default:
|
||||
return Cursors.Default;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResizeImageDisplay(Point currentPoint)
|
||||
{
|
||||
int deltaX = currentPoint.X - _resizeStartPoint.X;
|
||||
int deltaY = currentPoint.Y - _resizeStartPoint.Y;
|
||||
|
||||
float newWidth = _resizeStartWidth;
|
||||
float newHeight = _resizeStartHeight;
|
||||
|
||||
switch (_activeHandle)
|
||||
{
|
||||
case ResizeHandle.TopLeft:
|
||||
newWidth -= deltaX;
|
||||
newHeight -= deltaY;
|
||||
break;
|
||||
case ResizeHandle.Top:
|
||||
newHeight -= deltaY;
|
||||
break;
|
||||
case ResizeHandle.TopRight:
|
||||
newWidth += deltaX;
|
||||
newHeight -= deltaY;
|
||||
break;
|
||||
case ResizeHandle.Right:
|
||||
newWidth += deltaX;
|
||||
break;
|
||||
case ResizeHandle.BottomRight:
|
||||
newWidth += deltaX;
|
||||
newHeight += deltaY;
|
||||
break;
|
||||
case ResizeHandle.Bottom:
|
||||
newHeight += deltaY;
|
||||
break;
|
||||
case ResizeHandle.BottomLeft:
|
||||
newWidth -= deltaX;
|
||||
newHeight += deltaY;
|
||||
break;
|
||||
case ResizeHandle.Left:
|
||||
newWidth -= deltaX;
|
||||
break;
|
||||
}
|
||||
|
||||
// 최소 크기 제한
|
||||
if (newWidth < 50) newWidth = 50;
|
||||
if (newHeight < 50) newHeight = 50;
|
||||
|
||||
_imageDisplayWidth = newWidth;
|
||||
_imageDisplayHeight = newHeight;
|
||||
|
||||
UpdateImageRect();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 표시 크기로 실제 이미지 리사이즈
|
||||
/// </summary>
|
||||
public Bitmap GetResizedImage()
|
||||
{
|
||||
if (_editingImage == null)
|
||||
return null;
|
||||
|
||||
int targetWidth = (int)_imageDisplayWidth;
|
||||
int targetHeight = (int)_imageDisplayHeight;
|
||||
|
||||
// 크기가 같으면 원본 반환
|
||||
if (targetWidth == _editingImage.Width && targetHeight == _editingImage.Height)
|
||||
return new Bitmap(_editingImage);
|
||||
|
||||
// 리사이즈
|
||||
var resized = new Bitmap(targetWidth, targetHeight);
|
||||
using (var g = Graphics.FromImage(resized))
|
||||
{
|
||||
g.CompositingQuality = CompositingQuality.HighQuality;
|
||||
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
g.SmoothingMode = SmoothingMode.HighQuality;
|
||||
g.DrawImage(_editingImage, 0, 0, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
return resized;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_imageGraphics?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
188
AGVLogic/AGVMapEditor/Forms/ImageEditorForm.Designer.cs
generated
Normal file
188
AGVLogic/AGVMapEditor/Forms/ImageEditorForm.Designer.cs
generated
Normal file
@@ -0,0 +1,188 @@
|
||||
namespace AGVMapEditor.Forms
|
||||
{
|
||||
partial class ImageEditorForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.toolPanel = new System.Windows.Forms.Panel();
|
||||
this.chkBrushMode = new System.Windows.Forms.CheckBox();
|
||||
this.btnColor = new System.Windows.Forms.Button();
|
||||
this.trackBrush = new System.Windows.Forms.TrackBar();
|
||||
this.lblBrush = new System.Windows.Forms.Label();
|
||||
this.btnSave = new System.Windows.Forms.Button();
|
||||
this.btnResize = new System.Windows.Forms.Button();
|
||||
this.btnOpen = new System.Windows.Forms.Button();
|
||||
this.canvasPanel = new System.Windows.Forms.Panel();
|
||||
this.imageCanvas = new AGVMapEditor.Controls.ImageEditorCanvas();
|
||||
this.toolPanel.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trackBrush)).BeginInit();
|
||||
this.canvasPanel.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// toolPanel
|
||||
//
|
||||
this.toolPanel.BackColor = System.Drawing.Color.LightGray;
|
||||
this.toolPanel.Controls.Add(this.chkBrushMode);
|
||||
this.toolPanel.Controls.Add(this.btnColor);
|
||||
this.toolPanel.Controls.Add(this.trackBrush);
|
||||
this.toolPanel.Controls.Add(this.lblBrush);
|
||||
this.toolPanel.Controls.Add(this.btnSave);
|
||||
this.toolPanel.Controls.Add(this.btnResize);
|
||||
this.toolPanel.Controls.Add(this.btnOpen);
|
||||
this.toolPanel.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.toolPanel.Location = new System.Drawing.Point(0, 0);
|
||||
this.toolPanel.Name = "toolPanel";
|
||||
this.toolPanel.Size = new System.Drawing.Size(800, 50);
|
||||
this.toolPanel.TabIndex = 0;
|
||||
//
|
||||
// chkBrushMode
|
||||
//
|
||||
this.chkBrushMode.AutoSize = true;
|
||||
this.chkBrushMode.Location = new System.Drawing.Point(590, 15);
|
||||
this.chkBrushMode.Name = "chkBrushMode";
|
||||
this.chkBrushMode.Size = new System.Drawing.Size(96, 16);
|
||||
this.chkBrushMode.TabIndex = 6;
|
||||
this.chkBrushMode.Text = "브러시 모드";
|
||||
this.chkBrushMode.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// btnColor
|
||||
//
|
||||
this.btnColor.BackColor = System.Drawing.Color.Black;
|
||||
this.btnColor.ForeColor = System.Drawing.Color.White;
|
||||
this.btnColor.Location = new System.Drawing.Point(520, 10);
|
||||
this.btnColor.Name = "btnColor";
|
||||
this.btnColor.Size = new System.Drawing.Size(60, 23);
|
||||
this.btnColor.TabIndex = 5;
|
||||
this.btnColor.Text = "색상";
|
||||
this.btnColor.UseVisualStyleBackColor = false;
|
||||
this.btnColor.Click += new System.EventHandler(this.BtnColor_Click);
|
||||
//
|
||||
// trackBrush
|
||||
//
|
||||
this.trackBrush.Location = new System.Drawing.Point(410, 10);
|
||||
this.trackBrush.Maximum = 20;
|
||||
this.trackBrush.Minimum = 1;
|
||||
this.trackBrush.Name = "trackBrush";
|
||||
this.trackBrush.Size = new System.Drawing.Size(100, 45);
|
||||
this.trackBrush.TabIndex = 4;
|
||||
this.trackBrush.Value = 3;
|
||||
this.trackBrush.ValueChanged += new System.EventHandler(this.TrackBrush_ValueChanged);
|
||||
//
|
||||
// lblBrush
|
||||
//
|
||||
this.lblBrush.AutoSize = true;
|
||||
this.lblBrush.Location = new System.Drawing.Point(350, 15);
|
||||
this.lblBrush.Name = "lblBrush";
|
||||
this.lblBrush.Size = new System.Drawing.Size(54, 12);
|
||||
this.lblBrush.TabIndex = 3;
|
||||
this.lblBrush.Text = "브러시:";
|
||||
//
|
||||
// btnSave
|
||||
//
|
||||
this.btnSave.Location = new System.Drawing.Point(230, 10);
|
||||
this.btnSave.Name = "btnSave";
|
||||
this.btnSave.Size = new System.Drawing.Size(100, 23);
|
||||
this.btnSave.TabIndex = 2;
|
||||
this.btnSave.Text = "저장 및 닫기";
|
||||
this.btnSave.UseVisualStyleBackColor = true;
|
||||
this.btnSave.Click += new System.EventHandler(this.BtnSave_Click);
|
||||
//
|
||||
// btnResize
|
||||
//
|
||||
this.btnResize.Location = new System.Drawing.Point(120, 10);
|
||||
this.btnResize.Name = "btnResize";
|
||||
this.btnResize.Size = new System.Drawing.Size(100, 23);
|
||||
this.btnResize.TabIndex = 1;
|
||||
this.btnResize.Text = "크기 조정";
|
||||
this.btnResize.UseVisualStyleBackColor = true;
|
||||
this.btnResize.Click += new System.EventHandler(this.BtnResize_Click);
|
||||
//
|
||||
// btnOpen
|
||||
//
|
||||
this.btnOpen.Location = new System.Drawing.Point(10, 10);
|
||||
this.btnOpen.Name = "btnOpen";
|
||||
this.btnOpen.Size = new System.Drawing.Size(100, 23);
|
||||
this.btnOpen.TabIndex = 0;
|
||||
this.btnOpen.Text = "이미지 열기";
|
||||
this.btnOpen.UseVisualStyleBackColor = true;
|
||||
this.btnOpen.Click += new System.EventHandler(this.BtnOpen_Click);
|
||||
//
|
||||
// canvasPanel
|
||||
//
|
||||
this.canvasPanel.AutoScroll = true;
|
||||
this.canvasPanel.BackColor = System.Drawing.Color.White;
|
||||
this.canvasPanel.Controls.Add(this.imageCanvas);
|
||||
this.canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.canvasPanel.Location = new System.Drawing.Point(0, 50);
|
||||
this.canvasPanel.Name = "canvasPanel";
|
||||
this.canvasPanel.Size = new System.Drawing.Size(800, 550);
|
||||
this.canvasPanel.TabIndex = 1;
|
||||
//
|
||||
// imageCanvas
|
||||
//
|
||||
this.imageCanvas.BackColor = System.Drawing.Color.White;
|
||||
this.imageCanvas.BrushModeEnabled = false;
|
||||
this.imageCanvas.BrushSize = 3;
|
||||
this.imageCanvas.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.imageCanvas.DrawColor = System.Drawing.Color.Black;
|
||||
this.imageCanvas.Location = new System.Drawing.Point(0, 0);
|
||||
this.imageCanvas.Name = "imageCanvas";
|
||||
this.imageCanvas.Size = new System.Drawing.Size(800, 550);
|
||||
this.imageCanvas.TabIndex = 0;
|
||||
//
|
||||
// ImageEditorForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(800, 600);
|
||||
this.Controls.Add(this.canvasPanel);
|
||||
this.Controls.Add(this.toolPanel);
|
||||
this.Name = "ImageEditorForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "이미지 편집기";
|
||||
this.toolPanel.ResumeLayout(false);
|
||||
this.toolPanel.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trackBrush)).EndInit();
|
||||
this.canvasPanel.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Panel toolPanel;
|
||||
private System.Windows.Forms.Button btnOpen;
|
||||
private System.Windows.Forms.Button btnResize;
|
||||
private System.Windows.Forms.Button btnSave;
|
||||
private System.Windows.Forms.Label lblBrush;
|
||||
private System.Windows.Forms.TrackBar trackBrush;
|
||||
private System.Windows.Forms.Button btnColor;
|
||||
private System.Windows.Forms.CheckBox chkBrushMode;
|
||||
private System.Windows.Forms.Panel canvasPanel;
|
||||
private AGVMapEditor.Controls.ImageEditorCanvas imageCanvas;
|
||||
}
|
||||
}
|
||||
208
AGVLogic/AGVMapEditor/Forms/ImageEditorForm.cs
Normal file
208
AGVLogic/AGVMapEditor/Forms/ImageEditorForm.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Windows.Forms;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.Utils;
|
||||
|
||||
namespace AGVMapEditor.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// 이미지 노드의 이미지를 편집하기 위한 간단한 그림판
|
||||
/// 불러오기, 저장, 크기 조정, 기본 드로잉 기능 제공
|
||||
/// </summary>
|
||||
public partial class ImageEditorForm : Form
|
||||
{
|
||||
private MapImage _targetNode;
|
||||
|
||||
public ImageEditorForm(MapImage imageNode = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_targetNode = imageNode;
|
||||
SetupUI();
|
||||
|
||||
if (imageNode != null && imageNode.LoadedImage != null)
|
||||
{
|
||||
LoadImageFromNode(imageNode);
|
||||
}
|
||||
|
||||
this.KeyPreview = true;
|
||||
this.KeyDown += (s1, e1) => {
|
||||
if (e1.KeyCode == Keys.Escape) this.Close();
|
||||
};
|
||||
}
|
||||
|
||||
private void SetupUI()
|
||||
{
|
||||
// 캔버스 초기 설정
|
||||
imageCanvas.BrushSize = trackBrush.Value;
|
||||
imageCanvas.BrushModeEnabled = chkBrushMode.Checked;
|
||||
imageCanvas.BackColor = Color.FromArgb(32,32,32);
|
||||
|
||||
// 이벤트 연결
|
||||
chkBrushMode.CheckedChanged += (s, e) => imageCanvas.BrushModeEnabled = chkBrushMode.Checked;
|
||||
}
|
||||
|
||||
private void TrackBrush_ValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
imageCanvas.BrushSize = trackBrush.Value;
|
||||
}
|
||||
|
||||
private void LoadImageFromNode(MapImage node)
|
||||
{
|
||||
if (node.LoadedImage != null)
|
||||
{
|
||||
imageCanvas.EditingImage = new Bitmap(node.LoadedImage);
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnOpen_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (var ofd = new OpenFileDialog { Filter = "이미지|*.jpg;*.png;*.bmp|모든 파일|*.*" })
|
||||
{
|
||||
if (ofd.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
LoadImageFromFile(ofd.FileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadImageFromFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var loadedImage = Image.FromFile(filePath);
|
||||
|
||||
// 이미지 크기가 크면 자동 축소 (최대 512x512)
|
||||
Bitmap finalImage;
|
||||
if (loadedImage.Width > 512 || loadedImage.Height > 512)
|
||||
{
|
||||
finalImage = ResizeImage(loadedImage, 512, 512);
|
||||
loadedImage.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
finalImage = new Bitmap(loadedImage);
|
||||
loadedImage.Dispose();
|
||||
}
|
||||
|
||||
imageCanvas.EditingImage = finalImage;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"이미지 로드 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnResize_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (imageCanvas.EditingImage == null)
|
||||
{
|
||||
MessageBox.Show("먼저 이미지를 로드하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentSize = imageCanvas.ImageDisplaySize;
|
||||
|
||||
using (var form = new Form())
|
||||
{
|
||||
form.Text = "이미지 크기 조정";
|
||||
form.Size = new Size(300, 150);
|
||||
form.StartPosition = FormStartPosition.CenterParent;
|
||||
|
||||
var lblWidth = new Label { Text = "너비:", Left = 10, Top = 10, Width = 50 };
|
||||
var txtWidth = new TextBox { Left = 70, Top = 10, Width = 100, Text = currentSize.Width.ToString() };
|
||||
|
||||
var lblHeight = new Label { Text = "높이:", Left = 10, Top = 40, Width = 50 };
|
||||
var txtHeight = new TextBox { Left = 70, Top = 40, Width = 100, Text = currentSize.Height.ToString() };
|
||||
|
||||
var btnOk = new Button { Text = "적용", DialogResult = DialogResult.OK, Left = 70, Top = 70, Width = 100 };
|
||||
var btnCancel = new Button { Text = "취소", DialogResult = DialogResult.Cancel, Left = 180, Top = 70, Width = 70 };
|
||||
|
||||
form.Controls.Add(lblWidth);
|
||||
form.Controls.Add(txtWidth);
|
||||
form.Controls.Add(lblHeight);
|
||||
form.Controls.Add(txtHeight);
|
||||
form.Controls.Add(btnOk);
|
||||
form.Controls.Add(btnCancel);
|
||||
|
||||
if (form.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
if (int.TryParse(txtWidth.Text, out int width) && int.TryParse(txtHeight.Text, out int height))
|
||||
{
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
var resized = new Bitmap(imageCanvas.EditingImage, width, height);
|
||||
imageCanvas.EditingImage = resized;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnColor_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (var cfd = new ColorDialog { Color = imageCanvas.DrawColor })
|
||||
{
|
||||
if (cfd.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
imageCanvas.DrawColor = cfd.Color;
|
||||
(sender as Button).BackColor = cfd.Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnSave_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (imageCanvas.EditingImage == null)
|
||||
{
|
||||
MessageBox.Show("저장할 이미지가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_targetNode != null)
|
||||
{
|
||||
// 표시 크기로 리사이즈된 이미지 가져오기
|
||||
var finalImage = imageCanvas.GetResizedImage();
|
||||
|
||||
if (finalImage == null)
|
||||
{
|
||||
MessageBox.Show("이미지 처리 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
var displaySize = imageCanvas.ImageDisplaySize;
|
||||
MessageBox.Show($"이미지 크기: {displaySize.Width}x{displaySize.Height}로 저장됩니다.");
|
||||
|
||||
// 이미지를 Base64로 변환하여 저장
|
||||
_targetNode.ImageBase64 = ImageConverterUtil.ImageToBase64(finalImage, System.Drawing.Imaging.ImageFormat.Png);
|
||||
_targetNode.LoadedImage?.Dispose();
|
||||
_targetNode.LoadedImage = finalImage;
|
||||
|
||||
MessageBox.Show("이미지가 저장되었습니다.");
|
||||
this.DialogResult = DialogResult.OK;
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap ResizeImage(Image image, int maxWidth, int maxHeight)
|
||||
{
|
||||
double ratioX = (double)maxWidth / image.Width;
|
||||
double ratioY = (double)maxHeight / image.Height;
|
||||
double ratio = Math.Min(ratioX, ratioY);
|
||||
|
||||
int newWidth = (int)(image.Width * ratio);
|
||||
int newHeight = (int)(image.Height * ratio);
|
||||
|
||||
var resized = new Bitmap(newWidth, newHeight);
|
||||
using (var g = Graphics.FromImage(resized))
|
||||
{
|
||||
g.CompositingQuality = CompositingQuality.HighQuality;
|
||||
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
g.SmoothingMode = SmoothingMode.HighQuality;
|
||||
g.DrawImage(image, 0, 0, newWidth, newHeight);
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
}
|
||||
}
|
||||
667
AGVLogic/AGVMapEditor/Forms/MainForm.Designer.cs
generated
Normal file
667
AGVLogic/AGVMapEditor/Forms/MainForm.Designer.cs
generated
Normal file
@@ -0,0 +1,667 @@
|
||||
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()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
|
||||
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.listBoxNodes = new System.Windows.Forms.ListBox();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.tabPage1 = new System.Windows.Forms.TabPage();
|
||||
this.lstNodeConnection = new System.Windows.Forms.ListBox();
|
||||
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
|
||||
this.btNodeRemove = new System.Windows.Forms.ToolStripButton();
|
||||
this._propertyGrid = new System.Windows.Forms.PropertyGrid();
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.toolStrip3 = new System.Windows.Forms.ToolStrip();
|
||||
this.btnSelect = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnMove = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnAddNode = new System.Windows.Forms.ToolStripSplitButton();
|
||||
this.btnAddLabel = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnAddImage = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnDelete = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnEditImage = new System.Windows.Forms.ToolStripButton();
|
||||
this.separator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.btnConnect = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnDeleteConnection = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.btnToggleGrid = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnFitMap = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStrip2 = new System.Windows.Forms.ToolStrip();
|
||||
this.btnNew = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnOpen = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnReopen = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnClose = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.btnSave = new System.Windows.Forms.ToolStripButton();
|
||||
this.btnSaveAs = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.toolStripButton1 = new System.Windows.Forms.ToolStripDropDownButton();
|
||||
this.allTurnLeftRightCrossOnToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.tabPage2 = new System.Windows.Forms.TabPage();
|
||||
this.lstMagnetDirection = new System.Windows.Forms.ListBox();
|
||||
this.toolStrip4 = new System.Windows.Forms.ToolStrip();
|
||||
this.btDirDelete = new System.Windows.Forms.ToolStripButton();
|
||||
this.btMakeDirdata = new System.Windows.Forms.ToolStripButton();
|
||||
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
|
||||
this.button1 = new System.Windows.Forms.Button();
|
||||
this.button2 = new System.Windows.Forms.Button();
|
||||
this.button3 = new System.Windows.Forms.Button();
|
||||
this.toolStripButton2 = new System.Windows.Forms.ToolStripButton();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
|
||||
this.splitContainer1.Panel1.SuspendLayout();
|
||||
this.splitContainer1.Panel2.SuspendLayout();
|
||||
this.splitContainer1.SuspendLayout();
|
||||
this.tabControl1.SuspendLayout();
|
||||
this.tabPageNodes.SuspendLayout();
|
||||
this.tabPage1.SuspendLayout();
|
||||
this.toolStrip1.SuspendLayout();
|
||||
this.toolStrip3.SuspendLayout();
|
||||
this.toolStrip2.SuspendLayout();
|
||||
this.tabPage2.SuspendLayout();
|
||||
this.toolStrip4.SuspendLayout();
|
||||
this.tableLayoutPanel1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// 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, 25);
|
||||
this.splitContainer1.Name = "splitContainer1";
|
||||
//
|
||||
// splitContainer1.Panel1
|
||||
//
|
||||
this.splitContainer1.Panel1.Controls.Add(this.tabControl1);
|
||||
this.splitContainer1.Panel1.Controls.Add(this._propertyGrid);
|
||||
this.splitContainer1.Panel1MinSize = 300;
|
||||
//
|
||||
// splitContainer1.Panel2
|
||||
//
|
||||
this.splitContainer1.Panel2.Controls.Add(this.panel1);
|
||||
this.splitContainer1.Panel2.Controls.Add(this.toolStrip3);
|
||||
this.splitContainer1.Size = new System.Drawing.Size(1200, 726);
|
||||
this.splitContainer1.SplitterDistance = 300;
|
||||
this.splitContainer1.TabIndex = 2;
|
||||
//
|
||||
// tabControl1
|
||||
//
|
||||
this.tabControl1.Controls.Add(this.tabPageNodes);
|
||||
this.tabControl1.Controls.Add(this.tabPage1);
|
||||
this.tabControl1.Controls.Add(this.tabPage2);
|
||||
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, 335);
|
||||
this.tabControl1.TabIndex = 0;
|
||||
//
|
||||
// tabPageNodes
|
||||
//
|
||||
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, 309);
|
||||
this.tabPageNodes.TabIndex = 0;
|
||||
this.tabPageNodes.Text = "노드 관리";
|
||||
this.tabPageNodes.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// listBoxNodes
|
||||
//
|
||||
this.listBoxNodes.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.listBoxNodes.FormattingEnabled = true;
|
||||
this.listBoxNodes.ItemHeight = 12;
|
||||
this.listBoxNodes.Location = new System.Drawing.Point(3, 3);
|
||||
this.listBoxNodes.Name = "listBoxNodes";
|
||||
this.listBoxNodes.Size = new System.Drawing.Size(286, 303);
|
||||
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(57, 12);
|
||||
this.label1.TabIndex = 0;
|
||||
this.label1.Text = "노드 목록";
|
||||
//
|
||||
// tabPage1
|
||||
//
|
||||
this.tabPage1.Controls.Add(this.lstNodeConnection);
|
||||
this.tabPage1.Controls.Add(this.toolStrip1);
|
||||
this.tabPage1.Location = new System.Drawing.Point(4, 22);
|
||||
this.tabPage1.Name = "tabPage1";
|
||||
this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tabPage1.Size = new System.Drawing.Size(292, 309);
|
||||
this.tabPage1.TabIndex = 1;
|
||||
this.tabPage1.Text = "연결 관리";
|
||||
this.tabPage1.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// lstNodeConnection
|
||||
//
|
||||
this.lstNodeConnection.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.lstNodeConnection.FormattingEnabled = true;
|
||||
this.lstNodeConnection.ItemHeight = 12;
|
||||
this.lstNodeConnection.Location = new System.Drawing.Point(3, 3);
|
||||
this.lstNodeConnection.Name = "lstNodeConnection";
|
||||
this.lstNodeConnection.Size = new System.Drawing.Size(286, 278);
|
||||
this.lstNodeConnection.TabIndex = 2;
|
||||
//
|
||||
// toolStrip1
|
||||
//
|
||||
this.toolStrip1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.toolStrip1.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
||||
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.btNodeRemove});
|
||||
this.toolStrip1.Location = new System.Drawing.Point(3, 281);
|
||||
this.toolStrip1.Name = "toolStrip1";
|
||||
this.toolStrip1.Size = new System.Drawing.Size(286, 25);
|
||||
this.toolStrip1.TabIndex = 3;
|
||||
this.toolStrip1.Text = "toolStrip1";
|
||||
//
|
||||
// btNodeRemove
|
||||
//
|
||||
this.btNodeRemove.Image = ((System.Drawing.Image)(resources.GetObject("btNodeRemove.Image")));
|
||||
this.btNodeRemove.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btNodeRemove.Name = "btNodeRemove";
|
||||
this.btNodeRemove.Size = new System.Drawing.Size(70, 22);
|
||||
this.btNodeRemove.Text = "Remove";
|
||||
this.btNodeRemove.Click += new System.EventHandler(this.btNodeRemove_Click);
|
||||
//
|
||||
// _propertyGrid
|
||||
//
|
||||
this._propertyGrid.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this._propertyGrid.Location = new System.Drawing.Point(0, 335);
|
||||
this._propertyGrid.Name = "_propertyGrid";
|
||||
this._propertyGrid.Size = new System.Drawing.Size(300, 391);
|
||||
this._propertyGrid.TabIndex = 6;
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.panel1.Location = new System.Drawing.Point(0, 25);
|
||||
this.panel1.Name = "panel1";
|
||||
this.panel1.Size = new System.Drawing.Size(896, 701);
|
||||
this.panel1.TabIndex = 1;
|
||||
//
|
||||
// toolStrip3
|
||||
//
|
||||
this.toolStrip3.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.btnSelect,
|
||||
this.btnMove,
|
||||
this.btnAddNode,
|
||||
this.btnDelete,
|
||||
this.btnEditImage,
|
||||
this.separator1,
|
||||
this.btnConnect,
|
||||
this.btnDeleteConnection,
|
||||
this.toolStripSeparator1,
|
||||
this.btnToggleGrid,
|
||||
this.btnFitMap});
|
||||
this.toolStrip3.Location = new System.Drawing.Point(0, 0);
|
||||
this.toolStrip3.Name = "toolStrip3";
|
||||
this.toolStrip3.Size = new System.Drawing.Size(896, 25);
|
||||
this.toolStrip3.TabIndex = 0;
|
||||
this.toolStrip3.Text = "toolStrip3";
|
||||
//
|
||||
// btnSelect
|
||||
//
|
||||
this.btnSelect.Image = ((System.Drawing.Image)(resources.GetObject("btnSelect.Image")));
|
||||
this.btnSelect.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btnSelect.Name = "btnSelect";
|
||||
this.btnSelect.Size = new System.Drawing.Size(70, 22);
|
||||
this.btnSelect.Text = "선택 (S)";
|
||||
this.btnSelect.ToolTipText = "선택 모드 (S)";
|
||||
//
|
||||
// btnMove
|
||||
//
|
||||
this.btnMove.Image = ((System.Drawing.Image)(resources.GetObject("btnMove.Image")));
|
||||
this.btnMove.Name = "btnMove";
|
||||
this.btnMove.Size = new System.Drawing.Size(74, 22);
|
||||
this.btnMove.Text = "이동 (M)";
|
||||
//
|
||||
// btnAddNode
|
||||
//
|
||||
this.btnAddNode.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.btnAddLabel,
|
||||
this.btnAddImage});
|
||||
this.btnAddNode.Image = ((System.Drawing.Image)(resources.GetObject("btnAddNode.Image")));
|
||||
this.btnAddNode.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btnAddNode.Name = "btnAddNode";
|
||||
this.btnAddNode.Size = new System.Drawing.Size(111, 22);
|
||||
this.btnAddNode.Text = "노드 추가 (A)";
|
||||
this.btnAddNode.ToolTipText = "노드 추가 (A)";
|
||||
//
|
||||
// btnAddLabel
|
||||
//
|
||||
this.btnAddLabel.Image = ((System.Drawing.Image)(resources.GetObject("btnAddLabel.Image")));
|
||||
this.btnAddLabel.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btnAddLabel.Name = "btnAddLabel";
|
||||
this.btnAddLabel.Size = new System.Drawing.Size(97, 20);
|
||||
this.btnAddLabel.Text = "라벨 추가 (L)";
|
||||
this.btnAddLabel.ToolTipText = "라벨 추가 (L)";
|
||||
//
|
||||
// btnAddImage
|
||||
//
|
||||
this.btnAddImage.Image = ((System.Drawing.Image)(resources.GetObject("btnAddImage.Image")));
|
||||
this.btnAddImage.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btnAddImage.Name = "btnAddImage";
|
||||
this.btnAddImage.Size = new System.Drawing.Size(106, 20);
|
||||
this.btnAddImage.Text = "이미지 추가 (I)";
|
||||
this.btnAddImage.ToolTipText = "이미지 추가 (I)";
|
||||
//
|
||||
// btnDelete
|
||||
//
|
||||
this.btnDelete.Image = ((System.Drawing.Image)(resources.GetObject("btnDelete.Image")));
|
||||
this.btnDelete.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btnDelete.Name = "btnDelete";
|
||||
this.btnDelete.Size = new System.Drawing.Size(96, 22);
|
||||
this.btnDelete.Text = "노드삭제 (D)";
|
||||
this.btnDelete.ToolTipText = "삭제 모드 (D)";
|
||||
//
|
||||
// btnEditImage
|
||||
//
|
||||
this.btnEditImage.Enabled = false;
|
||||
this.btnEditImage.Image = ((System.Drawing.Image)(resources.GetObject("btnEditImage.Image")));
|
||||
this.btnEditImage.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btnEditImage.Name = "btnEditImage";
|
||||
this.btnEditImage.Size = new System.Drawing.Size(91, 22);
|
||||
this.btnEditImage.Text = "이미지 편집";
|
||||
this.btnEditImage.ToolTipText = "이미지 편집";
|
||||
this.btnEditImage.Click += new System.EventHandler(this.BtnToolbarEditImage_Click);
|
||||
//
|
||||
// separator1
|
||||
//
|
||||
this.separator1.Name = "separator1";
|
||||
this.separator1.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// btnConnect
|
||||
//
|
||||
this.btnConnect.Image = ((System.Drawing.Image)(resources.GetObject("btnConnect.Image")));
|
||||
this.btnConnect.Name = "btnConnect";
|
||||
this.btnConnect.Size = new System.Drawing.Size(95, 22);
|
||||
this.btnConnect.Text = "노드연결 (C)";
|
||||
//
|
||||
// btnDeleteConnection
|
||||
//
|
||||
this.btnDeleteConnection.Image = ((System.Drawing.Image)(resources.GetObject("btnDeleteConnection.Image")));
|
||||
this.btnDeleteConnection.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btnDeleteConnection.Name = "btnDeleteConnection";
|
||||
this.btnDeleteConnection.Size = new System.Drawing.Size(94, 22);
|
||||
this.btnDeleteConnection.Text = "연결삭제 (X)";
|
||||
this.btnDeleteConnection.ToolTipText = "연결 삭제 (X)";
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
this.toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
this.toolStripSeparator1.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// btnToggleGrid
|
||||
//
|
||||
this.btnToggleGrid.Image = ((System.Drawing.Image)(resources.GetObject("btnToggleGrid.Image")));
|
||||
this.btnToggleGrid.Name = "btnToggleGrid";
|
||||
this.btnToggleGrid.Size = new System.Drawing.Size(63, 22);
|
||||
this.btnToggleGrid.Text = "그리드";
|
||||
//
|
||||
// btnFitMap
|
||||
//
|
||||
this.btnFitMap.Image = ((System.Drawing.Image)(resources.GetObject("btnFitMap.Image")));
|
||||
this.btnFitMap.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btnFitMap.Name = "btnFitMap";
|
||||
this.btnFitMap.Size = new System.Drawing.Size(67, 22);
|
||||
this.btnFitMap.Text = "맵 맞춤";
|
||||
this.btnFitMap.ToolTipText = "맵 전체 보기";
|
||||
//
|
||||
// toolStrip2
|
||||
//
|
||||
this.toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.btnNew,
|
||||
this.btnOpen,
|
||||
this.btnReopen,
|
||||
this.btnClose,
|
||||
this.toolStripSeparator3,
|
||||
this.btnSave,
|
||||
this.btnSaveAs,
|
||||
this.toolStripSeparator2,
|
||||
this.toolStripButton1});
|
||||
this.toolStrip2.Location = new System.Drawing.Point(0, 0);
|
||||
this.toolStrip2.Name = "toolStrip2";
|
||||
this.toolStrip2.Size = new System.Drawing.Size(1200, 25);
|
||||
this.toolStrip2.TabIndex = 0;
|
||||
this.toolStrip2.Text = "toolStrip2";
|
||||
//
|
||||
// btnNew
|
||||
//
|
||||
this.btnNew.Image = ((System.Drawing.Image)(resources.GetObject("btnNew.Image")));
|
||||
this.btnNew.Name = "btnNew";
|
||||
this.btnNew.Size = new System.Drawing.Size(104, 22);
|
||||
this.btnNew.Text = "새로만들기(&N)";
|
||||
this.btnNew.ToolTipText = "새로 만들기 (Ctrl+N)";
|
||||
this.btnNew.Click += new System.EventHandler(this.btnNew_Click);
|
||||
//
|
||||
// btnOpen
|
||||
//
|
||||
this.btnOpen.Image = ((System.Drawing.Image)(resources.GetObject("btnOpen.Image")));
|
||||
this.btnOpen.Name = "btnOpen";
|
||||
this.btnOpen.Size = new System.Drawing.Size(68, 22);
|
||||
this.btnOpen.Text = "열기(&O)";
|
||||
this.btnOpen.ToolTipText = "열기 (Ctrl+O)";
|
||||
this.btnOpen.Click += new System.EventHandler(this.btnOpen_Click);
|
||||
//
|
||||
// btnReopen
|
||||
//
|
||||
this.btnReopen.Image = ((System.Drawing.Image)(resources.GetObject("btnReopen.Image")));
|
||||
this.btnReopen.Name = "btnReopen";
|
||||
this.btnReopen.Size = new System.Drawing.Size(90, 22);
|
||||
this.btnReopen.Text = "다시열기(&R)";
|
||||
this.btnReopen.ToolTipText = "현재 파일 다시 열기";
|
||||
this.btnReopen.Click += new System.EventHandler(this.btnReopen_Click);
|
||||
//
|
||||
// btnClose
|
||||
//
|
||||
this.btnClose.Image = ((System.Drawing.Image)(resources.GetObject("btnClose.Image")));
|
||||
this.btnClose.Name = "btnClose";
|
||||
this.btnClose.Size = new System.Drawing.Size(75, 22);
|
||||
this.btnClose.Text = "파일닫기";
|
||||
this.btnClose.ToolTipText = "닫기";
|
||||
this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
this.toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
this.toolStripSeparator3.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// btnSave
|
||||
//
|
||||
this.btnSave.Image = ((System.Drawing.Image)(resources.GetObject("btnSave.Image")));
|
||||
this.btnSave.Name = "btnSave";
|
||||
this.btnSave.Size = new System.Drawing.Size(66, 22);
|
||||
this.btnSave.Text = "저장(&S)";
|
||||
this.btnSave.ToolTipText = "저장 (Ctrl+S)";
|
||||
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
|
||||
//
|
||||
// btnSaveAs
|
||||
//
|
||||
this.btnSaveAs.Image = ((System.Drawing.Image)(resources.GetObject("btnSaveAs.Image")));
|
||||
this.btnSaveAs.Name = "btnSaveAs";
|
||||
this.btnSaveAs.Size = new System.Drawing.Size(123, 22);
|
||||
this.btnSaveAs.Text = "다른이름으로저장";
|
||||
this.btnSaveAs.ToolTipText = "다른 이름으로 저장";
|
||||
this.btnSaveAs.Click += new System.EventHandler(this.btnSaveAs_Click);
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
this.toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
this.toolStripSeparator2.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// toolStripButton1
|
||||
//
|
||||
this.toolStripButton1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.allTurnLeftRightCrossOnToolStripMenuItem});
|
||||
this.toolStripButton1.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButton1.Image")));
|
||||
this.toolStripButton1.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.toolStripButton1.Name = "toolStripButton1";
|
||||
this.toolStripButton1.Size = new System.Drawing.Size(68, 22);
|
||||
this.toolStripButton1.Text = "Debig";
|
||||
//
|
||||
// allTurnLeftRightCrossOnToolStripMenuItem
|
||||
//
|
||||
this.allTurnLeftRightCrossOnToolStripMenuItem.Name = "allTurnLeftRightCrossOnToolStripMenuItem";
|
||||
this.allTurnLeftRightCrossOnToolStripMenuItem.Size = new System.Drawing.Size(223, 22);
|
||||
this.allTurnLeftRightCrossOnToolStripMenuItem.Text = "All TurnLeft/Right/Cross On";
|
||||
this.allTurnLeftRightCrossOnToolStripMenuItem.Click += new System.EventHandler(this.allTurnLeftRightCrossOnToolStripMenuItem_Click);
|
||||
//
|
||||
// tabPage2
|
||||
//
|
||||
this.tabPage2.Controls.Add(this.lstMagnetDirection);
|
||||
this.tabPage2.Controls.Add(this.tableLayoutPanel1);
|
||||
this.tabPage2.Controls.Add(this.toolStrip4);
|
||||
this.tabPage2.Location = new System.Drawing.Point(4, 22);
|
||||
this.tabPage2.Name = "tabPage2";
|
||||
this.tabPage2.Size = new System.Drawing.Size(292, 309);
|
||||
this.tabPage2.TabIndex = 2;
|
||||
this.tabPage2.Text = "방향 관리";
|
||||
this.tabPage2.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// lstMagnetDirection
|
||||
//
|
||||
this.lstMagnetDirection.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.lstMagnetDirection.FormattingEnabled = true;
|
||||
this.lstMagnetDirection.ItemHeight = 12;
|
||||
this.lstMagnetDirection.Location = new System.Drawing.Point(0, 25);
|
||||
this.lstMagnetDirection.Name = "lstMagnetDirection";
|
||||
this.lstMagnetDirection.Size = new System.Drawing.Size(292, 246);
|
||||
this.lstMagnetDirection.TabIndex = 3;
|
||||
//
|
||||
// toolStrip4
|
||||
//
|
||||
this.toolStrip4.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.btDirDelete,
|
||||
this.btMakeDirdata,
|
||||
this.toolStripButton2});
|
||||
this.toolStrip4.Location = new System.Drawing.Point(0, 0);
|
||||
this.toolStrip4.Name = "toolStrip4";
|
||||
this.toolStrip4.Size = new System.Drawing.Size(292, 25);
|
||||
this.toolStrip4.TabIndex = 5;
|
||||
this.toolStrip4.Text = "toolStrip4";
|
||||
//
|
||||
// btDirDelete
|
||||
//
|
||||
this.btDirDelete.Image = ((System.Drawing.Image)(resources.GetObject("btDirDelete.Image")));
|
||||
this.btDirDelete.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btDirDelete.Name = "btDirDelete";
|
||||
this.btDirDelete.Size = new System.Drawing.Size(61, 22);
|
||||
this.btDirDelete.Text = "Delete";
|
||||
this.btDirDelete.Click += new System.EventHandler(this.btDirDelete_Click);
|
||||
//
|
||||
// btMakeDirdata
|
||||
//
|
||||
this.btMakeDirdata.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
|
||||
this.btMakeDirdata.Image = ((System.Drawing.Image)(resources.GetObject("btMakeDirdata.Image")));
|
||||
this.btMakeDirdata.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btMakeDirdata.Name = "btMakeDirdata";
|
||||
this.btMakeDirdata.Size = new System.Drawing.Size(69, 22);
|
||||
this.btMakeDirdata.Text = "Remake";
|
||||
this.btMakeDirdata.Click += new System.EventHandler(this.toolStripButton3_Click);
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
this.tableLayoutPanel1.ColumnCount = 3;
|
||||
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F));
|
||||
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F));
|
||||
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F));
|
||||
this.tableLayoutPanel1.Controls.Add(this.button1, 0, 0);
|
||||
this.tableLayoutPanel1.Controls.Add(this.button2, 1, 0);
|
||||
this.tableLayoutPanel1.Controls.Add(this.button3, 2, 0);
|
||||
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 271);
|
||||
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
this.tableLayoutPanel1.RowCount = 1;
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F));
|
||||
this.tableLayoutPanel1.Size = new System.Drawing.Size(292, 38);
|
||||
this.tableLayoutPanel1.TabIndex = 6;
|
||||
//
|
||||
// button1
|
||||
//
|
||||
this.button1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.button1.Location = new System.Drawing.Point(3, 3);
|
||||
this.button1.Name = "button1";
|
||||
this.button1.Size = new System.Drawing.Size(91, 32);
|
||||
this.button1.TabIndex = 0;
|
||||
this.button1.Text = "Left";
|
||||
this.button1.UseVisualStyleBackColor = true;
|
||||
this.button1.Click += new System.EventHandler(this.button1_Click);
|
||||
//
|
||||
// button2
|
||||
//
|
||||
this.button2.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.button2.Location = new System.Drawing.Point(100, 3);
|
||||
this.button2.Name = "button2";
|
||||
this.button2.Size = new System.Drawing.Size(91, 32);
|
||||
this.button2.TabIndex = 0;
|
||||
this.button2.Text = "Straight";
|
||||
this.button2.UseVisualStyleBackColor = true;
|
||||
this.button2.Click += new System.EventHandler(this.button2_Click);
|
||||
//
|
||||
// button3
|
||||
//
|
||||
this.button3.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.button3.Location = new System.Drawing.Point(197, 3);
|
||||
this.button3.Name = "button3";
|
||||
this.button3.Size = new System.Drawing.Size(92, 32);
|
||||
this.button3.TabIndex = 0;
|
||||
this.button3.Text = "Right";
|
||||
this.button3.UseVisualStyleBackColor = true;
|
||||
this.button3.Click += new System.EventHandler(this.button3_Click);
|
||||
//
|
||||
// toolStripButton2
|
||||
//
|
||||
this.toolStripButton2.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
|
||||
this.toolStripButton2.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButton2.Image")));
|
||||
this.toolStripButton2.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.toolStripButton2.Name = "toolStripButton2";
|
||||
this.toolStripButton2.Size = new System.Drawing.Size(54, 22);
|
||||
this.toolStripButton2.Text = "Clear";
|
||||
this.toolStripButton2.Click += new System.EventHandler(this.toolStripButton2_Click);
|
||||
//
|
||||
// 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.toolStrip2);
|
||||
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.statusStrip1.ResumeLayout(false);
|
||||
this.statusStrip1.PerformLayout();
|
||||
this.splitContainer1.Panel1.ResumeLayout(false);
|
||||
this.splitContainer1.Panel2.ResumeLayout(false);
|
||||
this.splitContainer1.Panel2.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
|
||||
this.splitContainer1.ResumeLayout(false);
|
||||
this.tabControl1.ResumeLayout(false);
|
||||
this.tabPageNodes.ResumeLayout(false);
|
||||
this.tabPageNodes.PerformLayout();
|
||||
this.tabPage1.ResumeLayout(false);
|
||||
this.tabPage1.PerformLayout();
|
||||
this.toolStrip1.ResumeLayout(false);
|
||||
this.toolStrip1.PerformLayout();
|
||||
this.toolStrip3.ResumeLayout(false);
|
||||
this.toolStrip3.PerformLayout();
|
||||
this.toolStrip2.ResumeLayout(false);
|
||||
this.toolStrip2.PerformLayout();
|
||||
this.tabPage2.ResumeLayout(false);
|
||||
this.tabPage2.PerformLayout();
|
||||
this.toolStrip4.ResumeLayout(false);
|
||||
this.toolStrip4.PerformLayout();
|
||||
this.tableLayoutPanel1.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
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.ListBox listBoxNodes;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.PropertyGrid _propertyGrid;
|
||||
private System.Windows.Forms.TabPage tabPage1;
|
||||
private System.Windows.Forms.ListBox lstNodeConnection;
|
||||
private System.Windows.Forms.ToolStrip toolStrip1;
|
||||
private System.Windows.Forms.ToolStripButton btNodeRemove;
|
||||
private System.Windows.Forms.ToolStrip toolStrip2;
|
||||
private System.Windows.Forms.ToolStripButton btnNew;
|
||||
private System.Windows.Forms.ToolStripButton btnOpen;
|
||||
private System.Windows.Forms.ToolStripButton btnReopen;
|
||||
private System.Windows.Forms.ToolStripButton btnClose;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
||||
private System.Windows.Forms.ToolStripButton btnSave;
|
||||
private System.Windows.Forms.ToolStripButton btnSaveAs;
|
||||
private System.Windows.Forms.ToolStrip toolStrip3;
|
||||
private System.Windows.Forms.ToolStripButton btnSelect;
|
||||
private System.Windows.Forms.ToolStripButton btnMove;
|
||||
private System.Windows.Forms.ToolStripButton btnEditImage;
|
||||
private System.Windows.Forms.ToolStripButton btnConnect;
|
||||
private System.Windows.Forms.ToolStripButton btnDelete;
|
||||
private System.Windows.Forms.ToolStripButton btnDeleteConnection;
|
||||
private System.Windows.Forms.ToolStripSeparator separator1;
|
||||
private System.Windows.Forms.ToolStripButton btnToggleGrid;
|
||||
private System.Windows.Forms.ToolStripButton btnFitMap;
|
||||
private System.Windows.Forms.ToolStripSplitButton btnAddNode;
|
||||
private System.Windows.Forms.ToolStripButton btnAddLabel;
|
||||
private System.Windows.Forms.ToolStripButton btnAddImage;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
||||
private System.Windows.Forms.ToolStripDropDownButton toolStripButton1;
|
||||
private System.Windows.Forms.ToolStripMenuItem allTurnLeftRightCrossOnToolStripMenuItem;
|
||||
private System.Windows.Forms.TabPage tabPage2;
|
||||
private System.Windows.Forms.ListBox lstMagnetDirection;
|
||||
private System.Windows.Forms.ToolStrip toolStrip4;
|
||||
private System.Windows.Forms.ToolStripButton btDirDelete;
|
||||
private System.Windows.Forms.ToolStripButton btMakeDirdata;
|
||||
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
|
||||
private System.Windows.Forms.Button button1;
|
||||
private System.Windows.Forms.Button button2;
|
||||
private System.Windows.Forms.Button button3;
|
||||
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
||||
}
|
||||
}
|
||||
1677
AGVLogic/AGVMapEditor/Forms/MainForm.cs
Normal file
1677
AGVLogic/AGVMapEditor/Forms/MainForm.cs
Normal file
File diff suppressed because it is too large
Load Diff
451
AGVLogic/AGVMapEditor/Forms/MainForm.resx
Normal file
451
AGVLogic/AGVMapEditor/Forms/MainForm.resx
Normal file
@@ -0,0 +1,451 @@
|
||||
<?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 through the TypeConverter architecture.
|
||||
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" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<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" type="xsd:string" use="required" />
|
||||
</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="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>132, 17</value>
|
||||
</metadata>
|
||||
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>249, 17</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="btNodeRemove.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
|
||||
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
|
||||
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
|
||||
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
|
||||
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
|
||||
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
|
||||
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
|
||||
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
|
||||
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
|
||||
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolStrip4.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 56</value>
|
||||
</metadata>
|
||||
<data name="btDirDelete.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
|
||||
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
|
||||
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
|
||||
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
|
||||
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
|
||||
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
|
||||
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
|
||||
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
|
||||
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
|
||||
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="btMakeDirdata.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
|
||||
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
|
||||
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
|
||||
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
|
||||
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
|
||||
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
|
||||
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
|
||||
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
|
||||
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
|
||||
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolStripButton2.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
|
||||
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
|
||||
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
|
||||
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
|
||||
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
|
||||
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
|
||||
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
|
||||
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
|
||||
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
|
||||
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolStrip3.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>462, 17</value>
|
||||
</metadata>
|
||||
<data name="btnSelect.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHrSURBVDhPldBLaBNBHMfx/0kUVBBJ0lxWPIhihBJKyAqS
|
||||
pHkQIS+9hXg3RhQviicrITnmJqFnQQ8RqiamRqkhj6VCQtuIQaVQc5di3d3s61Dy0w002KnU9nP67+x8
|
||||
h2GIDmD0kT+mLk/fZNf3pQkznCrM3DFnZflSRG05euast7izcpM72GGqMP1ZFRw1tXm+qq9dg9LiHgwb
|
||||
dnFYP51i9/6T0r4wp39Kwfh2F8bGI2irEYjvTmo/Gpbj7N4JpXNxShUcdbV1DvpaHMb3HNrP4uiVb2Cj
|
||||
cQtadxbSh6OQ3tM82+6iNLk5rXcd7ecJGIaB0WiE1dcp6F9v41eNvmxV6QzbTMjtKYtct9Wi0Si63S50
|
||||
XUe/30fjaQTG+n1IVRpKb4lnuzFtyc4Nl06VE4kE0uk0CoUCSqUSqvOzMNYfYnORtqVFWhEr9JhtJ+Lx
|
||||
+DjmeR5+vx+xWAzqSgRy3Q65dgJbFeLYZmIndrvd8Pl8sFqt5pWfbL6hbalCl6Uy9cSXlGG7sWQyiXw+
|
||||
P469Xi8sFgvMdblCV6RXVDNnvKAjPxfoKttSOBxGLpfbE+8QFyj09/cugUAA2WwWLpcLHo9nT7yvTCaD
|
||||
wWAAp9OJUCh0uNhkHtDpdFAsFscPxv7/r2AweM+8ts1mO3z8x29KrQsZMgRtMAAAAABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnMove.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
|
||||
dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAH0SURBVDhPlZNtT1JhGMfPtzjfJgUliBESAgcOyOHh
|
||||
CKZA4pE2a7IWriaBWeKaoyeNJytO2TkSzpg0JdL1prk5PwRv+AT/drMdK4618eL34r52/f7XvV33Tc23
|
||||
p0AQvoUx1wohfjgJANRlhGoe8LIbgU8u+D444a06QBF5+3wT5bOX/5UJvOymwzKH3Pcs1o9X4HlrA0Um
|
||||
F89eIP/zKR6eJBFrBsV+sSdLLB3YcXWTjdt40ExitZUGW7GCmjsKIdGaQeZHCoXTPJbadzHzxacK8e+4
|
||||
OouNBNbaGQi1KFxlK5iiBdTs4SRufeXp6EGwu3x8D/ePFnBzj6v3B3AiIyXqMQi7ESL3Bti3zL8bIg0/
|
||||
Pb3v60zteaV+WcG9PV5XZAVV06CoCoOiKgyKqjAofx04kREn3ttVG1CwPDdK5g1Dx/RMT18EBCW29/qI
|
||||
LNQiiMvTZE2qTYzljXW+zCFcCcC4rusa1kZp/ZMRUP6PTnAiAyKnm0uY342BKVg6/QHXNwyivziBBSmB
|
||||
YIGDKaeH7rEGFFd1IN1M4c5nAYIcIXLXvmW+uOKfXMvpRO9rFnzJi9lqBKPZYVCedzYsH6SQ2l+Eu2SD
|
||||
bfNyWeHqqhbxahSCGIM2MwSKrYzDWboBx5sxIsP6yvTPH0lk3YoGI9lhaB8NQZO+gl8Dj7SN1tpAvgAA
|
||||
AABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnAddLabel.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHJSURBVDhPnZJfa9pgFMb7JXa7sW9RkH2y3awro5dbS7Et
|
||||
u2vHRsRcBEw0QkTinyhB8e9MonOZ6NRZaME12YzyjPPKm+LcKtsDIbznPc/vOSRnr91uo9VqodFooF6v
|
||||
o1aroVqtolKp0PvR3i41m02sVqutZzgcMoiu6w9DKJkMg8GAJbuuG0KoZprmwxAamZopjc7lcrkyn88R
|
||||
BAGrE9AwjL9DKJU35vP5n4VCYeD7Pr59vUNBnbK7fr+PXC73Zwgl85GXyyXIfD31cXbo4Pi5jXxqDen1
|
||||
eshms1AUZRNimqY3m81Y02KxYMnRlw4uX/fx/tTF+WE3hDiOA03TNiGlUumZYRjedLpusuo3LPnq+DOE
|
||||
8y9bENu2oarqJqRYLEY0TfMmkwlr6rZuET2w8e7kHhI9cKAr6/tOpwNZliEIwj1E1/VIOp32xuPxFuRD
|
||||
1MXbox4ujj4iCJZsimQyiXg8/iQEkDKZTCSVSnmj0SiEnL6wcPGqi6uTLr7P/dAsCMLTDTOXqqoMQpvI
|
||||
PlzzBpdvPuGHH+w2c8myHEkkEh5tIv/FZFYUZbeZS5KkfUmSPFowy7L+zcwliuK+KIref5m5CBKLxR7/
|
||||
Xuf6BYuvFpozmyYBAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnAddImage.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAG7SURBVDhPnZLditpQFIXnJXrb0rcYkD5YL4pMYS7bGUrp
|
||||
O0TMRUp+hWRs/IkSFDUGfxARR6viRRmkJ+jJYZVzhpPB2o60C0LI3nt9a0P2RRzH6Pf76PV66Ha76HQ6
|
||||
aLfbaLVa/P3i4pyiKAJj7ORZLBYC4vv+8xCezA3z+Vwkz2azDMJrYRg+D+Er82Gexr+bzWZrt9uBUirq
|
||||
HBgEwd8hPFUOVqvVfa1WmydJAvrjO2hsi950OkWlUvkzhCfLldM0BTezhw1S6wrp17egsSV6k8kE5XIZ
|
||||
pmkeQ8IwJNvtVgwdDgeRnJp5MO8D2LdPYNZVBhmPx3Bd9xjSaDTeBEFANpvNI+S+I5LZ3Q1Y5csJZDQa
|
||||
wXGcY0i9Xs+5rkvW6/UjZBmBGnmwu9sniJkHjXTRHwwGMAwDiqI8QXzfz5VKJbJarU4h/mcw5xo/jWuw
|
||||
lIotLMtCsVh8lQG4PM/L2bZNlstlBjno78Ds90jcG+zJLjMrivL6yCzlOI6A8EsUkEUP1PsItk/Om6UM
|
||||
w8jpuk74JcpfzM2maZ43S2madqlpGuEHNhwO/80sparqpaqq5L/MUhxSKBRe/l6X+gWA2x2MFEPZrwAA
|
||||
AABJRU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnAddNode.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHdSURBVDhPnZLra9NgFMb3T/hV8b8YFP+efZE5RL+qQxEV
|
||||
dN6+OXfBTynNZtYmbeKSxfSS1pDSNq29UEpJW9tSULT6lvXCI+8LicTpij4QQs45z+85kLNSqVRQLpdR
|
||||
KpVQLBZRKBSQz+dh2zZ9X1hZJsdxsFgszjzdbpdBDMM4H0KTqcF1XZbcbrd9CK1ZlnU+hK5Mh2ka/c7l
|
||||
cvZ4PMZsNmN1CjRN8+8QmuoNplKp03Q67U4mEwy/dPDeOWS9VquFZDL5ZwhN9laez+eg5tHXT3ga3cDD
|
||||
gzXozgHrNZtN6LoOSZKCEMuyyGg0YkPT6RSDzx1sRa/h1btb2NXu4ll0w4c0Gg2oqhqEZLPZK6ZpkuFw
|
||||
yIY+uh/w6HANr4/v4I3+AHvavQCkXq9DluUgJJPJhFRVJYPBgA3VOnk8Edaxc7zpQ7aO1qEWw6xfrVYh
|
||||
iiI4jvsFMQwjpCgK6ff7ZyD7J/fxUrqBF0c3MZtP2RbxeByRSOSSD6DSNC2USCRIr9fzIY/fXsXz2HVs
|
||||
K5v4/uObb+Y47nLA7EmWZQahl8jWdW1sK7cxOSXLzZ5EUQzFYjFCL9H7xdQsSdJysydBEFYFQSD0wGq1
|
||||
2r+ZPfE8v8rzPPkvsycKCYfDF3+ve/oJ+zEPR++RdtEAAAAASUVORK5CYII=
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnDelete.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVDhP7Y+/TxphGMex4B/QqXQgxqGz/0CHRmw6aRhM
|
||||
dGjToQ4OujiAdqidOrRhccIEXyYT26EbFmLV4MEdx91xHBCahiLy+zwUFn80oeFr3jdCyKm1f0C/yWd5
|
||||
83y+7/NYLP/DEggEnISQOCEE/0jB7/dP9QsIITlFEVCt5hm1GuUXo14vMBqNQ+h6Ebp+hExGoSWVwQKj
|
||||
WMyB56OIRCIMjuMQjUYRi8XA8zzi8ThEUYSiKKzE5/Ndmgp+QBQFJpqlRCIBWZaZrGkpGEblZkGp9BOy
|
||||
LEEQBCZSSZIkJiWTSaRSKWiahmw2g2azdrOgXM5DVRUm0d+opKoqE9PpNOT1dzh49QTfncPYn3Vgc3G6
|
||||
05Oxt7eDk5M6Tk8pDbRaOtrt42sM5D9/Qtr9FJdBL7q5EM63liAtjOGL5yVYwX0EXXZcBL3A2iTgeQh8
|
||||
GEXz4zNsux6hf8bfsj1u63aVrxhMe9UO+m6evTXhCWvlbOMNsGrHb7cFLbcFpXkrQs+tVfPsreFmHr8X
|
||||
X4906p5RGCvDKMwNYfeF7c+3iQdvzbN3hptxLIed1iJdm27Uk68A8qiqJzQDmt8AAAAASUVORK5CYII=
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnEditImage.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAL0SURBVDhPhdLdT1N3HMdx/oPdL16ZbDfGuJtll4vbfNrM
|
||||
3JxzOGMbQ3SZYzHLdMUyJuL0CB3lQCFAC5xVfGCtyqQIOGzVItOKBwpFfBgOkedA7WlP258E8b202Tol
|
||||
e/gk36vf9/f6Jb/vN0vx9JYqbnVadqua1fn/JbtUzXFOLcv6K3UtN2cmJief6fE48YT4z5rTBTORGNWn
|
||||
b0QyQOrlmB6nqnWM0tYghRd+wnnLhqv/e04Fi6j0W/i8wc7X9V2Y6oeYjQlkd5+WAcqcqpbSqy+MIbV3
|
||||
4Oyx0TSwD0fPZ1TfyKEmsAdL52H21J7ErNxhRvsHIJYQ1LaPU9jWyOlgETWBXZT/ugOLfxvS1Wys/q8w
|
||||
VvxAwfF7TGtJZHfvi4CWEDg6JjjocfKjWoDcbUC6spVDvs18d2kzkjcXg1yM0tTI3ZPZ/Hb+CMH6tQSr
|
||||
VhjSQCQuqL84ieRpw+Y/Rll3DoW+jzD/somD3my+/TmPquP7mfKZ0AZaQZ8gMnAGf8n66TTwWBconVPY
|
||||
2vrJcylILXsp8e5C6txJvvsLrA25zA4WMj/WwtQVC8nQOZ4+vEZf7W6RlZptOCZweqfTSIUnyDdFBo5a
|
||||
30GyrKauYj3hwQIWk9dIPsglfPNL7jTmELR/MhewffBqGpiLCsyu9zE1bWTfqXex579OSNnC4Ik1PA4d
|
||||
YFFcJzFsRDzaTmzITKhq47O+sjWvpT+x1HlLC0cF/uHLmeoONdN1fi9TPaY/LxsQo58SHcznQe17KMpZ
|
||||
PTMFaxpIvgD4LhczdHE/i0k/YiSHJ492oIUOcL9mHeOjI8iu58ZY6VZHw5EoUV0QjT+hz9fI7TYz/e0y
|
||||
44GPSYxsJxzMY7juQ+Ymfmc2vcqBv1fZ0ayW2M/0dqWWIyW7it+G2EPu2rfgM79Md80GvEffXGxQPHrq
|
||||
vPxsr+ZoVuUMsDSHja8szF+vZD5QTuDIak4Yl+kdphWrlvb9aza98ZKQjMsX2o9tWOgsfetqa97K5Ut7
|
||||
ns8f9tyLJQW2uh8AAAAASUVORK5CYII=
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnConnect.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
|
||||
dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHrSURBVDhPY/j//z8DJRhDgFSMIUAqxhAgFcMZdnZ2
|
||||
3FlZWaWtra3v5s2b97+mpuZhTk6OeU5OjkRNTc1dkBhIDqQGpBbFACMjIzYfH5+uiRMn/n/x4sX/u3fv
|
||||
/j948OD/xsbGe9XV1Ydu3rz5AyR25MiR/yA1ILUgPXADDA0N/YqLiz9cvXr1XURExDZvb+8/qamp/ydN
|
||||
mvS/srLyP4gNEgPJrVix4iFILUgPsgu6mpqavk+ePDnd39+fy97e/vPevXv/nzt37v+hQ4f+r1279j9I
|
||||
DCQHUgNSC9IDN0BXVzczPT39/bRp00qCgoJ2Ojo6/nJzc/sfGBgIxiA2SAwkB1IDUgvSAzdAU1NT09fX
|
||||
93pvb+/7VatWvV23bt3/6dOn/y8sLPxfUVEBZvcvbP1fODXmX1qf//+IJse/Lrm6E+EGqKmpMdvb2xt4
|
||||
eXndT0lJ+ZKdnf09Ozv7Z3h4eFx4eLhzZm3cz8bVif+3XJn6/8KzXf/7d2f9D+/X/W+eLdmDEa/YcGy7
|
||||
y/eNlyf833ht0n8Q6NmT+r9/TzrIgO8YirFhn2rD/9uuzAFrhoFNl6aCDMBUjA1b5kh/796V9L99VwJY
|
||||
c/vOBNJcYJ4t2RLco/G/d1cK2GYQDeITHQZQQzrMsyU/gZwNpTtA4gBRO5Y8lpxI5AAAAABJRU5ErkJg
|
||||
gg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnDeleteConnection.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHvSURBVDhP7Y7Ni1JhGMVvXadgWrWzRQyzTEFyFa7U0Wvg
|
||||
R3A3KgS1SLFeSbCPGe0uHFyMIEzgBXOVECi00VbOKGWg4yYiW4i1ujNIEVKkQVPaZJ7hGUhMnP+gAy88
|
||||
nPM7h5fj/msiQRDOMMbux2KxXjabhSRJHcbYJcaYWpIkhTzKiCH2n7JGoznlcDiSqVQK3W4XiqKgVqtR
|
||||
YVeSpHqr1RqS12g0QAyx1JkM6PX6K6FQ6Fu73e55vd4tQRBGfr8fsiwjGo2CbvIoy+fzHWKpMz2QjMfj
|
||||
A1mWA06nc9FgMHyvVqtoNpuo1+soFAogjzJiiKXOZECn090KBAL9dDp9TxTFitlsPrDZbBBF8ejRTR5l
|
||||
xBBLncmAVqu94HK53icSiX4ul/taLBaRyWQQDocRiUSO7vIGw0tx6c+LlQU8sy0ePDGdfjgZ4DiONxqN
|
||||
F+12+57P59sPBoMDxtgvt9t9zePxWB5dNQ3f3jVgUNrE+F0ZP57eweugZvTcoro9PXKstq288rO0Ccgu
|
||||
YO0ssLGML0kTSmZ+d5adq60V1Xj8pohp9WNqkD/LzlXFyn/Yf3wDiKkxXOXQW+XQucmjLPAfZ9m52vGc
|
||||
W391fen3p7VlfI4uQPGdQPWyarRtPflglj1WO57zkYqF36Nv04/+lg8BALcaCRX7gQ0AAAAASUVORK5C
|
||||
YII=
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnToggleGrid.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
|
||||
dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAACLSURBVDhPY/j//z9Dzqzj0Tmzjt+EsttyZh1/SoBd
|
||||
kTPreBOIDeJkkagZxH4EZXuCiIc4FBFit+TMOr6NAYsESWycEkSyW0AENgmi2QPuhTYQgVWCWPaoFyBe
|
||||
KKcgOVeCiFQoB5S2sSnCxa4FWQ7zglfOrOPHsCgCsR9D2TU5s45fgrKLc2Yd7wCxAZ0nqHyAo+5ZAAAA
|
||||
AElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnFitMap.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKYSURBVDhPhZBfSFNRHMfvY0RPEfQSFD2tngqh9WBZmFOX
|
||||
ess2de2Pii7LJAsjHN1ScU4yc0WWzB4m2KZmdMkSV7qZ889tuHvuMlKocJ47aMWI9FwrCC4njrqX66Iv
|
||||
nJfD9/P9fX8/itrQrNek5vssLOgrTYT6LfJbr0mecZsTXHcxy3WeUSd9KSV4zVbhcXmc77WA2Z6zTNht
|
||||
UE27ClVct47hXHow1VUcDzpPWZXcmsjkddjo4KNoH1hCOgClWkGUaiJwJX8OLu8N3qUdk046Hrit3dyE
|
||||
91pYMjkcRSoA0QUAkU0Q0TUgrl4VoHRFECWjEP2xZ6KDBv5WLavkKeAtTcz2lDCRGCoEENWvw1LdBlwD
|
||||
RFQNoHTC36JlfC35CSVPhfpMcthlUAmidDE5NbkCaSRAVEVa+Bo0qkBbrqzkKc5jlMnBiJGAYEm6RMJ4
|
||||
iM6v/YmoMgKlYhLgd2g2B0y7jQnuoY7hxdWc9Raoeg2OISsQUYUAV8p4ER0duZ7F+GyazSvMdOnYmU49
|
||||
IIcSoFS0AVYK4ko5gQFE9LvYz10vmnJ/D9dnhpQ8FXQWqKce0PE3HbSDX/y1W4hJx0llAUr6CETpBPa3
|
||||
03/mBmtxoO4YflabblBmUONtBdagk477b50Eo83ZDNmXvBFbJjPenifPv7ThWNiFF6fu4LFmGj+vUO9U
|
||||
ZlABh1b92q5lfS2aBDnWmD1LHrqZvTzalIc/jbfirx968bcFD/442ogHLUdkb6qQVGIvZ5gnOkpwdNqJ
|
||||
4+978BJ3H4celWNHlurz0Lm0rUp/Sj2tTjeP2U/jheEbeH64AU/eK8KDVQe/P9Hv36b0/lMD1kNmjykN
|
||||
v2rMwf1l6i8eQ9oOpee/Gqg4vL275IDdXZqxJfn3F2EzpMPWLB83AAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolStrip2.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>356, 17</value>
|
||||
</metadata>
|
||||
<data name="btnNew.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO
|
||||
wQAADsEBuJFr7QAAAU1JREFUOE9j+E8GYEAGIIGrzz+AJYrW6IIxCMw6cO1/x/oTYFw2fQsKRjEE2eTp
|
||||
p73/Tz3pgSz0/+nbryi4c/eb/+EFE8FycANAtk475fV/0nH3//1HXP53HrCHu+TA/b9w/OnbL7ABKIbA
|
||||
DJhw1PV/9wGH/y17rP/XbTNHMWD3HQgGGQDDQYnVCANgoGGHxf+qzcbIQnADtt36A8Ybr//BNAAUYCCA
|
||||
HIgwZ6NrXn3lN6YBoJBGBzBnwzTCNMMM8AzNBquDGwASxGcrCC+5CMGg2EAxABS3bz5+wzAAm+Z55yAG
|
||||
OHvHYBqA7mxsmkH40csPuA1Adzqy5qmn/oDxvefv/9u5hqIaAAoDXLbDbIYZALIMxQBQqrp6+wnYEJAk
|
||||
LNmCnIqMQTaD5E+fufhf09AZ1QUgQ2AYFMcgDAppEAb5F4RBtoIwSDPWlEgOAAAYnVWGYLszpwAAAABJ
|
||||
RU5ErkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnOpen.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO
|
||||
wQAADsEBuJFr7QAAAhFJREFUOE/dz71rU1EYBvAO7t38E1xaizQfIh1cBHFyEV2sDiJoyCApFBVBO6QN
|
||||
iIGSqCDSKpSKtlb8QBCHEge1rW1iE733Jmlsmhh6g839vud+nHseSapJXFwFX3g45wzv7z1vT8//Ufwr
|
||||
X6acGna23p3tyhmn8Hpoo/h0YO/GE1/vH3nr611cPLynDQgvghZjHgAGsGZ27y6po/o+ZFc+hKzfqa1e
|
||||
JtWlkTL/fPBkB3gWkBklsKpxGKVJGJvTMMvzYK4E5ulgVAOjyu7bbYBR2xYWgrk2kH8cUD1HgVFKQi8m
|
||||
oH4Zw85yCPWVMKRsFNL6DUiZq2ikR9FIR+ASEfkFv9YB5gKOa25DyiXRWItBTI3AFJfAqPprevOU2z9o
|
||||
rifMBUgHmD3otPb3LMAzAWq0GqnJw6olQSpRmN9GYRQuQOdPg+ochBl/F/AgaDNqgqof4Upv4PyYh719
|
||||
H6RyE+bmNZjFMAxhGPrX49CyR0C1DISpLkC4F7Q8R4Wz8xK2OAOrdhtkKwqz1Jx6Hjp3ClruGNTPQ1DW
|
||||
DsBVP0G42w0k/IQ5EmzxIazvkyDlMZilCIz8OejcCWjZo1Azh6CsDkBe2QdXXUazpwPcGqzZckHWio88
|
||||
vTAFLX8HGh+Hxo1DzV2Hmr0CZT0CJROGkr4IIqYYH/fJbYAf77vEx/pnuYn+OhfbT/6aidap8NG+RBv4
|
||||
p/UTNSLqJCVq/CIAAAAASUVORK5CYII=
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnReopen.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO
|
||||
wQAADsEBuJFr7QAAAUZJREFUOE+lkzFLw0AYhvt3HP0DLqWDdCuECjooBRdBXJwEZ3VRBykUBCfRpSi4
|
||||
uXTSIYNiRSgFBxU6qHSoCMHiyfPJm9zFgKAHT0Mu9z53+fK15P4wSv5gojcYuvbNo3EU37tW587YaF8a
|
||||
q3unAYGEG4JInl7e3HKrFsCc5rlunj+7+spuJpFAiw4HiynN7mwqamxX3OsoMUEg4Ydjs0Ds9+cNBFud
|
||||
yK4SwOg9cbWFtUzA+7Lg4uHTkGgnrqdwf9YbG4UCJiWQhJ0JcwoJjrsfocA/+m8QlmA6WsoEfuH8owoF
|
||||
4eAqsYIXCiicdlPY35mwBOXqXCjwK6+i+UdXuBmPfwrWT2YMhQXfPx8Gmm6qEn0LWMSgMPkuLAoDvZAK
|
||||
+KGrrm/7JuGh2jcPO/OctROT5UzAHwSJ4BsDlQbeF9gVCAeN9J/xBd6kDTqyYu/xAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnClose.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
|
||||
dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHbSURBVDhPpZFbSxtBGIbnVhQULbFUpKVH0yqRZKUx
|
||||
RguJZWMSqk0PBm0oPdHD/8yFB/BvFDOhbMJmd7O785TZmJiDN6UXLwvDPM+373wCEP+TiQOEkNHxVcLR
|
||||
yPH743CDZ4/h5wf4cYz6foT6VkV9eY/69JbgyX18IVo3C/rwrxqYeZRhoJJJwvV1wrU1gmyaoLZP99Fd
|
||||
HH13RDAMH1fh4gJVqRAmEoSrqwSlEur8HL9cwD/cw3uwjH0l6Qt6v2zmI5h2G5pNgnKZoFgEKaHVQp2e
|
||||
0k08pfs6RztChwRR31SK8OAggvuSKBqWku7ODu7iIt6rF1jDAv3C6uvhdd9SaQBGkRJvexs3FsNZWMAt
|
||||
ZW8QfH7X6xuPExQKIwI93ctkcObn6czO4hbSk4LwY4VgZYXANAed9VfDWBbq8hLHMLCnp3FfbvBnWOBr
|
||||
QW0f/3kSdXbWm9po4G1t4W5uRrCyLMJ6nc7ybZx8alTgCSH1fv1qEd/MEZ6cRLDu25mbw9GPW6/jbKzj
|
||||
7Bq0lm7RFOL39RZAOEJIvd/um128+MNBX3tmBntqCnspFk22huARgY4thLTv3cErZ3H3MrhmOurr5A2c
|
||||
XHICnhDoWEJI/cL96K46zV6a4/cnBP+av+eVuIOQwaWDAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnSave.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQAAJXD9Iasxm6MqnSn2lZtjVaRyEpXbYu767TX/2KZztvr/4Gy5KrT/3ut32+gzlFh
|
||||
e+r0/0RNX9/u/9Ln+8Xg//n8/4e36CkxQz9GVkSCvKjL35/N/Je91K7T5bDS4////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQAAAAAACwAAAAAEAAQAAAIuQA/CBxIsKDACRwScggQwIGAhwIICBDYQcEEgwg+bNjw
|
||||
QKCHCQgkQBgpQcKBCg0AEBCoAaRIkhIsVBigUiAHCgwkKNjJU8GAAx0/3NwIAMABCwsaDHCwIGgAChuK
|
||||
HjiQdMDSAQYEPpWKtKqDBA6yfgiAwGhXpUsTJIgg0AGCo0nRfi1QgO0HAQyQNpCrtkAGDAIFbKi69GsC
|
||||
un8FEohqdEFavxkyXAhMoPKDBwYMRIiAAcOFoAZDCwwIADs=
|
||||
</value>
|
||||
</data>
|
||||
<data name="btnSaveAs.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO
|
||||
wQAADsEBuJFr7QAAAyZJREFUOE9tku1PW1Ucx+8r/Tt86wvRxRjjEmPMULeZNAsQXczipoCdDhxZlg0Z
|
||||
m7CisBYfthcDXLrBlJktUbIBGQ+hAn2cq3uga3lqBxQuUCil9OHc9t7bj2mjy3B+k29OTnK+n9/35Byp
|
||||
+NTowffN7iGDxb1hsLgxWNzC0OoWBotHGMwusafF4dt5fPzHF8uuPyf9n3Y32cfXtsR6NCG0aELhX69v
|
||||
5S0IyFvU31yg/MJ06r2q/uf/m5f2Njsi8VRGc81l8SyouOdUHI8zjM4o3PYnUVTonUxT3zPPp+en089A
|
||||
9jY70vFUBm9YwxtW+bMAyTA2qzDoT6GoOl5Zw7OUxdS/wNu1I93bAS1OkQfcC2vcW8xDNO7MZ7EHMwxP
|
||||
pXm8mixcJSFUVmMpiuvH1GcAm8kME7LGxLLOgwIk3yJbaDHkT9HnS3DrYZzNpMI7jXaxDbCn2SliyQyB
|
||||
5RyPlnV8ss5DWaP6oo86q4+atvs0/uzH+J2Xwz94Od5xn2PtD/jE7OaF/b+9Ju1udopoMsPkSo7ASg7/
|
||||
co6JJZ2K1rv0eBPkdcsVLqxPq9MW4WCLB+ldk1OsJzL4C2G94HyTj75xYf0jVjhcfeHuk6BQIa6A5fdF
|
||||
Dp1zIRU3OljbUgqTnwBknQPNbjptMbKajqrl0PUcmp4jq+b3Ok2/Bik5M4r0Vp1dyJuCsl6F0j5BSa9C
|
||||
yU2FitY7tN+OcPqXIF93Bzl1dYavrkxRe2WKk5cnOWmd5IDJhrTzxKgIRwXVYzrWAPzkh7YAHDJ7ON8n
|
||||
03BtgYZr89RfDVHXOUvt5RlOWKc41uHjg9NDSK/X2FKhSDJntOlUjKiUj2gFlzbYudgfIZJIIMejyPEY
|
||||
8maMpXiUaCpB7aVH7Drag7TDOBxajYut0FpaD0XSzP7jfWfG6eiPsZKIYLLto83zJe2eGky2UlaTi9Rd
|
||||
CvLG4etIrxoHvigqHxh+pXIo+nLlIEWVA6KoYkCUnXUy+Ff+FYK4Nr4lkOwikO7CtWEB5mjqCvLm5zfY
|
||||
9qme1o7PBh0fmsYxmic4+v08R1pnqTJPU30uwBGzl11VN3jp427+BmRb26lRD0KdAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolStripButton1.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
|
||||
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
|
||||
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
|
||||
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
|
||||
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
|
||||
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
|
||||
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
|
||||
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
|
||||
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
|
||||
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
||||
162
AGVLogic/AGVMapEditor/Models/EditorSettings.cs
Normal file
162
AGVLogic/AGVMapEditor/Models/EditorSettings.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 맵 에디터의 환경설정을 관리하는 클래스
|
||||
/// </summary>
|
||||
public class EditorSettings
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// 마지막으로 열었던 맵 파일의 경로
|
||||
/// </summary>
|
||||
public string LastMapFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 프로그램 시작시 마지막 맵 파일을 자동으로 로드할지 여부
|
||||
/// </summary>
|
||||
public bool AutoLoadLastMapFile { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 설정이 마지막으로 저장된 시간
|
||||
/// </summary>
|
||||
public DateTime LastSaved { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 맵 파일 저장 디렉토리
|
||||
/// </summary>
|
||||
public string DefaultMapDirectory { get; set; } = string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
|
||||
private static readonly string SettingsFileName = "EditorSettings.json";
|
||||
private static readonly string SettingsDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AGVMapEditor");
|
||||
|
||||
private static readonly string SettingsFilePath = Path.Combine(SettingsDirectory, SettingsFileName);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Instance
|
||||
|
||||
private static EditorSettings _instance;
|
||||
private static readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 싱글톤 인스턴스
|
||||
/// </summary>
|
||||
public static EditorSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = LoadSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에서 로드
|
||||
/// </summary>
|
||||
private static EditorSettings LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsFilePath))
|
||||
{
|
||||
string jsonContent = File.ReadAllText(SettingsFilePath);
|
||||
var settings = JsonConvert.DeserializeObject<EditorSettings>(jsonContent);
|
||||
return settings ?? new EditorSettings();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 설정 로드 실패시 기본 설정 사용
|
||||
System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
return new EditorSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에 저장
|
||||
/// </summary>
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 디렉토리가 없으면 생성
|
||||
if (!Directory.Exists(SettingsDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(SettingsDirectory);
|
||||
}
|
||||
|
||||
LastSaved = DateTime.Now;
|
||||
string jsonContent = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||||
File.WriteAllText(SettingsFilePath, jsonContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 맵 파일 경로 업데이트
|
||||
/// </summary>
|
||||
/// <param name="filePath">맵 파일 경로</param>
|
||||
public void UpdateLastMapFile(string filePath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
||||
{
|
||||
LastMapFilePath = filePath;
|
||||
|
||||
// 기본 디렉토리도 업데이트
|
||||
DefaultMapDirectory = Path.GetDirectoryName(filePath);
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 맵 파일이 존재하는지 확인
|
||||
/// </summary>
|
||||
public bool HasValidLastMapFile()
|
||||
{
|
||||
return !string.IsNullOrEmpty(LastMapFilePath) && File.Exists(LastMapFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정 초기화
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
LastMapFilePath = string.Empty;
|
||||
AutoLoadLastMapFile = true;
|
||||
DefaultMapDirectory = string.Empty;
|
||||
LastSaved = DateTime.Now;
|
||||
Save();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
77
AGVLogic/AGVMapEditor/Models/ImagePathEditor.cs
Normal file
77
AGVLogic/AGVMapEditor/Models/ImagePathEditor.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Forms.Design;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// PropertyGrid에서 이미지 파일 경로를 선택하기 위한 커스텀 UITypeEditor
|
||||
/// PropertyGrid에 "..." 버튼을 표시하고, 클릭 시 파일 열기 대화상자를 표시
|
||||
/// </summary>
|
||||
public class ImagePathEditor : UITypeEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// PropertyGrid에서 이 에디터의 UI 스타일 반환
|
||||
/// DropDown 스타일을 사용하여 "..." 버튼을 표시
|
||||
/// </summary>
|
||||
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
|
||||
{
|
||||
return UITypeEditorEditStyle.Modal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사용자가 "..." 버튼을 클릭할 때 호출되는 메서드
|
||||
/// </summary>
|
||||
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
|
||||
{
|
||||
// IWindowsFormsEditorService를 얻어서 대화상자를 표시
|
||||
var editorService = provider?.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
|
||||
if (editorService == null)
|
||||
return value;
|
||||
|
||||
// 파일 열기 대화상자 생성
|
||||
using (var ofd = new OpenFileDialog())
|
||||
{
|
||||
ofd.Title = "이미지 파일 선택";
|
||||
ofd.Filter = "이미지 파일|*.jpg;*.jpeg;*.png;*.bmp;*.gif|모든 파일|*.*";
|
||||
ofd.CheckFileExists = true;
|
||||
|
||||
// 현재 경로가 있으면 해당 위치에서 시작
|
||||
if (!string.IsNullOrEmpty(value?.ToString()))
|
||||
{
|
||||
try
|
||||
{
|
||||
string currentPath = value.ToString();
|
||||
if (System.IO.File.Exists(currentPath))
|
||||
{
|
||||
ofd.InitialDirectory = System.IO.Path.GetDirectoryName(currentPath);
|
||||
ofd.FileName = System.IO.Path.GetFileName(currentPath);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// 대화상자 표시
|
||||
if (ofd.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
// 선택된 파일 경로를 Base64로 변환하고 반환
|
||||
string filePath = ofd.FileName;
|
||||
return filePath; // MapNode의 ConvertImageToBase64는 setter에서 호출됨
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyGrid에서 이 타입의 값을 표시하는 방법
|
||||
/// 파일 경로를 파일명만 표시하도록 처리
|
||||
/// </summary>
|
||||
public override bool GetPaintValueSupported(ITypeDescriptorContext context)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
210
AGVLogic/AGVMapEditor/Models/MapImage.cs
Normal file
210
AGVLogic/AGVMapEditor/Models/MapImage.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 이미지 정보를 관리하는 클래스
|
||||
/// 디자인 요소용 이미지/비트맵 요소
|
||||
/// </summary>
|
||||
public class MapImage
|
||||
{
|
||||
/// <summary>
|
||||
/// 이미지 고유 ID
|
||||
/// </summary>
|
||||
public string ImageId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 파일 경로
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표 (좌상단 기준)
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 크기 (원본 크기 기준 배율)
|
||||
/// </summary>
|
||||
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 투명도 (0.0 ~ 1.0)
|
||||
/// </summary>
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 회전 각도 (도 단위)
|
||||
/// </summary>
|
||||
public float Rotation { get; set; } = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 설명
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public Image LoadedImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public MapImage()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="imageId">이미지 ID</param>
|
||||
/// <param name="imagePath">이미지 파일 경로</param>
|
||||
/// <param name="position">위치</param>
|
||||
public MapImage(string imageId, string imagePath, Point position)
|
||||
{
|
||||
ImageId = imageId;
|
||||
ImagePath = imagePath;
|
||||
Position = position;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadImage()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
var originalImage = Image.FromFile(ImagePath);
|
||||
|
||||
// 이미지 크기 체크 및 리사이즈
|
||||
if (originalImage.Width > 256 || originalImage.Height > 256)
|
||||
{
|
||||
LoadedImage = ResizeImage(originalImage, 256, 256);
|
||||
originalImage.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadedImage = originalImage;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 이미지 로드 실패
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 리사이즈 (비율 유지)
|
||||
/// </summary>
|
||||
/// <param name="image">원본 이미지</param>
|
||||
/// <param name="maxWidth">최대 너비</param>
|
||||
/// <param name="maxHeight">최대 높이</param>
|
||||
/// <returns>리사이즈된 이미지</returns>
|
||||
private Image ResizeImage(Image image, int maxWidth, int maxHeight)
|
||||
{
|
||||
// 비율 계산
|
||||
double ratioX = (double)maxWidth / image.Width;
|
||||
double ratioY = (double)maxHeight / image.Height;
|
||||
double ratio = Math.Min(ratioX, ratioY);
|
||||
|
||||
// 새로운 크기 계산
|
||||
int newWidth = (int)(image.Width * ratio);
|
||||
int newHeight = (int)(image.Height * ratio);
|
||||
|
||||
// 리사이즈된 이미지 생성
|
||||
var resizedImage = new Bitmap(newWidth, newHeight);
|
||||
using (var graphics = Graphics.FromImage(resizedImage))
|
||||
{
|
||||
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
|
||||
}
|
||||
|
||||
return resizedImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 표시될 크기 계산
|
||||
/// </summary>
|
||||
/// <returns>실제 크기</returns>
|
||||
public Size GetDisplaySize()
|
||||
{
|
||||
if (LoadedImage == null) return Size.Empty;
|
||||
|
||||
return new Size(
|
||||
(int)(LoadedImage.Width * Scale.Width),
|
||||
(int)(LoadedImage.Height * Scale.Height)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ImageId}: {System.IO.Path.GetFileName(ImagePath)} at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 복사
|
||||
/// </summary>
|
||||
/// <returns>복사된 이미지</returns>
|
||||
public MapImage Clone()
|
||||
{
|
||||
var clone = new MapImage
|
||||
{
|
||||
ImageId = ImageId,
|
||||
ImagePath = ImagePath,
|
||||
Position = Position,
|
||||
Scale = Scale,
|
||||
Opacity = Opacity,
|
||||
Rotation = Rotation,
|
||||
Description = Description,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
IsActive = IsActive
|
||||
};
|
||||
|
||||
// 이미지는 복사하지 않음 (필요시 LoadImage() 호출)
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
LoadedImage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
AGVLogic/AGVMapEditor/Models/MapLabel.cs
Normal file
125
AGVLogic/AGVMapEditor/Models/MapLabel.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 라벨 정보를 관리하는 클래스
|
||||
/// 디자인 요소용 텍스트 라벨
|
||||
/// </summary>
|
||||
public class MapLabel
|
||||
{
|
||||
/// <summary>
|
||||
/// 라벨 고유 ID
|
||||
/// </summary>
|
||||
public string LabelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 텍스트
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 정보
|
||||
/// </summary>
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 크기
|
||||
/// </summary>
|
||||
public float FontSize { get; set; } = 12;
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 스타일 (Bold, Italic 등)
|
||||
/// </summary>
|
||||
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
|
||||
|
||||
/// <summary>
|
||||
/// 글자 색상
|
||||
/// </summary>
|
||||
public Color ForeColor { get; set; } = Color.Black;
|
||||
|
||||
/// <summary>
|
||||
/// 배경 색상
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; } = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 배경 표시 여부
|
||||
/// </summary>
|
||||
public bool ShowBackground { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public MapLabel()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="labelId">라벨 ID</param>
|
||||
/// <param name="text">라벨 텍스트</param>
|
||||
/// <param name="position">위치</param>
|
||||
public MapLabel(string labelId, string text, Point position)
|
||||
{
|
||||
LabelId = labelId;
|
||||
Text = text;
|
||||
Position = position;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LabelId}: {Text} at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 복사
|
||||
/// </summary>
|
||||
/// <returns>복사된 라벨</returns>
|
||||
public MapLabel Clone()
|
||||
{
|
||||
return new MapLabel
|
||||
{
|
||||
LabelId = LabelId,
|
||||
Text = Text,
|
||||
Position = Position,
|
||||
FontFamily = FontFamily,
|
||||
FontSize = FontSize,
|
||||
FontStyle = FontStyle,
|
||||
ForeColor = ForeColor,
|
||||
BackColor = BackColor,
|
||||
ShowBackground = ShowBackground,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
IsActive = IsActive
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
26
AGVLogic/AGVMapEditor/Program.cs
Normal file
26
AGVLogic/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(string[] args)
|
||||
{
|
||||
// Windows Forms 애플리케이션 초기화
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
// 메인 폼 실행 (명령줄 인수 전달)
|
||||
Application.Run(new MainForm(args));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
AGVLogic/AGVMapEditor/Properties/AssemblyInfo.cs
Normal file
36
AGVLogic/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")]
|
||||
63
AGVLogic/AGVMapEditor/Properties/Resources.Designer.cs
generated
Normal file
63
AGVLogic/AGVMapEditor/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,63 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 이 코드는 도구를 사용하여 생성되었습니다.
|
||||
// 런타임 버전:4.0.30319.42000
|
||||
//
|
||||
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
|
||||
// 이러한 변경 내용이 손실됩니다.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace AGVMapEditor.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다.
|
||||
/// </summary>
|
||||
// 이 클래스는 ResGen 또는 Visual Studio와 같은 도구를 통해 StronglyTypedResourceBuilder
|
||||
// 클래스에서 자동으로 생성되었습니다.
|
||||
// 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여 ResGen을
|
||||
// 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AGVMapEditor.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해 현재 스레드의 CurrentUICulture 속성을
|
||||
/// 재정의합니다.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
AGVLogic/AGVMapEditor/Properties/Resources.resx
Normal file
120
AGVLogic/AGVMapEditor/Properties/Resources.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?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 through the TypeConverter architecture.
|
||||
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" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<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" type="xsd:string" use="required" />
|
||||
</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>
|
||||
29
AGVLogic/AGVMapEditor/build.bat
Normal file
29
AGVLogic/AGVMapEditor/build.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo Building V2GDecoder VC++ Project...
|
||||
|
||||
REM Check if Visual Studio 2022 is installed (Professional or Community)
|
||||
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
if exist %MSBUILD_PRO% (
|
||||
echo "Found Visual Studio 2022 Professional"
|
||||
set MSBUILD=%MSBUILD_PRO%
|
||||
) else if exist %MSBUILD_COM% (
|
||||
echo "Found Visual Studio 2022 Community"
|
||||
set MSBUILD=%MSBUILD_COM%
|
||||
) else if exist %MSBUILD_BT% (
|
||||
echo "Found Visual Studio 2022 BuildTools"
|
||||
set MSBUILD=%MSBUILD_BT%
|
||||
) else (
|
||||
echo "Visual Studio 2022 (Professional or Community) not found!"
|
||||
echo "Please install Visual Studio 2022 or update the MSBuild path."
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build Debug x64 configuration
|
||||
echo Building Debug x64 configuration...
|
||||
%MSBUILD% agvmapeditor.csproj
|
||||
|
||||
pause
|
||||
4
AGVLogic/AGVMapEditor/packages.config
Normal file
4
AGVLogic/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>
|
||||
119
AGVLogic/AGVNavigationCore/AGVNavigationCore.csproj
Normal file
119
AGVLogic/AGVNavigationCore/AGVNavigationCore.csproj
Normal file
@@ -0,0 +1,119 @@
|
||||
<?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>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>AGVNavigationCore</RootNamespace>
|
||||
<AssemblyName>AGVNavigationCore</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<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' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualBasic" />
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Controls\AGVState.cs" />
|
||||
<Compile Include="Controls\IAGV.cs" />
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Events.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Mouse.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Models\AGVCommand.cs" />
|
||||
<Compile Include="Models\Enums.cs" />
|
||||
<Compile Include="Models\IMovableAGV.cs" />
|
||||
<Compile Include="Models\VirtualAGV.cs" />
|
||||
<Compile Include="Models\MapLoader.cs" />
|
||||
<Compile Include="Models\MapMagnet.cs" />
|
||||
<Compile Include="Models\MapMark.cs" />
|
||||
<Compile Include="Models\MapNode.cs" />
|
||||
<Compile Include="Models\NodeBase.cs" />
|
||||
<Compile Include="Models\MapLabel.cs" />
|
||||
<Compile Include="Models\MapImage.cs" />
|
||||
<Compile Include="PathFinding\Planning\AGVPathfinder.cs" />
|
||||
<Compile Include="PathFinding\Planning\DirectionChangePlanner.cs" />
|
||||
<Compile Include="PathFinding\Planning\DirectionalPathfinder.cs" />
|
||||
<Compile Include="PathFinding\Validation\DockingValidationResult.cs" />
|
||||
<Compile Include="PathFinding\Validation\PathValidationResult.cs" />
|
||||
<Compile Include="PathFinding\Analysis\JunctionAnalyzer.cs" />
|
||||
<Compile Include="PathFinding\Core\PathNode.cs" />
|
||||
<Compile Include="PathFinding\Core\AStarPathfinder.cs" />
|
||||
<Compile Include="PathFinding\Core\AGVPathResult.cs" />
|
||||
<Compile Include="PathFinding\Planning\NodeMotorInfo.cs" />
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Designer.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Utils\DockingValidator.cs" />
|
||||
<Compile Include="Utils\DirectionalHelper.cs" />
|
||||
<Compile Include="Utils\LiftCalculator.cs" />
|
||||
<Compile Include="Utils\ImageConverterUtil.cs" />
|
||||
<Compile Include="Utils\AGVDirectionCalculator.cs" />
|
||||
<Compile Include="Utils\DirectionalPathfinderTest.cs" />
|
||||
<Compile Include="Utils\GetNextNodeIdTest.cs" />
|
||||
<Compile Include="Utils\TestRunner.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="build.bat" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="README.md" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
21
AGVLogic/AGVNavigationCore/Controls/AGVState.cs
Normal file
21
AGVLogic/AGVNavigationCore/Controls/AGVState.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
#region Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 열거형
|
||||
/// </summary>
|
||||
public enum AGVState
|
||||
{
|
||||
Idle, // 대기
|
||||
Moving, // 이동 중
|
||||
Rotating, // 회전 중
|
||||
Docking, // 도킹 중
|
||||
Charging, // 충전 중
|
||||
Error // 오류
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
30
AGVLogic/AGVNavigationCore/Controls/IAGV.cs
Normal file
30
AGVLogic/AGVNavigationCore/Controls/IAGV.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Drawing;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
#region Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// AGV 인터페이스 (가상/실제 AGV 통합)
|
||||
/// </summary>
|
||||
public interface IAGV
|
||||
{
|
||||
string AgvId { get; }
|
||||
Point CurrentPosition { get; set; }
|
||||
AgvDirection CurrentDirection { get; set; }
|
||||
AGVState CurrentState { get; set; }
|
||||
float BatteryLevel { get; }
|
||||
|
||||
// 이동 경로 정보 추가
|
||||
Point? PrevPosition { get; }
|
||||
MapNode CurrentNode { get; }
|
||||
MapNode PrevNode { get; }
|
||||
DockingDirection DockingDirection { get; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
41
AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs
generated
Normal file
41
AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
partial class UnifiedAGVCanvas
|
||||
{
|
||||
/// <summary>
|
||||
/// 필수 디자이너 변수입니다.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
|
||||
#region 구성 요소 디자이너에서 생성한 코드
|
||||
|
||||
/// <summary>
|
||||
/// 디자이너 지원에 필요한 메서드입니다.
|
||||
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// UnifiedAGVCanvas
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.White;
|
||||
this.Name = "UnifiedAGVCanvas";
|
||||
this.Size = new System.Drawing.Size(800, 600);
|
||||
this.Paint += new System.Windows.Forms.PaintEventHandler(this.UnifiedAGVCanvas_Paint);
|
||||
this.MouseClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseClick);
|
||||
this.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDoubleClick);
|
||||
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDown);
|
||||
this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseMove);
|
||||
this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseUp);
|
||||
this.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseWheel);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2536
AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
Normal file
2536
AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
Normal file
File diff suppressed because it is too large
Load Diff
1189
AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs
Normal file
1189
AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs
Normal file
File diff suppressed because it is too large
Load Diff
979
AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
Normal file
979
AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
Normal file
@@ -0,0 +1,979 @@
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Reflection.Emit;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 통합 AGV 캔버스 컨트롤
|
||||
/// 맵 편집, AGV 시뮬레이션, 실시간 모니터링을 모두 지원
|
||||
/// </summary>
|
||||
public partial class UnifiedAGVCanvas : UserControl
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const int NODE_SIZE = 24;
|
||||
private const int NODE_RADIUS = NODE_SIZE / 2;
|
||||
private const int GRID_SIZE = 20;
|
||||
private const float CONNECTION_WIDTH = 1.0f;
|
||||
private const int SNAP_DISTANCE = 10;
|
||||
private const int AGV_SIZE = 40;
|
||||
private const int CONNECTION_ARROW_SIZE = 8;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Enums
|
||||
|
||||
/// <summary>
|
||||
/// 캔버스 모드
|
||||
/// </summary>
|
||||
public enum CanvasMode
|
||||
{
|
||||
Edit, // 편집 가능 (맵 에디터)
|
||||
Sync, // 동기화 모드 (장비 설정 동기화)
|
||||
Emulator, // 에뮬레이터 모드
|
||||
Run // 가동 모드 (User Request)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
||||
/// </summary>
|
||||
public enum EditMode
|
||||
{
|
||||
Select, // 선택 모드
|
||||
Move, // 이동 모드
|
||||
AddNode, // 노드 추가 모드
|
||||
Connect, // 연결 모드
|
||||
Delete, // 삭제 모드
|
||||
DeleteConnection, // 연결 삭제 모드
|
||||
AddLabel, // 라벨 추가 모드
|
||||
AddImage, // 이미지 추가 모드
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
// 캔버스 모드
|
||||
private CanvasMode _canvasMode = CanvasMode.Edit;
|
||||
private EditMode _editMode = EditMode.Select;
|
||||
|
||||
// 맵 데이터
|
||||
private List<MapNode> _nodes;
|
||||
private List<MapLabel> _labels; // 추가
|
||||
private List<MapImage> _images; // 추가
|
||||
private List<MapMark> _marks;
|
||||
private List<MapMagnet> _magnets;
|
||||
|
||||
// 선택된 객체들 (나중에 NodeBase로 통일 필요)
|
||||
private NodeBase _selectedNode;
|
||||
|
||||
private List<NodeBase> _selectedNodes; // 다중 선택 (NodeBase로 변경 고려)
|
||||
|
||||
private NodeBase _hoveredNode;
|
||||
|
||||
private NodeBase _destinationNode;
|
||||
|
||||
// AGV 관련
|
||||
private List<IAGV> _agvList;
|
||||
private Dictionary<string, Point> _agvPositions;
|
||||
private Dictionary<string, AgvDirection> _agvDirections;
|
||||
private Dictionary<string, AGVState> _agvStates;
|
||||
|
||||
// 경로 관련
|
||||
private AGVPathResult _currentPath;
|
||||
private List<AGVPathResult> _allPaths;
|
||||
|
||||
// 도킹 검증 관련
|
||||
private Dictionary<string, bool> _dockingErrors;
|
||||
|
||||
// UI 요소들
|
||||
private Image _companyLogo;
|
||||
private string _companyLogoPath = string.Empty;
|
||||
private string _measurementInfo = string.Empty;
|
||||
|
||||
// 편집 관련 (EditMode에서만 사용)
|
||||
private bool _isDragging;
|
||||
private Point _dragOffset;
|
||||
private Point _dragStartPosition; // 드래그 시작 위치 (고스트 표시용)
|
||||
private Point _lastMousePosition;
|
||||
private bool _isConnectionMode;
|
||||
private MapNode _connectionStartNode;
|
||||
private Point _connectionEndPoint;
|
||||
private int _mouseMoveCounter = 0; // 디버그용: MouseMove 실행 횟수
|
||||
|
||||
// 영역 선택 관련
|
||||
private bool _isAreaSelecting;
|
||||
private Point _areaSelectStart;
|
||||
private Point _areaSelectEnd;
|
||||
|
||||
// 그리드 및 줌 관련
|
||||
private bool _showGrid = true;
|
||||
private float _zoomFactor = 1.0f;
|
||||
private PointF _panOffset = PointF.Empty; // float 정밀도로 변경 (팬 이동 정확도 개선)
|
||||
private bool _isPanning;
|
||||
|
||||
// 자동 증가 카운터
|
||||
private int _nodeCounter = 1;
|
||||
|
||||
// 강조 연결
|
||||
private (string FromNodeId, string ToNodeId)? _highlightedConnection = null;
|
||||
|
||||
// RFID 중복 검사
|
||||
private HashSet<string> _duplicateRfidNodes = new HashSet<string>();
|
||||
|
||||
// 동기화 모드 관련
|
||||
private string _syncMessage = "동기화 중...";
|
||||
private float _syncProgress = 0.0f;
|
||||
private string _syncDetail = "";
|
||||
|
||||
string _alertmesage = "";
|
||||
bool showalert = false;
|
||||
public void SetAlertMessage(string m)
|
||||
{
|
||||
_alertmesage = m;
|
||||
showalert = !string.IsNullOrEmpty(m);
|
||||
}
|
||||
|
||||
|
||||
// 브러쉬 및 펜
|
||||
private Brush _normalNodeBrush;
|
||||
private Brush _rotationNodeBrush;
|
||||
private Brush _dockingNodeBrush;
|
||||
private Brush _chargingNodeBrush;
|
||||
private Brush _selectedNodeBrush;
|
||||
private Brush _hoveredNodeBrush;
|
||||
private Brush _destinationNodeBrush;
|
||||
private Brush _gridBrush;
|
||||
private Brush _agvBrush;
|
||||
private Brush _pathBrush;
|
||||
|
||||
private Pen _connectionPen;
|
||||
private Pen _gridPen;
|
||||
private Pen _tempConnectionPen;
|
||||
private Pen _selectedNodePen;
|
||||
private Pen _destinationNodePen;
|
||||
private Pen _pathPen;
|
||||
private Pen _agvPen;
|
||||
private Pen _highlightedConnectionPen;
|
||||
private Pen _magnetPen;
|
||||
private Pen _markPen;
|
||||
private ToolTip _tooltip;
|
||||
|
||||
// 컨텍스트 메뉴
|
||||
private ContextMenuStrip _contextMenu;
|
||||
|
||||
// 이벤트
|
||||
public event EventHandler<NodeBase> NodeRightClicked;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
// 맵 편집 이벤트
|
||||
public delegate void NodeSelectHandler(object sender, NodeBase node, MouseEventArgs e);
|
||||
public event NodeSelectHandler NodeSelect;
|
||||
|
||||
public event EventHandler<NodeBase> NodeAdded;
|
||||
public event EventHandler<List<NodeBase>> NodesSelected; // 다중 선택 이벤트
|
||||
public event EventHandler<NodeBase> NodeDeleted;
|
||||
public event EventHandler<NodeBase> NodeMoved;
|
||||
public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted;
|
||||
public event EventHandler<MapImage> ImageDoubleClicked;
|
||||
public event EventHandler<MapLabel> LabelDoubleClicked;
|
||||
public event EventHandler MapChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public string PredictMessage { get; set; } = "";
|
||||
public string MapFileName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 캔버스 모드
|
||||
/// </summary>
|
||||
public CanvasMode Mode
|
||||
{
|
||||
get => _canvasMode;
|
||||
set
|
||||
{
|
||||
_canvasMode = value;
|
||||
UpdateModeUI();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 강조해서 표시할 특정 노드 ID (예: Gateway)
|
||||
/// 이 값이 설정되면 해당 노드만 강조 표시됩니다.
|
||||
/// </summary>
|
||||
public string HighlightNodeId { get; set; }
|
||||
|
||||
public void RemoveItem(NodeBase item)
|
||||
{
|
||||
if (item is MapImage img) RemoveImage(img);
|
||||
else if (item is MapLabel lb) RemoveLabel(lb);
|
||||
else if (item is MapNode nd) RemoveNode(nd);
|
||||
else if (item is MapMark mk) RemoveMark(mk);
|
||||
else if (item is MapMagnet mg) RemoveMagnet(mg);
|
||||
else throw new Exception("unknown type");
|
||||
|
||||
}
|
||||
public void RemoveNode(MapNode node)
|
||||
{
|
||||
if (_nodes != null && _nodes.Contains(node))
|
||||
{
|
||||
_nodes.Remove(node);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
public void RemoveLabel(MapLabel label)
|
||||
{
|
||||
if (_labels != null && _labels.Contains(label))
|
||||
{
|
||||
_labels.Remove(label);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveImage(MapImage image)
|
||||
{
|
||||
if (_images != null && _images.Contains(image))
|
||||
{
|
||||
_images.Remove(image);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveMark(MapMark mark)
|
||||
{
|
||||
if (_marks != null && _marks.Contains(mark))
|
||||
{
|
||||
_marks.Remove(mark);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveMagnet(MapMagnet magnet)
|
||||
{
|
||||
if (_magnets != null && _magnets.Contains(magnet))
|
||||
{
|
||||
_magnets.Remove(magnet);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
||||
/// </summary>
|
||||
public EditMode CurrentEditMode
|
||||
{
|
||||
get => _editMode;
|
||||
set
|
||||
{
|
||||
if (_canvasMode != CanvasMode.Edit) return;
|
||||
|
||||
_editMode = value;
|
||||
if (_editMode != EditMode.Connect)
|
||||
{
|
||||
CancelConnection();
|
||||
}
|
||||
Cursor = GetCursorForMode(_editMode);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그리드 표시 여부
|
||||
/// </summary>
|
||||
public bool ShowGrid
|
||||
{
|
||||
get => _showGrid;
|
||||
set
|
||||
{
|
||||
_showGrid = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 줌 팩터
|
||||
/// </summary>
|
||||
public float ZoomFactor
|
||||
{
|
||||
get => _zoomFactor;
|
||||
set
|
||||
{
|
||||
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
[Browsable(false)]
|
||||
public MapImage SelectedImage
|
||||
{
|
||||
get { return this._selectedNode as MapImage; }
|
||||
}
|
||||
|
||||
[Browsable(false)]
|
||||
public MapLabel SelectedLabel
|
||||
{
|
||||
get { return this._selectedNode as MapLabel; }
|
||||
}
|
||||
|
||||
[Browsable(false)]
|
||||
public MapMark SelectedMark
|
||||
{
|
||||
get { return this._selectedNode as MapMark; }
|
||||
}
|
||||
|
||||
|
||||
[Browsable(false)]
|
||||
public MapMagnet SelectedMagnet
|
||||
{
|
||||
get { return this._selectedNode as MapMagnet; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선택된 노드 (단일)
|
||||
/// </summary>
|
||||
public MapNode SelectedNode
|
||||
{
|
||||
get { return this._selectedNode as MapNode; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선택된 노드들 (다중)
|
||||
/// </summary>
|
||||
public List<NodeBase> SelectedNodes => _selectedNodes ?? new List<NodeBase>();
|
||||
|
||||
|
||||
public List<NodeBase> Items
|
||||
{
|
||||
get
|
||||
{
|
||||
List<NodeBase> items = new List<NodeBase>();
|
||||
if (Nodes != null && Nodes.Any()) items.AddRange(Nodes);
|
||||
if (Labels != null && Labels.Any()) items.AddRange(Labels);
|
||||
if (Images != null && Images.Any()) items.AddRange(Images);
|
||||
if (Marks != null && Marks.Any()) items.AddRange(Marks);
|
||||
if (Magnets != null && Magnets.Any()) items.AddRange(Magnets);
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map file loading 결과를 셋팅합니다
|
||||
/// </summary>
|
||||
/// <param name="result"></param>
|
||||
public void SetMapLoadResult(MapLoader.MapLoadResult result)
|
||||
{
|
||||
this.Nodes = result.Nodes;
|
||||
this.Labels = result.Labels; // 추가
|
||||
this.Images = result.Images; // 추가
|
||||
this.Marks = result.Marks;
|
||||
this.Magnets = result.Magnets;
|
||||
|
||||
// 🔥 맵 설정 적용 (배경색, 그리드 표시)
|
||||
if (result.Settings != null)
|
||||
{
|
||||
this.BackColor = Color.FromArgb(result.Settings.BackgroundColorArgb);
|
||||
this.ShowGrid = result.Settings.ShowGrid;
|
||||
}
|
||||
|
||||
this.FitToNodes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 목록
|
||||
/// </summary>
|
||||
public List<MapNode> Nodes
|
||||
{
|
||||
get => _nodes ?? new List<MapNode>();
|
||||
set
|
||||
{
|
||||
_nodes = value ?? new List<MapNode>();
|
||||
|
||||
// 기존 노드들의 최대 번호를 찾아서 _nodeCounter 설정
|
||||
UpdateNodeCounter();
|
||||
|
||||
// RFID 중복값 검사
|
||||
DetectDuplicateRfidNodes();
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 목록
|
||||
/// </summary>
|
||||
public List<MapLabel> Labels
|
||||
{
|
||||
get => _labels ?? new List<MapLabel>();
|
||||
set
|
||||
{
|
||||
_labels = value ?? new List<MapLabel>();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 목록
|
||||
/// </summary>
|
||||
public List<MapImage> Images
|
||||
{
|
||||
get => _images ?? new List<MapImage>();
|
||||
set
|
||||
{
|
||||
_images = value ?? new List<MapImage>();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마크 목록
|
||||
/// </summary>
|
||||
public List<MapMark> Marks
|
||||
{
|
||||
get => _marks ?? new List<MapMark>();
|
||||
set
|
||||
{
|
||||
_marks = value ?? new List<MapMark>();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마그넷 목록
|
||||
/// </summary>
|
||||
public List<MapMagnet> Magnets
|
||||
{
|
||||
get => _magnets ?? new List<MapMagnet>();
|
||||
set
|
||||
{
|
||||
_magnets = value ?? new List<MapMagnet>();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 목록
|
||||
/// </summary>
|
||||
public List<IAGV> AGVList
|
||||
{
|
||||
get => _agvList ?? new List<IAGV>();
|
||||
set
|
||||
{
|
||||
_agvList = value ?? new List<IAGV>();
|
||||
UpdateAGVData();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 표시할 경로
|
||||
/// </summary>
|
||||
public AGVPathResult CurrentPath
|
||||
{
|
||||
get => _currentPath;
|
||||
set
|
||||
{
|
||||
_currentPath = value;
|
||||
UpdateDestinationNode();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상세경로가 설정되어있는가?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasPath()
|
||||
{
|
||||
if (_currentPath == null) return false;
|
||||
if (_currentPath.DetailedPath == null) return false;
|
||||
return _currentPath.DetailedPath.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 경로 목록 (다중 AGV 경로 표시용)
|
||||
/// </summary>
|
||||
public List<AGVPathResult> AllPaths
|
||||
{
|
||||
get => _allPaths ?? new List<AGVPathResult>();
|
||||
set
|
||||
{
|
||||
_allPaths = value ?? new List<AGVPathResult>();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회사 로고 이미지
|
||||
/// </summary>
|
||||
public Image CompanyLogo
|
||||
{
|
||||
get => _companyLogo;
|
||||
set
|
||||
{
|
||||
_companyLogo = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 측정 정보 텍스트
|
||||
/// </summary>
|
||||
public string MeasurementInfo
|
||||
{
|
||||
get => _measurementInfo;
|
||||
set
|
||||
{
|
||||
_measurementInfo = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Connection Highlighting
|
||||
|
||||
/// <summary>
|
||||
/// 특정 연결을 강조 표시
|
||||
/// </summary>
|
||||
/// <param name="fromNodeId">시작 노드 ID</param>
|
||||
/// <param name="toNodeId">끝 노드 ID</param>
|
||||
public void HighlightConnection(string fromNodeId, string toNodeId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fromNodeId) || string.IsNullOrEmpty(toNodeId))
|
||||
{
|
||||
_highlightedConnection = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 사전순으로 정렬하여 저장 (연결이 단일 방향으로 저장되므로)
|
||||
if (string.Compare(fromNodeId, toNodeId, StringComparison.Ordinal) <= 0)
|
||||
{
|
||||
_highlightedConnection = (fromNodeId, toNodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_highlightedConnection = (toNodeId, fromNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 연결 강조 표시 해제
|
||||
/// </summary>
|
||||
public void ClearHighlightedConnection()
|
||||
{
|
||||
_highlightedConnection = null;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public UnifiedAGVCanvas()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeCanvas();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
private void InitializeCanvas()
|
||||
{
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint |
|
||||
ControlStyles.UserPaint |
|
||||
ControlStyles.DoubleBuffer |
|
||||
ControlStyles.ResizeRedraw, true);
|
||||
|
||||
_nodes = new List<MapNode>();
|
||||
_labels = new List<MapLabel>();
|
||||
_images = new List<MapImage>();
|
||||
_marks = new List<MapMark>();
|
||||
_magnets = new List<MapMagnet>();
|
||||
|
||||
_selectedNodes = new List<NodeBase>(); // 다중 선택 리스트 초기화
|
||||
_agvList = new List<IAGV>();
|
||||
_agvPositions = new Dictionary<string, Point>();
|
||||
_agvDirections = new Dictionary<string, AgvDirection>();
|
||||
_agvStates = new Dictionary<string, AGVState>();
|
||||
_allPaths = new List<AGVPathResult>();
|
||||
_dockingErrors = new Dictionary<string, bool>();
|
||||
|
||||
InitializeBrushesAndPens();
|
||||
CreateContextMenu();
|
||||
|
||||
_tooltip = new ToolTip();
|
||||
_tooltip.AutoPopDelay = 5000;
|
||||
_tooltip.InitialDelay = 1000;
|
||||
_tooltip.ReshowDelay = 500;
|
||||
_tooltip.ShowAlways = true;
|
||||
}
|
||||
|
||||
private void InitializeBrushesAndPens()
|
||||
{
|
||||
// 노드 브러쉬
|
||||
_normalNodeBrush = new SolidBrush(Color.LightBlue);
|
||||
_rotationNodeBrush = new SolidBrush(Color.Yellow);
|
||||
_dockingNodeBrush = new SolidBrush(Color.Orange);
|
||||
_chargingNodeBrush = new SolidBrush(Color.Green);
|
||||
_selectedNodeBrush = new SolidBrush(Color.Red);
|
||||
_hoveredNodeBrush = new SolidBrush(Color.LightCyan);
|
||||
_destinationNodeBrush = new SolidBrush(Color.Gold);
|
||||
|
||||
// AGV 및 경로 브러쉬
|
||||
_agvBrush = new SolidBrush(Color.Red);
|
||||
_pathBrush = new SolidBrush(Color.Purple);
|
||||
|
||||
// 그리드 브러쉬
|
||||
_gridBrush = new SolidBrush(Color.LightGray);
|
||||
|
||||
// 펜
|
||||
_connectionPen = new Pen(Color.White, CONNECTION_WIDTH);
|
||||
_connectionPen.DashStyle = DashStyle.Dash;
|
||||
_connectionPen.EndCap = LineCap.ArrowAnchor;
|
||||
|
||||
_gridPen = new Pen(Color.LightGray, 1);
|
||||
_tempConnectionPen = new Pen(Color.Orange, 2) { DashStyle = DashStyle.Dash };
|
||||
_selectedNodePen = new Pen(Color.Red, 3);
|
||||
_destinationNodePen = new Pen(Color.Orange, 4);
|
||||
_pathPen = new Pen(Color.Purple, 3);
|
||||
_agvPen = new Pen(Color.Red, 3);
|
||||
_highlightedConnectionPen = new Pen(Color.Red, 4) { DashStyle = DashStyle.Solid };
|
||||
_magnetPen = new Pen(Color.FromArgb(100, Color.LightSkyBlue), 15) { DashStyle = DashStyle.Solid };
|
||||
_markPen = new Pen(Color.White, 3); // 마크는 흰색 선으로 표시
|
||||
}
|
||||
|
||||
private void CreateContextMenu()
|
||||
{
|
||||
_contextMenu = new ContextMenuStrip();
|
||||
// 컨텍스트 메뉴는 EditMode에서만 사용
|
||||
}
|
||||
|
||||
private void UpdateModeUI()
|
||||
{
|
||||
// 모드에 따른 UI 업데이트
|
||||
_contextMenu.Enabled = true;
|
||||
Cursor = GetCursorForMode(_editMode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AGV Management
|
||||
|
||||
/// <summary>
|
||||
/// AGV 위치 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVPosition(string agvId, Point position)
|
||||
{
|
||||
if (_agvPositions.ContainsKey(agvId))
|
||||
_agvPositions[agvId] = position;
|
||||
else
|
||||
_agvPositions.Add(agvId, position);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 방향 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVDirection(string agvId, AgvDirection direction)
|
||||
{
|
||||
if (_agvDirections.ContainsKey(agvId))
|
||||
_agvDirections[agvId] = direction;
|
||||
else
|
||||
_agvDirections.Add(agvId, direction);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVState(string agvId, AGVState state)
|
||||
{
|
||||
if (_agvStates.ContainsKey(agvId))
|
||||
_agvStates[agvId] = state;
|
||||
else
|
||||
_agvStates.Add(agvId, state);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 위치 설정 (시뮬레이터용)
|
||||
/// </summary>
|
||||
/// <param name="agvId">AGV ID</param>
|
||||
/// <param name="position">새로운 위치</param>
|
||||
public void SetAGVPosition(string agvId, MapNode node, AgvDirection direction)
|
||||
{
|
||||
UpdateAGVPosition(agvId, node.Position);
|
||||
UpdateAGVDirection(agvId, direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 데이터 동기화
|
||||
/// </summary>
|
||||
private void UpdateAGVData()
|
||||
{
|
||||
if (_agvList == null) return;
|
||||
|
||||
foreach (var agv in _agvList)
|
||||
{
|
||||
UpdateAGVPosition(agv.AgvId, agv.CurrentPosition);
|
||||
UpdateAGVDirection(agv.AgvId, agv.CurrentDirection);
|
||||
UpdateAGVState(agv.AgvId, agv.CurrentState);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private Cursor GetCursorForMode(EditMode mode)
|
||||
{
|
||||
if (_canvasMode != CanvasMode.Edit)
|
||||
return Cursors.Default;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case EditMode.AddNode:
|
||||
return Cursors.Cross;
|
||||
case EditMode.Move:
|
||||
return Cursors.SizeAll;
|
||||
case EditMode.Connect:
|
||||
return Cursors.Hand;
|
||||
case EditMode.Delete:
|
||||
return Cursors.No;
|
||||
default:
|
||||
return Cursors.Default;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelConnection()
|
||||
{
|
||||
_isConnectionMode = false;
|
||||
_connectionStartNode = null;
|
||||
_connectionEndPoint = Point.Empty;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void UpdateDestinationNode()
|
||||
{
|
||||
_destinationNode = null;
|
||||
|
||||
if (_currentPath != null && _currentPath.Success && _currentPath.Path != null && _currentPath.Path.Count > 0)
|
||||
{
|
||||
// 경로의 마지막 노드가 목적지
|
||||
_destinationNode = _currentPath.Path[_currentPath.Path.Count - 1];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 동기화 상태 설정
|
||||
/// </summary>
|
||||
/// <param name="message">메인 메시지</param>
|
||||
/// <param name="progress">진행률 (0.0 ~ 1.0)</param>
|
||||
/// <param name="detail">상세 메시지</param>
|
||||
public void SetSyncStatus(string message, float progress, string detail = "")
|
||||
{
|
||||
_syncMessage = message;
|
||||
_syncProgress = Math.Max(0.0f, Math.Min(1.0f, progress));
|
||||
_syncDetail = detail;
|
||||
|
||||
if (_canvasMode != CanvasMode.Sync)
|
||||
{
|
||||
_canvasMode = CanvasMode.Sync;
|
||||
UpdateModeUI();
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 동기화 모드 종료
|
||||
/// </summary>
|
||||
public void ExitSyncMode(CanvasMode newmode)
|
||||
{
|
||||
if (_canvasMode == CanvasMode.Sync)
|
||||
{
|
||||
_canvasMode = newmode; // 기본 모드로 복귀 (또는 이전 모드)
|
||||
UpdateModeUI();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cleanup
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
// 브러쉬 정리
|
||||
_normalNodeBrush?.Dispose();
|
||||
_rotationNodeBrush?.Dispose();
|
||||
_dockingNodeBrush?.Dispose();
|
||||
_chargingNodeBrush?.Dispose();
|
||||
_selectedNodeBrush?.Dispose();
|
||||
_hoveredNodeBrush?.Dispose();
|
||||
_destinationNodeBrush?.Dispose();
|
||||
_gridBrush?.Dispose();
|
||||
_agvBrush?.Dispose();
|
||||
_pathBrush?.Dispose();
|
||||
|
||||
// 펜 정리
|
||||
_connectionPen?.Dispose();
|
||||
_gridPen?.Dispose();
|
||||
_tempConnectionPen?.Dispose();
|
||||
_selectedNodePen?.Dispose();
|
||||
_destinationNodePen?.Dispose();
|
||||
_pathPen?.Dispose();
|
||||
_agvPen?.Dispose();
|
||||
_highlightedConnectionPen?.Dispose();
|
||||
_magnetPen?.Dispose();
|
||||
_markPen?.Dispose();
|
||||
|
||||
// 컨텍스트 메뉴 정리
|
||||
_contextMenu?.Dispose();
|
||||
|
||||
// 이미지 정리
|
||||
_companyLogo?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// RFID 중복값을 가진 노드들을 감지하고 표시
|
||||
/// 나중에 추가된 노드(인덱스가 더 큰)를 중복으로 간주
|
||||
/// </summary>
|
||||
private void DetectDuplicateRfidNodes()
|
||||
{
|
||||
_duplicateRfidNodes.Clear();
|
||||
|
||||
if (_nodes == null || _nodes.Count == 0)
|
||||
return;
|
||||
|
||||
// RFID값과 해당 노드의 인덱스를 저장
|
||||
var rfidToNodeIndex = new Dictionary<ushort, List<int>>();
|
||||
|
||||
// 모든 노드의 RFID값 수집
|
||||
for (int i = 0; i < _nodes.Count; i++)
|
||||
{
|
||||
var node = _nodes[i];
|
||||
if (node.HasRfid())
|
||||
{
|
||||
if (!rfidToNodeIndex.ContainsKey(node.RfidId))
|
||||
{
|
||||
rfidToNodeIndex[node.RfidId] = new List<int>();
|
||||
}
|
||||
rfidToNodeIndex[node.RfidId].Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 중복된 RFID를 가진 노드들을 찾아서 나중에 추가된 것들을 표시
|
||||
foreach (var kvp in rfidToNodeIndex)
|
||||
{
|
||||
if (kvp.Value.Count > 1)
|
||||
{
|
||||
// 첫 번째 노드는 원본으로 유지, 나머지는 중복으로 표시
|
||||
for (int i = 1; i < kvp.Value.Count; i++)
|
||||
{
|
||||
int duplicateNodeIndex = kvp.Value[i];
|
||||
_duplicateRfidNodes.Add(_nodes[duplicateNodeIndex].Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 노드들의 최대 번호를 찾아서 _nodeCounter를 업데이트
|
||||
/// </summary>
|
||||
private void UpdateNodeCounter()
|
||||
{
|
||||
if (_nodes == null || _nodes.Count == 0)
|
||||
{
|
||||
_nodeCounter = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
int maxNumber = 0;
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
// NodeId에서 숫자 부분 추출 (예: "N001" -> 1)
|
||||
if (node.Id.StartsWith("N") && int.TryParse(node.Id.Substring(1), out int number))
|
||||
{
|
||||
maxNumber = Math.Max(maxNumber, number);
|
||||
}
|
||||
}
|
||||
|
||||
_nodeCounter = maxNumber + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 노드에 도킹 오류 표시를 설정/해제합니다.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="hasError">오류 여부</param>
|
||||
public void SetDockingError(string nodeId, bool hasError)
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
return;
|
||||
|
||||
if (hasError)
|
||||
{
|
||||
_dockingErrors[nodeId] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dockingErrors.Remove(nodeId);
|
||||
}
|
||||
|
||||
Invalidate(); // 화면 다시 그리기
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 노드에 도킹 오류가 있는지 확인합니다.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <returns>도킹 오류 여부</returns>
|
||||
public bool HasDockingError(string nodeId)
|
||||
{
|
||||
return _dockingErrors.ContainsKey(nodeId) && _dockingErrors[nodeId];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 도킹 오류를 초기화합니다.
|
||||
/// </summary>
|
||||
public void ClearDockingErrors()
|
||||
{
|
||||
_dockingErrors.Clear();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
54
AGVLogic/AGVNavigationCore/Models/AGVCommand.cs
Normal file
54
AGVLogic/AGVNavigationCore/Models/AGVCommand.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 제어 명령 클래스 (실제 AGV 제어용)
|
||||
/// Predict() 메서드가 반환하는 다음 동작 명령
|
||||
/// </summary>
|
||||
public class AGVCommand
|
||||
{
|
||||
/// <summary>모터 명령 (정지/전진/후진)</summary>
|
||||
public MotorCommand Motor { get; set; }
|
||||
|
||||
/// <summary>마그넷 위치 (직진/왼쪽/오른쪽)</summary>
|
||||
public MagnetPosition Magnet { get; set; }
|
||||
|
||||
/// <summary>속도 레벨 (저속/중속/고속)</summary>
|
||||
public SpeedLevel Speed { get; set; }
|
||||
|
||||
/// <summary>명령 이유 메세지- (디버깅/로깅용)</summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>명령 이유- (디버깅/로깅용)</summary>
|
||||
public eAGVCommandReason Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
public AGVCommand(MotorCommand motor, MagnetPosition magnet, SpeedLevel speed, eAGVCommandReason reason, string reasonmessage = "")
|
||||
{
|
||||
Motor = motor;
|
||||
Magnet = magnet;
|
||||
Speed = speed;
|
||||
Reason = reason;
|
||||
Message = reasonmessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public AGVCommand()
|
||||
{
|
||||
Motor = MotorCommand.Stop;
|
||||
Magnet = MagnetPosition.S;
|
||||
Speed = SpeedLevel.L;
|
||||
Message = "";
|
||||
Reason = eAGVCommandReason.Normal;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Motor:{Motor}, Magnet:{Magnet}, Speed:{Speed},Reason:{Reason}" +
|
||||
(string.IsNullOrEmpty(Message) ? "" : $" ({Message})");
|
||||
}
|
||||
}
|
||||
}
|
||||
182
AGVLogic/AGVNavigationCore/Models/Enums.cs
Normal file
182
AGVLogic/AGVNavigationCore/Models/Enums.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입 열거형
|
||||
/// </summary>
|
||||
public enum NodeType
|
||||
{
|
||||
/// <summary>일반 경로 노드</summary>
|
||||
Normal,
|
||||
|
||||
Label,
|
||||
/// <summary>이미지 (UI 요소)</summary>
|
||||
Image,
|
||||
/// <summary>
|
||||
/// 마크센서
|
||||
/// </summary>
|
||||
Mark,
|
||||
/// <summary>
|
||||
/// 마그넷라인
|
||||
/// </summary>
|
||||
Magnet
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 열거형
|
||||
/// </summary>
|
||||
public enum DockingDirection
|
||||
{
|
||||
/// <summary>도킹 방향 상관없음 (일반 경로 노드)</summary>
|
||||
DontCare,
|
||||
/// <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>
|
||||
Normal,
|
||||
/// <summary>로더</summary>
|
||||
Loader,
|
||||
/// <summary>클리너</summary>
|
||||
Clearner,
|
||||
/// <summary>오프로더</summary>
|
||||
UnLoader,
|
||||
/// <summary>버퍼</summary>
|
||||
Buffer,
|
||||
/// <summary>충전기1</summary>
|
||||
Charger1,
|
||||
/// <summary>충전기2</summary>
|
||||
Charger2,
|
||||
|
||||
/// <summary>
|
||||
/// 끝점(더이상 이동불가)
|
||||
/// </summary>
|
||||
Limit,
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV턴상태
|
||||
/// </summary>
|
||||
public enum AGVTurn
|
||||
{
|
||||
None=0,
|
||||
|
||||
/// <summary>
|
||||
/// left turn 90"
|
||||
/// </summary>
|
||||
L90,
|
||||
/// <summary>
|
||||
/// right turn 90"
|
||||
/// </summary>
|
||||
R90
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 모터 명령 열거형 (실제 AGV 제어용)
|
||||
/// </summary>
|
||||
public enum MotorCommand
|
||||
{
|
||||
/// <summary>정지</summary>
|
||||
Stop,
|
||||
/// <summary>전진 (Forward - 모니터 방향)</summary>
|
||||
Forward,
|
||||
/// <summary>후진 (Backward - 리프트 방향)</summary>
|
||||
Backward
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마그넷 위치 열거형 (실제 AGV 제어용)
|
||||
/// </summary>
|
||||
public enum MagnetPosition
|
||||
{
|
||||
/// <summary>직진 (Straight)</summary>
|
||||
S,
|
||||
/// <summary>왼쪽 (Left)</summary>
|
||||
L,
|
||||
/// <summary>오른쪽 (Right)</summary>
|
||||
R
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속도 레벨 열거형 (실제 AGV 제어용)
|
||||
/// </summary>
|
||||
public enum SpeedLevel
|
||||
{
|
||||
/// <summary>저속 (Low)</summary>
|
||||
L,
|
||||
/// <summary>중속 (Medium)</summary>
|
||||
M,
|
||||
/// <summary>고속 (High)</summary>
|
||||
H
|
||||
}
|
||||
|
||||
public enum eAGVCommandReason
|
||||
{
|
||||
/// <summary>
|
||||
/// 초기 미지정
|
||||
/// </summary>
|
||||
Normal,
|
||||
|
||||
/// <summary>
|
||||
/// 위치 미확정
|
||||
/// </summary>
|
||||
UnknownPosition,
|
||||
|
||||
/// <summary>
|
||||
/// 대상경로없음
|
||||
/// </summary>
|
||||
NoTarget,
|
||||
|
||||
/// <summary>
|
||||
/// 경로없음
|
||||
/// </summary>
|
||||
NoPath,
|
||||
|
||||
/// <summary>
|
||||
/// 경로이탈
|
||||
/// </summary>
|
||||
PathOut,
|
||||
|
||||
/// <summary>
|
||||
/// 마크스탑을 해야한다
|
||||
/// </summary>
|
||||
MarkStop,
|
||||
|
||||
/// <summary>
|
||||
/// 완료
|
||||
/// </summary>
|
||||
Complete,
|
||||
|
||||
}
|
||||
}
|
||||
210
AGVLogic/AGVNavigationCore/Models/IMovableAGV.cs
Normal file
210
AGVLogic/AGVNavigationCore/Models/IMovableAGV.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using AGVNavigationCore.Controls;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 이동 가능한 AGV 인터페이스
|
||||
/// 실제 AGV와 시뮬레이션 AGV 모두 구현해야 하는 기본 인터페이스
|
||||
/// </summary>
|
||||
public interface IMovableAGV
|
||||
{
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 변경 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<AGVState> StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 위치 변경 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<(Point, AgvDirection, MapNode)> PositionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 감지 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<string> RfidDetected;
|
||||
|
||||
/// <summary>
|
||||
/// 경로 완료 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<AGVPathResult> PathCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 오류 발생 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<string> ErrorOccurred;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// AGV ID
|
||||
/// </summary>
|
||||
string AgvId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치
|
||||
/// </summary>
|
||||
Point CurrentPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 방향 (모터 방향)
|
||||
/// </summary>
|
||||
AgvDirection CurrentDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 상태
|
||||
/// </summary>
|
||||
AGVState CurrentState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 속도
|
||||
/// </summary>
|
||||
float CurrentSpeed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 레벨 (0-100%)
|
||||
/// </summary>
|
||||
float BatteryLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 경로
|
||||
/// </summary>
|
||||
AGVPathResult CurrentPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID
|
||||
/// </summary>
|
||||
MapNode CurrentNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 목표 위치
|
||||
/// </summary>
|
||||
Point? PrevPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 목표 노드 ID
|
||||
/// </summary>
|
||||
MapNode PrevNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향
|
||||
/// </summary>
|
||||
DockingDirection DockingDirection { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sensor Input Methods (실제 AGV에서 호출)
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 설정 (실제 위치 센서에서)
|
||||
/// </summary>
|
||||
void SetCurrentPosition(Point position);
|
||||
|
||||
/// <summary>
|
||||
/// 감지된 RFID 설정 (실제 RFID 센서에서)
|
||||
/// </summary>
|
||||
void SetDetectedRfid(string rfidId);
|
||||
|
||||
/// <summary>
|
||||
/// 모터 방향 설정 (모터 컨트롤러에서)
|
||||
/// </summary>
|
||||
void SetMotorDirection(AgvDirection direction);
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 레벨 설정 (BMS에서)
|
||||
/// </summary>
|
||||
void SetBatteryLevel(float percentage);
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Query Methods
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 조회
|
||||
/// </summary>
|
||||
Point GetCurrentPosition();
|
||||
|
||||
/// <summary>
|
||||
/// 현재 상태 조회
|
||||
/// </summary>
|
||||
AGVState GetCurrentState();
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID 조회
|
||||
/// </summary>
|
||||
MapNode GetCurrentNode();
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 정보 문자열 조회
|
||||
/// </summary>
|
||||
string GetStatus();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Path Execution Methods
|
||||
|
||||
/// <summary>
|
||||
/// 경로 정지
|
||||
/// </summary>
|
||||
void StopPath();
|
||||
|
||||
/// <summary>
|
||||
/// 긴급 정지
|
||||
/// </summary>
|
||||
void EmergencyStop();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update Method
|
||||
|
||||
/// <summary>
|
||||
/// 프레임 업데이트 (외부에서 주기적으로 호출)
|
||||
/// 이 방식으로 타이머에 의존하지 않고 외부에서 제어 가능
|
||||
/// </summary>
|
||||
/// <param name="deltaTimeMs">마지막 업데이트 이후 경과 시간 (밀리초)</param>
|
||||
void Update(float deltaTimeMs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Manual Control Methods (테스트용)
|
||||
|
||||
/// <summary>
|
||||
/// 수동 이동
|
||||
/// </summary>
|
||||
void MoveTo(Point targetPosition);
|
||||
|
||||
/// <summary>
|
||||
/// 수동 회전
|
||||
/// </summary>
|
||||
void Rotate(AgvDirection direction);
|
||||
|
||||
/// <summary>
|
||||
/// 충전 시작
|
||||
/// </summary>
|
||||
void StartCharging();
|
||||
|
||||
/// <summary>
|
||||
/// 충전 종료
|
||||
/// </summary>
|
||||
void StopCharging();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cleanup
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
void Dispose();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
88
AGVLogic/AGVNavigationCore/Models/MapImage.cs
Normal file
88
AGVLogic/AGVNavigationCore/Models/MapImage.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using AGVNavigationCore.Utils;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
public class MapImage : NodeBase
|
||||
{
|
||||
[Category("기본 정보")]
|
||||
[Description("이미지의 이름입니다.")]
|
||||
public string Name { get; set; } = "Image";
|
||||
|
||||
[Category("이미지 설정")]
|
||||
[Description("이미지 파일 경로입니다 (편집기용).")]
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
|
||||
[ReadOnly(false)]
|
||||
public string ImageBase64 { get; set; } = string.Empty;
|
||||
|
||||
[Category("이미지 설정")]
|
||||
[Description("이미지 크기 배율입니다.")]
|
||||
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
|
||||
|
||||
[Category("이미지 설정")]
|
||||
[Description("이미지 투명도입니다 (0.0 ~ 1.0).")]
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
|
||||
[Category("이미지 설정")]
|
||||
[Description("이미지 회전 각도입니다.")]
|
||||
public float Rotation { get; set; } = 0.0f;
|
||||
|
||||
[JsonIgnore]
|
||||
[Browsable(false)]
|
||||
public Image LoadedImage { get; set; }
|
||||
|
||||
public MapImage()
|
||||
{
|
||||
Type = NodeType.Image;
|
||||
}
|
||||
|
||||
public bool LoadImage()
|
||||
{
|
||||
try
|
||||
{
|
||||
Image originalImage = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(ImageBase64))
|
||||
{
|
||||
originalImage = ImageConverterUtil.Base64ToImage(ImageBase64);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
|
||||
{
|
||||
originalImage = Image.FromFile(ImagePath);
|
||||
}
|
||||
|
||||
if (originalImage != null)
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
LoadedImage = originalImage; // 리사이즈 필요시 추가 구현
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 무시
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Size GetDisplaySize()
|
||||
{
|
||||
if (LoadedImage == null) return Size.Empty;
|
||||
return new Size(
|
||||
(int)(LoadedImage.Width * Scale.Width),
|
||||
(int)(LoadedImage.Height * Scale.Height)
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
LoadedImage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
AGVLogic/AGVNavigationCore/Models/MapLabel.cs
Normal file
42
AGVLogic/AGVNavigationCore/Models/MapLabel.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
public class MapLabel : NodeBase
|
||||
{
|
||||
[Category("라벨 설정")]
|
||||
[Description("표시할 텍스트입니다.")]
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
[Category("라벨 설정")]
|
||||
[Description("글자색입니다")]
|
||||
public Color ForeColor { get; set; } = Color.Black;
|
||||
|
||||
[Category("라벨 설정")]
|
||||
[Description("배경색입니다.")]
|
||||
public Color BackColor { get; set; } = Color.Transparent;
|
||||
|
||||
[Category("라벨 설정")]
|
||||
[Description("폰트 종류입니다.")]
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
|
||||
[Category("라벨 설정")]
|
||||
[Description("폰트 크기입니다.")]
|
||||
public float FontSize { get; set; } = 12.0f;
|
||||
|
||||
[Category("라벨 설정")]
|
||||
[Description("폰트 스타일입니다.")]
|
||||
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
|
||||
|
||||
[Category("라벨 설정")]
|
||||
[Description("내부 여백입니다.")]
|
||||
public int Padding { get; set; } = 5;
|
||||
|
||||
public MapLabel()
|
||||
{
|
||||
ForeColor = Color.Purple;
|
||||
Type = NodeType.Label;
|
||||
}
|
||||
}
|
||||
}
|
||||
514
AGVLogic/AGVNavigationCore/Models/MapLoader.cs
Normal file
514
AGVLogic/AGVNavigationCore/Models/MapLoader.cs
Normal file
@@ -0,0 +1,514 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 맵 파일 로딩/저장을 위한 공용 유틸리티 클래스
|
||||
/// AGVMapEditor와 AGVSimulator에서 공통으로 사용
|
||||
/// </summary>
|
||||
public static class MapLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 설정 정보 (배경색, 그리드 표시 등)
|
||||
/// </summary>
|
||||
public class MapSettings
|
||||
{
|
||||
public int BackgroundColorArgb { get; set; } = System.Drawing.Color.White.ToArgb();
|
||||
public bool ShowGrid { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 파일 로딩 결과
|
||||
/// </summary>
|
||||
public class MapLoadResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||
public List<MapLabel> Labels { get; set; } = new List<MapLabel>(); // 추가
|
||||
public List<MapImage> Images { get; set; } = new List<MapImage>(); // 추가
|
||||
public List<MapMark> Marks { get; set; } = new List<MapMark>();
|
||||
public List<MapMagnet> Magnets { get; set; } = new List<MapMagnet>();
|
||||
public MapSettings Settings { get; set; } = new MapSettings();
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 파일 저장용 데이터 구조
|
||||
/// </summary>
|
||||
public class MapFileData
|
||||
{
|
||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||
public List<MapLabel> Labels { get; set; } = new List<MapLabel>(); // 추가
|
||||
public List<MapImage> Images { get; set; } = new List<MapImage>(); // 추가
|
||||
public List<MapMark> Marks { get; set; } = new List<MapMark>();
|
||||
public List<MapMagnet> Magnets { get; set; } = new List<MapMagnet>();
|
||||
public MapSettings Settings { get; set; } = new MapSettings();
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string Version { get; set; } = "1.3"; // 버전 업그레이드
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 파일을 로드하여 노드를 반환
|
||||
/// </summary>
|
||||
/// <param name="filePath">맵 파일 경로</param>
|
||||
/// <returns>로딩 결과</returns>
|
||||
public static MapLoadResult LoadMapFromFile(string filePath)
|
||||
{
|
||||
var result = new MapLoadResult();
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
result.ErrorMessage = $"파일을 찾을 수 없습니다: {filePath}";
|
||||
return result;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(filePath);
|
||||
|
||||
// JSON 역직렬화 설정
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
MissingMemberHandling = MissingMemberHandling.Ignore,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Populate
|
||||
};
|
||||
|
||||
// 먼저 구조 파악을 위해 동적 객체로 로드하거나, MapFileData로 시도
|
||||
var mapData = JsonConvert.DeserializeObject<MapFileData>(json, settings);
|
||||
|
||||
if (mapData != null)
|
||||
{
|
||||
result.Nodes = new List<MapNode>();
|
||||
result.Labels = mapData.Labels ?? new List<MapLabel>();
|
||||
result.Images = mapData.Images ?? new List<MapImage>();
|
||||
result.Marks = mapData.Marks ?? new List<MapMark>();
|
||||
result.Magnets = mapData.Magnets ?? new List<MapMagnet>();
|
||||
result.Settings = mapData.Settings ?? new MapSettings();
|
||||
result.Version = mapData.Version ?? "1.0";
|
||||
result.CreatedDate = mapData.CreatedDate;
|
||||
|
||||
if (mapData.Nodes != null)
|
||||
{
|
||||
foreach (var node in mapData.Nodes)
|
||||
{
|
||||
// 마이그레이션: 기존 파일의 Nodes 리스트에 섞여있는 Label, Image 분리
|
||||
// (새 파일 구조에서는 이미 분리되어 로드됨)
|
||||
if (node.Type == NodeType.Label)
|
||||
{
|
||||
// MapNode -> MapLabel 변환 (필드 매핑)
|
||||
var label = new MapLabel
|
||||
{
|
||||
Id = node.Id, // 기존 NodeId -> Id
|
||||
Position = node.Position,
|
||||
CreatedDate = node.CreatedDate,
|
||||
ModifiedDate = node.ModifiedDate,
|
||||
|
||||
// Label 속성 매핑 (MapNode에서 임시로 가져오거나 Json Raw Parsing 필요할 수 있음)
|
||||
// 현재 MapNode 클래스에는 해당 속성들이 제거되었으므로,
|
||||
// Json 포맷 변경으로 인해 기존 데이터 로드시 정보 손실 가능성 있음.
|
||||
// * 중요 *: MapNode 클래스에서 속성을 지웠으므로 일반 Deserialize로는 Label/Image 속성을 못 읽음.
|
||||
// 해결책: JObject로 먼저 읽어서 분기 처리하거나, DTO 클래스를 별도로 두어야 함.
|
||||
// 하지만 시간 관계상, 만약 기존 MapNode가 속성을 가지고 있지 않다면 마이그레이션은 "위치/ID" 정도만 복구됨.
|
||||
// 완벽한 마이그레이션을 위해서는 MapNode에 Obsolete 속성을 잠시 두었어야 함.
|
||||
// 여기서는 일단 기본 정보라도 살림.
|
||||
};
|
||||
result.Labels.Add(label);
|
||||
}
|
||||
else if (node.Type == NodeType.Image)
|
||||
{
|
||||
var image = new MapImage
|
||||
{
|
||||
Id = node.Id,
|
||||
Position = node.Position,
|
||||
CreatedDate = node.CreatedDate,
|
||||
ModifiedDate = node.ModifiedDate,
|
||||
// 이미지/라벨 속성 복구 불가 (MapNode에서 삭제됨)
|
||||
};
|
||||
result.Images.Add(image);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Nodes.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 중복된 NodeId 정리 (Nav Node만)
|
||||
FixDuplicateNodeIds(result.Nodes);
|
||||
|
||||
// 고아 연결 정리
|
||||
CleanupOrphanConnections(result.Nodes);
|
||||
|
||||
// 양방향 연결 자동 설정
|
||||
EnsureBidirectionalConnections(result.Nodes);
|
||||
|
||||
// ConnectedMapNodes 채우기
|
||||
ResolveConnectedMapNodes(result.Nodes);
|
||||
|
||||
// 이미지 로드 (MapImage 객체에서)
|
||||
foreach (var img in result.Images)
|
||||
{
|
||||
img.LoadImage();
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ErrorMessage = "맵 데이터 파싱에 실패했습니다.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.ErrorMessage = $"맵 파일 로딩 중 오류 발생: {ex.Message}";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 데이터를 파일로 저장
|
||||
/// </summary>
|
||||
public static bool SaveMapToFile(string filePath, List<MapNode> nodes, List<MapLabel> labels = null, List<MapImage> images = null, List<MapMark> marks = null, List<MapMagnet> magnets = null, MapSettings settings = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 저장 전 고아 연결 정리
|
||||
CleanupOrphanConnections(nodes);
|
||||
|
||||
var mapData = new MapFileData
|
||||
{
|
||||
Nodes = nodes,
|
||||
Labels = labels ?? new List<MapLabel>(),
|
||||
Images = images ?? new List<MapImage>(),
|
||||
Marks = marks ?? new List<MapMark>(),
|
||||
Magnets = magnets ?? new List<MapMagnet>(),
|
||||
Settings = settings ?? new MapSettings(),
|
||||
CreatedDate = DateTime.Now,
|
||||
Version = "1.3"
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ConnectedMapNodes 채우기 (ConnectedNodes의 string ID → MapNode 객체 변환)
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void ResolveConnectedMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
// 빠른 조회를 위한 Dictionary 생성
|
||||
var nodeDict = mapNodes.ToDictionary(n => n.Id, n => n);
|
||||
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
// ConnectedMapNodes 초기화
|
||||
node.ConnectedMapNodes.Clear();
|
||||
|
||||
if (node.ConnectedNodes != null && node.ConnectedNodes.Count > 0)
|
||||
{
|
||||
foreach (var connectedNodeId in node.ConnectedNodes)
|
||||
{
|
||||
if (nodeDict.TryGetValue(connectedNodeId, out var connectedNode))
|
||||
{
|
||||
node.ConnectedMapNodes.Add(connectedNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 Description 데이터를 Name 필드로 마이그레이션
|
||||
/// JSON 파일에서 Description 필드가 있는 경우 Name으로 이동
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void MigrateDescriptionToName(List<MapNode> mapNodes)
|
||||
{
|
||||
// JSON에서 Description이 있던 기존 파일들을 위한 마이그레이션
|
||||
// 현재 MapNode 클래스에는 Description 속성이 제거되었으므로
|
||||
// 이 메서드는 호환성을 위해 유지되지만 실제로는 작동하지 않음
|
||||
// 기존 파일들은 다시 저장될 때 Description 없이 저장됨
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 중복된 NodeId를 가진 노드들을 고유한 NodeId로 수정
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void FixDuplicateNodeIds(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
var usedIds = new HashSet<string>();
|
||||
var duplicateNodes = new List<MapNode>();
|
||||
|
||||
// 첫 번째 패스: 중복된 노드들 식별
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (usedIds.Contains(node.Id))
|
||||
{
|
||||
duplicateNodes.Add(node);
|
||||
}
|
||||
else
|
||||
{
|
||||
usedIds.Add(node.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// 두 번째 패스: 중복된 노드들에게 새로운 NodeId 할당
|
||||
foreach (var duplicateNode in duplicateNodes)
|
||||
{
|
||||
string newNodeId = GenerateUniqueNodeId(usedIds);
|
||||
|
||||
// 다른 노드들의 연결에서 기존 NodeId를 새 NodeId로 업데이트
|
||||
UpdateConnections(mapNodes, duplicateNode.Id, newNodeId);
|
||||
|
||||
duplicateNode.Id = newNodeId;
|
||||
usedIds.Add(newNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사용되지 않는 고유한 NodeId 생성
|
||||
/// </summary>
|
||||
/// <param name="usedIds">이미 사용된 NodeId 목록</param>
|
||||
/// <returns>고유한 NodeId</returns>
|
||||
private static string GenerateUniqueNodeId(HashSet<string> usedIds)
|
||||
{
|
||||
int counter = 1;
|
||||
string nodeId;
|
||||
|
||||
do
|
||||
{
|
||||
nodeId = $"N{counter:D3}";
|
||||
counter++;
|
||||
}
|
||||
while (usedIds.Contains(nodeId));
|
||||
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 연결에서 NodeId 변경사항 반영
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
/// <param name="oldNodeId">기존 NodeId</param>
|
||||
/// <param name="newNodeId">새로운 NodeId</param>
|
||||
private static void UpdateConnections(List<MapNode> mapNodes, string oldNodeId, string newNodeId)
|
||||
{
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (node.ConnectedNodes != null)
|
||||
{
|
||||
for (int i = 0; i < node.ConnectedNodes.Count; i++)
|
||||
{
|
||||
if (node.ConnectedNodes[i] == oldNodeId)
|
||||
{
|
||||
node.ConnectedNodes[i] = newNodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 존재하지 않는 노드에 대한 연결을 정리합니다 (고아 연결 제거).
|
||||
/// 노드 삭제 후 저장된 맵 파일에서 삭제된 노드 ID가 ConnectedNodes에 남아있는 경우를 처리합니다.
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void CleanupOrphanConnections(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
// 존재하는 모든 노드 ID 집합 생성
|
||||
var validNodeIds = new HashSet<string>(mapNodes.Select(n => n.Id));
|
||||
|
||||
// 각 노드의 연결을 검증하고 존재하지 않는 노드 ID 제거
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (node.ConnectedNodes == null || node.ConnectedNodes.Count == 0)
|
||||
continue;
|
||||
|
||||
var orphanConnections = node.ConnectedNodes
|
||||
.Where(connectedId => !validNodeIds.Contains(connectedId))
|
||||
.ToList();
|
||||
|
||||
foreach (var orphanId in orphanConnections)
|
||||
{
|
||||
node.RemoveConnection(orphanId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [사용 중지됨] 중복 연결을 정리합니다. 양방향 중복 연결을 단일 연결로 통합합니다.
|
||||
/// 주의: 이 함수는 버그가 있어 사용 중지됨 - 양방향 연결을 단방향으로 변환하여 경로 탐색 실패 발생
|
||||
/// AGV 시스템에서는 모든 연결이 양방향이어야 하므로 EnsureBidirectionalConnections()만 사용
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
[Obsolete("이 함수는 양방향 연결을 단방향으로 변환하는 버그가 있습니다. 사용하지 마세요.")]
|
||||
private static void CleanupDuplicateConnections(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
var processedPairs = new HashSet<string>();
|
||||
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
var connectionsToRemove = new List<string>();
|
||||
|
||||
foreach (var connectedNodeId in node.ConnectedNodes.ToList())
|
||||
{
|
||||
var connectedNode = mapNodes.FirstOrDefault(n => n.Id == connectedNodeId);
|
||||
if (connectedNode == null) continue;
|
||||
|
||||
// 연결 쌍의 키 생성 (사전순 정렬)
|
||||
string pairKey = string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) < 0
|
||||
? $"{node.Id}-{connectedNodeId}"
|
||||
: $"{connectedNodeId}-{node.Id}";
|
||||
|
||||
if (processedPairs.Contains(pairKey))
|
||||
{
|
||||
// 이미 처리된 연결인 경우 중복으로 간주하고 제거
|
||||
connectionsToRemove.Add(connectedNodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 처리되지 않은 연결인 경우
|
||||
processedPairs.Add(pairKey);
|
||||
|
||||
// 양방향 연결인 경우 하나만 유지
|
||||
if (connectedNode.ConnectedNodes.Contains(node.Id))
|
||||
{
|
||||
// 사전순으로 더 작은 노드에만 연결을 유지
|
||||
if (string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) > 0)
|
||||
{
|
||||
connectionsToRemove.Add(connectedNodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 반대 방향 연결 제거
|
||||
connectedNode.RemoveConnection(node.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 연결 제거
|
||||
foreach (var connectionToRemove in connectionsToRemove)
|
||||
{
|
||||
node.RemoveConnection(connectionToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵의 모든 연결을 양방향으로 만듭니다.
|
||||
/// A→B 연결이 있으면 B→A 연결도 자동으로 추가합니다.
|
||||
/// GetNextNodeId() 메서드에서 현재 노드의 ConnectedNodes만으로 다음 노드를 찾을 수 있도록 하기 위함.
|
||||
///
|
||||
/// 예시:
|
||||
/// - 맵 에디터에서 002→003 연결을 생성했다면
|
||||
/// - 자동으로 003→002 연결도 추가됨
|
||||
/// - 따라서 003의 ConnectedNodes에 002가 포함됨
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void EnsureBidirectionalConnections(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
// 모든 노드의 연결 정보를 수집
|
||||
var allConnections = new Dictionary<string, HashSet<string>>();
|
||||
|
||||
// 1단계: 모든 명시적 연결 수집
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (!allConnections.ContainsKey(node.Id))
|
||||
{
|
||||
allConnections[node.Id] = new HashSet<string>();
|
||||
}
|
||||
|
||||
if (node.ConnectedNodes != null)
|
||||
{
|
||||
foreach (var connectedId in node.ConnectedNodes)
|
||||
{
|
||||
allConnections[node.Id].Add(connectedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2단계: 역방향 연결 추가
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (node.ConnectedNodes == null)
|
||||
{
|
||||
node.ConnectedNodes = new List<string>();
|
||||
}
|
||||
|
||||
// 이 노드를 연결하는 모든 노드 찾기
|
||||
foreach (var otherNodeId in allConnections.Keys)
|
||||
{
|
||||
if (otherNodeId == node.Id) continue;
|
||||
|
||||
// 다른 노드가 이 노드를 연결하고 있다면
|
||||
if (allConnections[otherNodeId].Contains(node.Id))
|
||||
{
|
||||
// 이 노드의 ConnectedNodes에 그 노드를 추가 (중복 방지)
|
||||
if (!node.ConnectedNodes.Contains(otherNodeId))
|
||||
{
|
||||
node.ConnectedNodes.Add(otherNodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MapNode 목록에서 RFID가 없는 노드들에 자동으로 RFID ID를 할당합니다.
|
||||
/// *** 에디터와 시뮬레이터 데이터 불일치 방지를 위해 비활성화됨 ***
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
[Obsolete("RFID 자동 할당은 에디터와 시뮬레이터 간 데이터 불일치를 야기하므로 사용하지 않음")]
|
||||
public static void AssignAutoRfidIds(List<MapNode> mapNodes)
|
||||
{
|
||||
// 에디터에서 설정한 RFID 값을 그대로 사용하기 위해 자동 할당 기능 비활성화
|
||||
// 에디터와 시뮬레이터 간 데이터 일관성 유지를 위함
|
||||
return;
|
||||
|
||||
/*
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
// 네비게이션 가능한 노드이면서 RFID가 없는 경우에만 자동 할당
|
||||
if (node.IsNavigationNode() && !node.HasRfid())
|
||||
{
|
||||
// 기본 RFID ID 생성 (N001 -> 001)
|
||||
var rfidId = node.NodeId.Replace("N", "").PadLeft(3, '0');
|
||||
|
||||
node.SetRfidInfo(rfidId, "", "정상");
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
72
AGVLogic/AGVNavigationCore/Models/MapMagnet.cs
Normal file
72
AGVLogic/AGVNavigationCore/Models/MapMagnet.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 상의 마그넷(Magnet) 정보를 나타내는 클래스
|
||||
/// </summary>
|
||||
public class MapMagnet : NodeBase
|
||||
{
|
||||
|
||||
public MapMagnet() {
|
||||
Type = NodeType.Magnet;
|
||||
}
|
||||
|
||||
[Category("위치 정보")]
|
||||
[Description("시작점 좌표")]
|
||||
public MagnetPoint P1 { get; set; } = new MagnetPoint();
|
||||
|
||||
[Category("위치 정보")]
|
||||
[Description("끝점 좌표")]
|
||||
public MagnetPoint P2 { get; set; } = new MagnetPoint();
|
||||
|
||||
[Category("위치 정보")]
|
||||
[Description("제어점 좌표 (곡선인 경우)")]
|
||||
public MagnetPoint ControlPoint { get; set; } = null;
|
||||
|
||||
public class MagnetPoint
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override Point Position
|
||||
{
|
||||
get => new Point((int)P1.X, (int)P1.Y);
|
||||
set
|
||||
{
|
||||
double dx = value.X - P1.X;
|
||||
double dy = value.Y - P1.Y;
|
||||
|
||||
P1.X += dx;
|
||||
P1.Y += dy;
|
||||
P2.X += dx;
|
||||
P2.Y += dy;
|
||||
|
||||
if (ControlPoint != null)
|
||||
{
|
||||
ControlPoint.X += dx;
|
||||
ControlPoint.Y += dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시작점 Point 반환
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
[JsonIgnore]
|
||||
public Point StartPoint => new Point((int)P1.X, (int)P1.Y);
|
||||
|
||||
/// <summary>
|
||||
/// 끝점 Point 반환
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
[JsonIgnore]
|
||||
public Point EndPoint => new Point((int)P2.X, (int)P2.Y);
|
||||
}
|
||||
}
|
||||
37
AGVLogic/AGVNavigationCore/Models/MapMark.cs
Normal file
37
AGVLogic/AGVNavigationCore/Models/MapMark.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 상의 마크(Mark) 정보를 나타내는 클래스
|
||||
/// </summary>
|
||||
public class MapMark : NodeBase
|
||||
{
|
||||
// Id is inherited from NodeBase
|
||||
public MapMark() {
|
||||
Type = NodeType.Mark;
|
||||
}
|
||||
|
||||
[Category("위치 정보")]
|
||||
[Description("마크의 X 좌표")]
|
||||
public double X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Point((int)value, Position.Y);
|
||||
}
|
||||
|
||||
[Category("위치 정보")]
|
||||
[Description("마크의 Y 좌표")]
|
||||
public double Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Point(Position.X, (int)value);
|
||||
}
|
||||
|
||||
[Category("위치 정보")]
|
||||
[Description("마크의 회전 각도")]
|
||||
public double Rotation { get; set; }
|
||||
}
|
||||
}
|
||||
185
AGVLogic/AGVNavigationCore/Models/MapNode.cs
Normal file
185
AGVLogic/AGVNavigationCore/Models/MapNode.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using AGVNavigationCore.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 노드 정보를 관리하는 클래스 (주행 경로용 노드)
|
||||
/// </summary>
|
||||
public class MapNode : NodeBase
|
||||
{
|
||||
|
||||
|
||||
[Category("라벨 설정")]
|
||||
[Description("표시할 텍스트입니다.")]
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
public StationType StationType { get; set; }
|
||||
|
||||
[Browsable(false)]
|
||||
public bool CanDocking
|
||||
{
|
||||
get
|
||||
{
|
||||
if (StationType == StationType.Buffer) return true;
|
||||
if (StationType == StationType.Loader) return true;
|
||||
if (StationType == StationType.UnLoader) return true;
|
||||
if (StationType == StationType.Clearner) return true;
|
||||
if (StationType == StationType.Charger1) return true;
|
||||
if (StationType == StationType.Charger2) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("노드 설정")]
|
||||
[Description("도킹/충전 노드의 진입 방향입니다.")]
|
||||
public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare;
|
||||
|
||||
|
||||
[Category("노드 설정")]
|
||||
[Description("각 연결된 노드로 향할 때의 마그넷 방향 정보입니다.")]
|
||||
public Dictionary<string, MagnetPosition> MagnetDirections { get; set; } = new Dictionary<string, MagnetPosition>();
|
||||
|
||||
[Category("연결 정보")]
|
||||
[Description("연결된 노드 ID 목록입니다.")]
|
||||
[ReadOnly(true)]
|
||||
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||
|
||||
[JsonIgnore]
|
||||
[Browsable(false)]
|
||||
public List<MapNode> ConnectedMapNodes { get; set; } = new List<MapNode>();
|
||||
|
||||
[Category("주행 설정")]
|
||||
[Description("제자리 회전(좌) 가능 여부입니다.")]
|
||||
public bool CanTurnLeft { get; set; } = true;
|
||||
|
||||
[Category("주행 설정")]
|
||||
[Description("제자리 회전(우) 가능 여부입니다.")]
|
||||
public bool CanTurnRight { get; set; } = true;
|
||||
|
||||
[Category("주행 설정")]
|
||||
[Description("교차로 주행 가능 여부입니다.")]
|
||||
public bool DisableCross
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Type != NodeType.Normal) return true;
|
||||
return _disablecross;
|
||||
}
|
||||
set { _disablecross = value; }
|
||||
}
|
||||
private bool _disablecross = false;
|
||||
|
||||
[Category("주행 설정")]
|
||||
[Description("노드 통과 시 제한 속도입니다.")]
|
||||
public SpeedLevel SpeedLimit { get; set; } = SpeedLevel.M;
|
||||
|
||||
[Category("노드 설정")]
|
||||
[Description("장비 ID 또는 별칭입니다.")]
|
||||
public string AliasName { get; set; } = string.Empty;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[Description("노드 사용 여부입니다.")]
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
[Category("RFID 정보")]
|
||||
[Description("물리적 RFID 태그 ID입니다.")]
|
||||
public UInt16 RfidId { get; set; } = 0;
|
||||
|
||||
|
||||
[Category("노드 텍스트"), DisplayName("TextColor")]
|
||||
[Description("텍스트 색상입니다.")]
|
||||
public Color NodeTextForeColor { get; set; } = Color.Black;
|
||||
|
||||
private float _textFontSize = 7.0f;
|
||||
[Category("노드 텍스트"), DisplayName("TextSize")]
|
||||
[Description("일반 노드 텍스트의 크기입니다.")]
|
||||
public float NodeTextFontSize
|
||||
{
|
||||
get => _textFontSize;
|
||||
set => _textFontSize = value > 0 ? value : 7.0f;
|
||||
}
|
||||
|
||||
public MapNode() : base()
|
||||
{
|
||||
Type = NodeType.Normal;
|
||||
}
|
||||
|
||||
public MapNode(string nodeId, Point position, StationType type) : base(nodeId, position)
|
||||
{
|
||||
Type = NodeType.Normal;
|
||||
}
|
||||
[Category("기본 정보")]
|
||||
[JsonIgnore]
|
||||
[ReadOnly(true), Browsable(false)]
|
||||
public bool isDockingNode
|
||||
{
|
||||
get
|
||||
{
|
||||
if (StationType == StationType.Charger1 || StationType == StationType.Charger2 || StationType == StationType.Buffer ||
|
||||
StationType == StationType.Clearner || StationType == StationType.Loader ||
|
||||
StationType == StationType.UnLoader) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddConnection(string nodeId)
|
||||
{
|
||||
if (!ConnectedNodes.Contains(nodeId))
|
||||
{
|
||||
ConnectedNodes.Add(nodeId);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveConnection(string nodeId)
|
||||
{
|
||||
if (ConnectedNodes.Remove(nodeId))
|
||||
{
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetChargingStation(string stationId)
|
||||
{
|
||||
//StationType = StationType.Charger;
|
||||
//Id = stationId;
|
||||
//DockDirection = DockingDirection.Forward;
|
||||
//ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"RFID:{RfidId}(NODE:{Id}): AS:{AliasName} ({Type}) at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID(*ID)
|
||||
/// </summary>
|
||||
public string ID2
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasRfid()) return $"{this.RfidId:0000}(*{this.Id})";
|
||||
else return $"(*{this.Id})";
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNavigationNode()
|
||||
{
|
||||
// 이제 MapNode는 항상 내비게이션 노드임 (Label, Image 분리됨)
|
||||
// 하지만 기존 로직 호환성을 위해 Active 체크만 유지
|
||||
return IsActive;
|
||||
}
|
||||
|
||||
public bool HasRfid()
|
||||
{
|
||||
return RfidId > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
AGVLogic/AGVNavigationCore/Models/NodeBase.cs
Normal file
60
AGVLogic/AGVNavigationCore/Models/NodeBase.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 상의 모든 객체의 최상위 기본 클래스
|
||||
/// 위치, 선택 상태, 기본 식별자 등을 관리
|
||||
/// </summary>
|
||||
public abstract class NodeBase
|
||||
{
|
||||
[Category("기본 정보")]
|
||||
[Description("객체의 고유 ID입니다.")]
|
||||
[ReadOnly(true)]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[Category("기본 정보")]
|
||||
public NodeType Type { protected set; get; } = NodeType.Normal;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[Description("객체의 좌표(X, Y)입니다.")]
|
||||
public virtual Point Position { get; set; } = Point.Empty;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[Description("객체 생성 일자입니다.")]
|
||||
[JsonIgnore]
|
||||
[ReadOnly(true), Browsable(false)]
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[Description("객체 수정 일자입니다.")]
|
||||
[JsonIgnore]
|
||||
[ReadOnly(true), Browsable(false)]
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
[Browsable(false)]
|
||||
[JsonIgnore]
|
||||
public bool IsSelected { get; set; } = false;
|
||||
|
||||
|
||||
[Browsable(false)]
|
||||
[JsonIgnore]
|
||||
public bool IsHovered { get; set; } = false;
|
||||
|
||||
|
||||
|
||||
public NodeBase()
|
||||
{
|
||||
}
|
||||
|
||||
public NodeBase(string id, Point position)
|
||||
{
|
||||
Id = id;
|
||||
Position = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
942
AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs
Normal file
942
AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs
Normal file
@@ -0,0 +1,942 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Controls;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 가상 AGV 클래스 (코어 비즈니스 로직)
|
||||
/// 실제 AGV와 시뮬레이터 모두에서 사용 가능한 공용 로직
|
||||
/// 시뮬레이션과 실제 동작이 동일하게 동작하도록 설계됨
|
||||
/// </summary>
|
||||
public class VirtualAGV : IMovableAGV, IAGV
|
||||
{
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 변경 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<AGVState> StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 위치 변경 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<(Point, AgvDirection, MapNode)> PositionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 감지 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<string> RfidDetected;
|
||||
|
||||
/// <summary>
|
||||
/// 경로 완료 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<AGVPathResult> PathCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 오류 발생 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<string> ErrorOccurred;
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
private string _agvId;
|
||||
private Point _currentPosition;
|
||||
private Point _prevPosition;
|
||||
private AgvDirection _currentDirection;
|
||||
private AgvDirection _prevDirection;
|
||||
private AGVState _currentState;
|
||||
private float _currentSpeed;
|
||||
|
||||
// 경로 관련
|
||||
private AGVPathResult _currentPath;
|
||||
private List<string> _remainingNodes;
|
||||
private int _currentNodeIndex;
|
||||
private MapNode _currentNode;
|
||||
private MapNode _prevNode;
|
||||
private AGVTurn _turn;
|
||||
|
||||
// 이동 관련
|
||||
private DateTime _lastUpdateTime;
|
||||
private Point _moveStartPosition;
|
||||
private Point _moveTargetPosition;
|
||||
|
||||
// 도킹 관련
|
||||
private DockingDirection _dockingDirection;
|
||||
|
||||
// 시뮬레이션 설정
|
||||
private readonly float _moveSpeed = 50.0f; // 픽셀/초
|
||||
private bool _isMoving;
|
||||
|
||||
// RFID 위치 추적 (실제 AGV용)
|
||||
private List<string> _detectedRfids = new List<string>(); // 감지된 RFID 목록
|
||||
private bool _isPositionConfirmed = false; // 위치 확정 여부 (RFID 2개 이상 감지)
|
||||
|
||||
// 에뮬레이터용 추가 속성
|
||||
public double Angle { get; set; } = 0; // 0 = Right, 90 = Down, 180 = Left, 270 = Up (Standard Math)
|
||||
// But AGV Direction: Forward usually means "Front of AGV".
|
||||
// Let's assume Angle is the orientation of the AGV in degrees.
|
||||
|
||||
public bool IsStopMarkOn { get; set; } = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public bool Turn180 { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 대상 이동시 모터 방향
|
||||
/// </summary>
|
||||
public AgvDirection PrevDirection => _prevDirection;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// AGV ID
|
||||
/// </summary>
|
||||
public string AgvId => _agvId;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치
|
||||
/// </summary>
|
||||
public Point CurrentPosition
|
||||
{
|
||||
get => _currentPosition;
|
||||
set => _currentPosition = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 방향
|
||||
/// 모터의 동작 방향
|
||||
/// </summary>
|
||||
public AgvDirection CurrentDirection
|
||||
{
|
||||
get => _currentDirection;
|
||||
set => _currentDirection = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 상태
|
||||
/// </summary>
|
||||
public AGVState CurrentState
|
||||
{
|
||||
get => _currentState;
|
||||
set => _currentState = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 속도
|
||||
/// </summary>
|
||||
public float CurrentSpeed => _currentSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 경로
|
||||
/// </summary>
|
||||
public AGVPathResult CurrentPath => _currentPath;
|
||||
|
||||
public void ClearPath()
|
||||
{
|
||||
_currentPath = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID
|
||||
/// </summary>
|
||||
public MapNode CurrentNode => _currentNode;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID (CurrentNode.Id)
|
||||
/// </summary>
|
||||
public string CurrentNodeId => _currentNode?.Id;
|
||||
|
||||
/// <summary>
|
||||
/// 현재노드의 RFID(id)값을 표시합니다 없는경우 (X)가 표시됩니다
|
||||
/// </summary>
|
||||
public string CurrentNodeID2
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_currentNode == null) return "(X)";
|
||||
return _currentNode.ID2;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이전 위치
|
||||
/// </summary>
|
||||
public Point? PrevPosition => _prevPosition;
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 레벨 (시뮬레이션)
|
||||
/// </summary>
|
||||
public float BatteryLevel { get; set; } = 100.0f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 이전 노드
|
||||
/// </summary>
|
||||
public MapNode PrevNode => _prevNode;
|
||||
|
||||
/// <summary>
|
||||
/// Turn 상태값
|
||||
/// </summary>
|
||||
public AGVTurn Turn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향
|
||||
/// </summary>
|
||||
public DockingDirection DockingDirection => _dockingDirection;
|
||||
|
||||
/// <summary>
|
||||
/// 위치 확정 여부 (RFID 2개 이상 감지 시 true)
|
||||
/// </summary>
|
||||
public bool IsPositionConfirmed => _isPositionConfirmed;
|
||||
|
||||
/// <summary>
|
||||
/// 감지된 RFID 개수
|
||||
/// </summary>
|
||||
public int DetectedRfidCount => _detectedRfids.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 부족 경고 임계값 (%)
|
||||
/// </summary>
|
||||
public float LowBatteryThreshold { get; set; } = 20.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;
|
||||
_dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹
|
||||
_currentNode = null;
|
||||
_prevNode = null;
|
||||
_isMoving = false;
|
||||
_lastUpdateTime = DateTime.Now;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 센서/RFID 입력 (실제 AGV에서 호출)
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 설정 (실제 AGV 센서에서)
|
||||
/// </summary>
|
||||
public void SetCurrentPosition(Point position)
|
||||
{
|
||||
_currentPosition = position;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 감지된 RFID 설정 (실제 RFID 센서에서)
|
||||
/// </summary>
|
||||
public void SetDetectedRfid(string rfidId)
|
||||
{
|
||||
// RFID 목록에 추가 (중복 제거)
|
||||
if (!_detectedRfids.Contains(rfidId))
|
||||
{
|
||||
_detectedRfids.Add(rfidId);
|
||||
}
|
||||
|
||||
// RFID 2개 이상 감지 시 위치 확정
|
||||
if (_detectedRfids.Count >= 2 && !_isPositionConfirmed)
|
||||
{
|
||||
_isPositionConfirmed = true;
|
||||
}
|
||||
|
||||
RfidDetected?.Invoke(this, rfidId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모터 방향 설정 (실제 모터 컨트롤러에서)
|
||||
/// </summary>
|
||||
public void SetMotorDirection(AgvDirection direction)
|
||||
{
|
||||
_currentDirection = direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 레벨 설정 (실제 BMS에서)
|
||||
/// </summary>
|
||||
public void SetBatteryLevel(float percentage)
|
||||
{
|
||||
BatteryLevel = Math.Max(0, Math.Min(100, percentage));
|
||||
|
||||
// 배터리 부족 경고
|
||||
if (BatteryLevel < LowBatteryThreshold && _currentState != AGVState.Charging)
|
||||
{
|
||||
OnError($"배터리 부족: {BatteryLevel:F1}% (기준: {LowBatteryThreshold}%)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드id의 개체를 IsPass 로 설정합니다
|
||||
/// </summary>
|
||||
public bool SetCurrentNodeMarkStop()
|
||||
{
|
||||
if (_currentNode == null) return false;
|
||||
if (_currentPath == null) return false;
|
||||
var 미완료된처음노드 = _currentPath.DetailedPath.Where(t => t.IsPass == false).OrderBy(t => t.seq).FirstOrDefault();
|
||||
if (미완료된처음노드 == null) return false;
|
||||
미완료된처음노드.IsPass = true;
|
||||
Console.WriteLine($"미완료된처음노드를 true러치합니다");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 동작 예측 (실제 AGV 제어용)
|
||||
/// AGV가 지속적으로 호출하여 현재 상태와 예측 상태를 일치시킴
|
||||
/// </summary>
|
||||
/// <returns>다음에 수행할 모터/마그넷/속도 명령</returns>
|
||||
public AGVCommand Predict()
|
||||
{
|
||||
|
||||
// 1. 위치 미확정 상태 (RFID 2개 미만 감지)
|
||||
if (!_isPositionConfirmed)
|
||||
{
|
||||
// 항상 전진 + 저속으로 이동 (RFID 감지 대기)
|
||||
return new AGVCommand(
|
||||
MotorCommand.Forward,
|
||||
MagnetPosition.S, // 직진
|
||||
SpeedLevel.L, // 저속
|
||||
eAGVCommandReason.UnknownPosition,
|
||||
$"위치 미확정 (RFID {_detectedRfids.Count}/2) - 전진하여 RFID 탐색"
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 위치 확정됨 + 경로 없음 → 정지 (목적지 미설정 상태)
|
||||
if (_currentPath == null || (_currentPath.DetailedPath?.Count ?? 0) < 1)
|
||||
{
|
||||
var curpos = "알수없음";
|
||||
if (_currentNode != null)
|
||||
{
|
||||
curpos = _currentNode.HasRfid() ? $"RFID #{_currentNode.RfidId} (*{_currentNode.Id})" : $"(*{_currentNode.Id})";
|
||||
}
|
||||
return new AGVCommand(
|
||||
MotorCommand.Stop,
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
eAGVCommandReason.NoPath,
|
||||
$"(목적지 미설정) - 현재={curpos}"
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 위치 확정됨 + 경로 있음 + 남은 노드 없음 → 정지 (목적지 도착)
|
||||
var lastNode = _currentPath.DetailedPath.Last();
|
||||
if (_currentPath.DetailedPath.Where(t => t.seq < lastNode.seq && t.IsPass == false).Any() == false)
|
||||
{
|
||||
// 마지막 노드에 도착했는지 확인 (현재 노드가 마지막 노드와 같은지) -
|
||||
// 모터방향오 같아야한다. 간혹 방향전환 후 MARK STOP하는경우가있다. 260127
|
||||
if (_currentNode != null && _currentNode.Id == lastNode.NodeId && lastNode.MotorDirection == CurrentDirection)
|
||||
{
|
||||
|
||||
if (lastNode.IsPass) //이미완료되었다.
|
||||
{
|
||||
return new AGVCommand(
|
||||
MotorCommand.Stop,
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
eAGVCommandReason.Complete,
|
||||
$"목적지 도착 - 최종:{CurrentNodeID2}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
//도킹노드라면 markstop 을 나머지는 바로 스탑한다.
|
||||
eAGVCommandReason reason = eAGVCommandReason.MarkStop;
|
||||
if (_targetnode.StationType == StationType.Normal || _targetnode.StationType == StationType.Limit)
|
||||
{
|
||||
//일반노드는 마크스탑포인트가 없으니 바로 종료되도록 한다
|
||||
reason = eAGVCommandReason.Complete;
|
||||
}
|
||||
|
||||
//마지막노드는 일혔지만 완료되지 않았다. 마크스탑필요
|
||||
return new AGVCommand(
|
||||
MotorCommand.Stop,
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
reason,
|
||||
$"목적지 도착 전(MarkStop) - 최종:{CurrentNodeID2}"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 경로이탈
|
||||
var TargetNode = _currentPath.DetailedPath.Where(t => t.IsPass == false && t.NodeId.Equals(_currentNode.Id)).FirstOrDefault();
|
||||
if (TargetNode == null)
|
||||
{
|
||||
return new AGVCommand(
|
||||
MotorCommand.Stop,
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
eAGVCommandReason.PathOut,
|
||||
$"(재탐색요청)경로이탈 현재위치:{CurrentNodeID2}"
|
||||
);
|
||||
}
|
||||
|
||||
return GetCommandFromPath(CurrentNode, "경로 실행 시작");
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 상태 조회
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 조회
|
||||
/// </summary>
|
||||
public Point GetCurrentPosition() => _currentPosition;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 상태 조회
|
||||
/// </summary>
|
||||
public AGVState GetCurrentState() => _currentState;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID 조회
|
||||
/// </summary>
|
||||
public MapNode GetCurrentNode() => _currentNode;
|
||||
|
||||
/// <summary>
|
||||
/// AGV 정보 조회
|
||||
/// </summary>
|
||||
public string GetStatus()
|
||||
{
|
||||
return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " +
|
||||
$"방향:{_currentDirection} 상태:{_currentState} " +
|
||||
$"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 경로 실행
|
||||
|
||||
/// <summary>
|
||||
/// 경로가 설정되어있는지?
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasPath()
|
||||
{
|
||||
if (_currentPath == null) return false;
|
||||
if (_currentPath.DetailedPath == null) return false;
|
||||
return _currentPath.DetailedPath.Any();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 설정 (실제 AGV 및 시뮬레이터에서 사용)
|
||||
/// </summary>
|
||||
/// <param name="path">실행할 경로</param>
|
||||
public void SetPath(AGVPathResult path)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
_currentPath = null;
|
||||
_remainingNodes.Clear();// = null;
|
||||
_currentNodeIndex = 0;
|
||||
OnError("경로가 null입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPath = path;
|
||||
_remainingNodes = path.Path.Select(n => n.Id).ToList(); // MapNode → NodeId 변환
|
||||
_currentNodeIndex = 0;
|
||||
|
||||
// 경로 시작 노드가 현재 노드와 다른 경우 경고
|
||||
if (_currentNode != null && _remainingNodes.Count > 0 && _remainingNodes[0] != _currentNode.Id)
|
||||
{
|
||||
OnError($"경로 시작 노드({_remainingNodes[0]})와 현재 노드({_currentNode.Id})가 다릅니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 정지
|
||||
/// </summary>
|
||||
public void StopPath()
|
||||
{
|
||||
_isMoving = false;
|
||||
_currentPath = null;
|
||||
_remainingNodes?.Clear();
|
||||
SetState(AGVState.Idle);
|
||||
_currentSpeed = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 긴급 정지
|
||||
/// </summary>
|
||||
public void EmergencyStop()
|
||||
{
|
||||
StopPath();
|
||||
OnError("긴급 정지가 실행되었습니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 일시 정지 (경로 유지)
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
_isMoving = false;
|
||||
_currentSpeed = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이동 재개
|
||||
/// </summary>
|
||||
public void Resume()
|
||||
{
|
||||
if (_currentPath != null && _remainingNodes != null && _remainingNodes.Count > 0)
|
||||
{
|
||||
_isMoving = true;
|
||||
SetState(AGVState.Moving);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 프레임 업데이트 (외부에서 정기적으로 호출)
|
||||
|
||||
/// <summary>
|
||||
/// 프레임 업데이트 (외부에서 주기적으로 호출)
|
||||
/// 이 방식으로 타이머에 의존하지 않고 외부에서 제어 가능
|
||||
/// </summary>
|
||||
/// <param name="deltaTimeMs">마지막 업데이트 이후 경과 시간 (밀리초)</param>
|
||||
public void Update(float deltaTimeMs)
|
||||
{
|
||||
var deltaTime = deltaTimeMs / 1000.0f; // 초 단위로 변환
|
||||
|
||||
UpdateMovement(deltaTime);
|
||||
UpdateBattery(deltaTime);
|
||||
|
||||
// 위치 변경 이벤트 발생
|
||||
PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 수동 제어 (테스트용)
|
||||
|
||||
/// <summary>
|
||||
/// 수동 이동 (테스트용)
|
||||
/// </summary>
|
||||
/// <param name="targetPosition">목표 위치</param>
|
||||
public void MoveTo(Point targetPosition)
|
||||
{
|
||||
_prevPosition = targetPosition;
|
||||
_moveStartPosition = _currentPosition;
|
||||
_moveTargetPosition = targetPosition;
|
||||
|
||||
SetState(AGVState.Moving);
|
||||
_isMoving = true;
|
||||
Turn = AGVTurn.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 수동 회전 (테스트용)
|
||||
/// </summary>
|
||||
/// <param name="direction">회전 방향</param>
|
||||
public void Rotate(AgvDirection direction)
|
||||
{
|
||||
if (_currentState != AGVState.Idle)
|
||||
return;
|
||||
|
||||
SetState(AGVState.Rotating);
|
||||
_currentDirection = direction;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - AGV 위치 설정 (시뮬레이터용)
|
||||
|
||||
/// <summary>
|
||||
/// AGV 위치 직접 설정
|
||||
/// PrevPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
|
||||
/// </summary>
|
||||
/// <param name="node">현재 노드</param>
|
||||
/// <param name="newPosition">새로운 위치</param>
|
||||
/// <param name="motorDirection">모터이동방향</param>
|
||||
public void SetPosition(MapNode node, AgvDirection motorDirection)
|
||||
{
|
||||
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
|
||||
if (_currentNode != null && _currentNode.Id != node.Id)
|
||||
{
|
||||
_prevPosition = _currentPosition; // 이전 위치
|
||||
_prevNode = _currentNode;
|
||||
_prevDirection = _currentDirection;
|
||||
}
|
||||
|
||||
////모터방향이 다르다면 적용한다
|
||||
//if (_currentDirection != motorDirection)
|
||||
//{
|
||||
// _prevDirection = motorDirection;
|
||||
//}
|
||||
|
||||
// 새로운 위치 설정
|
||||
_currentPosition = node.Position;
|
||||
_currentDirection = motorDirection;
|
||||
_currentNode = node;
|
||||
|
||||
// 🔥 노드 ID를 RFID로 간주하여 감지 목록에 추가 (시뮬레이터용)
|
||||
if (!string.IsNullOrEmpty(node.Id) && !_detectedRfids.Contains(node.Id))
|
||||
{
|
||||
_detectedRfids.Add(node.Id);
|
||||
}
|
||||
|
||||
// 🔥 RFID 2개 이상 감지 시 위치 확정
|
||||
if (_detectedRfids.Count >= 2 && !_isPositionConfirmed)
|
||||
{
|
||||
_isPositionConfirmed = true;
|
||||
}
|
||||
|
||||
//현재 경로값이 있는지 확인한다.
|
||||
if (CurrentPath != null && CurrentPath.DetailedPath != null && CurrentPath.DetailedPath.Any())
|
||||
{
|
||||
var item = CurrentPath.DetailedPath.FirstOrDefault(t => t.NodeId == node.Id && t.IsPass == false);
|
||||
if (item != null)
|
||||
{
|
||||
// [PathJump Check] 점프한 노드 개수 확인
|
||||
// 현재 노드(item)보다 이전인데 아직 IsPass가 안 된 노드의 개수
|
||||
int skippedCount = CurrentPath.DetailedPath.Count(t => t.seq < item.seq && t.IsPass == false);
|
||||
if (skippedCount > 2)
|
||||
{
|
||||
OnError($"PathJump: {skippedCount}개의 노드를 건너뛰었습니다. (허용: 2개, 현재노드: {node.Id})");
|
||||
return;
|
||||
}
|
||||
|
||||
//item.IsPass = true;
|
||||
//이전노드는 모두 지나친걸로 한다
|
||||
CurrentPath.DetailedPath.Where(t => t.seq < item.seq).ToList().ForEach(t => t.IsPass = true);
|
||||
}
|
||||
}
|
||||
|
||||
// 위치 변경 이벤트 발생
|
||||
PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 노드 ID를 RFID 값으로 변환 (NodeResolver 사용)
|
||||
/// </summary>
|
||||
public ushort GetRfidByNodeId(List<MapNode> _mapNodes, string nodeId)
|
||||
{
|
||||
var node = _mapNodes?.FirstOrDefault(n => n.Id == nodeId);
|
||||
if ((node?.HasRfid() ?? false) == false) return 0;
|
||||
return node.RfidId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// DetailedPath에서 노드 정보를 찾아 AGVCommand 생성
|
||||
/// </summary>
|
||||
private AGVCommand GetCommandFromPath(MapNode targetNode, string actionDescription)
|
||||
{
|
||||
// DetailedPath가 없으면 기본 명령 반환
|
||||
if (_currentPath == null || _currentPath.DetailedPath == null || _currentPath.DetailedPath.Count == 0)
|
||||
{
|
||||
// [Refactor] Predict와 일관성 유지: 경로가 없으면 정지
|
||||
return new AGVCommand(
|
||||
MotorCommand.Stop,
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
eAGVCommandReason.NoPath,
|
||||
$"{actionDescription} (DetailedPath 없음)"
|
||||
);
|
||||
}
|
||||
|
||||
// DetailedPath에서 targetNodeId에 해당하는 NodeMotorInfo 찾기
|
||||
// 지나가지 않은 경로를 찾는다
|
||||
var nodeInfo = _currentPath.DetailedPath.FirstOrDefault(n => n.NodeId == targetNode.Id && n.IsPass == false);
|
||||
|
||||
if (nodeInfo == null)
|
||||
{
|
||||
// 못 찾으면 기본 명령 반환
|
||||
var defaultMotor = _currentDirection == AgvDirection.Forward
|
||||
? MotorCommand.Forward
|
||||
: MotorCommand.Backward;
|
||||
|
||||
return new AGVCommand(
|
||||
defaultMotor,
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.M,
|
||||
eAGVCommandReason.NoTarget,
|
||||
$"{actionDescription} (노드 {targetNode.Id} 정보 없음)"
|
||||
);
|
||||
}
|
||||
|
||||
// MotorDirection → MotorCommand 변환
|
||||
MotorCommand motorCmd;
|
||||
switch (nodeInfo.MotorDirection)
|
||||
{
|
||||
case AgvDirection.Forward:
|
||||
motorCmd = MotorCommand.Forward;
|
||||
break;
|
||||
case AgvDirection.Backward:
|
||||
motorCmd = MotorCommand.Backward;
|
||||
break;
|
||||
default:
|
||||
motorCmd = MotorCommand.Stop;
|
||||
break;
|
||||
}
|
||||
|
||||
// MagnetDirection → MagnetPosition 변换
|
||||
MagnetPosition magnetPos;
|
||||
switch (nodeInfo.MagnetDirection)
|
||||
{
|
||||
case PathFinding.Planning.MagnetDirection.Left:
|
||||
magnetPos = MagnetPosition.L;
|
||||
break;
|
||||
case PathFinding.Planning.MagnetDirection.Right:
|
||||
magnetPos = MagnetPosition.R;
|
||||
break;
|
||||
case PathFinding.Planning.MagnetDirection.Straight:
|
||||
default:
|
||||
magnetPos = MagnetPosition.S;
|
||||
break;
|
||||
}
|
||||
|
||||
// [Speed Control] NodeMotorInfo에 설정된 속도 사용
|
||||
// 단, 회전 구간 등에서 안전을 위해 강제 감속이 필요한 경우 로직 추가 가능
|
||||
// 현재는 사용자 설정 우선
|
||||
SpeedLevel speed = nodeInfo.Speed;
|
||||
|
||||
// Optional: 회전 시 강제 감속 로직 (사용자 요청에 따라 주석 처리 또는 제거 가능)
|
||||
// if (nodeInfo.CanRotate || nodeInfo.IsDirectionChangePoint) speed = SpeedLevel.L;
|
||||
|
||||
return new AGVCommand(
|
||||
motorCmd,
|
||||
magnetPos,
|
||||
speed,
|
||||
eAGVCommandReason.Normal,
|
||||
$"{actionDescription} → {targetNode.Id} (Motor:{motorCmd}, Magnet:{magnetPos})"
|
||||
);
|
||||
}
|
||||
|
||||
private void StartMovement()
|
||||
{
|
||||
SetState(AGVState.Moving);
|
||||
_isMoving = true;
|
||||
_lastUpdateTime = DateTime.Now;
|
||||
}
|
||||
|
||||
private void UpdateMovement(float deltaTime)
|
||||
{
|
||||
if (_currentState != AGVState.Moving || !_isMoving)
|
||||
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);
|
||||
}
|
||||
|
||||
public MapNode StartNode { get; set; } = null;
|
||||
|
||||
private MapNode _targetnode = null;
|
||||
|
||||
/// <summary>
|
||||
/// 목적지를 설정합니다. 목적지가 변경되면 경로계산정보가 삭제 됩니다.
|
||||
/// </summary>
|
||||
public MapNode TargetNode
|
||||
{
|
||||
get
|
||||
{
|
||||
return _targetnode;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_targetnode != value)
|
||||
{
|
||||
_currentPath = null;
|
||||
_targetnode = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessNextNode()
|
||||
{
|
||||
if (_remainingNodes == null || _currentNodeIndex >= _remainingNodes.Count - 1)
|
||||
{
|
||||
// 경로 완료
|
||||
_isMoving = false;
|
||||
SetState(AGVState.Idle);
|
||||
PathCompleted?.Invoke(this, _currentPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 다음 노드로 이동
|
||||
_currentNodeIndex++;
|
||||
var nextNodeId = _remainingNodes[_currentNodeIndex];
|
||||
|
||||
// RFID 감지 시뮬레이션
|
||||
RfidDetected?.Invoke(this, $"RFID_{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()
|
||||
{
|
||||
StopPath();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Analysis
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 갈림길 분석 및 마그넷 센서 방향 계산 시스템
|
||||
/// </summary>
|
||||
public class JunctionAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// 갈림길 정보
|
||||
/// </summary>
|
||||
public class JunctionInfo
|
||||
{
|
||||
public string NodeId { get; set; }
|
||||
public List<string> ConnectedNodes { get; set; }
|
||||
public Dictionary<string, MagnetDirection> PathDirections { get; set; }
|
||||
public bool IsJunction => ConnectedNodes.Count > 2;
|
||||
|
||||
public JunctionInfo(string nodeId)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
ConnectedNodes = new List<string>();
|
||||
PathDirections = new Dictionary<string, MagnetDirection>();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (!IsJunction)
|
||||
return $"{NodeId}: 일반노드 ({ConnectedNodes.Count}연결)";
|
||||
|
||||
var paths = string.Join(", ", PathDirections.Select(p => $"{p.Key}({p.Value})"));
|
||||
return $"{NodeId}: 갈림길 - {paths}";
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<MapNode> _mapNodes;
|
||||
private readonly Dictionary<string, JunctionInfo> _junctions;
|
||||
|
||||
public JunctionAnalyzer(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_junctions = new Dictionary<string, JunctionInfo>();
|
||||
AnalyzeJunctions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 갈림길 분석
|
||||
/// </summary>
|
||||
private void AnalyzeJunctions()
|
||||
{
|
||||
foreach (var node in _mapNodes)
|
||||
{
|
||||
if (node.IsNavigationNode())
|
||||
{
|
||||
var junctionInfo = AnalyzeNode(node);
|
||||
_junctions[node.Id] = junctionInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 개별 노드의 갈림길 정보 분석
|
||||
/// </summary>
|
||||
private JunctionInfo AnalyzeNode(MapNode node)
|
||||
{
|
||||
var junction = new JunctionInfo(node.Id);
|
||||
|
||||
// 양방향 연결을 고려하여 모든 연결된 노드 찾기
|
||||
var connectedNodes = GetAllConnectedNodes(node);
|
||||
junction.ConnectedNodes = connectedNodes;
|
||||
|
||||
if (connectedNodes.Count > 2)
|
||||
{
|
||||
// 갈림길인 경우 각 방향별 마그넷 센서 방향 계산
|
||||
CalculateMagnetDirections(node, connectedNodes, junction);
|
||||
}
|
||||
|
||||
return junction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 양방향 연결을 고려한 모든 연결 노드 검색
|
||||
/// </summary>
|
||||
private List<string> GetAllConnectedNodes(MapNode node)
|
||||
{
|
||||
var connected = new HashSet<string>();
|
||||
|
||||
// 직접 연결된 노드들
|
||||
foreach (var connectedNode in node.ConnectedMapNodes)
|
||||
{
|
||||
if (connectedNode != null)
|
||||
{
|
||||
connected.Add(connectedNode.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// 역방향 연결된 노드들 (다른 노드에서 이 노드로 연결)
|
||||
foreach (var otherNode in _mapNodes)
|
||||
{
|
||||
if (otherNode.Id != node.Id && otherNode.ConnectedMapNodes.Any(n => n.Id == node.Id))
|
||||
{
|
||||
connected.Add(otherNode.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return connected.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길에서 각 방향별 마그넷 센서 방향 계산
|
||||
/// </summary>
|
||||
private void CalculateMagnetDirections(MapNode junctionNode, List<string> connectedNodes, JunctionInfo junction)
|
||||
{
|
||||
if (connectedNodes.Count < 3) return;
|
||||
|
||||
// 각 연결 노드의 각도 계산
|
||||
var nodeAngles = new List<(string NodeId, double Angle)>();
|
||||
|
||||
foreach (var connectedId in connectedNodes)
|
||||
{
|
||||
var connectedNode = _mapNodes.FirstOrDefault(n => n.Id == connectedId);
|
||||
if (connectedNode != null)
|
||||
{
|
||||
double angle = CalculateAngle(junctionNode.Position, connectedNode.Position);
|
||||
nodeAngles.Add((connectedId, angle));
|
||||
}
|
||||
}
|
||||
|
||||
// 각도순으로 정렬
|
||||
nodeAngles.Sort((a, b) => a.Angle.CompareTo(b.Angle));
|
||||
|
||||
// 마그넷 방향 할당
|
||||
AssignMagnetDirections(nodeAngles, junction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 점 사이의 각도 계산 (라디안)
|
||||
/// </summary>
|
||||
private double CalculateAngle(Point from, Point to)
|
||||
{
|
||||
double deltaX = to.X - from.X;
|
||||
double deltaY = to.Y - from.Y;
|
||||
double angle = Math.Atan2(deltaY, deltaX);
|
||||
|
||||
// 0~2π 범위로 정규화
|
||||
if (angle < 0)
|
||||
angle += 2 * Math.PI;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길에서 마그넷 센서 방향 할당
|
||||
/// </summary>
|
||||
private void AssignMagnetDirections(List<(string NodeId, double Angle)> sortedNodes, JunctionInfo junction)
|
||||
{
|
||||
int nodeCount = sortedNodes.Count;
|
||||
|
||||
for (int i = 0; i < nodeCount; i++)
|
||||
{
|
||||
string nodeId = sortedNodes[i].NodeId;
|
||||
MagnetDirection direction;
|
||||
|
||||
if (nodeCount == 3)
|
||||
{
|
||||
// 3갈래: 직진, 좌측, 우측
|
||||
switch (i)
|
||||
{
|
||||
case 0: direction = MagnetDirection.Straight; break;
|
||||
case 1: direction = MagnetDirection.Left; break;
|
||||
case 2: direction = MagnetDirection.Right; break;
|
||||
default: direction = MagnetDirection.Straight; break;
|
||||
}
|
||||
}
|
||||
else if (nodeCount == 4)
|
||||
{
|
||||
// 4갈래: 교차로
|
||||
switch (i)
|
||||
{
|
||||
case 0: direction = MagnetDirection.Straight; break;
|
||||
case 1: direction = MagnetDirection.Left; break;
|
||||
case 2: direction = MagnetDirection.Straight; break; // 반대편
|
||||
case 3: direction = MagnetDirection.Right; break;
|
||||
default: direction = MagnetDirection.Straight; break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 5갈래 이상: 각도 기반 배정
|
||||
double angleStep = 2 * Math.PI / nodeCount;
|
||||
double normalizedIndex = (double)i / nodeCount;
|
||||
|
||||
if (normalizedIndex < 0.33)
|
||||
direction = MagnetDirection.Left;
|
||||
else if (normalizedIndex < 0.67)
|
||||
direction = MagnetDirection.Straight;
|
||||
else
|
||||
direction = MagnetDirection.Right;
|
||||
}
|
||||
|
||||
junction.PathDirections[nodeId] = direction;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 경로에서 요구되는 마그넷 방향 계산
|
||||
/// </summary>
|
||||
/// <param name="fromNodeId">이전 노드 ID</param>
|
||||
/// <param name="currentNodeId">현재 노드 ID</param>
|
||||
/// <param name="toNodeId">목표 노드 ID</param>
|
||||
/// <param name="motorDirection">AGV 모터 방향 (Forward/Backward)</param>
|
||||
/// <returns>마그넷 방향 (모터 방향 고려)</returns>
|
||||
public MagnetDirection GetRequiredMagnetDirection(string fromNodeId, string currentNodeId, string toNodeId, AgvDirection motorDirection )
|
||||
{
|
||||
if (!_junctions.ContainsKey(currentNodeId))
|
||||
return MagnetDirection.Straight;
|
||||
|
||||
var junction = _junctions[currentNodeId];
|
||||
if (!junction.IsJunction)
|
||||
return MagnetDirection.Straight;
|
||||
|
||||
// 실제 각도 기반으로 마그넷 방향 계산
|
||||
var fromNode = _mapNodes.FirstOrDefault(n => n.Id == fromNodeId);
|
||||
var currentNode = _mapNodes.FirstOrDefault(n => n.Id == currentNodeId);
|
||||
var toNode = _mapNodes.FirstOrDefault(n => n.Id == toNodeId);
|
||||
|
||||
if (fromNode == null || currentNode == null || toNode == null)
|
||||
return MagnetDirection.Straight;
|
||||
|
||||
// 전진 방향(진행 방향) 계산
|
||||
double incomingAngle = CalculateAngle(fromNode.Position, currentNode.Position);
|
||||
|
||||
// 목표 방향 계산
|
||||
double outgoingAngle = CalculateAngle(currentNode.Position, toNode.Position);
|
||||
|
||||
// 각도 차이 계산 (전진 방향 기준)
|
||||
double angleDiff = outgoingAngle - incomingAngle;
|
||||
|
||||
// 각도를 -π ~ π 범위로 정규화
|
||||
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
|
||||
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
|
||||
|
||||
// 전진 방향 기준으로 마그넷 방향 결정
|
||||
// 각도 차이가 작으면 직진, 음수면 왼쪽, 양수면 오른쪽
|
||||
MagnetDirection baseMagnetDirection;
|
||||
if (Math.Abs(angleDiff) < Math.PI / 6) // 30도 이내는 직진
|
||||
baseMagnetDirection = MagnetDirection.Straight;
|
||||
else if (angleDiff < 0) // 음수면 왼쪽 회전
|
||||
baseMagnetDirection = MagnetDirection.Left;
|
||||
else // 양수면 오른쪽 회전
|
||||
baseMagnetDirection = MagnetDirection.Right;
|
||||
|
||||
// 후진 모터 방향일 경우 마그넷 방향 반대로 설정
|
||||
// Forward: Left/Right 그대로 사용
|
||||
// Backward: Left ↔ Right 반대로 사용
|
||||
if (motorDirection == AgvDirection.Backward)
|
||||
{
|
||||
if (baseMagnetDirection == MagnetDirection.Left)
|
||||
return MagnetDirection.Right;
|
||||
else if (baseMagnetDirection == MagnetDirection.Right)
|
||||
return MagnetDirection.Left;
|
||||
}
|
||||
|
||||
return baseMagnetDirection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 가능한 갈림길 검색
|
||||
/// </summary>
|
||||
public List<string> FindDirectionChangeJunctions(AgvDirection currentDirection, AgvDirection targetDirection)
|
||||
{
|
||||
var availableJunctions = new List<string>();
|
||||
|
||||
if (currentDirection == targetDirection)
|
||||
return availableJunctions;
|
||||
|
||||
foreach (var junction in _junctions.Values)
|
||||
{
|
||||
if (junction.IsJunction)
|
||||
{
|
||||
// 갈림길에서 방향 전환이 가능한지 확인
|
||||
// (실제로는 더 복잡한 로직이 필요하지만, 일단 모든 갈림길을 후보로 함)
|
||||
availableJunctions.Add(junction.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return availableJunctions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길 정보 반환
|
||||
/// </summary>
|
||||
public JunctionInfo GetJunctionInfo(string nodeId)
|
||||
{
|
||||
return _junctions.ContainsKey(nodeId) ? _junctions[nodeId] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 갈림길 목록 반환
|
||||
/// </summary>
|
||||
public List<JunctionInfo> GetAllJunctions()
|
||||
{
|
||||
return _junctions.Values.Where(j => j.IsJunction).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 디버깅용 갈림길 정보 출력
|
||||
/// </summary>
|
||||
public List<string> GetJunctionSummary()
|
||||
{
|
||||
var summary = new List<string>();
|
||||
|
||||
foreach (var junction in _junctions.Values.Where(j => j.IsJunction))
|
||||
{
|
||||
summary.Add(junction.ToString());
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
}
|
||||
314
AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs
Normal file
314
AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs
Normal file
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
using AGVNavigationCore.PathFinding.Validation;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 경로 계산 결과 (방향성 및 명령어 포함)
|
||||
/// </summary>
|
||||
public class AGVPathResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 찾기 성공 여부
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로 노드 목록 (시작 → 목적지 순서)
|
||||
/// </summary>
|
||||
public List<MapNode> Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AGV 명령어 목록 (이동 방향 시퀀스)
|
||||
/// </summary>
|
||||
public List<AgvDirection> Commands { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 거리
|
||||
/// </summary>
|
||||
public float TotalDistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계산 소요 시간 (밀리초)
|
||||
/// </summary>
|
||||
public long CalculationTimeMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 탐색된 노드 수
|
||||
/// </summary>
|
||||
public int ExploredNodeCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 탐색된 노드 수 (호환성용)
|
||||
/// </summary>
|
||||
public int ExploredNodes
|
||||
{
|
||||
get => ExploredNodeCount;
|
||||
set => ExploredNodeCount = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 (초)
|
||||
/// </summary>
|
||||
public float EstimatedTimeSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수
|
||||
/// </summary>
|
||||
public int RotationCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 검증 결과
|
||||
/// </summary>
|
||||
public DockingValidationResult DockingValidation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 상세 경로 정보 (NodeMotorInfo 목록)
|
||||
/// </summary>
|
||||
public List<NodeMotorInfo> DetailedPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계획 설명
|
||||
/// </summary>
|
||||
public string PlanDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 필요 여부
|
||||
/// </summary>
|
||||
public bool RequiredDirectionChange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 노드 ID
|
||||
/// </summary>
|
||||
public string DirectionChangeNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로계산시 사용했던 최초 이전 포인트 이전의 노드
|
||||
/// </summary>
|
||||
public MapNode PrevNode { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// PrevNode 에서 현재위치까지 이동한 모터의 방향값
|
||||
/// </summary>
|
||||
public AgvDirection PrevDirection { get; set; }
|
||||
|
||||
public MapNode Gateway { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public AGVPathResult()
|
||||
{
|
||||
Success = false;
|
||||
Path = new List<MapNode>();
|
||||
Commands = new List<AgvDirection>();
|
||||
DetailedPath = new List<NodeMotorInfo>();
|
||||
TotalDistance = 0;
|
||||
CalculationTimeMs = 0;
|
||||
ExploredNodes = 0;
|
||||
EstimatedTimeSeconds = 0;
|
||||
RotationCount = 0;
|
||||
Message = string.Empty;
|
||||
PlanDescription = string.Empty;
|
||||
RequiredDirectionChange = false;
|
||||
DirectionChangeNode = string.Empty;
|
||||
DockingValidation = DockingValidationResult.CreateNotRequired();
|
||||
PrevNode = null;
|
||||
PrevDirection = AgvDirection.Stop;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="path">경로</param>
|
||||
/// <param name="commands">AGV 명령어 목록</param>
|
||||
/// <param name="totalDistance">총 거리</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <returns>성공 결과</returns>
|
||||
public static AGVPathResult CreateSuccess(List<MapNode> path, List<AgvDirection> commands, float totalDistance, long calculationTimeMs)
|
||||
{
|
||||
var result = new AGVPathResult
|
||||
{
|
||||
Success = true,
|
||||
Path = new List<MapNode>(path),
|
||||
Commands = new List<AgvDirection>(commands),
|
||||
TotalDistance = totalDistance,
|
||||
CalculationTimeMs = calculationTimeMs
|
||||
};
|
||||
|
||||
result.CalculateMetrics();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">오류 메시지</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <param name="exploredNodes">탐색된 노드 수</param>
|
||||
/// <returns>실패 결과</returns>
|
||||
public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs = 0, int exploredNodes = 0)
|
||||
{
|
||||
return new AGVPathResult
|
||||
{
|
||||
Success = false,
|
||||
Message = errorMessage,
|
||||
CalculationTimeMs = calculationTimeMs,
|
||||
ExploredNodes = exploredNodes
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 경로 메트릭 계산
|
||||
/// </summary>
|
||||
private void CalculateMetrics()
|
||||
{
|
||||
RotationCount = CountRotations();
|
||||
EstimatedTimeSeconds = CalculateEstimatedTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수 계산
|
||||
/// </summary>
|
||||
private int CountRotations()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
if (command == AgvDirection.Left || command == AgvDirection.Right)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 계산
|
||||
/// </summary>
|
||||
/// <param name="agvSpeed">AGV 속도 (픽셀/초, 기본값: 100)</param>
|
||||
/// <param name="rotationTime">회전 시간 (초, 기본값: 3)</param>
|
||||
/// <returns>예상 소요 시간 (초)</returns>
|
||||
private float CalculateEstimatedTime(float agvSpeed = 100.0f, float rotationTime = 3.0f)
|
||||
{
|
||||
float moveTime = TotalDistance / agvSpeed;
|
||||
float totalRotationTime = RotationCount * rotationTime;
|
||||
return moveTime + totalRotationTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 명령어 요약 생성
|
||||
/// </summary>
|
||||
/// <returns>명령어 요약 문자열</returns>
|
||||
public string GetCommandSummary()
|
||||
{
|
||||
if (!Success) return "실패";
|
||||
|
||||
var summary = new List<string>();
|
||||
var currentCommand = AgvDirection.Stop;
|
||||
var count = 0;
|
||||
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
if (command == currentCommand)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
summary.Add($"{GetCommandText(currentCommand)}×{count}");
|
||||
}
|
||||
currentCommand = command;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
summary.Add($"{GetCommandText(currentCommand)}×{count}");
|
||||
}
|
||||
|
||||
return string.Join(" → ", summary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 명령어 텍스트 반환
|
||||
/// </summary>
|
||||
private string GetCommandText(AgvDirection command)
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case AgvDirection.Forward: return "전진";
|
||||
case AgvDirection.Backward: return "후진";
|
||||
case AgvDirection.Left: return "좌회전";
|
||||
case AgvDirection.Right: return "우회전";
|
||||
case AgvDirection.Stop: return "정지";
|
||||
default: return command.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로의 노드 정보를 포함
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string GetDetailedPathInfo(bool shortmessage = false)
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return $"경로 계산 실패: {Message} (계산시간: {CalculationTimeMs}ms)";
|
||||
}
|
||||
|
||||
var data = DetailedPath.Select(t =>
|
||||
{
|
||||
if (shortmessage)
|
||||
return $"{t.RfidId:00}{t.MotorDirection.ToString().Substring(0, 1)}{t.MagnetDirection.ToString().Substring(0, 1)}";
|
||||
else
|
||||
return $"{t.RfidId}[{t.NodeId}] {t.MotorDirection.ToString().Substring(0, 1)}-{t.MagnetDirection.ToString().Substring(0, 1)}";
|
||||
});
|
||||
return string.Join(" → ", data);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 단순 경로 목록 반환 (호환성용 - 노드 ID 문자열 목록)
|
||||
/// </summary>
|
||||
/// <returns>노드 ID 목록</returns>
|
||||
public List<string> GetSimplePath()
|
||||
{
|
||||
if (DetailedPath != null && DetailedPath.Count > 0)
|
||||
{
|
||||
return DetailedPath.Select(n => n.NodeId).ToList();
|
||||
}
|
||||
return Path?.Select(n => n.Id).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
if (Success)
|
||||
{
|
||||
return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Failed: {Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
622
AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs
Normal file
622
AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs
Normal file
@@ -0,0 +1,622 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A* 알고리즘 기반 경로 탐색기
|
||||
/// </summary>
|
||||
public class AStarPathfinder
|
||||
{
|
||||
private Dictionary<string, PathNode> _nodeMap;
|
||||
private List<MapNode> _mapNodes;
|
||||
private Dictionary<string, MapNode> _mapNodeLookup; // Quick lookup for node ID -> MapNode
|
||||
|
||||
/// <summary>
|
||||
/// 휴리스틱 가중치 (기본값: 1.0)
|
||||
/// 값이 클수록 목적지 방향을 우선시하나 최적 경로를 놓칠 수 있음
|
||||
/// </summary>
|
||||
public float HeuristicWeight { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 최대 탐색 노드 수 (무한 루프 방지)
|
||||
/// </summary>
|
||||
public int MaxSearchNodes { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
public AStarPathfinder()
|
||||
{
|
||||
_nodeMap = new Dictionary<string, PathNode>();
|
||||
_mapNodes = new List<MapNode>();
|
||||
_mapNodeLookup = new Dictionary<string, MapNode>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 노드 설정
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public void SetMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_nodeMap.Clear();
|
||||
_mapNodeLookup.Clear();
|
||||
|
||||
// 모든 네비게이션 노드를 PathNode로 변환하고 양방향 연결 생성
|
||||
foreach (var mapNode in _mapNodes)
|
||||
{
|
||||
_mapNodeLookup[mapNode.Id] = mapNode; // Add to lookup table
|
||||
|
||||
if (mapNode.IsNavigationNode())
|
||||
{
|
||||
var pathNode = new PathNode(mapNode.Id, mapNode.Position);
|
||||
_nodeMap[mapNode.Id] = pathNode;
|
||||
}
|
||||
}
|
||||
|
||||
// 단일 연결을 양방향으로 확장
|
||||
foreach (var mapNode in _mapNodes)
|
||||
{
|
||||
if (mapNode.IsNavigationNode() && _nodeMap.ContainsKey(mapNode.Id))
|
||||
{
|
||||
var pathNode = _nodeMap[mapNode.Id];
|
||||
|
||||
foreach (var connectedNode in mapNode.ConnectedMapNodes)
|
||||
{
|
||||
if (connectedNode != null && _nodeMap.ContainsKey(connectedNode.Id))
|
||||
{
|
||||
// 양방향 연결 생성 (단일 연결이 양방향을 의미)
|
||||
if (!pathNode.ConnectedNodes.Contains(connectedNode.Id))
|
||||
{
|
||||
pathNode.ConnectedNodes.Add(connectedNode.Id);
|
||||
}
|
||||
|
||||
var connectedPathNode = _nodeMap[connectedNode.Id];
|
||||
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.Id))
|
||||
{
|
||||
connectedPathNode.ConnectedNodes.Add(mapNode.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 ID로 MapNode 가져오기 (헬퍼 메서드)
|
||||
/// </summary>
|
||||
private MapNode GetMapNode(string nodeId)
|
||||
{
|
||||
return _mapNodeLookup.ContainsKey(nodeId) ? _mapNodeLookup[nodeId] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 찾기 (A* 알고리즘)
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <returns>경로 계산 결과</returns>
|
||||
public AGVPathResult FindPathAStar(MapNode start, MapNode end)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
if (!_nodeMap.ContainsKey(start.Id))
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {start.Id}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
|
||||
if (!_nodeMap.ContainsKey(end.Id))
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {end.Id}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
|
||||
//출발지와 목적지가 동일한 경우
|
||||
if (start.Id == end.Id)
|
||||
{
|
||||
//var startMapNode = GetMapNode(start);
|
||||
var singlePath = new List<MapNode> { start };
|
||||
return AGVPathResult.CreateSuccess(singlePath, new List<AgvDirection>(), 0, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
var startNode = _nodeMap[start.Id];
|
||||
var endNode = _nodeMap[end.Id];
|
||||
var openSet = new List<PathNode>();
|
||||
var closedSet = new HashSet<string>();
|
||||
var exploredCount = 0;
|
||||
|
||||
startNode.GCost = 0;
|
||||
startNode.HCost = CalculateHeuristic(startNode, endNode);
|
||||
startNode.Parent = null;
|
||||
openSet.Add(startNode);
|
||||
|
||||
while (openSet.Count > 0 && exploredCount < MaxSearchNodes)
|
||||
{
|
||||
var currentNode = GetLowestFCostNode(openSet);
|
||||
openSet.Remove(currentNode);
|
||||
closedSet.Add(currentNode.NodeId);
|
||||
exploredCount++;
|
||||
|
||||
if (currentNode.NodeId == end.Id)
|
||||
{
|
||||
var path = ReconstructPath(currentNode);
|
||||
var totalDistance = CalculatePathDistance(path);
|
||||
return AGVPathResult.CreateSuccess(path, new List<AgvDirection>(), totalDistance, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
foreach (var neighborId in currentNode.ConnectedNodes)
|
||||
{
|
||||
if (closedSet.Contains(neighborId) || !_nodeMap.ContainsKey(neighborId))
|
||||
continue;
|
||||
|
||||
var neighbor = _nodeMap[neighborId];
|
||||
var tentativeGCost = currentNode.GCost + currentNode.DistanceTo(neighbor);
|
||||
|
||||
if (!openSet.Contains(neighbor))
|
||||
{
|
||||
neighbor.Parent = currentNode;
|
||||
neighbor.GCost = tentativeGCost;
|
||||
neighbor.HCost = CalculateHeuristic(neighbor, endNode);
|
||||
openSet.Add(neighbor);
|
||||
}
|
||||
else if (tentativeGCost < neighbor.GCost)
|
||||
{
|
||||
neighbor.Parent = currentNode;
|
||||
neighbor.GCost = tentativeGCost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AGVPathResult.CreateFailure("경로를 찾을 수 없습니다", stopwatch.ElapsedMilliseconds, exploredCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 경유지를 거쳐 경로 찾기 (오버로드)
|
||||
///// 여러 경유지를 순차적으로 거쳐서 최종 목적지까지의 경로를 계산합니다.
|
||||
///// 기존 FindPath를 여러 번 호출하여 각 구간의 경로를 합칩니다.
|
||||
///// </summary>
|
||||
///// <param name="startNodeId">시작 노드 ID</param>
|
||||
///// <param name="endNodeId">최종 목적지 노드 ID</param>
|
||||
///// <param name="waypointNodeIds">경유지 노드 ID 배열 (선택사항)</param>
|
||||
///// <returns>경로 계산 결과 (모든 경유지를 거친 전체 경로)</returns>
|
||||
//public AGVPathResult FindPath(string startNodeId, string endNodeId, params string[] waypointNodeIds)
|
||||
//{
|
||||
// var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
// try
|
||||
// {
|
||||
// // 경유지가 없으면 기본 FindPath 호출
|
||||
// if (waypointNodeIds == null || waypointNodeIds.Length == 0)
|
||||
// {
|
||||
// return FindPathAStar(startNodeId, endNodeId);
|
||||
// }
|
||||
|
||||
// // 경유지 유효성 검증
|
||||
// var validWaypoints = new List<string>();
|
||||
// foreach (var waypointId in waypointNodeIds)
|
||||
// {
|
||||
// if (string.IsNullOrEmpty(waypointId))
|
||||
// continue;
|
||||
|
||||
// if (!_nodeMap.ContainsKey(waypointId))
|
||||
// {
|
||||
// return AGVPathResult.CreateFailure($"경유지 노드를 찾을 수 없습니다: {waypointId}", stopwatch.ElapsedMilliseconds, 0);
|
||||
// }
|
||||
|
||||
// validWaypoints.Add(waypointId);
|
||||
// }
|
||||
|
||||
// // 경유지가 없으면 기본 경로 계산
|
||||
// if (validWaypoints.Count == 0)
|
||||
// {
|
||||
// return FindPathAStar(startNodeId, endNodeId);
|
||||
// }
|
||||
|
||||
// // 첫 번째 경유지가 시작노드와 같은지 검사
|
||||
// if (validWaypoints[0] == startNodeId)
|
||||
// {
|
||||
// return AGVPathResult.CreateFailure(
|
||||
// $"첫 번째 경유지({validWaypoints[0]})가 시작 노드({startNodeId})와 동일합니다. 경유지는 시작노드와 달라야 합니다.",
|
||||
// stopwatch.ElapsedMilliseconds, 0);
|
||||
// }
|
||||
|
||||
// // 마지막 경유지가 목적지노드와 같은지 검사
|
||||
// if (validWaypoints[validWaypoints.Count - 1] == endNodeId)
|
||||
// {
|
||||
// return AGVPathResult.CreateFailure(
|
||||
// $"마지막 경유지({validWaypoints[validWaypoints.Count - 1]})가 목적지 노드({endNodeId})와 동일합니다. 경유지는 목적지노드와 달라야 합니다.",
|
||||
// stopwatch.ElapsedMilliseconds, 0);
|
||||
// }
|
||||
|
||||
// // 연속된 중복만 제거 (순서 유지)
|
||||
// // 예: [1, 2, 2, 3, 2] -> [1, 2, 3, 2] (연속 중복만 제거)
|
||||
// var deduplicatedWaypoints = new List<string>();
|
||||
// string lastWaypoint = null;
|
||||
// foreach (var waypoint in validWaypoints)
|
||||
// {
|
||||
// if (waypoint != lastWaypoint)
|
||||
// {
|
||||
// deduplicatedWaypoints.Add(waypoint);
|
||||
// lastWaypoint = waypoint;
|
||||
// }
|
||||
// }
|
||||
// validWaypoints = deduplicatedWaypoints;
|
||||
|
||||
// // 최종 경로 리스트와 누적 값
|
||||
// var combinedPath = new List<MapNode>();
|
||||
// float totalDistance = 0;
|
||||
// long totalCalculationTime = 0;
|
||||
|
||||
// // 현재 시작점
|
||||
// string currentStart = startNodeId;
|
||||
|
||||
// // 1단계: 각 경유지까지의 경로 계산
|
||||
// for (int i = 0; i < validWaypoints.Count; i++)
|
||||
// {
|
||||
// string waypoint = validWaypoints[i];
|
||||
|
||||
// // 현재 위치에서 경유지까지의 경로 계산
|
||||
// var segmentResult = FindPathAStar(currentStart, waypoint);
|
||||
|
||||
// if (!segmentResult.Success)
|
||||
// {
|
||||
// return AGVPathResult.CreateFailure(
|
||||
// $"경유지 {i + 1}({waypoint})까지의 경로 계산 실패: {segmentResult.ErrorMessage}",
|
||||
// stopwatch.ElapsedMilliseconds, 0);
|
||||
// }
|
||||
|
||||
// // 경로 합치기 (첫 번째 구간이 아니면 시작점 제거하여 중복 방지)
|
||||
// if (combinedPath.Count > 0 && segmentResult.Path.Count > 0)
|
||||
// {
|
||||
// // 시작 노드 제거 (이전 경로의 마지막 노드와 동일)
|
||||
// combinedPath.AddRange(segmentResult.Path.Skip(1));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// combinedPath.AddRange(segmentResult.Path);
|
||||
// }
|
||||
|
||||
// totalDistance += segmentResult.TotalDistance;
|
||||
// totalCalculationTime += segmentResult.CalculationTimeMs;
|
||||
|
||||
// // 다음 경유지의 시작점은 현재 경유지
|
||||
// currentStart = waypoint;
|
||||
// }
|
||||
|
||||
// // 2단계: 마지막 경유지에서 최종 목적지까지의 경로 계산
|
||||
// var finalSegmentResult = FindPathAStar(currentStart, endNodeId);
|
||||
|
||||
// if (!finalSegmentResult.Success)
|
||||
// {
|
||||
// return AGVPathResult.CreateFailure(
|
||||
// $"최종 목적지까지의 경로 계산 실패: {finalSegmentResult.ErrorMessage}",
|
||||
// stopwatch.ElapsedMilliseconds, 0);
|
||||
// }
|
||||
|
||||
// // 최종 경로 합치기 (시작점 제거)
|
||||
// if (combinedPath.Count > 0 && finalSegmentResult.Path.Count > 0)
|
||||
// {
|
||||
// combinedPath.AddRange(finalSegmentResult.Path.Skip(1));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// combinedPath.AddRange(finalSegmentResult.Path);
|
||||
// }
|
||||
|
||||
// totalDistance += finalSegmentResult.TotalDistance;
|
||||
// totalCalculationTime += finalSegmentResult.CalculationTimeMs;
|
||||
|
||||
// stopwatch.Stop();
|
||||
|
||||
// // 결과 생성
|
||||
// return AGVPathResult.CreateSuccess(
|
||||
// combinedPath,
|
||||
// new List<AgvDirection>(),
|
||||
// totalDistance,
|
||||
// totalCalculationTime
|
||||
// );
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// return AGVPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 두 경로 결과를 합치기
|
||||
/// 이전 경로의 마지막 노드와 현재 경로의 시작 노드가 같으면 시작 노드를 제거하고 합침
|
||||
/// </summary>
|
||||
/// <param name="previousResult">이전 경로 결과</param>
|
||||
/// <param name="currentResult">현재 경로 결과</param>
|
||||
/// <returns>합쳐진 경로 결과</returns>
|
||||
public AGVPathResult CombineResults( AGVPathResult previousResult, AGVPathResult currentResult)
|
||||
{
|
||||
// 입력 검증
|
||||
if (previousResult == null)
|
||||
return currentResult;
|
||||
|
||||
if (currentResult == null)
|
||||
return previousResult;
|
||||
|
||||
if (!previousResult.Success)
|
||||
return AGVPathResult.CreateFailure(
|
||||
$"이전 경로 결과 실패: {previousResult.Message}",
|
||||
previousResult.CalculationTimeMs);
|
||||
|
||||
if (!currentResult.Success)
|
||||
return AGVPathResult.CreateFailure(
|
||||
$"현재 경로 결과 실패: {currentResult.Message}",
|
||||
currentResult.CalculationTimeMs);
|
||||
|
||||
// 경로가 비어있는 경우 처리
|
||||
if (previousResult.Path == null || previousResult.Path.Count == 0)
|
||||
return currentResult;
|
||||
|
||||
if (currentResult.Path == null || currentResult.Path.Count == 0)
|
||||
return previousResult;
|
||||
|
||||
// 합친 경로 생성
|
||||
var combinedPath = new List<MapNode>(previousResult.Path);
|
||||
var combinedCommands = new List<AgvDirection>(previousResult.Commands);
|
||||
var combinedDetailedPath = new List<NodeMotorInfo>(previousResult.DetailedPath ?? new List<NodeMotorInfo>());
|
||||
|
||||
// 이전 경로의 마지막 노드와 현재 경로의 시작 노드 비교
|
||||
string lastNodeOfPrevious = previousResult.Path[previousResult.Path.Count - 1].Id;
|
||||
string firstNodeOfCurrent = currentResult.Path[0].Id;
|
||||
|
||||
if (lastNodeOfPrevious == firstNodeOfCurrent)
|
||||
{
|
||||
// 첫 번째 노드 제거 (중복 제거)
|
||||
combinedPath.RemoveAt(combinedPath.Count - 1);
|
||||
combinedPath.AddRange(currentResult.Path);
|
||||
|
||||
// DetailedPath도 첫 번째 노드 제거
|
||||
if (currentResult.DetailedPath != null && currentResult.DetailedPath.Count > 0)
|
||||
{
|
||||
combinedDetailedPath.RemoveAt(combinedDetailedPath.Count - 1);
|
||||
combinedDetailedPath.AddRange(currentResult.DetailedPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 그대로 붙임
|
||||
combinedPath.AddRange(currentResult.Path);
|
||||
|
||||
// DetailedPath도 그대로 붙임
|
||||
if (currentResult.DetailedPath != null && currentResult.DetailedPath.Count > 0)
|
||||
{
|
||||
combinedDetailedPath.AddRange(currentResult.DetailedPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 명령어 합치기
|
||||
combinedCommands.AddRange(currentResult.Commands);
|
||||
|
||||
// 총 거리 합산
|
||||
float combinedDistance = previousResult.TotalDistance + currentResult.TotalDistance;
|
||||
|
||||
// 계산 시간 합산
|
||||
long combinedCalculationTime = previousResult.CalculationTimeMs + currentResult.CalculationTimeMs;
|
||||
|
||||
// 합쳐진 결과 생성
|
||||
var result = AGVPathResult.CreateSuccess(
|
||||
combinedPath,
|
||||
combinedCommands,
|
||||
combinedDistance,
|
||||
combinedCalculationTime
|
||||
);
|
||||
|
||||
// DetailedPath 설정
|
||||
result.DetailedPath = combinedDetailedPath;
|
||||
result.PrevNode = previousResult.PrevNode;
|
||||
result.PrevDirection = previousResult.PrevDirection;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
///// <summary>
|
||||
///// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
|
||||
///// </summary>
|
||||
///// <param name="startNodeId">시작 노드 ID</param>
|
||||
///// <param name="targetNodeIds">목적지 후보 노드 ID 목록</param>
|
||||
///// <returns>경로 계산 결과</returns>
|
||||
//public AGVPathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
|
||||
//{
|
||||
// if (targetNodeIds == null || targetNodeIds.Count == 0)
|
||||
// {
|
||||
// return AGVPathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0);
|
||||
// }
|
||||
|
||||
// AGVPathResult bestResult = null;
|
||||
// foreach (var targetId in targetNodeIds)
|
||||
// {
|
||||
// var result = FindPathAStar(startNodeId, targetId);
|
||||
// if (result.Success && (bestResult == null || result.TotalDistance < bestResult.TotalDistance))
|
||||
// {
|
||||
// bestResult = result;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return bestResult ?? AGVPathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0);
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 휴리스틱 거리 계산 (유클리드 거리)
|
||||
/// </summary>
|
||||
private float CalculateHeuristic(PathNode from, PathNode to)
|
||||
{
|
||||
return from.DistanceTo(to) * HeuristicWeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F cost가 가장 낮은 노드 선택
|
||||
/// </summary>
|
||||
private PathNode GetLowestFCostNode(List<PathNode> nodes)
|
||||
{
|
||||
PathNode lowest = nodes[0];
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.FCost < lowest.FCost ||
|
||||
(Math.Abs(node.FCost - lowest.FCost) < 0.001f && node.HCost < lowest.HCost))
|
||||
{
|
||||
lowest = node;
|
||||
}
|
||||
}
|
||||
return lowest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 재구성 (부모 노드를 따라 역추적)
|
||||
/// </summary>
|
||||
private List<MapNode> ReconstructPath(PathNode endNode)
|
||||
{
|
||||
var path = new List<MapNode>();
|
||||
var current = endNode;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
var mapNode = GetMapNode(current.NodeId);
|
||||
if (mapNode != null)
|
||||
{
|
||||
path.Add(mapNode);
|
||||
}
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로의 총 거리 계산
|
||||
/// </summary>
|
||||
private float CalculatePathDistance(List<MapNode> path)
|
||||
{
|
||||
if (path.Count < 2) return 0;
|
||||
|
||||
float totalDistance = 0;
|
||||
for (int i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
var nodeId1 = path[i].Id;
|
||||
var nodeId2 = path[i + 1].Id;
|
||||
|
||||
if (_nodeMap.ContainsKey(nodeId1) && _nodeMap.ContainsKey(nodeId2))
|
||||
{
|
||||
totalDistance += _nodeMap[nodeId1].DistanceTo(_nodeMap[nodeId2]);
|
||||
}
|
||||
}
|
||||
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 노드가 연결되어 있는지 확인
|
||||
/// </summary>
|
||||
/// <param name="nodeId1">노드 1 ID</param>
|
||||
/// <param name="nodeId2">노드 2 ID</param>
|
||||
/// <returns>연결 여부</returns>
|
||||
public bool AreNodesConnected(string nodeId1, string nodeId2)
|
||||
{
|
||||
if (!_nodeMap.ContainsKey(nodeId1) || !_nodeMap.ContainsKey(nodeId2))
|
||||
return false;
|
||||
|
||||
return _nodeMap[nodeId1].ConnectedNodes.Contains(nodeId2);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 네비게이션 가능한 노드 목록 반환
|
||||
/// </summary>
|
||||
/// <returns>노드 ID 목록</returns>
|
||||
public List<string> GetNavigationNodes()
|
||||
{
|
||||
return _nodeMap.Keys.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 정보 반환
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <returns>노드 정보 또는 null</returns>
|
||||
public PathNode GetNode(string nodeId)
|
||||
{
|
||||
return _nodeMap.ContainsKey(nodeId) ? _nodeMap[nodeId] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환을 위한 대체 노드 찾기
|
||||
/// 교차로에 연결된 노드 중에서 왔던 길과 갈 길이 아닌 다른 노드를 찾음
|
||||
/// 방향 전환 시 왕복 경로에 사용될 노드
|
||||
/// </summary>
|
||||
/// <param name="junctionNodeId">교차로 노드 ID (B)</param>
|
||||
/// <param name="previousNodeId">이전 노드 ID (A - 왔던 길)</param>
|
||||
/// <param name="targetNodeId">목표 노드 ID (C - 갈 길)</param>
|
||||
/// <param name="mapNodes">전체 맵 노드 목록</param>
|
||||
/// <returns>방향 전환에 사용할 대체 노드, 없으면 null</returns>
|
||||
public MapNode FindAlternateNodeForDirectionChange(
|
||||
string junctionNodeId,
|
||||
string previousNodeId,
|
||||
string targetNodeId)
|
||||
{
|
||||
// 입력 검증
|
||||
if (string.IsNullOrEmpty(junctionNodeId) || string.IsNullOrEmpty(previousNodeId) || string.IsNullOrEmpty(targetNodeId))
|
||||
return null;
|
||||
|
||||
if (_mapNodes == null || _mapNodes.Count == 0)
|
||||
return null;
|
||||
|
||||
// 교차로 노드 찾기
|
||||
var junctionNode = _mapNodes.FirstOrDefault(n => n.Id == junctionNodeId);
|
||||
if (junctionNode == null || junctionNode.ConnectedNodes == null || junctionNode.ConnectedNodes.Count == 0)
|
||||
return null;
|
||||
|
||||
// 교차로에 연결된 모든 노드 중에서 조건을 만족하는 노드 찾기
|
||||
// 조건:
|
||||
// 1. 이전 노드(왔던 길)가 아님
|
||||
// 2. 목표 노드(갈 길)가 아님
|
||||
// 3. 실제로 존재하는 노드
|
||||
// 4. 활성 상태인 노드
|
||||
// 5. 네비게이션 가능한 노드
|
||||
|
||||
var alternateNodes = new List<MapNode>();
|
||||
|
||||
foreach (var connectedNodeId in junctionNode.ConnectedNodes)
|
||||
{
|
||||
if (connectedNodeId == null) continue;
|
||||
|
||||
// 조건 1: 왔던 길이 아님
|
||||
if (connectedNodeId == previousNodeId) continue;
|
||||
|
||||
// 조건 2: 갈 길이 아님
|
||||
if (connectedNodeId == targetNodeId) continue;
|
||||
|
||||
// 조건 3, 4, 5: 존재하고, 활성 상태이고, 네비게이션 가능
|
||||
var connectedNode = _mapNodes.FirstOrDefault(n => n.Id == connectedNodeId);
|
||||
if (connectedNode != null && connectedNode.IsActive && connectedNode.IsNavigationNode())
|
||||
{
|
||||
alternateNodes.Add(connectedNode);
|
||||
}
|
||||
}
|
||||
|
||||
// 찾은 노드가 없으면 null 반환
|
||||
if (alternateNodes.Count == 0)
|
||||
return null;
|
||||
|
||||
// 여러 개 찾았으면 첫 번째 노드 반환
|
||||
// (필요시 거리 기반으로 가장 가까운 노드를 선택할 수도 있음)
|
||||
return alternateNodes[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
101
AGVLogic/AGVNavigationCore/PathFinding/Core/PathNode.cs
Normal file
101
AGVLogic/AGVNavigationCore/PathFinding/Core/PathNode.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A* 알고리즘에서 사용하는 경로 노드
|
||||
/// </summary>
|
||||
public class PathNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 ID
|
||||
/// </summary>
|
||||
public string NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 노드 위치
|
||||
/// </summary>
|
||||
public Point Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 시작점으로부터의 실제 거리 (G cost)
|
||||
/// </summary>
|
||||
public float GCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목적지까지의 추정 거리 (H cost - 휴리스틱)
|
||||
/// </summary>
|
||||
public float HCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 비용 (F cost = G cost + H cost)
|
||||
/// </summary>
|
||||
public float FCost => GCost + HCost;
|
||||
|
||||
/// <summary>
|
||||
/// 부모 노드 (경로 추적용)
|
||||
/// </summary>
|
||||
public PathNode Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 ID 목록
|
||||
/// </summary>
|
||||
public System.Collections.Generic.List<string> ConnectedNodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="position">위치</param>
|
||||
public PathNode(string nodeId, Point position)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Position = position;
|
||||
GCost = 0;
|
||||
HCost = 0;
|
||||
Parent = null;
|
||||
ConnectedNodes = new System.Collections.Generic.List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드까지의 유클리드 거리 계산
|
||||
/// </summary>
|
||||
/// <param name="other">대상 노드</param>
|
||||
/// <returns>거리</returns>
|
||||
public float DistanceTo(PathNode other)
|
||||
{
|
||||
float dx = Position.X - other.Position.X;
|
||||
float dy = Position.Y - other.Position.Y;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId} - F:{FCost:F1} G:{GCost:F1} H:{HCost:F1}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 같음 비교 (NodeId 기준)
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is PathNode other)
|
||||
{
|
||||
return NodeId == other.NodeId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 해시코드 (NodeId 기준)
|
||||
/// </summary>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return NodeId?.GetHashCode() ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
749
AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs
Normal file
749
AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs
Normal file
@@ -0,0 +1,749 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.Utils;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
using AGVNavigationCore.PathFinding.Analysis;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Planning
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 경로 계획기
|
||||
/// 물리적 제약사항과 마그넷 센서를 고려한 실제 AGV 경로 생성
|
||||
/// </summary>
|
||||
public class AGVPathfinder
|
||||
{
|
||||
|
||||
private readonly List<MapNode> _mapNodes;
|
||||
private readonly AStarPathfinder _basicPathfinder;
|
||||
private readonly DirectionalPathfinder _directionPathfinder;
|
||||
private readonly JunctionAnalyzer _junctionAnalyzer;
|
||||
private readonly DirectionChangePlanner _directionChangePlanner;
|
||||
|
||||
public AGVPathfinder(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_basicPathfinder = new AStarPathfinder();
|
||||
_basicPathfinder.SetMapNodes(_mapNodes);
|
||||
_junctionAnalyzer = new JunctionAnalyzer(_mapNodes);
|
||||
_directionChangePlanner = new DirectionChangePlanner(_mapNodes);
|
||||
_directionPathfinder = new DirectionalPathfinder();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 노드에서 가장 가까운 교차로(3개 이상 연결된 노드)를 찾는다.
|
||||
/// </summary>
|
||||
/// <param name="startNode">기준이 되는 노드</param>
|
||||
/// <returns>가장 가까운 교차로 노드 (또는 null)</returns>
|
||||
public MapNode FindNearestJunction(MapNode startNode)
|
||||
{
|
||||
if (startNode == null || _mapNodes == null || _mapNodes.Count == 0)
|
||||
return null;
|
||||
|
||||
// 교차로: 3개 이상의 노드가 연결된 노드
|
||||
var junctions = _mapNodes.Where(n =>
|
||||
n.IsActive &&
|
||||
n.IsNavigationNode() &&
|
||||
n.ConnectedNodes != null &&
|
||||
n.DisableCross == false &&
|
||||
n.ConnectedNodes.Count >= 3 &&
|
||||
n.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false &&
|
||||
n.Id != startNode.Id
|
||||
).ToList();
|
||||
|
||||
// docking 포인트가 연결된 노드는 제거한다.
|
||||
|
||||
|
||||
if (junctions.Count == 0)
|
||||
return null;
|
||||
|
||||
// 직선 거리 기반으로 가장 가까운 교차로 찾기
|
||||
MapNode nearestJunction = null;
|
||||
float minDistance = float.MaxValue;
|
||||
|
||||
foreach (var junction in junctions)
|
||||
{
|
||||
float dx = junction.Position.X - startNode.Position.X;
|
||||
float dy = junction.Position.Y - startNode.Position.Y;
|
||||
float distance = (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
nearestJunction = junction;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestJunction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 노드에서 경로상 가장 가까운 교차로를 찾는다.
|
||||
/// (최단 경로 내에서 3개 이상 연결된 교차로를 찾음)
|
||||
/// </summary>
|
||||
/// <param name="startNode">시작 노드</param>
|
||||
/// <param name="targetNode">목적지 노드</param>
|
||||
/// <returns>경로상의 가장 가까운 교차로 노드 (또는 null)</returns>
|
||||
public MapNode FindNearestJunctionOnPath(AGVPathResult pathResult)
|
||||
{
|
||||
if (pathResult == null || !pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0)
|
||||
return null;
|
||||
|
||||
// 경로상의 모든 노드 중 교차로(3개 이상 연결) 찾기
|
||||
var StartNode = pathResult.Path.First();
|
||||
foreach (var pathNode in pathResult.Path)
|
||||
{
|
||||
if (pathNode != null &&
|
||||
pathNode.IsActive &&
|
||||
pathNode.IsNavigationNode() &&
|
||||
pathNode.DisableCross == false &&
|
||||
pathNode.ConnectedNodes != null &&
|
||||
pathNode.ConnectedNodes.Count >= 3 &&
|
||||
pathNode.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false)
|
||||
{
|
||||
if (pathNode.Id.Equals(StartNode.Id) == false)
|
||||
return pathNode;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public AGVPathResult FindPathAStar(MapNode startNode, MapNode targetNode)
|
||||
{
|
||||
// 기본값으로 경로 탐색 (이전 위치 = 현재 위치, 방향 = 전진)
|
||||
return _basicPathfinder.FindPathAStar(startNode, targetNode);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요
|
||||
/// </summary>
|
||||
|
||||
/// <summary>
|
||||
/// 단순 경로 찾기 (복잡한 제약조건/방향전환 로직 없이 A* 결과만 반환)
|
||||
/// </summary>
|
||||
public AGVPathResult FindBasicPath(MapNode startNode, MapNode targetNode, MapNode _prevNode, AgvDirection prevDirection)
|
||||
{
|
||||
// 1. 입력 검증
|
||||
if (startNode == null || targetNode == null)
|
||||
return AGVPathResult.CreateFailure("노드 정보 오류", 0, 0);
|
||||
|
||||
// 2. A* 경로 탐색
|
||||
var pathResult = _basicPathfinder.FindPathAStar(startNode, targetNode);
|
||||
pathResult.PrevNode = _prevNode;
|
||||
pathResult.PrevDirection = prevDirection;
|
||||
|
||||
if (!pathResult.Success)
|
||||
return AGVPathResult.CreateFailure(pathResult.Message ?? "경로 없음", 0, 0);
|
||||
|
||||
// 3. 상세 데이터 생성 (갈림길 마그넷 방향 계산 포함)
|
||||
// 3. 상세 데이터 생성 (갈림길 마그넷 방향 계산 포함)
|
||||
if (pathResult.Path != null && pathResult.Path.Count > 0)
|
||||
{
|
||||
var detailedPath = new List<NodeMotorInfo>();
|
||||
for (int i = 0; i < pathResult.Path.Count; i++)
|
||||
{
|
||||
var node = pathResult.Path[i];
|
||||
var nextNode = (i + 1 < pathResult.Path.Count) ? pathResult.Path[i + 1] : null;
|
||||
|
||||
// 마그넷 방향 계산 (갈림길인 경우)
|
||||
// 마그넷 방향 계산 (갈림길인 경우)
|
||||
MagnetDirection magnetDirection = MagnetDirection.Straight;
|
||||
|
||||
//갈림길에 있다면 미리 방향을 저장해준다.
|
||||
if ((node.ConnectedNodes?.Count ?? 0) > 2 && nextNode != null)
|
||||
{
|
||||
//다음 노드ID를 확인해서 마그넷 방향 데이터를 찾는다.
|
||||
if (node.MagnetDirections.ContainsKey(nextNode.Id) == false)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"{node.ID2}->{nextNode.ID2} 의 (목표)갈림길 방향이 입력되지 않았습니다", 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
var magdir = node.MagnetDirections[nextNode.Id].ToString();
|
||||
if (magdir == "L") magnetDirection = MagnetDirection.Left;
|
||||
else if (magdir == "R") magnetDirection = MagnetDirection.Right;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
var nodeInfo = new NodeMotorInfo(i + 1, node.Id, node.RfidId, prevDirection, nextNode, magnetDirection);
|
||||
|
||||
// 속도 설정
|
||||
var mapNode = _mapNodes.FirstOrDefault(n => n.Id == node.Id);
|
||||
if (mapNode != null)
|
||||
{
|
||||
nodeInfo.Speed = mapNode.SpeedLimit;
|
||||
detailedPath.Add(nodeInfo);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
pathResult.DetailedPath = detailedPath;
|
||||
}
|
||||
|
||||
return pathResult;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 길목(Gateway) 기반 고급 경로 계산 (기존 SimulatorForm.CalcPath 이관)
|
||||
/// </summary>
|
||||
public AGVPathResult CalculatePath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection prevDir)
|
||||
{
|
||||
AGVPathResult Retval;
|
||||
// var o_StartNode = startNode;
|
||||
// startNode, targetNode는 이미 인자로 받음
|
||||
|
||||
if (startNode == null || targetNode == null) return AGVPathResult.CreateFailure("시작/종료노드가 지정되지 않음");
|
||||
|
||||
try
|
||||
{
|
||||
// 종료노드라면 이전위치로 이동시켜야한다. (Simulator Logic)
|
||||
// 만약 시작노드가 끝단(ConnectedMapNodes.Count == 1)이라면,
|
||||
// AGV가 해당 노드에 '도착'한 상태가 아니라 '작업' 중일 수 있으므로
|
||||
// 이전 노드(진입점)로 위치를 보정하여 경로를 계산한다.
|
||||
AGVPathResult LimitPath = null;
|
||||
if (startNode.ConnectedMapNodes.Count == 1)
|
||||
{
|
||||
// 시작점 -> 이전점 경로 (보통 후진이나 전진 1칸)
|
||||
LimitPath = this.FindPathAStar(startNode, prevNode);
|
||||
if (LimitPath.Success)
|
||||
{
|
||||
for (int i = 0; i < LimitPath.Path.Count; i++)
|
||||
{
|
||||
var nodeinfo = LimitPath.Path[i];
|
||||
var dir = (prevDir == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
||||
LimitPath.DetailedPath.Add(new NodeMotorInfo(i + 1, nodeinfo.Id, nodeinfo.RfidId, dir));
|
||||
}
|
||||
|
||||
// 시작 위치 및 방향 변경
|
||||
// var org_start = startNode; // Unused
|
||||
startNode = prevNode;
|
||||
prevNode = LimitPath.Path.First(); // startNode (original)
|
||||
prevDir = (prevDir == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 경로 생성 실패 시 보정 없이 진행하거나 에러 처리
|
||||
// 여기서는 일단 기존 로직대로 진행
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Buffer-to-Buffer 예외 처리
|
||||
// 05~31 구간 체크
|
||||
var node05 = _mapNodes.FirstOrDefault(n => n.RfidId == 5);
|
||||
var node31 = _mapNodes.FirstOrDefault(n => n.RfidId == 31);
|
||||
|
||||
bool fixpath = false;
|
||||
Retval = null;
|
||||
MapNode gatewayNode = null;
|
||||
|
||||
if (node05 != null && node31 != null)
|
||||
{
|
||||
// 버퍼 구간 경로 테스트
|
||||
var rlt = this.FindPathAStar(node05, node31);
|
||||
if (rlt.Success)
|
||||
{
|
||||
// 버퍼구간내에 시작과 종료가 모두 포함되어있다
|
||||
if (rlt.Path.Find(n => n.Id == startNode.Id) != null &&
|
||||
rlt.Path.Find(n => n.Id == targetNode.Id) != null)
|
||||
{
|
||||
Retval = CalcPathBufferToBuffer(startNode, targetNode, prevNode, prevDir);
|
||||
fixpath = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fixpath)
|
||||
{
|
||||
// 3. 목적지별 Gateway 및 진입 조건 확인
|
||||
gatewayNode = GetGatewayNode(targetNode);
|
||||
|
||||
if (gatewayNode == null)
|
||||
{
|
||||
// 게이트웨이가 없는 경우라면(일반 노드 등), Gateway 로직 없이 기본 경로 탐색
|
||||
Retval = this.FindBasicPath(startNode, targetNode, prevNode, prevDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Gateway Node 찾음
|
||||
// 4. Start -> Gateway 경로 계산 (A*)
|
||||
var pathToGateway = this.FindBasicPath(startNode, gatewayNode, prevNode, prevDir);
|
||||
if (pathToGateway.Success == false)
|
||||
return AGVPathResult.CreateFailure($"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.Message}");
|
||||
|
||||
// 방향을 확인하여, 왔던 방향으로 되돌아가야 한다면 방향 반전
|
||||
if (pathToGateway.Path.Count > 1)
|
||||
{
|
||||
var predictNext = pathToGateway.Path[1];
|
||||
if (predictNext.Id == prevNode.Id)
|
||||
{
|
||||
var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
foreach (var item in pathToGateway.DetailedPath)
|
||||
item.MotorDirection = reverseDir;
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 경로는 게이트웨이이므로 제거 (Gateway 진입 후 처리는 GetPathFromGateway에서 담당)
|
||||
if (pathToGateway.Path.Count > 0 && pathToGateway.Path.Last().Id == gatewayNode.Id)
|
||||
{
|
||||
var idx = pathToGateway.Path.Count - 1;
|
||||
pathToGateway.Path.RemoveAt(idx);
|
||||
pathToGateway.DetailedPath.RemoveAt(idx);
|
||||
}
|
||||
|
||||
// 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함)
|
||||
MapNode GateprevNode = pathToGateway.Path.LastOrDefault() ?? prevNode;
|
||||
NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.LastOrDefault();
|
||||
|
||||
var arrivalOrientation = GatePrevDetail?.MotorDirection ?? prevDir;
|
||||
var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, GateprevNode, arrivalOrientation);
|
||||
|
||||
if (!gatewayPathResult.Success)
|
||||
return AGVPathResult.CreateFailure($"{gatewayPathResult.Message}");
|
||||
|
||||
Retval = CombinePaths(pathToGateway, gatewayPathResult);
|
||||
}
|
||||
}
|
||||
|
||||
//게이트웨이
|
||||
Retval.Gateway = gatewayNode;
|
||||
|
||||
// 경로 오류 검사
|
||||
if (Retval == null || Retval.Success == false) return Retval ?? AGVPathResult.CreateFailure("경로 계산 결과 없음");
|
||||
|
||||
if (LimitPath != null)
|
||||
{
|
||||
Retval = CombinePaths(LimitPath, Retval);
|
||||
}
|
||||
|
||||
// 해당 경로와 대상의 도킹포인트 방향 검사
|
||||
if (targetNode.DockDirection != DockingDirection.DontCare)
|
||||
{
|
||||
var lastPath = Retval.DetailedPath.LastOrDefault();
|
||||
if (lastPath != null)
|
||||
{
|
||||
if (targetNode.DockDirection == DockingDirection.Forward && lastPath.MotorDirection != AgvDirection.Forward)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"생성된 경로와 목적지의 도킹방향이 일치하지 않습니다(FWD) Target:{targetNode.DockDirection}");
|
||||
}
|
||||
if (targetNode.DockDirection == DockingDirection.Backward && lastPath.MotorDirection != AgvDirection.Backward)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"생성된 경로와 목적지의 도킹방향이 일치하지 않습니다(BWD) Target:{targetNode.DockDirection}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 경로 최적화: A -> B -> A 패턴 제거
|
||||
// 6[F][R] → 13[B][L] → 6[F][L] 같은 경우 제거
|
||||
while (fixpath == false)
|
||||
{
|
||||
var updatecount = 0;
|
||||
for (int i = 0; i < Retval.DetailedPath.Count - 2; i++)
|
||||
{
|
||||
var n1 = Retval.DetailedPath[i];
|
||||
var n2 = Retval.DetailedPath[i + 1];
|
||||
var n3 = Retval.DetailedPath[i + 2];
|
||||
|
||||
if (n1.NodeId == n3.NodeId)
|
||||
{
|
||||
bool isInverse = false;
|
||||
// 1. 모터 방향이 반대인가? (F <-> B)
|
||||
bool isMotorInverse = (n1.MotorDirection != n2.MotorDirection) &&
|
||||
(n1.MotorDirection == AgvDirection.Forward || n1.MotorDirection == AgvDirection.Backward) &&
|
||||
(n2.MotorDirection == AgvDirection.Forward || n2.MotorDirection == AgvDirection.Backward);
|
||||
|
||||
if (isMotorInverse)
|
||||
{
|
||||
// 2. 마그넷 방향이 반대인가? (L <-> R, S <-> S)
|
||||
bool isMagnetInverse = false;
|
||||
if (n1.MagnetDirection == MagnetDirection.Straight && n2.MagnetDirection == MagnetDirection.Straight) isMagnetInverse = true;
|
||||
else if (n1.MagnetDirection == MagnetDirection.Left && n2.MagnetDirection == MagnetDirection.Right) isMagnetInverse = true;
|
||||
else if (n1.MagnetDirection == MagnetDirection.Right && n2.MagnetDirection == MagnetDirection.Left) isMagnetInverse = true;
|
||||
|
||||
if (isMagnetInverse) isInverse = true;
|
||||
}
|
||||
|
||||
if (isInverse)
|
||||
{
|
||||
// 제자리 회귀 경로 발견 -> 앞의 두 노드(n1, n2)를 제거하여 n3만 남김
|
||||
Retval.DetailedPath.RemoveAt(i);
|
||||
Retval.DetailedPath.RemoveAt(i);
|
||||
|
||||
if (Retval.Path.Count > i + 1)
|
||||
{
|
||||
Retval.Path.RemoveAt(i);
|
||||
Retval.Path.RemoveAt(i);
|
||||
}
|
||||
i--; // 인덱스 재조정
|
||||
updatecount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updatecount == 0) break;
|
||||
}
|
||||
|
||||
// 불가능한 회전 경로 검사 (사용자 요청 로직 반영)
|
||||
for (int i = 0; i < Retval.DetailedPath.Count - 2; i++)
|
||||
{
|
||||
var n1 = Retval.DetailedPath[i];
|
||||
var n2 = Retval.DetailedPath[i + 1];
|
||||
var n3 = Retval.DetailedPath[i + 2];
|
||||
|
||||
if (n1.NodeId == n3.NodeId &&
|
||||
n1.MotorDirection == n3.MotorDirection &&
|
||||
n1.MotorDirection == n2.MotorDirection) // Fix: 중간 노드 방향도 같을 때만 에러
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"불가능한 회전 경로가 포함되어있습니다. {n1.RfidId}->{n2.RfidId}->{n3.RfidId}");
|
||||
}
|
||||
}
|
||||
|
||||
// 기타 검증 로직 (마지막 노드 도킹, 시작노드 일치 등)
|
||||
var lastnode = Retval.Path.Last();
|
||||
if (lastnode.StationType != StationType.Normal)
|
||||
{
|
||||
var lastnodePath = Retval.DetailedPath.Last();
|
||||
if (lastnode.DockDirection == DockingDirection.Forward && lastnodePath.MotorDirection != AgvDirection.Forward)
|
||||
return AGVPathResult.CreateFailure($"목적지의 모터방향({lastnode.DockDirection}) 불일치 경로방향({lastnodePath.MotorDirection})");
|
||||
if (lastnode.DockDirection == DockingDirection.Backward && lastnodePath.MotorDirection != AgvDirection.Backward)
|
||||
return AGVPathResult.CreateFailure($"목적지의 모터방향({lastnode.DockDirection}) 불일치 경로방향({lastnodePath.MotorDirection})");
|
||||
}
|
||||
|
||||
// 첫번째 노드 일치 검사 - 필요시 수행 (startNode가 변경될 수 있어서 o_StartNode 등 필요할 수도 있음)
|
||||
// 여기서는 생략 혹은 간단히 체크
|
||||
|
||||
// 되돌아가는 길 방향 일치 검사
|
||||
if (Retval.DetailedPath.Count > 1)
|
||||
{
|
||||
var FirstDetailPath = Retval.DetailedPath[0];
|
||||
var NextDetailPath = Retval.DetailedPath[1];
|
||||
AgvDirection? PredictNextDir = null;
|
||||
|
||||
if (NextDetailPath.NodeId == prevNode.Id)
|
||||
{
|
||||
if (NextDetailPath.MagnetDirection == MagnetDirection.Straight)
|
||||
PredictNextDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
}
|
||||
|
||||
if (PredictNextDir != null && (FirstDetailPath.MotorDirection != (AgvDirection)PredictNextDir))
|
||||
{
|
||||
// return AGVPathResult.CreateFailure($"되돌아가는 길인데 방향이 일치하지않음");
|
||||
// 경고 수준이나 무시 가능한 경우도 있음
|
||||
}
|
||||
}
|
||||
|
||||
// 연결성 검사
|
||||
for (int i = 0; i < Retval.DetailedPath.Count - 1; i++)
|
||||
{
|
||||
var cnode = Retval.Path[i];
|
||||
var nnode = Retval.Path[i + 1];
|
||||
|
||||
if (cnode.ConnectedNodes.Contains(nnode.Id) == false && cnode.Id != nnode.Id)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"[{cnode.RfidId}] 노드에 연결되지 않은 [{nnode.RfidId}]노드가 지정됨");
|
||||
}
|
||||
}
|
||||
|
||||
//각 도킹포인트별로 절대 움직이면 안되는 조건확인
|
||||
var firstnode = Retval.Path.FirstOrDefault();
|
||||
var firstDet = Retval.DetailedPath.First();
|
||||
var failmessage = $"[{firstnode.ID2}] 노드의 시작모터 방향({firstDet.MotorDirection})이 올바르지 않습니다";
|
||||
if (firstnode.StationType == StationType.Charger1 && firstDet.MotorDirection != AgvDirection.Forward)
|
||||
return AGVPathResult.CreateFailure(failmessage);
|
||||
else if (firstnode.StationType == StationType.Loader && firstDet.MotorDirection != AgvDirection.Backward)
|
||||
return AGVPathResult.CreateFailure(failmessage);
|
||||
else if (firstnode.StationType == StationType.UnLoader && firstDet.MotorDirection != AgvDirection.Backward)
|
||||
return AGVPathResult.CreateFailure(failmessage);
|
||||
else if (firstnode.StationType == StationType.Clearner && firstDet.MotorDirection != AgvDirection.Backward)
|
||||
return AGVPathResult.CreateFailure(failmessage);
|
||||
else if (firstnode.StationType == StationType.Buffer)
|
||||
{
|
||||
//버퍼는 도킹이되어잇느닞 확인하고. 그때 방향을 체크해야한다.
|
||||
}
|
||||
|
||||
|
||||
return Retval;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"[계산오류] {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private AGVPathResult CalcPathBufferToBuffer(MapNode start, MapNode target, MapNode prev, AgvDirection prevDir)
|
||||
{
|
||||
// Monitor Side 판단 및 Buffer 간 이동 로직
|
||||
int deltaX = 0;
|
||||
int deltaY = 0;
|
||||
if (prev == null) return AGVPathResult.CreateFailure("이전 노드 정보가 없습니다");
|
||||
else
|
||||
{
|
||||
deltaX = start.Position.X - prev.Position.X;
|
||||
deltaY = -(start.Position.Y - prev.Position.Y);
|
||||
}
|
||||
|
||||
if (Math.Abs(deltaY) > Math.Abs(deltaX))
|
||||
deltaX = deltaY;
|
||||
|
||||
bool isMonitorLeft = false;
|
||||
|
||||
if (deltaX > 0) // 오른쪽(Forward)으로 이동해 옴
|
||||
isMonitorLeft = (prevDir == AgvDirection.Backward);
|
||||
else if (deltaX < 0) // 왼쪽(Reverse)으로 이동해 옴
|
||||
isMonitorLeft = (prevDir == AgvDirection.Forward);
|
||||
else
|
||||
return AGVPathResult.CreateFailure("이전 노드와의 방향을 알 수 없습니다");
|
||||
|
||||
if (isMonitorLeft)
|
||||
{
|
||||
// Monitor Left -> Gateway 탈출
|
||||
var GateWayNode = _mapNodes.FirstOrDefault(n => n.RfidId == 6);
|
||||
var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
|
||||
AGVPathResult escPath = null;
|
||||
if (start.Position.X > prev.Position.X)
|
||||
escPath = this.FindBasicPath(start, GateWayNode, prev, prevDir);
|
||||
else
|
||||
escPath = this.FindBasicPath(start, GateWayNode, prev, reverseDir);
|
||||
|
||||
if (!escPath.Success) return AGVPathResult.CreateFailure("버퍼 탈출 경로 실패");
|
||||
|
||||
var lastNode = escPath.Path.Last();
|
||||
var lastPrev = escPath.Path[escPath.Path.Count - 2];
|
||||
var lastDir = escPath.DetailedPath.Last().MotorDirection;
|
||||
|
||||
var gateToTarget = GetPathFromGateway(GateWayNode, target, lastPrev, lastDir);
|
||||
|
||||
escPath.Path.RemoveAt(escPath.Path.Count - 1);
|
||||
escPath.DetailedPath.RemoveAt(escPath.DetailedPath.Count - 1);
|
||||
|
||||
return CombinePaths(escPath, gateToTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Monitor Right -> 직접 진입 또는 Overshoot
|
||||
bool isTargetLeft = target.Position.X < start.Position.X;
|
||||
|
||||
if (target == start)
|
||||
{
|
||||
// 제자리 재정렬 (Same as Simulator logic)
|
||||
var list = new List<MapNode>();
|
||||
var retval = AGVPathResult.CreateSuccess(list, new List<AgvDirection>(), 0, 0);
|
||||
var resversedir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
|
||||
retval.Path.Add(target);
|
||||
|
||||
if (deltaX < 0)
|
||||
{
|
||||
var nextNode = start.ConnectedMapNodes.Where(t => t.Id != prev.Id && t.StationType == StationType.Buffer).FirstOrDefault();
|
||||
if (nextNode != null)
|
||||
{
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(1, target.Id, target.RfidId, prevDir));
|
||||
retval.Path.Add(nextNode);
|
||||
var lastDefailt = retval.DetailedPath.Last();
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(lastDefailt.seq + 1, nextNode.Id, nextNode.RfidId, AgvDirection.Forward)
|
||||
{
|
||||
Speed = SpeedLevel.M,
|
||||
});
|
||||
retval.Path.Add(target);
|
||||
retval.DetailedPath.Add(new NodeMotorInfo((retval.DetailedPath.Max(t => t.seq) + 1), target.Id, target.RfidId, AgvDirection.Forward));
|
||||
retval.Path.Add(target);
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(retval.DetailedPath.Max(t => t.seq) + 1, target.Id, target.RfidId, AgvDirection.Backward));
|
||||
}
|
||||
else
|
||||
{
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(1, target.Id, target.RfidId, resversedir));
|
||||
retval.Path.Add(prev);
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(retval.DetailedPath.Last().seq + 1, prev.Id, prev.RfidId, prevDir)
|
||||
{
|
||||
Speed = SpeedLevel.M,
|
||||
});
|
||||
retval.Path.Add(target);
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(retval.DetailedPath.Max(t => t.seq) + 1, target.Id, target.RfidId, prevDir));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(1, target.Id, target.RfidId, prevDir));
|
||||
var nextNode = start.ConnectedMapNodes.Where(t => t.Id != prev.Id && t.StationType == StationType.Buffer).FirstOrDefault();
|
||||
retval.Path.Add(nextNode);
|
||||
var lastDefailt = retval.DetailedPath.Last();
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(lastDefailt.seq + 1, nextNode.Id, nextNode.RfidId, AgvDirection.Backward)
|
||||
{
|
||||
Speed = SpeedLevel.L,
|
||||
});
|
||||
retval.Path.Add(target);
|
||||
retval.DetailedPath.Add(new NodeMotorInfo(retval.DetailedPath.Max(t => t.seq) + 1, target.Id, target.RfidId, AgvDirection.Backward));
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
else if (isTargetLeft)
|
||||
{
|
||||
return this.FindBasicPath(start, target, prev, AgvDirection.Backward);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Overshoot
|
||||
var path1 = this.FindBasicPath(start, target, prev, AgvDirection.Forward);
|
||||
if (path1.Path.Count < 2) return AGVPathResult.CreateFailure("Overshoot 경로 생성 실패");
|
||||
|
||||
var last = path1.Path.Last();
|
||||
var lastD = path1.DetailedPath.Last();
|
||||
path1.Path.RemoveAt(path1.Path.Count - 1);
|
||||
path1.DetailedPath.RemoveAt(path1.DetailedPath.Count - 1);
|
||||
|
||||
path1.Path.Add(last);
|
||||
path1.DetailedPath.Add(new NodeMotorInfo(lastD.seq + 1, lastD.NodeId, lastD.RfidId, AgvDirection.Backward)
|
||||
{
|
||||
Speed = SpeedLevel.L,
|
||||
});
|
||||
|
||||
return path1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AGVPathResult GetPathFromGateway(MapNode GTNode, MapNode targetNode, MapNode PrevNode, AgvDirection PrevDirection)
|
||||
{
|
||||
AGVPathResult resultPath = null;
|
||||
var deltaX = GTNode.Position.X - PrevNode.Position.X;
|
||||
var isMonitorLeft = false;
|
||||
|
||||
if (deltaX > 0) isMonitorLeft = PrevDirection == AgvDirection.Backward;
|
||||
else isMonitorLeft = PrevDirection == AgvDirection.Forward;
|
||||
|
||||
if (targetNode.StationType == StationType.Loader || targetNode.StationType == StationType.Charger2)
|
||||
{
|
||||
deltaX = GTNode.Position.Y - PrevNode.Position.Y;
|
||||
if (deltaX < 0) isMonitorLeft = PrevDirection == AgvDirection.Backward;
|
||||
else isMonitorLeft = PrevDirection == AgvDirection.Forward;
|
||||
}
|
||||
|
||||
switch (targetNode.StationType)
|
||||
{
|
||||
case StationType.Loader:
|
||||
case StationType.Charger2:
|
||||
case StationType.Charger1:
|
||||
case StationType.UnLoader:
|
||||
case StationType.Clearner:
|
||||
case StationType.Buffer:
|
||||
var rlt1 = new AGVPathResult();
|
||||
rlt1.Success = true;
|
||||
|
||||
var motdir = targetNode.DockDirection == DockingDirection.Backward ? AgvDirection.Backward : AgvDirection.Forward;
|
||||
var pathtarget = this.FindBasicPath(GTNode, targetNode, PrevNode, motdir);
|
||||
|
||||
if ((targetNode.DockDirection == DockingDirection.Backward && isMonitorLeft) ||
|
||||
(targetNode.DockDirection == DockingDirection.Forward && !isMonitorLeft))
|
||||
{
|
||||
var turnPatterns = GetTurnaroundPattern(GTNode, targetNode);
|
||||
if (turnPatterns == null || !turnPatterns.Any()) return new AGVPathResult { Success = false, Message = $"회차 패턴 없음: Dir {PrevDirection}" };
|
||||
|
||||
foreach (var item in turnPatterns)
|
||||
{
|
||||
var rfidvalue = ushort.Parse(item.Substring(0, 4));
|
||||
var node = _mapNodes.FirstOrDefault(t => t.RfidId == rfidvalue);
|
||||
rlt1.Path.Add(node);
|
||||
|
||||
AgvDirection nodedir = item.Substring(4, 1) == "F" ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
MagnetDirection magnet = MagnetDirection.Straight;
|
||||
var magchar = item.Substring(5, 1);
|
||||
if (magchar == "L") magnet = MagnetDirection.Left;
|
||||
else if (magchar == "R") magnet = MagnetDirection.Right;
|
||||
|
||||
rlt1.DetailedPath.Add(new NodeMotorInfo(rlt1.DetailedPath.Count, node.Id, node.RfidId, nodedir, null, magnet)
|
||||
{
|
||||
Speed = SpeedLevel.L,
|
||||
});
|
||||
}
|
||||
|
||||
if (pathtarget.DetailedPath.First().NodeId != rlt1.DetailedPath.Last().NodeId ||
|
||||
pathtarget.DetailedPath.First().MotorDirection != rlt1.DetailedPath.Last().MotorDirection)
|
||||
{
|
||||
// Gateway 턴 마지막 주소 불일치 경고 (로깅 등)
|
||||
}
|
||||
|
||||
pathtarget.Path.RemoveAt(0);
|
||||
pathtarget.DetailedPath.RemoveAt(0);
|
||||
}
|
||||
return CombinePaths(rlt1, pathtarget);
|
||||
|
||||
default:
|
||||
return AGVPathResult.CreateFailure($"지원되지 않는 StationType: {targetNode.StationType}");
|
||||
}
|
||||
}
|
||||
|
||||
private MapNode GetGatewayNode(MapNode node)
|
||||
{
|
||||
var rfid = 0;
|
||||
if (node.StationType == StationType.UnLoader) rfid = 10;
|
||||
else if (node.StationType == StationType.Charger1) rfid = 9;
|
||||
else if (node.StationType == StationType.Clearner) rfid = 6;
|
||||
else if (node.StationType == StationType.Charger2) rfid = 13;
|
||||
else if (node.StationType == StationType.Loader) rfid = 13;
|
||||
else if (node.StationType == StationType.Buffer) rfid = 6;
|
||||
|
||||
if (rfid == 0) return null;
|
||||
return _mapNodes.FirstOrDefault(t => t.RfidId == rfid);
|
||||
}
|
||||
|
||||
private List<string> GetTurnaroundPattern(MapNode gatewayNode, MapNode targetNode)
|
||||
{
|
||||
switch (gatewayNode.RfidId)
|
||||
{
|
||||
case 6:
|
||||
if (targetNode.StationType == StationType.Buffer)
|
||||
return new List<string> { "0006BL", "0007FS", "0013BL", "0006BL" };
|
||||
else
|
||||
return new List<string> { "0006BL", "0007FS", "0013BL", "0006BS" };
|
||||
case 9: return new List<string> { "0009FL", "0010BS", "0007FL", "0009FS" };
|
||||
case 10: return new List<string> { "0010BR", "0009FR", "0007BS", "0010BS" };
|
||||
case 13: return new List<string> { "0013BL", "0006FL", "0007BS", "0013BS" };
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AGVPathResult CombinePaths(AGVPathResult p1, AGVPathResult p2)
|
||||
{
|
||||
var res = new AGVPathResult();
|
||||
res.Success = true;
|
||||
|
||||
var p1last = p1.DetailedPath.LastOrDefault();
|
||||
var p2fist = p2.DetailedPath.FirstOrDefault();
|
||||
|
||||
if (p1last != null && p2fist != null &&
|
||||
(p1last.NodeId == p2fist.NodeId && p1last.MotorDirection == p2fist.MotorDirection && p1last.MagnetDirection == p2fist.MagnetDirection))
|
||||
{
|
||||
p1.Path.RemoveAt(p1.Path.Count - 1);
|
||||
p1.DetailedPath.RemoveAt(p1.DetailedPath.Count - 1);
|
||||
}
|
||||
|
||||
foreach (var item in p1.Path) res.Path.Add(item);
|
||||
foreach (var item in p2.Path) res.Path.Add(item);
|
||||
|
||||
foreach (var item in p1.DetailedPath)
|
||||
{
|
||||
var maxseq = res.DetailedPath.Count == 0 ? 0 : res.DetailedPath.Max(t => t.seq);
|
||||
item.seq = maxseq + 1;
|
||||
res.DetailedPath.Add(item);
|
||||
}
|
||||
foreach (var item in p2.DetailedPath)
|
||||
{
|
||||
var maxseq = res.DetailedPath.Count == 0 ? 0 : res.DetailedPath.Max(t => t.seq);
|
||||
item.seq = maxseq + 1;
|
||||
res.DetailedPath.Add(item);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
using AGVNavigationCore.PathFinding.Analysis;
|
||||
using AGVNavigationCore.PathFinding.Validation;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Planning
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 방향 전환 경로 계획 시스템
|
||||
/// 물리적 제약사항을 고려한 방향 전환 경로 생성
|
||||
/// </summary>
|
||||
public class DirectionChangePlanner
|
||||
{
|
||||
/// <summary>
|
||||
/// 방향 전환 계획 결과
|
||||
/// </summary>
|
||||
public class DirectionChangePlan
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<MapNode> DirectionChangePath { get; set; }
|
||||
public string DirectionChangeNode { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public string PlanDescription { get; set; }
|
||||
|
||||
public DirectionChangePlan()
|
||||
{
|
||||
DirectionChangePath = new List<MapNode>();
|
||||
ErrorMessage = string.Empty;
|
||||
PlanDescription = string.Empty;
|
||||
}
|
||||
|
||||
public static DirectionChangePlan CreateSuccess(List<MapNode> path, string changeNode, string description)
|
||||
{
|
||||
return new DirectionChangePlan
|
||||
{
|
||||
Success = true,
|
||||
DirectionChangePath = path,
|
||||
DirectionChangeNode = changeNode,
|
||||
PlanDescription = description
|
||||
};
|
||||
}
|
||||
|
||||
public static DirectionChangePlan CreateFailure(string error)
|
||||
{
|
||||
return new DirectionChangePlan
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<MapNode> _mapNodes;
|
||||
private readonly JunctionAnalyzer _junctionAnalyzer;
|
||||
private readonly AStarPathfinder _pathfinder;
|
||||
|
||||
public DirectionChangePlanner(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_junctionAnalyzer = new JunctionAnalyzer(_mapNodes);
|
||||
_pathfinder = new AStarPathfinder();
|
||||
_pathfinder.SetMapNodes(_mapNodes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Planning
|
||||
{
|
||||
/// <summary>
|
||||
/// 방향 기반 경로 탐색기
|
||||
/// 이전 위치 + 현재 위치 + 이동 방향을 기반으로 다음 노드를 결정
|
||||
/// </summary>
|
||||
public class DirectionalPathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// 이동 방향별 가중치
|
||||
/// </summary>
|
||||
public class DirectionWeights
|
||||
{
|
||||
public float ForwardWeight { get; set; } = 1.0f; // 직진
|
||||
public float LeftWeight { get; set; } = 1.5f; // 좌측
|
||||
public float RightWeight { get; set; } = 1.5f; // 우측
|
||||
public float BackwardWeight { get; set; } = 2.0f; // 후진
|
||||
}
|
||||
|
||||
private readonly DirectionWeights _weights;
|
||||
|
||||
public DirectionalPathfinder(DirectionWeights weights = null)
|
||||
{
|
||||
_weights = weights ?? new DirectionWeights();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이전 위치와 현재 위치, 그리고 이동 방향을 기반으로 다음 노드 ID를 반환
|
||||
/// </summary>
|
||||
/// <param name="previousPos">이전 위치 (이전 RFID 감지 위치)</param>
|
||||
/// <param name="currentNode">현재 노드 (현재 RFID 노드)</param>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="direction">이동 방향 (Forward/Backward/Left/Right)</param>
|
||||
/// <param name="allNodes">맵의 모든 노드</param>
|
||||
/// <returns>다음 노드 ID (또는 null)</returns>
|
||||
public string GetNextNodeId(
|
||||
Point previousPos,
|
||||
MapNode currentNode,
|
||||
Point currentPos,
|
||||
AgvDirection direction,
|
||||
List<MapNode> allNodes)
|
||||
{
|
||||
// 전제조건: 최소 2개 위치 히스토리 필요
|
||||
if (previousPos == Point.Empty || currentPos == Point.Empty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentNode == null || allNodes == null || allNodes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 현재 노드에 연결된 노드들 가져오기
|
||||
var connectedNodeIds = currentNode.ConnectedNodes;
|
||||
if (connectedNodeIds == null || connectedNodeIds.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 연결된 노드 중 현재 노드가 아닌 것들만 필터링
|
||||
var candidateNodes = allNodes.Where(n =>
|
||||
connectedNodeIds.Contains(n.Id) && n.Id != currentNode.Id
|
||||
).ToList();
|
||||
|
||||
if (candidateNodes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 이전→현재 벡터 계산 (진행 방향 벡터)
|
||||
var movementVector = new PointF(
|
||||
currentPos.X - previousPos.X,
|
||||
currentPos.Y - previousPos.Y
|
||||
);
|
||||
|
||||
// 벡터 정규화
|
||||
var movementLength = (float)Math.Sqrt(
|
||||
movementVector.X * movementVector.X +
|
||||
movementVector.Y * movementVector.Y
|
||||
);
|
||||
|
||||
if (movementLength < 0.001f) // 거의 이동하지 않음
|
||||
{
|
||||
return candidateNodes[0].Id; // 첫 번째 연결 노드 반환
|
||||
}
|
||||
|
||||
var normalizedMovement = new PointF(
|
||||
movementVector.X / movementLength,
|
||||
movementVector.Y / movementLength
|
||||
);
|
||||
|
||||
// 각 후보 노드에 대해 방향 점수 계산
|
||||
var scoredCandidates = new List<(MapNode node, float score)>();
|
||||
|
||||
foreach (var candidate in candidateNodes)
|
||||
{
|
||||
var toNextVector = new PointF(
|
||||
candidate.Position.X - currentPos.X,
|
||||
candidate.Position.Y - currentPos.Y
|
||||
);
|
||||
|
||||
var toNextLength = (float)Math.Sqrt(
|
||||
toNextVector.X * toNextVector.X +
|
||||
toNextVector.Y * toNextVector.Y
|
||||
);
|
||||
|
||||
if (toNextLength < 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedToNext = new PointF(
|
||||
toNextVector.X / toNextLength,
|
||||
toNextVector.Y / toNextLength
|
||||
);
|
||||
|
||||
// 진행 방향 기반 점수 계산
|
||||
float score = CalculateDirectionalScore(
|
||||
normalizedMovement,
|
||||
normalizedToNext,
|
||||
direction
|
||||
);
|
||||
|
||||
scoredCandidates.Add((candidate, score));
|
||||
}
|
||||
|
||||
if (scoredCandidates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 가장 높은 점수를 가진 노드 반환
|
||||
var bestCandidate = scoredCandidates.OrderByDescending(x => x.score).First();
|
||||
return bestCandidate.node.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이동 방향을 기반으로 방향 점수를 계산
|
||||
/// 높은 점수 = 더 나은 선택지
|
||||
/// </summary>
|
||||
private float CalculateDirectionalScore(
|
||||
PointF movementDirection, // 정규화된 이전→현재 벡터
|
||||
PointF nextDirection, // 정규화된 현재→다음 벡터
|
||||
AgvDirection requestedDir) // 요청된 이동 방향
|
||||
{
|
||||
float baseScore = 0;
|
||||
|
||||
// 벡터 간 각도 계산 (내적)
|
||||
float dotProduct = (movementDirection.X * nextDirection.X) +
|
||||
(movementDirection.Y * nextDirection.Y);
|
||||
|
||||
// 외적으로 좌우 판별 (Z 성분)
|
||||
float crossProduct = (movementDirection.X * nextDirection.Y) -
|
||||
(movementDirection.Y * nextDirection.X);
|
||||
|
||||
switch (requestedDir)
|
||||
{
|
||||
case AgvDirection.Forward:
|
||||
// Forward: 직진 방향 선호 (dotProduct ≈ 1)
|
||||
if (dotProduct > 0.9f) // 거의 같은 방향
|
||||
{
|
||||
baseScore = 100.0f * _weights.ForwardWeight;
|
||||
}
|
||||
else if (dotProduct > 0.5f) // 비슷한 방향
|
||||
{
|
||||
baseScore = 80.0f * _weights.ForwardWeight;
|
||||
}
|
||||
else if (dotProduct > 0.0f) // 약간 다른 방향
|
||||
{
|
||||
baseScore = 50.0f * _weights.ForwardWeight;
|
||||
}
|
||||
else if (dotProduct > -0.5f) // 거의 반대 방향 아님
|
||||
{
|
||||
baseScore = 20.0f * _weights.BackwardWeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
baseScore = 0.0f; // 완전 반대
|
||||
}
|
||||
break;
|
||||
|
||||
case AgvDirection.Backward:
|
||||
// Backward: 역진 방향 선호 (dotProduct ≈ -1)
|
||||
if (dotProduct < -0.9f) // 거의 반대 방향
|
||||
{
|
||||
baseScore = 100.0f * _weights.BackwardWeight;
|
||||
}
|
||||
else if (dotProduct < -0.5f) // 비슷하게 반대
|
||||
{
|
||||
baseScore = 80.0f * _weights.BackwardWeight;
|
||||
}
|
||||
else if (dotProduct < 0.0f) // 약간 다른 방향
|
||||
{
|
||||
baseScore = 50.0f * _weights.BackwardWeight;
|
||||
}
|
||||
else if (dotProduct < 0.5f) // 거의 같은 방향 아님
|
||||
{
|
||||
baseScore = 20.0f * _weights.ForwardWeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
baseScore = 0.0f; // 완전 같은 방향
|
||||
}
|
||||
break;
|
||||
|
||||
case AgvDirection.Left:
|
||||
// Left: 좌측 방향 선호
|
||||
// Forward 상태에서: crossProduct > 0 = 좌측
|
||||
// Backward 상태에서: crossProduct < 0 = 좌측 (반대)
|
||||
if (dotProduct > 0.0f) // Forward 상태
|
||||
{
|
||||
// crossProduct > 0이면 좌측
|
||||
if (crossProduct > 0.5f)
|
||||
{
|
||||
baseScore = 100.0f * _weights.LeftWeight;
|
||||
}
|
||||
else if (crossProduct > 0.0f)
|
||||
{
|
||||
baseScore = 70.0f * _weights.LeftWeight;
|
||||
}
|
||||
else if (crossProduct > -0.5f)
|
||||
{
|
||||
baseScore = 50.0f * _weights.ForwardWeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
baseScore = 30.0f * _weights.RightWeight;
|
||||
}
|
||||
}
|
||||
else // Backward 상태 - 좌우 반전
|
||||
{
|
||||
// Backward에서 좌측 = crossProduct < 0
|
||||
if (crossProduct < -0.5f)
|
||||
{
|
||||
baseScore = 100.0f * _weights.LeftWeight;
|
||||
}
|
||||
else if (crossProduct < 0.0f)
|
||||
{
|
||||
baseScore = 70.0f * _weights.LeftWeight;
|
||||
}
|
||||
else if (crossProduct < 0.5f)
|
||||
{
|
||||
baseScore = 50.0f * _weights.BackwardWeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
baseScore = 30.0f * _weights.RightWeight;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case AgvDirection.Right:
|
||||
// Right: 우측 방향 선호
|
||||
// Forward 상태에서: crossProduct < 0 = 우측
|
||||
// Backward 상태에서: crossProduct > 0 = 우측 (반대)
|
||||
if (dotProduct > 0.0f) // Forward 상태
|
||||
{
|
||||
// crossProduct < 0이면 우측
|
||||
if (crossProduct < -0.5f)
|
||||
{
|
||||
baseScore = 100.0f * _weights.RightWeight;
|
||||
}
|
||||
else if (crossProduct < 0.0f)
|
||||
{
|
||||
baseScore = 70.0f * _weights.RightWeight;
|
||||
}
|
||||
else if (crossProduct < 0.5f)
|
||||
{
|
||||
baseScore = 50.0f * _weights.ForwardWeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
baseScore = 30.0f * _weights.LeftWeight;
|
||||
}
|
||||
}
|
||||
else // Backward 상태 - 좌우 반전
|
||||
{
|
||||
// Backward에서 우측 = crossProduct > 0
|
||||
if (crossProduct > 0.5f)
|
||||
{
|
||||
baseScore = 100.0f * _weights.RightWeight;
|
||||
}
|
||||
else if (crossProduct > 0.0f)
|
||||
{
|
||||
baseScore = 70.0f * _weights.RightWeight;
|
||||
}
|
||||
else if (crossProduct > -0.5f)
|
||||
{
|
||||
baseScore = 50.0f * _weights.BackwardWeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
baseScore = 30.0f * _weights.LeftWeight;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return baseScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 벡터 간 각도를 도 단위로 계산
|
||||
/// </summary>
|
||||
private float CalculateAngle(PointF vector1, PointF vector2)
|
||||
{
|
||||
float dotProduct = (vector1.X * vector2.X) + (vector1.Y * vector2.Y);
|
||||
float magnitude1 = (float)Math.Sqrt(vector1.X * vector1.X + vector1.Y * vector1.Y);
|
||||
float magnitude2 = (float)Math.Sqrt(vector2.X * vector2.X + vector2.Y * vector2.Y);
|
||||
|
||||
if (magnitude1 < 0.001f || magnitude2 < 0.001f)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
float cosAngle = dotProduct / (magnitude1 * magnitude2);
|
||||
cosAngle = Math.Max(-1.0f, Math.Min(1.0f, cosAngle)); // 범위 제한
|
||||
|
||||
return (float)(Math.Acos(cosAngle) * 180.0 / Math.PI);
|
||||
}
|
||||
}
|
||||
}
|
||||
131
AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs
Normal file
131
AGVLogic/AGVNavigationCore/PathFinding/Planning/NodeMotorInfo.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Planning
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 마그넷 센서 방향 제어
|
||||
/// </summary>
|
||||
public enum MagnetDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// 직진 - 기본 마그넷 라인 추종
|
||||
/// </summary>
|
||||
Straight = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 좌측 - 마그넷 센서 가중치를 좌측으로 조정
|
||||
/// </summary>
|
||||
Left = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 우측 - 마그넷 센서 가중치를 우측으로 조정
|
||||
/// </summary>
|
||||
Right = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드별 모터방향 정보 (방향 전환 지원 포함)
|
||||
/// </summary>
|
||||
public class NodeMotorInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 일련번호
|
||||
/// </summary>
|
||||
public int seq { get; set; }
|
||||
/// <summary>
|
||||
/// 노드 ID
|
||||
/// </summary>
|
||||
public string NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// RFID Value
|
||||
/// </summary>
|
||||
public ushort RfidId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 해당 노드에서의 모터방향
|
||||
/// </summary>
|
||||
public AgvDirection MotorDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 해당 노드에서의 제한 속도
|
||||
/// </summary>
|
||||
public SpeedLevel Speed { get; set; } = SpeedLevel.M;
|
||||
|
||||
/// <summary>
|
||||
/// 마그넷 센서 방향 제어 (갈림길 처리용)
|
||||
/// </summary>
|
||||
public MagnetDirection MagnetDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 다음 노드 ID (경로예측용)
|
||||
/// </summary>
|
||||
public MapNode NextNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능 노드 여부
|
||||
/// </summary>
|
||||
public bool CanRotate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환이 발생하는 노드 여부
|
||||
/// </summary>
|
||||
public bool IsDirectionChangePoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 특수 동작이 필요한 노드 여부 (갈림길 전진/후진 반복)
|
||||
/// </summary>
|
||||
public bool RequiresSpecialAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 해당노드가 인식되면 이 값이 셋팅됩니다.
|
||||
/// </summary>
|
||||
public bool IsPass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 특수 동작 설명
|
||||
/// </summary>
|
||||
public string SpecialActionDescription { get; set; }
|
||||
|
||||
public NodeMotorInfo(int seqno,string nodeId,ushort rfid, AgvDirection motorDirection, MapNode nextNodeId = null, MagnetDirection magnetDirection = MagnetDirection.Straight)
|
||||
{
|
||||
seq = seqno;
|
||||
NodeId = nodeId;
|
||||
RfidId = rfid;
|
||||
MotorDirection = motorDirection;
|
||||
MagnetDirection = magnetDirection;
|
||||
NextNode = nextNodeId;
|
||||
CanRotate = false;
|
||||
IsDirectionChangePoint = false;
|
||||
RequiresSpecialAction = false;
|
||||
SpecialActionDescription = string.Empty;
|
||||
IsPass = false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 디버깅용 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var result = $"R{RfidId}[*{NodeId}]:{MotorDirection}";
|
||||
|
||||
// 마그넷 방향이 직진이 아닌 경우 표시
|
||||
if (MagnetDirection != MagnetDirection.Straight)
|
||||
result += $"({MagnetDirection})";
|
||||
|
||||
if (IsDirectionChangePoint)
|
||||
result += " [방향전환]";
|
||||
|
||||
if (CanRotate)
|
||||
result += " [회전가능]";
|
||||
|
||||
if (RequiresSpecialAction)
|
||||
result += $" [특수동작:{SpecialActionDescription}]";
|
||||
|
||||
if (IsPass) result += "(O)";
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// 도킹 검증 결과
|
||||
/// </summary>
|
||||
public class DockingValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 도킹 검증이 필요한지 여부 (목적지가 도킹 대상인 경우)
|
||||
/// </summary>
|
||||
public bool IsValidationRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 검증 통과 여부
|
||||
/// </summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목적지 노드 ID
|
||||
/// </summary>
|
||||
public string TargetNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목적지 노드 타입
|
||||
/// </summary>
|
||||
public NodeType TargetNodeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 필요한 도킹 방향
|
||||
/// </summary>
|
||||
public AgvDirection RequiredDockingDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계산된 경로의 마지막 방향
|
||||
/// </summary>
|
||||
public AgvDirection CalculatedFinalDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 검증 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ValidationError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public DockingValidationResult()
|
||||
{
|
||||
IsValidationRequired = false;
|
||||
IsValid = true;
|
||||
TargetNodeId = string.Empty;
|
||||
RequiredDockingDirection = AgvDirection.Forward;
|
||||
CalculatedFinalDirection = AgvDirection.Forward;
|
||||
ValidationError = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 불필요한 경우 생성
|
||||
/// </summary>
|
||||
public static DockingValidationResult CreateNotRequired()
|
||||
{
|
||||
return new DockingValidationResult
|
||||
{
|
||||
IsValidationRequired = false,
|
||||
IsValid = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 성공 결과 생성
|
||||
/// </summary>
|
||||
public static DockingValidationResult CreateValid(string targetNodeId, NodeType nodeType, AgvDirection requiredDirection, AgvDirection calculatedDirection)
|
||||
{
|
||||
return new DockingValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = true,
|
||||
TargetNodeId = targetNodeId,
|
||||
TargetNodeType = nodeType,
|
||||
RequiredDockingDirection = requiredDirection,
|
||||
CalculatedFinalDirection = calculatedDirection
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 실패 결과 생성
|
||||
/// </summary>
|
||||
public static DockingValidationResult CreateInvalid(string targetNodeId, NodeType nodeType, AgvDirection requiredDirection, AgvDirection calculatedDirection, string error)
|
||||
{
|
||||
return new DockingValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = false,
|
||||
TargetNodeId = targetNodeId,
|
||||
TargetNodeType = nodeType,
|
||||
RequiredDockingDirection = requiredDirection,
|
||||
CalculatedFinalDirection = calculatedDirection,
|
||||
ValidationError = error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using System.Collections.Generic;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 검증 결과 (되돌아가기 패턴 검증 포함)
|
||||
/// </summary>
|
||||
public class PathValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 검증이 필요한지 여부
|
||||
/// </summary>
|
||||
public bool IsValidationRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로 검증 통과 여부
|
||||
/// </summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 검증된 경로
|
||||
/// </summary>
|
||||
public List<string> ValidatedPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 검출된 되돌아가기 패턴 목록 (A → B → A 형태)
|
||||
/// </summary>
|
||||
public List<BacktrackingPattern> BacktrackingPatterns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길 노드 목록
|
||||
/// </summary>
|
||||
public List<string> JunctionNodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 시작 노드 ID
|
||||
/// </summary>
|
||||
public string StartNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목표 노드 ID
|
||||
/// </summary>
|
||||
public string TargetNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길 노드 ID (방향 전환용)
|
||||
/// </summary>
|
||||
public string JunctionNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 검증 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ValidationError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public PathValidationResult()
|
||||
{
|
||||
IsValidationRequired = false;
|
||||
IsValid = true;
|
||||
ValidatedPath = new List<string>();
|
||||
BacktrackingPatterns = new List<BacktrackingPattern>();
|
||||
JunctionNodes = new List<string>();
|
||||
StartNodeId = string.Empty;
|
||||
TargetNodeId = string.Empty;
|
||||
JunctionNodeId = string.Empty;
|
||||
ValidationError = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 불필요한 경우 생성
|
||||
/// </summary>
|
||||
public static PathValidationResult CreateNotRequired()
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValidationRequired = false,
|
||||
IsValid = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 성공 결과 생성
|
||||
/// </summary>
|
||||
public static PathValidationResult CreateValid(List<string> path, string startNodeId, string targetNodeId, string junctionNodeId = "")
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = true,
|
||||
ValidatedPath = new List<string>(path),
|
||||
StartNodeId = startNodeId,
|
||||
TargetNodeId = targetNodeId,
|
||||
JunctionNodeId = junctionNodeId
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 실패 결과 생성 (되돌아가기 패턴 검출)
|
||||
/// </summary>
|
||||
public static PathValidationResult CreateInvalidWithBacktracking(
|
||||
List<string> path,
|
||||
List<BacktrackingPattern> backtrackingPatterns,
|
||||
string startNodeId,
|
||||
string targetNodeId,
|
||||
string junctionNodeId,
|
||||
string error)
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = false,
|
||||
ValidatedPath = new List<string>(path),
|
||||
BacktrackingPatterns = new List<BacktrackingPattern>(backtrackingPatterns),
|
||||
StartNodeId = startNodeId,
|
||||
TargetNodeId = targetNodeId,
|
||||
JunctionNodeId = junctionNodeId,
|
||||
ValidationError = error
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 일반 검증 실패 결과 생성
|
||||
/// </summary>
|
||||
public static PathValidationResult CreateInvalid(string startNodeId, string targetNodeId, string error)
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = false,
|
||||
StartNodeId = startNodeId,
|
||||
TargetNodeId = targetNodeId,
|
||||
ValidationError = error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 되돌아가기 패턴 정보 (A → B → A)
|
||||
/// </summary>
|
||||
public class BacktrackingPattern
|
||||
{
|
||||
/// <summary>
|
||||
/// 시작 노드 (A)
|
||||
/// </summary>
|
||||
public string StartNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 중간 노드 (B)
|
||||
/// </summary>
|
||||
public string MiddleNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 되돌아간 노드 (다시 A)
|
||||
/// </summary>
|
||||
public string ReturnNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로에서의 시작 인덱스
|
||||
/// </summary>
|
||||
public int StartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로에서의 종료 인덱스
|
||||
/// </summary>
|
||||
public int EndIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public BacktrackingPattern()
|
||||
{
|
||||
StartNode = string.Empty;
|
||||
MiddleNode = string.Empty;
|
||||
ReturnNode = string.Empty;
|
||||
StartIndex = -1;
|
||||
EndIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 되돌아가기 패턴 생성
|
||||
/// </summary>
|
||||
public static BacktrackingPattern Create(string startNode, string middleNode, string returnNode, int startIndex, int endIndex)
|
||||
{
|
||||
return new BacktrackingPattern
|
||||
{
|
||||
StartNode = startNode,
|
||||
MiddleNode = middleNode,
|
||||
ReturnNode = returnNode,
|
||||
StartIndex = startIndex,
|
||||
EndIndex = endIndex
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴 설명 문자열
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{StartNode} → {MiddleNode} → {ReturnNode} (인덱스: {StartIndex}-{EndIndex})";
|
||||
}
|
||||
}
|
||||
}
|
||||
36
AGVLogic/AGVNavigationCore/Properties/AssemblyInfo.cs
Normal file
36
AGVLogic/AGVNavigationCore/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("AGVNavigationCore")]
|
||||
[assembly: AssemblyDescription("AGV Navigation and Pathfinding Core Library")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("ENIG")]
|
||||
[assembly: AssemblyProduct("AGV Navigation System")]
|
||||
[assembly: AssemblyCopyright("Copyright © ENIG 2024")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("c5f7a8b2-8d3e-4a1b-9c6e-7f4d5e2a9b1c")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
155
AGVLogic/AGVNavigationCore/README.md
Normal file
155
AGVLogic/AGVNavigationCore/README.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# AGVNavigationCore
|
||||
|
||||
ENIG AGV 시스템을 위한 핵심 네비게이션 및 경로 탐색 라이브러리
|
||||
|
||||
## 📋 개요
|
||||
|
||||
AGVNavigationCore는 자동 유도 차량(AGV) 시스템의 경로 계획, 맵 편집, 시뮬레이션, 실시간 모니터링 기능을 제공하는 .NET Framework 4.8 라이브러리입니다.
|
||||
|
||||
## 🏗️ 프로젝트 구조
|
||||
|
||||
### 📁 Controls/
|
||||
**AGV 관련 사용자 인터페이스 컨트롤 및 AGV 추상화 계층**
|
||||
|
||||
- **AGVState.cs** - AGV 상태 열거형 (Idle, Moving, Rotating, Docking, Charging, Error)
|
||||
- **IAGV.cs** - AGV 인터페이스 정의 (가상/실제 AGV 통합)
|
||||
- **UnifiedAGVCanvas.cs** - 통합 AGV 캔버스 컨트롤 메인 클래스
|
||||
- **UnifiedAGVCanvas.Events.cs** - 그리기 및 렌더링 로직 (AGV, 노드, 경로 시각화)
|
||||
- **UnifiedAGVCanvas.Mouse.cs** - 마우스 이벤트 처리 (클릭, 드래그, 줌, 팬)
|
||||
|
||||
### 📁 Models/
|
||||
**데이터 모델 및 핵심 비즈니스 엔티티 정의**
|
||||
|
||||
- **Enums.cs** - 핵심 열거형 정의 (NodeType, DockingDirection, AgvDirection, StationType)
|
||||
- **MapNode.cs** - 맵 노드 엔티티 클래스 (논리적 노드 ID, 위치, 타입, 연결 정보, RFID 정보)
|
||||
- **MapLoader.cs** - 맵 파일 로딩/저장 유틸리티 (JSON 직렬화, 데이터 마이그레이션, 검증)
|
||||
|
||||
### 📁 PathFinding/
|
||||
**AGV 경로 탐색 및 계산 알고리즘**
|
||||
|
||||
#### 🟢 활발히 사용되는 클래스
|
||||
- **AGVPathfinder.cs** - 메인 AGV 경로 계획기 (물리적 제약사항 고려)
|
||||
- **AGVPathResult.cs** - 경로 계산 결과 데이터 클래스
|
||||
- **DockingValidationResult.cs** - 도킹 검증 결과 데이터 클래스
|
||||
|
||||
#### 🟡 내부 구현 클래스
|
||||
- **AStarPathfinder.cs** - A* 알고리즘 기반 기본 경로 탐색
|
||||
- **DirectionChangePlanner.cs** - AGV 방향 전환 경로 계획 시스템
|
||||
- **JunctionAnalyzer.cs** - 교차점 분석 및 마그넷 센서 방향 계산
|
||||
- **NodeMotorInfo.cs** - 노드별 모터방향 정보 (방향 전환 지원 포함)
|
||||
- **PathNode.cs** - A* 알고리즘용 경로 노드
|
||||
|
||||
### 📁 Utils/
|
||||
**유틸리티 및 계산 헬퍼 클래스**
|
||||
|
||||
- **DockingValidator.cs** - AGV 도킹 방향 검증 유틸리티
|
||||
- **LiftCalculator.cs** - AGV 리프트 방향 계산 유틸리티
|
||||
|
||||
### 📁 Properties/
|
||||
- **AssemblyInfo.cs** - 어셈블리 정보 및 버전 관리
|
||||
|
||||
## 🎯 주요 기능
|
||||
|
||||
### 🗺️ 맵 관리
|
||||
- **논리적 노드 시스템**: 물리적 RFID와 분리된 논리적 노드 ID 관리
|
||||
- **노드 타입**: Normal, Rotation, Docking, Charging 등 다양한 노드 타입 지원
|
||||
- **연결 관리**: 노드 간 방향성 연결 관리
|
||||
- **JSON 저장/로드**: 표준 JSON 형식으로 맵 데이터 관리
|
||||
|
||||
### 🧭 경로 탐색
|
||||
- **A* 알고리즘**: 효율적인 최단 경로 탐색
|
||||
- **AGV 물리적 제약**: 전진/후진 모터 방향, 회전 제약 고려
|
||||
- **방향 전환 계획**: 마그넷 센서 위치에서의 방향 전환 최적화
|
||||
- **도킹 검증**: 목적지 타입에 따른 도킹 방향 검증
|
||||
|
||||
### 🎮 시각화 및 편집
|
||||
- **통합 캔버스**: 맵 편집, 시뮬레이션, 모니터링 모드 지원
|
||||
- **실시간 렌더링**: AGV 위치, 경로, 상태 실시간 표시
|
||||
- **인터랙티브 편집**: 드래그앤드롭 노드 편집, 연결 관리
|
||||
- **줌/팬**: 대형 맵 탐색을 위한 줌/팬 기능
|
||||
|
||||
## 🔧 아키텍처 특징
|
||||
|
||||
### ✅ 장점
|
||||
- **계층화 아키텍처**: Models → Utils → PathFinding → Controls 의존성 구조
|
||||
- **관심사 분리**: 각 폴더별 명확한 책임 분담
|
||||
- **인터페이스 기반**: IAGV 인터페이스로 가상/실제 AGV 통합
|
||||
- **확장성**: 새로운 알고리즘, AGV 타입 추가 용이
|
||||
|
||||
### ⚠️ 개선 영역
|
||||
- **코드 크기**: 일부 클래스가 과도하게 큼 (UnifiedAGVCanvas.Events.cs: 1,699행)
|
||||
- **복잡도**: DirectionChangePlanner 등 복잡한 로직 포함
|
||||
- **분할 필요**: UnifiedAGVCanvas의 다중 책임 분리 필요
|
||||
|
||||
## 🚀 사용 방법
|
||||
|
||||
### 기본 맵 로딩
|
||||
```csharp
|
||||
var mapLoader = new MapLoader();
|
||||
var mapNodes = mapLoader.LoadMap("path/to/map.json");
|
||||
```
|
||||
|
||||
### 경로 계산
|
||||
```csharp
|
||||
var pathfinder = new AGVPathfinder();
|
||||
pathfinder.SetMapNodes(mapNodes);
|
||||
|
||||
var result = pathfinder.FindPath("START_NODE", "TARGET_NODE", AgvDirection.Forward);
|
||||
if (result.Success)
|
||||
{
|
||||
Console.WriteLine($"경로: {string.Join(" -> ", result.Path)}");
|
||||
Console.WriteLine($"거리: {result.TotalDistance:F1}px");
|
||||
}
|
||||
```
|
||||
|
||||
### 캔버스 사용
|
||||
```csharp
|
||||
var canvas = new UnifiedAGVCanvas();
|
||||
canvas.Nodes = mapNodes;
|
||||
canvas.CurrentPath = result;
|
||||
canvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
|
||||
```
|
||||
|
||||
## 📈 최근 업데이트 (2024.12)
|
||||
|
||||
### ✅ 완료된 개선사항
|
||||
- **중복 코드 정리**: PathResult, RfidPathResult 등 중복 클래스 제거
|
||||
- **아키텍처 통합**: AdvancedAGVPathfinder → AGVPathfinder 통합
|
||||
- **좌표 정확성**: 줌/팬 시 노드 선택 정확도 개선
|
||||
- **미사용 코드 제거**: PathfindingOptions 등 미사용 클래스 삭제
|
||||
|
||||
### 🔄 진행 중인 개선사항
|
||||
- **방향 계산 최적화**: 리프트 방향 계산 로직 개선
|
||||
- **도킹 검증**: 도킹 방향 검증 시스템 강화
|
||||
- **성능 최적화**: 대형 맵 처리 성능 개선
|
||||
|
||||
## 🏃♂️ 향후 계획
|
||||
|
||||
### 우선순위 1 (즉시)
|
||||
- UnifiedAGVCanvas 분할 (Rendering, Editing, Simulation 분리)
|
||||
- [완료] PathFinding 폴더 세분화 (Core, Validation, Planning, Analysis)
|
||||
|
||||
### 우선순위 2 (중기)
|
||||
- 인터페이스 표준화 (I접두사 통일)
|
||||
- Utils 폴더 확장 (Calculations, Validators, Converters)
|
||||
|
||||
### 우선순위 3 (장기)
|
||||
- 의존성 주입 도입
|
||||
- 성능 모니터링 시스템
|
||||
- 단위 테스트 확충
|
||||
|
||||
## 📦 의존성
|
||||
- .NET Framework 4.8
|
||||
- Newtonsoft.Json 13.0.3
|
||||
- System.Drawing
|
||||
- System.Windows.Forms
|
||||
|
||||
## 🔗 관련 프로젝트
|
||||
|
||||
- **AGVMapEditor**: 맵 편집 전용 애플리케이션
|
||||
- **AGVSimulator**: AGV 시뮬레이션 애플리케이션
|
||||
- **AGVCSharp**: 메인 AGV 제어 시스템
|
||||
|
||||
## 📞 연락처
|
||||
|
||||
ENIG AGV 개발팀 - 2024년 12월 업데이트
|
||||
125
AGVLogic/AGVNavigationCore/Utils/AGVDirectionCalculator.cs
Normal file
125
AGVLogic/AGVNavigationCore/Utils/AGVDirectionCalculator.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 방향 기반 다음 노드 계산기
|
||||
/// VirtualAGV 또는 실제 AGV 시스템에서 현재 방향을 알 때, 다음 목적지 노드를 결정
|
||||
/// </summary>
|
||||
public class AGVDirectionCalculator
|
||||
{
|
||||
private DirectionalPathfinder _pathfinder;
|
||||
|
||||
public AGVDirectionCalculator(DirectionalPathfinder.DirectionWeights weights = null)
|
||||
{
|
||||
_pathfinder = new DirectionalPathfinder(weights);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이전 RFID 위치 + 현재 위치 + 현재 방향을 기반으로 다음 노드 ID를 반환
|
||||
///
|
||||
/// 사용 예시:
|
||||
/// - 001에서 002로 이동 후 GetNextNodeId(001_pos, 002_node, 002_pos, Forward) → 003
|
||||
/// - 003에서 004로 이동 후, Left 선택 → 030
|
||||
/// - 004에서 003으로 이동(Backward) 후, GetNextNodeId(..., Backward) → 002
|
||||
/// </summary>
|
||||
/// <param name="previousRfidPos">이전 RFID 감지 위치</param>
|
||||
/// <param name="currentNode">현재 RFID 노드</param>
|
||||
/// <param name="currentRfidPos">현재 RFID 감지 위치</param>
|
||||
/// <param name="direction">이동 방향</param>
|
||||
/// <param name="allNodes">맵의 모든 노드</param>
|
||||
/// <returns>다음 노드 ID (실패 시 null)</returns>
|
||||
public string GetNextNodeId(
|
||||
Point previousRfidPos,
|
||||
MapNode currentNode,
|
||||
Point currentRfidPos,
|
||||
AgvDirection direction,
|
||||
List<MapNode> allNodes)
|
||||
{
|
||||
// 유효성 검사
|
||||
if (previousRfidPos == Point.Empty)
|
||||
{
|
||||
throw new ArgumentException("previousRfidPos는 빈 값일 수 없습니다. 최소 2개의 위치 히스토리가 필요합니다.");
|
||||
}
|
||||
|
||||
if (currentNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(currentNode), "currentNode는 null일 수 없습니다.");
|
||||
}
|
||||
|
||||
if (allNodes == null || allNodes.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("allNodes는 비어있을 수 없습니다.");
|
||||
}
|
||||
|
||||
return _pathfinder.GetNextNodeId(
|
||||
previousRfidPos,
|
||||
currentNode,
|
||||
currentRfidPos,
|
||||
direction,
|
||||
allNodes
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 모터 상태를 기반으로 실제 선택된 방향을 분석
|
||||
/// VirtualAGV의 현재/이전 상태로부터 선택된 방향을 역추적
|
||||
/// </summary>
|
||||
public AgvDirection AnalyzeSelectedDirection(
|
||||
Point previousPos,
|
||||
Point currentPos,
|
||||
MapNode selectedNextNode,
|
||||
List<MapNode> connectedNodes)
|
||||
{
|
||||
if (previousPos == Point.Empty || currentPos == Point.Empty || selectedNextNode == null)
|
||||
{
|
||||
return AgvDirection.Forward;
|
||||
}
|
||||
|
||||
// 이동 벡터
|
||||
var movementVector = new PointF(
|
||||
currentPos.X - previousPos.X,
|
||||
currentPos.Y - previousPos.Y
|
||||
);
|
||||
|
||||
// 다음 노드 벡터
|
||||
var nextVector = new PointF(
|
||||
selectedNextNode.Position.X - currentPos.X,
|
||||
selectedNextNode.Position.Y - currentPos.Y
|
||||
);
|
||||
|
||||
// 내적 계산 (유사도)
|
||||
float dotProduct = (movementVector.X * nextVector.X) +
|
||||
(movementVector.Y * nextVector.Y);
|
||||
|
||||
// 외적 계산 (좌우 판별)
|
||||
float crossProduct = (movementVector.X * nextVector.Y) -
|
||||
(movementVector.Y * nextVector.X);
|
||||
|
||||
// 진행 방향 판별
|
||||
if (dotProduct > 0) // 같은 방향으로 진행
|
||||
{
|
||||
if (Math.Abs(crossProduct) < 0.1f) // 거의 직진
|
||||
{
|
||||
return AgvDirection.Forward;
|
||||
}
|
||||
else if (crossProduct > 0) // 좌측으로 회전
|
||||
{
|
||||
return AgvDirection.Left;
|
||||
}
|
||||
else // 우측으로 회전
|
||||
{
|
||||
return AgvDirection.Right;
|
||||
}
|
||||
}
|
||||
else // 반대 방향으로 진행 (후진)
|
||||
{
|
||||
return AgvDirection.Backward;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
464
AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs
Normal file
464
AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs
Normal file
@@ -0,0 +1,464 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Analysis;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 방향 계산 헬퍼 유틸리티
|
||||
/// 현재 위치에서 주어진 모터 방향과 마그넷 방향으로 이동할 때 다음 노드를 계산
|
||||
/// 이전 이동 방향과 마그넷 방향을 고려하여 더 정확한 경로 예측
|
||||
/// </summary>
|
||||
public static class DirectionalHelper
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// AGV방향과 일치하는지 확인한다. 단 원본위치에서 dock 위치가 Don't Care 라면 true가 반환 됩니다.
|
||||
/// </summary>
|
||||
/// <param name="dock"></param>
|
||||
/// <param name="agvdirection"></param>
|
||||
/// <returns></returns>
|
||||
public static bool MatchAGVDirection(this DockingDirection dock, AgvDirection agvdirection)
|
||||
{
|
||||
if (dock == DockingDirection.DontCare) return true;
|
||||
if (dock == DockingDirection.Forward && agvdirection == AgvDirection.Forward) return true;
|
||||
if (dock == DockingDirection.Backward && agvdirection == AgvDirection.Backward) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static JunctionAnalyzer _junctionAnalyzer;
|
||||
|
||||
/// <summary>
|
||||
/// JunctionAnalyzer 초기화 (첫 호출 시)
|
||||
/// </summary>
|
||||
private static void InitializeJunctionAnalyzer(List<MapNode> allNodes)
|
||||
{
|
||||
if (_junctionAnalyzer == null && allNodes != null)
|
||||
{
|
||||
_junctionAnalyzer = new JunctionAnalyzer(allNodes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드에서 주어진 모터 방향과 마그넷 방향으로 이동할 때 다음 노드를 반환
|
||||
/// 이전 모터 방향과 마그넷 방향을 고려하여 더 정확한 경로 예측
|
||||
/// </summary>
|
||||
/// <param name="currentNode">현재 노드</param>
|
||||
/// <param name="prevNode">이전 노드 (진행 방향 기준점)</param>
|
||||
/// <param name="prevDirection">이전 구간의 모터 방향</param>
|
||||
/// <param name="direction">현재 모터 방향 (Forward 또는 Backward)</param>
|
||||
/// <param name="magnetDirection">현재 마그넷 방향 (Straight/Left/Right)</param>
|
||||
/// <param name="allNodes">모든 맵 노드</param>
|
||||
/// <returns>다음 노드 (또는 null)</returns>
|
||||
public static MapNode GetNextNodeByDirection(
|
||||
MapNode currentNode,
|
||||
MapNode prevNode,
|
||||
AgvDirection prevDirection,
|
||||
AgvDirection direction,
|
||||
MagnetDirection magnetDirection,
|
||||
List<MapNode> allNodes)
|
||||
{
|
||||
if (currentNode == null || prevNode == null || allNodes == null)
|
||||
return null;
|
||||
|
||||
// JunctionAnalyzer 초기화
|
||||
InitializeJunctionAnalyzer(allNodes);
|
||||
|
||||
// 현재 노드에 연결된 노드들 중 이전 노드가 아닌 노드들만 필터링
|
||||
var connectedMapNodes = currentNode.ConnectedMapNodes;
|
||||
if (connectedMapNodes == null || connectedMapNodes.Count == 0)
|
||||
return null;
|
||||
|
||||
List<MapNode> candidateNodes = new List<MapNode>();
|
||||
if (prevDirection == direction)
|
||||
{
|
||||
candidateNodes = connectedMapNodes.Where(n => n.Id != prevNode.Id).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateNodes = connectedMapNodes.ToList();
|
||||
}
|
||||
|
||||
if (candidateNodes.Count == 0)
|
||||
return null;
|
||||
|
||||
// 이전→현재 이동 벡터
|
||||
var movementVector = new PointF(
|
||||
currentNode.Position.X - prevNode.Position.X,
|
||||
currentNode.Position.Y - prevNode.Position.Y
|
||||
);
|
||||
|
||||
var movementLength = (float)Math.Sqrt(
|
||||
movementVector.X * movementVector.X +
|
||||
movementVector.Y * movementVector.Y
|
||||
);
|
||||
|
||||
if (movementLength < 0.001f)
|
||||
return candidateNodes[0];
|
||||
|
||||
var normalizedMovement = new PointF(
|
||||
movementVector.X / movementLength,
|
||||
movementVector.Y / movementLength
|
||||
);
|
||||
|
||||
// 각 후보 노드에 대해 점수 계산
|
||||
MapNode bestNode = null;
|
||||
float bestScore = float.MinValue;
|
||||
|
||||
Console.WriteLine(
|
||||
$"\n[GetNextNodeByDirection] ========== 다음 노드 선택 시작 ==========");
|
||||
Console.WriteLine(
|
||||
$" 현재노드: {currentNode.RfidId}[{currentNode.Id}]({currentNode.Position.X:F1}, {currentNode.Position.Y:F1})");
|
||||
Console.WriteLine(
|
||||
$" 이전노드: {prevNode.RfidId}[{prevNode.Id}]({prevNode.Position.X:F1}, {prevNode.Position.Y:F1})");
|
||||
Console.WriteLine(
|
||||
$" 이동벡터: ({movementVector.X:F2}, {movementVector.Y:F2}) → 정규화: ({normalizedMovement.X:F3}, {normalizedMovement.Y:F3})");
|
||||
Console.WriteLine(
|
||||
$" 현재방향: {direction}, 이전방향: {prevDirection}, 마그넷방향: {magnetDirection}");
|
||||
Console.WriteLine(
|
||||
$" 후보노드 개수: {candidateNodes.Count}");
|
||||
|
||||
foreach (var candidate in candidateNodes)
|
||||
{
|
||||
var toNextVector = new PointF(
|
||||
candidate.Position.X - currentNode.Position.X,
|
||||
candidate.Position.Y - currentNode.Position.Y
|
||||
);
|
||||
|
||||
var toNextLength = (float)Math.Sqrt(
|
||||
toNextVector.X * toNextVector.X +
|
||||
toNextVector.Y * toNextVector.Y
|
||||
);
|
||||
|
||||
if (toNextLength < 0.001f)
|
||||
continue;
|
||||
|
||||
var normalizedToNext = new PointF(
|
||||
toNextVector.X / toNextLength,
|
||||
toNextVector.Y / toNextLength
|
||||
);
|
||||
|
||||
// 내적 계산 (유사도: -1 ~ 1)
|
||||
float dotProduct = (normalizedMovement.X * normalizedToNext.X) +
|
||||
(normalizedMovement.Y * normalizedToNext.Y);
|
||||
|
||||
float score;
|
||||
if (direction == prevDirection)
|
||||
{
|
||||
// Forward: 진행 방향과 유사한 방향 선택 (높은 내적 = 좋음)
|
||||
score = dotProduct;
|
||||
}
|
||||
else // Backward
|
||||
{
|
||||
// Backward: 진행 방향과 반대인 방향 선택 (낮은 내적 = 좋음)
|
||||
score = -dotProduct;
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"\n [후보] {candidate.RfidId}[{candidate.Id}]({candidate.Position.X:F1}, {candidate.Position.Y:F1})");
|
||||
Console.WriteLine(
|
||||
$" 벡터: ({toNextVector.X:F2}, {toNextVector.Y:F2}), 길이: {toNextLength:F2}");
|
||||
Console.WriteLine(
|
||||
$" 정규화벡터: ({normalizedToNext.X:F3}, {normalizedToNext.Y:F3})");
|
||||
Console.WriteLine(
|
||||
$" 내적(dotProduct): {dotProduct:F4}");
|
||||
Console.WriteLine(
|
||||
$" 기본점수 ({(direction == prevDirection ? "방향유지" : "방향변경")}): {score:F4}");
|
||||
|
||||
// 이전 모터 방향이 제공된 경우: 방향 일관성 보너스 추가
|
||||
var scoreBeforeMotor = score;
|
||||
score = ApplyMotorDirectionConsistencyBonus(
|
||||
score,
|
||||
direction,
|
||||
prevDirection,
|
||||
dotProduct
|
||||
);
|
||||
Console.WriteLine(
|
||||
$" 모터방향 적용 후: {scoreBeforeMotor:F4} → {score:F4}");
|
||||
|
||||
// 마그넷 방향을 고려한 점수 조정
|
||||
var scoreBeforeMagnet = score;
|
||||
score = ApplyMagnetDirectionBonus(
|
||||
score,
|
||||
magnetDirection,
|
||||
normalizedMovement,
|
||||
normalizedToNext,
|
||||
currentNode,
|
||||
candidate,
|
||||
direction
|
||||
);
|
||||
Console.WriteLine(
|
||||
$" 마그넷방향 적용 후: {scoreBeforeMagnet:F4} → {score:F4}");
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestNode = candidate;
|
||||
Console.WriteLine(
|
||||
$" ⭐ 현재 최고점수 선택됨!");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"\n 최종선택: {bestNode?.RfidId ?? 0}[{bestNode?.Id ?? "null"}] (점수: {bestScore:F4})");
|
||||
Console.WriteLine(
|
||||
$"[GetNextNodeByDirection] ========== 다음 노드 선택 종료 ==========\n");
|
||||
|
||||
return bestNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모터 방향 일관성을 고려한 점수 보정
|
||||
/// 같은 방향으로 계속 이동하는 경우 보너스 점수 부여
|
||||
/// </summary>
|
||||
/// <param name="baseScore">기본 점수</param>
|
||||
/// <param name="currentDirection">현재 모터 방향</param>
|
||||
/// <param name="prevMotorDirection">이전 모터 방향</param>
|
||||
/// <param name="dotProduct">벡터 내적값</param>
|
||||
/// <returns>조정된 점수</returns>
|
||||
private static float ApplyMotorDirectionConsistencyBonus(
|
||||
float baseScore,
|
||||
AgvDirection currentDirection,
|
||||
AgvDirection prevMotorDirection,
|
||||
float dotProduct)
|
||||
{
|
||||
float adjustedScore = baseScore;
|
||||
|
||||
// 모터 방향이 변경되지 않은 경우: 일관성 보너스
|
||||
if (currentDirection == prevMotorDirection)
|
||||
{
|
||||
// Forward 지속: 직진 방향으로의 이동 선호
|
||||
// Backward 지속: 반대 방향으로의 이동 선호
|
||||
const float CONSISTENCY_BONUS = 0.2f;
|
||||
adjustedScore += CONSISTENCY_BONUS;
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"[DirectionalHelper] 모터 방향 일관성 보너스: {currentDirection} → {currentDirection} " +
|
||||
$"(점수: {baseScore:F3} → {adjustedScore:F3})");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 모터 방향이 변경된 경우: 방향 변경 페널티
|
||||
const float DIRECTION_CHANGE_PENALTY = 0.15f;
|
||||
adjustedScore -= DIRECTION_CHANGE_PENALTY;
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"[DirectionalHelper] 모터 방향 변경 페널티: {prevMotorDirection} → {currentDirection} " +
|
||||
$"(점수: {baseScore:F3} → {adjustedScore:F3})");
|
||||
}
|
||||
|
||||
return adjustedScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마그넷 방향을 고려한 점수 보정
|
||||
/// Straight/Left/Right 마그넷 방향에 따라 후보 노드를 평가
|
||||
/// </summary>
|
||||
/// <param name="baseScore">기본 점수</param>
|
||||
/// <param name="magnetDirection">마그넷 방향 (Straight/Left/Right)</param>
|
||||
/// <param name="normalizedMovement">정규화된 이동 벡터</param>
|
||||
/// <param name="normalizedToNext">정규화된 다음 이동 벡터</param>
|
||||
/// <param name="currentNode">현재 노드</param>
|
||||
/// <param name="candidate">후보 노드</param>
|
||||
/// <returns>조정된 점수</returns>
|
||||
private static float ApplyMagnetDirectionBonus(
|
||||
float baseScore,
|
||||
MagnetDirection magnetDirection,
|
||||
PointF normalizedMovement,
|
||||
PointF normalizedToNext,
|
||||
MapNode currentNode,
|
||||
MapNode candidate,
|
||||
AgvDirection direction)
|
||||
{
|
||||
float adjustedScore = baseScore;
|
||||
|
||||
// Straight: 일직선 방향 (높은 내적 보너스)
|
||||
if (magnetDirection == MagnetDirection.Straight)
|
||||
{
|
||||
const float STRAIGHT_BONUS = 0.5f;
|
||||
adjustedScore += STRAIGHT_BONUS;
|
||||
|
||||
Console.WriteLine(
|
||||
$" [마그넷 판정] Straight 보너스 +0.5: {baseScore:F4} → {adjustedScore:F4}");
|
||||
}
|
||||
// Left 또는 Right: 모터 위치에 따른 회전 방향 판단
|
||||
else if (magnetDirection == MagnetDirection.Left || magnetDirection == MagnetDirection.Right)
|
||||
{
|
||||
// 2D 외적: movement × toNext = movement.X * toNext.Y - movement.Y * toNext.X
|
||||
float crossProduct = (normalizedMovement.X * normalizedToNext.Y) -
|
||||
(normalizedMovement.Y * normalizedToNext.X);
|
||||
|
||||
bool isLeftMotorMatch = false;
|
||||
bool isRightMotorMatch = false;
|
||||
|
||||
// ===== 정방향(Forward) 이동 =====
|
||||
if (direction == AgvDirection.Forward)
|
||||
{
|
||||
// Forward 이동 시 외적 판정:
|
||||
// - 외적 < 0 (음수) = 반시계 회전 = Left 모터 멈춤
|
||||
// - 외적 > 0 (양수) = 시계 회전 = Right 모터 멈춤
|
||||
//
|
||||
// 예: 004 → 012 → 016 (Left 모터)
|
||||
// 외적 = -0.9407 (음수) → 반시계 → Left 일치 ✅
|
||||
|
||||
isLeftMotorMatch = crossProduct < 0; // 음수 = 반시계 = Left 멈춤
|
||||
isRightMotorMatch = crossProduct > 0; // 양수 = 시계 = Right 멈춤
|
||||
}
|
||||
// ===== 역방향(Backward) 이동 =====
|
||||
else // Backward
|
||||
{
|
||||
// Backward 이동 시 외적 판정:
|
||||
// - 외적 < 0 (음수) = 시계 회전 = Left 모터 멈춤
|
||||
// - 외적 > 0 (양수) = 반시계 회전 = Right 모터 멈춤
|
||||
//
|
||||
// 예: 012 → 004 → 003 (Left 모터)
|
||||
// 외적 = 0.9334 (양수) → 반시계(역방향 기준 시계) → Left 일치 ✅
|
||||
|
||||
isLeftMotorMatch = crossProduct > 0; // 양수 = 시계(역) = Left 멈춤
|
||||
isRightMotorMatch = crossProduct < 0; // 음수 = 반시계(역) = Right 멈춤
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$" [마그넷 판정] 외적(Cross): {crossProduct:F4}, Left모터일치: {isLeftMotorMatch}, Right모터일치: {isRightMotorMatch} [{direction}]");
|
||||
|
||||
// 외적의 절대값으로 회전 강도 판단 (0에 가까우면 약함, 1에 가까우면 강함)
|
||||
float rotationStrength = Math.Abs(crossProduct);
|
||||
|
||||
if ((magnetDirection == MagnetDirection.Left && isLeftMotorMatch) ||
|
||||
(magnetDirection == MagnetDirection.Right && isRightMotorMatch))
|
||||
{
|
||||
// 올바른 모터 방향: 회전 강도에 비례한 보너스
|
||||
// 강한 회전(|외적| ≈ 1): +2.0
|
||||
// 약한 회전(|외적| ≈ 0.2): +0.4
|
||||
float magnetBonus = rotationStrength * 2.0f;
|
||||
adjustedScore += magnetBonus;
|
||||
|
||||
Console.WriteLine(
|
||||
$" [마그넷 판정] ✅ {magnetDirection} 모터 일치 (회전강도: {rotationStrength:F4}, 보너스 +{magnetBonus:F4}): {baseScore:F4} → {adjustedScore:F4}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 잘못된 모터 방향: 회전 강도에 비례한 페널티
|
||||
// 강한 회전(|외적| ≈ 1): -2.0
|
||||
// 약한 회전(|외적| ≈ 0.2): -0.4
|
||||
float magnetPenalty = rotationStrength * 2.0f;
|
||||
adjustedScore -= magnetPenalty;
|
||||
|
||||
string actualMotor = crossProduct > 0 ? "Left" : "Right";
|
||||
|
||||
Console.WriteLine(
|
||||
$" [마그넷 판정] ❌ {magnetDirection} 모터 불일치 (실제: {actualMotor}, 회전강도: {rotationStrength:F4}, 페널티 -{magnetPenalty:F4}): {baseScore:F4} → {adjustedScore:F4}");
|
||||
}
|
||||
}
|
||||
|
||||
return adjustedScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모터 방향을 고려한 다음 노드 선택 (디버깅/분석용)
|
||||
/// </summary>
|
||||
public static (MapNode node, float score, string reason) GetNextNodeByDirectionWithDetails(
|
||||
MapNode currentNode,
|
||||
MapNode prevNode,
|
||||
AgvDirection direction,
|
||||
List<MapNode> allNodes,
|
||||
AgvDirection? prevMotorDirection)
|
||||
{
|
||||
if (currentNode == null || prevNode == null || allNodes == null)
|
||||
return (null, 0, "입력 파라미터가 null입니다");
|
||||
|
||||
var connectedMapNodes = currentNode.ConnectedMapNodes;
|
||||
if (connectedMapNodes == null || connectedMapNodes.Count == 0)
|
||||
return (null, 0, "연결된 노드가 없습니다");
|
||||
|
||||
var candidateNodes = connectedMapNodes.ToList();
|
||||
|
||||
if (candidateNodes.Count == 0)
|
||||
return (null, 0, "후보 노드가 없습니다");
|
||||
|
||||
var movementVector = new PointF(
|
||||
currentNode.Position.X - prevNode.Position.X,
|
||||
currentNode.Position.Y - prevNode.Position.Y
|
||||
);
|
||||
|
||||
var movementLength = (float)Math.Sqrt(
|
||||
movementVector.X * movementVector.X +
|
||||
movementVector.Y * movementVector.Y
|
||||
);
|
||||
|
||||
if (movementLength < 0.001f)
|
||||
return (candidateNodes[0], 1.0f, "움직임이 거의 없음");
|
||||
|
||||
var normalizedMovement = new PointF(
|
||||
movementVector.X / movementLength,
|
||||
movementVector.Y / movementLength
|
||||
);
|
||||
|
||||
MapNode bestNode = null;
|
||||
float bestScore = float.MinValue;
|
||||
string reason = "";
|
||||
|
||||
foreach (var candidate in candidateNodes)
|
||||
{
|
||||
var toNextVector = new PointF(
|
||||
candidate.Position.X - currentNode.Position.X,
|
||||
candidate.Position.Y - currentNode.Position.Y
|
||||
);
|
||||
|
||||
var toNextLength = (float)Math.Sqrt(
|
||||
toNextVector.X * toNextVector.X +
|
||||
toNextVector.Y * toNextVector.Y
|
||||
);
|
||||
|
||||
if (toNextLength < 0.001f)
|
||||
continue;
|
||||
|
||||
var normalizedToNext = new PointF(
|
||||
toNextVector.X / toNextLength,
|
||||
toNextVector.Y / toNextLength
|
||||
);
|
||||
|
||||
float dotProduct = (normalizedMovement.X * normalizedToNext.X) +
|
||||
(normalizedMovement.Y * normalizedToNext.Y);
|
||||
|
||||
float score = (direction == AgvDirection.Forward) ? dotProduct : -dotProduct;
|
||||
|
||||
if (prevMotorDirection.HasValue)
|
||||
{
|
||||
score = ApplyMotorDirectionConsistencyBonus(
|
||||
score,
|
||||
direction,
|
||||
prevMotorDirection.Value,
|
||||
dotProduct
|
||||
);
|
||||
}
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestNode = candidate;
|
||||
|
||||
// 선택 이유 생성
|
||||
if (prevMotorDirection.HasValue && direction == prevMotorDirection)
|
||||
{
|
||||
reason = $"모터 방향 일관성 유지 ({direction}) → {candidate.Id}";
|
||||
}
|
||||
else if (prevMotorDirection.HasValue)
|
||||
{
|
||||
reason = $"모터 방향 변경 ({prevMotorDirection} → {direction}) → {candidate.Id}";
|
||||
}
|
||||
else
|
||||
{
|
||||
reason = $"방향 기반 선택 ({direction}) → {candidate.Id}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (bestNode, bestScore, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
AGVLogic/AGVNavigationCore/Utils/DirectionalPathfinderTest.cs
Normal file
198
AGVLogic/AGVNavigationCore/Utils/DirectionalPathfinderTest.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// DirectionalPathfinder 테스트 클래스
|
||||
/// NewMap.json 로드하여 방향별 다음 노드를 검증
|
||||
/// </summary>
|
||||
public class DirectionalPathfinderTest
|
||||
{
|
||||
private List<MapNode> _allNodes;
|
||||
private Dictionary<ushort, MapNode> _nodesByRfidId;
|
||||
private AGVDirectionCalculator _calculator;
|
||||
|
||||
public DirectionalPathfinderTest()
|
||||
{
|
||||
_nodesByRfidId = new Dictionary<ushort, MapNode>();
|
||||
_calculator = new AGVDirectionCalculator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NewMap.json 파일 로드
|
||||
/// </summary>
|
||||
public bool LoadMapFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Console.WriteLine($"파일을 찾을 수 없습니다: {filePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
string jsonContent = File.ReadAllText(filePath);
|
||||
var mapData = JsonConvert.DeserializeObject<MapFileData>(jsonContent);
|
||||
|
||||
if (mapData?.Nodes == null || mapData.Nodes.Count == 0)
|
||||
{
|
||||
Console.WriteLine("맵 파일이 비어있습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_allNodes = mapData.Nodes;
|
||||
|
||||
// RFID ID로 인덱싱
|
||||
foreach (var node in _allNodes)
|
||||
{
|
||||
if (node.HasRfid())
|
||||
{
|
||||
_nodesByRfidId[node.RfidId] = node;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"✓ 맵 파일 로드 성공: {_allNodes.Count}개 노드 로드");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"✗ 맵 파일 로드 실패: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 테스트: RFID 번호로 노드를 찾고, 다음 노드를 계산
|
||||
/// </summary>
|
||||
public void TestDirectionalMovement(ushort previousRfidId, ushort currentRfidId, AgvDirection direction)
|
||||
{
|
||||
Console.WriteLine($"\n========================================");
|
||||
Console.WriteLine($"테스트: {previousRfidId} → {currentRfidId} (방향: {direction})");
|
||||
Console.WriteLine($"========================================");
|
||||
|
||||
// RFID ID로 노드 찾기
|
||||
if (!_nodesByRfidId.TryGetValue(previousRfidId, out var previousNode))
|
||||
{
|
||||
Console.WriteLine($"✗ 이전 RFID를 찾을 수 없습니다: {previousRfidId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_nodesByRfidId.TryGetValue(currentRfidId, out var currentNode))
|
||||
{
|
||||
Console.WriteLine($"✗ 현재 RFID를 찾을 수 없습니다: {currentRfidId}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"이전 노드: {previousNode.Id} (RFID: {previousNode.RfidId}) - 위치: {previousNode.Position}");
|
||||
Console.WriteLine($"현재 노드: {currentNode.Id} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}");
|
||||
Console.WriteLine($"이동 벡터: ({currentNode.Position.X - previousNode.Position.X}, " +
|
||||
$"{currentNode.Position.Y - previousNode.Position.Y})");
|
||||
|
||||
// 다음 노드 계산
|
||||
string nextNodeId = _calculator.GetNextNodeId(
|
||||
previousNode.Position,
|
||||
currentNode,
|
||||
currentNode.Position,
|
||||
direction,
|
||||
_allNodes
|
||||
);
|
||||
|
||||
if (string.IsNullOrEmpty(nextNodeId))
|
||||
{
|
||||
Console.WriteLine($"✗ 다음 노드를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 다음 노드 정보 출력
|
||||
var nextNode = _allNodes.FirstOrDefault(n => n.Id == nextNodeId);
|
||||
if (nextNode != null)
|
||||
{
|
||||
Console.WriteLine($"✓ 다음 노드: {nextNode.Id} (RFID: {nextNode.RfidId}) - 위치: {nextNode.Position}");
|
||||
Console.WriteLine($" ├─ 노드 타입: {GetNodeTypeName(nextNode.Type)}");
|
||||
Console.WriteLine($" └─ 연결된 노드: {string.Join(", ", nextNode.ConnectedNodes)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"✗ 다음 노드 정보를 찾을 수 없습니다: {nextNodeId}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 노드 정보 출력
|
||||
/// </summary>
|
||||
public void PrintAllNodes()
|
||||
{
|
||||
Console.WriteLine("\n========== 모든 노드 정보 ==========");
|
||||
foreach (var node in _allNodes.OrderBy(n => n.RfidId))
|
||||
{
|
||||
Console.WriteLine($"{node.RfidId:D3} → {node.Id} ({GetNodeTypeName(node.Type)})");
|
||||
Console.WriteLine($" 위치: {node.Position}, 연결: {string.Join(", ", node.ConnectedNodes)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 RFID 노드의 상세 정보 출력
|
||||
/// </summary>
|
||||
public void PrintNodeInfo(ushort rfidId)
|
||||
{
|
||||
if (!_nodesByRfidId.TryGetValue(rfidId, out var node))
|
||||
{
|
||||
Console.WriteLine($"노드를 찾을 수 없습니다: {rfidId}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n========== RFID {rfidId} 상세 정보 ==========");
|
||||
Console.WriteLine($"노드 ID: {node.Id}");
|
||||
Console.WriteLine($"RFID: {node.RfidId}");
|
||||
Console.WriteLine($"ALIAS: {node.AliasName}");
|
||||
Console.WriteLine($"위치: {node.Position}");
|
||||
Console.WriteLine($"타입: {GetNodeTypeName(node.Type)}");
|
||||
Console.WriteLine($"TurnLeft/Right/교차로 : {(node.CanTurnLeft ? "O":"X")}/{(node.CanTurnRight ? "O" : "X")}/{(node.DisableCross ? "X" : "O")}");
|
||||
Console.WriteLine($"활성: {node.IsActive}");
|
||||
Console.WriteLine($"연결된 노드:");
|
||||
|
||||
if (node.ConnectedNodes.Count == 0)
|
||||
{
|
||||
Console.WriteLine(" (없음)");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var connectedId in node.ConnectedNodes)
|
||||
{
|
||||
var connectedNode = _allNodes.FirstOrDefault(n => n.Id == connectedId);
|
||||
if (connectedNode != null)
|
||||
{
|
||||
Console.WriteLine($" → {connectedId} (RFID: {connectedNode.RfidId}) - 위치: {connectedNode.Position}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" → {connectedId} (노드 찾을 수 없음)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetNodeTypeName(NodeType type)
|
||||
{
|
||||
return type.ToString();
|
||||
}
|
||||
|
||||
// JSON 파일 매핑을 위한 임시 클래스
|
||||
[Serializable]
|
||||
private class MapFileData
|
||||
{
|
||||
[JsonProperty("Nodes")]
|
||||
public List<MapNode> Nodes { get; set; }
|
||||
|
||||
[JsonProperty("RfidMappings")]
|
||||
public List<dynamic> RfidMappings { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
346
AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs
Normal file
346
AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs
Normal file
@@ -0,0 +1,346 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
using AGVNavigationCore.PathFinding.Validation;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 도킹 방향 검증 유틸리티
|
||||
/// 경로 계산 후 마지막 도킹 방향이 올바른지 검증
|
||||
/// </summary>
|
||||
public static class DockingValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로의 도킹 방향 검증
|
||||
/// </summary>
|
||||
/// <param name="pathResult">경로 계산 결과</param>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
/// <param name="currentDirection">AGV 현재 방향</param>
|
||||
/// <returns>도킹 검증 결과</returns>
|
||||
public static DockingValidationResult ValidateDockingDirection(AGVPathResult pathResult, List<MapNode> mapNodes)
|
||||
{
|
||||
// 경로가 없거나 실패한 경우
|
||||
if (pathResult == null || !pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 경로 없음");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
if (pathResult.DetailedPath.Any() == false && pathResult.Path.Any() && pathResult.Path.Count == 2 &&
|
||||
pathResult.Path[0].Id == pathResult.Path[1].Id)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 동일포인트");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
// 목적지 노드 가져오기 (Path는 이제 List<MapNode>)
|
||||
var LastNode = pathResult.Path[pathResult.Path.Count - 1];
|
||||
|
||||
if (LastNode == null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드가 null입니다");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드: {LastNode.Id} 타입:{LastNode.Type} ({(int)LastNode.Type})");
|
||||
|
||||
////detail 경로 이동 예측 검증
|
||||
//for (int i = 0; i < pathResult.DetailedPath.Count - 1; i++)
|
||||
//{
|
||||
// var curNodeId = pathResult.DetailedPath[i].NodeId;
|
||||
// var nextNodeId = pathResult.DetailedPath[i + 1].NodeId;
|
||||
|
||||
// var curNode = mapNodes?.FirstOrDefault(n => n.Id == curNodeId);
|
||||
// var nextNode = mapNodes?.FirstOrDefault(n => n.Id == nextNodeId);
|
||||
|
||||
// if (curNode != null && nextNode != null)
|
||||
// {
|
||||
// MapNode prevNode = null;
|
||||
// AgvDirection prevDir = AgvDirection.Stop;
|
||||
// if (i == 0)
|
||||
// {
|
||||
// prevNode = pathResult.PrevNode;
|
||||
// prevDir = pathResult.PrevDirection;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var prevNodeId = pathResult.DetailedPath[i - 1].NodeId;
|
||||
// prevNode = mapNodes?.FirstOrDefault(n => n.Id == prevNodeId);
|
||||
// prevDir = pathResult.DetailedPath[i - 1].MotorDirection;
|
||||
// }
|
||||
|
||||
|
||||
// if (prevNode != null)
|
||||
// {
|
||||
// // DirectionalHelper를 사용하여 예상되는 다음 노드 확인
|
||||
// Console.WriteLine(
|
||||
// $"\n[ValidateDockingDirection] 경로 검증 단계 {i}:");
|
||||
// Console.WriteLine(
|
||||
// $" 이전→현재→다음: {prevNode.Id}({prevNode.RfidId}) → {curNode.Id}({curNode.RfidId}) → {nextNode.Id}({nextNode.RfidId})");
|
||||
// Console.WriteLine(
|
||||
// $" 현재 노드 위치: ({curNode.Position.X:F1}, {curNode.Position.Y:F1})");
|
||||
// Console.WriteLine(
|
||||
// $" 이전 모터방향: {prevDir}, 현재 모터방향: {pathResult.DetailedPath[i].MotorDirection}");
|
||||
// Console.WriteLine(
|
||||
// $" 마그넷방향: {pathResult.DetailedPath[i].MagnetDirection}");
|
||||
|
||||
// var expectedNextNode = DirectionalHelper.GetNextNodeByDirection(
|
||||
// curNode,
|
||||
// prevNode,
|
||||
// prevDir,
|
||||
// pathResult.DetailedPath[i].MotorDirection,
|
||||
// pathResult.DetailedPath[i].MagnetDirection,
|
||||
// mapNodes
|
||||
// );
|
||||
|
||||
// var expectedNextNodeL = DirectionalHelper.GetNextNodeByDirection(
|
||||
// curNode,
|
||||
// prevNode,
|
||||
// prevDir,
|
||||
// pathResult.DetailedPath[i].MotorDirection,
|
||||
// PathFinding.Planning.MagnetDirection.Left,
|
||||
// mapNodes
|
||||
// );
|
||||
|
||||
// var expectedNextNodeR = DirectionalHelper.GetNextNodeByDirection(
|
||||
// curNode,
|
||||
// prevNode,
|
||||
// prevDir,
|
||||
// pathResult.DetailedPath[i].MotorDirection,
|
||||
// PathFinding.Planning.MagnetDirection.Right,
|
||||
// mapNodes
|
||||
// );
|
||||
|
||||
// var expectedNextNodeS = DirectionalHelper.GetNextNodeByDirection(
|
||||
// curNode,
|
||||
// prevNode,
|
||||
// prevDir,
|
||||
// pathResult.DetailedPath[i].MotorDirection,
|
||||
// PathFinding.Planning.MagnetDirection.Straight,
|
||||
// mapNodes
|
||||
// );
|
||||
|
||||
|
||||
// Console.WriteLine(
|
||||
// $" [예상] GetNextNodeByDirection 결과: {expectedNextNode?.Id ?? "null"}");
|
||||
// Console.WriteLine(
|
||||
// $" [실제] DetailedPath 다음 노드: {nextNode.RfidId}[{nextNode.Id}]");
|
||||
|
||||
// if (expectedNextNode != null && !expectedNextNode.Id.Equals(nextNode.Id))
|
||||
// {
|
||||
// string error =
|
||||
// $"[DockingValidator] ⚠️ 경로 방향 불일치" +
|
||||
// $"\n현재={curNode.RfidId}[{curNodeId}] 이전={prevNode.RfidId}[{(prevNode?.Id ?? string.Empty)}] " +
|
||||
// $"\n예상다음={expectedNextNode.RfidId}[{expectedNextNode.Id}] 실제다음={nextNode.RfidId}[{nextNodeId}]";
|
||||
// Console.WriteLine(
|
||||
// $"[ValidateDockingDirection] ❌ 경로 방향 불일치 검출!");
|
||||
// Console.WriteLine(
|
||||
// $" 이동 벡터:");
|
||||
// Console.WriteLine(
|
||||
// $" 이전→현재: ({(curNode.Position.X - prevNode.Position.X):F2}, {(curNode.Position.Y - prevNode.Position.Y):F2})");
|
||||
// Console.WriteLine(
|
||||
// $" 현재→예상: ({(expectedNextNode.Position.X - curNode.Position.X):F2}, {(expectedNextNode.Position.Y - curNode.Position.Y):F2})");
|
||||
// Console.WriteLine(
|
||||
// $" 현재→실제: ({(nextNode.Position.X - curNode.Position.X):F2}, {(nextNode.Position.Y - curNode.Position.Y):F2})");
|
||||
// Console.WriteLine($"[ValidateDockingDirection] 에러메시지: {error}");
|
||||
// return DockingValidationResult.CreateInvalid(
|
||||
// LastNode.Id,
|
||||
// LastNode.Type,
|
||||
// pathResult.DetailedPath[i].MotorDirection,
|
||||
// pathResult.DetailedPath[i].MotorDirection,
|
||||
// error);
|
||||
|
||||
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Console.WriteLine(
|
||||
// $" ✅ 경로 방향 일치!");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
// 도킹이 필요한 노드인지 확인 (DockDirection이 DontCare가 아닌 경우)
|
||||
if (LastNode.DockDirection == DockingDirection.DontCare)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 불필요: {LastNode.DockDirection}");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 필요한 도킹 방향 확인
|
||||
var requiredDirection = GetRequiredDockingDirection(LastNode.DockDirection);
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 필요한 도킹 방향: {requiredDirection}");
|
||||
|
||||
var LastNodeInfo = pathResult.DetailedPath.Last();
|
||||
if (LastNodeInfo.NodeId != LastNode.Id)
|
||||
{
|
||||
string error = $"마지막 노드의 도킹방향과 경로정보의 노드ID 불일치: 필요={LastNode.Id}, 계산됨={LastNodeInfo.NodeId }";
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}");
|
||||
return DockingValidationResult.CreateInvalid(
|
||||
LastNode.Id,
|
||||
LastNode.Type,
|
||||
requiredDirection,
|
||||
LastNodeInfo.MotorDirection,
|
||||
error);
|
||||
}
|
||||
|
||||
// 검증 수행
|
||||
if (LastNodeInfo.MotorDirection == requiredDirection && pathResult.DetailedPath[pathResult.DetailedPath.Count - 1].MotorDirection == requiredDirection)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공");
|
||||
return DockingValidationResult.CreateValid(
|
||||
LastNode.Id,
|
||||
LastNode.Type,
|
||||
requiredDirection,
|
||||
LastNodeInfo.MotorDirection);
|
||||
}
|
||||
else
|
||||
{
|
||||
string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(LastNodeInfo.MotorDirection)}";
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}");
|
||||
return DockingValidationResult.CreateInvalid(
|
||||
LastNode.Id,
|
||||
LastNode.Type,
|
||||
requiredDirection,
|
||||
LastNodeInfo.MotorDirection,
|
||||
error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹이 필요한 노드인지 확인 (도킹방향이 DontCare가 아닌 경우)
|
||||
/// </summary>
|
||||
private static bool IsDockingRequired(DockingDirection dockDirection)
|
||||
{
|
||||
return dockDirection != DockingDirection.DontCare;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 도킹 방향에 따른 필요한 AGV 방향 반환
|
||||
/// </summary>
|
||||
private static AgvDirection GetRequiredDockingDirection(DockingDirection dockDirection)
|
||||
{
|
||||
switch (dockDirection)
|
||||
{
|
||||
case DockingDirection.Forward:
|
||||
return AgvDirection.Forward; // 전진 도킹
|
||||
case DockingDirection.Backward:
|
||||
return AgvDirection.Backward; // 후진 도킹
|
||||
case DockingDirection.DontCare:
|
||||
default:
|
||||
return AgvDirection.Forward; // 기본값 (사실상 사용되지 않음)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 기반 최종 방향 계산
|
||||
/// 개선된 구현: 경로 진행 방향과 목적지 노드 타입을 고려
|
||||
/// </summary>
|
||||
private static AgvDirection CalculateFinalDirection(List<MapNode> path, List<MapNode> mapNodes, AgvDirection currentDirection)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 입력 - 경로 수: {path?.Count}, 현재 방향: {currentDirection}");
|
||||
|
||||
// 경로가 1개 이하면 현재 방향 유지
|
||||
if (path.Count < 2)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 경로가 짧음, 현재 방향 유지: {currentDirection}");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
// 목적지 노드 확인 (Path는 이제 List<MapNode>)
|
||||
var lastNode = path[path.Count - 1];
|
||||
|
||||
if (lastNode == null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 목적지 노드가 null입니다");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
// 도킹 노드인 경우, 필요한 도킹 방향으로 설정
|
||||
if (IsDockingRequired(lastNode.DockDirection))
|
||||
{
|
||||
var requiredDockingDirection = GetRequiredDockingDirection(lastNode.DockDirection);
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 도킹 노드(DockDirection={lastNode.DockDirection}) 감지, 필요 방향: {requiredDockingDirection}");
|
||||
|
||||
// 현재 방향이 필요한 도킹 방향과 다르면 경고 로그
|
||||
if (currentDirection != requiredDockingDirection)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] ⚠️ 현재 방향({currentDirection})과 필요 도킹 방향({requiredDockingDirection}) 불일치");
|
||||
}
|
||||
|
||||
// 도킹 노드의 경우 항상 필요한 도킹 방향 반환
|
||||
return requiredDockingDirection;
|
||||
}
|
||||
|
||||
// 일반 노드인 경우 마지막 구간의 이동 방향 분석
|
||||
var secondLastNode = path[path.Count - 2];
|
||||
|
||||
if (secondLastNode == null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 이전 노드가 null입니다");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
// 마지막 구간의 이동 벡터 계산
|
||||
var deltaX = lastNode.Position.X - secondLastNode.Position.X;
|
||||
var deltaY = lastNode.Position.Y - secondLastNode.Position.Y;
|
||||
var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 마지막 구간: {secondLastNode.Id} → {lastNode.Id}, 벡터: ({deltaX}, {deltaY}), 거리: {distance:F2}");
|
||||
|
||||
// 이동 거리가 매우 작으면 현재 방향 유지
|
||||
if (distance < 1.0)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 이동 거리 너무 짧음, 현재 방향 유지: {currentDirection}");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
// 일반 노드의 경우 현재 방향 유지 (방향 전환은 회전 노드에서만 발생)
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 일반 노드, 현재 방향 유지: {currentDirection}");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향을 텍스트로 변환
|
||||
/// </summary>
|
||||
private static string GetDirectionText(AgvDirection direction)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case AgvDirection.Forward:
|
||||
return "전진";
|
||||
case AgvDirection.Backward:
|
||||
return "후진";
|
||||
default:
|
||||
return direction.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 검증 결과를 문자열로 변환 (디버깅용)
|
||||
/// </summary>
|
||||
public static string GetValidationSummary(DockingValidationResult validation)
|
||||
{
|
||||
if (validation == null)
|
||||
return "검증 결과 없음";
|
||||
|
||||
if (!validation.IsValidationRequired)
|
||||
return "도킹 검증 불필요";
|
||||
|
||||
if (validation.IsValid)
|
||||
{
|
||||
return $"도킹 검증 통과: {validation.TargetNodeId}({validation.TargetNodeType}) - {GetDirectionText(validation.RequiredDockingDirection)} 도킹";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"도킹 검증 실패: {validation.TargetNodeId}({validation.TargetNodeType}) - {validation.ValidationError}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
342
AGVLogic/AGVNavigationCore/Utils/GetNextNodeIdTest.cs
Normal file
342
AGVLogic/AGVNavigationCore/Utils/GetNextNodeIdTest.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// GetNextNodeId() 메서드의 동작을 검증하는 테스트 클래스
|
||||
///
|
||||
/// 테스트 시나리오:
|
||||
/// - 001(65,229) → 002(206,244) → Forward → 003이 나와야 함
|
||||
/// - 001(65,229) → 002(206,244) → Backward → 001이 나와야 함
|
||||
/// - 002(206,244) → 003(278,278) → Forward → 004가 나와야 함
|
||||
/// - 002(206,244) → 003(278,278) → Backward → 002가 나와야 함
|
||||
/// </summary>
|
||||
public class GetNextNodeIdTest
|
||||
{
|
||||
/// <summary>
|
||||
/// 가상의 VirtualAGV 상태를 시뮬레이션하여 GetNextNodeId 테스트
|
||||
/// </summary>
|
||||
public void TestGetNextNodeId()
|
||||
{
|
||||
Console.WriteLine("\n================================================");
|
||||
Console.WriteLine("GetNextNodeId() 동작 검증");
|
||||
Console.WriteLine("================================================\n");
|
||||
|
||||
// 테스트 노드 생성
|
||||
var node001 = new MapNode { Id = "N001", RfidId = 001, Position = new Point(65, 229), ConnectedNodes = new List<string> { "N002" } };
|
||||
var node002 = new MapNode { Id = "N002", RfidId = 002, Position = new Point(206, 244), ConnectedNodes = new List<string> { "N001", "N003" } };
|
||||
var node003 = new MapNode { Id = "N003", RfidId = 003, Position = new Point(278, 278), ConnectedNodes = new List<string> { "N002", "N004" } };
|
||||
var node004 = new MapNode { Id = "N004", RfidId = 004, Position = new Point(380, 340), ConnectedNodes = new List<string> { "N003", "N022", "N031" } };
|
||||
|
||||
var allNodes = new List<MapNode> { node001, node002, node003, node004 };
|
||||
|
||||
// VirtualAGV 시뮬레이션 (실제 인스턴스 생성 불가하므로 로직만 재현)
|
||||
Console.WriteLine("테스트 시나리오 1: 001 → 002 → Forward");
|
||||
Console.WriteLine("─────────────────────────────────────────");
|
||||
TestScenario(
|
||||
"Forward 이동: 001에서 002로, 다음은 Forward",
|
||||
node001.Position, node002, node003,
|
||||
AgvDirection.Forward, allNodes,
|
||||
"003 (예상)"
|
||||
);
|
||||
|
||||
Console.WriteLine("\n테스트 시나리오 2: 001 → 002 → Backward");
|
||||
Console.WriteLine("─────────────────────────────────────────");
|
||||
TestScenario(
|
||||
"Backward 이동: 001에서 002로, 다음은 Backward",
|
||||
node001.Position, node002, node001,
|
||||
AgvDirection.Backward, allNodes,
|
||||
"001 (예상)"
|
||||
);
|
||||
|
||||
Console.WriteLine("\n테스트 시나리오 3: 002 → 003 → Forward");
|
||||
Console.WriteLine("─────────────────────────────────────────");
|
||||
TestScenario(
|
||||
"Forward 이동: 002에서 003으로, 다음은 Forward",
|
||||
node002.Position, node003, node004,
|
||||
AgvDirection.Forward, allNodes,
|
||||
"004 (예상)"
|
||||
);
|
||||
|
||||
Console.WriteLine("\n테스트 시나리오 4: 002 → 003 Forward → Backward");
|
||||
Console.WriteLine("─────────────────────────────────────────");
|
||||
TestScenario(
|
||||
"Forward 이동: 002에서 003으로, 다음은 Backward (경로 반대)",
|
||||
node002.Position, node003, node002,
|
||||
AgvDirection.Backward, allNodes,
|
||||
"002 (예상 - 경로 반대)"
|
||||
);
|
||||
|
||||
Console.WriteLine("\n테스트 시나리오 5: 002 → 003 Backward → Forward");
|
||||
Console.WriteLine("─────────────────────────────────────────");
|
||||
TestScenario(
|
||||
"Backward 이동: 002에서 003으로, 다음은 Forward (경로 반대)",
|
||||
node002.Position, node003, node002,
|
||||
AgvDirection.Forward, allNodes,
|
||||
"002 (예상 - 경로 반대)",
|
||||
AgvDirection.Backward // 현재 모터 방향
|
||||
);
|
||||
|
||||
Console.WriteLine("\n테스트 시나리오 6: 002 → 003 Backward → Backward");
|
||||
Console.WriteLine("─────────────────────────────────────────");
|
||||
TestScenario(
|
||||
"Backward 이동: 002에서 003으로, 다음은 Backward (경로 계속)",
|
||||
node002.Position, node003, node004,
|
||||
AgvDirection.Backward, allNodes,
|
||||
"004 (예상 - 경로 계속)",
|
||||
AgvDirection.Backward // 현재 모터 방향
|
||||
);
|
||||
|
||||
Console.WriteLine("\n\n================================================");
|
||||
Console.WriteLine("테스트 완료");
|
||||
Console.WriteLine("================================================\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 개별 테스트 시나리오 실행
|
||||
/// </summary>
|
||||
private void TestScenario(
|
||||
string description,
|
||||
Point prevPos,
|
||||
MapNode currentNode,
|
||||
MapNode expectedNextNode,
|
||||
AgvDirection direction,
|
||||
List<MapNode> allNodes,
|
||||
string expectedNodeIdStr,
|
||||
AgvDirection? currentMotorDirection = null)
|
||||
{
|
||||
// 현재 모터 방향이 지정되지 않으면 direction과 동일하다고 가정
|
||||
AgvDirection motorDir = currentMotorDirection ?? direction;
|
||||
|
||||
Console.WriteLine($"설명: {description}");
|
||||
Console.WriteLine($"이전 위치: {prevPos} (RFID: {allNodes.First(n => n.Position == prevPos)?.RfidId.ToString("0000") ?? "?"})");
|
||||
Console.WriteLine($"현재 노드: {currentNode.Id} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}");
|
||||
Console.WriteLine($"현재 모터 방향: {motorDir}");
|
||||
Console.WriteLine($"요청 방향: {direction}");
|
||||
|
||||
// 이동 벡터 계산
|
||||
var movementVector = new PointF(
|
||||
currentNode.Position.X - prevPos.X,
|
||||
currentNode.Position.Y - prevPos.Y
|
||||
);
|
||||
|
||||
Console.WriteLine($"이동 벡터: ({movementVector.X}, {movementVector.Y})");
|
||||
|
||||
// 각 후보 노드에 대한 점수 계산
|
||||
Console.WriteLine($"\n현재 노드({currentNode.Id})의 ConnectedNodes: {string.Join(", ", currentNode.ConnectedNodes)}");
|
||||
Console.WriteLine($"가능한 다음 노드들:");
|
||||
|
||||
var candidateNodes = allNodes.Where(n =>
|
||||
currentNode.ConnectedNodes.Contains(n.Id) && n.Id != currentNode.Id
|
||||
).ToList();
|
||||
|
||||
foreach (var candidate in candidateNodes)
|
||||
{
|
||||
var score = CalculateScoreAndPrint(movementVector, currentNode.Position, candidate, direction);
|
||||
string isExpected = (candidate.Id == expectedNextNode.Id) ? " ← 예상 노드" : "";
|
||||
Console.WriteLine($" {candidate.Id} (RFID: {candidate.RfidId}) - 위치: {candidate.Position} - 점수: {score:F1}{isExpected}");
|
||||
}
|
||||
|
||||
// 최고 점수 노드 선택
|
||||
var bestCandidate = GetBestCandidate(movementVector, currentNode.Position, candidateNodes, direction);
|
||||
|
||||
Console.WriteLine($"\n✓ 선택된 노드: {bestCandidate.Id} (RFID: {bestCandidate.RfidId})");
|
||||
|
||||
if (bestCandidate.Id == expectedNextNode.Id)
|
||||
{
|
||||
Console.WriteLine($"✅ 정답! ({expectedNodeIdStr})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"❌ 오답! 예상: {expectedNextNode.Id}, 실제: {bestCandidate.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 점수 계산 및 상세 정보 출력
|
||||
/// </summary>
|
||||
private float CalculateScoreAndPrint(PointF movementVector, Point currentPos, MapNode candidate, AgvDirection direction)
|
||||
{
|
||||
// 벡터 정규화
|
||||
var movementLength = (float)Math.Sqrt(
|
||||
movementVector.X * movementVector.X +
|
||||
movementVector.Y * movementVector.Y
|
||||
);
|
||||
|
||||
var normalizedMovement = new PointF(
|
||||
movementVector.X / movementLength,
|
||||
movementVector.Y / movementLength
|
||||
);
|
||||
|
||||
// 다음 벡터
|
||||
var toNextVector = new PointF(
|
||||
candidate.Position.X - currentPos.X,
|
||||
candidate.Position.Y - currentPos.Y
|
||||
);
|
||||
|
||||
var toNextLength = (float)Math.Sqrt(
|
||||
toNextVector.X * toNextVector.X +
|
||||
toNextVector.Y * toNextVector.Y
|
||||
);
|
||||
|
||||
var normalizedToNext = new PointF(
|
||||
toNextVector.X / toNextLength,
|
||||
toNextVector.Y / toNextLength
|
||||
);
|
||||
|
||||
// 내적 및 외적 계산
|
||||
float dotProduct = (normalizedMovement.X * normalizedToNext.X) +
|
||||
(normalizedMovement.Y * normalizedToNext.Y);
|
||||
|
||||
float crossProduct = (normalizedMovement.X * normalizedToNext.Y) -
|
||||
(normalizedMovement.Y * normalizedToNext.X);
|
||||
|
||||
float score = CalculateDirectionalScore(dotProduct, crossProduct, direction);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 점수 계산 (VirtualAGV.CalculateDirectionalScore()와 동일)
|
||||
/// </summary>
|
||||
private float CalculateDirectionalScore(float dotProduct, float crossProduct, AgvDirection direction)
|
||||
{
|
||||
float baseScore = 0;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case AgvDirection.Forward:
|
||||
if (dotProduct > 0.9f)
|
||||
baseScore = 100.0f;
|
||||
else if (dotProduct > 0.5f)
|
||||
baseScore = 80.0f;
|
||||
else if (dotProduct > 0.0f)
|
||||
baseScore = 50.0f;
|
||||
else if (dotProduct > -0.5f)
|
||||
baseScore = 20.0f;
|
||||
break;
|
||||
|
||||
case AgvDirection.Backward:
|
||||
if (dotProduct < -0.9f)
|
||||
baseScore = 100.0f;
|
||||
else if (dotProduct < -0.5f)
|
||||
baseScore = 80.0f;
|
||||
else if (dotProduct < 0.0f)
|
||||
baseScore = 50.0f;
|
||||
else if (dotProduct < 0.5f)
|
||||
baseScore = 20.0f;
|
||||
break;
|
||||
|
||||
case AgvDirection.Left:
|
||||
if (dotProduct > 0.0f)
|
||||
{
|
||||
if (crossProduct > 0.5f)
|
||||
baseScore = 100.0f;
|
||||
else if (crossProduct > 0.0f)
|
||||
baseScore = 70.0f;
|
||||
else if (crossProduct > -0.5f)
|
||||
baseScore = 50.0f;
|
||||
else
|
||||
baseScore = 30.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (crossProduct < -0.5f)
|
||||
baseScore = 100.0f;
|
||||
else if (crossProduct < 0.0f)
|
||||
baseScore = 70.0f;
|
||||
else if (crossProduct < 0.5f)
|
||||
baseScore = 50.0f;
|
||||
else
|
||||
baseScore = 30.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case AgvDirection.Right:
|
||||
if (dotProduct > 0.0f)
|
||||
{
|
||||
if (crossProduct < -0.5f)
|
||||
baseScore = 100.0f;
|
||||
else if (crossProduct < 0.0f)
|
||||
baseScore = 70.0f;
|
||||
else if (crossProduct < 0.5f)
|
||||
baseScore = 50.0f;
|
||||
else
|
||||
baseScore = 30.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (crossProduct > 0.5f)
|
||||
baseScore = 100.0f;
|
||||
else if (crossProduct > 0.0f)
|
||||
baseScore = 70.0f;
|
||||
else if (crossProduct > -0.5f)
|
||||
baseScore = 50.0f;
|
||||
else
|
||||
baseScore = 30.0f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return baseScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 최고 점수 노드 반환
|
||||
/// </summary>
|
||||
private MapNode GetBestCandidate(PointF movementVector, Point currentPos, List<MapNode> candidates, AgvDirection direction)
|
||||
{
|
||||
var movementLength = (float)Math.Sqrt(
|
||||
movementVector.X * movementVector.X +
|
||||
movementVector.Y * movementVector.Y
|
||||
);
|
||||
|
||||
var normalizedMovement = new PointF(
|
||||
movementVector.X / movementLength,
|
||||
movementVector.Y / movementLength
|
||||
);
|
||||
|
||||
MapNode bestCandidate = null;
|
||||
float bestScore = -1;
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var toNextVector = new PointF(
|
||||
candidate.Position.X - currentPos.X,
|
||||
candidate.Position.Y - currentPos.Y
|
||||
);
|
||||
|
||||
var toNextLength = (float)Math.Sqrt(
|
||||
toNextVector.X * toNextVector.X +
|
||||
toNextVector.Y * toNextVector.Y
|
||||
);
|
||||
|
||||
var normalizedToNext = new PointF(
|
||||
toNextVector.X / toNextLength,
|
||||
toNextVector.Y / toNextLength
|
||||
);
|
||||
|
||||
float dotProduct = (normalizedMovement.X * normalizedToNext.X) +
|
||||
(normalizedMovement.Y * normalizedToNext.Y);
|
||||
|
||||
float crossProduct = (normalizedMovement.X * normalizedToNext.Y) -
|
||||
(normalizedMovement.Y * normalizedToNext.X);
|
||||
|
||||
float score = CalculateDirectionalScore(dotProduct, crossProduct, direction);
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestCandidate = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return bestCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
153
AGVLogic/AGVNavigationCore/Utils/ImageConverterUtil.cs
Normal file
153
AGVLogic/AGVNavigationCore/Utils/ImageConverterUtil.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 이미지와 문자열 간 변환을 위한 유틸리티 클래스
|
||||
/// Base64 인코딩을 사용하여 이미지를 문자열로 변환하거나 그 반대로 수행
|
||||
/// </summary>
|
||||
public static class ImageConverterUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Image 객체를 Base64 문자열로 변환
|
||||
/// </summary>
|
||||
/// <param name="image">변환할 이미지</param>
|
||||
/// <param name="format">이미지 포맷 (기본값: PNG)</param>
|
||||
/// <returns>Base64 인코딩된 문자열, null인 경우 빈 문자열 반환</returns>
|
||||
public static string ImageToBase64(Image image, ImageFormat format = null)
|
||||
{
|
||||
if (image == null)
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
format = format ?? ImageFormat.Png;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
image.Save(memoryStream, format);
|
||||
byte[] imageBytes = memoryStream.ToArray();
|
||||
return Convert.ToBase64String(imageBytes);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"이미지 변환 실패: {ex.Message}");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파일 경로의 이미지를 Base64 문자열로 변환
|
||||
/// </summary>
|
||||
/// <param name="filePath">이미지 파일 경로</param>
|
||||
/// <param name="format">변환할 포맷 (기본값: PNG, 원본 포맷 유지하려면 null)</param>
|
||||
/// <returns>Base64 인코딩된 문자열</returns>
|
||||
public static string FileToBase64(string filePath, ImageFormat format = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
using (var image = Image.FromFile(filePath))
|
||||
{
|
||||
return ImageToBase64(image, format ?? ImageFormat.Png);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"파일 변환 실패: {ex.Message}");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base64 문자열을 Image 객체로 변환
|
||||
/// </summary>
|
||||
/// <param name="base64String">Base64 인코딩된 문자열</param>
|
||||
/// <returns>변환된 Image 객체, 실패 시 null</returns>
|
||||
public static Image Base64ToImage(string base64String)
|
||||
{
|
||||
if (string.IsNullOrEmpty(base64String))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
byte[] imageBytes = Convert.FromBase64String(base64String);
|
||||
using (var memoryStream = new MemoryStream(imageBytes))
|
||||
{
|
||||
return Image.FromStream(memoryStream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Base64 이미지 변환 실패: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base64 문자열을 Bitmap 객체로 변환
|
||||
/// Image 대신 Bitmap을 반환하므로 메모리 관리가 더 안정적
|
||||
/// </summary>
|
||||
/// <param name="base64String">Base64 인코딩된 문자열</param>
|
||||
/// <returns>변환된 Bitmap 객체, 실패 시 null</returns>
|
||||
public static Bitmap Base64ToBitmap(string base64String)
|
||||
{
|
||||
if (string.IsNullOrEmpty(base64String))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
byte[] imageBytes = Convert.FromBase64String(base64String);
|
||||
using (var memoryStream = new MemoryStream(imageBytes))
|
||||
{
|
||||
return new Bitmap(memoryStream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Base64 Bitmap 변환 실패: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base64 문자열이 유효한지 확인
|
||||
/// </summary>
|
||||
/// <param name="base64String">검증할 Base64 문자열</param>
|
||||
/// <returns>유효하면 true, 그 외 false</returns>
|
||||
public static bool IsValidBase64(string base64String)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(base64String))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
Convert.FromBase64String(base64String);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base64 이미지 데이터의 크기를 대략적으로 계산 (바이트 단위)
|
||||
/// </summary>
|
||||
/// <param name="base64String">Base64 문자열</param>
|
||||
/// <returns>예상 바이트 크기</returns>
|
||||
public static long GetApproximateSize(string base64String)
|
||||
{
|
||||
if (string.IsNullOrEmpty(base64String))
|
||||
return 0;
|
||||
|
||||
// Base64는 원본 데이터보다 약 33% 더 큼
|
||||
return (long)(base64String.Length * 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
281
AGVLogic/AGVNavigationCore/Utils/LiftCalculator.cs
Normal file
281
AGVLogic/AGVNavigationCore/Utils/LiftCalculator.cs
Normal file
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 리프트 방향 계산 유틸리티 클래스
|
||||
/// 모든 리프트 방향 계산 로직을 중앙화하여 일관성 보장
|
||||
/// </summary>
|
||||
public static class LiftCalculator
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 경로 예측 기반 리프트 방향 계산
|
||||
/// 현재 노드에서 연결된 다음 노드들을 분석하여 리프트 방향 결정
|
||||
/// </summary>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="previousPos">이전 위치</param>
|
||||
/// <param name="motorDirection">모터 방향</param>
|
||||
/// <param name="mapNodes">맵 노드 리스트 (경로 예측용)</param>
|
||||
/// <param name="tolerance">위치 허용 오차</param>
|
||||
/// <returns>리프트 계산 결과</returns>
|
||||
public static LiftCalculationResult CalculateLiftInfoWithPathPrediction(
|
||||
Point currentPos, Point previousPos, AgvDirection motorDirection,
|
||||
List<MapNode> mapNodes, int tolerance = 10)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0)
|
||||
{
|
||||
// 맵 노드 정보가 없으면 기존 방식 사용
|
||||
return CalculateLiftInfo(previousPos, currentPos, motorDirection);
|
||||
}
|
||||
|
||||
// 현재 위치에 해당하는 노드 찾기
|
||||
var currentNode = FindNodeByPosition(mapNodes, currentPos, tolerance);
|
||||
|
||||
if (currentNode == null)
|
||||
{
|
||||
// 현재 노드를 찾을 수 없으면 기존 방식 사용
|
||||
return CalculateLiftInfo(previousPos, currentPos, motorDirection);
|
||||
}
|
||||
|
||||
// 이전 위치에 해당하는 노드 찾기
|
||||
var previousNode = FindNodeByPosition(mapNodes, previousPos, tolerance);
|
||||
|
||||
Point targetPosition;
|
||||
string calculationMethod;
|
||||
|
||||
// 모터 방향에 따른 예측 방향 결정
|
||||
if (motorDirection == AgvDirection.Backward)
|
||||
{
|
||||
// 후진 모터: AGV가 리프트 쪽(목표 위치)으로 이동
|
||||
// 경로 예측 없이 단순히 현재→목표 방향 사용
|
||||
return CalculateLiftInfo(currentPos, previousPos, motorDirection);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 전진 모터: 기존 로직 (다음 노드 예측)
|
||||
var nextNodes = GetConnectedNodes(mapNodes, currentNode);
|
||||
|
||||
// 이전 노드 제외 (되돌아가는 방향 제외)
|
||||
if (previousNode != null)
|
||||
{
|
||||
nextNodes = nextNodes.Where(n => n.Id != previousNode.Id).ToList();
|
||||
}
|
||||
|
||||
if (nextNodes.Count == 1)
|
||||
{
|
||||
// 직선 경로: 다음 노드 방향으로 예측
|
||||
targetPosition = nextNodes.First().Position;
|
||||
calculationMethod = $"전진 경로 예측 ({currentNode.Id}→{nextNodes.First().Id})";
|
||||
}
|
||||
else if (nextNodes.Count > 1)
|
||||
{
|
||||
// 갈래길: 이전 위치 기반 계산 사용
|
||||
var prevResult = CalculateLiftInfo(previousPos, currentPos, motorDirection);
|
||||
prevResult.CalculationMethod += " (전진 갈래길)";
|
||||
return prevResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 연결된 노드가 없으면 기존 방식 사용
|
||||
return CalculateLiftInfo(previousPos, currentPos, motorDirection);
|
||||
}
|
||||
}
|
||||
|
||||
// 리프트 각도 계산
|
||||
var angleRadians = CalculateLiftAngleRadians(currentPos, targetPosition, motorDirection);
|
||||
var angleDegrees = angleRadians * 180.0 / Math.PI;
|
||||
|
||||
// 0-360도 범위로 정규화
|
||||
while (angleDegrees < 0) angleDegrees += 360;
|
||||
while (angleDegrees >= 360) angleDegrees -= 360;
|
||||
|
||||
var directionString = AngleToDirectionString(angleDegrees);
|
||||
|
||||
return new LiftCalculationResult
|
||||
{
|
||||
AngleRadians = angleRadians,
|
||||
AngleDegrees = angleDegrees,
|
||||
DirectionString = directionString,
|
||||
CalculationMethod = calculationMethod,
|
||||
MotorDirection = motorDirection
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 방향과 모터 방향을 기반으로 리프트 각도 계산
|
||||
/// </summary>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="targetPos">목표 위치</param>
|
||||
/// <param name="motorDirection">모터 방향</param>
|
||||
/// <returns>리프트 각도 (라디안)</returns>
|
||||
public static double CalculateLiftAngleRadians(Point currentPos, Point targetPos, AgvDirection motorDirection)
|
||||
{
|
||||
// 모터 방향에 따른 리프트 위치 계산
|
||||
if (motorDirection == AgvDirection.Forward)
|
||||
{
|
||||
// 전진 모터: AGV가 앞으로 가므로 리프트는 뒤쪽 (타겟 → 현재 방향)
|
||||
var dx = currentPos.X - targetPos.X;
|
||||
var dy = currentPos.Y - targetPos.Y;
|
||||
return Math.Atan2(dy, dx);
|
||||
}
|
||||
else if (motorDirection == AgvDirection.Backward)
|
||||
{
|
||||
// 후진 모터: AGV가 리프트 쪽으로 이동하므로 리프트는 AGV 이동 방향에 위치
|
||||
// 007→006 후진시: 리프트는 006방향(이동방향)을 향해야 함 (타겟→현재 반대방향)
|
||||
var dx = currentPos.X - targetPos.X;
|
||||
var dy = currentPos.Y - targetPos.Y;
|
||||
return Math.Atan2(dy, dx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 기본값: 전진 모터와 동일
|
||||
var dx = currentPos.X - targetPos.X;
|
||||
var dy = currentPos.Y - targetPos.Y;
|
||||
return Math.Atan2(dy, dx);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 방향과 모터 방향을 기반으로 리프트 각도 계산 (도 단위)
|
||||
/// </summary>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="targetPos">목표 위치</param>
|
||||
/// <param name="motorDirection">모터 방향</param>
|
||||
/// <returns>리프트 각도 (도)</returns>
|
||||
public static double CalculateLiftAngleDegrees(Point currentPos, Point targetPos, AgvDirection motorDirection)
|
||||
{
|
||||
var radians = CalculateLiftAngleRadians(currentPos, targetPos, motorDirection);
|
||||
var degrees = radians * 180.0 / Math.PI;
|
||||
|
||||
// 0-360도 범위로 정규화
|
||||
while (degrees < 0) degrees += 360;
|
||||
while (degrees >= 360) degrees -= 360;
|
||||
|
||||
return degrees;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 각도를 8방향 문자열로 변환 (화면 좌표계 기준)
|
||||
/// 화면 좌표계: 0°=동쪽, 90°=남쪽, 180°=서쪽, 270°=북쪽
|
||||
/// </summary>
|
||||
/// <param name="angleDegrees">각도 (도)</param>
|
||||
/// <returns>방향 문자열</returns>
|
||||
public static string AngleToDirectionString(double angleDegrees)
|
||||
{
|
||||
// 0-360도 범위로 정규화
|
||||
while (angleDegrees < 0) angleDegrees += 360;
|
||||
while (angleDegrees >= 360) angleDegrees -= 360;
|
||||
|
||||
// 8방향으로 분류 (화면 좌표계)
|
||||
if (angleDegrees >= 337.5 || angleDegrees < 22.5)
|
||||
return "동쪽(→)";
|
||||
else if (angleDegrees >= 22.5 && angleDegrees < 67.5)
|
||||
return "남동쪽(↘)";
|
||||
else if (angleDegrees >= 67.5 && angleDegrees < 112.5)
|
||||
return "남쪽(↓)";
|
||||
else if (angleDegrees >= 112.5 && angleDegrees < 157.5)
|
||||
return "남서쪽(↙)";
|
||||
else if (angleDegrees >= 157.5 && angleDegrees < 202.5)
|
||||
return "서쪽(←)";
|
||||
else if (angleDegrees >= 202.5 && angleDegrees < 247.5)
|
||||
return "북서쪽(↖)";
|
||||
else if (angleDegrees >= 247.5 && angleDegrees < 292.5)
|
||||
return "북쪽(↑)";
|
||||
else if (angleDegrees >= 292.5 && angleDegrees < 337.5)
|
||||
return "북동쪽(↗)";
|
||||
else
|
||||
return "알 수 없음";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리프트 계산 결과 정보
|
||||
/// </summary>
|
||||
public class LiftCalculationResult
|
||||
{
|
||||
public double AngleRadians { get; set; }
|
||||
public double AngleDegrees { get; set; }
|
||||
public string DirectionString { get; set; }
|
||||
public string CalculationMethod { get; set; }
|
||||
public AgvDirection MotorDirection { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 종합적인 리프트 계산 (모든 정보 포함)
|
||||
/// </summary>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="targetPos">목표 위치</param>
|
||||
/// <param name="motorDirection">모터 방향</param>
|
||||
/// <returns>리프트 계산 결과</returns>
|
||||
public static LiftCalculationResult CalculateLiftInfo(Point currentPos, Point targetPos, AgvDirection motorDirection)
|
||||
{
|
||||
var angleRadians = CalculateLiftAngleRadians(currentPos, targetPos, motorDirection);
|
||||
var angleDegrees = angleRadians * 180.0 / Math.PI;
|
||||
|
||||
// 0-360도 범위로 정규화
|
||||
while (angleDegrees < 0) angleDegrees += 360;
|
||||
while (angleDegrees >= 360) angleDegrees -= 360;
|
||||
|
||||
var directionString = AngleToDirectionString(angleDegrees);
|
||||
|
||||
string calculationMethod;
|
||||
if (motorDirection == AgvDirection.Forward)
|
||||
calculationMethod = "이동방향 + 180도 (전진모터)";
|
||||
else if (motorDirection == AgvDirection.Backward)
|
||||
calculationMethod = "이동방향과 동일 (후진모터 - 리프트는 이동방향에 위치)";
|
||||
else
|
||||
calculationMethod = "기본값 (전진모터)";
|
||||
|
||||
return new LiftCalculationResult
|
||||
{
|
||||
AngleRadians = angleRadians,
|
||||
AngleDegrees = angleDegrees,
|
||||
DirectionString = directionString,
|
||||
CalculationMethod = calculationMethod,
|
||||
MotorDirection = motorDirection
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 위치 기반 노드 찾기
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 리스트</param>
|
||||
/// <param name="position">찾을 위치</param>
|
||||
/// <param name="tolerance">허용 오차</param>
|
||||
/// <returns>해당하는 노드 또는 null</returns>
|
||||
private static MapNode FindNodeByPosition(List<MapNode> mapNodes, Point position, int tolerance)
|
||||
{
|
||||
return mapNodes.FirstOrDefault(node =>
|
||||
Math.Abs(node.Position.X - position.X) <= tolerance &&
|
||||
Math.Abs(node.Position.Y - position.Y) <= tolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드에서 연결된 다른 노드들 찾기
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 리스트</param>
|
||||
/// <param name="currentNode">현재 노드</param>
|
||||
/// <returns>연결된 노드 리스트</returns>
|
||||
private static List<MapNode> GetConnectedNodes(List<MapNode> mapNodes, MapNode currentNode)
|
||||
{
|
||||
var connectedNodes = new List<MapNode>();
|
||||
|
||||
foreach (var nodeId in currentNode.ConnectedNodes)
|
||||
{
|
||||
var connectedNode = mapNodes.FirstOrDefault(n => n.Id == nodeId);
|
||||
if (connectedNode != null)
|
||||
{
|
||||
connectedNodes.Add(connectedNode);
|
||||
}
|
||||
}
|
||||
|
||||
return connectedNodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
AGVLogic/AGVNavigationCore/Utils/TestRunner.cs
Normal file
56
AGVLogic/AGVNavigationCore/Utils/TestRunner.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// DirectionalPathfinder 테스트 실행 프로그램
|
||||
///
|
||||
/// 사용법:
|
||||
/// var runner = new TestRunner();
|
||||
/// runner.RunTests();
|
||||
/// </summary>
|
||||
public class TestRunner
|
||||
{
|
||||
public void RunTests()
|
||||
{
|
||||
string mapFilePath = @"C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.json";
|
||||
|
||||
var tester = new DirectionalPathfinderTest();
|
||||
|
||||
// 맵 파일 로드
|
||||
if (!tester.LoadMapFile(mapFilePath))
|
||||
{
|
||||
Console.WriteLine("맵 파일 로드 실패!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 노드 정보 출력
|
||||
tester.PrintAllNodes();
|
||||
|
||||
// 테스트 시나리오 1: 001 → 002 → Forward (003 기대)
|
||||
tester.PrintNodeInfo(001);
|
||||
tester.PrintNodeInfo(002);
|
||||
tester.TestDirectionalMovement(001, 002, AgvDirection.Forward);
|
||||
|
||||
// 테스트 시나리오 2: 002 → 001 → Backward (000 또는 이전 기대)
|
||||
tester.TestDirectionalMovement(002, 001, AgvDirection.Backward);
|
||||
|
||||
// 테스트 시나리오 3: 002 → 003 → Forward
|
||||
tester.PrintNodeInfo(003);
|
||||
tester.TestDirectionalMovement(002, 003, AgvDirection.Forward);
|
||||
|
||||
// 테스트 시나리오 4: 003 → 004 → Forward
|
||||
tester.PrintNodeInfo(004);
|
||||
tester.TestDirectionalMovement(003, 004, AgvDirection.Forward);
|
||||
|
||||
// 테스트 시나리오 5: 003 → 004 → Right (030 기대)
|
||||
tester.TestDirectionalMovement(003, 004, AgvDirection.Right);
|
||||
|
||||
// 테스트 시나리오 6: 004 → 003 → Backward
|
||||
tester.TestDirectionalMovement(004, 003, AgvDirection.Backward);
|
||||
|
||||
Console.WriteLine("\n\n=== 테스트 완료 ===");
|
||||
}
|
||||
}
|
||||
}
|
||||
29
AGVLogic/AGVNavigationCore/build.bat
Normal file
29
AGVLogic/AGVNavigationCore/build.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo Building V2GDecoder VC++ Project...
|
||||
|
||||
REM Check if Visual Studio 2022 is installed (Professional or Community)
|
||||
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
if exist %MSBUILD_PRO% (
|
||||
echo "Found Visual Studio 2022 Professional"
|
||||
set MSBUILD=%MSBUILD_PRO%
|
||||
) else if exist %MSBUILD_COM% (
|
||||
echo "Found Visual Studio 2022 Community"
|
||||
set MSBUILD=%MSBUILD_COM%
|
||||
) else if exist %MSBUILD_BT% (
|
||||
echo "Found Visual Studio 2022 BuildTools"
|
||||
set MSBUILD=%MSBUILD_BT%
|
||||
) else (
|
||||
echo "Visual Studio 2022 (Professional or Community) not found!"
|
||||
echo "Please install Visual Studio 2022 or update the MSBuild path."
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build Debug x64 configuration
|
||||
echo Building Debug x64 configuration...
|
||||
%MSBUILD% AGVNavigationCore.csproj
|
||||
|
||||
pause
|
||||
4
AGVLogic/AGVNavigationCore/packages.config
Normal file
4
AGVLogic/AGVNavigationCore/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.4" targetFramework="net48" />
|
||||
</packages>
|
||||
88
AGVLogic/AGVSimulator/AGVSimulator.csproj
Normal file
88
AGVLogic/AGVSimulator/AGVSimulator.csproj
Normal file
@@ -0,0 +1,88 @@
|
||||
<?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-0000-0000-0000-000000000000}</ProjectGuid>
|
||||
<OutputType>Exe</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>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</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="Forms\ComboBoxItem.cs" />
|
||||
<Compile Include="Forms\DirectionItem.cs" />
|
||||
<Compile Include="Forms\PathTestLogItem.cs" />
|
||||
<Compile Include="Forms\ProgressLogForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Forms\ProgressLogForm.Designer.cs">
|
||||
<DependentUpon>ProgressLogForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Models\SimulatorConfig.cs" />
|
||||
<Compile Include="Models\SimulationState.cs" />
|
||||
<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="Forms\SimulatorForm.resx">
|
||||
<DependentUpon>SimulatorForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="build.bat" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AGVNavigationCore\AGVNavigationCore.csproj">
|
||||
<Project>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</Project>
|
||||
<Name>AGVNavigationCore</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\AGVMapEditor\AGVMapEditor.csproj">
|
||||
<Project>{a1b2c3d4-e5f6-7890-abcd-ef1234567890}</Project>
|
||||
<Name>AGVMapEditor</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
23
AGVLogic/AGVSimulator/Forms/ComboBoxItem.cs
Normal file
23
AGVLogic/AGVSimulator/Forms/ComboBoxItem.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace AGVSimulator.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// 제네릭 콤보박스 아이템 클래스
|
||||
/// </summary>
|
||||
/// <typeparam name="T">값의 타입</typeparam>
|
||||
public class ComboBoxItem<T>
|
||||
{
|
||||
public T Value { get; }
|
||||
public string DisplayText { get; }
|
||||
|
||||
public ComboBoxItem(T value, string displayText)
|
||||
{
|
||||
Value = value;
|
||||
DisplayText = displayText;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return DisplayText;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
AGVLogic/AGVSimulator/Forms/DirectionItem.cs
Normal file
24
AGVLogic/AGVSimulator/Forms/DirectionItem.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVSimulator.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// 방향 콤보박스용 아이템 클래스
|
||||
/// </summary>
|
||||
public class DirectionItem
|
||||
{
|
||||
public AgvDirection Direction { get; }
|
||||
public string DisplayText { get; }
|
||||
|
||||
public DirectionItem(AgvDirection direction, string displayText)
|
||||
{
|
||||
Direction = direction;
|
||||
DisplayText = displayText;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return DisplayText;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
AGVLogic/AGVSimulator/Forms/PathTestLogItem.cs
Normal file
25
AGVLogic/AGVSimulator/Forms/PathTestLogItem.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace AGVSimulator.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 예측 테스트 결과 로그 항목
|
||||
/// </summary>
|
||||
public class PathTestLogItem
|
||||
{
|
||||
public string PreviousPosition { get; set; }
|
||||
public string MotorDirection { get; set; } // 정방향 or 역방향
|
||||
public string CurrentPosition { get; set; }
|
||||
public string TargetPosition { get; set; }
|
||||
public string DockingPosition { get; set; } // 도킹위치
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string DetailedPath { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
public PathTestLogItem()
|
||||
{
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
221
AGVLogic/AGVSimulator/Forms/ProgressLogForm.Designer.cs
generated
Normal file
221
AGVLogic/AGVSimulator/Forms/ProgressLogForm.Designer.cs
generated
Normal file
@@ -0,0 +1,221 @@
|
||||
namespace AGVSimulator.Forms
|
||||
{
|
||||
partial class ProgressLogForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this._statusLabel = new System.Windows.Forms.Label();
|
||||
this._progressBar = new System.Windows.Forms.ProgressBar();
|
||||
this._logListView = new System.Windows.Forms.ListView();
|
||||
this.colPreviousPosition = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.colMotorDirection = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.colCurrentPosition = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.colTargetPosition = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.colDockingPosition = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.colSuccess = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.colMessage = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.colDetailedPath = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.colTimestamp = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.buttonPanel = new System.Windows.Forms.Panel();
|
||||
this._saveCSVButton = new System.Windows.Forms.Button();
|
||||
this._closeButton = new System.Windows.Forms.Button();
|
||||
this._cancelButton = new System.Windows.Forms.Button();
|
||||
this.buttonPanel.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// _statusLabel
|
||||
//
|
||||
this._statusLabel.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._statusLabel.Font = new System.Drawing.Font("맑은 고딕", 10F, System.Drawing.FontStyle.Bold);
|
||||
this._statusLabel.Location = new System.Drawing.Point(0, 0);
|
||||
this._statusLabel.Name = "_statusLabel";
|
||||
this._statusLabel.Padding = new System.Windows.Forms.Padding(10, 5, 10, 5);
|
||||
this._statusLabel.Size = new System.Drawing.Size(1200, 30);
|
||||
this._statusLabel.TabIndex = 0;
|
||||
this._statusLabel.Text = "준비 중...";
|
||||
this._statusLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// _progressBar
|
||||
//
|
||||
this._progressBar.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._progressBar.Location = new System.Drawing.Point(0, 30);
|
||||
this._progressBar.Maximum = 100;
|
||||
this._progressBar.Name = "_progressBar";
|
||||
this._progressBar.Size = new System.Drawing.Size(1200, 25);
|
||||
this._progressBar.TabIndex = 1;
|
||||
//
|
||||
// _logListView
|
||||
//
|
||||
this._logListView.BackColor = System.Drawing.Color.White;
|
||||
this._logListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
|
||||
this.colPreviousPosition,
|
||||
this.colMotorDirection,
|
||||
this.colCurrentPosition,
|
||||
this.colTargetPosition,
|
||||
this.colDockingPosition,
|
||||
this.colSuccess,
|
||||
this.colMessage,
|
||||
this.colDetailedPath,
|
||||
this.colTimestamp});
|
||||
this._logListView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this._logListView.Font = new System.Drawing.Font("맑은 고딕", 9F);
|
||||
this._logListView.FullRowSelect = true;
|
||||
this._logListView.GridLines = true;
|
||||
this._logListView.HideSelection = false;
|
||||
this._logListView.Location = new System.Drawing.Point(0, 55);
|
||||
this._logListView.Name = "_logListView";
|
||||
this._logListView.Size = new System.Drawing.Size(1200, 495);
|
||||
this._logListView.TabIndex = 2;
|
||||
this._logListView.UseCompatibleStateImageBehavior = false;
|
||||
this._logListView.View = System.Windows.Forms.View.Details;
|
||||
//
|
||||
// colPreviousPosition
|
||||
//
|
||||
this.colPreviousPosition.Text = "이전위치";
|
||||
this.colPreviousPosition.Width = 80;
|
||||
//
|
||||
// colMotorDirection
|
||||
//
|
||||
this.colMotorDirection.Text = "모터방향";
|
||||
this.colMotorDirection.Width = 80;
|
||||
//
|
||||
// colCurrentPosition
|
||||
//
|
||||
this.colCurrentPosition.Text = "현재위치";
|
||||
this.colCurrentPosition.Width = 80;
|
||||
//
|
||||
// colTargetPosition
|
||||
//
|
||||
this.colTargetPosition.Text = "대상위치";
|
||||
this.colTargetPosition.Width = 80;
|
||||
//
|
||||
// colDockingPosition
|
||||
//
|
||||
this.colDockingPosition.Text = "도킹위치";
|
||||
this.colDockingPosition.Width = 80;
|
||||
//
|
||||
// colSuccess
|
||||
//
|
||||
this.colSuccess.Text = "성공";
|
||||
this.colSuccess.Width = 50;
|
||||
//
|
||||
// colMessage
|
||||
//
|
||||
this.colMessage.Text = "메세지";
|
||||
this.colMessage.Width = 180;
|
||||
//
|
||||
// colDetailedPath
|
||||
//
|
||||
this.colDetailedPath.Text = "상세경로";
|
||||
this.colDetailedPath.Width = 350;
|
||||
//
|
||||
// colTimestamp
|
||||
//
|
||||
this.colTimestamp.Text = "시간";
|
||||
this.colTimestamp.Width = 90;
|
||||
//
|
||||
// buttonPanel
|
||||
//
|
||||
this.buttonPanel.Controls.Add(this._saveCSVButton);
|
||||
this.buttonPanel.Controls.Add(this._closeButton);
|
||||
this.buttonPanel.Controls.Add(this._cancelButton);
|
||||
this.buttonPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.buttonPanel.Location = new System.Drawing.Point(0, 550);
|
||||
this.buttonPanel.Name = "buttonPanel";
|
||||
this.buttonPanel.Size = new System.Drawing.Size(1200, 50);
|
||||
this.buttonPanel.TabIndex = 3;
|
||||
//
|
||||
// _saveCSVButton
|
||||
//
|
||||
this._saveCSVButton.Location = new System.Drawing.Point(230, 10);
|
||||
this._saveCSVButton.Name = "_saveCSVButton";
|
||||
this._saveCSVButton.Size = new System.Drawing.Size(100, 30);
|
||||
this._saveCSVButton.TabIndex = 2;
|
||||
this._saveCSVButton.Text = "CSV 저장";
|
||||
this._saveCSVButton.UseVisualStyleBackColor = true;
|
||||
this._saveCSVButton.Click += new System.EventHandler(this.OnSaveCSV_Click);
|
||||
//
|
||||
// _closeButton
|
||||
//
|
||||
this._closeButton.Enabled = false;
|
||||
this._closeButton.Location = new System.Drawing.Point(120, 10);
|
||||
this._closeButton.Name = "_closeButton";
|
||||
this._closeButton.Size = new System.Drawing.Size(100, 30);
|
||||
this._closeButton.TabIndex = 1;
|
||||
this._closeButton.Text = "닫기";
|
||||
this._closeButton.UseVisualStyleBackColor = true;
|
||||
this._closeButton.Click += new System.EventHandler(this.OnClose_Click);
|
||||
//
|
||||
// _cancelButton
|
||||
//
|
||||
this._cancelButton.Location = new System.Drawing.Point(10, 10);
|
||||
this._cancelButton.Name = "_cancelButton";
|
||||
this._cancelButton.Size = new System.Drawing.Size(100, 30);
|
||||
this._cancelButton.TabIndex = 0;
|
||||
this._cancelButton.Text = "취소";
|
||||
this._cancelButton.UseVisualStyleBackColor = true;
|
||||
this._cancelButton.Click += new System.EventHandler(this.OnCancel_Click);
|
||||
//
|
||||
// ProgressLogForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(1200, 600);
|
||||
this.Controls.Add(this._logListView);
|
||||
this.Controls.Add(this.buttonPanel);
|
||||
this.Controls.Add(this._progressBar);
|
||||
this.Controls.Add(this._statusLabel);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
|
||||
this.MinimumSize = new System.Drawing.Size(800, 400);
|
||||
this.Name = "ProgressLogForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "경로 예측 테스트 진행 상황";
|
||||
this.buttonPanel.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label _statusLabel;
|
||||
private System.Windows.Forms.ProgressBar _progressBar;
|
||||
private System.Windows.Forms.ListView _logListView;
|
||||
private System.Windows.Forms.ColumnHeader colPreviousPosition;
|
||||
private System.Windows.Forms.ColumnHeader colMotorDirection;
|
||||
private System.Windows.Forms.ColumnHeader colCurrentPosition;
|
||||
private System.Windows.Forms.ColumnHeader colTargetPosition;
|
||||
private System.Windows.Forms.ColumnHeader colDockingPosition;
|
||||
private System.Windows.Forms.ColumnHeader colSuccess;
|
||||
private System.Windows.Forms.ColumnHeader colMessage;
|
||||
private System.Windows.Forms.ColumnHeader colDetailedPath;
|
||||
private System.Windows.Forms.ColumnHeader colTimestamp;
|
||||
private System.Windows.Forms.Panel buttonPanel;
|
||||
private System.Windows.Forms.Button _cancelButton;
|
||||
private System.Windows.Forms.Button _closeButton;
|
||||
private System.Windows.Forms.Button _saveCSVButton;
|
||||
}
|
||||
}
|
||||
244
AGVLogic/AGVSimulator/Forms/ProgressLogForm.cs
Normal file
244
AGVLogic/AGVSimulator/Forms/ProgressLogForm.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AGVSimulator.Forms
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 경로 예측 테스트 진행 상황 로그 표시 폼
|
||||
/// </summary>
|
||||
public partial class ProgressLogForm : Form
|
||||
{
|
||||
private List<PathTestLogItem> _logItems;
|
||||
|
||||
/// <summary>
|
||||
/// 취소 요청 여부
|
||||
/// </summary>
|
||||
public bool CancelRequested { get; private set; }
|
||||
|
||||
public ProgressLogForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
CancelRequested = false;
|
||||
_logItems = new List<PathTestLogItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로그 추가 (PathTestLogItem)
|
||||
/// </summary>
|
||||
public void AddLogItem(PathTestLogItem item)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action<PathTestLogItem>(AddLogItem), item);
|
||||
return;
|
||||
}
|
||||
|
||||
_logItems.Add(item);
|
||||
|
||||
var listItem = new ListViewItem(item.PreviousPosition ?? "-");
|
||||
listItem.SubItems.Add(item.MotorDirection ?? "-");
|
||||
listItem.SubItems.Add(item.CurrentPosition ?? "-");
|
||||
listItem.SubItems.Add(item.TargetPosition ?? "-");
|
||||
listItem.SubItems.Add(item.DockingPosition ?? "-");
|
||||
listItem.SubItems.Add(item.Success ? "O" : "X");
|
||||
listItem.SubItems.Add(item.Message ?? "-");
|
||||
listItem.SubItems.Add(item.DetailedPath ?? "-");
|
||||
listItem.SubItems.Add(item.Timestamp.ToString("HH:mm:ss"));
|
||||
|
||||
// 성공 여부에 따라 색상 설정
|
||||
if (!item.Success)
|
||||
{
|
||||
listItem.BackColor = Color.LightPink;
|
||||
}
|
||||
|
||||
var dockpos = item.DockingPosition ?? string.Empty;
|
||||
var targerpos = item.TargetPosition ?? string.Empty;
|
||||
if (dockpos.Equals("충전기") && targerpos.StartsWith("0015"))
|
||||
listItem.ForeColor = Color.DarkViolet;
|
||||
|
||||
_logListView.Items.Add(listItem);
|
||||
_logListView.EnsureVisible(_logListView.Items.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 간단한 텍스트 로그 추가 (상태 메시지용)
|
||||
/// </summary>
|
||||
public void AppendLog(string message)
|
||||
{
|
||||
var item = new PathTestLogItem
|
||||
{
|
||||
Message = message,
|
||||
Success = true
|
||||
};
|
||||
AddLogItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상태 메시지 업데이트
|
||||
/// </summary>
|
||||
public void UpdateStatus(string status)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action<string>(UpdateStatus), status);
|
||||
return;
|
||||
}
|
||||
|
||||
_statusLabel.Text = status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 프로그레스바 업데이트
|
||||
/// </summary>
|
||||
public void UpdateProgress(int value, int maximum)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action<int, int>(UpdateProgress), value, maximum);
|
||||
return;
|
||||
}
|
||||
|
||||
_progressBar.Maximum = maximum;
|
||||
_progressBar.Value = Math.Min(value, maximum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 작업 완료 시 호출
|
||||
/// </summary>
|
||||
public void SetCompleted()
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action(SetCompleted));
|
||||
return;
|
||||
}
|
||||
|
||||
_cancelButton.Enabled = false;
|
||||
_closeButton.Enabled = true;
|
||||
UpdateStatus("작업 완료");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 작업 취소 시 호출
|
||||
/// </summary>
|
||||
public void SetCancelled()
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action(SetCancelled));
|
||||
return;
|
||||
}
|
||||
|
||||
_cancelButton.Enabled = false;
|
||||
_closeButton.Enabled = true;
|
||||
UpdateStatus("작업 취소됨");
|
||||
}
|
||||
|
||||
private void OnCancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
"진행 중인 작업을 취소하시겠습니까?",
|
||||
"취소 확인",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (result == DialogResult.Yes)
|
||||
{
|
||||
CancelRequested = true;
|
||||
_cancelButton.Enabled = false;
|
||||
UpdateStatus("취소 요청됨...");
|
||||
AppendLog("사용자가 취소를 요청했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSaveCSV_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_logItems.Count == 0)
|
||||
{
|
||||
MessageBox.Show("저장할 데이터가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var saveDialog = new SaveFileDialog())
|
||||
{
|
||||
saveDialog.Filter = "CSV 파일 (*.csv)|*.csv|모든 파일 (*.*)|*.*";
|
||||
saveDialog.DefaultExt = "csv";
|
||||
saveDialog.FileName = $"경로예측테스트_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
|
||||
|
||||
if (saveDialog.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
try
|
||||
{
|
||||
SaveToCSV(saveDialog.FileName);
|
||||
MessageBox.Show($"CSV 파일이 저장되었습니다.\n{saveDialog.FileName}",
|
||||
"저장 완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
|
||||
var prc = new System.Diagnostics.Process();
|
||||
prc.StartInfo = new System.Diagnostics.ProcessStartInfo("explorer", saveDialog.FileName);
|
||||
prc.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"CSV 저장 중 오류가 발생했습니다:\n{ex.Message}",
|
||||
"오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CSV 파일로 저장
|
||||
/// </summary>
|
||||
private void SaveToCSV(string filePath)
|
||||
{
|
||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8))
|
||||
{
|
||||
// 헤더 작성
|
||||
writer.WriteLine("이전위치,모터방향,현재위치,대상위치,도킹위치,성공,메세지,상세경로,시간");
|
||||
|
||||
// 데이터 작성
|
||||
foreach (var item in _logItems)
|
||||
{
|
||||
var line = $"{EscapeCSV(item.PreviousPosition)}," +
|
||||
$"{EscapeCSV(item.MotorDirection)}," +
|
||||
$"{EscapeCSV(item.CurrentPosition)}," +
|
||||
$"{EscapeCSV(item.TargetPosition)}," +
|
||||
$"{EscapeCSV(item.DockingPosition)}," +
|
||||
$"{(item.Success ? "O" : "X")}," +
|
||||
$"{EscapeCSV(item.Message)}," +
|
||||
$"{EscapeCSV(item.DetailedPath)}," +
|
||||
$"{item.Timestamp:yyyy-MM-dd HH:mm:ss}";
|
||||
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CSV 셀 데이터 이스케이프 처리
|
||||
/// </summary>
|
||||
private string EscapeCSV(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return "";
|
||||
|
||||
// 쉼표, 큰따옴표, 줄바꿈이 있으면 큰따옴표로 감싸고 내부 큰따옴표는 두 개로
|
||||
if (value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r"))
|
||||
{
|
||||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private void OnClose_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
941
AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
Normal file
941
AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
Normal file
@@ -0,0 +1,941 @@
|
||||
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.components = new System.ComponentModel.Container();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SimulatorForm));
|
||||
this._menuStrip = new System.Windows.Forms.MenuStrip();
|
||||
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.openMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.reloadMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.맵저장SToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.맵다른이름으로저장ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.launchMapEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.simulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.startSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.stopSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.resetToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.viewToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.fitToMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.resetZoomToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this._toolStrip = new System.Windows.Forms.ToolStrip();
|
||||
this.reloadMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.launchMapEditorToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.startSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.stopSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.resetToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.btAllReset = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.resetZoomToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.toolStripButton1 = new System.Windows.Forms.ToolStripButton();
|
||||
this.btPredict = new System.Windows.Forms.ToolStripButton();
|
||||
this.btMakeMap = new System.Windows.Forms.ToolStripButton();
|
||||
this._statusStrip = new System.Windows.Forms.StatusStrip();
|
||||
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.prb1 = new System.Windows.Forms.ToolStripProgressBar();
|
||||
this._controlPanel = new System.Windows.Forms.Panel();
|
||||
this.groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
this.propertyNode = new System.Windows.Forms.PropertyGrid();
|
||||
this._statusGroup = new System.Windows.Forms.GroupBox();
|
||||
this._pathLengthLabel = new System.Windows.Forms.Label();
|
||||
this._agvCountLabel = new System.Windows.Forms.Label();
|
||||
this._simulationStatusLabel = new System.Windows.Forms.Label();
|
||||
this._pathGroup = new System.Windows.Forms.GroupBox();
|
||||
this.btPath2 = new System.Windows.Forms.Button();
|
||||
this._clearPathButton = new System.Windows.Forms.Button();
|
||||
this._targetCalcButton = new System.Windows.Forms.Button();
|
||||
this._avoidRotationCheckBox = new System.Windows.Forms.CheckBox();
|
||||
this._targetNodeCombo = new System.Windows.Forms.ComboBox();
|
||||
this.targetNodeLabel = new System.Windows.Forms.Label();
|
||||
this._startNodeCombo = new System.Windows.Forms.ComboBox();
|
||||
this.startNodeLabel = new System.Windows.Forms.Label();
|
||||
this._agvControlGroup = new System.Windows.Forms.GroupBox();
|
||||
this._setPositionButton = new System.Windows.Forms.Button();
|
||||
this._rfidTextBox = new System.Windows.Forms.TextBox();
|
||||
this._rfidLabel = new System.Windows.Forms.Label();
|
||||
this._directionCombo = new System.Windows.Forms.ComboBox();
|
||||
this._directionLabel = new System.Windows.Forms.Label();
|
||||
this._stopSimulationButton = new System.Windows.Forms.Button();
|
||||
this._startSimulationButton = new System.Windows.Forms.Button();
|
||||
this._removeAgvButton = new System.Windows.Forms.Button();
|
||||
this._addAgvButton = new System.Windows.Forms.Button();
|
||||
this._agvListCombo = new System.Windows.Forms.ComboBox();
|
||||
this._canvasPanel = new System.Windows.Forms.Panel();
|
||||
this.lbPredict = new System.Windows.Forms.RichTextBox();
|
||||
this._agvInfoPanel = new System.Windows.Forms.Panel();
|
||||
this._pathDebugLabel = new System.Windows.Forms.TextBox();
|
||||
this._agvInfoTitleLabel = new System.Windows.Forms.Label();
|
||||
this._liftDirectionLabel = new System.Windows.Forms.Label();
|
||||
this._motorDirectionLabel = new System.Windows.Forms.Label();
|
||||
this.timer1 = new System.Windows.Forms.Timer(this.components);
|
||||
this.btSelectMapEditor = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this._menuStrip.SuspendLayout();
|
||||
this._toolStrip.SuspendLayout();
|
||||
this._statusStrip.SuspendLayout();
|
||||
this._controlPanel.SuspendLayout();
|
||||
this.groupBox1.SuspendLayout();
|
||||
this._statusGroup.SuspendLayout();
|
||||
this._pathGroup.SuspendLayout();
|
||||
this._agvControlGroup.SuspendLayout();
|
||||
this._canvasPanel.SuspendLayout();
|
||||
this._agvInfoPanel.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// _menuStrip
|
||||
//
|
||||
this._menuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.fileToolStripMenuItem,
|
||||
this.simulationToolStripMenuItem,
|
||||
this.viewToolStripMenuItem,
|
||||
this.helpToolStripMenuItem});
|
||||
this._menuStrip.Location = new System.Drawing.Point(0, 0);
|
||||
this._menuStrip.Name = "_menuStrip";
|
||||
this._menuStrip.Size = new System.Drawing.Size(1248, 24);
|
||||
this._menuStrip.TabIndex = 0;
|
||||
this._menuStrip.Text = "menuStrip";
|
||||
//
|
||||
// fileToolStripMenuItem
|
||||
//
|
||||
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.openMapToolStripMenuItem,
|
||||
this.reloadMapToolStripMenuItem,
|
||||
this.맵저장SToolStripMenuItem,
|
||||
this.맵다른이름으로저장ToolStripMenuItem,
|
||||
this.toolStripSeparator1,
|
||||
this.launchMapEditorToolStripMenuItem,
|
||||
this.btSelectMapEditor,
|
||||
this.toolStripSeparator4,
|
||||
this.exitToolStripMenuItem});
|
||||
this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
|
||||
this.fileToolStripMenuItem.Size = new System.Drawing.Size(57, 20);
|
||||
this.fileToolStripMenuItem.Text = "파일(&F)";
|
||||
//
|
||||
// openMapToolStripMenuItem
|
||||
//
|
||||
this.openMapToolStripMenuItem.Name = "openMapToolStripMenuItem";
|
||||
this.openMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
|
||||
this.openMapToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.openMapToolStripMenuItem.Text = "맵 열기(&O)...";
|
||||
this.openMapToolStripMenuItem.Click += new System.EventHandler(this.OnOpenMap_Click);
|
||||
//
|
||||
// reloadMapToolStripMenuItem
|
||||
//
|
||||
this.reloadMapToolStripMenuItem.Name = "reloadMapToolStripMenuItem";
|
||||
this.reloadMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.R)));
|
||||
this.reloadMapToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.reloadMapToolStripMenuItem.Text = "맵 다시열기(&R)";
|
||||
this.reloadMapToolStripMenuItem.Click += new System.EventHandler(this.OnReloadMap_Click);
|
||||
//
|
||||
// 맵저장SToolStripMenuItem
|
||||
//
|
||||
this.맵저장SToolStripMenuItem.Name = "맵저장SToolStripMenuItem";
|
||||
this.맵저장SToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.맵저장SToolStripMenuItem.Text = "맵 저장(&S)";
|
||||
this.맵저장SToolStripMenuItem.Click += new System.EventHandler(this.맵저장SToolStripMenuItem_Click);
|
||||
//
|
||||
// 맵다른이름으로저장ToolStripMenuItem
|
||||
//
|
||||
this.맵다른이름으로저장ToolStripMenuItem.Name = "맵다른이름으로저장ToolStripMenuItem";
|
||||
this.맵다른이름으로저장ToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.맵다른이름으로저장ToolStripMenuItem.Text = "맵 다른 이름으로 저장";
|
||||
this.맵다른이름으로저장ToolStripMenuItem.Click += new System.EventHandler(this.btMapSaveAs_Click);
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
this.toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
this.toolStripSeparator1.Size = new System.Drawing.Size(218, 6);
|
||||
//
|
||||
// launchMapEditorToolStripMenuItem
|
||||
//
|
||||
this.launchMapEditorToolStripMenuItem.Name = "launchMapEditorToolStripMenuItem";
|
||||
this.launchMapEditorToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.M)));
|
||||
this.launchMapEditorToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.launchMapEditorToolStripMenuItem.Text = "MapEditor 실행(&M)";
|
||||
this.launchMapEditorToolStripMenuItem.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
this.toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
this.toolStripSeparator4.Size = new System.Drawing.Size(218, 6);
|
||||
//
|
||||
// exitToolStripMenuItem
|
||||
//
|
||||
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
|
||||
this.exitToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Alt | System.Windows.Forms.Keys.F4)));
|
||||
this.exitToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.exitToolStripMenuItem.Text = "종료(&X)";
|
||||
this.exitToolStripMenuItem.Click += new System.EventHandler(this.OnExit_Click);
|
||||
//
|
||||
// simulationToolStripMenuItem
|
||||
//
|
||||
this.simulationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.startSimulationToolStripMenuItem,
|
||||
this.stopSimulationToolStripMenuItem,
|
||||
this.resetToolStripMenuItem});
|
||||
this.simulationToolStripMenuItem.Name = "simulationToolStripMenuItem";
|
||||
this.simulationToolStripMenuItem.Size = new System.Drawing.Size(94, 20);
|
||||
this.simulationToolStripMenuItem.Text = "시뮬레이션(&S)";
|
||||
//
|
||||
// startSimulationToolStripMenuItem
|
||||
//
|
||||
this.startSimulationToolStripMenuItem.Name = "startSimulationToolStripMenuItem";
|
||||
this.startSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F5;
|
||||
this.startSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.startSimulationToolStripMenuItem.Text = "시작(&S)";
|
||||
this.startSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// stopSimulationToolStripMenuItem
|
||||
//
|
||||
this.stopSimulationToolStripMenuItem.Name = "stopSimulationToolStripMenuItem";
|
||||
this.stopSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F6;
|
||||
this.stopSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.stopSimulationToolStripMenuItem.Text = "정지(&T)";
|
||||
this.stopSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// resetToolStripMenuItem
|
||||
//
|
||||
this.resetToolStripMenuItem.Name = "resetToolStripMenuItem";
|
||||
this.resetToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F7;
|
||||
this.resetToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.resetToolStripMenuItem.Text = "초기화(&R)";
|
||||
this.resetToolStripMenuItem.Click += new System.EventHandler(this.OnReset_Click);
|
||||
//
|
||||
// viewToolStripMenuItem
|
||||
//
|
||||
this.viewToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.fitToMapToolStripMenuItem,
|
||||
this.resetZoomToolStripMenuItem});
|
||||
this.viewToolStripMenuItem.Name = "viewToolStripMenuItem";
|
||||
this.viewToolStripMenuItem.Size = new System.Drawing.Size(59, 20);
|
||||
this.viewToolStripMenuItem.Text = "보기(&V)";
|
||||
//
|
||||
// fitToMapToolStripMenuItem
|
||||
//
|
||||
this.fitToMapToolStripMenuItem.Name = "fitToMapToolStripMenuItem";
|
||||
this.fitToMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.F)));
|
||||
this.fitToMapToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
|
||||
this.fitToMapToolStripMenuItem.Text = "맵 맞춤(&F)";
|
||||
this.fitToMapToolStripMenuItem.Click += new System.EventHandler(this.OnFitToMap_Click);
|
||||
//
|
||||
// resetZoomToolStripMenuItem
|
||||
//
|
||||
this.resetZoomToolStripMenuItem.Name = "resetZoomToolStripMenuItem";
|
||||
this.resetZoomToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.D0)));
|
||||
this.resetZoomToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
|
||||
this.resetZoomToolStripMenuItem.Text = "줌 초기화(&Z)";
|
||||
this.resetZoomToolStripMenuItem.Click += new System.EventHandler(this.OnResetZoom_Click);
|
||||
//
|
||||
// helpToolStripMenuItem
|
||||
//
|
||||
this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.aboutToolStripMenuItem});
|
||||
this.helpToolStripMenuItem.Name = "helpToolStripMenuItem";
|
||||
this.helpToolStripMenuItem.Size = new System.Drawing.Size(72, 20);
|
||||
this.helpToolStripMenuItem.Text = "도움말(&H)";
|
||||
//
|
||||
// aboutToolStripMenuItem
|
||||
//
|
||||
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
|
||||
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(123, 22);
|
||||
this.aboutToolStripMenuItem.Text = "정보(&A)...";
|
||||
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.OnAbout_Click);
|
||||
//
|
||||
// _toolStrip
|
||||
//
|
||||
this._toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.reloadMapToolStripButton,
|
||||
this.launchMapEditorToolStripButton,
|
||||
this.toolStripSeparator2,
|
||||
this.startSimulationToolStripButton,
|
||||
this.stopSimulationToolStripButton,
|
||||
this.resetToolStripButton,
|
||||
this.btAllReset,
|
||||
this.toolStripSeparator3,
|
||||
this.fitToMapToolStripButton,
|
||||
this.resetZoomToolStripButton,
|
||||
this.toolStripSeparator5,
|
||||
this.toolStripButton1,
|
||||
this.btPredict,
|
||||
this.btMakeMap});
|
||||
this._toolStrip.Location = new System.Drawing.Point(0, 24);
|
||||
this._toolStrip.Name = "_toolStrip";
|
||||
this._toolStrip.Size = new System.Drawing.Size(1248, 25);
|
||||
this._toolStrip.TabIndex = 1;
|
||||
this._toolStrip.Text = "toolStrip";
|
||||
//
|
||||
// reloadMapToolStripButton
|
||||
//
|
||||
this.reloadMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.reloadMapToolStripButton.Name = "reloadMapToolStripButton";
|
||||
this.reloadMapToolStripButton.Size = new System.Drawing.Size(59, 22);
|
||||
this.reloadMapToolStripButton.Text = "다시열기";
|
||||
this.reloadMapToolStripButton.ToolTipText = "현재 맵을 다시 로드합니다";
|
||||
this.reloadMapToolStripButton.Click += new System.EventHandler(this.OnReloadMap_Click);
|
||||
//
|
||||
// launchMapEditorToolStripButton
|
||||
//
|
||||
this.launchMapEditorToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.launchMapEditorToolStripButton.Name = "launchMapEditorToolStripButton";
|
||||
this.launchMapEditorToolStripButton.Size = new System.Drawing.Size(66, 22);
|
||||
this.launchMapEditorToolStripButton.Text = "MapEditor";
|
||||
this.launchMapEditorToolStripButton.ToolTipText = "MapEditor를 실행합니다";
|
||||
this.launchMapEditorToolStripButton.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
this.toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
this.toolStripSeparator2.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// startSimulationToolStripButton
|
||||
//
|
||||
this.startSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.startSimulationToolStripButton.Name = "startSimulationToolStripButton";
|
||||
this.startSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
|
||||
this.startSimulationToolStripButton.Text = "시뮬레이션 시작";
|
||||
this.startSimulationToolStripButton.ToolTipText = "시뮬레이션을 시작합니다";
|
||||
this.startSimulationToolStripButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// stopSimulationToolStripButton
|
||||
//
|
||||
this.stopSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.stopSimulationToolStripButton.Name = "stopSimulationToolStripButton";
|
||||
this.stopSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
|
||||
this.stopSimulationToolStripButton.Text = "시뮬레이션 정지";
|
||||
this.stopSimulationToolStripButton.ToolTipText = "시뮬레이션을 정지합니다";
|
||||
this.stopSimulationToolStripButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// resetToolStripButton
|
||||
//
|
||||
this.resetToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.resetToolStripButton.Name = "resetToolStripButton";
|
||||
this.resetToolStripButton.Size = new System.Drawing.Size(47, 22);
|
||||
this.resetToolStripButton.Text = "초기화";
|
||||
this.resetToolStripButton.ToolTipText = "시뮬레이션을 초기화합니다";
|
||||
this.resetToolStripButton.Click += new System.EventHandler(this.OnReset_Click);
|
||||
//
|
||||
// btAllReset
|
||||
//
|
||||
this.btAllReset.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.btAllReset.Name = "btAllReset";
|
||||
this.btAllReset.Size = new System.Drawing.Size(71, 22);
|
||||
this.btAllReset.Text = "전체초기화";
|
||||
this.btAllReset.ToolTipText = "시뮬레이션을 초기화합니다";
|
||||
this.btAllReset.Click += new System.EventHandler(this.btAllReset_Click);
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
this.toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
this.toolStripSeparator3.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// fitToMapToolStripButton
|
||||
//
|
||||
this.fitToMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.fitToMapToolStripButton.Name = "fitToMapToolStripButton";
|
||||
this.fitToMapToolStripButton.Size = new System.Drawing.Size(51, 22);
|
||||
this.fitToMapToolStripButton.Text = "맵 맞춤";
|
||||
this.fitToMapToolStripButton.ToolTipText = "맵 전체를 화면에 맞춥니다";
|
||||
this.fitToMapToolStripButton.Click += new System.EventHandler(this.OnFitToMap_Click);
|
||||
//
|
||||
// resetZoomToolStripButton
|
||||
//
|
||||
this.resetZoomToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.resetZoomToolStripButton.Name = "resetZoomToolStripButton";
|
||||
this.resetZoomToolStripButton.Size = new System.Drawing.Size(63, 22);
|
||||
this.resetZoomToolStripButton.Text = "줌 초기화";
|
||||
this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
|
||||
this.resetZoomToolStripButton.Click += new System.EventHandler(this.OnResetZoom_Click);
|
||||
//
|
||||
// toolStripSeparator5
|
||||
//
|
||||
this.toolStripSeparator5.Name = "toolStripSeparator5";
|
||||
this.toolStripSeparator5.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// toolStripButton1
|
||||
//
|
||||
this.toolStripButton1.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButton1.Image")));
|
||||
this.toolStripButton1.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.toolStripButton1.Name = "toolStripButton1";
|
||||
this.toolStripButton1.Size = new System.Drawing.Size(111, 22);
|
||||
this.toolStripButton1.Text = "전체경로테스트";
|
||||
this.toolStripButton1.Click += new System.EventHandler(this.toolStripButton1_Click);
|
||||
//
|
||||
// btPredict
|
||||
//
|
||||
this.btPredict.Image = ((System.Drawing.Image)(resources.GetObject("btPredict.Image")));
|
||||
this.btPredict.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btPredict.Name = "btPredict";
|
||||
this.btPredict.Size = new System.Drawing.Size(107, 22);
|
||||
this.btPredict.Text = "다음 행동 예측";
|
||||
this.btPredict.Click += new System.EventHandler(this.btPredict_Click);
|
||||
//
|
||||
// btMakeMap
|
||||
//
|
||||
this.btMakeMap.Image = ((System.Drawing.Image)(resources.GetObject("btMakeMap.Image")));
|
||||
this.btMakeMap.ImageTransparentColor = System.Drawing.Color.Magenta;
|
||||
this.btMakeMap.Name = "btMakeMap";
|
||||
this.btMakeMap.Size = new System.Drawing.Size(63, 22);
|
||||
this.btMakeMap.Text = "맵기록";
|
||||
this.btMakeMap.Click += new System.EventHandler(this.btMakeMap_Click);
|
||||
//
|
||||
// _statusStrip
|
||||
//
|
||||
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this._statusLabel,
|
||||
this._coordLabel,
|
||||
this.prb1});
|
||||
this._statusStrip.Location = new System.Drawing.Point(0, 689);
|
||||
this._statusStrip.Name = "_statusStrip";
|
||||
this._statusStrip.Size = new System.Drawing.Size(1248, 22);
|
||||
this._statusStrip.TabIndex = 2;
|
||||
this._statusStrip.Text = "statusStrip";
|
||||
//
|
||||
// _statusLabel
|
||||
//
|
||||
this._statusLabel.Name = "_statusLabel";
|
||||
this._statusLabel.Size = new System.Drawing.Size(31, 17);
|
||||
this._statusLabel.Text = "준비";
|
||||
//
|
||||
// _coordLabel
|
||||
//
|
||||
this._coordLabel.Name = "_coordLabel";
|
||||
this._coordLabel.Size = new System.Drawing.Size(0, 17);
|
||||
//
|
||||
// prb1
|
||||
//
|
||||
this.prb1.Name = "prb1";
|
||||
this.prb1.Size = new System.Drawing.Size(200, 16);
|
||||
//
|
||||
// _controlPanel
|
||||
//
|
||||
this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
|
||||
this._controlPanel.Controls.Add(this.groupBox1);
|
||||
this._controlPanel.Controls.Add(this._statusGroup);
|
||||
this._controlPanel.Controls.Add(this._pathGroup);
|
||||
this._controlPanel.Controls.Add(this._agvControlGroup);
|
||||
this._controlPanel.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this._controlPanel.Location = new System.Drawing.Point(1015, 49);
|
||||
this._controlPanel.Name = "_controlPanel";
|
||||
this._controlPanel.Size = new System.Drawing.Size(233, 640);
|
||||
this._controlPanel.TabIndex = 3;
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
this.groupBox1.Controls.Add(this.propertyNode);
|
||||
this.groupBox1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.groupBox1.Location = new System.Drawing.Point(0, 546);
|
||||
this.groupBox1.Name = "groupBox1";
|
||||
this.groupBox1.Size = new System.Drawing.Size(233, 94);
|
||||
this.groupBox1.TabIndex = 4;
|
||||
this.groupBox1.TabStop = false;
|
||||
this.groupBox1.Text = "노드 정보";
|
||||
//
|
||||
// propertyNode
|
||||
//
|
||||
this.propertyNode.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.propertyNode.Location = new System.Drawing.Point(3, 17);
|
||||
this.propertyNode.Name = "propertyNode";
|
||||
this.propertyNode.Size = new System.Drawing.Size(227, 74);
|
||||
this.propertyNode.TabIndex = 0;
|
||||
//
|
||||
// _statusGroup
|
||||
//
|
||||
this._statusGroup.Controls.Add(this._pathLengthLabel);
|
||||
this._statusGroup.Controls.Add(this._agvCountLabel);
|
||||
this._statusGroup.Controls.Add(this._simulationStatusLabel);
|
||||
this._statusGroup.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._statusGroup.Location = new System.Drawing.Point(0, 446);
|
||||
this._statusGroup.Name = "_statusGroup";
|
||||
this._statusGroup.Size = new System.Drawing.Size(233, 100);
|
||||
this._statusGroup.TabIndex = 3;
|
||||
this._statusGroup.TabStop = false;
|
||||
this._statusGroup.Text = "상태 정보";
|
||||
//
|
||||
// _pathLengthLabel
|
||||
//
|
||||
this._pathLengthLabel.AutoSize = true;
|
||||
this._pathLengthLabel.Location = new System.Drawing.Point(10, 65);
|
||||
this._pathLengthLabel.Name = "_pathLengthLabel";
|
||||
this._pathLengthLabel.Size = new System.Drawing.Size(71, 12);
|
||||
this._pathLengthLabel.TabIndex = 2;
|
||||
this._pathLengthLabel.Text = "경로 길이: -";
|
||||
//
|
||||
// _agvCountLabel
|
||||
//
|
||||
this._agvCountLabel.AutoSize = true;
|
||||
this._agvCountLabel.Location = new System.Drawing.Point(10, 45);
|
||||
this._agvCountLabel.Name = "_agvCountLabel";
|
||||
this._agvCountLabel.Size = new System.Drawing.Size(60, 12);
|
||||
this._agvCountLabel.TabIndex = 1;
|
||||
this._agvCountLabel.Text = "AGV 수: 0";
|
||||
//
|
||||
// _simulationStatusLabel
|
||||
//
|
||||
this._simulationStatusLabel.AutoSize = true;
|
||||
this._simulationStatusLabel.Location = new System.Drawing.Point(10, 25);
|
||||
this._simulationStatusLabel.Name = "_simulationStatusLabel";
|
||||
this._simulationStatusLabel.Size = new System.Drawing.Size(97, 12);
|
||||
this._simulationStatusLabel.TabIndex = 0;
|
||||
this._simulationStatusLabel.Text = "시뮬레이션: 정지";
|
||||
//
|
||||
// _pathGroup
|
||||
//
|
||||
this._pathGroup.Controls.Add(this.btPath2);
|
||||
this._pathGroup.Controls.Add(this._clearPathButton);
|
||||
this._pathGroup.Controls.Add(this._targetCalcButton);
|
||||
this._pathGroup.Controls.Add(this._avoidRotationCheckBox);
|
||||
this._pathGroup.Controls.Add(this._targetNodeCombo);
|
||||
this._pathGroup.Controls.Add(this.targetNodeLabel);
|
||||
this._pathGroup.Controls.Add(this._startNodeCombo);
|
||||
this._pathGroup.Controls.Add(this.startNodeLabel);
|
||||
this._pathGroup.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._pathGroup.Location = new System.Drawing.Point(0, 214);
|
||||
this._pathGroup.Name = "_pathGroup";
|
||||
this._pathGroup.Size = new System.Drawing.Size(233, 232);
|
||||
this._pathGroup.TabIndex = 1;
|
||||
this._pathGroup.TabStop = false;
|
||||
this._pathGroup.Text = "경로 제어";
|
||||
//
|
||||
// btPath2
|
||||
//
|
||||
this.btPath2.Location = new System.Drawing.Point(12, 177);
|
||||
this.btPath2.Name = "btPath2";
|
||||
this.btPath2.Size = new System.Drawing.Size(106, 25);
|
||||
this.btPath2.TabIndex = 10;
|
||||
this.btPath2.Text = "경로 계산2";
|
||||
this.btPath2.UseVisualStyleBackColor = true;
|
||||
this.btPath2.Click += new System.EventHandler(this.btPath2_Click);
|
||||
//
|
||||
// _clearPathButton
|
||||
//
|
||||
this._clearPathButton.Location = new System.Drawing.Point(121, 177);
|
||||
this._clearPathButton.Name = "_clearPathButton";
|
||||
this._clearPathButton.Size = new System.Drawing.Size(111, 25);
|
||||
this._clearPathButton.TabIndex = 6;
|
||||
this._clearPathButton.Text = "경로 지우기";
|
||||
this._clearPathButton.UseVisualStyleBackColor = true;
|
||||
this._clearPathButton.Click += new System.EventHandler(this.OnClearPath_Click);
|
||||
//
|
||||
// _targetCalcButton
|
||||
//
|
||||
this._targetCalcButton.Location = new System.Drawing.Point(10, 148);
|
||||
this._targetCalcButton.Name = "_targetCalcButton";
|
||||
this._targetCalcButton.Size = new System.Drawing.Size(70, 25);
|
||||
this._targetCalcButton.TabIndex = 9;
|
||||
this._targetCalcButton.Text = "타겟계산";
|
||||
this._targetCalcButton.UseVisualStyleBackColor = true;
|
||||
this._targetCalcButton.Click += new System.EventHandler(this.OnTargetCalc_Click);
|
||||
//
|
||||
// _avoidRotationCheckBox
|
||||
//
|
||||
this._avoidRotationCheckBox.AutoSize = true;
|
||||
this._avoidRotationCheckBox.Location = new System.Drawing.Point(10, 126);
|
||||
this._avoidRotationCheckBox.Name = "_avoidRotationCheckBox";
|
||||
this._avoidRotationCheckBox.Size = new System.Drawing.Size(104, 16);
|
||||
this._avoidRotationCheckBox.TabIndex = 7;
|
||||
this._avoidRotationCheckBox.Text = "회전 구간 회피";
|
||||
this._avoidRotationCheckBox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// _targetNodeCombo
|
||||
//
|
||||
this._targetNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._targetNodeCombo.Font = new System.Drawing.Font("돋움체", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._targetNodeCombo.Location = new System.Drawing.Point(10, 97);
|
||||
this._targetNodeCombo.Name = "_targetNodeCombo";
|
||||
this._targetNodeCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._targetNodeCombo.TabIndex = 3;
|
||||
//
|
||||
// targetNodeLabel
|
||||
//
|
||||
this.targetNodeLabel.AutoSize = true;
|
||||
this.targetNodeLabel.Location = new System.Drawing.Point(10, 75);
|
||||
this.targetNodeLabel.Name = "targetNodeLabel";
|
||||
this.targetNodeLabel.Size = new System.Drawing.Size(63, 12);
|
||||
this.targetNodeLabel.TabIndex = 2;
|
||||
this.targetNodeLabel.Text = "목표 RFID:";
|
||||
//
|
||||
// _startNodeCombo
|
||||
//
|
||||
this._startNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._startNodeCombo.Font = new System.Drawing.Font("돋움체", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._startNodeCombo.Location = new System.Drawing.Point(10, 45);
|
||||
this._startNodeCombo.Name = "_startNodeCombo";
|
||||
this._startNodeCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._startNodeCombo.TabIndex = 1;
|
||||
//
|
||||
// startNodeLabel
|
||||
//
|
||||
this.startNodeLabel.AutoSize = true;
|
||||
this.startNodeLabel.Location = new System.Drawing.Point(10, 25);
|
||||
this.startNodeLabel.Name = "startNodeLabel";
|
||||
this.startNodeLabel.Size = new System.Drawing.Size(63, 12);
|
||||
this.startNodeLabel.TabIndex = 0;
|
||||
this.startNodeLabel.Text = "시작 RFID:";
|
||||
//
|
||||
// _agvControlGroup
|
||||
//
|
||||
this._agvControlGroup.Controls.Add(this._setPositionButton);
|
||||
this._agvControlGroup.Controls.Add(this._rfidTextBox);
|
||||
this._agvControlGroup.Controls.Add(this._rfidLabel);
|
||||
this._agvControlGroup.Controls.Add(this._directionCombo);
|
||||
this._agvControlGroup.Controls.Add(this._directionLabel);
|
||||
this._agvControlGroup.Controls.Add(this._stopSimulationButton);
|
||||
this._agvControlGroup.Controls.Add(this._startSimulationButton);
|
||||
this._agvControlGroup.Controls.Add(this._removeAgvButton);
|
||||
this._agvControlGroup.Controls.Add(this._addAgvButton);
|
||||
this._agvControlGroup.Controls.Add(this._agvListCombo);
|
||||
this._agvControlGroup.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._agvControlGroup.Location = new System.Drawing.Point(0, 0);
|
||||
this._agvControlGroup.Name = "_agvControlGroup";
|
||||
this._agvControlGroup.Size = new System.Drawing.Size(233, 214);
|
||||
this._agvControlGroup.TabIndex = 0;
|
||||
this._agvControlGroup.TabStop = false;
|
||||
this._agvControlGroup.Text = "AGV 제어";
|
||||
//
|
||||
// _setPositionButton
|
||||
//
|
||||
this._setPositionButton.Location = new System.Drawing.Point(160, 138);
|
||||
this._setPositionButton.Name = "_setPositionButton";
|
||||
this._setPositionButton.Size = new System.Drawing.Size(60, 39);
|
||||
this._setPositionButton.TabIndex = 7;
|
||||
this._setPositionButton.Text = "위치설정";
|
||||
this._setPositionButton.UseVisualStyleBackColor = true;
|
||||
this._setPositionButton.Click += new System.EventHandler(this.OnSetPosition_Click);
|
||||
//
|
||||
// _rfidTextBox
|
||||
//
|
||||
this._rfidTextBox.Location = new System.Drawing.Point(10, 140);
|
||||
this._rfidTextBox.Name = "_rfidTextBox";
|
||||
this._rfidTextBox.Size = new System.Drawing.Size(140, 21);
|
||||
this._rfidTextBox.TabIndex = 6;
|
||||
this._rfidTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.OnRfidTextBox_KeyPress);
|
||||
//
|
||||
// _rfidLabel
|
||||
//
|
||||
this._rfidLabel.AutoSize = true;
|
||||
this._rfidLabel.Location = new System.Drawing.Point(10, 120);
|
||||
this._rfidLabel.Name = "_rfidLabel";
|
||||
this._rfidLabel.Size = new System.Drawing.Size(87, 12);
|
||||
this._rfidLabel.TabIndex = 5;
|
||||
this._rfidLabel.Text = "RFID 현재위치:";
|
||||
//
|
||||
// _directionCombo
|
||||
//
|
||||
this._directionCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._directionCombo.FormattingEnabled = true;
|
||||
this._directionCombo.Location = new System.Drawing.Point(10, 185);
|
||||
this._directionCombo.Name = "_directionCombo";
|
||||
this._directionCombo.Size = new System.Drawing.Size(140, 20);
|
||||
this._directionCombo.TabIndex = 8;
|
||||
//
|
||||
// _directionLabel
|
||||
//
|
||||
this._directionLabel.AutoSize = true;
|
||||
this._directionLabel.Location = new System.Drawing.Point(10, 165);
|
||||
this._directionLabel.Name = "_directionLabel";
|
||||
this._directionLabel.Size = new System.Drawing.Size(85, 12);
|
||||
this._directionLabel.TabIndex = 9;
|
||||
this._directionLabel.Text = "모터 구동방향:";
|
||||
//
|
||||
// _stopSimulationButton
|
||||
//
|
||||
this._stopSimulationButton.Location = new System.Drawing.Point(120, 85);
|
||||
this._stopSimulationButton.Name = "_stopSimulationButton";
|
||||
this._stopSimulationButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._stopSimulationButton.TabIndex = 4;
|
||||
this._stopSimulationButton.Text = "시뮬레이션 정지";
|
||||
this._stopSimulationButton.UseVisualStyleBackColor = true;
|
||||
this._stopSimulationButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// _startSimulationButton
|
||||
//
|
||||
this._startSimulationButton.Location = new System.Drawing.Point(10, 85);
|
||||
this._startSimulationButton.Name = "_startSimulationButton";
|
||||
this._startSimulationButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._startSimulationButton.TabIndex = 3;
|
||||
this._startSimulationButton.Text = "시뮬레이션 시작";
|
||||
this._startSimulationButton.UseVisualStyleBackColor = true;
|
||||
this._startSimulationButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// _removeAgvButton
|
||||
//
|
||||
this._removeAgvButton.Location = new System.Drawing.Point(120, 55);
|
||||
this._removeAgvButton.Name = "_removeAgvButton";
|
||||
this._removeAgvButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._removeAgvButton.TabIndex = 2;
|
||||
this._removeAgvButton.Text = "AGV 제거";
|
||||
this._removeAgvButton.UseVisualStyleBackColor = true;
|
||||
this._removeAgvButton.Click += new System.EventHandler(this.OnRemoveAGV_Click);
|
||||
//
|
||||
// _addAgvButton
|
||||
//
|
||||
this._addAgvButton.Location = new System.Drawing.Point(10, 55);
|
||||
this._addAgvButton.Name = "_addAgvButton";
|
||||
this._addAgvButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._addAgvButton.TabIndex = 1;
|
||||
this._addAgvButton.Text = "AGV 추가";
|
||||
this._addAgvButton.UseVisualStyleBackColor = true;
|
||||
this._addAgvButton.Click += new System.EventHandler(this.OnAddAGV_Click);
|
||||
//
|
||||
// _agvListCombo
|
||||
//
|
||||
this._agvListCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._agvListCombo.Location = new System.Drawing.Point(10, 25);
|
||||
this._agvListCombo.Name = "_agvListCombo";
|
||||
this._agvListCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._agvListCombo.TabIndex = 0;
|
||||
this._agvListCombo.SelectedIndexChanged += new System.EventHandler(this.OnAGVList_SelectedIndexChanged);
|
||||
//
|
||||
// _canvasPanel
|
||||
//
|
||||
this._canvasPanel.Controls.Add(this.lbPredict);
|
||||
this._canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this._canvasPanel.Location = new System.Drawing.Point(0, 129);
|
||||
this._canvasPanel.Name = "_canvasPanel";
|
||||
this._canvasPanel.Size = new System.Drawing.Size(1015, 560);
|
||||
this._canvasPanel.TabIndex = 4;
|
||||
//
|
||||
// lbPredict
|
||||
//
|
||||
this.lbPredict.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.lbPredict.Location = new System.Drawing.Point(0, 513);
|
||||
this.lbPredict.Name = "lbPredict";
|
||||
this.lbPredict.Size = new System.Drawing.Size(1015, 47);
|
||||
this.lbPredict.TabIndex = 0;
|
||||
this.lbPredict.Text = "";
|
||||
//
|
||||
// _agvInfoPanel
|
||||
//
|
||||
this._agvInfoPanel.BackColor = System.Drawing.Color.LightBlue;
|
||||
this._agvInfoPanel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this._agvInfoPanel.Controls.Add(this._pathDebugLabel);
|
||||
this._agvInfoPanel.Controls.Add(this._agvInfoTitleLabel);
|
||||
this._agvInfoPanel.Controls.Add(this._liftDirectionLabel);
|
||||
this._agvInfoPanel.Controls.Add(this._motorDirectionLabel);
|
||||
this._agvInfoPanel.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._agvInfoPanel.Location = new System.Drawing.Point(0, 49);
|
||||
this._agvInfoPanel.Name = "_agvInfoPanel";
|
||||
this._agvInfoPanel.Size = new System.Drawing.Size(1015, 80);
|
||||
this._agvInfoPanel.TabIndex = 5;
|
||||
//
|
||||
// _pathDebugLabel
|
||||
//
|
||||
this._pathDebugLabel.BackColor = System.Drawing.Color.LightBlue;
|
||||
this._pathDebugLabel.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
this._pathDebugLabel.Font = new System.Drawing.Font("굴림", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._pathDebugLabel.Location = new System.Drawing.Point(10, 30);
|
||||
this._pathDebugLabel.Multiline = true;
|
||||
this._pathDebugLabel.Name = "_pathDebugLabel";
|
||||
this._pathDebugLabel.Size = new System.Drawing.Size(947, 45);
|
||||
this._pathDebugLabel.TabIndex = 4;
|
||||
this._pathDebugLabel.Text = "경로: 설정되지 않음";
|
||||
//
|
||||
// _agvInfoTitleLabel
|
||||
//
|
||||
this._agvInfoTitleLabel.AutoSize = true;
|
||||
this._agvInfoTitleLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._agvInfoTitleLabel.Location = new System.Drawing.Point(10, 8);
|
||||
this._agvInfoTitleLabel.Name = "_agvInfoTitleLabel";
|
||||
this._agvInfoTitleLabel.Size = new System.Drawing.Size(91, 15);
|
||||
this._agvInfoTitleLabel.TabIndex = 0;
|
||||
this._agvInfoTitleLabel.Text = "AGV 상태 정보:";
|
||||
//
|
||||
// _liftDirectionLabel
|
||||
//
|
||||
this._liftDirectionLabel.AutoSize = true;
|
||||
this._liftDirectionLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._liftDirectionLabel.Location = new System.Drawing.Point(120, 8);
|
||||
this._liftDirectionLabel.Name = "_liftDirectionLabel";
|
||||
this._liftDirectionLabel.Size = new System.Drawing.Size(83, 15);
|
||||
this._liftDirectionLabel.TabIndex = 1;
|
||||
this._liftDirectionLabel.Text = "리프트 방향: -";
|
||||
//
|
||||
// _motorDirectionLabel
|
||||
//
|
||||
this._motorDirectionLabel.AutoSize = true;
|
||||
this._motorDirectionLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._motorDirectionLabel.Location = new System.Drawing.Point(250, 8);
|
||||
this._motorDirectionLabel.Name = "_motorDirectionLabel";
|
||||
this._motorDirectionLabel.Size = new System.Drawing.Size(71, 15);
|
||||
this._motorDirectionLabel.TabIndex = 2;
|
||||
this._motorDirectionLabel.Text = "모터 방향: -";
|
||||
//
|
||||
// timer1
|
||||
//
|
||||
this.timer1.Interval = 500;
|
||||
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
|
||||
//
|
||||
// btSelectMapEditor
|
||||
//
|
||||
this.btSelectMapEditor.Name = "btSelectMapEditor";
|
||||
this.btSelectMapEditor.Size = new System.Drawing.Size(221, 22);
|
||||
this.btSelectMapEditor.Text = "Mapeditor 선택";
|
||||
this.btSelectMapEditor.Click += new System.EventHandler(this.btSelectMapEditor_Click);
|
||||
//
|
||||
// SimulatorForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(1248, 711);
|
||||
this.Controls.Add(this._canvasPanel);
|
||||
this.Controls.Add(this._agvInfoPanel);
|
||||
this.Controls.Add(this._controlPanel);
|
||||
this.Controls.Add(this._statusStrip);
|
||||
this.Controls.Add(this._toolStrip);
|
||||
this.Controls.Add(this._menuStrip);
|
||||
this.MainMenuStrip = this._menuStrip;
|
||||
this.Name = "SimulatorForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "AGV 시뮬레이터";
|
||||
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
|
||||
this._menuStrip.ResumeLayout(false);
|
||||
this._menuStrip.PerformLayout();
|
||||
this._toolStrip.ResumeLayout(false);
|
||||
this._toolStrip.PerformLayout();
|
||||
this._statusStrip.ResumeLayout(false);
|
||||
this._statusStrip.PerformLayout();
|
||||
this._controlPanel.ResumeLayout(false);
|
||||
this.groupBox1.ResumeLayout(false);
|
||||
this._statusGroup.ResumeLayout(false);
|
||||
this._statusGroup.PerformLayout();
|
||||
this._pathGroup.ResumeLayout(false);
|
||||
this._pathGroup.PerformLayout();
|
||||
this._agvControlGroup.ResumeLayout(false);
|
||||
this._agvControlGroup.PerformLayout();
|
||||
this._canvasPanel.ResumeLayout(false);
|
||||
this._agvInfoPanel.ResumeLayout(false);
|
||||
this._agvInfoPanel.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.MenuStrip _menuStrip;
|
||||
private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem openMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
||||
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem simulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem startSimulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem stopSimulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem resetToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem viewToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem fitToMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem resetZoomToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem helpToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStrip _toolStrip;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
||||
private System.Windows.Forms.ToolStripButton startSimulationToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton stopSimulationToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton resetToolStripButton;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
||||
private System.Windows.Forms.ToolStripButton fitToMapToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton resetZoomToolStripButton;
|
||||
private System.Windows.Forms.StatusStrip _statusStrip;
|
||||
private System.Windows.Forms.ToolStripStatusLabel _statusLabel;
|
||||
private System.Windows.Forms.ToolStripStatusLabel _coordLabel;
|
||||
private System.Windows.Forms.Panel _controlPanel;
|
||||
private System.Windows.Forms.GroupBox _agvControlGroup;
|
||||
private System.Windows.Forms.ComboBox _agvListCombo;
|
||||
private System.Windows.Forms.Button _addAgvButton;
|
||||
private System.Windows.Forms.Button _removeAgvButton;
|
||||
private System.Windows.Forms.Button _startSimulationButton;
|
||||
private System.Windows.Forms.Button _stopSimulationButton;
|
||||
private System.Windows.Forms.GroupBox _pathGroup;
|
||||
private System.Windows.Forms.Label startNodeLabel;
|
||||
private System.Windows.Forms.ComboBox _startNodeCombo;
|
||||
private System.Windows.Forms.Label targetNodeLabel;
|
||||
private System.Windows.Forms.ComboBox _targetNodeCombo;
|
||||
private System.Windows.Forms.Button _clearPathButton;
|
||||
private System.Windows.Forms.Button _targetCalcButton;
|
||||
private System.Windows.Forms.CheckBox _avoidRotationCheckBox;
|
||||
private System.Windows.Forms.GroupBox _statusGroup;
|
||||
private System.Windows.Forms.Label _simulationStatusLabel;
|
||||
private System.Windows.Forms.Label _agvCountLabel;
|
||||
private System.Windows.Forms.Label _pathLengthLabel;
|
||||
private System.Windows.Forms.Panel _canvasPanel;
|
||||
private System.Windows.Forms.Label _rfidLabel;
|
||||
private System.Windows.Forms.TextBox _rfidTextBox;
|
||||
private System.Windows.Forms.Button _setPositionButton;
|
||||
private System.Windows.Forms.ComboBox _directionCombo;
|
||||
private System.Windows.Forms.Label _directionLabel;
|
||||
private System.Windows.Forms.ToolStripButton btAllReset;
|
||||
private System.Windows.Forms.ToolStripMenuItem reloadMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem launchMapEditorToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
||||
private System.Windows.Forms.ToolStripButton reloadMapToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton launchMapEditorToolStripButton;
|
||||
private System.Windows.Forms.Panel _agvInfoPanel;
|
||||
private System.Windows.Forms.Label _liftDirectionLabel;
|
||||
private System.Windows.Forms.Label _motorDirectionLabel;
|
||||
private System.Windows.Forms.Label _agvInfoTitleLabel;
|
||||
private System.Windows.Forms.TextBox _pathDebugLabel;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator5;
|
||||
private System.Windows.Forms.ToolStripButton toolStripButton1;
|
||||
private System.Windows.Forms.ToolStripProgressBar prb1;
|
||||
private System.Windows.Forms.ToolStripButton btPredict;
|
||||
private System.Windows.Forms.RichTextBox lbPredict;
|
||||
private System.Windows.Forms.Timer timer1;
|
||||
private System.Windows.Forms.ToolStripButton btMakeMap;
|
||||
private System.Windows.Forms.ToolStripMenuItem 맵저장SToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 맵다른이름으로저장ToolStripMenuItem;
|
||||
private System.Windows.Forms.GroupBox groupBox1;
|
||||
private System.Windows.Forms.PropertyGrid propertyNode;
|
||||
private System.Windows.Forms.Button btPath2;
|
||||
private System.Windows.Forms.ToolStripMenuItem btSelectMapEditor;
|
||||
}
|
||||
}
|
||||
2626
AGVLogic/AGVSimulator/Forms/SimulatorForm.cs
Normal file
2626
AGVLogic/AGVSimulator/Forms/SimulatorForm.cs
Normal file
File diff suppressed because it is too large
Load Diff
178
AGVLogic/AGVSimulator/Forms/SimulatorForm.resx
Normal file
178
AGVLogic/AGVSimulator/Forms/SimulatorForm.resx
Normal file
@@ -0,0 +1,178 @@
|
||||
<?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 through the TypeConverter architecture.
|
||||
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" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<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" type="xsd:string" use="required" />
|
||||
</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="_menuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="_toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>132, 17</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="toolStripButton1.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
|
||||
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
|
||||
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
|
||||
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
|
||||
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
|
||||
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
|
||||
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
|
||||
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
|
||||
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
|
||||
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="btPredict.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
|
||||
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
|
||||
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
|
||||
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
|
||||
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
|
||||
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
|
||||
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
|
||||
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
|
||||
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
|
||||
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<data name="btMakeMap.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
|
||||
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
|
||||
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
|
||||
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
|
||||
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
|
||||
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
|
||||
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
|
||||
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
|
||||
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
|
||||
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="_statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>237, 17</value>
|
||||
</metadata>
|
||||
<metadata name="timer1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>352, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
135
AGVLogic/AGVSimulator/Models/SimulationState.cs
Normal file
135
AGVLogic/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
|
||||
}
|
||||
}
|
||||
128
AGVLogic/AGVSimulator/Models/SimulatorConfig.cs
Normal file
128
AGVLogic/AGVSimulator/Models/SimulatorConfig.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVSimulator.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 시뮬레이터 환경 설정 클래스
|
||||
/// </summary>
|
||||
public class SimulatorConfig
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// MapEditor 실행 파일 경로
|
||||
/// </summary>
|
||||
public string MapEditorExecutablePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막으로 로드한 맵 파일 경로
|
||||
/// </summary>
|
||||
public string LastMapFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 설정 파일 자동 저장 여부
|
||||
/// </summary>
|
||||
public bool AutoSave { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 프로그램 시작시 마지막 맵 파일을 자동으로 로드할지 여부
|
||||
/// </summary>
|
||||
public bool AutoLoadLastMapFile { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Methods
|
||||
|
||||
/// <summary>
|
||||
/// 설정 파일 기본 경로
|
||||
/// </summary>
|
||||
private static string ConfigFilePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AGVSimulator",
|
||||
"config.json");
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에서 로드
|
||||
/// </summary>
|
||||
/// <returns>로드된 설정 객체</returns>
|
||||
public static SimulatorConfig Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(ConfigFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(ConfigFilePath);
|
||||
return JsonConvert.DeserializeObject<SimulatorConfig>(json) ?? new SimulatorConfig();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
return new SimulatorConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에 저장
|
||||
/// </summary>
|
||||
/// <param name="config">저장할 설정 객체</param>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public static bool Save(SimulatorConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(ConfigFilePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
File.WriteAllText(ConfigFilePath, json);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Instance Methods
|
||||
|
||||
/// <summary>
|
||||
/// 현재 설정을 저장
|
||||
/// </summary>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public bool Save()
|
||||
{
|
||||
return Save(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MapEditor 실행 파일 경로 유효성 확인
|
||||
/// </summary>
|
||||
/// <returns>유효한 경로인지 여부</returns>
|
||||
public bool IsMapEditorPathValid()
|
||||
{
|
||||
return !string.IsNullOrEmpty(MapEditorExecutablePath) &&
|
||||
File.Exists(MapEditorExecutablePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 맵 파일이 존재하는지 확인
|
||||
/// </summary>
|
||||
/// <returns>마지막 맵 파일이 유효한지 여부</returns>
|
||||
public bool HasValidLastMapFile()
|
||||
{
|
||||
return !string.IsNullOrEmpty(LastMapFilePath) && File.Exists(LastMapFilePath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
44
AGVLogic/AGVSimulator/Program.cs
Normal file
44
AGVLogic/AGVSimulator/Program.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AGVSimulator.Forms;
|
||||
|
||||
namespace AGVSimulator
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 시뮬레이터 프로그램 진입점
|
||||
/// </summary>
|
||||
static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 콘솔 출력 (타임스탬프 포함)
|
||||
/// </summary>
|
||||
public static void WriteLine(string message)
|
||||
{
|
||||
string timestampedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
|
||||
Console.WriteLine(timestampedMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애플리케이션의 주 진입점입니다.
|
||||
/// </summary>
|
||||
/// <param name="args">명령줄 인수</param>
|
||||
[STAThread]
|
||||
static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new SimulatorForm());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ERROR] 시뮬레이터 실행 중 오류: {ex.Message}");
|
||||
Console.WriteLine($"[ERROR] 스택 트레이스: {ex.StackTrace}");
|
||||
|
||||
MessageBox.Show($"시뮬레이터 실행 중 오류가 발생했습니다:\n{ex.Message}",
|
||||
"시스템 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
AGVLogic/AGVSimulator/Properties/AssemblyInfo.cs
Normal file
36
AGVLogic/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")]
|
||||
29
AGVLogic/AGVSimulator/build.bat
Normal file
29
AGVLogic/AGVSimulator/build.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo Building V2GDecoder VC++ Project...
|
||||
|
||||
REM Check if Visual Studio 2022 is installed (Professional or Community)
|
||||
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
if exist %MSBUILD_PRO% (
|
||||
echo "Found Visual Studio 2022 Professional"
|
||||
set MSBUILD=%MSBUILD_PRO%
|
||||
) else if exist %MSBUILD_COM% (
|
||||
echo "Found Visual Studio 2022 Community"
|
||||
set MSBUILD=%MSBUILD_COM%
|
||||
) else if exist %MSBUILD_BT% (
|
||||
echo "Found Visual Studio 2022 BuildTools"
|
||||
set MSBUILD=%MSBUILD_BT%
|
||||
) else (
|
||||
echo "Visual Studio 2022 (Professional or Community) not found!"
|
||||
echo "Please install Visual Studio 2022 or update the MSBuild path."
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build Debug x64 configuration
|
||||
echo Building Debug x64 configuration...
|
||||
%MSBUILD% AGVSimulator.csproj
|
||||
|
||||
pause
|
||||
4
AGVLogic/AGVSimulator/packages.config
Normal file
4
AGVLogic/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>
|
||||
13
AGVLogic/EnigProtocol/.gitignore
vendored
Normal file
13
AGVLogic/EnigProtocol/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
obj
|
||||
bin
|
||||
*.user
|
||||
*.v12
|
||||
*.suo
|
||||
.git
|
||||
.vs
|
||||
Debug
|
||||
__vm
|
||||
*.pdb
|
||||
desktop.ini
|
||||
packages
|
||||
~*.xlsx
|
||||
53
AGVLogic/EnigProtocol/ENIGProtocol.Tests/EEProtocolTests.cs
Normal file
53
AGVLogic/EnigProtocol/ENIGProtocol.Tests/EEProtocolTests.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Xunit;
|
||||
using ENIG;
|
||||
|
||||
namespace ENIGProtocol.Tests
|
||||
{
|
||||
public class EEProtocolTests
|
||||
{
|
||||
[Fact]
|
||||
public void TestCRC16Calculation()
|
||||
{
|
||||
// 테스트 데이터
|
||||
byte[] testData = new byte[] { 0x02,0x00,0xFF }; //payload에는 stx, len, ... crc,etx 는 제외한다
|
||||
|
||||
// CRC16 계산
|
||||
var protocol = new EEProtocol();
|
||||
ushort crc = protocol.CalculateCRC16(testData);
|
||||
|
||||
// 예상 결과와 비교
|
||||
Assert.Equal(0x1789, crc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestPacketCreation()
|
||||
{
|
||||
// 패킷 생성 테스트
|
||||
var protocol = new EEProtocol();
|
||||
byte[] packet = protocol.CreatePacket(0x01, 0x02, new byte[] { 0x03, 0x04 });
|
||||
|
||||
// 패킷 구조 검증
|
||||
Assert.Equal(0x02, packet[0]); // STX
|
||||
Assert.Equal(0x04, packet[1]); // Length
|
||||
Assert.Equal(0x01, packet[2]); // ID
|
||||
Assert.Equal(0x02, packet[3]); // Command
|
||||
Assert.Equal(0x03, packet[4]); // Data[0]
|
||||
Assert.Equal(0x04, packet[5]); // Data[1]
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestPacketParsing()
|
||||
{
|
||||
// 패킷 파싱 테스트
|
||||
var protocol = new EEProtocol();
|
||||
|
||||
//byte[] testPacket = new byte[] { 0x02, 0x04, 0x01, 0x02, 0x03, 0x04, 0x12, 0x34, 0x03 };
|
||||
byte[] testPacket = new byte[] { 0x02, 0x02, 0x00, 0xFF, 0x89, 0x17, 0x03 };
|
||||
|
||||
|
||||
bool result = protocol.ParsePacket(testPacket);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||
<FileUpgradeFlags>
|
||||
</FileUpgradeFlags>
|
||||
<UpgradeBackupLocation>
|
||||
</UpgradeBackupLocation>
|
||||
<OldToolsVersion>2.0</OldToolsVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\enigprotocol\enigprotocol.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
10
AGVLogic/EnigProtocol/ENIGProtocol.Tests/UnitTest1.cs
Normal file
10
AGVLogic/EnigProtocol/ENIGProtocol.Tests/UnitTest1.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ENIGProtocol.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
43
AGVLogic/EnigProtocol/ENIGProtocol.sln
Normal file
43
AGVLogic/EnigProtocol/ENIGProtocol.sln
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Express 15 for Windows Desktop
|
||||
VisualStudioVersion = 15.0.28307.1000
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "솔루션 항목", "솔루션 항목", "{0A11874A-E5C6-4170-9787-1FFF7AF0D289}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitignore = .gitignore
|
||||
ReadMe.MD = ReadMe.MD
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleProject", "sample\SampleProject.csproj", "{FAB31C8A-7DCF-4152-8A82-76F3C10BABA4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ENIGProtocol.Tests", "ENIGProtocol.Tests\ENIGProtocol.Tests.csproj", "{3A677629-1F08-49B2-BC75-58282E439FD4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ENIGProtocol", "enigprotocol\ENIGProtocol.csproj", "{499D8912-4B96-41E5-A70D-CFE797883D65}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FAB31C8A-7DCF-4152-8A82-76F3C10BABA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FAB31C8A-7DCF-4152-8A82-76F3C10BABA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FAB31C8A-7DCF-4152-8A82-76F3C10BABA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FAB31C8A-7DCF-4152-8A82-76F3C10BABA4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3A677629-1F08-49B2-BC75-58282E439FD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3A677629-1F08-49B2-BC75-58282E439FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3A677629-1F08-49B2-BC75-58282E439FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3A677629-1F08-49B2-BC75-58282E439FD4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{499D8912-4B96-41E5-A70D-CFE797883D65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{499D8912-4B96-41E5-A70D-CFE797883D65}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{499D8912-4B96-41E5-A70D-CFE797883D65}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{499D8912-4B96-41E5-A70D-CFE797883D65}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {07331835-C46C-4B93-965F-AD2714F97F88}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
246
AGVLogic/EnigProtocol/ReadMe.MD
Normal file
246
AGVLogic/EnigProtocol/ReadMe.MD
Normal file
@@ -0,0 +1,246 @@
|
||||
# ENIG Protocol
|
||||
|
||||
ENIG 프로토콜 통신 라이브러리
|
||||
|
||||
**Explanation of the README Content:**
|
||||
|
||||
1. **Title and Overview:** Clearly states the purpose of the library.
|
||||
2. **Key Components:** Explains the main building blocks:
|
||||
* `DeviceType` enum: Lists the device types.
|
||||
* `Packet` class: Describes the packet structure.
|
||||
* `EEProtocol` class: Explains its role in packet handling.
|
||||
3. **Key Features:** Details the core functionalities of the `EEProtocol` class:
|
||||
* Packet creation.
|
||||
* Packet parsing.
|
||||
* Data reception.
|
||||
* Event-driven nature.
|
||||
* CRC16 calculation.
|
||||
4. **Usage:** Provides code examples for:
|
||||
* Creating a packet.
|
||||
* Parsing a packet.
|
||||
* Processing received data.
|
||||
5. **Error Handling:** Explains how the library handles invalid data and incomplete packets.
|
||||
6. **Dependencies:** Lists any required frameworks or libraries.
|
||||
7. **License:** Placeholder for the project's license.
|
||||
|
||||
This README provides a comprehensive overview of the `ENIGProtocol` library, making it easy for other developers to understand and use it. I hope this is helpful!
|
||||
|
||||
|
||||
## 개발 환경
|
||||
- Frameworks : .NET Standard 2.0
|
||||
- Visual Studio 2022 또는 Visual Studio Code
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
- `EEProtocol.cs`: ENIG 프로토콜 구현
|
||||
- `EEProtocolTests.cs`: 단위 테스트 코드
|
||||
- `Sample`: C# Winform 샘플 프로젝트
|
||||
|
||||
## 장비 목록
|
||||
```
|
||||
public enum DeviceType : byte
|
||||
{
|
||||
ACS = 0,
|
||||
AGV1 = 10+1,
|
||||
AGV2 = 10+2,
|
||||
BUFFER1 = 20+1,
|
||||
BUFFER2 = 20+2,
|
||||
BUFFER3 = 20+3,
|
||||
BUFFER4 = 20+4,
|
||||
BUFFER5 = 20+5,
|
||||
DOOR = 30,
|
||||
}
|
||||
```
|
||||
public enum DeviceAlias : byte
|
||||
{
|
||||
B1 = 20 + 1, //BUFFER1 ~ 5
|
||||
B2 = 20 + 2,
|
||||
B3 = 20 + 3,
|
||||
B4 = 20 + 4,
|
||||
B5 = 20 + 5,
|
||||
C1 = 40 + 1, //충전소 1
|
||||
C2 = 40 + 2, //충전소 2
|
||||
C3 = 40 + 3, //충전소 3
|
||||
C4 = 40 + 4, //충전소 4
|
||||
E1 = 90 + 1, //장비1 (SSOTRON Loader)
|
||||
E2 = 90 + 2, //장비2 (TOPS ENIG)
|
||||
E3 = 90 + 3, //장비3 (SSOTRON DIVERTER)
|
||||
}
|
||||
//11번 AGV야! BUFFER1로 이동해라!
|
||||
//0x02 0x03 0x0B 0x6B 0x42 0x31 {CRC} 0x03
|
||||
|
||||
### 기본 패킷 구조
|
||||
```
|
||||
[STX][LEN][ID][CMD][DATA][CRC16][ETX]
|
||||
```
|
||||
- **STX (Start of Text)**: 0x02
|
||||
- **LEN (Length)**: 데이터 길이 (1바이트) = {CMD+DATA}
|
||||
- **ID (Client ID)**: 데이터 길이 (1바이트) : 디바이스식별코드(=DeviceType)
|
||||
- **CMD (Command)**: 명령어 코드 (1바이트)
|
||||
- **DATA**: 명령어에 따른 데이터 (가변 길이)
|
||||
- **CRC16**: 데이터 무결성 검사 (2바이트)
|
||||
- **ETX (End of Text)**: 0x03
|
||||
|
||||
### 통신 방향 (호스트=ACS, 장비=agv,buffer,door)
|
||||
- H -> E: 호스트에서 장비로 전송
|
||||
- E -> H: 장비에서 호스트로 전송
|
||||
|
||||
### 명령어 목록
|
||||
1. **ACS (AGV Control System)**
|
||||
|
||||
|
||||
2. **Buffer**
|
||||
- E -> H | cmd(3): 상태 (data len=1 : 0=카트없음, 1=카트있음, 2=바쁨, 3=알수없음, 255=오류)
|
||||
- H -> E | cmd(1): Lock
|
||||
- Target[1] = {DeviceType}
|
||||
- H -> E | cmd(2): UnLock
|
||||
- Target[1] = {DeviceType}
|
||||
|
||||
3. **AGV**
|
||||
- H -> E | Move : cmd(100) : 대상태그까지 이동(자동이동)
|
||||
- Target[1] = {DeviceType}
|
||||
- TagID[4] = "0000"
|
||||
|
||||
- H -> E | Stop : cmd(101) : 멈춤
|
||||
- H -> E | Reset : cmd(102) : 오류 소거
|
||||
|
||||
- H -> E | SetCurrent : cmd(103) : 현재위치설정
|
||||
- Target[1] = {DeviceType}
|
||||
- TagID[4] = "0000"
|
||||
|
||||
- H -> E | MoveManual : cmd(104) : 메뉴얼이동
|
||||
- Target[1] = {DeviceType}
|
||||
- Direction[1] : 0=Backward, 1=Forward, 2=TurnLeft, 3=TurnRight
|
||||
- Speed[1] : 0=Slow, 1=Normal, 2=Fast
|
||||
|
||||
- H -> E | MarkStop : cmd(105) : 마크센서스톱
|
||||
- Target[1] = {DeviceType}
|
||||
|
||||
- H -> E | Lift Control : cmd(106) : 리프트제어
|
||||
- Target[1] = {DeviceType}
|
||||
- Action[1] : 0=STOP, 1=UP, 2=DOWN
|
||||
|
||||
- H -> E | Move : cmd(107) : 대상별칭까지 이동(자동이동)
|
||||
- Target[1] = {DeviceType}
|
||||
- AliasName[n] = ".....
|
||||
|
||||
- H -> E | MoveAuto : cmd(108) : 자동이동
|
||||
- Target[1] = {DeviceType}
|
||||
- MotDirection[1] : 0=Backward, 1=Forward
|
||||
- MagnetDirection[1] : 0=Straight,1=Left, 2=Right
|
||||
- Speed[1] : 0=Slow, 1=Normal, 2=Fast
|
||||
|
||||
- H -> E | Charge On: cmd(109) : 충전실행(충전기 이동 후 자동 충전 진행)
|
||||
- Target[1] = {DeviceType}
|
||||
- Action[1] : 0=Charge Off, 1=Charge On
|
||||
|
||||
|
||||
- E -> H | Move Complete : cmd(1) : 목적지이동완료 후 전송
|
||||
- TagID[4] : "0000"
|
||||
- E -> H | TagID Received : cmd(2) : 태그값 인식시 전송
|
||||
- TagID[4] : "0000"
|
||||
|
||||
- E -> H | Status : cmd(3)
|
||||
- Mode[1] : 0=manual, 1=auto
|
||||
- RunSt[1] : 0=stop, 1=run, 2=error
|
||||
- Diection[1] : 0=straight, 1=left, 2=right, 3=markstop
|
||||
- Inposition[1] : 0=off, 1=on : 목적위치에 도달완료 시 설정 이동 이동시 OFF됨
|
||||
- ChargeSt[1] : 0=off, 1=on
|
||||
- CartSt[1] : 0=off, 1=on, 2=unknown
|
||||
- LiftSt[1] : 0=down , 1=up, 2=unknown
|
||||
- LastTag[4] : "0000"
|
||||
- CurrentPath[1] : Path ID , 0=미설정, 1~255(순차증가)
|
||||
|
||||
4. **Door**
|
||||
- H -> E | cmd(1): 출입문 열기
|
||||
- H -> E | cmd(2): 출입문 닫기
|
||||
- E -> H | cmd(3): 출입문 상태 (data len=1 : 0=닫힘, 1=열림, 2=바쁨, 3=알수없음, 255=오류)
|
||||
|
||||
### CRC16 계산
|
||||
- CRC16 다항식 사용
|
||||
- 초기값: 0xFFFF
|
||||
- 데이터 무결성 검증에 사용
|
||||
|
||||
|
||||
#### CRC-16 테이블 값
|
||||
```
|
||||
0x0000, 0x408E, 0x73EF, 0x3361, 0x152D, 0x55A3, 0x66C2, 0x264C,
|
||||
0x2A5A, 0x6AD4, 0x59B5, 0x193B, 0x3F77, 0x7FF9, 0x4C98, 0x0C16,
|
||||
0x54B4, 0x143A, 0x275B, 0x67D5, 0x4199, 0x0117, 0x3276, 0x72F8,
|
||||
0x7EEE, 0x3E60, 0x0D01, 0x4D8F, 0x6BC3, 0x2B4D, 0x182C, 0x58A2,
|
||||
0x5B9B, 0x1B15, 0x2874, 0x68FA, 0x4EB6, 0x0E38, 0x3D59, 0x7DD7,
|
||||
0x71C1, 0x314F, 0x022E, 0x42A0, 0x64EC, 0x2462, 0x1703, 0x578D,
|
||||
0x0F2F, 0x4FA1, 0x7CC0, 0x3C4E, 0x1A02, 0x5A8C, 0x69ED, 0x2963,
|
||||
0x2575, 0x65FB, 0x569A, 0x1614, 0x3058, 0x70D6, 0x43B7, 0x0339,
|
||||
0x45C5, 0x054B, 0x362A, 0x76A4, 0x50E8, 0x1066, 0x2307, 0x6389,
|
||||
0x6F9F, 0x2F11, 0x1C70, 0x5CFE, 0x7AB2, 0x3A3C, 0x095D, 0x49D3,
|
||||
0x1171, 0x51FF, 0x629E, 0x2210, 0x045C, 0x44D2, 0x77B3, 0x373D,
|
||||
0x3B2B, 0x7BA5, 0x48C4, 0x084A, 0x2E06, 0x6E88, 0x5DE9, 0x1D67,
|
||||
0x1E5E, 0x5ED0, 0x6DB1, 0x2D3F, 0x0B73, 0x4BFD, 0x789C, 0x3812,
|
||||
0x3404, 0x748A, 0x47EB, 0x0765, 0x2129, 0x61A7, 0x52C6, 0x1248,
|
||||
0x4AEA, 0x0A64, 0x3905, 0x798B, 0x5FC7, 0x1F49, 0x2C28, 0x6CA6,
|
||||
0x60B0, 0x203E, 0x135F, 0x53D1, 0x759D, 0x3513, 0x0672, 0x46FC,
|
||||
0x7979, 0x39F7, 0x0A96, 0x4A18, 0x6C54, 0x2CDA, 0x1FBB, 0x5F35,
|
||||
0x5323, 0x13AD, 0x20CC, 0x6042, 0x460E, 0x0680, 0x35E1, 0x756F,
|
||||
0x2DCD, 0x6D43, 0x5E22, 0x1EAC, 0x38E0, 0x786E, 0x4B0F, 0x0B81,
|
||||
0x0797, 0x4719, 0x7478, 0x34F6, 0x12BA, 0x5234, 0x6155, 0x21DB,
|
||||
0x22E2, 0x626C, 0x510D, 0x1183, 0x37CF, 0x7741, 0x4420, 0x04AE,
|
||||
0x08B8, 0x4836, 0x7B57, 0x3BD9, 0x1D95, 0x5D1B, 0x6E7A, 0x2EF4,
|
||||
0x7656, 0x36D8, 0x05B9, 0x4537, 0x637B, 0x23F5, 0x1094, 0x501A,
|
||||
0x5C0C, 0x1C82, 0x2FE3, 0x6F6D, 0x4921, 0x09AF, 0x3ACE, 0x7A40,
|
||||
0x3CBC, 0x7C32, 0x4F53, 0x0FDD, 0x2991, 0x691F, 0x5A7E, 0x1AF0,
|
||||
0x16E6, 0x5668, 0x6509, 0x2587, 0x03CB, 0x4345, 0x7024, 0x30AA,
|
||||
0x6808, 0x2886, 0x1BE7, 0x5B69, 0x7D25, 0x3DAB, 0x0ECA, 0x4E44,
|
||||
0x4252, 0x02DC, 0x31BD, 0x7133, 0x577F, 0x17F1, 0x2490, 0x641E,
|
||||
0x6727, 0x27A9, 0x14C8, 0x5446, 0x720A, 0x3284, 0x01E5, 0x416B,
|
||||
0x4D7D, 0x0DF3, 0x3E92, 0x7E1C, 0x5850, 0x18DE, 0x2BBF, 0x6B31,
|
||||
0x3393, 0x731D, 0x407C, 0x00F2, 0x26BE, 0x6630, 0x5551, 0x15DF,
|
||||
0x19C9, 0x5947, 0x6A26, 0x2AA8, 0x0CE4, 0x4C6A, 0x7F0B, 0x3F85,
|
||||
```
|
||||
|
||||
|
||||
#### CRC16 계산 테이블 생성 코드
|
||||
```csharp
|
||||
const ushort polynomial = 0x7979;
|
||||
ushort[] CRC16_TABLE = new ushort[256];
|
||||
|
||||
for (ushort i = 0; i < CRC16_TABLE.Length; i++)
|
||||
{
|
||||
ushort value = 0;
|
||||
ushort temp = i;
|
||||
for (byte j = 0; j < 8; j++)
|
||||
{
|
||||
if (((value ^ temp) & 0x0001) != 0)
|
||||
{
|
||||
value = (ushort)((value >> 1) ^ polynomial);
|
||||
}
|
||||
else
|
||||
{
|
||||
value >>= 1;
|
||||
}
|
||||
temp >>= 1;
|
||||
}
|
||||
CRC16_TABLE[i] = value;
|
||||
}
|
||||
```
|
||||
|
||||
#### CRC16 계산 예시
|
||||
```csharp
|
||||
ushort CalculateCRC16(byte[] data)
|
||||
{
|
||||
ushort crc = 0xFFFF;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
byte index = (byte)(crc ^ data[i]);
|
||||
crc = (ushort)((crc >> 8) ^ CRC16_TABLE[index]);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
```
|
||||
|
||||
## 라이센스
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
8
AGVLogic/EnigProtocol/enigprotocol/.gitignore
vendored
Normal file
8
AGVLogic/EnigProtocol/enigprotocol/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
################################################################################
|
||||
# 이 .gitignore 파일은 Microsoft(R) Visual Studio에서 자동으로 만들어졌습니다.
|
||||
################################################################################
|
||||
|
||||
/obj
|
||||
/bin
|
||||
/.vs
|
||||
/.git
|
||||
111
AGVLogic/EnigProtocol/enigprotocol/Commands.cs
Normal file
111
AGVLogic/EnigProtocol/enigprotocol/Commands.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace ENIGProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// host -> eq
|
||||
/// </summary>
|
||||
public enum AGVCommandHE : byte
|
||||
{
|
||||
Goto = 100,
|
||||
Stop = 101,
|
||||
Reset = 102,
|
||||
SetCurrent = 103,
|
||||
Manual = 104,
|
||||
MarkStop = 105,
|
||||
LiftControl = 106,
|
||||
GotoAlias = 107,
|
||||
AutoMove = 108,
|
||||
ChargeControl = 109,
|
||||
Charger = 112,
|
||||
LTurn = 113,
|
||||
RTurn = 114,
|
||||
LTurn180 = 115,
|
||||
RTurn180 = 116,
|
||||
PickOnEnter = 117,
|
||||
PickOffEnter = 118,
|
||||
PickOnExit = 119,
|
||||
PickOffExit = 120,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// eq -> host
|
||||
/// </summary>
|
||||
public enum AGVCommandEH : byte
|
||||
{
|
||||
Error = 1,
|
||||
Arrived = 2,
|
||||
ReadRFID = 3,
|
||||
Status = 9,
|
||||
}
|
||||
|
||||
public enum AGVErrorCode : byte
|
||||
{
|
||||
None = 0,
|
||||
PredictFix,
|
||||
TurnTimeout,
|
||||
TurnError,
|
||||
EmptyNode,
|
||||
Goto,
|
||||
ManualMode,
|
||||
UnknownCommand,
|
||||
UnknownAlias,
|
||||
|
||||
// Operational Errors
|
||||
CART_EXIST,
|
||||
MARK_TIMEOUT,
|
||||
MARK_SENSOR_FAIL,
|
||||
LIFT_ERROR,
|
||||
AGV_SPEED_SET_FAIL,
|
||||
AGV_RUN_FAIL,
|
||||
AGV_STOP_FAIL,
|
||||
PATH_INTEGRITY_FAIL,
|
||||
TURN_FAIL,
|
||||
NO_CHARGEPOINT,
|
||||
NOTSET_CHARGEPOINT,
|
||||
ALREADY_CHARGE,
|
||||
CHARGE_RETRY_OVER,
|
||||
}
|
||||
|
||||
public static class AGVUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// 에러코드에 해당하는 오류메세지를 반환 합니다
|
||||
/// </summary>
|
||||
/// <param name="ecode"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetAGVErrorMessage(AGVErrorCode ecode)
|
||||
{
|
||||
switch (ecode)
|
||||
{
|
||||
case AGVErrorCode.None: return "No Error";
|
||||
case AGVErrorCode.PredictFix: return "이동 예측이 동작하지 않습니다";
|
||||
case AGVErrorCode.TurnTimeout: return "회전작업 시간초과";
|
||||
case AGVErrorCode.TurnError: return "회전작업이 완료되지 않았습니다";
|
||||
case AGVErrorCode.EmptyNode: return "노드정보를 찾을 수 없습니다";
|
||||
case AGVErrorCode.Goto: return "이동 명령 오류";
|
||||
case AGVErrorCode.ManualMode: return "자동운전 상태가 아닙니다";
|
||||
case AGVErrorCode.UnknownCommand: return "알수 없는 명령입니다";
|
||||
case AGVErrorCode.UnknownAlias: return "알수 없는 별칭 입니다";
|
||||
|
||||
case AGVErrorCode.CART_EXIST: return "카트 감지 센서 오류";
|
||||
case AGVErrorCode.MARK_TIMEOUT: return "마크 정지 신호 시간초과";
|
||||
case AGVErrorCode.MARK_SENSOR_FAIL: return "마크 센서 미감지";
|
||||
case AGVErrorCode.LIFT_ERROR: return "리프트 동작 오류";
|
||||
case AGVErrorCode.AGV_SPEED_SET_FAIL: return "AGV 속도 설정 실패";
|
||||
case AGVErrorCode.AGV_RUN_FAIL: return "AGV 구동 실패";
|
||||
case AGVErrorCode.AGV_STOP_FAIL: return "AGV 정지 실패";
|
||||
case AGVErrorCode.PATH_INTEGRITY_FAIL: return "경로 무결성 검증 실패";
|
||||
case AGVErrorCode.TURN_FAIL: return "턴 동작 실패";
|
||||
case AGVErrorCode.NO_CHARGEPOINT: return "충전 위치 아님";
|
||||
case AGVErrorCode.NOTSET_CHARGEPOINT: return "충전기 노드 미설정";
|
||||
case AGVErrorCode.ALREADY_CHARGE: return "이미 충전 중 상태임";
|
||||
case AGVErrorCode.CHARGE_RETRY_OVER: return $"충전명령 재전송 횟수 초과";
|
||||
default: return ecode.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
235
AGVLogic/EnigProtocol/enigprotocol/EEProtocol.cs
Normal file
235
AGVLogic/EnigProtocol/enigprotocol/EEProtocol.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace ENIG
|
||||
{
|
||||
// 장비 타입 정의
|
||||
public enum DeviceType
|
||||
{
|
||||
ACS = 0,
|
||||
AGV1 = 10,
|
||||
AGV2 = 11,
|
||||
BUFFER1 = 20,
|
||||
BUFFER2 = 21,
|
||||
BUFFER3 = 22,
|
||||
BUFFER4 = 23,
|
||||
BUFFER5 = 24,
|
||||
DOOR = 30,
|
||||
}
|
||||
|
||||
public partial class EEProtocol
|
||||
{
|
||||
// 패킷 수신 이벤트 정의
|
||||
// 데이터 수신 이벤트 정의
|
||||
public event EventHandler<DataEventArgs> OnDataReceived;
|
||||
public event EventHandler<MessageEventArgs> OnMessage;
|
||||
|
||||
// CRC16 계산을 위한 테이블
|
||||
private static readonly ushort[] CRC16_TABLE = new ushort[256];
|
||||
|
||||
// CRC16 테이블 초기화
|
||||
public EEProtocol()
|
||||
{
|
||||
const ushort polynomial = 0x7979;
|
||||
for (ushort i = 0; i < CRC16_TABLE.Length; i++)
|
||||
{
|
||||
ushort value = 0;
|
||||
ushort temp = i;
|
||||
for (byte j = 0; j < 8; j++)
|
||||
{
|
||||
if (((value ^ temp) & 0x0001) != 0)
|
||||
{
|
||||
value = (ushort)((value >> 1) ^ polynomial);
|
||||
}
|
||||
else
|
||||
{
|
||||
value >>= 1;
|
||||
}
|
||||
temp >>= 1;
|
||||
}
|
||||
CRC16_TABLE[i] = value;
|
||||
}
|
||||
|
||||
//// CRC 테이블 출력
|
||||
//Console.WriteLine("CRC16 테이블 값:");
|
||||
//for (int i = 0; i < CRC16_TABLE.Length; i++)
|
||||
//{
|
||||
// if (i % 8 == 0)
|
||||
// {
|
||||
// Console.WriteLine();
|
||||
// }
|
||||
// Console.Write($"0x{CRC16_TABLE[i]:X4}, ");
|
||||
//}
|
||||
//Console.WriteLine();
|
||||
|
||||
|
||||
}
|
||||
|
||||
// CRC16 계산 메서드
|
||||
public ushort CalculateCRC16(byte[] data)
|
||||
{
|
||||
ushort crc = 0xFFFF;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
byte index = (byte)(crc ^ data[i]);
|
||||
crc = (ushort)((crc >> 8) ^ CRC16_TABLE[index]);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
// 패킷 생성 메서드
|
||||
public byte[] CreatePacket(byte id, byte command, byte[] data)
|
||||
{
|
||||
var packet = new Packet
|
||||
{
|
||||
ID = id,
|
||||
Command = command,
|
||||
Data = data ?? new byte[0],
|
||||
Length = (byte)(1 + 1 + (data?.Length ?? 0)) // ID + Command + Data 길이
|
||||
};
|
||||
|
||||
// 패킷 조립
|
||||
List<byte> packetData = new List<byte>();
|
||||
packetData.Add(Packet.STX);
|
||||
packetData.Add(packet.Length);
|
||||
packetData.Add(packet.ID);
|
||||
packetData.Add(packet.Command);
|
||||
if (packet.Data != null)
|
||||
packetData.AddRange(packet.Data);
|
||||
|
||||
// CRC16 계산
|
||||
packet.CRC16 = CalculateCRC16(packetData.Skip(1).ToArray()); // STX 제외하고 계산
|
||||
packetData.AddRange(BitConverter.GetBytes(packet.CRC16));
|
||||
packetData.Add(Packet.ETX);
|
||||
|
||||
return packetData.ToArray();
|
||||
}
|
||||
|
||||
//패킷테스트
|
||||
public void PacketTest(byte[] rawData)
|
||||
{
|
||||
var hexstr = string.Join(" ", rawData.Select(t => t.ToString("X2")));
|
||||
RaiseMessage( $"TestPacket : {hexstr}");
|
||||
ParsePacket(rawData);
|
||||
}
|
||||
|
||||
//메세지 발생
|
||||
public void RaiseMessage(string message, bool isError = false)
|
||||
{
|
||||
OnMessage?.Invoke(this, new MessageEventArgs { IsError = isError, Message = message });
|
||||
}
|
||||
|
||||
// 패킷 파싱 메서드
|
||||
public bool ParsePacket(byte[] rawData)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (rawData.Length < 7) // 최소 패킷 크기
|
||||
{
|
||||
var hexstring = string.Join(" ", rawData.Select(t => t.ToString("X2")));
|
||||
RaiseMessage($"Too Short Data:{hexstring}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (rawData[0] != Packet.STX || rawData[rawData.Length - 1] != Packet.ETX)
|
||||
{
|
||||
var hexstring = string.Join(" ", rawData.Select(t => t.ToString("X2")));
|
||||
RaiseMessage($"STX/ETX Error Data:{hexstring}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
byte length = rawData[1];
|
||||
if (length + 5 != rawData.Length) // STX + Length + CRC16(2) + ETX = 5
|
||||
{
|
||||
var hexstring = string.Join(" ", rawData.Select(t => t.ToString("X2")));
|
||||
RaiseMessage($"Length Error ({length+5} != {rawData.Length}) Data:{hexstring}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// CRC16 검증
|
||||
byte[] dataForCrc = rawData.Skip(1).Take(length + 1).ToArray();
|
||||
ushort calculatedCrc = CalculateCRC16(dataForCrc);
|
||||
ushort receivedCrc = BitConverter.ToUInt16(rawData, rawData.Length - 3);
|
||||
|
||||
if (receivedCrc != 0xFFFF && calculatedCrc != receivedCrc) //FF 무시
|
||||
{
|
||||
RaiseMessage($"CRC Error ID:{rawData[2]:X2},CMD:{rawData[3]:X2}", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 패킷 생성
|
||||
var packet = new Packet
|
||||
{
|
||||
Length = length,
|
||||
ID = rawData[2],
|
||||
Command = rawData[3],
|
||||
Data = rawData.Skip(4).Take(length - 2).ToArray(), // ID와 Command 길이(2) 제외
|
||||
CRC16 = receivedCrc,
|
||||
RawData = rawData,
|
||||
};
|
||||
|
||||
// 이벤트 발생
|
||||
OnDataReceived?.Invoke(this, new DataEventArgs { ReceivedPacket = packet });
|
||||
return true;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
RaiseMessage(ex.Message, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 데이터 수신 처리 메서드 (시리얼 포트에서 데이터를 받았을 때 호출)
|
||||
private List<byte> buffer = new List<byte>();
|
||||
private int ProtocolParseError = 0;
|
||||
public void ProcessReceivedData(byte[] data)
|
||||
{
|
||||
buffer.AddRange(data);
|
||||
|
||||
while (buffer.Count > 0)
|
||||
{
|
||||
// STX 찾기
|
||||
int stxIndex = buffer.FindIndex(b => b == Packet.STX);
|
||||
if (stxIndex == -1)
|
||||
{
|
||||
buffer.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
// 불필요한 데이터 제거
|
||||
if (stxIndex > 0)
|
||||
buffer.RemoveRange(0, stxIndex);
|
||||
|
||||
// 패킷 길이 확인을 위한 최소 데이터 확인
|
||||
if (buffer.Count < 2)
|
||||
break;
|
||||
|
||||
int expectedLength = buffer[1] + 5; // 전체 패킷 길이
|
||||
if (buffer.Count < expectedLength)
|
||||
break;
|
||||
|
||||
// 패킷 추출 및 처리
|
||||
byte[] packetData = buffer.Take(expectedLength).ToArray();
|
||||
buffer.RemoveRange(0, expectedLength);
|
||||
|
||||
var parseOK = ParsePacket(packetData);
|
||||
if(parseOK==false) //분석이 실패되었다면 해당 데이터는 삭제한다.
|
||||
{
|
||||
ProtocolParseError += 1;
|
||||
if (ProtocolParseError > 3) buffer.Clear();
|
||||
} else ProtocolParseError = 0;
|
||||
|
||||
if(buffer.Any())
|
||||
{
|
||||
System.Threading.Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
AGVLogic/EnigProtocol/enigprotocol/ENIGProtocol.csproj
Normal file
56
AGVLogic/EnigProtocol/enigprotocol/ENIGProtocol.csproj
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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>{9365803B-933D-4237-93C7-B502C855A71C}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>enigprotocol</RootNamespace>
|
||||
<AssemblyName>enigprotocol</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<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' ">
|
||||
<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.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Commands.cs" />
|
||||
<Compile Include="EEProtocol.cs" />
|
||||
<Compile Include="EventArgs.cs" />
|
||||
<Compile Include="Packet.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include=".gitignore" />
|
||||
<None Include="ReadMe.MD" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
18
AGVLogic/EnigProtocol/enigprotocol/EventArgs.cs
Normal file
18
AGVLogic/EnigProtocol/enigprotocol/EventArgs.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace ENIG
|
||||
{
|
||||
|
||||
public partial class EEProtocol
|
||||
{
|
||||
public class MessageEventArgs : EventArgs
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public bool IsError { get; set; }
|
||||
}
|
||||
public class DataEventArgs : EventArgs
|
||||
{
|
||||
public Packet ReceivedPacket { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
20
AGVLogic/EnigProtocol/enigprotocol/Packet.cs
Normal file
20
AGVLogic/EnigProtocol/enigprotocol/Packet.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace ENIG
|
||||
{
|
||||
// 패킷 구조체
|
||||
public class Packet
|
||||
{
|
||||
public const byte STX = 0x02;
|
||||
public const byte ETX = 0x03;
|
||||
public byte Length { get; set; }
|
||||
public byte ID { get; set; }
|
||||
public byte Command { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
public ushort CRC16 { get; set; }
|
||||
|
||||
public byte[] RawData { get; set; }
|
||||
public Packet()
|
||||
{
|
||||
Data = new byte[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
|
||||
// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
|
||||
// 이러한 특성 값을 변경하세요.
|
||||
[assembly: AssemblyTitle("enigprotocol")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("enigprotocol")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2025")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
|
||||
// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
|
||||
// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다.
|
||||
[assembly: Guid("9365803b-933d-4237-93c7-b502c855a71c")]
|
||||
|
||||
// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다.
|
||||
//
|
||||
// 주 버전
|
||||
// 부 버전
|
||||
// 빌드 번호
|
||||
// 수정 버전
|
||||
//
|
||||
// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호를
|
||||
// 기본값으로 할 수 있습니다.
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
236
AGVLogic/EnigProtocol/enigprotocol/ReadMe.MD
Normal file
236
AGVLogic/EnigProtocol/enigprotocol/ReadMe.MD
Normal file
@@ -0,0 +1,236 @@
|
||||
# ENIG Protocol
|
||||
|
||||
ENIG 프로토콜 통신 라이브러리
|
||||
|
||||
**Explanation of the README Content:**
|
||||
|
||||
1. **Title and Overview:** Clearly states the purpose of the library.
|
||||
2. **Key Components:** Explains the main building blocks:
|
||||
* `DeviceType` enum: Lists the device types.
|
||||
* `Packet` class: Describes the packet structure.
|
||||
* `EEProtocol` class: Explains its role in packet handling.
|
||||
3. **Key Features:** Details the core functionalities of the `EEProtocol` class:
|
||||
* Packet creation.
|
||||
* Packet parsing.
|
||||
* Data reception.
|
||||
* Event-driven nature.
|
||||
* CRC16 calculation.
|
||||
4. **Usage:** Provides code examples for:
|
||||
* Creating a packet.
|
||||
* Parsing a packet.
|
||||
* Processing received data.
|
||||
5. **Error Handling:** Explains how the library handles invalid data and incomplete packets.
|
||||
6. **Dependencies:** Lists any required frameworks or libraries.
|
||||
7. **License:** Placeholder for the project's license.
|
||||
|
||||
This README provides a comprehensive overview of the `ENIGProtocol` library, making it easy for other developers to understand and use it. I hope this is helpful!
|
||||
|
||||
|
||||
## 개발 환경
|
||||
- Frameworks : .NET Standard 2.0
|
||||
- Visual Studio 2022 또는 Visual Studio Code
|
||||
|
||||
## 프로젝트 구조
|
||||
|
||||
- `EEProtocol.cs`: ENIG 프로토콜 구현
|
||||
- `EEProtocolTests.cs`: 단위 테스트 코드
|
||||
- `Sample`: C# Winform 샘플 프로젝트
|
||||
|
||||
### Xbee Setting value
|
||||
|
||||
- **NARMI 70 : AGV No 70
|
||||
- **NARMI 70 (LIFT) : P46A6,C17,DH:0,DL:FFFF,MY40
|
||||
- **NARMI 70 (AGV) : P46A6,C17,DH:0,DL:FFFF,MY41
|
||||
|
||||
- **NARMI 77 : AGV No 71
|
||||
- **NARMI 71 (LIFT) : P46A6,C17,DH:0,DL:FFFF,MY30
|
||||
- **NARMI 71 (AGV) : P46A6,C17,DH:0,DL:FFFF,MY31
|
||||
|
||||
- **충전기04 : P46A6,C17,DH:0,DL:FFFF,MY41
|
||||
- **충전기71 : P46A6,C17,DH:0,DL:FFFF,MY46
|
||||
|
||||
- **ACS : P46A5,C17,DH:0,DL:FFFF,MY10
|
||||
|
||||
- **BUFFER : P46A5,C17,DH:0,DL:FFFF,MY60~65
|
||||
|
||||
- **AGV1 : P46A5,C17,DH:0,DL:FFFF,MY50
|
||||
- **AGV2 : P46A5,C17,DH:0,DL:FFFF,MY51
|
||||
|
||||
- **DOOR : P46A5,C17,DH:0,DL:FFFF,MY30
|
||||
|
||||
|
||||
## 장비 목록
|
||||
```
|
||||
public enum DeviceType : byte
|
||||
{
|
||||
ACS = 0,
|
||||
AGV1 = 10+1,
|
||||
AGV2 = 10+2,
|
||||
BUFFER1 = 20+1,
|
||||
BUFFER2 = 20+2,
|
||||
BUFFER3 = 20+3,
|
||||
BUFFER4 = 20+4,
|
||||
BUFFER5 = 20+5,
|
||||
DOOR = 30,
|
||||
}
|
||||
```
|
||||
### 기본 패킷 구조
|
||||
```
|
||||
[STX][LEN][ID][CMD][DATA][CRC16][ETX]
|
||||
```
|
||||
- **STX (Start of Text)**: 0x02
|
||||
- **LEN (Length)**: 데이터 길이 (1바이트) = {ID+CMD+DATA}
|
||||
- **ID (Client ID)**: 데이터 길이 (1바이트) : 디바이스식별코드(=DeviceType)
|
||||
- **CMD (Command)**: 명령어 코드 (1바이트)
|
||||
- **DATA**: 명령어에 따른 데이터 (가변 길이)
|
||||
- **CRC16**: 데이터 무결성 검사 (2바이트)
|
||||
- **ETX (End of Text)**: 0x03
|
||||
|
||||
### 통신 방향 (호스트=ACS, 장비=agv,buffer,door)
|
||||
- H -> E: 호스트에서 장비로 전송
|
||||
- E -> H: 장비에서 호스트로 전송
|
||||
|
||||
### 명령어 목록
|
||||
1. **ACS (AGV Control System)**
|
||||
|
||||
|
||||
2. **Buffer**
|
||||
- E -> H | cmd('S'): 상태 (............) BIT & 1BYTE 0:CART1, 1:CART2, 2:BASKET1, 3:BASKET2, 4:OPEN, 5:CLOSE
|
||||
- H -> E | cmd('L'): Lock
|
||||
- Target[1] = {DeviceType}
|
||||
- H -> E | cmd('U'): UnLock
|
||||
- Target[1] = {DeviceType}
|
||||
|
||||
3. **AGV**
|
||||
- H -> E | Move : cmd(100) : 대상태그까지 이동(자동이동)
|
||||
- Target[1] = {DeviceType}
|
||||
- TagID[4] = "0000"
|
||||
- H -> E | Move : cmd(107) : 대상별칭까지 이동(자동이동)
|
||||
- Target[1] = {DeviceType}
|
||||
- AliasName[n] = "....."
|
||||
- H -> E | Stop : cmd(101) : 멈춤
|
||||
- H -> E | Reset : cmd(102) : 오류 소거
|
||||
- H -> E | Charge On: cmd(103) : 충전실행(충전기 이동 후 자동 충전 진행)
|
||||
- Target[1] = {DeviceType}
|
||||
- Action[1] : 0=Charge Off, 1=Charge On
|
||||
- H -> E | MoveManual : cmd(104) : 메뉴얼이동
|
||||
- Target[1] = {DeviceType}
|
||||
- Direction[1] : 0=Backward, 1=Forward, 2=TurnLeft, 3=TurnRight
|
||||
- Speed[1] : 0=Slow, 1=Normal, 2=Fast
|
||||
- Runtime[1] : 0 second
|
||||
- H -> E | MarkStop : cmd(105) : 마크센서스톱
|
||||
- Target[1] = {DeviceType}
|
||||
|
||||
- H -> E | Lift Control : cmd(106) : 리프트제어
|
||||
- Target[1] = {DeviceType}
|
||||
- Action[1] : 0=STOP, 1=UP, 2=DOWN
|
||||
|
||||
- E -> H | Move Complete : cmd(1) : 목적지이동완료 후 전송
|
||||
- TagID[4] : "0000"
|
||||
- E -> H | TagID Received : cmd(2) : 태그값 인식시 전송
|
||||
- TagID[4] : "0000"
|
||||
|
||||
- E -> H | Status : cmd(3) - 총 12바이트
|
||||
- Mode[1] : 0=manual, 1=auto
|
||||
- RunSt[1] : 0=stop, 1=run, 2=error
|
||||
MotDirection[1] : 0:Forward, 1:Backward, 0xFF:unknown
|
||||
- MagDiection[1] : 0=straight, 1=left, 2=right , 0xFF:unknown
|
||||
- Inposition[1] : 0=off, 1=on : 목적위치에 도달완료 시 설정 이동 이동시 OFF됨
|
||||
- ChargeSt[1] : 0=off, 1=on
|
||||
- CartSt[1] : 0=off, 1=on, 2=unknown
|
||||
- LiftSt[1] : 0=down , 1=up, 2=unknown
|
||||
- LastTag[4] : "0000" (ASCII 4바이트)
|
||||
|
||||
4. **Door**
|
||||
- H -> E | cmd(1): 출입문 열기
|
||||
- H -> E | cmd(2): 출입문 닫기
|
||||
- E -> H | cmd(3): 출입문 상태 (data len=1 : 0=닫힘, 1=열림, 2=바쁨, 3=알수없음, 255=오류)
|
||||
|
||||
### CRC16 계산
|
||||
- CRC16 다항식 사용
|
||||
- 초기값: 0xFFFF
|
||||
- 데이터 무결성 검증에 사용
|
||||
|
||||
|
||||
#### CRC-16 테이블 값
|
||||
```
|
||||
0x0000, 0x408E, 0x73EF, 0x3361, 0x152D, 0x55A3, 0x66C2, 0x264C,
|
||||
0x2A5A, 0x6AD4, 0x59B5, 0x193B, 0x3F77, 0x7FF9, 0x4C98, 0x0C16,
|
||||
0x54B4, 0x143A, 0x275B, 0x67D5, 0x4199, 0x0117, 0x3276, 0x72F8,
|
||||
0x7EEE, 0x3E60, 0x0D01, 0x4D8F, 0x6BC3, 0x2B4D, 0x182C, 0x58A2,
|
||||
0x5B9B, 0x1B15, 0x2874, 0x68FA, 0x4EB6, 0x0E38, 0x3D59, 0x7DD7,
|
||||
0x71C1, 0x314F, 0x022E, 0x42A0, 0x64EC, 0x2462, 0x1703, 0x578D,
|
||||
0x0F2F, 0x4FA1, 0x7CC0, 0x3C4E, 0x1A02, 0x5A8C, 0x69ED, 0x2963,
|
||||
0x2575, 0x65FB, 0x569A, 0x1614, 0x3058, 0x70D6, 0x43B7, 0x0339,
|
||||
0x45C5, 0x054B, 0x362A, 0x76A4, 0x50E8, 0x1066, 0x2307, 0x6389,
|
||||
0x6F9F, 0x2F11, 0x1C70, 0x5CFE, 0x7AB2, 0x3A3C, 0x095D, 0x49D3,
|
||||
0x1171, 0x51FF, 0x629E, 0x2210, 0x045C, 0x44D2, 0x77B3, 0x373D,
|
||||
0x3B2B, 0x7BA5, 0x48C4, 0x084A, 0x2E06, 0x6E88, 0x5DE9, 0x1D67,
|
||||
0x1E5E, 0x5ED0, 0x6DB1, 0x2D3F, 0x0B73, 0x4BFD, 0x789C, 0x3812,
|
||||
0x3404, 0x748A, 0x47EB, 0x0765, 0x2129, 0x61A7, 0x52C6, 0x1248,
|
||||
0x4AEA, 0x0A64, 0x3905, 0x798B, 0x5FC7, 0x1F49, 0x2C28, 0x6CA6,
|
||||
0x60B0, 0x203E, 0x135F, 0x53D1, 0x759D, 0x3513, 0x0672, 0x46FC,
|
||||
0x7979, 0x39F7, 0x0A96, 0x4A18, 0x6C54, 0x2CDA, 0x1FBB, 0x5F35,
|
||||
0x5323, 0x13AD, 0x20CC, 0x6042, 0x460E, 0x0680, 0x35E1, 0x756F,
|
||||
0x2DCD, 0x6D43, 0x5E22, 0x1EAC, 0x38E0, 0x786E, 0x4B0F, 0x0B81,
|
||||
0x0797, 0x4719, 0x7478, 0x34F6, 0x12BA, 0x5234, 0x6155, 0x21DB,
|
||||
0x22E2, 0x626C, 0x510D, 0x1183, 0x37CF, 0x7741, 0x4420, 0x04AE,
|
||||
0x08B8, 0x4836, 0x7B57, 0x3BD9, 0x1D95, 0x5D1B, 0x6E7A, 0x2EF4,
|
||||
0x7656, 0x36D8, 0x05B9, 0x4537, 0x637B, 0x23F5, 0x1094, 0x501A,
|
||||
0x5C0C, 0x1C82, 0x2FE3, 0x6F6D, 0x4921, 0x09AF, 0x3ACE, 0x7A40,
|
||||
0x3CBC, 0x7C32, 0x4F53, 0x0FDD, 0x2991, 0x691F, 0x5A7E, 0x1AF0,
|
||||
0x16E6, 0x5668, 0x6509, 0x2587, 0x03CB, 0x4345, 0x7024, 0x30AA,
|
||||
0x6808, 0x2886, 0x1BE7, 0x5B69, 0x7D25, 0x3DAB, 0x0ECA, 0x4E44,
|
||||
0x4252, 0x02DC, 0x31BD, 0x7133, 0x577F, 0x17F1, 0x2490, 0x641E,
|
||||
0x6727, 0x27A9, 0x14C8, 0x5446, 0x720A, 0x3284, 0x01E5, 0x416B,
|
||||
0x4D7D, 0x0DF3, 0x3E92, 0x7E1C, 0x5850, 0x18DE, 0x2BBF, 0x6B31,
|
||||
0x3393, 0x731D, 0x407C, 0x00F2, 0x26BE, 0x6630, 0x5551, 0x15DF,
|
||||
0x19C9, 0x5947, 0x6A26, 0x2AA8, 0x0CE4, 0x4C6A, 0x7F0B, 0x3F85,
|
||||
```
|
||||
|
||||
|
||||
#### CRC16 계산 테이블 생성 코드
|
||||
```csharp
|
||||
const ushort polynomial = 0x7979;
|
||||
ushort[] CRC16_TABLE = new ushort[256];
|
||||
|
||||
for (ushort i = 0; i < CRC16_TABLE.Length; i++)
|
||||
{
|
||||
ushort value = 0;
|
||||
ushort temp = i;
|
||||
for (byte j = 0; j < 8; j++)
|
||||
{
|
||||
if (((value ^ temp) & 0x0001) != 0)
|
||||
{
|
||||
value = (ushort)((value >> 1) ^ polynomial);
|
||||
}
|
||||
else
|
||||
{
|
||||
value >>= 1;
|
||||
}
|
||||
temp >>= 1;
|
||||
}
|
||||
CRC16_TABLE[i] = value;
|
||||
}
|
||||
```
|
||||
|
||||
#### CRC16 계산 예시
|
||||
```csharp
|
||||
ushort CalculateCRC16(byte[] data)
|
||||
{
|
||||
ushort crc = 0xFFFF;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
byte index = (byte)(crc ^ data[i]);
|
||||
crc = (ushort)((crc >> 8) ^ CRC16_TABLE[index]);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
```
|
||||
|
||||
## 라이센스
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
6
AGVLogic/EnigProtocol/sample/App.config
Normal file
6
AGVLogic/EnigProtocol/sample/App.config
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
</configuration>
|
||||
450
AGVLogic/EnigProtocol/sample/Form1.Designer.cs
generated
Normal file
450
AGVLogic/EnigProtocol/sample/Form1.Designer.cs
generated
Normal file
@@ -0,0 +1,450 @@
|
||||
namespace SampleProject
|
||||
{
|
||||
partial class Form1
|
||||
{
|
||||
/// <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.components = new System.ComponentModel.Container();
|
||||
this.rtRx = new System.Windows.Forms.RichTextBox();
|
||||
this.rtTx = new System.Windows.Forms.RichTextBox();
|
||||
this.tbmsg = new System.Windows.Forms.TextBox();
|
||||
this.btsend = new System.Windows.Forms.Button();
|
||||
this.cmbport = new System.Windows.Forms.ComboBox();
|
||||
this.btconnect = new System.Windows.Forms.Button();
|
||||
this.tbbaud = new System.Windows.Forms.ComboBox();
|
||||
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
|
||||
this.sbPort = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.timer1 = new System.Windows.Forms.Timer(this.components);
|
||||
this.cmbid = new System.Windows.Forms.ComboBox();
|
||||
this.btsim = new System.Windows.Forms.Button();
|
||||
this.rtMsg = new System.Windows.Forms.RichTextBox();
|
||||
this.tbCmd = new System.Windows.Forms.TextBox();
|
||||
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.label5 = new System.Windows.Forms.Label();
|
||||
this.label4 = new System.Windows.Forms.Label();
|
||||
this.label3 = new System.Windows.Forms.Label();
|
||||
this.panel2 = new System.Windows.Forms.Panel();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.tabControl1 = new System.Windows.Forms.TabControl();
|
||||
this.tabPage1 = new System.Windows.Forms.TabPage();
|
||||
this.tabPage2 = new System.Windows.Forms.TabPage();
|
||||
this.btSave = new System.Windows.Forms.Button();
|
||||
this.btload = new System.Windows.Forms.Button();
|
||||
this.rtCmd = new System.Windows.Forms.RichTextBox();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
this.tableLayoutPanel1.SuspendLayout();
|
||||
this.panel1.SuspendLayout();
|
||||
this.panel2.SuspendLayout();
|
||||
this.tabControl1.SuspendLayout();
|
||||
this.tabPage1.SuspendLayout();
|
||||
this.tabPage2.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// rtRx
|
||||
//
|
||||
this.rtRx.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.rtRx.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.rtRx.Location = new System.Drawing.Point(4, 5);
|
||||
this.rtRx.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.rtRx.Name = "rtRx";
|
||||
this.rtRx.Size = new System.Drawing.Size(462, 302);
|
||||
this.rtRx.TabIndex = 0;
|
||||
this.rtRx.Text = "";
|
||||
//
|
||||
// rtTx
|
||||
//
|
||||
this.rtTx.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.rtTx.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.rtTx.Location = new System.Drawing.Point(474, 5);
|
||||
this.rtTx.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.rtTx.Name = "rtTx";
|
||||
this.rtTx.Size = new System.Drawing.Size(462, 302);
|
||||
this.rtTx.TabIndex = 1;
|
||||
this.rtTx.Text = "";
|
||||
//
|
||||
// tbmsg
|
||||
//
|
||||
this.tbmsg.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192)))));
|
||||
this.tbmsg.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.tbmsg.Font = new System.Drawing.Font("굴림", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.tbmsg.Location = new System.Drawing.Point(436, 5);
|
||||
this.tbmsg.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.tbmsg.Name = "tbmsg";
|
||||
this.tbmsg.Size = new System.Drawing.Size(273, 44);
|
||||
this.tbmsg.TabIndex = 2;
|
||||
this.tbmsg.Text = "mesage";
|
||||
this.tbmsg.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
this.tbmsg.TextChanged += new System.EventHandler(this.tbmsg_TextChanged);
|
||||
//
|
||||
// btsend
|
||||
//
|
||||
this.btsend.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this.btsend.Font = new System.Drawing.Font("굴림", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.btsend.Location = new System.Drawing.Point(709, 5);
|
||||
this.btsend.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.btsend.Name = "btsend";
|
||||
this.btsend.Size = new System.Drawing.Size(120, 42);
|
||||
this.btsend.TabIndex = 3;
|
||||
this.btsend.Text = "Send";
|
||||
this.btsend.UseVisualStyleBackColor = true;
|
||||
this.btsend.Click += new System.EventHandler(this.btsend_Click);
|
||||
//
|
||||
// cmbport
|
||||
//
|
||||
this.cmbport.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.cmbport.Font = new System.Drawing.Font("굴림", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.cmbport.FormattingEnabled = true;
|
||||
this.cmbport.Location = new System.Drawing.Point(58, 3);
|
||||
this.cmbport.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.cmbport.Name = "cmbport";
|
||||
this.cmbport.Size = new System.Drawing.Size(218, 40);
|
||||
this.cmbport.TabIndex = 4;
|
||||
//
|
||||
// btconnect
|
||||
//
|
||||
this.btconnect.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this.btconnect.Location = new System.Drawing.Point(765, 3);
|
||||
this.btconnect.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.btconnect.Name = "btconnect";
|
||||
this.btconnect.Size = new System.Drawing.Size(186, 40);
|
||||
this.btconnect.TabIndex = 5;
|
||||
this.btconnect.Text = "connect";
|
||||
this.btconnect.UseVisualStyleBackColor = true;
|
||||
this.btconnect.Click += new System.EventHandler(this.btconnect_Click);
|
||||
//
|
||||
// tbbaud
|
||||
//
|
||||
this.tbbaud.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.tbbaud.Font = new System.Drawing.Font("굴림", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.tbbaud.FormattingEnabled = true;
|
||||
this.tbbaud.Location = new System.Drawing.Point(331, 3);
|
||||
this.tbbaud.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.tbbaud.Name = "tbbaud";
|
||||
this.tbbaud.Size = new System.Drawing.Size(171, 40);
|
||||
this.tbbaud.TabIndex = 6;
|
||||
this.tbbaud.Text = "9600";
|
||||
//
|
||||
// statusStrip1
|
||||
//
|
||||
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.sbPort});
|
||||
this.statusStrip1.Location = new System.Drawing.Point(0, 583);
|
||||
this.statusStrip1.Name = "statusStrip1";
|
||||
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 20, 0);
|
||||
this.statusStrip1.Size = new System.Drawing.Size(954, 22);
|
||||
this.statusStrip1.TabIndex = 7;
|
||||
this.statusStrip1.Text = "statusStrip1";
|
||||
//
|
||||
// sbPort
|
||||
//
|
||||
this.sbPort.Name = "sbPort";
|
||||
this.sbPort.Size = new System.Drawing.Size(121, 17);
|
||||
this.sbPort.Text = "toolStripStatusLabel1";
|
||||
//
|
||||
// timer1
|
||||
//
|
||||
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
|
||||
//
|
||||
// cmbid
|
||||
//
|
||||
this.cmbid.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.cmbid.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.cmbid.Font = new System.Drawing.Font("굴림", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.cmbid.FormattingEnabled = true;
|
||||
this.cmbid.Location = new System.Drawing.Point(60, 5);
|
||||
this.cmbid.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.cmbid.Name = "cmbid";
|
||||
this.cmbid.Size = new System.Drawing.Size(183, 40);
|
||||
this.cmbid.TabIndex = 8;
|
||||
//
|
||||
// btsim
|
||||
//
|
||||
this.btsim.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this.btsim.Font = new System.Drawing.Font("굴림", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.btsim.Location = new System.Drawing.Point(829, 5);
|
||||
this.btsim.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.btsim.Name = "btsim";
|
||||
this.btsim.Size = new System.Drawing.Size(120, 42);
|
||||
this.btsim.TabIndex = 9;
|
||||
this.btsim.Text = "Test";
|
||||
this.btsim.UseVisualStyleBackColor = true;
|
||||
this.btsim.Click += new System.EventHandler(this.btsim_Click);
|
||||
//
|
||||
// rtMsg
|
||||
//
|
||||
this.rtMsg.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(224)))), ((int)(((byte)(224)))), ((int)(((byte)(224)))));
|
||||
this.tableLayoutPanel1.SetColumnSpan(this.rtMsg, 2);
|
||||
this.rtMsg.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.rtMsg.Location = new System.Drawing.Point(4, 317);
|
||||
this.rtMsg.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.rtMsg.Name = "rtMsg";
|
||||
this.rtMsg.Size = new System.Drawing.Size(932, 124);
|
||||
this.rtMsg.TabIndex = 10;
|
||||
this.rtMsg.Text = "";
|
||||
//
|
||||
// tbCmd
|
||||
//
|
||||
this.tbCmd.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.tbCmd.Font = new System.Drawing.Font("굴림", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.tbCmd.Location = new System.Drawing.Point(298, 5);
|
||||
this.tbCmd.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.tbCmd.Name = "tbCmd";
|
||||
this.tbCmd.Size = new System.Drawing.Size(83, 44);
|
||||
this.tbCmd.TabIndex = 11;
|
||||
this.tbCmd.Text = "cmd";
|
||||
this.tbCmd.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
this.tableLayoutPanel1.ColumnCount = 2;
|
||||
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
|
||||
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
|
||||
this.tableLayoutPanel1.Controls.Add(this.rtMsg, 0, 1);
|
||||
this.tableLayoutPanel1.Controls.Add(this.rtRx, 0, 0);
|
||||
this.tableLayoutPanel1.Controls.Add(this.rtTx, 1, 0);
|
||||
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.tableLayoutPanel1.Location = new System.Drawing.Point(3, 3);
|
||||
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
this.tableLayoutPanel1.RowCount = 2;
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 70F));
|
||||
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 30F));
|
||||
this.tableLayoutPanel1.Size = new System.Drawing.Size(940, 446);
|
||||
this.tableLayoutPanel1.TabIndex = 12;
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
this.panel1.Controls.Add(this.tbmsg);
|
||||
this.panel1.Controls.Add(this.label5);
|
||||
this.panel1.Controls.Add(this.tbCmd);
|
||||
this.panel1.Controls.Add(this.label4);
|
||||
this.panel1.Controls.Add(this.cmbid);
|
||||
this.panel1.Controls.Add(this.label3);
|
||||
this.panel1.Controls.Add(this.btsend);
|
||||
this.panel1.Controls.Add(this.btsim);
|
||||
this.panel1.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
this.panel1.Location = new System.Drawing.Point(0, 531);
|
||||
this.panel1.Name = "panel1";
|
||||
this.panel1.Padding = new System.Windows.Forms.Padding(5);
|
||||
this.panel1.Size = new System.Drawing.Size(954, 52);
|
||||
this.panel1.TabIndex = 13;
|
||||
//
|
||||
// label5
|
||||
//
|
||||
this.label5.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.label5.Font = new System.Drawing.Font("Arial", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||
this.label5.Location = new System.Drawing.Point(381, 5);
|
||||
this.label5.Name = "label5";
|
||||
this.label5.Size = new System.Drawing.Size(55, 42);
|
||||
this.label5.TabIndex = 14;
|
||||
this.label5.Text = "DATA\r\n(n)";
|
||||
this.label5.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// label4
|
||||
//
|
||||
this.label4.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.label4.Font = new System.Drawing.Font("Arial", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||
this.label4.Location = new System.Drawing.Point(243, 5);
|
||||
this.label4.Name = "label4";
|
||||
this.label4.Size = new System.Drawing.Size(55, 42);
|
||||
this.label4.TabIndex = 13;
|
||||
this.label4.Text = "CMD\r\n(1)";
|
||||
this.label4.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// label3
|
||||
//
|
||||
this.label3.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.label3.Font = new System.Drawing.Font("Arial", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
||||
this.label3.Location = new System.Drawing.Point(5, 5);
|
||||
this.label3.Name = "label3";
|
||||
this.label3.Size = new System.Drawing.Size(55, 42);
|
||||
this.label3.TabIndex = 12;
|
||||
this.label3.Text = "ID\r\n(1)";
|
||||
this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// panel2
|
||||
//
|
||||
this.panel2.Controls.Add(this.tbbaud);
|
||||
this.panel2.Controls.Add(this.label2);
|
||||
this.panel2.Controls.Add(this.cmbport);
|
||||
this.panel2.Controls.Add(this.btconnect);
|
||||
this.panel2.Controls.Add(this.label1);
|
||||
this.panel2.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.panel2.Location = new System.Drawing.Point(0, 0);
|
||||
this.panel2.Name = "panel2";
|
||||
this.panel2.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.panel2.Size = new System.Drawing.Size(954, 46);
|
||||
this.panel2.TabIndex = 14;
|
||||
//
|
||||
// label2
|
||||
//
|
||||
this.label2.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.label2.Location = new System.Drawing.Point(276, 3);
|
||||
this.label2.Name = "label2";
|
||||
this.label2.Size = new System.Drawing.Size(55, 40);
|
||||
this.label2.TabIndex = 8;
|
||||
this.label2.Text = "baud";
|
||||
this.label2.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.label1.Location = new System.Drawing.Point(3, 3);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(55, 40);
|
||||
this.label1.TabIndex = 7;
|
||||
this.label1.Text = "port";
|
||||
this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// tabControl1
|
||||
//
|
||||
this.tabControl1.Controls.Add(this.tabPage1);
|
||||
this.tabControl1.Controls.Add(this.tabPage2);
|
||||
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.tabControl1.Location = new System.Drawing.Point(0, 46);
|
||||
this.tabControl1.Name = "tabControl1";
|
||||
this.tabControl1.SelectedIndex = 0;
|
||||
this.tabControl1.Size = new System.Drawing.Size(954, 485);
|
||||
this.tabControl1.TabIndex = 15;
|
||||
//
|
||||
// tabPage1
|
||||
//
|
||||
this.tabPage1.Controls.Add(this.tableLayoutPanel1);
|
||||
this.tabPage1.Location = new System.Drawing.Point(4, 29);
|
||||
this.tabPage1.Name = "tabPage1";
|
||||
this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tabPage1.Size = new System.Drawing.Size(946, 452);
|
||||
this.tabPage1.TabIndex = 0;
|
||||
this.tabPage1.Text = "Test";
|
||||
this.tabPage1.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// tabPage2
|
||||
//
|
||||
this.tabPage2.Controls.Add(this.btSave);
|
||||
this.tabPage2.Controls.Add(this.btload);
|
||||
this.tabPage2.Controls.Add(this.rtCmd);
|
||||
this.tabPage2.Location = new System.Drawing.Point(4, 29);
|
||||
this.tabPage2.Name = "tabPage2";
|
||||
this.tabPage2.Padding = new System.Windows.Forms.Padding(3);
|
||||
this.tabPage2.Size = new System.Drawing.Size(946, 452);
|
||||
this.tabPage2.TabIndex = 1;
|
||||
this.tabPage2.Text = "Command";
|
||||
this.tabPage2.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// btSave
|
||||
//
|
||||
this.btSave.Location = new System.Drawing.Point(820, 50);
|
||||
this.btSave.Name = "btSave";
|
||||
this.btSave.Size = new System.Drawing.Size(113, 29);
|
||||
this.btSave.TabIndex = 13;
|
||||
this.btSave.Text = "Save";
|
||||
this.btSave.UseVisualStyleBackColor = true;
|
||||
this.btSave.Click += new System.EventHandler(this.btSave_Click);
|
||||
//
|
||||
// btload
|
||||
//
|
||||
this.btload.Location = new System.Drawing.Point(820, 15);
|
||||
this.btload.Name = "btload";
|
||||
this.btload.Size = new System.Drawing.Size(113, 29);
|
||||
this.btload.TabIndex = 12;
|
||||
this.btload.Text = "Load";
|
||||
this.btload.UseVisualStyleBackColor = true;
|
||||
this.btload.Click += new System.EventHandler(this.btload_Click);
|
||||
//
|
||||
// rtCmd
|
||||
//
|
||||
this.rtCmd.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.rtCmd.Location = new System.Drawing.Point(3, 3);
|
||||
this.rtCmd.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.rtCmd.Name = "rtCmd";
|
||||
this.rtCmd.Size = new System.Drawing.Size(940, 446);
|
||||
this.rtCmd.TabIndex = 11;
|
||||
this.rtCmd.Text = "";
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(10F, 19F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(954, 605);
|
||||
this.Controls.Add(this.tabControl1);
|
||||
this.Controls.Add(this.panel2);
|
||||
this.Controls.Add(this.panel1);
|
||||
this.Controls.Add(this.statusStrip1);
|
||||
this.Font = new System.Drawing.Font("굴림", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
|
||||
this.Name = "Form1";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "!!";
|
||||
this.Load += new System.EventHandler(this.Form1_Load);
|
||||
this.statusStrip1.ResumeLayout(false);
|
||||
this.statusStrip1.PerformLayout();
|
||||
this.tableLayoutPanel1.ResumeLayout(false);
|
||||
this.panel1.ResumeLayout(false);
|
||||
this.panel1.PerformLayout();
|
||||
this.panel2.ResumeLayout(false);
|
||||
this.tabControl1.ResumeLayout(false);
|
||||
this.tabPage1.ResumeLayout(false);
|
||||
this.tabPage2.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.RichTextBox rtRx;
|
||||
private System.Windows.Forms.RichTextBox rtTx;
|
||||
private System.Windows.Forms.TextBox tbmsg;
|
||||
private System.Windows.Forms.Button btsend;
|
||||
private System.Windows.Forms.ComboBox cmbport;
|
||||
private System.Windows.Forms.Button btconnect;
|
||||
private System.Windows.Forms.ComboBox tbbaud;
|
||||
private System.Windows.Forms.StatusStrip statusStrip1;
|
||||
private System.Windows.Forms.ToolStripStatusLabel sbPort;
|
||||
private System.Windows.Forms.Timer timer1;
|
||||
private System.Windows.Forms.ComboBox cmbid;
|
||||
private System.Windows.Forms.Button btsim;
|
||||
private System.Windows.Forms.RichTextBox rtMsg;
|
||||
private System.Windows.Forms.TextBox tbCmd;
|
||||
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.Panel panel2;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Label label5;
|
||||
private System.Windows.Forms.Label label4;
|
||||
private System.Windows.Forms.Label label3;
|
||||
private System.Windows.Forms.TabControl tabControl1;
|
||||
private System.Windows.Forms.TabPage tabPage1;
|
||||
private System.Windows.Forms.TabPage tabPage2;
|
||||
private System.Windows.Forms.RichTextBox rtCmd;
|
||||
private System.Windows.Forms.Button btSave;
|
||||
private System.Windows.Forms.Button btload;
|
||||
}
|
||||
}
|
||||
|
||||
225
AGVLogic/EnigProtocol/sample/Form1.cs
Normal file
225
AGVLogic/EnigProtocol/sample/Form1.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using AR;
|
||||
using ENIG;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SampleProject
|
||||
{
|
||||
enum messageType
|
||||
{
|
||||
rx,
|
||||
tx,
|
||||
normal,
|
||||
error
|
||||
}
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
|
||||
System.IO.Ports.SerialPort dev;
|
||||
EEProtocol proto;
|
||||
Dictionary<string, byte> idlist;
|
||||
public Form1()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.Text = $"Amkor ENIG Protocol Test (PanID:46A5, CH:17)";
|
||||
proto = new EEProtocol();
|
||||
proto.OnDataReceived += Proto_OnDataReceived;
|
||||
proto.OnMessage += Proto_OnMessage;
|
||||
dev = new System.IO.Ports.SerialPort();
|
||||
dev.ReadTimeout = 2000;
|
||||
dev.WriteTimeout = 1000;
|
||||
dev.DataReceived += (s1, e1) =>
|
||||
{
|
||||
var buffer = new byte[dev.BytesToRead];
|
||||
dev.Read(buffer, 0, buffer.Length);
|
||||
proto.ProcessReceivedData(buffer);
|
||||
};
|
||||
|
||||
idlist = new Dictionary<string, byte>();
|
||||
idlist.Add("ACS", 0);
|
||||
idlist.Add("AGV1", 10);
|
||||
idlist.Add("AGV2", 11);
|
||||
idlist.Add("BUFFER1", 20);
|
||||
idlist.Add("BUFFER2", 21);
|
||||
idlist.Add("BUFFER3", 22);
|
||||
idlist.Add("BUFFER4", 23);
|
||||
idlist.Add("BUFFER5", 24);
|
||||
idlist.Add("DOOR", 30);
|
||||
|
||||
this.cmbid.Items.Clear();
|
||||
foreach (var item in idlist)
|
||||
cmbid.Items.Add($"{item.Key}");
|
||||
|
||||
tbCmd.Text = "FF";
|
||||
tbmsg.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void Proto_OnDataReceived(object sender, EEProtocol.DataEventArgs e)
|
||||
{
|
||||
var hexstrRaw = e.ReceivedPacket.RawData.HexString();
|
||||
var hexstr = e.ReceivedPacket.Data.HexString();
|
||||
var cmd = e.ReceivedPacket.Command.ToString("X2");
|
||||
var id = e.ReceivedPacket.ID.ToString("X2");
|
||||
AddMessage($"{hexstrRaw}\nID:{id},CMD:{cmd},DATA:{hexstr}", messageType.rx);
|
||||
}
|
||||
private void Proto_OnMessage(object sender, EEProtocol.MessageEventArgs e)
|
||||
{
|
||||
AddMessage(e.Message);
|
||||
}
|
||||
|
||||
void AddMessage(string msg, messageType type = messageType.normal)
|
||||
{
|
||||
if (this.InvokeRequired)
|
||||
{
|
||||
this.BeginInvoke(new Action(() => AddMessage(msg, type)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == messageType.rx)
|
||||
{
|
||||
rtRx.AppendText($"<{DateTime.Now.ToString("HH:mm:ss")} RX> {msg}\r\n");
|
||||
rtRx.ScrollToCaret();
|
||||
}
|
||||
else if (type == messageType.tx)
|
||||
{
|
||||
rtTx.AppendText($"<{DateTime.Now.ToString("HH:mm:ss")} TX> {msg}\r\n");
|
||||
rtTx.ScrollToCaret();
|
||||
}
|
||||
else
|
||||
{
|
||||
rtMsg.AppendText($"<{DateTime.Now.ToString("HH:mm:ss")} > {msg}\r\n");
|
||||
rtMsg.ScrollToCaret();
|
||||
}
|
||||
}
|
||||
|
||||
private void Form1_Load(object sender, EventArgs e)
|
||||
{
|
||||
cmbport.Items.Clear();
|
||||
foreach (var item in System.IO.Ports.SerialPort.GetPortNames())
|
||||
{
|
||||
cmbport.Items.Add(item);
|
||||
}
|
||||
if (Pub.Setting.LastPort.isEmpty() == false) cmbport.Text = Pub.Setting.LastPort;
|
||||
else if (cmbport.Items.Count > 0) cmbport.SelectedIndex = 0;
|
||||
|
||||
if (Pub.Setting.LastBaud.isEmpty() == false) tbbaud.Text = Pub.Setting.LastBaud;
|
||||
if (cmbid.Items.Count > 0) cmbid.SelectedIndex = 0;
|
||||
LoadCmds();
|
||||
this.timer1.Start();
|
||||
}
|
||||
|
||||
private void btconnect_Click(object sender, EventArgs e)
|
||||
{
|
||||
var port = cmbport.Text.Trim();
|
||||
var baud = tbbaud.Text.toInt();
|
||||
|
||||
//setting save
|
||||
Pub.Setting.LastPort = port;
|
||||
Pub.Setting.LastBaud = baud.ToString();
|
||||
Pub.Setting.Save();
|
||||
|
||||
if (dev.IsOpen)
|
||||
{
|
||||
dev.Close();
|
||||
AddMessage("port closed");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
this.dev.PortName = port;
|
||||
this.dev.BaudRate = baud;
|
||||
dev.Open();
|
||||
AddMessage("port opened");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddMessage(ex.Message, messageType.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void timer1_Tick(object sender, EventArgs e)
|
||||
{
|
||||
sbPort.Text = $"{dev.PortName}:{dev.BaudRate} {(dev.IsOpen ? "Open" : "Closed")}";
|
||||
}
|
||||
|
||||
private void tbmsg_TextChanged(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void btsend_Click(object sender, EventArgs e)
|
||||
{
|
||||
//send to device
|
||||
var msg = tbmsg.Text.Trim();
|
||||
var id = this.idlist[cmbid.Text];
|
||||
var cmdHex = this.tbCmd.Text.Trim();
|
||||
byte cmd = Convert.ToByte(cmdHex, 16);
|
||||
byte[] msgBytes = Encoding.ASCII.GetBytes(msg); // 메시지를 byte 배열로 변환
|
||||
|
||||
var data = proto.CreatePacket(id, cmd, msgBytes);
|
||||
SendToDevice(data);
|
||||
}
|
||||
|
||||
void SendToDevice(byte[] packet)
|
||||
{
|
||||
if (dev.IsOpen == false)
|
||||
{
|
||||
AddMessage("port closed", messageType.error);
|
||||
}
|
||||
else
|
||||
{
|
||||
dev.Write(packet, 0, packet.Length);
|
||||
var hexstring = packet.HexString();
|
||||
AddMessage(hexstring, messageType.tx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void btsim_Click(object sender, EventArgs e)
|
||||
{
|
||||
//packet test
|
||||
var msg = tbmsg.Text.Trim();
|
||||
var id = this.idlist[cmbid.Text];
|
||||
var cmdHex = this.tbCmd.Text.Trim();
|
||||
byte cmd = Convert.ToByte(cmdHex, 16); // 16진수 문자열을 byte로 변환
|
||||
byte[] msgBytes = Encoding.ASCII.GetBytes(msg); // 메시지를 byte 배열로 변환
|
||||
|
||||
var data = proto.CreatePacket(id, cmd, msgBytes);
|
||||
this.proto.PacketTest(data);
|
||||
}
|
||||
string fn = "cmds.rtf";
|
||||
private void btSave_Click(object sender, EventArgs e)
|
||||
{
|
||||
System.IO.File.WriteAllText("cmds.rtf", rtCmd.Rtf, System.Text.Encoding.Default);
|
||||
}
|
||||
|
||||
private void btload_Click(object sender, EventArgs e)
|
||||
{
|
||||
LoadCmds();
|
||||
}
|
||||
|
||||
void LoadCmds()
|
||||
{
|
||||
var fi = new System.IO.FileInfo("cmds.rtf");
|
||||
if (fi.Exists == false)
|
||||
{
|
||||
AddMessage("no cmds file", messageType.error);
|
||||
return;
|
||||
}
|
||||
var data = System.IO.File.ReadAllText("cmds.rtf", System.Text.Encoding.Default);
|
||||
rtCmd.Rtf = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
AGVLogic/EnigProtocol/sample/Form1.resx
Normal file
126
AGVLogic/EnigProtocol/sample/Form1.resx
Normal file
@@ -0,0 +1,126 @@
|
||||
<?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 through the TypeConverter architecture.
|
||||
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" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<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" type="xsd:string" use="required" />
|
||||
</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="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="timer1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>134, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
23
AGVLogic/EnigProtocol/sample/Program.cs
Normal file
23
AGVLogic/EnigProtocol/sample/Program.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SampleProject
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 해당 애플리케이션의 주 진입점입니다.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Pub.init();
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
}
|
||||
}
|
||||
36
AGVLogic/EnigProtocol/sample/Properties/AssemblyInfo.cs
Normal file
36
AGVLogic/EnigProtocol/sample/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
|
||||
// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
|
||||
// 이러한 특성 값을 변경하세요.
|
||||
[assembly: AssemblyTitle("SampleProject")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("ATK")]
|
||||
[assembly: AssemblyProduct("SampleProject")]
|
||||
[assembly: AssemblyCopyright("Copyright © ATK 2025")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
|
||||
// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
|
||||
// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다.
|
||||
[assembly: Guid("fab31c8a-7dcf-4152-8a82-76f3c10baba4")]
|
||||
|
||||
// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다.
|
||||
//
|
||||
// 주 버전
|
||||
// 부 버전
|
||||
// 빌드 번호
|
||||
// 수정 버전
|
||||
//
|
||||
// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호를
|
||||
// 기본값으로 할 수 있습니다.
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
71
AGVLogic/EnigProtocol/sample/Properties/Resources.Designer.cs
generated
Normal file
71
AGVLogic/EnigProtocol/sample/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,71 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 이 코드는 도구를 사용하여 생성되었습니다.
|
||||
// 런타임 버전:4.0.30319.42000
|
||||
//
|
||||
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
|
||||
// 이러한 변경 내용이 손실됩니다.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace SampleProject.Properties
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다.
|
||||
/// </summary>
|
||||
// 이 클래스는 ResGen 또는 Visual Studio와 같은 도구를 통해 StronglyTypedResourceBuilder
|
||||
// 클래스에서 자동으로 생성되었습니다.
|
||||
// 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여
|
||||
// ResGen을 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources
|
||||
{
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if ((resourceMan == null))
|
||||
{
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SampleProject.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해 현재 스레드의 CurrentUICulture 속성을
|
||||
/// 재정의합니다.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture
|
||||
{
|
||||
get
|
||||
{
|
||||
return resourceCulture;
|
||||
}
|
||||
set
|
||||
{
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
AGVLogic/EnigProtocol/sample/Properties/Resources.resx
Normal file
117
AGVLogic/EnigProtocol/sample/Properties/Resources.resx
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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 through the TypeConverter architecture.
|
||||
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.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: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" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</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" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</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" type="xsd:string" use="required" />
|
||||
</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=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
30
AGVLogic/EnigProtocol/sample/Properties/Settings.Designer.cs
generated
Normal file
30
AGVLogic/EnigProtocol/sample/Properties/Settings.Designer.cs
generated
Normal file
@@ -0,0 +1,30 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace SampleProject.Properties
|
||||
{
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
|
||||
{
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default
|
||||
{
|
||||
get
|
||||
{
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
||||
21
AGVLogic/EnigProtocol/sample/Pub.cs
Normal file
21
AGVLogic/EnigProtocol/sample/Pub.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SampleProject
|
||||
{
|
||||
public static class Pub
|
||||
{
|
||||
public static Settings Setting { get; set; }
|
||||
public static void init()
|
||||
{
|
||||
Setting = new Settings();
|
||||
Setting.Load();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
94
AGVLogic/EnigProtocol/sample/SampleProject.csproj
Normal file
94
AGVLogic/EnigProtocol/sample/SampleProject.csproj
Normal file
@@ -0,0 +1,94 @@
|
||||
<?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>{FAB31C8A-7DCF-4152-8A82-76F3C10BABA4}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>SampleProject</RootNamespace>
|
||||
<AssemblyName>SampleProject</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
</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="arCommUtil">
|
||||
<HintPath>.\arCommUtil.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Deployment" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Form1.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Form1.Designer.cs">
|
||||
<DependentUpon>Form1.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Pub.cs" />
|
||||
<Compile Include="Settings.cs" />
|
||||
<EmbeddedResource Include="Form1.resx">
|
||||
<DependentUpon>Form1.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<None Include="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
<Compile Include="Properties\Settings.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\enigprotocol\ENIGProtocol.csproj">
|
||||
<Project>{499d8912-4b96-41e5-a70d-cfe797883d65}</Project>
|
||||
<Name>ENIGProtocol</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
25
AGVLogic/EnigProtocol/sample/Settings.cs
Normal file
25
AGVLogic/EnigProtocol/sample/Settings.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using AR;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SampleProject
|
||||
{
|
||||
public class Settings : AR.Setting
|
||||
{
|
||||
public string LastPort { get; set; }
|
||||
public string LastBaud { get; set; }
|
||||
public override void AfterLoad()
|
||||
{
|
||||
// throw new NotImplementedException();
|
||||
if (LastBaud.isEmpty()) LastBaud = "9600";
|
||||
}
|
||||
|
||||
public override void AfterSave()
|
||||
{
|
||||
// throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user