using System; using MyParking.Shared; namespace MultiWheelC.Fleet { /// 表示成员车在一次车队动作中的本地执行阶段。 public enum FleetMemberAgentState { Idle = 0, Preparing = 1, Ready = 2, Active = 3, Faulted = 4 } /// 区分固定β滚动运动和车辆中心纯自转的准备方式。 public enum FleetMemberPreparationMode { Rolling = 0, Spin = 1 } /// 负责一辆成员车的舵轮准备、激活和本车速度命令执行。 public sealed class FleetMemberAgent { private const double MotionDeadband = 1e-6; private readonly MultiWheelChassisAdapter _adapter; private readonly double _alignmentToleranceRadians; private readonly double _alignmentStableSeconds; private double _alignedDurationSeconds; private double? _lastAcceptedCommandTimeSeconds; private double? _commandDeadlineSeconds; /// 创建绑定到一辆多舵轮底盘的成员车执行器。 public FleetMemberAgent( MultiWheelChassisAdapter adapter, double alignmentToleranceRadians, double alignmentStableSeconds) { _adapter = adapter ?? throw new ArgumentNullException(nameof(adapter)); NumericGuard.EnsureFinitePositive( alignmentToleranceRadians, nameof(alignmentToleranceRadians)); NumericGuard.EnsureFiniteNonNegative( alignmentStableSeconds, nameof(alignmentStableSeconds)); if (alignmentToleranceRadians > Math.PI) { throw new ArgumentOutOfRangeException( nameof(alignmentToleranceRadians), "舵轮到位容差不能大于π。"); } _alignmentToleranceRadians = alignmentToleranceRadians; _alignmentStableSeconds = alignmentStableSeconds; State = FleetMemberAgentState.Idle; LastFailureReason = string.Empty; } public int VehicleId => _adapter.VehicleId; public FleetMemberAgentState State { get; private set; } public FleetMemberPreparationMode? PreparationMode { get; private set; } public long CurrentPlanId { get; private set; } public double MotionDirectionInBodyRadians { get; private set; } public string LastFailureReason { get; private set; } // 使用从车本机单调时钟记录,不依赖主车或Detour时间戳。 public double? LastAcceptedCommandTimeSeconds => _lastAcceptedCommandTimeSeconds; public double? CommandDeadlineSeconds => _commandDeadlineSeconds; /// 停车并开始准备本车固定β滚动运动系。 public bool BeginRollingPreparation( long planId, double motionDirectionInBodyRadians) { ValidatePlanId(planId); NumericGuard.EnsureFinite( motionDirectionInBodyRadians, nameof(motionDirectionInBodyRadians)); return BeginPreparation( planId, FleetMemberPreparationMode.Rolling, AngleMath.NormalizeRadians( motionDirectionInBodyRadians)); } /// 停车并开始准备车辆中心纯自转所需的舵轮方向。 public bool BeginSpinPreparation(long planId) { ValidatePlanId(planId); return BeginPreparation( planId, FleetMemberPreparationMode.Spin, motionDirectionInBodyRadians: 0.0); } /// 检查舵轮是否已连续稳定到位;宿主应在准备阶段周期调用。 public FleetMemberAgentState UpdatePreparation( double deltaTimeSeconds) { NumericGuard.EnsureFinitePositive( deltaTimeSeconds, nameof(deltaTimeSeconds)); if (State != FleetMemberAgentState.Preparing) { return State; } bool aligned; try { aligned = UpdateAndCheckAlignment(); } catch (InvalidOperationException exception) { Fail(exception.Message); return State; } catch (ArgumentException exception) { Fail(exception.Message); return State; } if (State == FleetMemberAgentState.Faulted) { return State; } _alignedDurationSeconds = aligned ? _alignedDurationSeconds + deltaTimeSeconds : 0.0; if (aligned && _alignedDurationSeconds >= _alignmentStableSeconds) { State = FleetMemberAgentState.Ready; LastFailureReason = string.Empty; } return State; } /// 在主车确认全队Ready后激活本车已经准备好的运动方式。 public bool Activate( long planId, double commandReceivedTimeSeconds, double validForSeconds) { NumericGuard.EnsureFiniteNonNegative( commandReceivedTimeSeconds, nameof(commandReceivedTimeSeconds)); NumericGuard.EnsureFinitePositive( validForSeconds, nameof(validForSeconds)); if (planId != CurrentPlanId) { LastFailureReason = "激活任务编号与当前准备任务不一致。"; return false; } if (State == FleetMemberAgentState.Active) { // 重复激活只允许幂等确认,不能替代周期运动命令延长车辆运动时间。 return UpdateCommandWatchdog( commandReceivedTimeSeconds); } if (State != FleetMemberAgentState.Ready || !PreparationMode.HasValue) { return RejectWhileStopped( "成员车尚未完成舵轮准备。"); } bool stillAligned; try { stillAligned = ArePreparedWheelsStillAligned(); } catch (InvalidOperationException exception) { return Fail(exception.Message); } catch (ArgumentException exception) { return Fail(exception.Message); } if (!stillAligned) { State = FleetMemberAgentState.Preparing; _alignedDurationSeconds = 0.0; return RejectWhileStopped( "成员车在激活前失去舵轮到位状态。"); } try { if (PreparationMode.Value == FleetMemberPreparationMode.Rolling) { _adapter.ActivateMotionFrame( MotionDirectionInBodyRadians); } else if (!_adapter.AdoptPreparedSpinForXYTh( _alignmentToleranceRadians)) { return Fail( BuildAdapterFailureReason( "无法激活已经准备好的原地自转舵轮。")); } } catch (InvalidOperationException exception) { return Fail(exception.Message); } catch (ArgumentException exception) { return Fail(exception.Message); } State = FleetMemberAgentState.Active; AcceptCommandDeadline( commandReceivedTimeSeconds, validForSeconds); LastFailureReason = string.Empty; return true; } /// 校验任务和车号后执行分配给本车的车体系速度命令。 public bool Execute( long planId, FleetMemberCommand command, double commandReceivedTimeSeconds, double validForSeconds, TimeSpan? interval = null) { ValidatePlanId(planId); NumericGuard.EnsureFinite( command.TwistInVehicleBody, nameof(command)); NumericGuard.EnsureFiniteNonNegative( commandReceivedTimeSeconds, nameof(commandReceivedTimeSeconds)); NumericGuard.EnsureFinitePositive( validForSeconds, nameof(validForSeconds)); if (planId != CurrentPlanId) { return Fail( "速度命令任务编号与当前激活任务不一致。"); } if (command.VehicleId != VehicleId) { return Fail( $"速度命令属于车辆{command.VehicleId}," + $"当前成员车号为{VehicleId}。"); } if (State != FleetMemberAgentState.Active || !PreparationMode.HasValue) { return RejectWhileStopped( "成员车尚未激活,不能执行速度命令。"); } // 先检查上一条命令是否已经过期,禁止失联后由迟到命令自动恢复运动。 if (!UpdateCommandWatchdog( commandReceivedTimeSeconds)) { return false; } if (!IsCommandCompatibleWithPreparation( command.TwistInVehicleBody)) { return Fail( "速度命令与本次舵轮准备方式不一致。"); } try { if (!_adapter.SendBodyTwist( command.TwistInVehicleBody, interval)) { return Fail( BuildAdapterFailureReason( "成员车底盘拒绝执行速度命令。")); } } catch (InvalidOperationException exception) { return Fail(exception.Message); } catch (ArgumentException exception) { return Fail(exception.Message); } AcceptCommandDeadline( commandReceivedTimeSeconds, validForSeconds); LastFailureReason = string.Empty; return true; } // 运行循环即使没有收到新命令也必须调用本方法,超时后会本地停车并锁存Faulted。 public bool UpdateCommandWatchdog( double currentTimeSeconds) { NumericGuard.EnsureFiniteNonNegative( currentTimeSeconds, nameof(currentTimeSeconds)); if (State == FleetMemberAgentState.Faulted) { return false; } if (State != FleetMemberAgentState.Active) { return true; } if (!_lastAcceptedCommandTimeSeconds.HasValue || !_commandDeadlineSeconds.HasValue) { return Fail( "成员车已经激活,但本地命令看门狗尚未初始化。"); } if (currentTimeSeconds < _lastAcceptedCommandTimeSeconds.Value) { return Fail( "成员车本地单调时钟发生倒退,无法继续校验命令时效。"); } if (currentTimeSeconds <= _commandDeadlineSeconds.Value) { return true; } var commandAgeSeconds = currentTimeSeconds - _lastAcceptedCommandTimeSeconds.Value; return Fail( "成员车等待主车有效命令超时," + $"最近一次命令距今{commandAgeSeconds:F3}s。"); } /// 正常取消当前任务并立即停止驱动轮。 public void Stop() { _adapter.StopImmediately(); State = FleetMemberAgentState.Idle; PreparationMode = null; CurrentPlanId = 0; MotionDirectionInBodyRadians = 0.0; _alignedDurationSeconds = 0.0; ClearCommandWatchdog(); LastFailureReason = string.Empty; } /// 重置上一动作并下发本次滚动或自转舵轮准备目标。 private bool BeginPreparation( long planId, FleetMemberPreparationMode mode, double motionDirectionInBodyRadians) { try { _adapter.StopImmediately(); _adapter.ResetToBodyFrame(); CurrentPlanId = planId; PreparationMode = mode; MotionDirectionInBodyRadians = motionDirectionInBodyRadians; State = FleetMemberAgentState.Preparing; LastFailureReason = string.Empty; _alignedDurationSeconds = 0.0; ClearCommandWatchdog(); var accepted = mode == FleetMemberPreparationMode.Rolling ? _adapter.PrepareParallelDirection( motionDirectionInBodyRadians) : _adapter.PrepareSpin( alignmentToleranceDegrees: AngleMath.RadiansToDegrees( _alignmentToleranceRadians)); if (!accepted) { return Fail( BuildAdapterFailureReason( "成员车底盘拒绝舵轮准备目标。")); } return true; } catch (InvalidOperationException exception) { return Fail(exception.Message); } catch (ArgumentException exception) { return Fail(exception.Message); } } /// 更新当前准备目标并读取舵轮到位状态。 private bool UpdateAndCheckAlignment() { if (PreparationMode == FleetMemberPreparationMode.Rolling) { return _adapter.AreParallelWheelsAligned( MotionDirectionInBodyRadians, _alignmentToleranceRadians); } if (!_adapter.PrepareSpin( alignmentToleranceDegrees: AngleMath.RadiansToDegrees( _alignmentToleranceRadians))) { Fail( BuildAdapterFailureReason( "成员车底盘无法继续更新原地自转准备。")); return false; } return _adapter.AreSpinWheelsAligned; } /// 确认舵轮在全队释放前仍保持到位。 private bool ArePreparedWheelsStillAligned() { return PreparationMode == FleetMemberPreparationMode.Rolling ? _adapter.AreParallelWheelsAligned( MotionDirectionInBodyRadians, _alignmentToleranceRadians) : _adapter.AreSpinWheelsAligned; } /// 禁止滚动准备执行纯自转,也禁止自转准备执行平移。 private bool IsCommandCompatibleWithPreparation( Twist2D bodyTwist) { var linearSpeed = Math.Sqrt( bodyTwist.VxMetersPerSecond * bodyTwist.VxMetersPerSecond + bodyTwist.VyMetersPerSecond * bodyTwist.VyMetersPerSecond); var hasLinearMotion = linearSpeed > MotionDeadband; var hasAngularMotion = Math.Abs( bodyTwist.OmegaRadiansPerSecond) > MotionDeadband; if (!hasLinearMotion && !hasAngularMotion) { return true; } return PreparationMode == FleetMemberPreparationMode.Rolling ? hasLinearMotion : !hasLinearMotion && hasAngularMotion; } /// 拒绝未满足执行条件的命令并保持车辆零速。 private bool RejectWhileStopped(string reason) { _adapter.StopImmediately(); LastFailureReason = reason ?? string.Empty; return false; } private void AcceptCommandDeadline( double commandReceivedTimeSeconds, double validForSeconds) { var commandDeadlineSeconds = commandReceivedTimeSeconds + validForSeconds; NumericGuard.EnsureFinite( commandDeadlineSeconds, nameof(validForSeconds)); _lastAcceptedCommandTimeSeconds = commandReceivedTimeSeconds; _commandDeadlineSeconds = commandDeadlineSeconds; } private void ClearCommandWatchdog() { _lastAcceptedCommandTimeSeconds = null; _commandDeadlineSeconds = null; } /// 锁存成员车故障并立即清零驱动轮速度。 private bool Fail(string reason) { _adapter.StopImmediately(); State = FleetMemberAgentState.Faulted; LastFailureReason = reason ?? string.Empty; return false; } /// 优先返回底盘提供的具体失败原因。 private string BuildAdapterFailureReason( string fallbackReason) { return string.IsNullOrWhiteSpace( _adapter.LastFailureReason) ? fallbackReason : _adapter.LastFailureReason; } /// 拒绝零值和负值任务编号。 private static void ValidatePlanId(long planId) { if (planId <= 0) { throw new ArgumentOutOfRangeException( nameof(planId), "车队动作任务编号必须大于零。"); } } } }