贯通车队运行链并支持轨迹自动推导β
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user