refactor: Move AGV development projects to separate AGVLogic folder

- Reorganized AGVMapEditor, AGVNavigationCore, AGVSimulator into AGVLogic folder
- Removed deleted project files from root folder tracking
- Updated CLAUDE.md with AGVLogic-specific development guidelines
- Clean separation of independent project development from main codebase
- Projects now ready for independent development and future integration

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
backuppc
2025-10-23 10:00:40 +09:00
parent ce78752c2c
commit dbaf647d4e
63 changed files with 1398 additions and 212 deletions

View File

@@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Express 15 for Windows Desktop
VisualStudioVersion = 17.14.36310.24 VisualStudioVersion = 15.0.36324.19
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sub", "Sub", "{C423C39A-44E7-4F09-B2F7-7943975FF948}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sub", "Sub", "{C423C39A-44E7-4F09-B2F7-7943975FF948}"
EndProject EndProject
@@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "솔루션 항목", "솔루
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVNavigationCore", "AGVNavigationCore\AGVNavigationCore.csproj", "{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVNavigationCore", "AGVNavigationCore\AGVNavigationCore.csproj", "{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Logic", "Logic", "{E5C75D32-5AD6-44DD-8F27-E32023206EBB}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -220,6 +222,9 @@ Global
{14E8C9A5-013E-49BA-B435-EFEFC77DD623} = {C423C39A-44E7-4F09-B2F7-7943975FF948} {14E8C9A5-013E-49BA-B435-EFEFC77DD623} = {C423C39A-44E7-4F09-B2F7-7943975FF948}
{EB77976F-4DE4-46A5-8B25-D07226204C32} = {7AF32085-E7A6-4D06-BA6E-C6B1EBAEA99A} {EB77976F-4DE4-46A5-8B25-D07226204C32} = {7AF32085-E7A6-4D06-BA6E-C6B1EBAEA99A}
{9365803B-933D-4237-93C7-B502C855A71C} = {C423C39A-44E7-4F09-B2F7-7943975FF948} {9365803B-933D-4237-93C7-B502C855A71C} = {C423C39A-44E7-4F09-B2F7-7943975FF948}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {E5C75D32-5AD6-44DD-8F27-E32023206EBB}
{B2C3D4E5-0000-0000-0000-000000000000} = {E5C75D32-5AD6-44DD-8F27-E32023206EBB}
{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C} = {E5C75D32-5AD6-44DD-8F27-E32023206EBB}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B5B1FD72-356F-4840-83E8-B070AC21C8D9} SolutionGuid = {B5B1FD72-356F-4840-83E8-B070AC21C8D9}

View File

@@ -73,6 +73,8 @@
<SubType>UserControl</SubType> <SubType>UserControl</SubType>
</Compile> </Compile>
<Compile Include="Models\Enums.cs" /> <Compile Include="Models\Enums.cs" />
<Compile Include="Models\IMovableAGV.cs" />
<Compile Include="Models\VirtualAGV.cs" />
<Compile Include="Models\MapLoader.cs" /> <Compile Include="Models\MapLoader.cs" />
<Compile Include="Models\MapNode.cs" /> <Compile Include="Models\MapNode.cs" />
<Compile Include="PathFinding\Planning\AGVPathfinder.cs" /> <Compile Include="PathFinding\Planning\AGVPathfinder.cs" />

View File

@@ -0,0 +1,215 @@
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>
string CurrentNodeId { get; }
/// <summary>
/// 목표 위치
/// </summary>
Point? TargetPosition { get; }
/// <summary>
/// 목표 노드 ID
/// </summary>
string TargetNodeId { 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>
string GetCurrentNodeId();
/// <summary>
/// AGV 상태 정보 문자열 조회
/// </summary>
string GetStatus();
#endregion
#region Path Execution Methods
/// <summary>
/// 경로 실행
/// </summary>
void ExecutePath(AGVPathResult path, List<MapNode> mapNodes);
/// <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
}
}

View File

