Files

698 lines
29 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.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using ClumsyCore;
using ClumsyCore.Interfaces;
using ClumsyCore.Pilot;
using CommonUsage.Chassis;
using MDCSToolBox.Clumsy.Pilot;
using MultiWheelC.Trajectory;
using MultiWheelC.TrajectoryPlanning.CoarsePath;
using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade;
using MultiWheelC.TrajectoryPlanning.EMPlanner;
using MultiWheelC.TrajectoryPlanning.TrajectoryObservation;
using MyParking.Shared;
using TrajectoryPlanningVisualization;
using PlanningPose2D = MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D;
namespace MultiWheelC;
/// <summary>规划一次并冻结首个 EM 方向段,再使用现有几何控制器完成实车状态反馈闭环。</summary>
[MovementTest(name = "EM闭环测试")]
public sealed class EmClosedLoopMovementTest : MovementTest
{
private const int MaximumManualObstacleCount = 20;
private static long nextObstacleSnapshotVersion;
public double GoalXmm = double.NaN;
public double GoalYmm = double.NaN;
public double GoalYawDeg;
public double MapPaddingMeters = 2d;
public float MapResolutionMm = 50f;
public double SolverTimeoutSeconds = 5d;
public int MaximumOsqpIterations = 100000;
public double OutputTimeStepSeconds = 0.10d;
public double VehicleLengthMeters = 0.80d;
public double VehicleWidthMeters = 0.60d;
public double SafetyMarginMeters = 0.05d;
public double MaximumCurvaturePerMeter = 1d / 1.20d;
public bool EnableWebVisualization = true;
public bool AutoOpenWebVisualization = true;
public int WebVisualizationPort;
public double WebRefreshRateHz = 10d;
public int VisualizationHistoryCycleLimit = 60;
public bool EnableNativePainterVisualization = true;
public double MaximumCommandSpeedMetersPerSecond = 1.00d;
public double MaximumDistanceToTrajectoryMeters = 0.30d;
public double ExecutionTimeoutSeconds = 120d;
public float WheelAlignmentToleranceDegrees = 2f;
/// <summary>读取冻结输入并启动一次规划、一次控制器接管的后台会话。</summary>
public override void Test()
{
try
{
double goalXMillimeters = GoalXmm;
double goalYMillimeters = GoalYmm;
double goalYawDegrees = GoalYawDeg;
if (!IsFinite(goalXMillimeters) || !IsFinite(goalYMillimeters))
{
goalXMillimeters = ReadFiniteInput("EM闭环终点 X(世界 mm");
goalYMillimeters = ReadFiniteInput("EM闭环终点 Y(世界 mm");
goalYawDegrees = ReadFiniteInput("EM闭环终点航向(世界 deg");
}
EnsureFinite(goalXMillimeters, nameof(GoalXmm));
EnsureFinite(goalYMillimeters, nameof(GoalYmm));
EnsureFinite(goalYawDegrees, nameof(GoalYawDeg));
EnsurePositive(MaximumCommandSpeedMetersPerSecond,
nameof(MaximumCommandSpeedMetersPerSecond));
EnsurePositive(MaximumDistanceToTrajectoryMeters,
nameof(MaximumDistanceToTrajectoryMeters));
EnsurePositive(ExecutionTimeoutSeconds, nameof(ExecutionTimeoutSeconds));
EnsurePositive(WheelAlignmentToleranceDegrees,
nameof(WheelAlignmentToleranceDegrees));
var settings = new TrajectoryObservationSettings
{
PlanningScope = EmPlanningScope.FullDirectionSegment,
MapPaddingMeters = MapPaddingMeters,
MapResolutionMillimeters = MapResolutionMm,
SolverTimeoutSeconds = SolverTimeoutSeconds,
MaximumOsqpIterations = MaximumOsqpIterations,
OutputTimeStepSeconds = OutputTimeStepSeconds,
VehicleLengthMeters = VehicleLengthMeters,
VehicleWidthMeters = VehicleWidthMeters,
SafetyMarginMeters = SafetyMarginMeters,
MaximumCurvaturePerMeter = MaximumCurvaturePerMeter,
EnableWebVisualization = EnableWebVisualization,
AutoOpenWebVisualization = AutoOpenWebVisualization,
WebVisualizationPort = WebVisualizationPort,
WebRefreshRateHz = WebRefreshRateHz,
VisualizationHistoryCycleLimit = VisualizationHistoryCycleLimit,
EnableNativePainterVisualization = EnableNativePainterVisualization,
}.CreateValidatedSnapshot();
IReadOnlyList<TrajectoryObservationObstacle> obstacles = ReadManualObstacles();
long obstacleSnapshotVersion = obstacles.Count == 0
? 0L
: Interlocked.Increment(ref nextObstacleSnapshotVersion);
var goal = new PlanningPose2D(
goalXMillimeters / 1000d,
goalYMillimeters / 1000d,
goalYawDegrees * Math.PI / 180d);
EmClosedLoopMovementTestRunner.Start(goal, settings, obstacles,
obstacleSnapshotVersion, MaximumCommandSpeedMetersPerSecond,
MaximumDistanceToTrajectoryMeters, ExecutionTimeoutSeconds,
WheelAlignmentToleranceDegrees);
}
catch (Exception exception)
{
EmClosedLoopMovementTestRunner.ShowFailure("测试未启动", exception);
}
}
/// <summary>取消尚未完成的规划并停止正在运行的控制任务。</summary>
public override void TestStop()
{
EmClosedLoopMovementTestRunner.Stop();
}
private static IReadOnlyList<TrajectoryObservationObstacle> ReadManualObstacles()
{
int count = ReadBoundedIntegerInput("EM闭环手动障碍物数量(0-20",
0, MaximumManualObstacleCount);
var obstacles = new List<TrajectoryObservationObstacle>(count);
for (int index = 0; index < count; index++)
{
string label = "障碍物 " + (index + 1).ToString(CultureInfo.InvariantCulture);
int kind = ReadBoundedIntegerInput(label + " 类型(1=圆形,2=轴对齐矩形)", 1, 2);
double centerXMillimeters = ReadFiniteInput(label + " 中心 X(世界 mm");
double centerYMillimeters = ReadFiniteInput(label + " 中心 Y(世界 mm");
if (kind == 1)
{
double radiusMillimeters = ReadPositiveFiniteInput(label + " 半径(mm");
obstacles.Add(TrajectoryObservationObstacle.Circle(
centerXMillimeters, centerYMillimeters, radiusMillimeters));
}
else
{
double lengthXMillimeters = ReadPositiveFiniteInput(label + " X 方向长度(mm");
double widthYMillimeters = ReadPositiveFiniteInput(label + " Y 方向宽度(mm");
obstacles.Add(TrajectoryObservationObstacle.Rectangle(
centerXMillimeters - lengthXMillimeters / 2d,
centerXMillimeters + lengthXMillimeters / 2d,
centerYMillimeters - widthYMillimeters / 2d,
centerYMillimeters + widthYMillimeters / 2d));
}
}
return obstacles;
}
private static int ReadBoundedIntegerInput(string prompt, int minimum, int maximum)
{
object raw = UI.GetInput(prompt);
string text = Convert.ToString(raw, CultureInfo.CurrentCulture);
int value;
if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out value) &&
!int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
{
throw new ArgumentException("输入必须是整数:" + prompt);
}
if (value < minimum || value > maximum)
throw new ArgumentOutOfRangeException(nameof(prompt), "输入超出允许范围:" + prompt);
return value;
}
private static double ReadPositiveFiniteInput(string prompt)
{
double value = ReadFiniteInput(prompt);
EnsurePositive(value, prompt);
return value;
}
private static double ReadFiniteInput(string prompt)
{
object raw = UI.GetInput(prompt);
string text = Convert.ToString(raw, CultureInfo.CurrentCulture);
double value;
if (!double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value) &&
!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value))
{
throw new ArgumentException("输入必须是有限数字:" + prompt);
}
EnsureFinite(value, prompt);
return value;
}
private static void EnsurePositive(double value, string name)
{
EnsureFinite(value, name);
if (value <= 0d)
throw new ArgumentOutOfRangeException(name, "输入必须是正数:" + name);
}
private static void EnsureFinite(double value, string name)
{
if (!IsFinite(value))
throw new ArgumentException("输入必须是有限数字:" + name);
}
private static bool IsFinite(double value)
{
return !double.IsNaN(value) && !double.IsInfinity(value);
}
}
/// <summary>拥有单次规划和单个控制任务的会话生命周期,并保证停止操作幂等。</summary>
internal static class EmClosedLoopMovementTestRunner
{
private static readonly object SessionSync = new object();
private static CancellationTokenSource activeCancellation;
private static Task activeTask;
private static DriveTask activeDriveTask;
private static TrajectoryObservationPresentation activePresentation;
private static TrajectoryObservationVisualizationPublisher activeWebPublisher;
private static long nextSessionId;
private static long activeSessionId;
private static long stateSequence;
internal static void Start(PlanningPose2D goal, TrajectoryObservationSettings settings,
IReadOnlyList<TrajectoryObservationObstacle> obstacles, long obstacleSnapshotVersion,
double maximumCommandSpeedMetersPerSecond,
double maximumDistanceToTrajectoryMeters,
double executionTimeoutSeconds,
float wheelAlignmentToleranceDegrees)
{
if (goal == null) throw new ArgumentNullException(nameof(goal));
if (settings == null) throw new ArgumentNullException(nameof(settings));
if (obstacles == null) throw new ArgumentNullException(nameof(obstacles));
var cancellation = new CancellationTokenSource();
CancellationTokenSource previousCancellation;
DriveTask previousDriveTask;
TrajectoryObservationPresentation previousPresentation;
TrajectoryObservationVisualizationPublisher previousWebPublisher;
long sessionId;
lock (SessionSync)
{
previousCancellation = activeCancellation;
previousDriveTask = activeDriveTask;
previousPresentation = activePresentation;
previousWebPublisher = activeWebPublisher;
activeCancellation = cancellation;
activeDriveTask = null;
activePresentation = null;
activeWebPublisher = null;
activeTask = null;
activeSessionId = sessionId = ++nextSessionId;
}
Cancel(previousCancellation);
StopDriveTask(previousDriveTask);
ClearPresentation(previousPresentation);
StopWebPublisher(previousWebPublisher);
PrintStatus("会话 " + sessionId.ToString(CultureInfo.InvariantCulture) +
" 已启动;车辆运动前将只规划一次。");
Task task = Task.Run(() => RunSessionAsync(sessionId, goal, settings,
obstacles, obstacleSnapshotVersion, maximumCommandSpeedMetersPerSecond,
maximumDistanceToTrajectoryMeters, executionTimeoutSeconds,
wheelAlignmentToleranceDegrees, cancellation.Token), cancellation.Token);
lock (SessionSync)
{
if (activeSessionId == sessionId)
activeTask = task;
}
_ = task.ContinueWith(completed => Finish(sessionId, cancellation, completed),
TaskScheduler.Default);
}
internal static void Stop()
{
CancellationTokenSource cancellation;
DriveTask driveTask;
TrajectoryObservationPresentation presentation;
TrajectoryObservationVisualizationPublisher webPublisher;
lock (SessionSync)
{
cancellation = activeCancellation;
driveTask = activeDriveTask;
presentation = activePresentation;
webPublisher = activeWebPublisher;
activeCancellation = null;
activeDriveTask = null;
activePresentation = null;
activeWebPublisher = null;
activeTask = null;
activeSessionId = 0L;
}
Cancel(cancellation);
StopDriveTask(driveTask);
ClearPresentation(presentation);
StopWebPublisher(webPublisher);
PrintStatus("已请求取消规划并停止控制任务。");
}
internal static void ShowFailure(string context, Exception exception)
{
PrintStatus(context + "" + (exception == null ? "未知错误" : exception.Message));
}
private static async Task RunSessionAsync(long sessionId, PlanningPose2D goal,
TrajectoryObservationSettings settings,
IReadOnlyList<TrajectoryObservationObstacle> obstacles,
long obstacleSnapshotVersion,
double maximumCommandSpeedMetersPerSecond,
double maximumDistanceToTrajectoryMeters,
double executionTimeoutSeconds,
float wheelAlignmentToleranceDegrees,
CancellationToken token)
{
token.ThrowIfCancellationRequested();
VehicleMotionState initialState = ReadPlanningState();
using var planningDeadline = new TrajectoryObservationPlanningDeadline(
TimeSpan.FromSeconds(settings.SolverTimeoutSeconds), token);
CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob(
initialState.Pose, goal, settings, obstacles, obstacleSnapshotVersion);
TrajectoryObservationBootstrapResult bootstrap =
new TrajectoryObservationBootstrapper().Bootstrap(job, planningDeadline, token);
if (!bootstrap.Succeeded)
throw new InvalidOperationException(bootstrap.FailureReason);
var controller = new TrajectoryObservationController(
bootstrap, settings, new EmPlanningService(new OsqpNativeSolver()),
"em-closed-loop-" + sessionId.ToString(CultureInfo.InvariantCulture));
PlanningCycleResult cycle = await controller.StartCycle(
initialState.CapturedAtUtc, initialState, planningDeadline, token).ConfigureAwait(false);
if (!cycle.Published || controller.PublishedTrajectory == null)
{
string reason = cycle.Result == null ? string.Empty : cycle.Result.FailureReason;
throw new InvalidOperationException(string.IsNullOrWhiteSpace(reason)
? cycle.Diagnostic
: reason);
}
EmTrajectory emTrajectory = controller.PublishedTrajectory;
Trajectory2D controlTrajectory = new EmControlTrajectoryAdapter().Create(emTrajectory);
TrajectoryObservationVisualizationPublisher webPublisher = StartWebVisualization(
sessionId, bootstrap, controller, settings, obstacleSnapshotVersion, token);
TrajectoryObservationPresentation presentation = settings.EnableNativePainterVisualization
? new TrajectoryObservationPresentation()
: null;
if (!TryRegisterPresentation(sessionId, presentation, token))
{
ClearPresentation(presentation);
token.ThrowIfCancellationRequested();
return;
}
UpdateVisualization(sessionId, presentation, bootstrap, controller, emTrajectory,
initialState, cycle, "规划已冻结,尚未向底盘下发控制指令。");
PrintStatus("规划完成:EM点数=" + emTrajectory.Points.Count.ToString(CultureInfo.InvariantCulture) +
",控制点数=" + controlTrajectory.Count.ToString(CultureInfo.InvariantCulture) +
",长度=" + controlTrajectory.TotalLengthMeters.ToString("F3", CultureInfo.InvariantCulture) +
" m,方向=" + emTrajectory.Metadata.Direction +
",终端=" + emTrajectory.Metadata.TerminalType + "。");
token.ThrowIfCancellationRequested();
EmClosedLoopWheelSafety.RequireWheelsForward(wheelAlignmentToleranceDegrees);
var movement = new TrajectoryTrackingMovement
{
Trajectory = controlTrajectory,
MaximumCommandSpeedMetersPerSecond = maximumCommandSpeedMetersPerSecond,
MaximumDistanceToTrajectoryMeters = maximumDistanceToTrajectoryMeters,
ExecutionTimeoutSeconds = executionTimeoutSeconds,
CycleObserver = control => UpdatePresentationFromControlCycle(
sessionId, presentation, bootstrap, controller, emTrajectory, cycle, control),
};
var driveTask = new DriveTask(movement.Get());
if (!TryRegisterDriveTask(sessionId, driveTask, token))
{
StopDriveTask(driveTask);
token.ThrowIfCancellationRequested();
return;
}
try
{
PrintStatus("冻结轨迹已交给现有控制器,开始单方向段闭环跟踪。");
driveTask.Wait();
if (emTrajectory.Metadata.TerminalType == EmTerminalType.GearSwitch)
PrintStatus("已在换向边界停车;本测试不启动下一方向段。");
else
PrintStatus("首个方向段已完成并停车。");
}
finally
{
StopDriveTask(driveTask);
ClearDriveTask(sessionId, driveTask);
}
}
private static void UpdatePresentationFromControlCycle(long sessionId,
TrajectoryObservationPresentation presentation,
TrajectoryObservationBootstrapResult bootstrap,
TrajectoryObservationController planningController,
EmTrajectory trajectory,
PlanningCycleResult cycle,
MultiWheelC.Control.Execution.ParkingGeometricController control)
{
if (control == null || !control.LastVehicleState.HasValue)
return;
var state = control.LastVehicleState.Value;
var planningState = new VehicleMotionState(
new PlanningPose2D(state.PoseInWorld.XMeters, state.PoseInWorld.YMeters,
state.PoseInWorld.YawRadians),
state.TwistInBody.VxMetersPerSecond, null, DateTimeOffset.UtcNow,
Interlocked.Increment(ref stateSequence));
string diagnostic = "现有控制器正在执行冻结轨迹";
if (control.LastCommand.HasValue)
{
var command = control.LastCommand.Value;
diagnostic += ":底盘速度=" +
command.SpeedMetersPerSecond.ToString("F3", CultureInfo.InvariantCulture) +
" m/s,前GCP=" +
(command.FrontAngleRadians * 180d / Math.PI).ToString("F2", CultureInfo.InvariantCulture) +
" deg,后GCP=" +
(command.RearAngleRadians * 180d / Math.PI).ToString("F2", CultureInfo.InvariantCulture) + " deg。";
}
UpdateVisualization(sessionId, presentation, bootstrap, planningController,
trajectory, planningState, cycle, diagnostic);
}
private static void UpdateVisualization(long sessionId,
TrajectoryObservationPresentation presentation,
TrajectoryObservationBootstrapResult bootstrap,
TrajectoryObservationController controller,
EmTrajectory trajectory,
VehicleMotionState state,
PlanningCycleResult cycle,
string diagnostic)
{
lock (SessionSync)
{
if (activeSessionId != sessionId)
return;
var observation = new TrajectoryObservationObservation(
DateTimeOffset.UtcNow, state, trajectory, null, null, null);
DirectionSegmentView segment = controller.ActiveSegment;
TrajectoryObservationCharts charts = TrajectoryObservationCharts.Build(
trajectory, segment,
controller.CreateEffectiveConfigurationSnapshot().Frenet.MaximumProjectionDistanceMeters);
if (presentation != null && ReferenceEquals(activePresentation, presentation))
{
presentation.DrawWorld(bootstrap, observation,
TrajectoryObservationRuntimeState.Create(DateTimeOffset.UtcNow, trajectory), diagnostic);
presentation.DrawLs(charts, diagnostic);
presentation.DrawSt(charts, diagnostic);
}
TrajectoryObservationVisualizationPublisher publisher = activeWebPublisher;
if (publisher != null)
{
var tick = new TrajectoryObservationLoopTick(observation, cycle, TimeSpan.Zero,
false, false, true, false, controller.SegmentState);
publisher.TryPublish(observation.ObservedAtUtc, () =>
{
PlanningVisualizationDynamicSnapshot source =
new TrajectoryObservationDynamicSnapshotBuilder().Build(
Interlocked.Increment(ref stateSequence), tick, segment,
controller.PreviousTrajectoryForVisualization, bootstrap.Vehicle,
controller.CreateEffectiveConfigurationSnapshot());
return new PlanningVisualizationDynamicSnapshot(source.Sequence,
source.ObservedAtUtc, "EM闭环控制中", source.ActiveSegmentIndex,
source.ActiveDirection, source.VehiclePose, source.DynamicPolylines,
source.DynamicMarkers, source.Charts, source.StatusValues, source.CycleSummary);
});
}
}
}
private static TrajectoryObservationVisualizationPublisher StartWebVisualization(
long sessionId, TrajectoryObservationBootstrapResult bootstrap,
TrajectoryObservationController controller, TrajectoryObservationSettings settings,
long obstacleSnapshotVersion, CancellationToken token)
{
if (!settings.EnableWebVisualization)
return null;
var publisher = new TrajectoryObservationVisualizationPublisher(settings,
new PlanningVisualizationSessionSink(), PrintStatus);
try
{
PlanningVisualizationStaticSnapshot source =
new TrajectoryObservationStaticSnapshotBuilder().Build(bootstrap,
controller.CreateEffectiveConfigurationSnapshot(), settings, obstacleSnapshotVersion);
var closedLoopSnapshot = new PlanningVisualizationStaticSnapshot("EM闭环测试",
source.WorldBounds, source.OccupancyGrid, source.StaticPolylines,
source.StaticMarkers, source.DirectionSegments, source.ConfigurationGroups);
PlanningVisualizationSessionInfo info = publisher.Start(closedLoopSnapshot);
if (info == null)
return null;
lock (SessionSync)
{
if (activeSessionId != sessionId || token.IsCancellationRequested)
{
publisher.Stop();
return null;
}
activeWebPublisher = publisher;
}
PrintStatus("网页可视化地址(含会话令牌):" + info.Uri.AbsoluteUri);
if (settings.AutoOpenWebVisualization)
{
Process.Start(new ProcessStartInfo
{
FileName = info.Uri.AbsoluteUri,
UseShellExecute = true,
});
}
return publisher;
}
catch (Exception exception)
{
publisher.Disable(exception);
PrintStatus("网页可视化未启动:" + exception.Message);
return null;
}
}
private static bool TryRegisterPresentation(long sessionId,
TrajectoryObservationPresentation presentation, CancellationToken token)
{
if (presentation == null)
return true;
lock (SessionSync)
{
if (activeSessionId != sessionId || activeCancellation == null ||
activeCancellation.IsCancellationRequested || token.IsCancellationRequested)
return false;
activePresentation = presentation;
return true;
}
}
private static VehicleMotionState ReadPlanningState()
{
var location = DetourInterface.getCartLocation();
if (location == null)
throw new InvalidOperationException("实时定位不可用。");
if (BasicPilotBase.Chassis == null)
throw new InvalidOperationException("实时底盘读接口不可用。");
var speed = BasicPilotBase.Chassis.GetCarSpeed(true);
return new VehicleMotionState(
new PlanningPose2D(location.x / 1000d, location.y / 1000d,
location.th * Math.PI / 180d),
speed.Vx, null, DateTimeOffset.UtcNow,
Interlocked.Increment(ref stateSequence));
}
private static bool TryRegisterDriveTask(long sessionId, DriveTask driveTask,
CancellationToken token)
{
lock (SessionSync)
{
if (activeSessionId != sessionId || activeCancellation == null ||
activeCancellation.IsCancellationRequested || token.IsCancellationRequested)
{
return false;
}
activeDriveTask = driveTask;
return true;
}
}
private static void ClearDriveTask(long sessionId, DriveTask driveTask)
{
lock (SessionSync)
{
if (activeSessionId == sessionId && ReferenceEquals(activeDriveTask, driveTask))
activeDriveTask = null;
ClearPresentation(activePresentation);
activePresentation = null;
StopWebPublisher(activeWebPublisher);
activeWebPublisher = null;
}
}
private static void Finish(long sessionId, CancellationTokenSource cancellation,
Task completed)
{
lock (SessionSync)
{
if (activeSessionId == sessionId)
{
activeCancellation = null;
activeDriveTask = null;
activeTask = null;
activeSessionId = 0L;
}
}
if (completed.IsFaulted)
{
Exception failure = completed.Exception == null
? null
: completed.Exception.GetBaseException();
ShowFailure("闭环会话失败并已停车", failure);
}
else if (completed.IsCanceled)
{
PrintStatus("闭环会话已取消。");
}
cancellation.Dispose();
}
private static void Cancel(CancellationTokenSource cancellation)
{
if (cancellation == null)
return;
try
{
cancellation.Cancel();
}
catch (ObjectDisposedException)
{
}
}
private static void StopDriveTask(DriveTask driveTask)
{
if (driveTask == null)
return;
try
{
driveTask.Stop();
}
catch (Exception exception)
{
ShowFailure("停止控制任务失败", exception);
}
}
private static void ClearPresentation(TrajectoryObservationPresentation presentation)
{
if (presentation == null)
return;
try
{
presentation.ClearAll();
}
catch (Exception exception)
{
ShowFailure("清理闭环可视化失败", exception);
}
}
private static void StopWebPublisher(TrajectoryObservationVisualizationPublisher publisher)
{
if (publisher == null)
return;
try
{
publisher.Stop();
}
catch (Exception exception)
{
ShowFailure("停止闭环网页可视化失败", exception);
}
}
private static void PrintStatus(string message)
{
Console.WriteLine("[EM闭环测试] " + message);
}
}
/// <summary>在控制器接管底盘前只读检查四个舵轮是否已经与车体前向对齐。</summary>
internal static class EmClosedLoopWheelSafety
{
internal static void RequireWheelsForward(float toleranceDegrees)
{
var chassis = PilotDefinition.Chassis as MultiWheelChassis;
if (chassis == null)
throw new InvalidOperationException("当前底盘不是 MultiWheelChassis。");
var adapter = new MultiWheelChassisAdapter(chassis, PilotDefinition.Self.CarNum);
double toleranceRadians = AngleMath.DegreesToRadians(toleranceDegrees);
if (!adapter.AreParallelWheelsAligned(0d, toleranceRadians))
{
throw new InvalidOperationException(
"四个舵轮尚未与车头方向一致,控制器未接管底盘。");
}
}
}