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 { /// /// 가상 AGV 클래스 (코어 비즈니스 로직) /// 실제 AGV와 시뮬레이터 모두에서 사용 가능한 공용 로직 /// 시뮬레이션과 실제 동작이 동일하게 동작하도록 설계됨 /// public class VirtualAGV : IMovableAGV, IAGV { #region Events /// /// AGV 상태 변경 이벤트 /// public event EventHandler StateChanged; /// /// 위치 변경 이벤트 /// public event EventHandler<(Point, AgvDirection, MapNode)> 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 _prevPosition; private AgvDirection _currentDirection; private AgvDirection _prevDirection; private AGVState _currentState; private float _currentSpeed; private MapNode _targetnode = null; // 경로 관련 private AGVPathResult _currentPath; private List _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 _detectedRfids = new List(); // 감지된 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 AgvDirection PrevDirection => _prevDirection; /// /// AGV ID /// public string AgvId => _agvId; /// /// 현재 위치 /// public Point CurrentPosition { get => _currentPosition; set => _currentPosition = value; } /// /// 현재 방향 /// 모터의 동작 방향 /// public AgvDirection CurrentDirection { get => _currentDirection; set => _currentDirection = value; } /// /// 현재 상태 /// public AGVState CurrentState { get => _currentState; set => _currentState = value; } /// /// 현재 속도 /// public float CurrentSpeed => _currentSpeed; /// /// 현재 경로 /// public AGVPathResult CurrentPath => _currentPath; public void ClearPath() { _currentPath = null; } /// /// 현재 노드 ID /// public MapNode CurrentNode => _currentNode; /// /// 현재 노드 ID (CurrentNode.Id) /// public string CurrentNodeId => _currentNode?.Id; /// /// 현재노드의 RFID(id)값을 표시합니다 없는경우 (X)가 표시됩니다 /// public string CurrentNodeID2 { get { if (_currentNode == null) return "(X)"; return _currentNode.ID2; } } /// /// 이전 위치 /// public Point? PrevPosition => _prevPosition; /// /// 배터리 레벨 (시뮬레이션) /// public float BatteryLevel { get; set; } = 100.0f; /// /// 이전 노드 /// public MapNode PrevNode => _prevNode; /// /// Turn 상태값 /// public AGVTurn Turn { get; set; } /// /// 도킹 방향 /// public DockingDirection DockingDirection => _dockingDirection; /// /// 위치 확정 여부 (RFID 2개 이상 감지 시 true) /// public bool IsPositionConfirmed => _isPositionConfirmed; /// /// 감지된 RFID 개수 /// public int DetectedRfidCount => _detectedRfids.Count; /// /// 배터리 부족 경고 임계값 (%) /// public float LowBatteryThreshold { get; set; } = 20.0f; #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; // 기본값: 전진 도킹 _currentNode = null; _prevNode = null; _isMoving = false; _lastUpdateTime = DateTime.Now; } #endregion #region Public Methods - 센서/RFID 입력 (실제 AGV에서 호출) /// /// 현재 위치 설정 (실제 AGV 센서에서) /// public void SetCurrentPosition(Point position) { _currentPosition = position; } /// /// 감지된 RFID 설정 (실제 RFID 센서에서) /// 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); } /// /// 모터 방향 설정 (실제 모터 컨트롤러에서) /// public void SetMotorDirection(AgvDirection direction) { _currentDirection = direction; } /// /// 배터리 레벨 설정 (실제 BMS에서) /// public void SetBatteryLevel(float percentage) { BatteryLevel = Math.Max(0, Math.Min(100, percentage)); // 배터리 부족 경고 if (BatteryLevel < LowBatteryThreshold && _currentState != AGVState.Charging) { OnError($"배터리 부족: {BatteryLevel:F1}% (기준: {LowBatteryThreshold}%)"); } } /// /// 현재 노드id의 개체를 IsPass 로 설정합니다 /// 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; } /// /// 다음 동작 예측 (실제 AGV 제어용) /// AGV가 지속적으로 호출하여 현재 상태와 예측 상태를 일치시킴 /// /// 다음에 수행할 모터/마그넷/속도 명령 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 { //움직이지 않는다면 움직이게하고, 움직인다면 마크스탑하낟.i //도킹노드라면 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 checkPathOutNode = _currentPath.DetailedPath.Where(t => t.IsPass == false && t.NodeId.Equals(_currentNode.Id)).FirstOrDefault(); if (checkPathOutNode == null) { return new AGVCommand( MotorCommand.Stop, MagnetPosition.S, SpeedLevel.L, eAGVCommandReason.PathOut, $"(재탐색요청)경로이탈 현재위치:{CurrentNodeID2}" ); } return GetCommandFromPath(CurrentNode, "경로 실행 시작"); } #endregion #region Public Methods - 상태 조회 /// /// 현재 위치 조회 /// public Point GetCurrentPosition() => _currentPosition; /// /// 현재 상태 조회 /// public AGVState GetCurrentState() => _currentState; /// /// 현재 노드 ID 조회 /// public MapNode GetCurrentNode() => _currentNode; /// /// AGV 정보 조회 /// public string GetStatus() { return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " + $"방향:{_currentDirection} 상태:{_currentState} " + $"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%"; } #endregion #region Public Methods - 경로 실행 /// /// 경로가 설정되어있는지? /// /// public bool HasPath() { if (_currentPath == null) return false; if (_currentPath.DetailedPath == null) return false; return _currentPath.DetailedPath.Any(); } /// /// 경로 설정 (실제 AGV 및 시뮬레이터에서 사용) /// /// 실행할 경로 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})가 다릅니다."); } } /// /// 경로 정지 /// public void StopPath() { _isMoving = false; _currentPath = null; _remainingNodes?.Clear(); SetState(AGVState.Idle); _currentSpeed = 0; } /// /// 긴급 정지 /// public void EmergencyStop() { StopPath(); OnError("긴급 정지가 실행되었습니다."); } /// /// 일시 정지 (경로 유지) /// public void Pause() { _isMoving = false; _currentSpeed = 0; } /// /// 이동 재개 /// public void Resume() { if (_currentPath != null && _remainingNodes != null && _remainingNodes.Count > 0) { _isMoving = true; SetState(AGVState.Moving); } } #endregion #region Public Methods - 프레임 업데이트 (외부에서 정기적으로 호출) /// /// 프레임 업데이트 (외부에서 주기적으로 호출) /// 이 방식으로 타이머에 의존하지 않고 외부에서 제어 가능 /// /// 마지막 업데이트 이후 경과 시간 (밀리초) public void Update(float deltaTimeMs) { var deltaTime = deltaTimeMs / 1000.0f; // 초 단위로 변환 UpdateMovement(deltaTime); UpdateBattery(deltaTime); // 위치 변경 이벤트 발생 PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode)); } #endregion #region Public Methods - 수동 제어 (테스트용) /// /// 수동 이동 (테스트용) /// /// 목표 위치 public void MoveTo(Point targetPosition) { _prevPosition = targetPosition; _moveStartPosition = _currentPosition; _moveTargetPosition = targetPosition; SetState(AGVState.Moving); _isMoving = true; Turn = AGVTurn.None; } /// /// 수동 회전 (테스트용) /// /// 회전 방향 public void Rotate(AgvDirection direction) { if (_currentState != AGVState.Idle) return; SetState(AGVState.Rotating); _currentDirection = direction; SetState(AGVState.Idle); } /// /// 충전 시작 /// public void StartCharging() { if (_currentState == AGVState.Idle) { SetState(AGVState.Charging); } } /// /// 충전 종료 /// public void StopCharging() { if (_currentState == AGVState.Charging) { SetState(AGVState.Idle); } } #endregion #region Public Methods - AGV 위치 설정 (시뮬레이터용) /// /// AGV 위치 직접 설정 /// PrevPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함 /// /// 현재 노드 /// 새로운 위치 /// 모터이동방향 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 /// /// 노드 ID를 RFID 값으로 변환 (NodeResolver 사용) /// public ushort GetRfidByNodeId(List _mapNodes, string nodeId) { var node = _mapNodes?.FirstOrDefault(n => n.Id == nodeId); if ((node?.HasRfid() ?? false) == false) return 0; return node.RfidId; } #region Private Methods /// /// DetailedPath에서 노드 정보를 찾아 AGVCommand 생성 /// 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; eAGVCommandReason reason = eAGVCommandReason.Normal; if (nodeInfo.IsTurn) { motorCmd = MotorCommand.Stop; reason = eAGVCommandReason.MarkStop; } else { 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 MagnetDirection.Left: magnetPos = MagnetPosition.L; break; case MagnetDirection.Right: magnetPos = MagnetPosition.R; break; case 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, reason, $"{actionDescription} → {targetNode.Id} (Motor:{motorCmd}, Magnet:{magnetPos})" ) { IsTurn = nodeInfo.IsTurn }; } 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; /// /// 목적지를 설정합니다. 목적지가 변경되면 경로계산정보가 삭제 됩니다. /// 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 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 /// /// 리소스 정리 /// public void Dispose() { StopPath(); } #endregion } }