431 lines
14 KiB
C#
431 lines
14 KiB
C#
using System;
|
||||
|
|
using System.Numerics;
|
|||
|
|
using CommonUsage.Chassis;
|
|||
|
|
using MultiWheelC.Control.Abstractions;
|
|||
|
|
using MultiWheelC.Control.Allocation;
|
|||
|
|
using MultiWheelC.Fleet;
|
|||
|
|
using MultiWheelC.StateEstimation;
|
|||
|
|
using MultiWheelC.Trajectory;
|
|||
|
|
using MyParking.Shared;
|
|||
|
|
|
|||
|
|
namespace MultiWheelC.Tests
|
|||
|
|
{
|
|||
|
|
internal static class FleetRuntimeTests
|
|||
|
|
{
|
|||
|
|
private const long PlanId = 21;
|
|||
|
|
|
|||
|
|
public static void Run()
|
|||
|
|
{
|
|||
|
|
VerifyTwoVehiclePlanBecomesActive();
|
|||
|
|
VerifyStaleCommandIsIgnored();
|
|||
|
|
VerifyInvalidCommandLatchesMemberFault();
|
|||
|
|
VerifyMissingReportFaultsLeader();
|
|||
|
|
VerifyMemberWatchdogStopsLocally();
|
|||
|
|
VerifyUnavailableMemberStateFaultsFleet();
|
|||
|
|
|
|||
|
|
Console.WriteLine(
|
|||
|
|
"FleetRuntime端到端测试通过,共6个场景。");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void VerifyTwoVehiclePlanBecomesActive()
|
|||
|
|
{
|
|||
|
|
var fleet = CreateFleet();
|
|||
|
|
StartAndActivate(fleet);
|
|||
|
|
|
|||
|
|
AssertState(
|
|||
|
|
fleet.LeaderRuntime,
|
|||
|
|
FleetRuntimeState.Active,
|
|||
|
|
"主车正常激活");
|
|||
|
|
AssertState(
|
|||
|
|
fleet.MemberRuntime,
|
|||
|
|
FleetRuntimeState.Active,
|
|||
|
|
"从车正常激活");
|
|||
|
|
AssertTrue(
|
|||
|
|
fleet.LeaderRuntime.LastCoordinationOutput != null,
|
|||
|
|
"主车激活后应产生首周期协调输出");
|
|||
|
|
AssertTrue(
|
|||
|
|
fleet.MemberRuntime.LastAppliedCommandSequence > 0,
|
|||
|
|
"从车应确认已经执行主车命令");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void VerifyStaleCommandIsIgnored()
|
|||
|
|
{
|
|||
|
|
var fleet = CreateFleet();
|
|||
|
|
StartAndActivate(fleet);
|
|||
|
|
var appliedSequence =
|
|||
|
|
fleet.MemberRuntime.LastAppliedCommandSequence;
|
|||
|
|
|
|||
|
|
fleet.LeaderTransport.SendCommand(
|
|||
|
|
new FleetCommand(
|
|||
|
|
PlanId,
|
|||
|
|
appliedSequence,
|
|||
|
|
targetVehicleId: 2,
|
|||
|
|
FleetCommandKind.Motion,
|
|||
|
|
motionDirectionInBodyRadians: 0.0,
|
|||
|
|
twistInVehicleBody:
|
|||
|
|
new Twist2D(1.0, 0.0, 0.0),
|
|||
|
|
validForSeconds: 0.2));
|
|||
|
|
fleet.MemberProvider.SetTimestamp(0.04);
|
|||
|
|
fleet.MemberRuntime.Update(0.04, 0.02);
|
|||
|
|
|
|||
|
|
AssertState(
|
|||
|
|
fleet.MemberRuntime,
|
|||
|
|
FleetRuntimeState.Active,
|
|||
|
|
"忽略旧序列命令");
|
|||
|
|
AssertTrue(
|
|||
|
|
fleet.MemberRuntime.LastAppliedCommandSequence ==
|
|||
|
|
appliedSequence,
|
|||
|
|
"旧序列命令不应更新最近执行序号");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void VerifyMissingReportFaultsLeader()
|
|||
|
|
{
|
|||
|
|
var fleet = CreateFleet();
|
|||
|
|
AssertTrue(
|
|||
|
|
fleet.LeaderRuntime.StartRollingPlan(
|
|||
|
|
PlanId,
|
|||
|
|
fleet.Layout,
|
|||
|
|
CreateTrajectory()),
|
|||
|
|
"主车应成功启动测试任务");
|
|||
|
|
|
|||
|
|
fleet.LeaderRuntime.Update(0.0, 0.02);
|
|||
|
|
fleet.LeaderProvider.SetTimestamp(0.25);
|
|||
|
|
fleet.LeaderRuntime.Update(0.25, 0.02);
|
|||
|
|
|
|||
|
|
AssertState(
|
|||
|
|
fleet.LeaderRuntime,
|
|||
|
|
FleetRuntimeState.Faulted,
|
|||
|
|
"成员报告超时");
|
|||
|
|
AssertTrue(
|
|||
|
|
fleet.LeaderRuntime.LastFailureReason.Contains(
|
|||
|
|
"未收到成员车2"),
|
|||
|
|
"通信超时应指出缺失的成员车");
|
|||
|
|
AssertTrue(
|
|||
|
|
fleet.LeaderAgent.State ==
|
|||
|
|
FleetMemberAgentState.Idle,
|
|||
|
|
"主车故障后必须立即停止本车执行器");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void VerifyInvalidCommandLatchesMemberFault()
|
|||
|
|
{
|
|||
|
|
var fleet = CreateFleet();
|
|||
|
|
StartAndActivate(fleet);
|
|||
|
|
|
|||
|
|
fleet.LeaderTransport.SendCommand(
|
|||
|
|
new FleetCommand(
|
|||
|
|
PlanId,
|
|||
|
|
fleet.MemberRuntime.LastAppliedCommandSequence + 1,
|
|||
|
|
targetVehicleId: 2,
|
|||
|
|
FleetCommandKind.Motion,
|
|||
|
|
motionDirectionInBodyRadians: 0.0,
|
|||
|
|
twistInVehicleBody: Twist2D.Zero,
|
|||
|
|
validForSeconds: double.NaN));
|
|||
|
|
fleet.MemberRuntime.Update(0.04, 0.02);
|
|||
|
|
|
|||
|
|
AssertState(
|
|||
|
|
fleet.MemberRuntime,
|
|||
|
|
FleetRuntimeState.Faulted,
|
|||
|
|
"非法命令字段触发并锁存本地故障");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void VerifyMemberWatchdogStopsLocally()
|
|||
|
|
{
|
|||
|
|
var fleet = CreateFleet();
|
|||
|
|
StartAndActivate(fleet);
|
|||
|
|
|
|||
|
|
fleet.MemberProvider.SetTimestamp(0.25);
|
|||
|
|
fleet.MemberRuntime.Update(0.25, 0.02);
|
|||
|
|
|
|||
|
|
AssertState(
|
|||
|
|
fleet.MemberRuntime,
|
|||
|
|
FleetRuntimeState.Faulted,
|
|||
|
|
"从车命令看门狗超时");
|
|||
|
|
AssertTrue(
|
|||
|
|
fleet.MemberRuntime.LastFailureReason.Contains(
|
|||
|
|
"超时"),
|
|||
|
|
"从车看门狗应保留超时原因");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void VerifyUnavailableMemberStateFaultsFleet()
|
|||
|
|
{
|
|||
|
|
var fleet = CreateFleet();
|
|||
|
|
StartAndActivate(fleet);
|
|||
|
|
|
|||
|
|
fleet.MemberProvider.IsAvailable = false;
|
|||
|
|
fleet.MemberRuntime.Update(0.04, 0.02);
|
|||
|
|
fleet.LeaderProvider.SetTimestamp(0.04);
|
|||
|
|
fleet.LeaderRuntime.Update(0.04, 0.02);
|
|||
|
|
|
|||
|
|
AssertState(
|
|||
|
|
fleet.MemberRuntime,
|
|||
|
|
FleetRuntimeState.Faulted,
|
|||
|
|
"从车状态不可用时本地停车");
|
|||
|
|
AssertState(
|
|||
|
|
fleet.LeaderRuntime,
|
|||
|
|
FleetRuntimeState.Faulted,
|
|||
|
|
"从车状态不可用时整队停车");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void StartAndActivate(TestFleet fleet)
|
|||
|
|
{
|
|||
|
|
AssertTrue(
|
|||
|
|
fleet.LeaderRuntime.StartRollingPlan(
|
|||
|
|
PlanId,
|
|||
|
|
fleet.Layout,
|
|||
|
|
CreateTrajectory()),
|
|||
|
|
"主车应成功启动测试任务");
|
|||
|
|
|
|||
|
|
fleet.MemberRuntime.Update(0.0, 0.02);
|
|||
|
|
fleet.LeaderRuntime.Update(0.0, 0.02);
|
|||
|
|
fleet.MemberProvider.SetTimestamp(0.02);
|
|||
|
|
fleet.MemberRuntime.Update(0.02, 0.02);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static TestFleet CreateFleet()
|
|||
|
|
{
|
|||
|
|
var layout = new FleetLayout(
|
|||
|
|
new[]
|
|||
|
|
{
|
|||
|
|
new VehicleLayout(
|
|||
|
|
1,
|
|||
|
|
new Pose2D(-0.5, 0.0, 0.0)),
|
|||
|
|
new VehicleLayout(
|
|||
|
|
2,
|
|||
|
|
new Pose2D(0.5, 0.0, 0.0))
|
|||
|
|
});
|
|||
|
|
var network = new InMemoryFleetTransportNetwork(
|
|||
|
|
new[] { 1, 2 },
|
|||
|
|
leaderVehicleId: 1);
|
|||
|
|
var leaderTransport = network.CreateEndpoint(1);
|
|||
|
|
var memberTransport = network.CreateEndpoint(2);
|
|||
|
|
var leaderProvider = new MutableStateProvider(
|
|||
|
|
new Pose2D(-0.5, 0.0, 0.0));
|
|||
|
|
var memberProvider = new MutableStateProvider(
|
|||
|
|
new Pose2D(0.5, 0.0, 0.0));
|
|||
|
|
var leaderAgent = CreateAgent(1);
|
|||
|
|
var memberAgent = CreateAgent(2);
|
|||
|
|
|
|||
|
|
var leaderRuntime = new FleetRuntime(
|
|||
|
|
selfVehicleId: 1,
|
|||
|
|
leaderVehicleId: 1,
|
|||
|
|
leaderTransport,
|
|||
|
|
leaderAgent,
|
|||
|
|
leaderProvider,
|
|||
|
|
new FleetPreparationCoordinator(),
|
|||
|
|
CreateCoordinator(),
|
|||
|
|
new FleetSafetySupervisor(
|
|||
|
|
communicationTimeoutSeconds: 0.2),
|
|||
|
|
commandValidForSeconds: 0.2,
|
|||
|
|
preparationTimeoutSeconds: 1.0);
|
|||
|
|
var memberRuntime = new FleetRuntime(
|
|||
|
|
selfVehicleId: 2,
|
|||
|
|
leaderVehicleId: 1,
|
|||
|
|
memberTransport,
|
|||
|
|
memberAgent,
|
|||
|
|
memberProvider,
|
|||
|
|
commandValidForSeconds: 0.2);
|
|||
|
|
|
|||
|
|
return new TestFleet(
|
|||
|
|
layout,
|
|||
|
|
leaderTransport,
|
|||
|
|
leaderProvider,
|
|||
|
|
memberProvider,
|
|||
|
|
leaderAgent,
|
|||
|
|
leaderRuntime,
|
|||
|
|
memberRuntime);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static FleetCoordinator CreateCoordinator()
|
|||
|
|
{
|
|||
|
|
return new FleetCoordinator(
|
|||
|
|
new FleetStateEstimator(
|
|||
|
|
maximumMemberStateAgeSeconds: 0.5,
|
|||
|
|
maximumPositionDisagreementMeters: 0.2,
|
|||
|
|
maximumYawDisagreementRadians:
|
|||
|
|
AngleMath.DegreesToRadians(5.0)),
|
|||
|
|
new FleetController(
|
|||
|
|
new StraightLateralController(),
|
|||
|
|
new ZeroLongitudinalController(),
|
|||
|
|
new GcpCommandAllocator(Math.PI / 4.0),
|
|||
|
|
virtualControlPointRadiusMeters: 0.5),
|
|||
|
|
new FleetMemberCommandCorrector(
|
|||
|
|
longitudinalPositionGainPerSecond: 1.0,
|
|||
|
|
lateralPositionGainPerSecond: 1.0,
|
|||
|
|
yawGainPerSecond: 1.0,
|
|||
|
|
positionErrorDeadbandMeters: 0.005,
|
|||
|
|
yawErrorDeadbandRadians:
|
|||
|
|
AngleMath.DegreesToRadians(0.5),
|
|||
|
|
maximumLinearCorrectionMetersPerSecond: 0.03,
|
|||
|
|
maximumAngularCorrectionRadiansPerSecond:
|
|||
|
|
AngleMath.DegreesToRadians(2.0)),
|
|||
|
|
memberPositionErrorWarningMeters: 0.02,
|
|||
|
|
maximumMemberPositionErrorMeters: 0.05,
|
|||
|
|
memberYawErrorWarningRadians:
|
|||
|
|
AngleMath.DegreesToRadians(1.0),
|
|||
|
|
maximumMemberYawErrorRadians:
|
|||
|
|
AngleMath.DegreesToRadians(3.0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static Trajectory2D CreateTrajectory()
|
|||
|
|
{
|
|||
|
|
return new Trajectory2D(
|
|||
|
|
new[]
|
|||
|
|
{
|
|||
|
|
new TrajectoryPoint(
|
|||
|
|
0.0,
|
|||
|
|
Pose2D.Identity,
|
|||
|
|
0.0,
|
|||
|
|
0.2),
|
|||
|
|
new TrajectoryPoint(
|
|||
|
|
1.0,
|
|||
|
|
new Pose2D(1.0, 0.0, 0.0),
|
|||
|
|
0.0,
|
|||
|
|
0.2)
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static FleetMemberAgent CreateAgent(int vehicleId)
|
|||
|
|
{
|
|||
|
|
var chassis = new MultiWheelChassis();
|
|||
|
|
chassis.AddWheel(CreateWheel(-500f, 300f));
|
|||
|
|
chassis.AddWheel(CreateWheel(-500f, -300f));
|
|||
|
|
chassis.AddWheel(CreateWheel(500f, 300f));
|
|||
|
|
chassis.AddWheel(CreateWheel(500f, -300f));
|
|||
|
|
chassis.Initialize();
|
|||
|
|
|
|||
|
|
return new FleetMemberAgent(
|
|||
|
|
new MultiWheelChassisAdapter(chassis, vehicleId),
|
|||
|
|
alignmentToleranceRadians:
|
|||
|
|
AngleMath.DegreesToRadians(1.0),
|
|||
|
|
alignmentStableSeconds: 0.0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static SteerWheel CreateWheel(float x, float y)
|
|||
|
|
{
|
|||
|
|
var speed = 0f;
|
|||
|
|
var angle = 0f;
|
|||
|
|
return new SteerWheel(
|
|||
|
|
new Vector2(x, y),
|
|||
|
|
angleLowerLimit: -120f,
|
|||
|
|
angleUpperLimit: 120f,
|
|||
|
|
speedWriter: value => speed = value,
|
|||
|
|
speedReader: () => speed,
|
|||
|
|
angleWriter: value => angle = value,
|
|||
|
|
angleReader: () => angle);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void AssertState(
|
|||
|
|
FleetRuntime runtime,
|
|||
|
|
FleetRuntimeState expected,
|
|||
|
|
string scenario)
|
|||
|
|
{
|
|||
|
|
AssertTrue(
|
|||
|
|
runtime.State == expected,
|
|||
|
|
$"{scenario}状态错误:" +
|
|||
|
|
$"actual={runtime.State}, expected={expected}," +
|
|||
|
|
$"reason={runtime.LastFailureReason}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void AssertTrue(bool condition, string message)
|
|||
|
|
{
|
|||
|
|
if (!condition)
|
|||
|
|
{
|
|||
|
|
throw new InvalidOperationException(message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private sealed class MutableStateProvider :
|
|||
|
|
IVehicleStateProvider
|
|||
|
|
{
|
|||
|
|
private readonly Pose2D _poseInWorld;
|
|||
|
|
private double _timestampSeconds;
|
|||
|
|
|
|||
|
|
public MutableStateProvider(Pose2D poseInWorld)
|
|||
|
|
{
|
|||
|
|
_poseInWorld = poseInWorld;
|
|||
|
|
IsAvailable = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool IsAvailable { get; set; }
|
|||
|
|
|
|||
|
|
public void SetTimestamp(double timestampSeconds)
|
|||
|
|
{
|
|||
|
|
_timestampSeconds = timestampSeconds;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryGetState(out VehicleState state)
|
|||
|
|
{
|
|||
|
|
state = new VehicleState(
|
|||
|
|
_timestampSeconds,
|
|||
|
|
_poseInWorld,
|
|||
|
|
Twist2D.Zero,
|
|||
|
|
hasValidVelocityEstimate: true);
|
|||
|
|
return IsAvailable;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private sealed class StraightLateralController :
|
|||
|
|
ILateralController
|
|||
|
|
{
|
|||
|
|
public LateralControlCommand Compute(
|
|||
|
|
PathTrackingContext context)
|
|||
|
|
{
|
|||
|
|
return LateralControlCommand.Straight;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Reset()
|
|||
|
|
{
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private sealed class ZeroLongitudinalController :
|
|||
|
|
ILongitudinalController
|
|||
|
|
{
|
|||
|
|
public double ComputeSpeedMetersPerSecond(
|
|||
|
|
PathTrackingContext context)
|
|||
|
|
{
|
|||
|
|
return 0.0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Reset()
|
|||
|
|
{
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private sealed class TestFleet
|
|||
|
|
{
|
|||
|
|
public TestFleet(
|
|||
|
|
FleetLayout layout,
|
|||
|
|
InMemoryFleetTransport leaderTransport,
|
|||
|
|
MutableStateProvider leaderProvider,
|
|||
|
|
MutableStateProvider memberProvider,
|
|||
|
|
FleetMemberAgent leaderAgent,
|
|||
|
|
FleetRuntime leaderRuntime,
|
|||
|
|
FleetRuntime memberRuntime)
|
|||
|
|
{
|
|||
|
|
Layout = layout;
|
|||
|
|
LeaderTransport = leaderTransport;
|
|||
|
|
LeaderProvider = leaderProvider;
|
|||
|
|
MemberProvider = memberProvider;
|
|||
|
|
LeaderAgent = leaderAgent;
|
|||
|
|
LeaderRuntime = leaderRuntime;
|
|||
|
|
MemberRuntime = memberRuntime;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public FleetLayout Layout { get; }
|
|||
|
|
|
|||
|
|
public InMemoryFleetTransport LeaderTransport { get; }
|
|||
|
|
|
|||
|
|
public MutableStateProvider LeaderProvider { get; }
|
|||
|
|
|
|||
|
|
public MutableStateProvider MemberProvider { get; }
|
|||
|
|
|
|||
|
|
public FleetMemberAgent LeaderAgent { get; }
|
|||
|
|
|
|||
|
|
public FleetRuntime LeaderRuntime { get; }
|
|||
|
|
|
|||
|
|
public FleetRuntime MemberRuntime { get; }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|