Files

162 lines
7.3 KiB
C#

using System;
using MultiWheelC.TrajectoryPlanning.CoarsePath;
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
/// <summary>纯 caller-clocked 的零速换向状态机;只依据调用方时间、测量速度和方向确认推进状态。</summary>
public sealed class GearSwitchStateMachine
{
private readonly double stopSpeedToleranceMetersPerSecond;
private readonly TimeSpan zeroSpeedHoldDuration;
private DateTimeOffset? zeroSpeedSince;
/// <summary>创建换向状态机。</summary>
/// <param name="stopSpeedToleranceMetersPerSecond">判定已停稳的绝对带符号速度上限,单位 m/s。</param>
/// <param name="zeroSpeedHoldSeconds">发送方向请求前必须连续保持停稳的时长,单位 s。</param>
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);
}
/// <summary>当前不可回退的换向状态;初始为 <see cref="GearSwitchState.Following"/>。</summary>
public GearSwitchState State { get; private set; } = GearSwitchState.Following;
/// <summary>基于调用方时刻、测量速度和边界语义推进一次换向状态。</summary>
/// <param name="now">调用方当前时刻,用于计算零速 dwell,不读取系统时钟。</param>
/// <param name="measuredSignedSpeed">测量带符号纵向速度,单位 m/s。</param>
/// <param name="desiredDirection">轨迹要求的实际行驶方向。</param>
/// <param name="currentDirection">调用方确认的当前实际方向。</param>
/// <param name="directionConfirmed">调用方是否已确认硬件方向切换完成。</param>
/// <param name="atGearSwitchBoundary">选中点是否为精确换向接近边界。</param>
/// <param name="atTerminal">选中点是否为 Goal 或 RollingSafetyStop 终端。</param>
/// <returns>包含停零、一次换向请求、是否允许轨迹运动和完成语义的不可变状态更新。</returns>
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;
}
}
/// <summary>一次换向状态机更新的不可变安全决策,供执行器映射为零速度/制动或轨迹跟随命令。</summary>
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;
}
/// <summary>更新后的换向状态。</summary>
public GearSwitchState State { get; }
public bool HoldZero { get; }
/// <summary>是否仅在本次更新请求一次方向变更。</summary>
public bool RequestDirectionChange { get; }
public bool AllowsTrajectoryMotion { get; }
/// <summary>是否因 Goal 或 RollingSafetyStop 完成轨迹并需要持续制动。</summary>
public bool IsTrajectoryComplete { get; }
public string Reason { get; }
}