Files
ParkingRobot/MultiWheelC.Tests/FleetControllerTests.cs
T

375 lines
11 KiB
C#

using System;
using MultiWheelC.Control.Abstractions;
using MultiWheelC.Control.Allocation;
using MultiWheelC.Fleet;
using MultiWheelC.Trajectory;
using MyParking.Shared;
namespace MultiWheelC.Tests
{
internal static class FleetControllerTests
{
private const double Tolerance = 1e-9;
public static void Run()
{
VerifyInactiveControllerStops();
VerifyStraightCommand();
VerifyFortyFiveDegreeMotionDirection();
VerifyNinetyDegreeMotionDirection();
VerifyInvalidVelocityUsesReferenceSpeed();
VerifyCompletionStops();
VerifyExcessiveTrackingErrorFaults();
VerifyCancelStops();
Console.WriteLine(
"FleetController车队中心控制测试通过。共8个场景。");
}
private static void VerifyInactiveControllerStops()
{
var controller = CreateController();
var result = controller.ComputeCommand(
CreateState(0.5, 0.0, 0.0, true),
0.02,
out var command);
AssertResult(
result,
FleetControlCycleResult.Inactive,
"未启动控制器");
AssertStop(command, "未启动控制器");
}
private static void VerifyStraightCommand()
{
var controller = CreateController();
controller.Start(CreateStraightTrajectory());
var result = controller.ComputeCommand(
CreateState(0.5, 0.0, 0.4, true),
0.02,
out var command);
AssertResult(
result,
FleetControlCycleResult.CommandGenerated,
"直线控制");
AssertNear(
command.ReferencePointInFleet.XMeters,
0.0,
"直线控制参考点X");
AssertNear(
command.ReferencePointInFleet.YMeters,
0.0,
"直线控制参考点Y");
AssertTwist(
command.TwistAtReferencePoint,
0.4,
0.0,
0.0,
"直线控制");
}
private static void VerifyInvalidVelocityUsesReferenceSpeed()
{
var controller = CreateController();
controller.Start(CreateStraightTrajectory());
var result = controller.ComputeCommand(
CreateState(0.5, 0.0, 0.0, false),
0.02,
out var command);
AssertResult(
result,
FleetControlCycleResult.CommandGenerated,
"速度尚未初始化");
AssertTwist(
command.TwistAtReferencePoint,
0.4,
0.0,
0.0,
"速度尚未初始化");
}
private static void VerifyFortyFiveDegreeMotionDirection()
{
VerifyMotionDirection(
Math.PI / 4.0,
"45度运动方向");
}
private static void VerifyNinetyDegreeMotionDirection()
{
VerifyMotionDirection(
Math.PI / 2.0,
"90度运动方向");
}
private static void VerifyMotionDirection(
double motionDirectionInFleetRadians,
string scenario)
{
var controller = CreateController(
motionDirectionInFleetRadians);
controller.Start(
CreateStraightTrajectory(
motionDirectionInFleetRadians));
var directionX =
Math.Cos(motionDirectionInFleetRadians);
var directionY =
Math.Sin(motionDirectionInFleetRadians);
var result = controller.ComputeCommand(
CreateState(
0.5 * directionX,
0.5 * directionY,
0.4 * directionX,
0.4 * directionY,
true),
0.02,
out var command);
AssertResult(
result,
FleetControlCycleResult.CommandGenerated,
scenario);
AssertTwist(
command.TwistAtReferencePoint,
0.4 * directionX,
0.4 * directionY,
0.0,
scenario);
}
private static void VerifyCompletionStops()
{
var controller = CreateController();
controller.Start(CreateStraightTrajectory());
var result = controller.ComputeCommand(
CreateState(1.0, 0.0, 0.0, true),
0.02,
out var command);
AssertResult(
result,
FleetControlCycleResult.Completed,
"终点完成");
AssertStop(command, "终点完成");
if (controller.IsActive || !controller.IsCompleted)
{
throw new InvalidOperationException(
"终点完成后控制器状态错误。");
}
}
private static void VerifyExcessiveTrackingErrorFaults()
{
var controller = CreateController();
controller.Start(CreateStraightTrajectory());
var result = controller.ComputeCommand(
CreateState(0.5, 0.5, 0.0, true),
0.02,
out var command);
AssertResult(
result,
FleetControlCycleResult.Faulted,
"轨迹偏离保护");
AssertStop(command, "轨迹偏离保护");
if (string.IsNullOrWhiteSpace(
controller.LastFailureReason))
{
throw new InvalidOperationException(
"轨迹偏离故障没有保存原因。");
}
}
private static void VerifyCancelStops()
{
var controller = CreateController();
controller.Start(CreateStraightTrajectory());
controller.Cancel();
var result = controller.ComputeCommand(
CreateState(0.5, 0.0, 0.0, true),
0.02,
out var command);
AssertResult(
result,
FleetControlCycleResult.Inactive,
"取消控制");
AssertStop(command, "取消控制");
}
private static FleetController CreateController(
double motionDirectionInFleetRadians = 0.0)
{
return new FleetController(
new StraightLateralController(),
new ReferenceLongitudinalController(),
new GcpCommandAllocator(
Math.PI / 4.0),
virtualControlPointRadiusMeters: 0.5,
motionDirectionInFleetRadians:
motionDirectionInFleetRadians);
}
private static Trajectory2D CreateStraightTrajectory(
double motionDirectionInFleetRadians = 0.0)
{
var directionX =
Math.Cos(motionDirectionInFleetRadians);
var directionY =
Math.Sin(motionDirectionInFleetRadians);
return new Trajectory2D(
new[]
{
new TrajectoryPoint(
0.0,
Pose2D.Identity,
0.0,
0.4),
new TrajectoryPoint(
1.0,
new Pose2D(
directionX,
directionY,
0.0),
0.0,
0.4)
});
}
private static FleetState CreateState(
double xMeters,
double yMeters,
double vxMetersPerSecond,
bool hasValidVelocityEstimate)
{
return CreateState(
xMeters,
yMeters,
vxMetersPerSecond,
0.0,
hasValidVelocityEstimate);
}
private static FleetState CreateState(
double xMeters,
double yMeters,
double vxMetersPerSecond,
double vyMetersPerSecond,
bool hasValidVelocityEstimate)
{
return new FleetState(
sampleTimestampSeconds: 1.0,
fleetPoseInWorld: new Pose2D(
xMeters,
yMeters,
0.0),
twistAtFleetOriginInWorld: new Twist2D(
vxMetersPerSecond,
vyMetersPerSecond,
0.0),
hasValidVelocityEstimate:
hasValidVelocityEstimate);
}
private static void AssertResult(
FleetControlCycleResult actual,
FleetControlCycleResult expected,
string scenario)
{
if (actual != expected)
{
throw new InvalidOperationException(
$"{scenario}结果错误:" +
$"actual={actual}, expected={expected}。");
}
}
private static void AssertStop(
FleetMotionCommand command,
string scenario)
{
AssertTwist(
command.TwistAtReferencePoint,
0.0,
0.0,
0.0,
scenario);
}
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()
{
}
}
}
}