Files
ParkingRobot/MultiWheelC/Experiments/CompositeMotionPlanTests.cs
T

367 lines
13 KiB
C#

using System;
using System.Drawing;
using System.Numerics;
using ClumsyCore;
using ClumsyCore.DTools;
using ClumsyCore.Interfaces;
using ClumsyCore.Pilot;
using CommonUsage.Chassis;
using MultiWheelC.Control.Execution;
using MultiWheelC.StateEstimation;
using MultiWheelC.Trajectory;
using MyParking.Shared;
namespace MultiWheelC
{
/// <summary>
/// 测试平滑左转轨迹、停车原地左转90°和再次直行的组合运动执行过程。
/// </summary>
[MovementTest(name = "新版控制器:直线-圆弧-折线组合测试")]
public sealed class CompositeStopTurnGoTest : MovementTest
{
private const float MillimetersPerMeter = 1000f;
private readonly Painter _painter =
UI.GetPainter("CompositeStopTurnGoTest");
private DriveTask _task;
private TrackingExperimentRecorder _recorder;
public int TrialNumber = 1; // 重复实验编号。
public double StraightLengthMeters = 2.0; // 圆弧前后直线长度,单位m。
public double TurnRadiusMeters = 2.0; // 平滑左转名义半径,单位m。
public double TurnAngleDegrees = 90.0; // 含过渡段在内的总左转角度。
public double CurvatureTransitionLengthMeters = 0.80; // 单侧过渡长度,单位m。
public double InPlaceLeftTurnDegrees = 90.0; // 停车后的原地左转角度。
public double FinalStraightLengthMeters = 4.5; // 自转后的直线长度,单位m。
public double StraightMaximumSpeedMetersPerSecond = 0.40; // 直线限速。
public double CurveMaximumSpeedMetersPerSecond = 0.30; // 转弯和过渡段限速。
public double AccelerationMetersPerSecondSquared = 0.20; // 参考加速度。
public double DecelerationMetersPerSecondSquared = 0.08; // 参考减速度。
public double PointSpacingMeters = 0.02; // 离散轨迹点间距。
/// <summary>
/// 从当前Detour位姿构造完整计划并依次执行连续跟踪、原地自转和最终直线。
/// </summary>
public override void Test()
{
if (_task != null)
{
Console.WriteLine(
"曲线-停车自转-直线组合测试已经在运行。");
return;
}
if (!TrajectoryExperimentInput
.TryReadLateralOffsetMeters(
out var lateralOffsetMeters))
{
return;
}
var chassis = PilotDefinition.Chassis as MultiWheelChassis;
if (chassis == null)
{
Console.WriteLine(
"当前底盘不是MultiWheelChassis,无法执行组合运动测试。");
return;
}
var stateProvider =
ParkingVehicleStateProviderFactory.Create(
chassis);
if (!stateProvider.TryGetState(
out var initialState))
{
Console.WriteLine(
"无法读取组合运动起点状态:" +
stateProvider.LastFailureReason);
return;
}
var planStartPose =
TrajectoryExperimentInput.OffsetPoseLaterally(
initialState.PoseInWorld,
lateralOffsetMeters);
var firstTrajectory =
TestTrajectoryFactory
.CreateStraightSmoothLeftTurnStraight(
planStartPose,
StraightLengthMeters,
TurnRadiusMeters,
AngleMath.DegreesToRadians(
TurnAngleDegrees),
CurvatureTransitionLengthMeters,
StraightMaximumSpeedMetersPerSecond,
CurveMaximumSpeedMetersPerSecond,
AccelerationMetersPerSecondSquared,
DecelerationMetersPerSecondSquared,
PointSpacingMeters);
var firstStopPose =
firstTrajectory.EndPoint.PoseInWorld;
var finalStraightYawRadians =
AngleMath.NormalizeRadians(
firstStopPose.YawRadians +
AngleMath.DegreesToRadians(
InPlaceLeftTurnDegrees));
var finalStraightStartPose =
new Pose2D(
firstStopPose.XMeters,
firstStopPose.YMeters,
finalStraightYawRadians);
var finalTrajectory =
TestTrajectoryFactory.CreateStraight(
finalStraightStartPose,
FinalStraightLengthMeters,
StraightMaximumSpeedMetersPerSecond,
AccelerationMetersPerSecondSquared,
DecelerationMetersPerSecondSquared,
PointSpacingMeters);
DrawPlan(
firstTrajectory,
finalTrajectory,
firstStopPose);
var plan = new MotionPlanSegment[]
{
new TrackMotionPlanSegment(firstTrajectory)
{
// 中间停车点允许后续原地转向和末段跟踪继续收敛位置误差。
FinishDistanceMeters = 0.05,
FinishSpeedMetersPerSecond = 0.03,
FinishHeadingToleranceRadians =
AngleMath.DegreesToRadians(3.0)
},
new RotateInPlaceMotionPlanSegment(
finalStraightYawRadians),
new TrackMotionPlanSegment(finalTrajectory)
};
_recorder = new TrackingExperimentRecorder(
controllerName: "NewStanleyPidComposite",
trajectoryName:
TrajectoryExperimentInput.BuildTrajectoryName(
"SmoothTurnStopRotateStraight",
lateralOffsetMeters),
trialNumber: TrialNumber,
referenceStart: ToMillimeterVector(
firstTrajectory.StartPoint.PoseInWorld),
referenceEnd: ToMillimeterVector(
finalTrajectory.EndPoint.PoseInWorld),
referenceSpeed:
(float)StraightMaximumSpeedMetersPerSecond,
sampleIntervalMs: 50,
referenceAccelerationMetersPerSecondSquared:
(float)AccelerationMetersPerSecondSquared,
referenceDecelerationMetersPerSecondSquared:
(float)DecelerationMetersPerSecondSquared,
diagnosticChassis: chassis);
_recorder.Start();
var controlPointRadiusMeters =
chassis.ControlPointRadius /
MillimetersPerMeter;
var movement = new MotionPlanExecutor
{
Segments = plan,
StateProvider = stateProvider,
SegmentStarted = (index, segment) =>
{
_recorder?.ClearControlReference();
_recorder?.ClearGcpCommand();
_recorder?.UpdateCommand(0f, 0f);
Console.WriteLine(
$"组合运动开始第{index + 1}段:" +
segment.GetType().Name);
},
TrackingCycleObserver = (index, controller) =>
RecordTrackingCycle(
index,
controller,
controlPointRadiusMeters,
stateProvider),
RotationCommandObserver = (index, omega) =>
_recorder?.UpdateCommand(
0f,
(float)omega)
};
try
{
_task = new DriveTask(movement.Get());
_task.Wait();
}
finally
{
_task?.Stop();
_recorder?.UpdateCommand(0f, 0f);
_recorder?.StopAndSave();
_task = null;
_recorder = null;
_painter?.Clear();
}
}
/// <summary>
/// 停止组合运动、保存已有实验数据并清除计划轨迹。
/// </summary>
public override void TestStop()
{
_task?.Stop();
_recorder?.UpdateCommand(0f, 0f);
_recorder?.StopAndSave();
_task = null;
_recorder = null;
_painter?.Clear();
}
/// <summary>
/// 将两段轨迹和中间原地自转位置绘制到Clumsy界面。
/// </summary>
private void DrawPlan(
Trajectory2D firstTrajectory,
Trajectory2D finalTrajectory,
Pose2D rotationPoseInWorld)
{
_painter.Clear();
DrawTrajectory(
firstTrajectory,
Color.DeepSkyBlue);
DrawTrajectory(
finalTrajectory,
Color.Gold);
var rotationPoint =
ToMillimeterVector(rotationPoseInWorld);
_painter.DrawDot(
Color.Magenta,
rotationPoint.X,
rotationPoint.Y,
10f);
}
/// <summary>
/// 绘制一段离散世界坐标系轨迹。
/// </summary>
private void DrawTrajectory(
Trajectory2D trajectory,
Color color)
{
for (var index = 0;
index < trajectory.Count;
index++)
{
var point = ToMillimeterVector(
trajectory[index].PoseInWorld);
_painter.DrawDot(
color,
point.X,
point.Y,
3f);
if (index == 0)
{
continue;
}
var previousPoint = ToMillimeterVector(
trajectory[index - 1].PoseInWorld);
_painter.DrawLine(
color,
previousPoint.X,
previousPoint.Y,
point.X,
point.Y,
width: 2);
}
}
/// <summary>
/// 将轨迹控制周期使用的状态、参考误差和最终GCP命令写入记录器。
/// </summary>
private void RecordTrackingCycle(
int motionSegmentIndex,
ParkingGeometricController controller,
double controlPointRadiusMeters,
WheelFeedbackVehicleStateProvider stateProvider)
{
if (controller.LastCycleTiming.HasValue)
{
_recorder?.RecordControlCycleTiming(
controller.LastCycleTiming.Value,
motionSegmentIndex);
}
if (controller.LastVehicleState.HasValue)
{
_recorder?.UpdateProcessedState(
controller.LastVehicleState.Value);
}
if (stateProvider.TryGetLatestVelocityDiagnostics(
out var detourBodyVx,
out var detourVelocityValid,
out var rawWheelBodyVx,
out var filteredWheelBodyVx,
out var wheelVelocityValid))
{
_recorder?.UpdateVelocityDiagnostics(
detourBodyVx,
detourVelocityValid,
rawWheelBodyVx,
filteredWheelBodyVx,
wheelVelocityValid);
}
if (!controller.LastCommand.HasValue)
{
return;
}
if (controller.LastProjection.HasValue &&
controller.LastControlReferenceSpeedMetersPerSecond.HasValue)
{
var projection =
controller.LastProjection.Value;
_recorder?.UpdateControlReference(
projection.ArcLengthMeters,
controller
.LastControlReferenceSpeedMetersPerSecond.Value,
projection.LateralErrorMeters,
projection.HeadingErrorRadians,
projection.DistanceToTrajectoryMeters,
projection.RemainingDistanceMeters);
}
var command = controller.LastCommand.Value;
_recorder?.UpdateGcpCommand(
command.FrontAngleRadians,
command.RearAngleRadians);
var curvaturePerMeter = Math.Tan(
command.FrontAngleRadians) /
controlPointRadiusMeters;
var angularSpeedRadiansPerSecond =
command.SpeedMetersPerSecond *
curvaturePerMeter;
_recorder?.UpdateCommand(
(float)command.SpeedMetersPerSecond,
(float)angularSpeedRadiansPerSecond);
}
/// <summary>
/// 将米制世界位姿转换为Clumsy绘图和记录使用的毫米坐标。
/// </summary>
private static Vector2 ToMillimeterVector(
Pose2D poseInWorld)
{
return new Vector2(
(float)(poseInWorld.XMeters *
MillimetersPerMeter),
(float)(poseInWorld.YMeters *
MillimetersPerMeter));
}
}
}