Files
ParkingRobot/MultiWheelC/Fleet/FleetRuntime.cs
T

1011 lines
34 KiB
C#

using System;
using System.Collections.Generic;
using MultiWheelC.StateEstimation;
using MultiWheelC.Trajectory;
using MyParking.Shared;
namespace MultiWheelC.Fleet
{
/// <summary>表示本机车队任务运行入口的生命周期阶段。</summary>
public enum FleetRuntimeState
{
Idle = 0,
Preparing = 1,
Ready = 2,
Active = 3,
Completed = 4,
Faulted = 5
}
/// <summary>在主车或从车上串联固定布局滚动车队任务。</summary>
public sealed class FleetRuntime
{
private const int RuntimeFailureCode = 1;
private const int FaultStopReasonCode = 1;
private readonly IFleetTransport _transport;
private readonly FleetMemberAgent _localAgent;
private readonly IVehicleStateProvider _localStateProvider;
private readonly FleetPreparationCoordinator _preparationCoordinator;
private readonly FleetCoordinator _fleetCoordinator;
private readonly FleetSafetySupervisor _safetySupervisor;
private readonly double _commandValidForSeconds;
private readonly double _preparationTimeoutSeconds;
private readonly Dictionary<int, AcceptedReport> _reports =
new Dictionary<int, AcceptedReport>();
private readonly Dictionary<int, long> _lastReportSequences =
new Dictionary<int, long>();
private FleetLayout _activeLayout;
private VehicleState? _lastLocalState;
private double? _planStartTimeSeconds;
private long _nextCommandSequence;
private long _nextReportSequence;
private long _lastReceivedCommandSequence;
private long _lastAppliedCommandSequence;
/// <summary>创建本车运行入口;主车必须额外提供三个主车侧组件。</summary>
public FleetRuntime(
int selfVehicleId,
int leaderVehicleId,
IFleetTransport transport,
FleetMemberAgent localAgent,
IVehicleStateProvider localStateProvider,
FleetPreparationCoordinator preparationCoordinator = null,
FleetCoordinator fleetCoordinator = null,
FleetSafetySupervisor safetySupervisor = null,
double commandValidForSeconds = 0.5,
double preparationTimeoutSeconds = 10.0)
{
if (selfVehicleId <= 0)
{
throw new ArgumentOutOfRangeException(nameof(selfVehicleId));
}
if (leaderVehicleId <= 0)
{
throw new ArgumentOutOfRangeException(nameof(leaderVehicleId));
}
_transport = transport ??
throw new ArgumentNullException(nameof(transport));
_localAgent = localAgent ??
throw new ArgumentNullException(nameof(localAgent));
_localStateProvider = localStateProvider ??
throw new ArgumentNullException(nameof(localStateProvider));
NumericGuard.EnsureFinitePositive(
commandValidForSeconds,
nameof(commandValidForSeconds));
NumericGuard.EnsureFinitePositive(
preparationTimeoutSeconds,
nameof(preparationTimeoutSeconds));
if (_localAgent.VehicleId != selfVehicleId)
{
throw new ArgumentException(
"本车执行器车号与运行入口车号不一致。",
nameof(localAgent));
}
SelfVehicleId = selfVehicleId;
LeaderVehicleId = leaderVehicleId;
_preparationCoordinator = preparationCoordinator;
_fleetCoordinator = fleetCoordinator;
_safetySupervisor = safetySupervisor;
_commandValidForSeconds = commandValidForSeconds;
_preparationTimeoutSeconds = preparationTimeoutSeconds;
if (IsLeader &&
(_preparationCoordinator == null ||
_fleetCoordinator == null ||
_safetySupervisor == null))
{
throw new ArgumentException(
"主车必须提供准备协调器、车队协调器和安全监督器。");
}
State = FleetRuntimeState.Idle;
LastFailureReason = string.Empty;
}
public int SelfVehicleId { get; }
public int LeaderVehicleId { get; }
public bool IsLeader => SelfVehicleId == LeaderVehicleId;
public FleetRuntimeState State { get; private set; }
public long CurrentPlanId { get; private set; }
public long LastAppliedCommandSequence =>
_lastAppliedCommandSequence;
public string LastFailureReason { get; private set; }
public FleetCoordinationCycleOutput LastCoordinationOutput
{
get;
private set;
}
/// <summary>由主车启动一次使用当前控制器固定β的滚动轨迹任务。</summary>
public bool StartRollingPlan(
long planId,
FleetLayout layout,
Trajectory2D trajectory)
{
EnsureLeader();
if (planId <= 0)
{
throw new ArgumentOutOfRangeException(nameof(planId));
}
if (layout == null)
{
throw new ArgumentNullException(nameof(layout));
}
if (trajectory == null)
{
throw new ArgumentNullException(nameof(trajectory));
}
if (!layout.TryGetVehicle(SelfVehicleId, out _))
{
throw new ArgumentException(
"主车不在当前车队布局中。",
nameof(layout));
}
if (State == FleetRuntimeState.Preparing ||
State == FleetRuntimeState.Active)
{
LastFailureReason = "当前车队任务尚未结束。";
return false;
}
ResetLeaderForNewPlan();
CurrentPlanId = planId;
_activeLayout = layout;
State = FleetRuntimeState.Preparing;
try
{
_fleetCoordinator.Start(layout, trajectory);
_preparationCoordinator.StartRollingPreparation(
planId,
layout,
_fleetCoordinator.MotionDirectionInFleetRadians);
_safetySupervisor.Start(planId);
foreach (var target in _preparationCoordinator.Targets)
{
var command = NewCommand(
target.VehicleId,
FleetCommandKind.PrepareRolling,
target.MotionDirectionInBodyRadians,
Twist2D.Zero);
if (target.VehicleId == SelfVehicleId)
{
if (!_localAgent.BeginRollingPreparation(
planId,
target.MotionDirectionInBodyRadians))
{
return FaultLeader(
LocalFailure("主车舵轮准备失败。"));
}
AcceptLocalCommand(command.SequenceNumber);
}
else
{
_transport.SendCommand(command);
}
}
return true;
}
catch (Exception exception)
{
return FaultLeader(
"启动车队任务失败:" + exception.Message);
}
}
/// <summary>执行一个本机调度周期,并返回更新后的运行阶段。</summary>
public FleetRuntimeState Update(
double currentTimeSeconds,
double deltaTimeSeconds)
{
NumericGuard.EnsureFiniteNonNegative(
currentTimeSeconds,
nameof(currentTimeSeconds));
NumericGuard.EnsureFinitePositive(
deltaTimeSeconds,
nameof(deltaTimeSeconds));
return IsLeader
? UpdateLeader(currentTimeSeconds, deltaTimeSeconds)
: UpdateMember(currentTimeSeconds, deltaTimeSeconds);
}
/// <summary>正常取消当前任务;主车同时向其余成员广播停止。</summary>
public void Stop(string reason = "")
{
if (IsLeader && CurrentPlanId > 0)
{
BroadcastStop(reasonCode: 0);
_preparationCoordinator.Cancel();
_fleetCoordinator.Cancel();
_safetySupervisor.Reset();
}
_localAgent.Stop();
_activeLayout = null;
CurrentPlanId = FleetProtocol.NoActivePlanId;
State = FleetRuntimeState.Idle;
_planStartTimeSeconds = null;
LastCoordinationOutput = null;
LastFailureReason = reason ?? string.Empty;
}
private FleetRuntimeState UpdateLeader(
double currentTimeSeconds,
double deltaTimeSeconds)
{
if (State != FleetRuntimeState.Preparing &&
State != FleetRuntimeState.Active)
{
return State;
}
_planStartTimeSeconds ??= currentTimeSeconds;
if (!ReceiveReports(currentTimeSeconds))
{
return State;
}
if (!TickLocalAgent(currentTimeSeconds, deltaTimeSeconds))
{
FaultLeader(LocalFailure("主车本地执行失败。"));
return State;
}
if (!TryReadLocalState(out var localState))
{
FaultLeader("主车本地状态不可用。" );
return State;
}
StoreLocalReport(localState.Value, currentTimeSeconds);
if (State == FleetRuntimeState.Preparing)
{
UpdatePreparationBarrier();
if (_preparationCoordinator.State ==
FleetPreparationCoordinatorState.Faulted)
{
FaultLeader(_preparationCoordinator.LastFailureReason);
return State;
}
if (currentTimeSeconds - _planStartTimeSeconds.Value >
_preparationTimeoutSeconds)
{
FaultLeader("车队舵轮准备超时,未能全员Ready。" );
return State;
}
}
if (!CheckLeaderSafety(currentTimeSeconds))
{
return State;
}
if (State == FleetRuntimeState.Preparing &&
!ActivateFleet(currentTimeSeconds))
{
return State;
}
if (State == FleetRuntimeState.Active)
{
RunCoordination(currentTimeSeconds, deltaTimeSeconds);
}
return State;
}
private FleetRuntimeState UpdateMember(
double currentTimeSeconds,
double deltaTimeSeconds)
{
ReceiveCommands(currentTimeSeconds);
if (!TickLocalAgent(currentTimeSeconds, deltaTimeSeconds))
{
if (State != FleetRuntimeState.Faulted)
{
FaultMember(LocalFailure("成员车本地执行失败。"));
}
}
var hasState = TryReadLocalState(out var localState);
if (!hasState && CurrentPlanId > 0 &&
State != FleetRuntimeState.Faulted)
{
FaultMember("成员车本地状态不可用。" );
}
SendLocalReport(localState, hasState, currentTimeSeconds);
return State;
}
private bool TickLocalAgent(
double currentTimeSeconds,
double deltaTimeSeconds)
{
if (State == FleetRuntimeState.Faulted)
{
return false;
}
if (_localAgent.State == FleetMemberAgentState.Preparing)
{
_localAgent.UpdatePreparation(deltaTimeSeconds);
}
if (_localAgent.State == FleetMemberAgentState.Active &&
!_localAgent.UpdateCommandWatchdog(currentTimeSeconds))
{
return false;
}
if (_localAgent.State == FleetMemberAgentState.Faulted)
{
return false;
}
if (!IsLeader)
{
State = ToRuntimeState(_localAgent.State);
}
return true;
}
private bool TryReadLocalState(out VehicleState? state)
{
try
{
if (_localStateProvider.TryGetState(out var value))
{
_lastLocalState = value;
state = value;
return true;
}
}
catch (Exception exception)
{
LastFailureReason =
"读取本车状态失败:" + exception.Message;
}
state = null;
return false;
}
private bool ReceiveReports(double currentTimeSeconds)
{
try
{
while (_transport.TryReceiveReport(out var report))
{
if (report.VehicleId == SelfVehicleId ||
_activeLayout == null ||
!_activeLayout.TryGetVehicle(report.VehicleId, out _) ||
report.PlanId != CurrentPlanId)
{
continue;
}
ValidateReport(report);
if (_lastReportSequences.TryGetValue(
report.VehicleId,
out var sequence) &&
report.SequenceNumber <= sequence)
{
continue;
}
_lastReportSequences[report.VehicleId] =
report.SequenceNumber;
_reports[report.VehicleId] =
new AcceptedReport(report, currentTimeSeconds);
}
return true;
}
catch (Exception exception)
{
return FaultLeader(
"接收成员报告失败:" + exception.Message);
}
}
private void StoreLocalReport(
VehicleState state,
double currentTimeSeconds)
{
_reports[SelfVehicleId] = new AcceptedReport(
CreateReport(state, isStateAvailable: true),
currentTimeSeconds);
}
private void UpdatePreparationBarrier()
{
foreach (var accepted in _reports.Values)
{
var report = accepted.Report;
_preparationCoordinator.ReportMemberStatus(
new FleetMemberPreparationStatus(
CurrentPlanId,
report.VehicleId,
ToAgentState(report.State),
report.State == FleetMemberState.Faulted
? $"成员车故障码{report.FailureCode}。"
: string.Empty));
}
}
private bool CheckLeaderSafety(double currentTimeSeconds)
{
var statuses = new List<FleetMemberSafetyStatus>();
foreach (var accepted in _reports.Values)
{
var report = accepted.Report;
statuses.Add(new FleetMemberSafetyStatus(
report.VehicleId,
report.PlanId,
report.IsStateAvailable,
report.State == FleetMemberState.Faulted,
report.FailureCode,
accepted.ReceivedTimeSeconds));
}
var graceExpired =
currentTimeSeconds - _planStartTimeSeconds.Value >
_safetySupervisor.CommunicationTimeoutSeconds;
if (statuses.Count != _activeLayout.VehicleCount &&
!graceExpired)
{
return true;
}
var decision = _safetySupervisor.Evaluate(
_activeLayout,
statuses,
currentTimeSeconds);
return !decision.ShouldStop ||
FaultLeader(decision.Reason);
}
private bool ActivateFleet(double currentTimeSeconds)
{
if (_preparationCoordinator.State !=
FleetPreparationCoordinatorState.ReadyToActivate)
{
return false;
}
if (!_preparationCoordinator.TryAuthorizeActivation(
CurrentPlanId))
{
return FaultLeader("无法授权全队激活。" );
}
var command = NewCommand(
FleetProtocol.BroadcastVehicleId,
FleetCommandKind.Activate,
0.0,
Twist2D.Zero);
if (!_localAgent.Activate(
CurrentPlanId,
currentTimeSeconds,
command.ValidForSeconds))
{
return FaultLeader(LocalFailure("主车激活失败。"));
}
AcceptLocalCommand(command.SequenceNumber);
try
{
_transport.SendCommand(command);
State = FleetRuntimeState.Active;
return true;
}
catch (Exception exception)
{
return FaultLeader(
"广播激活命令失败:" + exception.Message);
}
}
private void RunCoordination(
double currentTimeSeconds,
double deltaTimeSeconds)
{
var result = _fleetCoordinator.ExecuteCycle(
BuildMemberStates(),
currentTimeSeconds,
deltaTimeSeconds,
out var output);
LastCoordinationOutput = output;
if (result == FleetCoordinationCycleResult.CommandGenerated ||
result == FleetCoordinationCycleResult.WaitingForState)
{
Dispatch(output.MemberCommands, currentTimeSeconds);
return;
}
if (result == FleetCoordinationCycleResult.Completed)
{
BroadcastStop(reasonCode: 0);
_localAgent.Stop();
_preparationCoordinator.Cancel();
_safetySupervisor.Reset();
State = FleetRuntimeState.Completed;
return;
}
var reason = string.IsNullOrWhiteSpace(output.Reason)
? _fleetCoordinator.LastFailureReason
: output.Reason;
FaultLeader(string.IsNullOrWhiteSpace(reason)
? "车队协调器未生成可执行命令。"
: reason);
}
private bool Dispatch(
IReadOnlyList<FleetMemberCommand> commands,
double currentTimeSeconds)
{
if (commands == null ||
commands.Count != _activeLayout.VehicleCount)
{
return FaultLeader("成员命令数量与布局不一致。" );
}
try
{
foreach (var memberCommand in commands)
{
var command = NewCommand(
memberCommand.VehicleId,
FleetCommandKind.Motion,
0.0,
memberCommand.TwistInVehicleBody);
if (memberCommand.VehicleId == SelfVehicleId)
{
if (!_localAgent.Execute(
CurrentPlanId,
memberCommand,
currentTimeSeconds,
command.ValidForSeconds))
{
return FaultLeader(
LocalFailure("主车执行速度命令失败。"));
}
AcceptLocalCommand(command.SequenceNumber);
}
else
{
_transport.SendCommand(command);
}
}
return true;
}
catch (Exception exception)
{
return FaultLeader(
"分发成员命令失败:" + exception.Message);
}
}
private void ReceiveCommands(double currentTimeSeconds)
{
try
{
while (_transport.TryReceiveCommand(out var command))
{
ApplyCommand(command, currentTimeSeconds);
}
}
catch (Exception exception)
{
FaultMember(
"接收或执行主车命令失败:" + exception.Message);
}
}
private void ApplyCommand(
FleetCommand command,
double currentTimeSeconds)
{
if (command.TargetVehicleId !=
FleetProtocol.BroadcastVehicleId &&
command.TargetVehicleId != SelfVehicleId)
{
return;
}
ValidateCommand(command);
var isPreparation =
command.Kind == FleetCommandKind.PrepareRolling ||
command.Kind == FleetCommandKind.PrepareSpin;
if (isPreparation && command.PlanId != CurrentPlanId)
{
if (State == FleetRuntimeState.Preparing ||
State == FleetRuntimeState.Ready ||
State == FleetRuntimeState.Active)
{
return;
}
CurrentPlanId = command.PlanId;
_lastReceivedCommandSequence = 0;
_lastAppliedCommandSequence = 0;
LastFailureReason = string.Empty;
}
if (command.PlanId != CurrentPlanId ||
command.SequenceNumber <= _lastReceivedCommandSequence)
{
return;
}
_lastReceivedCommandSequence = command.SequenceNumber;
bool accepted;
switch (command.Kind)
{
case FleetCommandKind.PrepareRolling:
accepted = _localAgent.BeginRollingPreparation(
command.PlanId,
command.MotionDirectionInBodyRadians);
break;
case FleetCommandKind.PrepareSpin:
accepted =
_localAgent.BeginSpinPreparation(command.PlanId);
break;
case FleetCommandKind.Activate:
accepted = _localAgent.Activate(
command.PlanId,
currentTimeSeconds,
command.ValidForSeconds);
break;
case FleetCommandKind.Motion:
accepted = _localAgent.Execute(
command.PlanId,
new FleetMemberCommand(
SelfVehicleId,
command.TwistInVehicleBody),
currentTimeSeconds,
command.ValidForSeconds);
break;
case FleetCommandKind.Stop:
_localAgent.Stop();
_lastAppliedCommandSequence = command.SequenceNumber;
CurrentPlanId = FleetProtocol.NoActivePlanId;
State = FleetRuntimeState.Idle;
LastFailureReason = string.Empty;
return;
default:
accepted = false;
break;
}
if (!accepted)
{
FaultMember(LocalFailure(
$"成员车拒绝{command.Kind}命令。"));
return;
}
_lastAppliedCommandSequence = command.SequenceNumber;
State = ToRuntimeState(_localAgent.State);
}
private void SendLocalReport(
VehicleState? state,
bool isStateAvailable,
double currentTimeSeconds)
{
try
{
var value = state ?? _lastLocalState ??
new VehicleState(
currentTimeSeconds,
Pose2D.Identity,
Twist2D.Zero,
hasValidVelocityEstimate: false);
_transport.SendReport(
CreateReport(value, isStateAvailable));
}
catch (Exception exception)
{
FaultMember(
"发送成员报告失败:" + exception.Message);
}
}
private FleetMemberReport CreateReport(
VehicleState state,
bool isStateAvailable)
{
return new FleetMemberReport(
SelfVehicleId,
CurrentPlanId,
NextReportSequence(),
state.SampleTimestampSeconds,
state.PoseInWorld,
state.TwistInWorld,
isStateAvailable,
isStateAvailable && state.HasValidVelocityEstimate,
ToProtocolState(),
_lastAppliedCommandSequence,
State == FleetRuntimeState.Faulted
? RuntimeFailureCode
: 0);
}
private List<FleetMemberStateSample> BuildMemberStates()
{
var states = new List<FleetMemberStateSample>(
_activeLayout.VehicleCount);
foreach (var vehicle in _activeLayout.Vehicles)
{
var report = _reports[vehicle.VehicleId].Report;
states.Add(new FleetMemberStateSample(
report.VehicleId,
report.SampleTimestampSeconds,
report.PoseInCommonWorld,
report.TwistAtVehicleOriginInCommonWorld,
report.IsStateAvailable,
report.HasValidVelocityEstimate));
}
return states;
}
private FleetCommand NewCommand(
int targetVehicleId,
FleetCommandKind kind,
double motionDirectionInBodyRadians,
Twist2D twist,
int reasonCode = 0)
{
return new FleetCommand(
CurrentPlanId,
NextCommandSequence(),
targetVehicleId,
kind,
motionDirectionInBodyRadians,
twist,
_commandValidForSeconds,
reasonCode);
}
private bool FaultLeader(string reason)
{
var preservedReason = string.IsNullOrWhiteSpace(reason)
? "车队运行入口发生未说明故障。"
: reason;
if (CurrentPlanId > 0)
{
BroadcastStop(FaultStopReasonCode);
}
_localAgent.Stop();
State = FleetRuntimeState.Faulted;
LastFailureReason = preservedReason;
return false;
}
private void FaultMember(string reason)
{
var preservedReason = string.IsNullOrWhiteSpace(reason)
? "成员车运行入口发生未说明故障。"
: reason;
_localAgent.Stop();
State = FleetRuntimeState.Faulted;
LastFailureReason = preservedReason;
}
private void BroadcastStop(int reasonCode)
{
try
{
_transport.SendCommand(NewCommand(
FleetProtocol.BroadcastVehicleId,
FleetCommandKind.Stop,
0.0,
Twist2D.Zero,
reasonCode));
}
catch
{
// 本车仍立即停车,失联成员由各自命令看门狗兜底。
}
}
private void ResetLeaderForNewPlan()
{
_localAgent.Stop();
_preparationCoordinator.Cancel();
_fleetCoordinator.Cancel();
_safetySupervisor.Reset();
_reports.Clear();
_lastReportSequences.Clear();
_activeLayout = null;
_lastLocalState = null;
_planStartTimeSeconds = null;
_lastReceivedCommandSequence = 0;
_lastAppliedCommandSequence = 0;
LastCoordinationOutput = null;
LastFailureReason = string.Empty;
}
private void AcceptLocalCommand(long sequenceNumber)
{
_lastReceivedCommandSequence = sequenceNumber;
_lastAppliedCommandSequence = sequenceNumber;
}
private long NextCommandSequence()
{
if (++_nextCommandSequence <= 0)
{
throw new InvalidOperationException("主车命令序号耗尽。" );
}
return _nextCommandSequence;
}
private long NextReportSequence()
{
if (++_nextReportSequence <= 0)
{
throw new InvalidOperationException("成员报告序号耗尽。" );
}
return _nextReportSequence;
}
private string LocalFailure(string fallback)
{
return string.IsNullOrWhiteSpace(_localAgent.LastFailureReason)
? fallback
: _localAgent.LastFailureReason;
}
private void EnsureLeader()
{
if (!IsLeader)
{
throw new InvalidOperationException(
"只有主车可以启动完整车队任务。");
}
}
private FleetMemberState ToProtocolState()
{
if (State == FleetRuntimeState.Faulted)
{
return FleetMemberState.Faulted;
}
return _localAgent.State switch
{
FleetMemberAgentState.Idle => FleetMemberState.Idle,
FleetMemberAgentState.Preparing => FleetMemberState.Preparing,
FleetMemberAgentState.Ready => FleetMemberState.Ready,
FleetMemberAgentState.Active => FleetMemberState.Active,
FleetMemberAgentState.Faulted => FleetMemberState.Faulted,
_ => throw new InvalidOperationException("本车状态无效。")
};
}
private static FleetRuntimeState ToRuntimeState(
FleetMemberAgentState state)
{
return state switch
{
FleetMemberAgentState.Idle => FleetRuntimeState.Idle,
FleetMemberAgentState.Preparing => FleetRuntimeState.Preparing,
FleetMemberAgentState.Ready => FleetRuntimeState.Ready,
FleetMemberAgentState.Active => FleetRuntimeState.Active,
FleetMemberAgentState.Faulted => FleetRuntimeState.Faulted,
_ => throw new ArgumentOutOfRangeException(nameof(state))
};
}
private static FleetMemberAgentState ToAgentState(
FleetMemberState state)
{
return state switch
{
FleetMemberState.Idle => FleetMemberAgentState.Idle,
FleetMemberState.Preparing => FleetMemberAgentState.Preparing,
FleetMemberState.Ready => FleetMemberAgentState.Ready,
FleetMemberState.Active => FleetMemberAgentState.Active,
FleetMemberState.Faulted => FleetMemberAgentState.Faulted,
_ => throw new ArgumentOutOfRangeException(nameof(state))
};
}
private static void ValidateCommand(FleetCommand command)
{
if (command.PlanId <= 0 || command.SequenceNumber <= 0)
{
throw new ArgumentException("车队命令任务号或序号无效。" );
}
if (!Enum.IsDefined(typeof(FleetCommandKind), command.Kind))
{
throw new ArgumentException("车队命令类型无效。" );
}
NumericGuard.EnsureFinite(
command.MotionDirectionInBodyRadians,
nameof(command));
NumericGuard.EnsureFinite(
command.TwistInVehicleBody,
nameof(command));
NumericGuard.EnsureFinitePositive(
command.ValidForSeconds,
nameof(command));
}
private static void ValidateReport(FleetMemberReport report)
{
if (report.VehicleId <= 0 || report.PlanId <= 0 ||
report.SequenceNumber <= 0 ||
report.LastAppliedCommandSequence < 0 ||
!Enum.IsDefined(typeof(FleetMemberState), report.State))
{
throw new ArgumentException("成员状态报告字段无效。" );
}
NumericGuard.EnsureFiniteNonNegative(
report.SampleTimestampSeconds,
nameof(report));
NumericGuard.EnsureFinite(
report.PoseInCommonWorld,
nameof(report));
NumericGuard.EnsureFinite(
report.TwistAtVehicleOriginInCommonWorld,
nameof(report));
}
private readonly struct AcceptedReport
{
public AcceptedReport(
FleetMemberReport report,
double receivedTimeSeconds)
{
Report = report;
ReceivedTimeSeconds = receivedTimeSeconds;
}
public FleetMemberReport Report { get; }
public double ReceivedTimeSeconds { get; }
}
}
}