Files
ENIG/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
ChiKyun Kim de0e39e030 refactor: Consolidate RFID mapping and add bidirectional pathfinding
Major improvements to AGV navigation system:

• Consolidated RFID management into MapNode, removing duplicate RfidMapping class
• Enhanced MapNode with RFID metadata fields (RfidStatus, RfidDescription)
• Added automatic bidirectional connection generation in pathfinding algorithms
• Updated all components to use unified MapNode-based RFID system
• Added command line argument support for AGVMapEditor auto-loading files
• Fixed pathfinding failures by ensuring proper node connectivity

Technical changes:
- Removed RfidMapping class and dependencies across all projects
- Updated AStarPathfinder with EnsureBidirectionalConnections() method
- Modified MapLoader to use AssignAutoRfidIds() for RFID automation
- Enhanced UnifiedAGVCanvas, SimulatorForm, and MainForm for MapNode integration
- Improved data consistency and reduced memory footprint

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 16:41:52 +09:00

473 lines
14 KiB
C#

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