@@ -2,20 +2,19 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using AGVMapEditor.Models; using AGVNavigationCore.Controls;
using AGVNavigationCore.Models; using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding; using AGVNavigationCore.PathFinding;
using AGVNavigationCore.PathFinding.Core; using AGVNavigationCore.PathFinding.Core;
using AGVNavigationCore.Controls;
namespace AGVSimulator.Models namespace AGVNavigationCore.Models
{ {
/// <summary> /// <summary>
/// 가상 AGV 클래스 /// 가상 AGV 클래스 (코어 비즈니스 로직)
/// 실제 AGV의 동작을 시뮬레이션 /// 실제 AGV와 시뮬레이터 모두에서 사용 가능한 공용 로직
/// 시뮬레이션과 실제 동작이 동일하게 동작하도록 설계됨
/// </summary> /// </summary>
public class VirtualAGV : IAGV public class VirtualAGV : IMovableAGV, IAGV
{ {
#region Events #region Events
@@ -66,8 +65,7 @@ namespace AGVSimulator.Models
private MapNode _targetNode; private MapNode _targetNode;
// 이동 관련 // 이동 관련
private System.Windows.Forms.Timer _moveTimer; private DateTime _lastUpdateTime;
private DateTime _lastMoveTime;
private Point _moveStartPosition; private Point _moveStartPosition;
private Point _moveTargetPosition; private Point _moveTargetPosition;
private float _moveProgress; private float _moveProgress;
@@ -78,7 +76,7 @@ namespace AGVSimulator.Models
// 시뮬레이션 설정 // 시뮬레이션 설정
private readonly float _moveSpeed = 50.0f; // 픽셀/초 private readonly float _moveSpeed = 50.0f; // 픽셀/초
private readonly float _rotationSpeed = 90.0f; // 도/초 private readonly float _rotationSpeed = 90.0f; // 도/초
private readonly int _updateInterval = 50; // ms private bool _isMoving;
#endregion #endregion
@@ -130,7 +128,7 @@ namespace AGVSimulator.Models
/// <summary> /// <summary>
/// 현재 노드 ID /// 현재 노드 ID
/// </summary> /// </summary>
public string CurrentNodeId => _currentNode.NodeId; public string CurrentNodeId => _currentNode?.NodeId;
/// <summary> /// <summary>
/// 목표 위치 /// 목표 위치
@@ -145,7 +143,7 @@ namespace AGVSimulator.Models
/// <summary> /// <summary>
/// 목표 노드 ID /// 목표 노드 ID
/// </summary> /// </summary>
public string TargetNodeId => _targetNode.NodeId; public string TargetNodeId => _targetNode?.NodeId;
/// <summary> /// <summary>
/// 도킹 방향 /// 도킹 방향
@@ -170,34 +168,93 @@ namespace AGVSimulator.Models
_currentState = AGVState.Idle; _currentState = AGVState.Idle;
_currentSpeed = 0; _currentSpeed = 0;
_dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹 _dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹
_currentNode = null; // = string.Empty; _currentNode = null;
_targetNode = null;// string.Empty; _targetNode = null;
_isMoving = false;
InitializeTimer(); _lastUpdateTime = DateTime.Now;
} }
#endregion #endregion
#region Initialization #region Public Methods - /RFID ( AGV에서 )
private void InitializeTimer() /// <summary>
/// 현재 위치 설정 (실제 AGV 센서에서)
/// </summary>
public void SetCurrentPosition(Point position)
{ {
_moveTimer = new System.Windows.Forms.Timer(); _currentPosition = position;
_moveTimer.Interval = _updateInterval; }
_moveTimer.Tick += OnMoveTimer_Tick;
_lastMoveTime = DateTime.Now; /// <summary>
/// 감지된 RFID 설정 (실제 RFID 센서에서)
/// </summary>
public void SetDetectedRfid(string rfidId)
{
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 < 20.0f && _currentState != AGVState.Charging)
{
OnError($"배터리 부족: {BatteryLevel:F1}%");
}
} }
#endregion #endregion
#region Public Methods #region Public Methods -
/// <summary>
/// 현재 위치 조회
/// </summary>
public Point GetCurrentPosition() => _currentPosition;
/// <summary>
/// 현재 상태 조회
/// </summary>
public AGVState GetCurrentState() => _currentState;
/// <summary>
/// 현재 노드 ID 조회
/// </summary>
public string GetCurrentNodeId() => _currentNode?.NodeId;
/// <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>
/// 경로 실행 시작 /// 경로 실행 시작
/// </summary> /// </summary>
/// <param name="path">실행할 경로</param> /// <param name="path">실행할 경로</param>
/// <param name="mapNodes">맵 노드 목록</param> /// <param name="mapNodes">맵 노드 목록</param>
public void StartPath(AGVPathResult path, List<MapNode> mapNodes) public void ExecutePath(AGVPathResult path, List<MapNode> mapNodes)
{ {
if (path == null || !path.Success) if (path == null || !path.Success)
{ {
@@ -220,13 +277,14 @@ namespace AGVSimulator.Models
// 목표 노드 설정 (경로의 마지막 노드) // 목표 노드 설정 (경로의 마지막 노드)
if (_remainingNodes.Count > 1) if (_remainingNodes.Count > 1)
{ {
var _targetNodeId = _remainingNodes[_remainingNodes.Count - 1]; var targetNodeId = _remainingNodes[_remainingNodes.Count - 1];
var targetNode = mapNodes.FirstOrDefault(n => n.NodeId == _targetNodeId); var targetNode = mapNodes.FirstOrDefault(n => n.NodeId == targetNodeId);
// 목표 노드의 타입에 따라 도킹 방향 결정 // 목표 노드의 타입에 따라 도킹 방향 결정
if (targetNode != null) if (targetNode != null)
{ {
_dockingDirection = GetDockingDirection(targetNode.Type); _dockingDirection = GetDockingDirection(targetNode.Type);
_targetNode = targetNode;
} }
} }
@@ -239,12 +297,20 @@ namespace AGVSimulator.Models
} }
} }
/// <summary>
/// 간단한 경로 실행 (경로 객체 없이 노드만)
/// </summary>
public void StartPath(AGVPathResult path, List<MapNode> mapNodes)
{
ExecutePath(path, mapNodes);
}
/// <summary> /// <summary>
/// 경로 정지 /// 경로 정지
/// </summary> /// </summary>
public void StopPath() public void StopPath()
{ {
_moveTimer.Stop(); _isMoving = false;
_currentPath = null; _currentPath = null;
_remainingNodes?.Clear(); _remainingNodes?.Clear();
SetState(AGVState.Idle); SetState(AGVState.Idle);
@@ -260,6 +326,30 @@ namespace AGVSimulator.Models
OnError("긴급 정지가 실행되었습니다."); OnError("긴급 정지가 실행되었습니다.");
} }
#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>
/// 수동 이동 (테스트용) /// 수동 이동 (테스트용)
/// </summary> /// </summary>
@@ -272,7 +362,7 @@ namespace AGVSimulator.Models
_moveProgress = 0; _moveProgress = 0;
SetState(AGVState.Moving); SetState(AGVState.Moving);
_moveTimer.Start(); _isMoving = true;
} }
/// <summary> /// <summary>
@@ -285,51 +375,18 @@ namespace AGVSimulator.Models
return; return;
SetState(AGVState.Rotating); SetState(AGVState.Rotating);
// 시뮬레이션: 즉시 방향 변경 (실제로는 시간이 걸림)
_currentDirection = direction; _currentDirection = direction;
System.Threading.Thread.Sleep(500); // 회전 시간 시뮬레이션
SetState(AGVState.Idle); SetState(AGVState.Idle);
} }
/// <summary> /// <summary>
/// AGV 위치 직접 설정 (시뮬레이터용) /// 충전 시작
/// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
/// </summary>
/// <param name="newPosition">새로운 위치</param>
/// <param name="motorDirection">모터이동방향</param>
public void SetPosition(MapNode node, Point newPosition, AgvDirection motorDirection)
{
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
if (_currentPosition != Point.Empty)
{
_targetPosition = _currentPosition; // 이전 위치 (previousPos 역할)
_targetDirection = _currentDirection;
_targetNode = node;
}
// 새로운 위치 설정
_currentPosition = newPosition;
_currentDirection = motorDirection;
_currentNode = node;
// 위치 변경 이벤트 발생
PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
}
/// <summary>
/// 충전 시작 (시뮬레이션)
/// </summary> /// </summary>
public void StartCharging() public void StartCharging()
{ {
if (_currentState == AGVState.Idle) if (_currentState == AGVState.Idle)
{ {
SetState(AGVState.Charging); SetState(AGVState.Charging);
// 충전 시뮬레이션 시작
} }
} }
@@ -344,14 +401,34 @@ namespace AGVSimulator.Models
} }
} }
#endregion
#region Public Methods - AGV ()
/// <summary> /// <summary>
/// AGV 정보 조회 /// AGV 위치 직접 설
/// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
/// </summary> /// </summary>
public string GetStatus() /// <param name="node">현재 노드</param>
/// <param name="newPosition">새로운 위치</param>
/// <param name="motorDirection">모터이동방향</param>
public void SetPosition(MapNode node, Point newPosition, AgvDirection motorDirection)
{ {
return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " + // 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
$"방향:{_currentDirection} 상태:{_currentState} " + if (_currentPosition != Point.Empty)
$"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%"; {
_targetPosition = _currentPosition; // 이전 위치
_targetDirection = _currentDirection;
_targetNode = node;
}
// 새로운 위치 설정
_currentPosition = newPosition;
_currentDirection = motorDirection;
_currentNode = node;
// 위치 변경 이벤트 발생
PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
} }
/// <summary> /// <summary>
@@ -359,12 +436,10 @@ namespace AGVSimulator.Models
/// </summary> /// </summary>
public string SimulateRfidReading(List<MapNode> mapNodes) public string SimulateRfidReading(List<MapNode> mapNodes)
{ {
// 현재 위치에서 가장 가까운 노드 찾기
var closestNode = FindClosestNode(_currentPosition, mapNodes); var closestNode = FindClosestNode(_currentPosition, mapNodes);
if (closestNode == null) if (closestNode == null)
return null; return null;
// 해당 노드의 RFID 정보 반환 (MapNode에 RFID 정보 포함)
return closestNode.HasRfid() ? closestNode.RfidId : null; return closestNode.HasRfid() ? closestNode.RfidId : null;
} }
@@ -375,26 +450,13 @@ namespace AGVSimulator.Models
private void StartMovement() private void StartMovement()
{ {
SetState(AGVState.Moving); SetState(AGVState.Moving);
_moveTimer.Start(); _isMoving = true;
_lastMoveTime = DateTime.Now; _lastUpdateTime = DateTime.Now;
}
private void OnMoveTimer_Tick(object sender, EventArgs e)
{
var now = DateTime.Now;
var deltaTime = (float)(now - _lastMoveTime).TotalSeconds;
_lastMoveTime = now;
UpdateMovement(deltaTime);
UpdateBattery(deltaTime);
// 위치 변경 이벤트 발생
PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
} }
private void UpdateMovement(float deltaTime) private void UpdateMovement(float deltaTime)
{ {
if (_currentState != AGVState.Moving) if (_currentState != AGVState.Moving || !_isMoving)
return; return;
// 목표 위치까지의 거리 계산 // 목표 위치까지의 거리 계산
@@ -450,12 +512,6 @@ namespace AGVSimulator.Models
} }
BatteryLevel = Math.Max(0, BatteryLevel); BatteryLevel = Math.Max(0, BatteryLevel);
// 배터리 부족 경고
if (BatteryLevel < 20.0f && _currentState != AGVState.Charging)
{
OnError($"배터리 부족: {BatteryLevel:F1}%");
}
} }
private void ProcessNextNode() private void ProcessNextNode()
@@ -463,7 +519,7 @@ namespace AGVSimulator.Models
if (_remainingNodes == null || _currentNodeIndex >= _remainingNodes.Count - 1) if (_remainingNodes == null || _currentNodeIndex >= _remainingNodes.Count - 1)
{ {
// 경로 완료 // 경로 완료
_moveTimer.Stop(); _isMoving = false;
SetState(AGVState.Idle); SetState(AGVState.Idle);
PathCompleted?.Invoke(this, _currentPath); PathCompleted?.Invoke(this, _currentPath);
return; return;
@@ -475,10 +531,8 @@ namespace AGVSimulator.Models
// RFID 감지 시뮬레이션 // RFID 감지 시뮬레이션
RfidDetected?.Invoke(this, $"RFID_{nextNodeId}"); RfidDetected?.Invoke(this, $"RFID_{nextNodeId}");
//_currentNodeId = nextNodeId;
// 다음 목표 위치 설정 (실제로는 맵에서 좌표 가져와야 함) // 다음 목표 위치 설정 (실제로는 맵에서 좌표 가져와야 함)
// 여기서는 간단히 현재 위치에서 랜덤 오프셋으로 설정
var random = new Random(); var random = new Random();
_moveTargetPosition = new Point( _moveTargetPosition = new Point(
_currentPosition.X + random.Next(-100, 100), _currentPosition.X + random.Next(-100, 100),
@@ -504,7 +558,6 @@ namespace AGVSimulator.Models
} }
} }
// 일정 거리 내에 있는 노드만 반환
return closestDistance < 50.0f ? closestNode : null; return closestDistance < 50.0f ? closestNode : null;
} }
@@ -529,11 +582,11 @@ namespace AGVSimulator.Models
switch (nodeType) switch (nodeType)
{ {
case NodeType.Charging: case NodeType.Charging:
return DockingDirection.Forward; // 충전기: 전진 도킹 return DockingDirection.Forward;
case NodeType.Docking: case NodeType.Docking:
return DockingDirection.Backward; // 장비 (로더, 클리너, 오프로더, 버퍼): 후진 도킹 return DockingDirection.Backward;
default: default:
return DockingDirection.Forward; // 기본값: 전진 return DockingDirection.Forward;
} }
} }
@@ -552,10 +605,9 @@ namespace AGVSimulator.Models
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
_moveTimer?.Stop(); StopPath();
_moveTimer?.Dispose();
} }
#endregion #endregion
} }
} }

