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 { /// /// 가상 AGV 클래스 /// 실제 AGV의 동작을 시뮬레이션 /// public class VirtualAGV : IAGV { #region Events /// /// AGV 상태 변경 이벤트 /// public event EventHandler StateChanged; /// /// 위치 변경 이벤트 /// public event EventHandler PositionChanged; /// /// RFID 감지 이벤트 /// public event EventHandler RfidDetected; /// /// 경로 완료 이벤트 /// public event EventHandler PathCompleted; /// /// 오류 발생 이벤트 /// public event EventHandler ErrorOccurred; #endregion #region Fields private string _agvId; private Point _currentPosition; private Point _targetPosition; private AgvDirection _currentDirection; private AGVState _currentState; private float _currentSpeed; // 경로 관련 private AGVPathResult _currentPath; private List _remainingNodes; private int _currentNodeIndex; private string _currentNodeId; private string _targetNodeId; // 이동 관련 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 /// /// AGV ID /// public string AgvId => _agvId; /// /// 현재 위치 /// public Point CurrentPosition => _currentPosition; /// /// 현재 방향 /// public AgvDirection CurrentDirection => _currentDirection; /// /// 현재 상태 /// public AGVState CurrentState => _currentState; /// /// 현재 속도 /// public float CurrentSpeed => _currentSpeed; /// /// 현재 경로 /// public AGVPathResult CurrentPath => _currentPath; /// /// 현재 노드 ID /// public string CurrentNodeId => _currentNodeId; /// /// 목표 위치 /// public Point? TargetPosition => _targetPosition; /// /// 배터리 레벨 (시뮬레이션) /// public float BatteryLevel { get; set; } = 100.0f; /// /// 목표 노드 ID /// public string TargetNodeId => _targetNodeId; /// /// 도킹 방향 /// public DockingDirection DockingDirection => _dockingDirection; #endregion #region Constructor /// /// 생성자 /// /// AGV ID /// 시작 위치 /// 시작 방향 public VirtualAGV(string agvId, Point startPosition, AgvDirection startDirection = AgvDirection.Forward) { _agvId = agvId; _currentPosition = startPosition; _currentDirection = startDirection; _currentState = AGVState.Idle; _currentSpeed = 0; _dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹 _currentNodeId = string.Empty; _targetNodeId = 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 /// /// 경로 실행 시작 /// /// 실행할 경로 /// 맵 노드 목록 public void StartPath(AGVPathResult path, List mapNodes) { if (path == null || !path.Success) { OnError("유효하지 않은 경로입니다."); return; } _currentPath = path; _remainingNodes = new List(path.Path); _currentNodeIndex = 0; // 시작 노드와 목표 노드 설정 if (_remainingNodes.Count > 0) { var startNode = mapNodes.FirstOrDefault(n => n.NodeId == _remainingNodes[0]); if (startNode != null) { _currentNodeId = startNode.NodeId; // 목표 노드 설정 (경로의 마지막 노드) if (_remainingNodes.Count > 1) { _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]}"); } } } /// /// 경로 정지 /// public void StopPath() { _moveTimer.Stop(); _currentPath = null; _remainingNodes?.Clear(); SetState(AGVState.Idle); _currentSpeed = 0; } /// /// 긴급 정지 /// public void EmergencyStop() { StopPath(); OnError("긴급 정지가 실행되었습니다."); } /// /// 수동 이동 (테스트용) /// /// 목표 위치 public void MoveTo(Point targetPosition) { _targetPosition = targetPosition; _moveStartPosition = _currentPosition; _moveTargetPosition = targetPosition; _moveProgress = 0; SetState(AGVState.Moving); _moveTimer.Start(); } /// /// 수동 회전 (테스트용) /// /// 회전 방향 public void Rotate(AgvDirection direction) { if (_currentState != AGVState.Idle) return; SetState(AGVState.Rotating); // 시뮬레이션: 즉시 방향 변경 (실제로는 시간이 걸림) _currentDirection = direction; System.Threading.Thread.Sleep(500); // 회전 시간 시뮬레이션 SetState(AGVState.Idle); } /// /// AGV 방향 직접 설정 (시뮬레이터용) /// /// 설정할 방향 public void SetDirection(AgvDirection direction) { _currentDirection = direction; } /// /// AGV 위치 직접 설정 (시뮬레이터용) /// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함 /// /// 새로운 위치 public void SetPosition(Point newPosition) { // 현재 위치를 이전 위치로 저장 (리프트 방향 계산용) if (_currentPosition != Point.Empty) { _targetPosition = _currentPosition; // 이전 위치 (previousPos 역할) } // 새로운 위치 설정 _currentPosition = newPosition; // 위치 변경 이벤트 발생 PositionChanged?.Invoke(this, _currentPosition); } /// /// 충전 시작 (시뮬레이션) /// public void StartCharging() { if (_currentState == AGVState.Idle) { SetState(AGVState.Charging); // 충전 시뮬레이션 시작 } } /// /// 충전 종료 /// public void StopCharging() { if (_currentState == AGVState.Charging) { SetState(AGVState.Idle); } } /// /// AGV 정보 조회 /// public string GetStatus() { return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " + $"방향:{_currentDirection} 상태:{_currentState} " + $"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%"; } /// /// 현재 RFID 시뮬레이션 (현재 위치 기준) /// public string SimulateRfidReading(List 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); } 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 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 /// /// 리소스 정리 /// public void Dispose() { _moveTimer?.Stop(); _moveTimer?.Dispose(); } #endregion } }