파일정리
This commit is contained in:
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user