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;
|
||
}
|
||
}
|
||
}
|