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