View File

@@ -33,7 +33,7 @@ namespace AGVNavigationCore.PathFinding.Planning
/// AGV 경로 계산 /// AGV 경로 계산
/// </summary> /// </summary>
public AGVPathResult FindPath(MapNode startNode, MapNode targetNode, public AGVPathResult FindPath(MapNode startNode, MapNode targetNode,
MapNode prevNode) MapNode prevNode, AgvDirection currentDirection = AgvDirection.Forward)
{ {
var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -52,7 +52,7 @@ namespace AGVNavigationCore.PathFinding.Planning
// 통합된 경로 계획 함수 사용 // 통합된 경로 계획 함수 사용
AGVPathResult result = PlanPath(startNode, targetNode, prevNode, requiredDirection); AGVPathResult result = PlanPath(startNode, targetNode, prevNode, requiredDirection, currentDirection);
result.CalculationTimeMs = stopwatch.ElapsedMilliseconds; result.CalculationTimeMs = stopwatch.ElapsedMilliseconds;
@@ -90,10 +90,10 @@ namespace AGVNavigationCore.PathFinding.Planning
/// <summary> /// <summary>
/// 통합 경로 계획 (직접 경로 또는 방향 전환 경로) /// 통합 경로 계획 (직접 경로 또는 방향 전환 경로)
/// </summary> /// </summary>
private AGVPathResult PlanPath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection? requiredDirection = null) private AGVPathResult PlanPath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection? requiredDirection = null, AgvDirection currentDirection = AgvDirection.Forward)
{ {
bool needDirectionChange = false;// requiredDirection.HasValue && (currentDirection != requiredDirection.Value); bool needDirectionChange = requiredDirection.HasValue && (currentDirection != requiredDirection.Value);
//현재 위치에서 목적지까지의 최단 거리 모록을 찾는다. //현재 위치에서 목적지까지의 최단 거리 모록을 찾는다.
var DirectPathResult = _basicPathfinder.FindPath(startNode.NodeId, targetNode.NodeId); var DirectPathResult = _basicPathfinder.FindPath(startNode.NodeId, targetNode.NodeId);

View File

@@ -12,6 +12,100 @@ namespace AGVNavigationCore.Utils
/// </summary> /// </summary>
public static class LiftCalculator 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.NodeId != previousNode.NodeId).ToList();
}
if (nextNodes.Count == 1)
{
// 직선 경로: 다음 노드 방향으로 예측
targetPosition = nextNodes.First().Position;
calculationMethod = $"전진 경로 예측 ({currentNode.NodeId}→{nextNodes.First().NodeId})";
}
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> /// <summary>
/// AGV 이동 방향과 모터 방향을 기반으로 리프트 각도 계산 /// AGV 이동 방향과 모터 방향을 기반으로 리프트 각도 계산
/// </summary> /// </summary>
@@ -146,98 +240,7 @@ namespace AGVNavigationCore.Utils
}; };
} }
/// <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.NodeId != previousNode.NodeId).ToList();
}
if (nextNodes.Count == 1)
{
// 직선 경로: 다음 노드 방향으로 예측
targetPosition = nextNodes.First().Position;
calculationMethod = $"전진 경로 예측 ({currentNode.NodeId}→{nextNodes.First().NodeId})";
}
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> /// <summary>
/// 위치 기반 노드 찾기 /// 위치 기반 노드 찾기

