using System; using System.Collections.Generic; using MultiWheelC.Control.Abstractions; using MultiWheelC.Control.Allocation; using MultiWheelC.Fleet; using MultiWheelC.Trajectory; using MyParking.Shared; namespace MultiWheelC.Tests { internal static class FleetCoordinatorTests { private const double Tolerance = 1e-9; public static void Run() { VerifyCommandCycle(); VerifySmallLayoutErrorIsCorrected(); VerifyWarningRangeScalesAllCommands(); VerifyUnavailableStateStopsAndRecovers(); VerifyUnsafeLayoutFaultsAndLatches(); VerifyCompletionStops(); VerifyTrackingFaultStops(); VerifyCancelReturnsInactive(); Console.WriteLine( "FleetCoordinator车队协调测试通过。共8个场景。"); } private static void VerifyCommandCycle() { var layout = CreateLayout(); var coordinator = CreateCoordinator(); coordinator.Start(layout, CreateTrajectory()); var result = coordinator.ExecuteCycle( CreateRigidMemberStates( layout, new Pose2D(0.5, 0.0, 0.0), new Twist2D(0.4, 0.0, 0.0), 1.0), targetTimestampSeconds: 1.0, deltaTimeSeconds: 0.02, out var output); AssertResult( result, FleetCoordinationCycleResult.CommandGenerated, "正常协调周期"); if (!output.State.HasValue) { throw new InvalidOperationException( "正常协调周期没有返回车队状态。"); } AssertNear( output.State.Value.FleetPoseInWorld.XMeters, 0.5, "正常协调周期中心X"); AssertNear( output.SpeedScale, 1.0, "正常协调周期速度比例"); AssertTwist( output.FleetCommand.TwistAtReferencePoint, 0.4, 0.0, 0.0, "正常协调周期车队命令"); AssertTwist( FindCommand(output.MemberCommands, 1) .TwistInVehicleBody, 0.4, 0.0, 0.0, "正常协调周期车辆1"); AssertTwist( FindCommand(output.MemberCommands, 2) .TwistInVehicleBody, -0.4, 0.0, 0.0, "正常协调周期车辆2"); } private static void VerifySmallLayoutErrorIsCorrected() { var layout = CreateLayout(); var coordinator = CreateCoordinator(); coordinator.Start(layout, CreateTrajectory()); var result = coordinator.ExecuteCycle( new[] { CreateMemberState( 1, new Pose2D(-0.99, 0.0, 0.0), 1.0), CreateMemberState( 2, new Pose2D(0.99, 0.0, Math.PI), 1.0) }, targetTimestampSeconds: 1.0, deltaTimeSeconds: 0.02, out var output); AssertResult( result, FleetCoordinationCycleResult.CommandGenerated, "小范围布局误差纠偏"); AssertNear( output.SpeedScale, 1.0, "小范围布局误差速度比例"); AssertTwist( FindCommand(output.BaseMemberCommands, 1) .TwistInVehicleBody, 0.4, 0.0, 0.0, "车辆1基础命令"); AssertTwist( FindCommand(output.BaseMemberCommands, 2) .TwistInVehicleBody, -0.4, 0.0, 0.0, "车辆2基础命令"); AssertTwist( FindCommand(output.MemberCommands, 1) .TwistInVehicleBody, 0.395, 0.0, 0.0, "车辆1纠偏后命令"); AssertTwist( FindCommand(output.MemberCommands, 2) .TwistInVehicleBody, -0.405, 0.0, 0.0, "车辆2纠偏后命令"); } private static void VerifyWarningRangeScalesAllCommands() { var layout = CreateLayout(); var coordinator = CreateCoordinator(); coordinator.Start(layout, CreateTrajectory()); var result = coordinator.ExecuteCycle( new[] { CreateMemberState( 1, new Pose2D(-0.965, 0.0, 0.0), 1.0), CreateMemberState( 2, new Pose2D(0.965, 0.0, Math.PI), 1.0) }, targetTimestampSeconds: 1.0, deltaTimeSeconds: 0.02, out var output); AssertResult( result, FleetCoordinationCycleResult.CommandGenerated, "警告区间统一缩放"); AssertNear( output.SpeedScale, 0.5, "警告区间速度比例"); AssertTwist( output.FleetCommand.TwistAtReferencePoint, 0.2, 0.0, 0.0, "警告区间车队命令"); AssertTwist( FindCommand(output.MemberCommands, 1) .TwistInVehicleBody, 0.2, 0.0, 0.0, "警告区间车辆1"); AssertTwist( FindCommand(output.MemberCommands, 2) .TwistInVehicleBody, -0.2, 0.0, 0.0, "警告区间车辆2"); if (string.IsNullOrWhiteSpace(output.Reason)) { throw new InvalidOperationException( "警告区间缩放没有返回限制原因。"); } } private static void VerifyUnavailableStateStopsAndRecovers() { var layout = CreateLayout(); var coordinator = CreateCoordinator(); coordinator.Start(layout, CreateTrajectory()); var unavailableStates = CreateRigidMemberStates( layout, new Pose2D(0.5, 0.0, 0.0), new Twist2D(0.4, 0.0, 0.0), 1.0); unavailableStates[1] = new FleetMemberStateSample( unavailableStates[1].VehicleId, unavailableStates[1].SampleTimestampSeconds, unavailableStates[1].PoseInWorld, unavailableStates[1].TwistAtVehicleOriginInWorld, isStateAvailable: false, hasValidVelocityEstimate: true); var waitingResult = coordinator.ExecuteCycle( unavailableStates, targetTimestampSeconds: 1.0, deltaTimeSeconds: 0.02, out var waitingOutput); AssertResult( waitingResult, FleetCoordinationCycleResult.WaitingForState, "状态暂时不可用"); AssertStop(waitingOutput, layout.VehicleCount); if (!coordinator.IsActive) { throw new InvalidOperationException( "状态暂时不可用不应取消车队控制器。"); } var recoveredResult = coordinator.ExecuteCycle( CreateRigidMemberStates( layout, new Pose2D(0.5, 0.0, 0.0), new Twist2D(0.4, 0.0, 0.0), 1.02), targetTimestampSeconds: 1.02, deltaTimeSeconds: 0.02, out _); AssertResult( recoveredResult, FleetCoordinationCycleResult.CommandGenerated, "状态恢复"); } private static void VerifyUnsafeLayoutFaultsAndLatches() { var layout = CreateLayout(); var coordinator = CreateCoordinator(); coordinator.Start(layout, CreateTrajectory()); var deformedStates = new[] { CreateMemberState( 1, new Pose2D(-0.9, 0.0, 0.0), 1.0), CreateMemberState( 2, new Pose2D(0.9, 0.0, Math.PI), 1.0) }; var result = coordinator.ExecuteCycle( deformedStates, targetTimestampSeconds: 1.0, deltaTimeSeconds: 0.02, out var output); AssertResult( result, FleetCoordinationCycleResult.Faulted, "布局误差超限"); AssertStop(output, layout.VehicleCount); if (!coordinator.IsFaulted || string.IsNullOrWhiteSpace( coordinator.LastFailureReason)) { throw new InvalidOperationException( "布局误差故障没有被锁存。"); } var latchedResult = coordinator.ExecuteCycle( CreateRigidMemberStates( layout, new Pose2D(0.5, 0.0, 0.0), new Twist2D(0.4, 0.0, 0.0), 1.02), targetTimestampSeconds: 1.02, deltaTimeSeconds: 0.02, out var latchedOutput); AssertResult( latchedResult, FleetCoordinationCycleResult.Faulted, "布局误差故障锁存"); AssertStop(latchedOutput, layout.VehicleCount); } private static void VerifyCompletionStops() { var layout = CreateLayout(); var coordinator = CreateCoordinator(); coordinator.Start(layout, CreateTrajectory()); var result = coordinator.ExecuteCycle( CreateRigidMemberStates( layout, new Pose2D(1.0, 0.0, 0.0), Twist2D.Zero, 1.0), targetTimestampSeconds: 1.0, deltaTimeSeconds: 0.02, out var output); AssertResult( result, FleetCoordinationCycleResult.Completed, "车队轨迹完成"); AssertStop(output, layout.VehicleCount); if (!coordinator.IsCompleted) { throw new InvalidOperationException( "车队轨迹完成状态没有被保存。"); } } private static void VerifyTrackingFaultStops() { var layout = CreateLayout(); var coordinator = CreateCoordinator(); coordinator.Start(layout, CreateTrajectory()); var result = coordinator.ExecuteCycle( CreateRigidMemberStates( layout, new Pose2D(0.5, 0.5, 0.0), Twist2D.Zero, 1.0), targetTimestampSeconds: 1.0, deltaTimeSeconds: 0.02, out var output); AssertResult( result, FleetCoordinationCycleResult.Faulted, "车队中心跟踪故障"); AssertStop(output, layout.VehicleCount); if (!coordinator.IsFaulted) { throw new InvalidOperationException( "车队中心跟踪故障没有传递到协调器。"); } } private static void VerifyCancelReturnsInactive() { var layout = CreateLayout(); var coordinator = CreateCoordinator(); coordinator.Start(layout, CreateTrajectory()); coordinator.Cancel(); var result = coordinator.ExecuteCycle( CreateRigidMemberStates( layout, new Pose2D(0.5, 0.0, 0.0), Twist2D.Zero, 1.0), targetTimestampSeconds: 1.0, deltaTimeSeconds: 0.02, out var output); AssertResult( result, FleetCoordinationCycleResult.Inactive, "取消车队协调"); AssertStop(output, layout.VehicleCount); } private static FleetCoordinator CreateCoordinator() { var estimator = new FleetStateEstimator( maximumMemberStateAgeSeconds: 0.25, maximumPositionDisagreementMeters: 0.5, maximumYawDisagreementRadians: AngleMath.DegreesToRadians(10.0)); var controller = new FleetController( new StraightLateralController(), new ReferenceLongitudinalController(), new GcpCommandAllocator(Math.PI / 4.0), virtualControlPointRadiusMeters: 0.5); var commandCorrector = 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)); return new FleetCoordinator( estimator, controller, commandCorrector, memberPositionErrorWarningMeters: 0.02, maximumMemberPositionErrorMeters: 0.05, memberYawErrorWarningRadians: AngleMath.DegreesToRadians(1.0), maximumMemberYawErrorRadians: AngleMath.DegreesToRadians(3.0)); } private static FleetLayout CreateLayout() { return new FleetLayout( new[] { new VehicleLayout( 1, new Pose2D(-1.0, 0.0, 0.0)), new VehicleLayout( 2, new Pose2D(1.0, 0.0, Math.PI)) }); } private static Trajectory2D CreateTrajectory() { return new Trajectory2D( new[] { new TrajectoryPoint( 0.0, Pose2D.Identity, 0.0, 0.4), new TrajectoryPoint( 1.0, new Pose2D(1.0, 0.0, 0.0), 0.0, 0.4) }); } private static FleetMemberStateSample[] CreateRigidMemberStates( FleetLayout layout, Pose2D fleetPoseInWorld, Twist2D twistAtFleetOriginInWorld, double timestampSeconds) { var states = new FleetMemberStateSample[layout.VehicleCount]; for (var index = 0; index < layout.Vehicles.Count; index++) { var vehicle = layout.Vehicles[index]; var poseInWorld = FrameTransform2D.Compose( fleetPoseInWorld, vehicle.PoseInFleet); var offsetX = poseInWorld.XMeters - fleetPoseInWorld.XMeters; var offsetY = poseInWorld.YMeters - fleetPoseInWorld.YMeters; var twistInWorld = new Twist2D( twistAtFleetOriginInWorld.VxMetersPerSecond - twistAtFleetOriginInWorld.OmegaRadiansPerSecond * offsetY, twistAtFleetOriginInWorld.VyMetersPerSecond + twistAtFleetOriginInWorld.OmegaRadiansPerSecond * offsetX, twistAtFleetOriginInWorld.OmegaRadiansPerSecond); states[index] = new FleetMemberStateSample( vehicle.VehicleId, timestampSeconds, poseInWorld, twistInWorld, isStateAvailable: true, hasValidVelocityEstimate: true); } return states; } private static FleetMemberStateSample CreateMemberState( int vehicleId, Pose2D poseInWorld, double timestampSeconds) { return new FleetMemberStateSample( vehicleId, timestampSeconds, poseInWorld, Twist2D.Zero, isStateAvailable: true, hasValidVelocityEstimate: true); } private static FleetMemberCommand FindCommand( IReadOnlyList commands, int vehicleId) { for (var index = 0; index < commands.Count; index++) { if (commands[index].VehicleId == vehicleId) { return commands[index]; } } throw new InvalidOperationException( $"没有找到车辆{vehicleId}的成员命令。"); } private static void AssertStop( FleetCoordinationCycleOutput output, int expectedMemberCount) { AssertTwist( output.FleetCommand.TwistAtReferencePoint, 0.0, 0.0, 0.0, "车队停止命令"); if (output.MemberCommands.Count != expectedMemberCount) { throw new InvalidOperationException( "停止输出的成员命令数量错误。"); } if (output.BaseMemberCommands.Count != expectedMemberCount) { throw new InvalidOperationException( "停止输出的成员基础命令数量错误。"); } for (var index = 0; index < output.MemberCommands.Count; index++) { AssertTwist( output.BaseMemberCommands[index] .TwistInVehicleBody, 0.0, 0.0, 0.0, "成员基础停止命令"); AssertTwist( output.MemberCommands[index].TwistInVehicleBody, 0.0, 0.0, 0.0, "成员停止命令"); } } private static void AssertResult( FleetCoordinationCycleResult actual, FleetCoordinationCycleResult expected, string scenario) { if (actual != expected) { throw new InvalidOperationException( $"{scenario}结果错误:" + $"actual={actual}, expected={expected}。"); } } private static void AssertTwist( Twist2D actual, double expectedVx, double expectedVy, double expectedOmega, string scenario) { AssertNear( actual.VxMetersPerSecond, expectedVx, scenario + " Vx"); AssertNear( actual.VyMetersPerSecond, expectedVy, scenario + " Vy"); AssertNear( actual.OmegaRadiansPerSecond, expectedOmega, scenario + " Omega"); } private static void AssertNear( double actual, double expected, string valueName) { if (Math.Abs(actual - expected) > Tolerance) { throw new InvalidOperationException( $"{valueName}错误:" + $"actual={actual:F9}, expected={expected:F9}。"); } } private sealed class StraightLateralController : ILateralController { public LateralControlCommand Compute( PathTrackingContext context) { return LateralControlCommand.Straight; } public void Reset() { } } private sealed class ReferenceLongitudinalController : ILongitudinalController { public double ComputeSpeedMetersPerSecond( PathTrackingContext context) { return context.ControlReferenceSpeedMetersPerSecond; } public void Reset() { } } } }