using System; using System.Drawing; using System.Globalization; using System.Numerics; using System.Threading; using ClumsyCore; using ClumsyCore.DTools; using ClumsyCore.Interfaces; using ClumsyCore.Pilot; using CommonUsage.Chassis; using MultiWheelC.Control.Execution; using MultiWheelC.StateEstimation; using MyParking.Shared; namespace MultiWheelC { /// /// 统一读取轨迹实验的有符号横向偏移,并将车体局部偏移转换到世界坐标系。 /// internal static class TrajectoryExperimentInput { private const double MaximumOffsetCentimeters = 30.0; /// /// 从Clumsy输入框读取车体左正右负的横向偏移,单位转换为m。 /// public static bool TryReadLateralOffsetMeters( out double lateralOffsetMeters) { lateralOffsetMeters = 0.0; var input = UI.GetInput( "输入轨迹横向偏移(cm,左正右负,范围-30~30):"); var parsed = double.TryParse( input, NumberStyles.Float, CultureInfo.CurrentCulture, out var offsetCentimeters) || double.TryParse( input, NumberStyles.Float, CultureInfo.InvariantCulture, out offsetCentimeters); if (!parsed || double.IsNaN(offsetCentimeters) || double.IsInfinity(offsetCentimeters) || Math.Abs(offsetCentimeters) > MaximumOffsetCentimeters) { Console.WriteLine( "轨迹横向偏移必须是-30~30cm之间的有限数值,测试未启动。"); return false; } lateralOffsetMeters = offsetCentimeters / 100.0; return true; } /// /// 沿初始车体左方向平移参考轨迹起点,同时保持世界坐标航向不变。 /// public static Pose2D OffsetPoseLaterally( Pose2D poseInWorld, double lateralOffsetMeters) { var yawRadians = poseInWorld.YawRadians; return new Pose2D( poseInWorld.XMeters - Math.Sin(yawRadians) * lateralOffsetMeters, poseInWorld.YMeters + Math.Cos(yawRadians) * lateralOffsetMeters, yawRadians); } /// /// 生成带毫米偏移标识的实验轨迹名称。 /// public static string BuildTrajectoryName( string baseName, double lateralOffsetMeters) { return baseName + "_Offset" + (lateralOffsetMeters * 1000.0) .ToString("+0;-0;0", CultureInfo.InvariantCulture) + "mm"; } } /// /// 从当前Detour位姿开始执行新版控制器4m直线跟踪并保存实验数据。 /// [MovementTest(name = "新版控制器:4m直线轨迹跟踪")] public class NewControllerStraight4mTest : MovementTest { private const float MillimetersPerMeter = 1000f; private readonly Painter _painter = UI.GetPainter("NewControllerStraight4m"); private DriveTask _task; private TrackingExperimentRecorder _recorder; private IVehicleStateProvider _stateProvider; /// /// 获取或设置本次测试编号,用于区分重复实验CSV。 /// public int TrialNumber = 1; /// /// 获取或设置4m直线的巡航参考速度,单位为m/s。 /// public double CruiseSpeedMetersPerSecond = 0.40; /// /// 获取或设置参考速度加速度,单位为m/s²。 /// public double AccelerationMetersPerSecondSquared = 0.20; /// /// 获取或设置参考速度减速度,单位为m/s²。 /// public double DecelerationMetersPerSecondSquared = 0.08; /// /// 获取或设置离散轨迹点间距,单位为m。 /// public double PointSpacingMeters = 0.02; /// /// 获取实验记录使用的轨迹基础名称,供同一套直线测试流程区分前进和倒车。 /// protected virtual string ExperimentTrajectoryBaseName => "ProfiledStraight4m"; /// /// 获取直线主运动方向相对车头的夹角,单位为rad。 /// protected virtual double MotionDirectionInBodyRadians => 0.0; /// /// 获取是否由生成后的轨迹自动推导底盘运动坐标系方向。 /// protected virtual bool ResolveMotionDirectionFromTrajectory => false; /// /// 获取轨迹完成后是否需要将舵轮主动恢复到车头方向。 /// protected virtual bool ReturnWheelsForwardAfterCompletion => false; /// /// 读取当前位姿、绘制离散轨迹并启动新版轨迹跟踪动作。 /// public override void Test() { if (_task != null) { Console.WriteLine( "新版4m直线轨迹测试已经在运行,请先停止当前测试。"); 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); _stateProvider = null; return; } _stateProvider = stateProvider; var trajectoryStartPose = TrajectoryExperimentInput.OffsetPoseLaterally( initialState.PoseInWorld, lateralOffsetMeters); var trajectory = TestTrajectoryFactory.CreateStraight4Meters( trajectoryStartPose, CruiseSpeedMetersPerSecond, AccelerationMetersPerSecondSquared, DecelerationMetersPerSecondSquared, PointSpacingMeters, MotionDirectionInBodyRadians); DrawTrajectory(trajectory); var referenceStart = ToMillimeterVector( trajectory.StartPoint.PoseInWorld); var referenceEnd = ToMillimeterVector( trajectory.EndPoint.PoseInWorld); var recorder = new TrackingExperimentRecorder( controllerName: "NewStanleyPid", trajectoryName: TrajectoryExperimentInput.BuildTrajectoryName( ExperimentTrajectoryBaseName, lateralOffsetMeters), trialNumber: TrialNumber, referenceStart: referenceStart, referenceEnd: referenceEnd, referenceSpeed: (float)CruiseSpeedMetersPerSecond, sampleIntervalMs: 50, referenceMotionFrameYawDegrees: (float)AngleMath.RadiansToDegrees( MotionDirectionInBodyRadians), referenceAccelerationMetersPerSecondSquared: (float)AccelerationMetersPerSecondSquared, referenceDecelerationMetersPerSecondSquared: (float)DecelerationMetersPerSecondSquared, diagnosticChassis: chassis, diagnosticStateProvider: stateProvider); _recorder = recorder; var controlPointRadiusMeters = chassis.ControlPointRadius / MillimetersPerMeter; var movement = new TrajectoryTrackingMovement { Trajectory = trajectory, StateProvider = _stateProvider, MotionDirectionInBodyRadians = ResolveMotionDirectionFromTrajectory ? (double?)null : MotionDirectionInBodyRadians, ReturnWheelsForwardAfterCompletion = ReturnWheelsForwardAfterCompletion, CycleObserver = controller => RecordControlCycle( recorder, controller, controlPointRadiusMeters, _stateProvider as WheelFeedbackVehicleStateProvider) }; recorder.Start(); try { _task = new DriveTask(movement.Get()); _task.Wait(); // 保留少量停车后原始Detour数据,并生成一帧处理后的静止状态。 Thread.Sleep(400); recorder.UpdateCommand(0f, 0f); if (_stateProvider.TryGetState( out var stoppedState)) { recorder.UpdateProcessedState( stoppedState); } } finally { _task?.Stop(); recorder.UpdateCommand(0f, 0f); recorder.StopAndSave(); _painter.Clear(); _task = null; _recorder = null; _stateProvider = null; } } /// /// 停止正在运行的测试、保存已有数据并清除Clumsy轨迹可视化。 /// public override void TestStop() { _task?.Stop(); _recorder?.UpdateCommand(0f, 0f); _recorder?.StopAndSave(); _painter.Clear(); _task = null; _recorder = null; _stateProvider = null; } /// /// 将离散轨迹点和相邻线段从SI单位转换为Clumsy毫米坐标后绘制。 /// private void DrawTrajectory( Trajectory.Trajectory2D trajectory) { _painter.Clear(); for (var index = 0; index < trajectory.Count; index++) { var point = ToMillimeterVector( trajectory[index].PoseInWorld); _painter.DrawDot( Color.Cyan, point.X, point.Y, 3f); if (index == 0) { continue; } var previousPoint = ToMillimeterVector( trajectory[index - 1].PoseInWorld); _painter.DrawLine( Color.DeepSkyBlue, previousPoint.X, previousPoint.Y, point.X, point.Y, width: 2); } var start = ToMillimeterVector( trajectory.StartPoint.PoseInWorld); var end = ToMillimeterVector( trajectory.EndPoint.PoseInWorld); _painter.DrawDot( Color.LimeGreen, start.X, start.Y, 8f); _painter.DrawDot( Color.OrangeRed, end.X, end.Y, 8f); } /// /// 将控制器本周期使用的状态和最终GCP命令同步给实验记录器。 /// private static void RecordControlCycle( TrackingExperimentRecorder recorder, ParkingGeometricController controller, double controlPointRadiusMeters, WheelFeedbackVehicleStateProvider stateProvider) { if (controller.LastCycleTiming.HasValue) { recorder.RecordControlCycleTiming( controller.LastCycleTiming.Value, requestedCommand: controller.LastRequestedCommand, sentCommand: controller.LastCommand); } if (controller.LastVehicleState.HasValue) { recorder.UpdateProcessedState( controller.LastVehicleState.Value); } UpdateVelocityDiagnostics( recorder, stateProvider); 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, controller.LastCurvaturePreviewDistanceMeters ?? 0.0, controller.LastFeedforwardCurvaturePerMeter ?? projection.ReferencePoint.CurvaturePerMeter); } var requestedCommand = controller.LastRequestedCommand ?? controller.LastCommand.Value; var command = controller.LastCommand.Value; recorder.UpdateGcpCommand( requestedCommand.FrontAngleRadians, requestedCommand.RearAngleRadians, command.FrontAngleRadians, command.RearAngleRadians); var curvaturePerMeter = Math.Tan( command.FrontAngleRadians) / controlPointRadiusMeters; var angularSpeedRadiansPerSecond = command.SpeedMetersPerSecond * curvaturePerMeter; recorder.UpdateCommand( (float)command.SpeedMetersPerSecond, (float)angularSpeedRadiansPerSecond); } /// /// 将同一周期的Detour速度和轮速解算速度写入实验记录器。 /// private static void UpdateVelocityDiagnostics( TrackingExperimentRecorder recorder, WheelFeedbackVehicleStateProvider stateProvider) { if (stateProvider == null || !stateProvider.TryGetLatestVelocityDiagnostics( out var detourBodyVx, out var detourVelocityValid, out var rawWheelBodyVx, out var filteredWheelBodyVx, out var rawWheelBodyVy, out var filteredWheelBodyVy, out var wheelVelocityValid)) { return; } recorder.UpdateVelocityDiagnostics( detourBodyVx, detourVelocityValid, rawWheelBodyVx, filteredWheelBodyVx, rawWheelBodyVy, filteredWheelBodyVy, wheelVelocityValid); } /// /// 将Shared世界坐标系米制位姿转换为Clumsy绘图和旧记录器使用的毫米坐标。 /// private static Vector2 ToMillimeterVector( Pose2D poseInWorld) { return new Vector2( (float)( poseInWorld.XMeters * MillimetersPerMeter), (float)( poseInWorld.YMeters * MillimetersPerMeter)); } } /// /// 从当前Detour位姿开始,沿车体后方执行新版控制器4m直线倒车跟踪并保存实验数据。 /// [MovementTest(name = "新版控制器:4m直线倒车轨迹跟踪")] public sealed class NewControllerReverseStraight4mTest : NewControllerStraight4mTest { /// /// 使用负参考速度,使轨迹工厂沿车尾方向生成轨迹并触发倒车控制语义。 /// public NewControllerReverseStraight4mTest() { CruiseSpeedMetersPerSecond = -0.40; } /// /// 将倒车实验与前进直线实验的CSV名称明确区分。 /// protected override string ExperimentTrajectoryBaseName => "ProfiledReverseStraight4m"; } /// /// 将舵轮准备到车体左前45°,以0.4m/s跟踪4m直线,停车后再恢复车头方向。 /// [MovementTest(name = "新版控制器:45°蟹行4m直线轨迹跟踪")] public sealed class NewControllerCrab45Straight4mTest : NewControllerStraight4mTest { /// /// 使用车体左前45°作为本次直线轨迹的固定运动方向。 /// protected override double MotionDirectionInBodyRadians => Math.PI / 4.0; /// /// 只用45°定义参考轨迹,底盘β由轨迹切线和车身参考航向自动推导。 /// protected override bool ResolveMotionDirectionFromTrajectory => true; /// /// 蟹行轨迹正常完成后主动将四个舵轮恢复到车头方向。 /// protected override bool ReturnWheelsForwardAfterCompletion => true; /// /// 将45°蟹行实验与普通前进和倒车实验的CSV名称明确区分。 /// protected override string ExperimentTrajectoryBaseName => "ProfiledCrab45Straight4m"; } /// /// 从当前Detour位姿开始执行“3m直线—左半圆—3m直线”新版控制器跟踪实验。 /// [MovementTest(name = "新版控制器:直线-左半圆-直线轨迹跟踪")] public class NewControllerStraightSemicircleStraightTest : MovementTest { private const float MillimetersPerMeter = 1000f; private readonly Painter _painter = UI.GetPainter( "NewControllerStraightSemicircleStraight"); private DriveTask _task; private TrackingExperimentRecorder _recorder; private IVehicleStateProvider _stateProvider; /// /// 获取或设置本次测试编号,用于区分重复实验CSV。 /// public int TrialNumber = 1; /// /// 获取或设置半圆前后两段直线的长度,单位为m。 /// public double StraightLengthMeters = 3.0; /// /// 获取或设置左转半圆的转弯半径,单位为m。 /// public double TurnRadiusMeters = 2.0; /// /// 获取或设置直线与等曲率转弯之间的曲率过渡长度,单位为m。 /// public double CurvatureTransitionLengthMeters = 0.70; /// /// 获取或设置两段直线的最大参考速度,单位为m/s。 /// public double StraightMaximumSpeedMetersPerSecond = 0.40; /// /// 获取或设置半圆段的最大参考速度,单位为m/s。 /// public double SemicircleMaximumSpeedMetersPerSecond = 0.30; /// /// 获取或设置参考速度加速度,单位为m/s²。 /// public double AccelerationMetersPerSecondSquared = 0.20; /// /// 获取或设置参考速度减速度,单位为m/s²。 /// public double DecelerationMetersPerSecondSquared = 0.08; /// /// 获取或设置离散轨迹点间距,单位为m。 /// public double PointSpacingMeters = 0.02; /// /// 获取组合轨迹主运动方向相对车头的夹角,单位为rad。 /// protected virtual double MotionDirectionInBodyRadians => 0.0; /// /// 获取是否由生成后的轨迹自动推导底盘运动坐标系方向。 /// protected virtual bool ResolveMotionDirectionFromTrajectory => false; /// /// 获取轨迹完成后是否需要将舵轮主动恢复到车头方向。 /// protected virtual bool ReturnWheelsForwardAfterCompletion => false; /// /// 获取实验记录使用的轨迹基础名称。 /// protected virtual string ExperimentTrajectoryBaseName => "ProfiledStraightSmoothLeftTurnStraight"; /// /// 读取当前位姿、绘制组合轨迹并启动新版轨迹跟踪动作。 /// 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); _stateProvider = null; return; } _stateProvider = stateProvider; var trajectoryStartPose = TrajectoryExperimentInput.OffsetPoseLaterally( initialState.PoseInWorld, lateralOffsetMeters); var trajectory = TestTrajectoryFactory .CreateStraightLeftSemicircleStraight( trajectoryStartPose, StraightLengthMeters, TurnRadiusMeters, CurvatureTransitionLengthMeters, StraightMaximumSpeedMetersPerSecond, SemicircleMaximumSpeedMetersPerSecond, AccelerationMetersPerSecondSquared, DecelerationMetersPerSecondSquared, PointSpacingMeters, MotionDirectionInBodyRadians); DrawTrajectory(trajectory); var referenceStart = ToMillimeterVector( trajectory.StartPoint.PoseInWorld); var referenceEnd = ToMillimeterVector( trajectory.EndPoint.PoseInWorld); var recorder = new TrackingExperimentRecorder( controllerName: "NewStanleyPid", trajectoryName: TrajectoryExperimentInput.BuildTrajectoryName( ExperimentTrajectoryBaseName, lateralOffsetMeters), trialNumber: TrialNumber, referenceStart: referenceStart, referenceEnd: referenceEnd, referenceSpeed: (float)StraightMaximumSpeedMetersPerSecond, sampleIntervalMs: 50, referenceMotionFrameYawDegrees: (float)AngleMath.RadiansToDegrees( MotionDirectionInBodyRadians), referenceAccelerationMetersPerSecondSquared: (float)AccelerationMetersPerSecondSquared, referenceDecelerationMetersPerSecondSquared: (float)DecelerationMetersPerSecondSquared, diagnosticChassis: chassis, diagnosticStateProvider: stateProvider); _recorder = recorder; var controlPointRadiusMeters = chassis.ControlPointRadius / MillimetersPerMeter; var movement = new TrajectoryTrackingMovement { Trajectory = trajectory, StateProvider = _stateProvider, MotionDirectionInBodyRadians = ResolveMotionDirectionFromTrajectory ? (double?)null : MotionDirectionInBodyRadians, ReturnWheelsForwardAfterCompletion = ReturnWheelsForwardAfterCompletion, CycleObserver = controller => RecordControlCycle( recorder, controller, controlPointRadiusMeters, _stateProvider as WheelFeedbackVehicleStateProvider) }; recorder.Start(); try { _task = new DriveTask(movement.Get()); _task.Wait(); // 保留少量停车后原始Detour数据,并生成一帧处理后的静止状态。 Thread.Sleep(400); recorder.UpdateCommand(0f, 0f); if (_stateProvider.TryGetState( out var stoppedState)) { recorder.UpdateProcessedState( stoppedState); } } finally { _task?.Stop(); recorder.UpdateCommand(0f, 0f); recorder.StopAndSave(); _painter.Clear(); _task = null; _recorder = null; _stateProvider = null; } } /// /// 停止组合轨迹测试、保存已有数据并清除Clumsy轨迹可视化。 /// public override void TestStop() { _task?.Stop(); _recorder?.UpdateCommand(0f, 0f); _recorder?.StopAndSave(); _painter.Clear(); _task = null; _recorder = null; _stateProvider = null; } /// /// 将组合轨迹的离散点和相邻线段转换为Clumsy毫米坐标后绘制。 /// private void DrawTrajectory( Trajectory.Trajectory2D trajectory) { _painter.Clear(); for (var index = 0; index < trajectory.Count; index++) { var point = ToMillimeterVector( trajectory[index].PoseInWorld); _painter.DrawDot( Color.Cyan, point.X, point.Y, 3f); if (index == 0) { continue; } var previousPoint = ToMillimeterVector( trajectory[index - 1].PoseInWorld); _painter.DrawLine( Color.DeepSkyBlue, previousPoint.X, previousPoint.Y, point.X, point.Y, width: 2); } var start = ToMillimeterVector( trajectory.StartPoint.PoseInWorld); var end = ToMillimeterVector( trajectory.EndPoint.PoseInWorld); _painter.DrawDot( Color.LimeGreen, start.X, start.Y, 8f); _painter.DrawDot( Color.OrangeRed, end.X, end.Y, 8f); } /// /// 将组合轨迹控制周期的状态、参考量和最终GCP命令同步给实验记录器。 /// private static void RecordControlCycle( TrackingExperimentRecorder recorder, ParkingGeometricController controller, double controlPointRadiusMeters, WheelFeedbackVehicleStateProvider stateProvider) { if (controller.LastCycleTiming.HasValue) { recorder.RecordControlCycleTiming( controller.LastCycleTiming.Value, requestedCommand: controller.LastRequestedCommand, sentCommand: controller.LastCommand); } if (controller.LastVehicleState.HasValue) { recorder.UpdateProcessedState( controller.LastVehicleState.Value); } UpdateVelocityDiagnostics( recorder, stateProvider); 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, controller.LastCurvaturePreviewDistanceMeters ?? 0.0, controller.LastFeedforwardCurvaturePerMeter ?? projection.ReferencePoint.CurvaturePerMeter); } var requestedCommand = controller.LastRequestedCommand ?? controller.LastCommand.Value; var command = controller.LastCommand.Value; recorder.UpdateGcpCommand( requestedCommand.FrontAngleRadians, requestedCommand.RearAngleRadians, command.FrontAngleRadians, command.RearAngleRadians); var curvaturePerMeter = Math.Tan( command.FrontAngleRadians) / controlPointRadiusMeters; var angularSpeedRadiansPerSecond = command.SpeedMetersPerSecond * curvaturePerMeter; recorder.UpdateCommand( (float)command.SpeedMetersPerSecond, (float)angularSpeedRadiansPerSecond); } /// /// 将同一周期的Detour速度和轮速解算速度写入实验记录器。 /// private static void UpdateVelocityDiagnostics( TrackingExperimentRecorder recorder, WheelFeedbackVehicleStateProvider stateProvider) { if (stateProvider == null || !stateProvider.TryGetLatestVelocityDiagnostics( out var detourBodyVx, out var detourVelocityValid, out var rawWheelBodyVx, out var filteredWheelBodyVx, out var rawWheelBodyVy, out var filteredWheelBodyVy, out var wheelVelocityValid)) { return; } recorder.UpdateVelocityDiagnostics( detourBodyVx, detourVelocityValid, rawWheelBodyVx, filteredWheelBodyVx, rawWheelBodyVy, filteredWheelBodyVy, wheelVelocityValid); } /// /// 将Shared世界坐标系米制位姿转换为Clumsy绘图和记录器使用的毫米坐标。 /// private static Vector2 ToMillimeterVector( Pose2D poseInWorld) { return new Vector2( (float)( poseInWorld.XMeters * MillimetersPerMeter), (float)( poseInWorld.YMeters * MillimetersPerMeter)); } } /// /// 将舵轮准备到车体左前45°,跟踪直线—左半圆—直线轨迹,并在停车后恢复车头方向。 /// [MovementTest(name = "新版控制器:45°蟹行直线-左半圆-直线轨迹跟踪")] public sealed class NewControllerCrab45StraightSemicircleStraightTest : NewControllerStraightSemicircleStraightTest { /// /// 使用车体左前45°作为组合轨迹的固定运动方向。 /// protected override double MotionDirectionInBodyRadians => Math.PI / 4.0; /// /// 只用45°定义参考轨迹,底盘β由整段轨迹自动推导并检查一致性。 /// protected override bool ResolveMotionDirectionFromTrajectory => true; /// /// 蟹行组合轨迹正常完成后主动将四个舵轮恢复到车头方向。 /// protected override bool ReturnWheelsForwardAfterCompletion => true; /// /// 将45°蟹行组合实验与普通组合轨迹实验的CSV名称明确区分。 /// protected override string ExperimentTrajectoryBaseName => "ProfiledCrab45StraightSmoothLeftTurnStraight"; } }