View File

@@ -9,10 +9,10 @@ using AGVNavigationCore.Models;
using AGVNavigationCore.Controls; using AGVNavigationCore.Controls;
using AGVNavigationCore.PathFinding; using AGVNavigationCore.PathFinding;
using AGVNavigationCore.Utils; using AGVNavigationCore.Utils;
using AGVSimulator.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using AGVNavigationCore.PathFinding.Planning; using AGVNavigationCore.PathFinding.Planning;
using AGVNavigationCore.PathFinding.Core; using AGVNavigationCore.PathFinding.Core;
using AGVSimulator.Models;
namespace AGVSimulator.Forms namespace AGVSimulator.Forms
{ {
@@ -302,8 +302,22 @@ namespace AGVSimulator.Forms
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
var currentDirection = selectedAGV?.CurrentDirection ?? AgvDirection.Forward; var currentDirection = selectedAGV?.CurrentDirection ?? AgvDirection.Forward;
// AGV의 이전 위치에서 가장 가까운 노드 찾기
MapNode prevNode = startNode; // 기본값으로 시작 노드 사용
if (selectedAGV != null && _mapNodes != null && _mapNodes.Count > 0)
{
// AGV 현재 위치에서 가장 가까운 노드 찾기
var agvPos = selectedAGV.CurrentPosition;
prevNode = _mapNodes.OrderBy(n =>
Math.Sqrt(Math.Pow(n.Position.X - agvPos.X, 2) +
Math.Pow(n.Position.Y - agvPos.Y, 2))).FirstOrDefault();
if (prevNode == null)
prevNode = startNode;
}
// 고급 경로 계획 사용 (노드 객체 직접 전달) // 고급 경로 계획 사용 (노드 객체 직접 전달)
var advancedResult = _advancedPathfinder.FindPath(startNode, targetNode, currentDirection); var advancedResult = _advancedPathfinder.FindPath(startNode, targetNode, prevNode, currentDirection);
if (advancedResult.Success) if (advancedResult.Success)
{ {
@@ -519,8 +533,18 @@ namespace AGVSimulator.Forms
private void OnSimulationTimer_Tick(object sender, EventArgs e) private void OnSimulationTimer_Tick(object sender, EventArgs e)
{ {
// 시뮬레이션 업데이트는 각 AGV의 내부 타이머에서 처리됨 // 모든 AGV의 업데이트 메서드 호출 (100ms 간격)
if (_agvList != null)
{
foreach (var agv in _agvList)
{
agv.Update(100); // 100ms 간격으로 업데이트
}
}
// UI 업데이트
UpdateUI(); UpdateUI();
_simulatorCanvas.Invalidate(); // 화면 다시 그리기
} }
#endregion #endregion

View File

@@ -0,0 +1,561 @@
//using System;
//using System.Collections.Generic;
//using System.Drawing;
//using System.Linq;
//using AGVMapEditor.Models;
//using AGVNavigationCore.Models;
//using AGVNavigationCore.PathFinding;
//using AGVNavigationCore.PathFinding.Core;
//using AGVNavigationCore.Controls;
//namespace AGVSimulator.Models
//{
// /// <summary>
// /// 가상 AGV 클래스
// /// 실제 AGV의 동작을 시뮬레이션
// /// </summary>
// public class VirtualAGV : 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 _targetPosition;
// private string _targetId;
// private string _currentId;
// private AgvDirection _currentDirection;
// private AgvDirection _targetDirection;
// private AGVState _currentState;
// private float _currentSpeed;
// // 경로 관련
// private AGVPathResult _currentPath;
// private List<string> _remainingNodes;
// private int _currentNodeIndex;
// private MapNode _currentNode;
// private MapNode _targetNode;
// // 이동 관련
// private System.Windows.Forms.Timer _moveTimer;
// private DateTime _lastMoveTime;
// private Point _moveStartPosition;
// private Point _moveTargetPosition;
// private float _moveProgress;
// // 도킹 관련
// private DockingDirection _dockingDirection;
// // 시뮬레이션 설정
// private readonly float _moveSpeed = 50.0f; // 픽셀/초
// private readonly float _rotationSpeed = 90.0f; // 도/초
// private readonly int _updateInterval = 50; // ms
// #endregion
// #region Properties
// /// <summary>
// /// AGV ID
// /// </summary>
// public string AgvId => _agvId;
// /// <summary>
// /// 현재 위치
// /// </summary>
// public Point CurrentPosition
// {
// 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;
// /// <summary>
// /// 현재 노드 ID
// /// </summary>
// public string CurrentNodeId => _currentNode.NodeId;
// /// <summary>
// /// 목표 위치
// /// </summary>
// public Point? TargetPosition => _targetPosition;
// /// <summary>
// /// 배터리 레벨 (시뮬레이션)
// /// </summary>
// public float BatteryLevel { get; set; } = 100.0f;
// /// <summary>
// /// 목표 노드 ID
// /// </summary>
// public string TargetNodeId => _targetNode.NodeId;
// /// <summary>
// /// 도킹 방향
// /// </summary>
// public DockingDirection DockingDirection => _dockingDirection;
// #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; // = string.Empty;
// _targetNode = null;// string.Empty;
// InitializeTimer();
// }
// #endregion
// #region Initialization
// private void InitializeTimer()
// {
// _moveTimer = new System.Windows.Forms.Timer();
// _moveTimer.Interval = _updateInterval;
// _moveTimer.Tick += OnMoveTimer_Tick;
// _lastMoveTime = DateTime.Now;
// }
// #endregion
// #region Public Methods
// /// <summary>
// /// 경로 실행 시작
// /// </summary>
// /// <param name="path">실행할 경로</param>
// /// <param name="mapNodes">맵 노드 목록</param>
// public void StartPath(AGVPathResult path, List<MapNode> mapNodes)
// {
// if (path == null || !path.Success)
// {
// OnError("유효하지 않은 경로입니다.");
// return;
// }
// _currentPath = path;
// _remainingNodes = new List<string>(path.Path);
// _currentNodeIndex = 0;
// // 시작 노드와 목표 노드 설정
// if (_remainingNodes.Count > 0)
// {
// var startNode = mapNodes.FirstOrDefault(n => n.NodeId == _remainingNodes[0]);
// if (startNode != null)
// {
// _currentNode = startNode;
// // 목표 노드 설정 (경로의 마지막 노드)
// if (_remainingNodes.Count > 1)
// {
// var _targetNodeId = _remainingNodes[_remainingNodes.Count - 1];
// var targetNode = mapNodes.FirstOrDefault(n => n.NodeId == _targetNodeId);
// // 목표 노드의 타입에 따라 도킹 방향 결정
// if (targetNode != null)
// {
// _dockingDirection = GetDockingDirection(targetNode.Type);
// }
// }
// StartMovement();
// }
// else
// {
// OnError($"시작 노드를 찾을 수 없습니다: {_remainingNodes[0]}");
// }
// }
// }
// /// <summary>
// /// 경로 정지
// /// </summary>
// public void StopPath()
// {
// _moveTimer.Stop();
// _currentPath = null;
// _remainingNodes?.Clear();
// SetState(AGVState.Idle);
// _currentSpeed = 0;
// }
// /// <summary>
// /// 긴급 정지
// /// </summary>
// public void EmergencyStop()
// {
// StopPath();
// OnError("긴급 정지가 실행되었습니다.");
// }
// /// <summary>
// /// 수동 이동 (테스트용)
// /// </summary>
// /// <param name="targetPosition">목표 위치</param>
// public void MoveTo(Point targetPosition)
// {
// _targetPosition = targetPosition;
// _moveStartPosition = _currentPosition;
// _moveTargetPosition = targetPosition;
// _moveProgress = 0;
// SetState(AGVState.Moving);
// _moveTimer.Start();
// }
// /// <summary>
// /// 수동 회전 (테스트용)
// /// </summary>
// /// <param name="direction">회전 방향</param>
// public void Rotate(AgvDirection direction)
// {
// if (_currentState != AGVState.Idle)
// return;
// SetState(AGVState.Rotating);
// // 시뮬레이션: 즉시 방향 변경 (실제로는 시간이 걸림)
// _currentDirection = direction;
// System.Threading.Thread.Sleep(500); // 회전 시간 시뮬레이션
// SetState(AGVState.Idle);
// }
// /// <summary>
// /// AGV 위치 직접 설정 (시뮬레이터용)
// /// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
// /// </summary>
// /// <param name="newPosition">새로운 위치</param>
// /// <param name="motorDirection">모터이동방향</param>
// public void SetPosition(MapNode node, Point newPosition, AgvDirection motorDirection)
// {
// // 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
// if (_currentPosition != Point.Empty)
// {
// _targetPosition = _currentPosition; // 이전 위치 (previousPos 역할)
// _targetDirection = _currentDirection;
// _targetNode = node;
// }
// // 새로운 위치 설정
// _currentPosition = newPosition;
// _currentDirection = motorDirection;
// _currentNode = node;
// // 위치 변경 이벤트 발생
// PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
// }
// /// <summary>
// /// 충전 시작 (시뮬레이션)
// /// </summary>
// public void StartCharging()
// {
// if (_currentState == AGVState.Idle)
// {
// SetState(AGVState.Charging);
// // 충전 시뮬레이션 시작
// }
// }
// /// <summary>
// /// 충전 종료
// /// </summary>
// public void StopCharging()
// {
// if (_currentState == AGVState.Charging)
// {
// SetState(AGVState.Idle);
// }
// }
// /// <summary>
// /// AGV 정보 조회
// /// </summary>
// public string GetStatus()
// {
// return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " +
// $"방향:{_currentDirection} 상태:{_currentState} " +
// $"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%";
// }
// /// <summary>
// /// 현재 RFID 시뮬레이션 (현재 위치 기준)
// /// </summary>
// public string SimulateRfidReading(List<MapNode> mapNodes)
// {
// // 현재 위치에서 가장 가까운 노드 찾기
// var closestNode = FindClosestNode(_currentPosition, mapNodes);
// if (closestNode == null)
// return null;
// // 해당 노드의 RFID 정보 반환 (MapNode에 RFID 정보 포함)
// return closestNode.HasRfid() ? closestNode.RfidId : null;
// }
// #endregion
// #region Private Methods
// private void StartMovement()
// {
// SetState(AGVState.Moving);
// _moveTimer.Start();
// _lastMoveTime = DateTime.Now;
// }
// private void OnMoveTimer_Tick(object sender, EventArgs e)
// {
// var now = DateTime.Now;
// var deltaTime = (float)(now - _lastMoveTime).TotalSeconds;
// _lastMoveTime = now;
// UpdateMovement(deltaTime);
// UpdateBattery(deltaTime);
// // 위치 변경 이벤트 발생
// PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
// }
// private void UpdateMovement(float deltaTime)
// {
// if (_currentState != AGVState.Moving)
// return;
// // 목표 위치까지의 거리 계산
// var distance = CalculateDistance(_currentPosition, _moveTargetPosition);
// if (distance < 5.0f) // 도달 임계값
// {
// // 목표 도달
// _currentPosition = _moveTargetPosition;
// _currentSpeed = 0;
// // 다음 노드로 이동
// ProcessNextNode();
// }
// else
// {
// // 계속 이동
// var moveDistance = _moveSpeed * deltaTime;
// var direction = new PointF(
// _moveTargetPosition.X - _currentPosition.X,
// _moveTargetPosition.Y - _currentPosition.Y
// );
// // 정규화
// var length = (float)Math.Sqrt(direction.X * direction.X + direction.Y * direction.Y);
// if (length > 0)
// {
// direction.X /= length;
// direction.Y /= length;
// }
// // 새 위치 계산
// _currentPosition = new Point(
// (int)(_currentPosition.X + direction.X * moveDistance),
// (int)(_currentPosition.Y + direction.Y * moveDistance)
// );
// _currentSpeed = _moveSpeed;
// }
// }
// private void UpdateBattery(float deltaTime)
// {
// // 배터리 소모 시뮬레이션
// if (_currentState == AGVState.Moving)
// {
// BatteryLevel -= 0.1f * deltaTime; // 이동시 소모
// }
// else if (_currentState == AGVState.Charging)
// {
// BatteryLevel += 5.0f * deltaTime; // 충전
// BatteryLevel = Math.Min(100.0f, BatteryLevel);
// }
// BatteryLevel = Math.Max(0, BatteryLevel);
// // 배터리 부족 경고
// if (BatteryLevel < 20.0f && _currentState != AGVState.Charging)
// {
// OnError($"배터리 부족: {BatteryLevel:F1}%");
// }
// }
// private void ProcessNextNode()
// {
// if (_remainingNodes == null || _currentNodeIndex >= _remainingNodes.Count - 1)
// {
// // 경로 완료
// _moveTimer.Stop();
// SetState(AGVState.Idle);
// PathCompleted?.Invoke(this, _currentPath);
// return;
// }
// // 다음 노드로 이동
// _currentNodeIndex++;
// var nextNodeId = _remainingNodes[_currentNodeIndex];
// // RFID 감지 시뮬레이션
// RfidDetected?.Invoke(this, $"RFID_{nextNodeId}");
// //_currentNodeId = nextNodeId;
// // 다음 목표 위치 설정 (실제로는 맵에서 좌표 가져와야 함)
// // 여기서는 간단히 현재 위치에서 랜덤 오프셋으로 설정
// var random = new Random();
// _moveTargetPosition = new Point(
// _currentPosition.X + random.Next(-100, 100),
// _currentPosition.Y + random.Next(-100, 100)
// );
// }
// private MapNode FindClosestNode(Point position, List<MapNode> mapNodes)
// {
// if (mapNodes == null || mapNodes.Count == 0)
// return null;
// MapNode closestNode = null;
// float closestDistance = float.MaxValue;
// foreach (var node in mapNodes)
// {
// var distance = CalculateDistance(position, node.Position);
// if (distance < closestDistance)
// {
// closestDistance = distance;
// closestNode = node;
// }
// }
// // 일정 거리 내에 있는 노드만 반환
// return closestDistance < 50.0f ? closestNode : null;
// }
// private float CalculateDistance(Point from, Point to)
// {
// var dx = to.X - from.X;
// var dy = to.Y - from.Y;
// return (float)Math.Sqrt(dx * dx + dy * dy);
// }
// private void SetState(AGVState newState)
// {
// if (_currentState != newState)
// {
// _currentState = newState;
// StateChanged?.Invoke(this, newState);
// }
// }
// private DockingDirection GetDockingDirection(NodeType nodeType)
// {
// switch (nodeType)
// {
// case NodeType.Charging:
// return DockingDirection.Forward; // 충전기: 전진 도킹
// case NodeType.Docking:
// return DockingDirection.Backward; // 장비 (로더, 클리너, 오프로더, 버퍼): 후진 도킹
// default:
// return DockingDirection.Forward; // 기본값: 전진
// }
// }
// private void OnError(string message)
// {
// SetState(AGVState.Error);
// ErrorOccurred?.Invoke(this, message);
// }
// #endregion
// #region Cleanup
// /// <summary>
// /// 리소스 정리
// /// </summary>
// public void Dispose()
// {
// _moveTimer?.Stop();
// _moveTimer?.Dispose();
// }
// #endregion
// }
//}

221
Cs_HMI/AGVLogic/CLAUDE.md Normal file
View File

@@ -0,0 +1,221 @@
# CLAUDE.md (AGVLogic 폴더)
이 파일은 AGVLogic 폴더에서 개발 중인 AGV 관련 프로젝트들을 위한 개발 가이드입니다.
**현재 폴더 위치**: `C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\AGVLogic\`
**맵데이터**: `../Data/NewMap.agvmap` 파일을 기준으로 사용
---
## 프로젝트 개요
현재 AGVLogic 폴더에서 다음 3개의 독립 프로젝트를 개발 중입니다:
### 1. AGVMapEditor (맵 에디터)
**위치**: `./AGVMapEditor/`
**실행파일**: `./AGVMapEditor/bin/Debug/AGVMapEditor.exe`
#### 핵심 기능
- **맵 노드 관리**: 논리적 노드 생성, 연결, 속성 설정
- **RFID 매핑**: 물리적 RFID ID ↔ 논리적 노드 ID 매핑
- **시각적 편집**: 드래그앤드롭으로 노드 배치 및 연결
- **JSON 저장**: 맵 데이터를 JSON 형식으로 저장/로드
- **노드 연결 관리**: 연결 목록 표시 및 직접 삭제 기능
#### 핵심 클래스
- **MapNode**: 논리적 맵 노드 (NodeId, 위치, 타입, 연결 정보)
- **RfidMapping**: RFID 물리적 ID ↔ 논리적 노드 ID 매핑
- **NodeResolver**: RFID ID를 통한 노드 해석기
- **MapCanvas**: 시각적 맵 편집 컨트롤
### 2. AGVNavigationCore (경로 탐색 라이브러리)
**위치**: `./AGVNavigationCore/`
#### 핵심 기능
- **A* 경로 탐색**: 최적 경로 계산 알고리즘
- **방향 제어**: 전진/후진 모터 방향 결정
- **도킹 검증**: 충전기/장비 도킹 방향 검증
- **리프트 계산**: AGV 리프트 각도 계산
- **경로 최적화**: 회전 구간 회피 등 고급 옵션
#### 핵심 클래스
- **PathFinding/Core/AStarPathfinder.cs**: A* 알고리즘 구현
- **PathFinding/Planning/AGVPathfinder.cs**: 경로 탐색 메인 클래스
- **PathFinding/Planning/DirectionChangePlanner.cs**: 방향 변경 계획
- **Utils/LiftCalculator.cs**: 리프트 각도 계산
- **Utils/DockingValidator.cs**: 도킹 유효성 검증
- **Controls/UnifiedAGVCanvas.cs**: 맵 및 AGV 시각화
### 3. AGVSimulator (AGV 시뮬레이터)
**위치**: `./AGVSimulator/`
**실행파일**: `./AGVSimulator/bin/Debug/AGVSimulator.exe`
#### 핵심 기능
- **가상 AGV 시뮬레이션**: 실시간 AGV 움직임 및 상태 관리
- **맵 시각화**: 맵 에디터에서 생성한 맵 파일 로드 및 표시
- **경로 실행**: 계산된 경로를 따라 AGV 시뮬레이션
- **상태 모니터링**: AGV 상태, 위치, 배터리 등 실시간 표시
#### 핵심 클래스
- **VirtualAGV**: 가상 AGV 동작 시뮬레이션 (이동, 회전, 도킹, 충전)
- **SimulatorCanvas**: AGV 및 맵 시각화 캔버스
- **SimulatorForm**: 시뮬레이터 메인 인터페이스
- **SimulationState**: 시뮬레이션 상태 관리
#### AGV 상태
- **Idle**: 대기
- **Moving**: 이동 중
- **Rotating**: 회전 중
- **Docking**: 도킹 중
- **Charging**: 충전 중
- **Error**: 오류
---
## AGV 방향 제어 및 도킹 시스템
### AGV 하드웨어 레이아웃
```
LIFT --- AGV --- MONITOR
↑ ↑ ↑
후진시 AGV본체 전진시
도달위치 도달위치
```
### 모터 방향과 이동 방향
- **전진 모터 (Forward)**: AGV가 모니터 방향으로 이동 (→)
- **후진 모터 (Backward)**: AGV가 리프트 방향으로 이동 (←)
### 도킹 방향 규칙
- **충전기 (Charging)**: 전진 도킹 (Forward) - 모니터가 충전기 면
- **장비 (Docking)**: 후진 도킹 (Backward) - 리프트가 장비 면
### 핵심 계산 파일들
1. **LiftCalculator.cs** - 리프트 방향 계산
- `CalculateLiftAngleRadians(Point currentPos, Point targetPos, AgvDirection motorDirection)`
2. **DirectionChangePlanner.cs** - 도킹 방향 결정
- `GetRequiredDockingDirection(string targetNodeId)` - 노드타입별 도킹 방향 반환
3. **VirtualAGV.cs** - AGV 위치/방향 관리
- `SetPosition(Point newPosition)` - AGV 위치 및 방향 설정
---
## AGVNavigationCore 프로젝트 구조
### 📁 폴더 구조
```
AGVNavigationCore/
├── Controls/
│ ├── UnifiedAGVCanvas.cs # AGV 및 맵 시각화 메인 캔버스
│ ├── UnifiedAGVCanvas.Events.cs # 그리기 및 이벤트 처리
│ ├── UnifiedAGVCanvas.Mouse.cs # 마우스 인터랙션
│ ├── AGVState.cs # AGV 상태 정의
│ └── IAGV.cs # AGV 인터페이스
├── Models/
│ ├── MapNode.cs # 맵 노드 데이터 모델
│ ├── MapLoader.cs # JSON 맵 파일 로더
│ └── Enums.cs # 열거형 정의 (NodeType, AgvDirection 등)
├── Utils/
│ ├── LiftCalculator.cs # 리프트 각도 계산
│ └── DockingValidator.cs # 도킹 유효성 검증
└── PathFinding/
├── Analysis/
│ └── JunctionAnalyzer.cs # 교차점 분석
├── Core/
│ ├── AStarPathfinder.cs # A* 알고리즘
│ ├── PathNode.cs # 경로 노드
│ └── AGVPathResult.cs # 경로 계산 결과
├── Planning/
│ ├── AGVPathfinder.cs # 경로 탐색 메인 클래스
│ ├── AdvancedAGVPathfinder.cs # 고급 경로 탐색
│ ├── DirectionChangePlanner.cs # 방향 변경 계획
│ ├── NodeMotorInfo.cs # 노드별 모터 정보
│ └── PathfindingOptions.cs # 경로 탐색 옵션
└── Validation/
├── DockingValidationResult.cs # 도킹 검증 결과
└── PathValidationResult.cs # 경로 검증 결과
```
### 🎯 클래스 배치 원칙
#### PathFinding/Validation/
- **검증 결과 클래스**: `*ValidationResult.cs` 패턴 사용
- **패턴**: 정적 팩토리 메서드 (CreateValid, CreateInvalid, CreateNotRequired)
- **속성**: IsValid, ValidationError, 관련 상세 정보
#### PathFinding/Planning/
- **경로 계획 클래스**: 실제 경로 탐색 및 계획 로직
- **방향 변경 로직**: DirectionChangePlanner.cs
- **경로 최적화**: 경로 생성과 관련된 전략
#### PathFinding/Core/
- **핵심 알고리즘**: A* 알고리즘 등 기본 경로 탐색
- **기본 경로 탐색**: 단순한 점-to-점 경로 계산
#### PathFinding/Analysis/
- **경로 분석**: 생성된 경로의 품질 및 특성 분석
- **성능 분석**: 경로 효율성 및 최적화 분석
---
## 개발 워크플로우
### 권장 개발 순서
1. **맵 데이터 준비**: AGVMapEditor로 맵 노드 배치 및 RFID 매핑 설정
2. **경로 탐색 구현**: AGVNavigationCore에서 경로 계산 알고리즘 개발
3. **시뮬레이션 테스트**: AGVSimulator로 AGV 동작 검증
4. **메인 프로젝트 통합**: 개발 완료 후 부모 폴더(Cs_HMI)에 병합
### 중요한 개발 패턴
- **이벤트 기반 아키텍처**: UI 업데이트는 이벤트를 통해 자동화
- **상태 관리**: _hasChanges 플래그로 변경사항 추적
- **에러 처리**: 사용자 확인 다이얼로그와 상태바 메시지 활용
- **코드 재사용**: UnifiedAGVCanvas를 맵에디터와 시뮬레이터에서 공통 사용
### 주의사항
- **PathFinding 로직 변경시**: 반드시 시뮬레이터에서 테스트 후 적용
- **노드 연결 관리**: 물리적 RFID와 논리적 노드 ID 분리 원칙 유지
- **JSON 파일 형식**: 맵 데이터는 MapNodes, RfidMappings 두 섹션으로 구성
- **좌표 시스템**: 줌/팬 상태에서 좌표 변환 정확성 지속 모니터링
---
## 최근 구현 완료 기능
### ✅ 회전 구간 회피 기능 (PathFinding)
- **목적**: AGV 회전 오류를 피하기 위한 선택적 회전 구간 회피
- **파일**: `PathFinding/PathfindingOptions.cs`
- **UI**: AGVSimulator에 "회전 구간 회피" 체크박스
### ✅ 맵 에디터 마우스 좌표 오차 수정
- **문제**: 줌 인/아웃 시 노드 선택 히트 영역이 너무 작음
- **해결**: 최소 화면 히트 영역(20픽셀) 보장
- **파일**: `AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs`
### ✅ 노드 연결 관리 시스템
- **기능**: 노드 연결 목록 표시 및 삭제
- **파일들**:
- `AGVMapEditor/Forms/MainForm.cs` - UI 및 이벤트 처리
- `UnifiedAGVCanvas.cs` - 편집 모드 및 이벤트 정의
- `UnifiedAGVCanvas.Mouse.cs` - 마우스 연결 삭제 기능
---
## 향후 개발 우선순위
1. **방향 전환 기능**: AGV 현재 방향과 목표 방향 불일치 시 회전 노드 경유 로직
2. **맵 검증 기능**: 연결 무결성, 고립된 노드, 순환 경로 등 검증
3. **성능 최적화**: 대형 맵에서 경로 계산 및 연결 목록 표시 성능 개선
4. **실시간 동기화**: 맵 에디터와 시뮬레이터 간 실시간 맵 동기화
---
**최종 업데이트**: 2025-10-23 - AGVLogic 폴더 기준으로 정리

13
Cs_HMI/AGVLogic/build.bat Normal file
View File

@@ -0,0 +1,13 @@
@echo off
echo Building AGV C# HMI Project...
REM Set MSBuild path
REM set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\WDExpress\MSBuild\15.0\Bin\MSBuild.exe"
REM Rebuild Debug x86 configuration (VS-style Rebuild)
%MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild
pause

View File

@@ -0,0 +1 @@
claude --dangerously-skip-permissions

View File

@@ -2,6 +2,101 @@
이 파일은 AGV HMI 시스템의 주요 변경 사항을 기록합니다. 이 파일은 AGV HMI 시스템의 주요 변경 사항을 기록합니다.
## [2025.10.22] - VirtualAGV를 코어 라이브러리로 마이그레이션
### ⚠️ 중요 변경
- **VirtualAGV를 AGVNavigationCore 라이브러리로 이동**
- 이전: `AGVSimulator\Models\VirtualAGV.cs` (시뮬레이터 전용)
- 현재: `AGVNavigationCore\Models\VirtualAGV.cs` (공용 코어 라이브러리)
- **의미**: 실제 AGV 제어 시스템과 시뮬레이터에서 동일한 비즈니스 로직 사용
### ✨ 새로운 기능
- **IMovableAGV 인터페이스 추가**
- **위치**: `AGVNavigationCore\Models\IMovableAGV.cs` (신규)
- **목적**: 실제 AGV와 시뮬레이션 AGV의 계약 정의
- **주요 메서드**:
```csharp
// 센서 입력
void SetCurrentPosition(Point position); // 위치 센서
void SetDetectedRfid(string rfidId); // RFID 센서
void SetMotorDirection(AgvDirection direction); // 모터
void SetBatteryLevel(float percentage); // BMS
// 프레임 업데이트 (외부에서 호출)
void Update(float deltaTimeMs);
```
### 🔧 기술적 개선사항
- **타이머 의존성 제거**
- System.Windows.Forms.Timer 제거
- 외부에서 Update(deltaTimeMs)를 주기적으로 호출하는 방식으로 변경
- 장점: 타이머 없이도 동작 가능, 실제 시스템과 시뮬레이터 모두 적용 가능
- **UI 의존성 제거**
- EventHandler 이외의 UI 라이브러리 제거
- 순수 .NET 라이브러리로 변환 (System 네임스페이스만 사용)
- **프레임 업데이트 방식 개선**
- 이전: 내부 타이머로 자동 업데이트
- 현재: 외부에서 Update(100)을 정기적으로 호출
```csharp
// AGVSimulator\Forms\SimulatorForm.cs의 OnSimulationTimer_Tick
foreach (var agv in _agvList)
{
agv.Update(100); // 100ms 간격으로 업데이트
}
```
### 📝 아키텍처 변화
```
Before:
AGVSimulator (시뮬레이터 전용)
└── VirtualAGV (시뮬레이터용)
└── System.Windows.Forms.Timer
After:
AGVNavigationCore (공용 코어)
├── IMovableAGV (인터페이스)
└── VirtualAGV (구현체)
└── Update(deltaTime) 메서드만
AGVSimulator (시뮬레이터)
└── SimulatorForm.OnSimulationTimer_Tick
└── agv.Update(100)
Project (실제 AGV)
└── AgvController
└── agv.Update(deltaTime)
```
### 🎯 향후 활용
1. **실제 AGV 시스템에서**:
```csharp
var agv = new VirtualAGV("AGV1", startPos);
// 하드웨어 센서에서
agv.SetCurrentPosition(sensorData.Position);
agv.SetDetectedRfid(rfidSensor.Value);
agv.SetMotorDirection(motorController.Direction);
agv.SetBatteryLevel(bms.Level);
// 제어 루프에서
agv.Update(deltaTime);
```
2. **시뮬레이션 테스트**:
- 시뮬레이터에서 완벽하게 동작한 VirtualAGV
- 실제 AGV에 그대로 적용 가능
- 테스트 신뢰도 향상
### 📋 변경 파일
- ✨ 추가: `AGVNavigationCore\Models\VirtualAGV.cs`
- ✨ 추가: `AGVNavigationCore\Models\IMovableAGV.cs`
- 📝 수정: `AGVNavigationCore\AGVNavigationCore.csproj` (프로젝트 파일 업데이트)
- 📝 수정: `AGVSimulator\Forms\SimulatorForm.cs` (Update 호출 추가)
---
## [2024.12.16] - AGV 방향 표시 수정 및 타겟계산 기능 추가 ## [2024.12.16] - AGV 방향 표시 수정 및 타겟계산 기능 추가
### 🐛 버그 수정 ### 🐛 버그 수정

View File

@@ -92,7 +92,6 @@
<Compile Include="Models\CustomLine.cs" /> <Compile Include="Models\CustomLine.cs" />
<Compile Include="Models\enumStruct.cs" /> <Compile Include="Models\enumStruct.cs" />
<Compile Include="Models\MagnetLine.cs" /> <Compile Include="Models\MagnetLine.cs" />
<Compile Include="Models\MapData.cs" />
<Compile Include="Models\MapElements.cs" /> <Compile Include="Models\MapElements.cs" />
<Compile Include="Models\MapText.cs" /> <Compile Include="Models\MapText.cs" />
<Compile Include="Models\PathResult.cs" /> <Compile Include="Models\PathResult.cs" />

View File

@@ -1,16 +1,11 @@
@echo off @echo off
echo Building AGV C# HMI Project... echo Building AGV C# HMI Project...
REM Check if Visual Studio 2022 is installed
if not exist "C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" (
echo Visual Studio 2022 Professional not found!
echo Please install Visual Studio 2022 Professional or update the MSBuild path.
pause
exit /b 1
)
REM Set MSBuild path REM Set MSBuild path
set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" REM set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\WDExpress\MSBuild\15.0\Bin\MSBuild.exe"
REM Rebuild Debug x86 configuration (VS-style Rebuild) REM Rebuild Debug x86 configuration (VS-style Rebuild)
%MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild %MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild