Files
ParkingRobot/MultiWheelC/Control/Execution/PathTrackingCore.cs
T

794 lines
29 KiB
C#
Raw 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.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;
}
}
}