Files
ParkingRobot/MultiWheelC/Fleet/FleetMemberAgent.cs
T

570 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using MyParking.Shared;
namespace MultiWheelC.Fleet
{
/// <summary>表示成员车在一次车队动作中的本地执行阶段。</summary>
public enum FleetMemberAgentState
{
Idle = 0,
Preparing = 1,
Ready = 2,
Active = 3,
Faulted = 4
}
/// <summary>区分固定β滚动运动和车辆中心纯自转的准备方式。</summary>
public enum FleetMemberPreparationMode
{
Rolling = 0,
Spin = 1
}
/// <summary>负责一辆成员车的舵轮准备、激活和本车速度命令执行。</summary>
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;
/// <summary>创建绑定到一辆多舵轮底盘的成员车执行器。</summary>
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;
/// <summary>停车并开始准备本车固定β滚动运动系。</summary>
public bool BeginRollingPreparation(
long planId,
double motionDirectionInBodyRadians)
{
ValidatePlanId(planId);
NumericGuard.EnsureFinite(
motionDirectionInBodyRadians,
nameof(motionDirectionInBodyRadians));
return BeginPreparation(
planId,
FleetMemberPreparationMode.Rolling,
AngleMath.NormalizeRadians(
motionDirectionInBodyRadians));
}
/// <summary>停车并开始准备车辆中心纯自转所需的舵轮方向。</summary>
public bool BeginSpinPreparation(long planId)
{
ValidatePlanId(planId);
return BeginPreparation(
planId,
FleetMemberPreparationMode.Spin,
motionDirectionInBodyRadians: 0.0);
}
/// <summary>检查舵轮是否已连续稳定到位;宿主应在准备阶段周期调用。</summary>
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;
}
/// <summary>在主车确认全队Ready后激活本车已经准备好的运动方式。</summary>
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;
}
/// <summary>校验任务和车号后执行分配给本车的车体系速度命令。</summary>
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。");
}
/// <summary>正常取消当前任务并立即停止驱动轮。</summary>
public void Stop()
{
_adapter.StopImmediately();
State = FleetMemberAgentState.Idle;
PreparationMode = null;
CurrentPlanId = 0;
MotionDirectionInBodyRadians = 0.0;
_alignedDurationSeconds = 0.0;
ClearCommandWatchdog();
LastFailureReason = string.Empty;
}
/// <summary>重置上一动作并下发本次滚动或自转舵轮准备目标。</summary>
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);
}
}
/// <summary>更新当前准备目标并读取舵轮到位状态。</summary>
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;
}
/// <summary>确认舵轮在全队释放前仍保持到位。</summary>
private bool ArePreparedWheelsStillAligned()
{
return PreparationMode ==
FleetMemberPreparationMode.Rolling
? _adapter.AreParallelWheelsAligned(
MotionDirectionInBodyRadians,
_alignmentToleranceRadians)
: _adapter.AreSpinWheelsAligned;
}
/// <summary>禁止滚动准备执行纯自转,也禁止自转准备执行平移。</summary>
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;
}
/// <summary>拒绝未满足执行条件的命令并保持车辆零速。</summary>
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;
}
/// <summary>锁存成员车故障并立即清零驱动轮速度。</summary>
private bool Fail(string reason)
{
_adapter.StopImmediately();
State = FleetMemberAgentState.Faulted;
LastFailureReason = reason ?? string.Empty;
return false;
}
/// <summary>优先返回底盘提供的具体失败原因。</summary>
private string BuildAdapterFailureReason(
string fallbackReason)
{
return string.IsNullOrWhiteSpace(
_adapter.LastFailureReason)
? fallbackReason
: _adapter.LastFailureReason;
}
/// <summary>拒绝零值和负值任务编号。</summary>
private static void ValidatePlanId(long planId)
{
if (planId <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(planId),
"车队动作任务编号必须大于零。");
}
}
}
}