Files
ParkingRobot/MultiWheelC.Tests/FleetRuntimeTests.cs

431 lines
14 KiB
C#
Raw Permalink 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 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; }
}
}
}