增加车队轨迹控制核心与轮组自转模式

This commit is contained in:
2026-08-21 17:33:29 +08:00
parent fea2265e2d
commit 0ab409cd2a
33 changed files with 1949 additions and 989 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,793 @@
using System;
using System.Diagnostics;
using MultiWheelC.Control.Abstractions;
using MultiWheelC.Control.Allocation;
using MultiWheelC.Trajectory;
using MyParking.Shared;
namespace MultiWheelC.Control.Execution
{
// 表示公共轨迹跟踪核心单周期的计算结果,不包含底盘发送结果。
public enum PathTrackingCycleResult
{
Inactive = 0,
CommandGenerated = 1,
Completed = 2,
Faulted = 3
}
// 保存公共核心生成的GCP命令、轨迹投影和分阶段计算耗时。
public readonly struct PathTrackingCycleOutput
{
public PathTrackingCycleOutput(
PathTrackingCycleResult result,
GcpMotionCommand? command,
TrajectoryProjection? projection,
double projectionMilliseconds,
double controllerComputeMilliseconds)
{
Result = result;
Command = command;
Projection = projection;
ProjectionMilliseconds = projectionMilliseconds;
ControllerComputeMilliseconds =
controllerComputeMilliseconds;
}
public PathTrackingCycleResult Result { get; }
public GcpMotionCommand? Command { get; }
public TrajectoryProjection? Projection { get; }
public double ProjectionMilliseconds { get; }
public double ControllerComputeMilliseconds { get; }
}
// 统一处理单车和虚拟车队共有的轨迹投影、速度整形及GCP命令生成。
public sealed class PathTrackingCore
{
private const double ZeroReferenceSpeedToleranceMetersPerSecond =
1e-6;
private const double StartupRegionMeters = 0.02;
private const double StartupPreviewDistanceMeters = 0.05;
private const double MaximumStartupSpeedMetersPerSecond = 0.08;
private const double ProjectionBackwardSearchDistanceMeters =
0.10;
private const double ProjectionForwardSearchDistanceMeters =
1.00;
private readonly ILateralController _lateralController;
private readonly ILongitudinalController _longitudinalController;
private readonly GcpCommandAllocator _gcpAllocator;
private readonly double _motionDirectionInBodyRadians;
private Trajectory2D _trajectory;
private double _terminalTravelDirection = 1.0;
public PathTrackingCore(
ILateralController lateralController,
ILongitudinalController longitudinalController,
GcpCommandAllocator gcpAllocator,
double finishDistanceMeters = 0.04,
double finishSpeedMetersPerSecond = 0.02,
double finishHeadingToleranceRadians =
3.0 * Math.PI / 180.0,
double maximumDistanceToTrajectoryMeters = 0.30,
double terminalBrakingPreviewMeters = 0.02,
double terminalApproachDistanceMeters = 0.10,
double terminalApproachGainPerSecond = 0.8,
double maximumTerminalApproachSpeedMetersPerSecond = 0.05,
double curvaturePreviewSeconds = 0.20,
double maximumCurvaturePreviewMeters = 0.12,
double motionDirectionInBodyRadians = 0.0)
{
_lateralController = lateralController ??
throw new ArgumentNullException(
nameof(lateralController));
_longitudinalController = longitudinalController ??
throw new ArgumentNullException(
nameof(longitudinalController));
_gcpAllocator = gcpAllocator ??
throw new ArgumentNullException(
nameof(gcpAllocator));
NumericGuard.EnsureFinitePositive(
finishDistanceMeters,
nameof(finishDistanceMeters));
NumericGuard.EnsureFiniteNonNegative(
finishSpeedMetersPerSecond,
nameof(finishSpeedMetersPerSecond));
NumericGuard.EnsureFinitePositive(
finishHeadingToleranceRadians,
nameof(finishHeadingToleranceRadians));
NumericGuard.EnsureFinitePositive(
maximumDistanceToTrajectoryMeters,
nameof(maximumDistanceToTrajectoryMeters));
NumericGuard.EnsureFiniteNonNegative(
terminalBrakingPreviewMeters,
nameof(terminalBrakingPreviewMeters));
NumericGuard.EnsureFinitePositive(
terminalApproachDistanceMeters,
nameof(terminalApproachDistanceMeters));
NumericGuard.EnsureFinitePositive(
terminalApproachGainPerSecond,
nameof(terminalApproachGainPerSecond));
NumericGuard.EnsureFinitePositive(
maximumTerminalApproachSpeedMetersPerSecond,
nameof(maximumTerminalApproachSpeedMetersPerSecond));
NumericGuard.EnsureFiniteNonNegative(
curvaturePreviewSeconds,
nameof(curvaturePreviewSeconds));
NumericGuard.EnsureFiniteNonNegative(
maximumCurvaturePreviewMeters,
nameof(maximumCurvaturePreviewMeters));
NumericGuard.EnsureFinite(
motionDirectionInBodyRadians,
nameof(motionDirectionInBodyRadians));
if (terminalApproachDistanceMeters <=
finishDistanceMeters)
{
throw new ArgumentOutOfRangeException(
nameof(terminalApproachDistanceMeters),
"终点单向逼近范围必须大于终点位置容差。");
}
FinishDistanceMeters = finishDistanceMeters;
FinishSpeedMetersPerSecond =
finishSpeedMetersPerSecond;
FinishHeadingToleranceRadians =
finishHeadingToleranceRadians;
MaximumDistanceToTrajectoryMeters =
maximumDistanceToTrajectoryMeters;
TerminalBrakingPreviewMeters =
terminalBrakingPreviewMeters;
TerminalApproachDistanceMeters =
terminalApproachDistanceMeters;
TerminalApproachGainPerSecond =
terminalApproachGainPerSecond;
MaximumTerminalApproachSpeedMetersPerSecond =
maximumTerminalApproachSpeedMetersPerSecond;
CurvaturePreviewSeconds = curvaturePreviewSeconds;
MaximumCurvaturePreviewMeters =
maximumCurvaturePreviewMeters;
_motionDirectionInBodyRadians =
AngleMath.NormalizeRadians(
motionDirectionInBodyRadians);
}
public double FinishDistanceMeters { get; }
public double FinishSpeedMetersPerSecond { get; }
public double FinishHeadingToleranceRadians { get; }
public double MaximumDistanceToTrajectoryMeters { get; }
public double TerminalBrakingPreviewMeters { get; }
public double TerminalApproachDistanceMeters { get; }
public double TerminalApproachGainPerSecond { get; }
public double MaximumTerminalApproachSpeedMetersPerSecond { get; }
public double CurvaturePreviewSeconds { get; }
public double MaximumCurvaturePreviewMeters { get; }
public bool IsActive { get; private set; }
public bool IsCompleted { get; private set; }
public string LastFailureReason { get; private set; } =
string.Empty;
public Exception LastException { get; private set; }
public TrajectoryProjection? LastProjection { get; private set; }
public GcpMotionCommand? LastRequestedCommand { get; private set; }
public double? LastControlReferenceSpeedMetersPerSecond { get; private set; }
public double? LastCurvaturePreviewDistanceMeters { get; private set; }
public double? LastFeedforwardCurvaturePerMeter { get; private set; }
// 重置跨周期状态,并从轨迹起点开始新的跟踪过程。
public void Start(Trajectory2D trajectory)
{
if (trajectory == null)
{
throw new ArgumentNullException(
nameof(trajectory));
}
var terminalTravelDirection =
ResolveTerminalTravelDirection(trajectory);
ResetFeedbackControllers();
_trajectory = trajectory;
_terminalTravelDirection =
terminalTravelDirection;
IsActive = true;
IsCompleted = false;
ClearDiagnostics();
}
// 将受控刚体的位姿和速度转换为本周期GCP命令。
public PathTrackingCycleOutput Compute(
Pose2D poseInWorld,
Twist2D actualTwistInBody,
bool hasValidVelocityEstimate,
double deltaTimeSeconds)
{
NumericGuard.EnsureFinite(
poseInWorld,
nameof(poseInWorld));
NumericGuard.EnsureFinite(
actualTwistInBody,
nameof(actualTwistInBody));
NumericGuard.EnsureFinitePositive(
deltaTimeSeconds,
nameof(deltaTimeSeconds));
if (!IsActive || _trajectory == null)
{
return new PathTrackingCycleOutput(
PathTrackingCycleResult.Inactive,
null,
LastProjection,
0.0,
0.0);
}
var projectionStartTimestamp =
Stopwatch.GetTimestamp();
var projectionCompleted = false;
var projectionMilliseconds = 0.0;
var controllerComputeStartTimestamp = 0L;
try
{
var projection = LastProjection.HasValue
? TrajectoryProjector.Project(
_trajectory,
poseInWorld,
LastProjection.Value.ArcLengthMeters,
ProjectionBackwardSearchDistanceMeters,
ProjectionForwardSearchDistanceMeters)
: TrajectoryProjector.Project(
_trajectory,
poseInWorld);
projectionMilliseconds =
GetElapsedMilliseconds(
projectionStartTimestamp);
projectionCompleted = true;
LastProjection = projection;
controllerComputeStartTimestamp =
Stopwatch.GetTimestamp();
if (projection.DistanceToTrajectoryMeters >
MaximumDistanceToTrajectoryMeters)
{
Fail(
"受控刚体距离参考轨迹" +
$"{projection.DistanceToTrajectoryMeters:F3}m" +
"超过允许值" +
$"{MaximumDistanceToTrajectoryMeters:F3}m。");
return CreateOutput(
PathTrackingCycleResult.Faulted,
null,
projection,
projectionMilliseconds,
controllerComputeStartTimestamp);
}
if (HasReachedEnd(
poseInWorld,
actualTwistInBody,
hasValidVelocityEstimate,
projection))
{
CompleteTrajectory();
return CreateOutput(
PathTrackingCycleResult.Completed,
null,
projection,
projectionMilliseconds,
controllerComputeStartTimestamp);
}
if (HasStoppedAtUnsatisfiedTerminal(
poseInWorld,
actualTwistInBody,
hasValidVelocityEstimate,
projection,
out var terminalFailureReason))
{
Fail(terminalFailureReason);
return CreateOutput(
PathTrackingCycleResult.Faulted,
null,
projection,
projectionMilliseconds,
controllerComputeStartTimestamp);
}
var controlReferenceSpeedMetersPerSecond =
ResolveControlReferenceSpeed(
poseInWorld,
projection);
LastControlReferenceSpeedMetersPerSecond =
controlReferenceSpeedMetersPerSecond;
var curvaturePreviewDistanceMeters =
ResolveCurvaturePreviewDistanceMeters(
actualTwistInBody,
hasValidVelocityEstimate,
controlReferenceSpeedMetersPerSecond);
LastCurvaturePreviewDistanceMeters =
curvaturePreviewDistanceMeters;
var feedforwardCurvaturePerMeter =
ResolveFeedforwardCurvaturePerMeter(
projection,
curvaturePreviewDistanceMeters);
LastFeedforwardCurvaturePerMeter =
feedforwardCurvaturePerMeter;
var context = new PathTrackingContext(
actualTwistInBody,
hasValidVelocityEstimate,
projection,
controlReferenceSpeedMetersPerSecond,
feedforwardCurvaturePerMeter,
deltaTimeSeconds,
_motionDirectionInBodyRadians);
var lateralCommand =
_lateralController.Compute(context);
var commandSpeedMetersPerSecond =
_longitudinalController
.ComputeSpeedMetersPerSecond(context);
var command = _gcpAllocator.Allocate(
commandSpeedMetersPerSecond,
lateralCommand);
LastRequestedCommand = command;
LastFailureReason = string.Empty;
LastException = null;
return CreateOutput(
PathTrackingCycleResult.CommandGenerated,
command,
projection,
projectionMilliseconds,
controllerComputeStartTimestamp);
}
catch (Exception exception)
{
if (!projectionCompleted)
{
projectionMilliseconds =
GetElapsedMilliseconds(
projectionStartTimestamp);
}
Fail(
"轨迹跟踪核心计算异常:" +
exception.Message,
exception);
return CreateOutput(
PathTrackingCycleResult.Faulted,
null,
LastProjection,
projectionMilliseconds,
controllerComputeStartTimestamp);
}
}
// 状态暂不可用时重置反馈历史,但保留当前轨迹和投影进度等待恢复。
public void PauseForUnavailableState(string reason)
{
ResetFeedbackControllers();
LastRequestedCommand = null;
LastFailureReason = reason ?? string.Empty;
LastException = null;
}
// 将外层执行故障同步到公共核心,并终止当前轨迹。
public void Fail(
string reason,
Exception exception = null)
{
ResetFeedbackControllers();
_trajectory = null;
IsActive = false;
IsCompleted = false;
LastRequestedCommand = null;
LastFailureReason = reason ?? string.Empty;
LastException = exception;
}
// 取消当前轨迹并清除全部跟踪状态。
public void Cancel()
{
ResetFeedbackControllers();
_trajectory = null;
_terminalTravelDirection = 1.0;
IsActive = false;
IsCompleted = false;
ClearDiagnostics();
}
private double ResolveControlReferenceSpeed(
Pose2D poseInWorld,
TrajectoryProjection projection)
{
if (projection.RemainingDistanceMeters >
TerminalApproachDistanceMeters)
{
return ResolveReferenceSpeedForControl(
projection);
}
return ResolveTerminalApproachSpeed(
poseInWorld);
}
private double ResolveCurvaturePreviewDistanceMeters(
Twist2D actualTwistInBody,
bool hasValidVelocityEstimate,
double controlReferenceSpeedMetersPerSecond)
{
if (CurvaturePreviewSeconds <= 0.0 ||
MaximumCurvaturePreviewMeters <= 0.0)
{
return 0.0;
}
var previewSpeedMetersPerSecond =
hasValidVelocityEstimate
? CalculateActualLongitudinalSpeedMetersPerSecond(
actualTwistInBody)
: Math.Abs(
controlReferenceSpeedMetersPerSecond);
return Math.Min(
MaximumCurvaturePreviewMeters,
previewSpeedMetersPerSecond *
CurvaturePreviewSeconds);
}
private double ResolveFeedforwardCurvaturePerMeter(
TrajectoryProjection projection,
double previewDistanceMeters)
{
var previewArcLengthMeters = Math.Min(
_trajectory.TotalLengthMeters,
projection.ArcLengthMeters +
previewDistanceMeters);
return _trajectory
.SampleAtArcLength(previewArcLengthMeters)
.CurvaturePerMeter;
}
private double ResolveTerminalApproachSpeed(
Pose2D poseInWorld)
{
var distanceToEndMeters =
CalculateDistanceToEndMeters(
poseInWorld);
var headingErrorToEndRadians =
CalculateHeadingErrorToEndRadians(
poseInWorld);
if (distanceToEndMeters <=
FinishDistanceMeters &&
headingErrorToEndRadians <=
FinishHeadingToleranceRadians)
{
return 0.0;
}
var endPose = _trajectory.EndPoint.PoseInWorld;
var deltaX = endPose.XMeters -
poseInWorld.XMeters;
var deltaY = endPose.YMeters -
poseInWorld.YMeters;
var longitudinalErrorMeters =
deltaX * Math.Cos(endPose.YawRadians) +
deltaY * Math.Sin(endPose.YawRadians);
var remainingAlongTravelMeters =
_terminalTravelDirection *
longitudinalErrorMeters;
// 越过终点后不生成与原轨迹方向相反的修正速度。
if (remainingAlongTravelMeters <= 0.0)
{
return 0.0;
}
var speedMagnitudeMetersPerSecond =
Math.Min(
MaximumTerminalApproachSpeedMetersPerSecond,
TerminalApproachGainPerSecond *
remainingAlongTravelMeters);
return _terminalTravelDirection *
speedMagnitudeMetersPerSecond;
}
private double ResolveReferenceSpeedForControl(
TrajectoryProjection projection)
{
var currentReferenceSpeed =
ApplyTerminalBrakingPreview(
projection,
projection.ReferencePoint
.ReferenceSpeedMetersPerSecond);
var isInStartupRegion =
projection.ArcLengthMeters <=
StartupRegionMeters &&
projection.RemainingDistanceMeters >
FinishDistanceMeters;
if (!isInStartupRegion)
{
return currentReferenceSpeed;
}
var previewArcLengthMeters = Math.Min(
_trajectory.TotalLengthMeters,
projection.ArcLengthMeters +
StartupPreviewDistanceMeters);
var previewReferenceSpeed =
_trajectory
.SampleAtArcLength(previewArcLengthMeters)
.ReferenceSpeedMetersPerSecond;
if (Math.Abs(previewReferenceSpeed) <=
ZeroReferenceSpeedToleranceMetersPerSecond)
{
return currentReferenceSpeed;
}
var startupReleaseSpeed =
Math.Sign(previewReferenceSpeed) *
Math.Min(
Math.Abs(previewReferenceSpeed),
MaximumStartupSpeedMetersPerSecond);
if (Math.Sign(currentReferenceSpeed) ==
Math.Sign(startupReleaseSpeed) &&
Math.Abs(currentReferenceSpeed) >=
Math.Abs(startupReleaseSpeed))
{
return currentReferenceSpeed;
}
return startupReleaseSpeed;
}
private double ApplyTerminalBrakingPreview(
TrajectoryProjection projection,
double currentReferenceSpeed)
{
if (TerminalBrakingPreviewMeters <= 0.0)
{
return currentReferenceSpeed;
}
var previewArcLengthMeters = Math.Min(
_trajectory.TotalLengthMeters,
projection.ArcLengthMeters +
TerminalBrakingPreviewMeters);
var previewReferenceSpeed =
_trajectory
.SampleAtArcLength(previewArcLengthMeters)
.ReferenceSpeedMetersPerSecond;
var previewIsStop =
Math.Abs(previewReferenceSpeed) <=
ZeroReferenceSpeedToleranceMetersPerSecond;
var hasSameDirection =
Math.Sign(previewReferenceSpeed) ==
Math.Sign(currentReferenceSpeed);
var previewIsSlower =
Math.Abs(previewReferenceSpeed) <
Math.Abs(currentReferenceSpeed);
if (previewIsSlower &&
(previewIsStop || hasSameDirection))
{
return previewReferenceSpeed;
}
return currentReferenceSpeed;
}
private static double ResolveTerminalTravelDirection(
Trajectory2D trajectory)
{
for (var index = trajectory.Count - 1;
index >= 0;
index--)
{
var referenceSpeedMetersPerSecond =
trajectory[index]
.ReferenceSpeedMetersPerSecond;
if (Math.Abs(referenceSpeedMetersPerSecond) >
ZeroReferenceSpeedToleranceMetersPerSecond)
{
return Math.Sign(
referenceSpeedMetersPerSecond);
}
}
throw new ArgumentException(
"轨迹必须在终点前包含至少一个非零参考速度。",
nameof(trajectory));
}
private bool HasReachedEnd(
Pose2D poseInWorld,
Twist2D actualTwistInBody,
bool hasValidVelocityEstimate,
TrajectoryProjection projection)
{
if (!hasValidVelocityEstimate)
{
return false;
}
return projection.RemainingDistanceMeters <=
FinishDistanceMeters &&
CalculateDistanceToEndMeters(poseInWorld) <=
FinishDistanceMeters &&
CalculateHeadingErrorToEndRadians(poseInWorld) <=
FinishHeadingToleranceRadians &&
CalculateActualLongitudinalSpeedMetersPerSecond(
actualTwistInBody) <=
FinishSpeedMetersPerSecond;
}
private bool HasStoppedAtUnsatisfiedTerminal(
Pose2D poseInWorld,
Twist2D actualTwistInBody,
bool hasValidVelocityEstimate,
TrajectoryProjection projection,
out string failureReason)
{
failureReason = string.Empty;
var isTerminalZeroSpeedReference =
projection.RemainingDistanceMeters <=
FinishDistanceMeters &&
Math.Abs(
projection.ReferencePoint
.ReferenceSpeedMetersPerSecond) <=
ZeroReferenceSpeedToleranceMetersPerSecond;
if (!isTerminalZeroSpeedReference ||
!hasValidVelocityEstimate ||
CalculateActualLongitudinalSpeedMetersPerSecond(
actualTwistInBody) >
FinishSpeedMetersPerSecond)
{
return false;
}
var positionErrorMeters =
CalculateDistanceToEndMeters(
poseInWorld);
var headingErrorRadians =
CalculateHeadingErrorToEndRadians(
poseInWorld);
failureReason =
"受控刚体已在终点零速参考处停稳,但终点精度不满足要求:" +
$"位置误差={positionErrorMeters:F3}m" +
"航向误差=" +
$"{AngleMath.RadiansToDegrees(headingErrorRadians):F2}°。";
return true;
}
private double CalculateDistanceToEndMeters(
Pose2D poseInWorld)
{
var endPose = _trajectory.EndPoint.PoseInWorld;
var deltaX = poseInWorld.XMeters -
endPose.XMeters;
var deltaY = poseInWorld.YMeters -
endPose.YMeters;
return Math.Sqrt(
deltaX * deltaX +
deltaY * deltaY);
}
private double CalculateHeadingErrorToEndRadians(
Pose2D poseInWorld)
{
return Math.Abs(
AngleMath.ShortestDifferenceRadians(
_trajectory.EndPoint
.PoseInWorld.YawRadians,
poseInWorld.YawRadians));
}
private double CalculateActualLongitudinalSpeedMetersPerSecond(
Twist2D actualTwistInBody)
{
return Math.Abs(
Math.Cos(_motionDirectionInBodyRadians) *
actualTwistInBody.VxMetersPerSecond +
Math.Sin(_motionDirectionInBodyRadians) *
actualTwistInBody.VyMetersPerSecond);
}
private void CompleteTrajectory()
{
ResetFeedbackControllers();
_trajectory = null;
IsActive = false;
IsCompleted = true;
LastRequestedCommand = new GcpMotionCommand(
0.0,
0.0,
0.0);
LastFailureReason = string.Empty;
LastException = null;
}
private void ResetFeedbackControllers()
{
_lateralController.Reset();
_longitudinalController.Reset();
}
private void ClearDiagnostics()
{
LastProjection = null;
LastRequestedCommand = null;
LastControlReferenceSpeedMetersPerSecond = null;
LastCurvaturePreviewDistanceMeters = null;
LastFeedforwardCurvaturePerMeter = null;
LastFailureReason = string.Empty;
LastException = null;
}
private static PathTrackingCycleOutput CreateOutput(
PathTrackingCycleResult result,
GcpMotionCommand? command,
TrajectoryProjection? projection,
double projectionMilliseconds,
long controllerComputeStartTimestamp)
{
var controllerComputeMilliseconds =
controllerComputeStartTimestamp == 0L
? 0.0
: GetElapsedMilliseconds(
controllerComputeStartTimestamp);
return new PathTrackingCycleOutput(
result,
command,
projection,
projectionMilliseconds,
controllerComputeMilliseconds);
}
private static double GetElapsedMilliseconds(
long startTimestamp)
{
return (Stopwatch.GetTimestamp() - startTimestamp) *
1000.0 /
Stopwatch.Frequency;
}
}
}