using System; using MultiWheelC.TrajectoryPlanning.CoarsePath; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; /// 纯 caller-clocked 的零速换向状态机;只依据调用方时间、测量速度和方向确认推进状态。 public sealed class GearSwitchStateMachine { private readonly double stopSpeedToleranceMetersPerSecond; private readonly TimeSpan zeroSpeedHoldDuration; private DateTimeOffset? zeroSpeedSince; /// 创建换向状态机。 /// 判定已停稳的绝对带符号速度上限,单位 m/s。 /// 发送方向请求前必须连续保持停稳的时长,单位 s。 public GearSwitchStateMachine(double stopSpeedToleranceMetersPerSecond, double zeroSpeedHoldSeconds) { if (!IsNonNegativeFinite(stopSpeedToleranceMetersPerSecond)) throw new ArgumentOutOfRangeException(nameof(stopSpeedToleranceMetersPerSecond)); if (!IsNonNegativeFinite(zeroSpeedHoldSeconds)) throw new ArgumentOutOfRangeException(nameof(zeroSpeedHoldSeconds)); this.stopSpeedToleranceMetersPerSecond = stopSpeedToleranceMetersPerSecond; zeroSpeedHoldDuration = TimeSpan.FromSeconds(zeroSpeedHoldSeconds); } /// 当前不可回退的换向状态;初始为 public GearSwitchState State { get; private set; } = GearSwitchState.Following; /// 基于调用方时刻、测量速度和边界语义推进一次换向状态。 /// 调用方当前时刻,用于计算零速 dwell,不读取系统时钟。 /// 测量带符号纵向速度,单位 m/s。 /// 轨迹要求的实际行驶方向。 /// 调用方确认的当前实际方向。 /// 调用方是否已确认硬件方向切换完成。 /// 选中点是否为精确换向接近边界。 /// 选中点是否为 Goal 或 RollingSafetyStop 终端。 /// 包含停零、一次换向请求、是否允许轨迹运动和完成语义的不可变状态更新。 public GearSwitchStateUpdate Update(DateTimeOffset now, double measuredSignedSpeed, TravelDirection desiredDirection, TravelDirection currentDirection, bool directionConfirmed, bool atGearSwitchBoundary, bool atTerminal) { if (double.IsNaN(measuredSignedSpeed) || double.IsInfinity(measuredSignedSpeed)) throw new ArgumentOutOfRangeException(nameof(measuredSignedSpeed)); if (!Enum.IsDefined(typeof(TravelDirection), desiredDirection) || !Enum.IsDefined(typeof(TravelDirection), currentDirection)) { throw new ArgumentOutOfRangeException(nameof(desiredDirection)); } if (atTerminal) { State = GearSwitchState.Completed; zeroSpeedSince = null; return Held("terminal boundary reached", false, true); } switch (State) { case GearSwitchState.Following: if (desiredDirection != currentDirection) { State = GearSwitchState.ApproachingGearSwitch; return Moving("approaching gear-switch boundary"); } return Moving("following current direction segment"); case GearSwitchState.ApproachingGearSwitch: if (!atGearSwitchBoundary) return Moving("approaching gear-switch boundary"); State = GearSwitchState.HoldingZero; zeroSpeedSince = IsStopped(measuredSignedSpeed) ? now : null; return Held("holding at gear-switch boundary", false, false); case GearSwitchState.HoldingZero: if (!IsStopped(measuredSignedSpeed)) { zeroSpeedSince = null; return Held("measured speed remains above stop tolerance", false, false); } if (!zeroSpeedSince.HasValue) { zeroSpeedSince = now; return Held("zero-speed dwell started", false, false); } if (now - zeroSpeedSince.Value < zeroSpeedHoldDuration) return Held("zero-speed dwell in progress", false, false); State = GearSwitchState.RequestingDirectionChange; return Held("requesting direction change", true, false); case GearSwitchState.RequestingDirectionChange: State = GearSwitchState.AwaitingDirectionConfirmation; return Held("awaiting direction confirmation", false, false); case GearSwitchState.AwaitingDirectionConfirmation: if (directionConfirmed && desiredDirection == currentDirection) { State = GearSwitchState.Following; zeroSpeedSince = null; return Moving("direction change confirmed"); } return Held("awaiting direction confirmation", false, false); case GearSwitchState.Completed: return Held("trajectory already completed", false, true); default: throw new InvalidOperationException("Unsupported gear-switch state."); } } private GearSwitchStateUpdate Moving(string reason) { return new GearSwitchStateUpdate(State, false, false, true, false, reason); } private GearSwitchStateUpdate Held(string reason, bool requestDirectionChange, bool isTrajectoryComplete) { return new GearSwitchStateUpdate(State, true, requestDirectionChange, false, isTrajectoryComplete, reason); } private bool IsStopped(double measuredSignedSpeed) { return Math.Abs(measuredSignedSpeed) < stopSpeedToleranceMetersPerSecond; } private static bool IsNonNegativeFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0d; } } /// 一次换向状态机更新的不可变安全决策,供执行器映射为零速度/制动或轨迹跟随命令。 public sealed class GearSwitchStateUpdate { internal GearSwitchStateUpdate(GearSwitchState state, bool holdZero, bool requestDirectionChange, bool allowsTrajectoryMotion, bool isTrajectoryComplete, string reason) { State = state; HoldZero = holdZero; RequestDirectionChange = requestDirectionChange; AllowsTrajectoryMotion = allowsTrajectoryMotion; IsTrajectoryComplete = isTrajectoryComplete; Reason = reason ?? string.Empty; } /// 更新后的换向状态。 public GearSwitchState State { get; } public bool HoldZero { get; } /// 是否仅在本次更新请求一次方向变更。 public bool RequestDirectionChange { get; } public bool AllowsTrajectoryMotion { get; } /// 是否因 Goal 或 RollingSafetyStop 完成轨迹并需要持续制动。 public bool IsTrajectoryComplete { get; } public string Reason { get; } }