feat: integrate trajectory tracking controller runtime

This commit is contained in:
梁薄云
2026-08-10 13:26:56 +08:00
parent f923affc8f
commit 54ef239231
29 changed files with 5135 additions and 0 deletions
@@ -0,0 +1,509 @@
using System;
using System.Diagnostics;
using ClumsyCore.Interfaces;
using MyParking.Shared;
namespace MultiWheelC.StateEstimation
{
/// <summary>
/// 读取Detour位姿,忽略重复或明显异常的观测,并估算车辆二维速度。
/// </summary>
public sealed class DetourVehicleStateProvider
: IVehicleStateProvider
{
public const double DefaultMaximumLinearSpeedMetersPerSecond =
1.20;
public const double DefaultMaximumAngularSpeedRadiansPerSecond =
Math.PI / 4.0;
public const double DefaultPositionJumpMarginMeters =
0.03;
public const double DefaultHeadingJumpMarginRadians =
5.0 * Math.PI / 180.0;
public const double DefaultVelocityPositionResidualMeters =
0.04;
public const double DefaultVelocityHeadingResidualRadians =
5.0 * Math.PI / 180.0;
public const double DefaultStationaryConfirmationSeconds =
0.35;
private const double MillimetersPerMeter = 1000.0;
private const double PositionEqualityToleranceMeters = 1e-9;
private const double HeadingEqualityToleranceRadians = 1e-8;
private readonly object _syncRoot = new object();
private readonly Stopwatch _clock = Stopwatch.StartNew();
private readonly VelocityEstimator2D _velocityEstimator;
private readonly double _maximumLinearSpeedMetersPerSecond;
private readonly double _maximumAngularSpeedRadiansPerSecond;
private readonly double _positionJumpMarginMeters;
private readonly double _headingJumpMarginRadians;
private readonly double _velocityPositionResidualMeters;
private readonly double _velocityHeadingResidualRadians;
private readonly double _stationaryConfirmationSeconds;
private bool _hasAcceptedPose;
private Pose2D _acceptedPoseInWorld;
private double _acceptedTimestampSeconds;
private VehicleState _latestState;
private bool _stationaryHoldActive;
/// <summary>
/// 创建使用停车机器人默认物理边界和速度滤波参数的Detour状态源。
/// </summary>
public DetourVehicleStateProvider()
: this(
new VelocityEstimator2D(),
DefaultMaximumLinearSpeedMetersPerSecond,
DefaultMaximumAngularSpeedRadiansPerSecond,
DefaultPositionJumpMarginMeters,
DefaultHeadingJumpMarginRadians,
DefaultVelocityPositionResidualMeters,
DefaultVelocityHeadingResidualRadians,
DefaultStationaryConfirmationSeconds)
{
}
/// <summary>
/// 创建使用指定物理边界、静止确认时间和速度估计器的Detour状态源。
/// </summary>
public DetourVehicleStateProvider(
VelocityEstimator2D velocityEstimator,
double maximumLinearSpeedMetersPerSecond,
double maximumAngularSpeedRadiansPerSecond,
double positionJumpMarginMeters,
double headingJumpMarginRadians,
double velocityPositionResidualMeters,
double velocityHeadingResidualRadians,
double stationaryConfirmationSeconds)
{
_velocityEstimator = velocityEstimator ??
throw new ArgumentNullException(
nameof(velocityEstimator));
EnsureFinitePositive(
maximumLinearSpeedMetersPerSecond,
nameof(maximumLinearSpeedMetersPerSecond));
EnsureFinitePositive(
maximumAngularSpeedRadiansPerSecond,
nameof(maximumAngularSpeedRadiansPerSecond));
EnsureFiniteNonNegative(
positionJumpMarginMeters,
nameof(positionJumpMarginMeters));
EnsureFiniteNonNegative(
headingJumpMarginRadians,
nameof(headingJumpMarginRadians));
EnsureFinitePositive(
velocityPositionResidualMeters,
nameof(velocityPositionResidualMeters));
EnsureFinitePositive(
velocityHeadingResidualRadians,
nameof(velocityHeadingResidualRadians));
EnsureFinitePositive(
stationaryConfirmationSeconds,
nameof(stationaryConfirmationSeconds));
_maximumLinearSpeedMetersPerSecond =
maximumLinearSpeedMetersPerSecond;
_maximumAngularSpeedRadiansPerSecond =
maximumAngularSpeedRadiansPerSecond;
_positionJumpMarginMeters =
positionJumpMarginMeters;
_headingJumpMarginRadians =
headingJumpMarginRadians;
_velocityPositionResidualMeters =
velocityPositionResidualMeters;
_velocityHeadingResidualRadians =
velocityHeadingResidualRadians;
_stationaryConfirmationSeconds =
stationaryConfirmationSeconds;
}
/// <summary>
/// 获取最近一次读取失败或异常观测被忽略的原因,正常时为空字符串。
/// </summary>
public string LastFailureReason { get; private set; } = "";
/// <summary>
/// 尝试读取Detour;重复帧保留最近状态,明显异常帧只忽略本次观测。
/// </summary>
public bool TryGetState(out VehicleState state)
{
lock (_syncRoot)
{
try
{
var poseInWorld =
ReadDetourPoseInWorld();
var timestampSeconds =
_clock.Elapsed.TotalSeconds;
if (!_hasAcceptedPose)
{
state = AcceptPoseAfterReset(
poseInWorld,
timestampSeconds);
LastFailureReason = "";
return true;
}
if (ArePosesEquivalent(
poseInWorld,
_acceptedPoseInWorld))
{
state = HandleRepeatedPose(
timestampSeconds);
LastFailureReason = "";
return true;
}
// 静止保持后出现新定位时重新建立差分基准,
// 避免用很长的静止时间稀释第一次运动速度。
if (_stationaryHoldActive)
{
state = AcceptPoseAfterReset(
poseInWorld,
timestampSeconds);
LastFailureReason = "";
return true;
}
var elapsedSeconds =
timestampSeconds -
_acceptedTimestampSeconds;
if (!IsMotionPlausible(
_acceptedPoseInWorld,
poseInWorld,
elapsedSeconds))
{
// 单帧异常不进入差分器,也不中断调用方;下一次
// 正常观测仍相对最近有效位姿和真实时间差计算。
state = _latestState;
LastFailureReason =
"Detour位姿变化超过车辆绝对运动边界,本次观测已忽略。";
return true;
}
if (IsVelocityInnovationAbnormal(
poseInWorld,
elapsedSeconds))
{
state = AcceptPoseAfterReset(
poseInWorld,
timestampSeconds);
LastFailureReason =
"Detour位姿偏离速度预测,已重新建立速度估计基准。";
return true;
}
state = AcceptContinuousPose(
poseInWorld,
timestampSeconds);
LastFailureReason = "";
return true;
}
catch (Exception exception)
{
state = default;
LastFailureReason =
"Detour车辆状态读取失败:" +
exception.Message;
return false;
}
}
}
/// <summary>
/// 清除Detour位姿历史和速度估计状态。
/// </summary>
public void Reset()
{
lock (_syncRoot)
{
_velocityEstimator.Reset();
_hasAcceptedPose = false;
_acceptedPoseInWorld = Pose2D.Identity;
_acceptedTimestampSeconds = 0.0;
_latestState = default;
_stationaryHoldActive = false;
LastFailureReason = "";
}
}
/// <summary>
/// 读取Detour毫米和角度数据并转换为世界坐标SI位姿。
/// </summary>
private static Pose2D ReadDetourPoseInWorld()
{
var location =
DetourInterface.getCartLocation();
EnsureFinite(location.x, "DetourX");
EnsureFinite(location.y, "DetourY");
EnsureFinite(location.th, "DetourTheta");
return new Pose2D(
location.x / MillimetersPerMeter,
location.y / MillimetersPerMeter,
AngleMath.NormalizeRadians(
AngleMath.DegreesToRadians(
location.th)));
}
/// <summary>
/// 接受连续有效定位并更新速度估计和差分基准。
/// </summary>
private VehicleState AcceptContinuousPose(
Pose2D poseInWorld,
double timestampSeconds)
{
_latestState =
_velocityEstimator.Update(
poseInWorld,
timestampSeconds);
_acceptedPoseInWorld = poseInWorld;
_acceptedTimestampSeconds =
timestampSeconds;
_stationaryHoldActive = false;
return _latestState;
}
/// <summary>
/// 接受跳变后的新位姿基准,但不让该位移进入速度差分和低通滤波器。
/// </summary>
private VehicleState AcceptPoseAfterVelocityRebase(
Pose2D poseInWorld,
double timestampSeconds)
{
_latestState =
_velocityEstimator
.RebasePreservingVelocity(
poseInWorld,
timestampSeconds);
_acceptedPoseInWorld = poseInWorld;
_acceptedTimestampSeconds =
timestampSeconds;
_stationaryHoldActive = false;
return _latestState;
}
/// <summary>
/// 接受首帧或静止后的首个新位姿并重新建立零速差分基准。
/// </summary>
private VehicleState AcceptPoseAfterReset(
Pose2D poseInWorld,
double timestampSeconds)
{
_latestState =
_velocityEstimator.Reset(
poseInWorld,
timestampSeconds);
_acceptedPoseInWorld = poseInWorld;
_acceptedTimestampSeconds =
timestampSeconds;
_hasAcceptedPose = true;
_stationaryHoldActive = false;
return _latestState;
}
/// <summary>
/// 对重复Detour观测保留最近状态,并在持续不变后将速度归零。
/// </summary>
private VehicleState HandleRepeatedPose(
double timestampSeconds)
{
var unchangedSeconds =
timestampSeconds -
_acceptedTimestampSeconds;
if (!_stationaryHoldActive &&
unchangedSeconds >=
_stationaryConfirmationSeconds)
{
_latestState =
new VehicleState(
timestampSeconds,
_acceptedPoseInWorld,
Twist2D.Zero,
true);
_stationaryHoldActive = true;
}
return _latestState;
}
/// <summary>
/// 判断两次有效Detour观测之间的变化是否超过车辆绝对运动能力。
/// </summary>
private bool IsMotionPlausible(
Pose2D startPoseInWorld,
Pose2D endPoseInWorld,
double deltaTimeSeconds)
{
if (!IsFinite(deltaTimeSeconds) ||
deltaTimeSeconds <= 0.0)
{
return false;
}
var deltaX =
endPoseInWorld.XMeters -
startPoseInWorld.XMeters;
var deltaY =
endPoseInWorld.YMeters -
startPoseInWorld.YMeters;
var displacementMeters =
Math.Sqrt(
deltaX * deltaX +
deltaY * deltaY);
var headingChangeRadians =
Math.Abs(
AngleMath.ShortestDifferenceRadians(
endPoseInWorld.YawRadians,
startPoseInWorld.YawRadians));
var maximumDisplacementMeters =
_maximumLinearSpeedMetersPerSecond *
deltaTimeSeconds +
_positionJumpMarginMeters;
var maximumHeadingChangeRadians =
_maximumAngularSpeedRadiansPerSecond *
deltaTimeSeconds +
_headingJumpMarginRadians;
return displacementMeters <=
maximumDisplacementMeters &&
headingChangeRadians <=
maximumHeadingChangeRadians;
}
/// <summary>
/// 判断新位姿是否明显偏离上一滤波速度给出的恒速预测。
/// </summary>
private bool IsVelocityInnovationAbnormal(
Pose2D poseInWorld,
double deltaTimeSeconds)
{
if (!_latestState.HasValidVelocityEstimate)
{
return false;
}
var predictedX =
_acceptedPoseInWorld.XMeters +
_latestState.TwistInWorld
.VxMetersPerSecond *
deltaTimeSeconds;
var predictedY =
_acceptedPoseInWorld.YMeters +
_latestState.TwistInWorld
.VyMetersPerSecond *
deltaTimeSeconds;
var predictedYaw =
AngleMath.NormalizeRadians(
_acceptedPoseInWorld.YawRadians +
_latestState.TwistInWorld
.OmegaRadiansPerSecond *
deltaTimeSeconds);
var positionResidualX =
poseInWorld.XMeters - predictedX;
var positionResidualY =
poseInWorld.YMeters - predictedY;
var positionResidualMeters =
Math.Sqrt(
positionResidualX * positionResidualX +
positionResidualY * positionResidualY);
var headingResidualRadians =
Math.Abs(
AngleMath.ShortestDifferenceRadians(
poseInWorld.YawRadians,
predictedYaw));
return positionResidualMeters >
_velocityPositionResidualMeters ||
headingResidualRadians >
_velocityHeadingResidualRadians;
}
/// <summary>
/// 判断两次读取是否为Detour保持输出的同一数值帧。
/// </summary>
private static bool ArePosesEquivalent(
Pose2D firstPose,
Pose2D secondPose)
{
return Math.Abs(
firstPose.XMeters -
secondPose.XMeters) <=
PositionEqualityToleranceMeters &&
Math.Abs(
firstPose.YMeters -
secondPose.YMeters) <=
PositionEqualityToleranceMeters &&
Math.Abs(
AngleMath.ShortestDifferenceRadians(
firstPose.YawRadians,
secondPose.YawRadians)) <=
HeadingEqualityToleranceRadians;
}
/// <summary>
/// 检查数值是否为正有限值。
/// </summary>
private static void EnsureFinitePositive(
double value,
string parameterName)
{
EnsureFinite(value, parameterName);
if (value <= 0.0)
{
throw new ArgumentOutOfRangeException(
parameterName,
"状态源参数必须是正有限值。");
}
}
/// <summary>
/// 检查数值是否为非负有限值。
/// </summary>
private static void EnsureFiniteNonNegative(
double value,
string parameterName)
{
EnsureFinite(value, parameterName);
if (value < 0.0)
{
throw new ArgumentOutOfRangeException(
parameterName,
"状态源参数必须是非负有限值。");
}
}
/// <summary>
/// 检查数值是否为有限值。
/// </summary>
private static void EnsureFinite(
double value,
string parameterName)
{
if (!IsFinite(value))
{
throw new ArgumentOutOfRangeException(
parameterName,
"状态源参数和Detour位姿必须是有限值。");
}
}
/// <summary>
/// 判断数值是否可用于状态估计。
/// </summary>
private static bool IsFinite(double value)
{
return !double.IsNaN(value) &&
!double.IsInfinity(value);
}
}
}
@@ -0,0 +1,142 @@
using System;
namespace MultiWheelC.StateEstimation
{
/// <summary>
/// 使用真实采样时间间隔对单个连续量执行在线一阶低通滤波。
/// </summary>
public sealed class FirstOrderLowPassFilter
{
private readonly double _timeConstantSeconds;
private bool _isInitialized;
private double _value;
/// <summary>
/// 创建使用指定时间常数的一阶低通滤波器。
/// </summary>
public FirstOrderLowPassFilter(
double timeConstantSeconds)
{
EnsureFinitePositive(
timeConstantSeconds,
nameof(timeConstantSeconds));
_timeConstantSeconds =
timeConstantSeconds;
}
/// <summary>
/// 获取滤波时间常数,单位为s;数值越大,滤波越强但响应越慢。
/// </summary>
public double TimeConstantSeconds =>
_timeConstantSeconds;
/// <summary>
/// 获取滤波器是否已经接收过有效初值。
/// </summary>
public bool IsInitialized =>
_isInitialized;
/// <summary>
/// 获取当前滤波输出;尚未初始化时读取会抛出异常。
/// </summary>
public double Value
{
get
{
if (!_isInitialized)
{
throw new InvalidOperationException(
"一阶低通滤波器尚未初始化。");
}
return _value;
}
}
/// <summary>
/// 使用当前输入和真实采样间隔更新滤波结果。
/// </summary>
public double Update(
double input,
double deltaTimeSeconds)
{
EnsureFinite(
input,
nameof(input));
EnsureFinitePositive(
deltaTimeSeconds,
nameof(deltaTimeSeconds));
if (!_isInitialized)
{
_value = input;
_isInitialized = true;
return _value;
}
var alpha =
deltaTimeSeconds /
(_timeConstantSeconds +
deltaTimeSeconds);
_value += alpha * (input - _value);
return _value;
}
/// <summary>
/// 清除历史输出,使下一次有效输入直接成为新的初值。
/// </summary>
public void Reset()
{
_value = 0.0;
_isInitialized = false;
}
/// <summary>
/// 将滤波器立即重置到指定的有限初值。
/// </summary>
public void Reset(double initialValue)
{
EnsureFinite(
initialValue,
nameof(initialValue));
_value = initialValue;
_isInitialized = true;
}
/// <summary>
/// 检查数值是否为正有限值。
/// </summary>
private static void EnsureFinitePositive(
double value,
string parameterName)
{
EnsureFinite(value, parameterName);
if (value <= 0.0)
{
throw new ArgumentOutOfRangeException(
parameterName,
"滤波时间常数和采样间隔必须是正有限值。");
}
}
/// <summary>
/// 检查数值是否为有限值。
/// </summary>
private static void EnsureFinite(
double value,
string parameterName)
{
if (double.IsNaN(value) ||
double.IsInfinity(value))
{
throw new ArgumentOutOfRangeException(
parameterName,
"滤波输入必须是有限值。");
}
}
}
}
@@ -0,0 +1,14 @@
namespace MultiWheelC.StateEstimation
{
/// <summary>
/// 为轨迹控制器提供与具体定位来源无关的统一车辆状态读取接口。
/// </summary>
public interface IVehicleStateProvider
{
/// <summary>
/// 尝试读取当前有效车辆状态;定位不可用或过期时返回false。
/// </summary>
bool TryGetState(out VehicleState state);
}
}
+138
View File
@@ -0,0 +1,138 @@
using System;
using MyParking.Shared;
namespace MultiWheelC.StateEstimation
{
/// <summary>
/// 保存一次经过校验的车辆位姿和速度估计快照,统一使用SI单位。
/// </summary>
public readonly struct VehicleState
{
/// <summary>
/// 创建车辆状态,并将世界坐标速度同步转换到车体坐标系。
/// </summary>
public VehicleState(
double sampleTimestampSeconds,
Pose2D poseInWorld,
Twist2D twistInWorld,
bool hasValidVelocityEstimate)
{
EnsureFiniteNonNegative(
sampleTimestampSeconds,
nameof(sampleTimestampSeconds));
EnsureFinitePose(
poseInWorld,
nameof(poseInWorld));
EnsureFiniteTwist(
twistInWorld,
nameof(twistInWorld));
SampleTimestampSeconds =
sampleTimestampSeconds;
PoseInWorld = new Pose2D(
poseInWorld.XMeters,
poseInWorld.YMeters,
AngleMath.NormalizeRadians(
poseInWorld.YawRadians));
HasValidVelocityEstimate =
hasValidVelocityEstimate;
// 第一帧或定位重置后的速度不可用于闭环控制,
// 此时显式置零,避免调用方误用残留速度。
TwistInWorld = hasValidVelocityEstimate
? twistInWorld
: Twist2D.Zero;
var worldPoseInBody =
FrameTransform2D.Inverse(
PoseInWorld);
TwistInBody =
FrameTransform2D.TransformTwistAtSamePoint(
worldPoseInBody,
TwistInWorld);
}
/// <summary>
/// 获取状态源单调时钟中的采样时刻,单位为s。
/// </summary>
public double SampleTimestampSeconds { get; }
/// <summary>
/// 获取车体中心在Detour世界坐标系中的位姿,单位为m和rad。
/// </summary>
public Pose2D PoseInWorld { get; }
/// <summary>
/// 获取在世界坐标系中表达的车辆速度,单位为m/s和rad/s。
/// </summary>
public Twist2D TwistInWorld { get; }
/// <summary>
/// 获取在车体坐标系中表达的车辆速度,X向前、Y向左、逆时针为正。
/// </summary>
public Twist2D TwistInBody { get; }
/// <summary>
/// 获取当前速度是否已由至少两个连续有效定位样本估算得到。
/// </summary>
public bool HasValidVelocityEstimate { get; }
/// <summary>
/// 检查位姿是否由有限数值组成。
/// </summary>
private static void EnsureFinitePose(
Pose2D pose,
string parameterName)
{
if (!IsFinite(pose.XMeters) ||
!IsFinite(pose.YMeters) ||
!IsFinite(pose.YawRadians))
{
throw new ArgumentOutOfRangeException(
parameterName,
"车辆位姿必须由有限数值组成。");
}
}
/// <summary>
/// 检查速度是否由有限数值组成。
/// </summary>
private static void EnsureFiniteTwist(
Twist2D twist,
string parameterName)
{
if (!IsFinite(twist.VxMetersPerSecond) ||
!IsFinite(twist.VyMetersPerSecond) ||
!IsFinite(twist.OmegaRadiansPerSecond))
{
throw new ArgumentOutOfRangeException(
parameterName,
"车辆速度必须由有限数值组成。");
}
}
/// <summary>
/// 检查数值是否为非负有限值。
/// </summary>
private static void EnsureFiniteNonNegative(
double value,
string parameterName)
{
if (!IsFinite(value) || value < 0.0)
{
throw new ArgumentOutOfRangeException(
parameterName,
"采样时刻必须是非负有限值。");
}
}
/// <summary>
/// 判断数值是否可用于车辆状态计算。
/// </summary>
private static bool IsFinite(double value)
{
return !double.IsNaN(value) &&
!double.IsInfinity(value);
}
}
}
@@ -0,0 +1,275 @@
using System;
using MyParking.Shared;
namespace MultiWheelC.StateEstimation
{
/// <summary>
/// 根据连续有效的Detour世界位姿和真实时间差估算车辆二维速度。
/// </summary>
public sealed class VelocityEstimator2D
{
public const double DefaultLinearFilterTimeConstantSeconds =
0.15;
public const double DefaultAngularFilterTimeConstantSeconds =
0.20;
private readonly FirstOrderLowPassFilter
_worldVelocityXFilter;
private readonly FirstOrderLowPassFilter
_worldVelocityYFilter;
private readonly FirstOrderLowPassFilter
_angularVelocityFilter;
private bool _hasPreviousSample;
private Pose2D _previousPoseInWorld;
private double _previousTimestampSeconds;
/// <summary>
/// 创建使用默认0.15s线速度和0.20s角速度时间常数的估计器。
/// </summary>
public VelocityEstimator2D()
: this(
DefaultLinearFilterTimeConstantSeconds,
DefaultAngularFilterTimeConstantSeconds)
{
}
/// <summary>
/// 创建使用指定线速度和角速度滤波时间常数的估计器。
/// </summary>
public VelocityEstimator2D(
double linearFilterTimeConstantSeconds,
double angularFilterTimeConstantSeconds)
{
_worldVelocityXFilter =
new FirstOrderLowPassFilter(
linearFilterTimeConstantSeconds);
_worldVelocityYFilter =
new FirstOrderLowPassFilter(
linearFilterTimeConstantSeconds);
_angularVelocityFilter =
new FirstOrderLowPassFilter(
angularFilterTimeConstantSeconds);
}
/// <summary>
/// 获取是否已经保存了可用于下一次差分的位姿基准。
/// </summary>
public bool HasPreviousSample =>
_hasPreviousSample;
/// <summary>
/// 使用一个新的有效定位样本更新并返回车辆状态。
/// </summary>
public VehicleState Update(
Pose2D poseInWorld,
double sampleTimestampSeconds)
{
EnsureFinitePose(
poseInWorld,
nameof(poseInWorld));
EnsureFiniteNonNegative(
sampleTimestampSeconds,
nameof(sampleTimestampSeconds));
var normalizedPoseInWorld =
new Pose2D(
poseInWorld.XMeters,
poseInWorld.YMeters,
AngleMath.NormalizeRadians(
poseInWorld.YawRadians));
if (!_hasPreviousSample)
{
return Reset(
normalizedPoseInWorld,
sampleTimestampSeconds);
}
var deltaTimeSeconds =
sampleTimestampSeconds -
_previousTimestampSeconds;
if (deltaTimeSeconds <= 0.0)
{
throw new ArgumentOutOfRangeException(
nameof(sampleTimestampSeconds),
"新定位样本的单调时间戳必须严格大于上一帧。");
}
var rawVelocityXInWorld =
(normalizedPoseInWorld.XMeters -
_previousPoseInWorld.XMeters) /
deltaTimeSeconds;
var rawVelocityYInWorld =
(normalizedPoseInWorld.YMeters -
_previousPoseInWorld.YMeters) /
deltaTimeSeconds;
var rawAngularVelocity =
AngleMath.ShortestDifferenceRadians(
normalizedPoseInWorld.YawRadians,
_previousPoseInWorld.YawRadians) /
deltaTimeSeconds;
var filteredTwistInWorld =
new Twist2D(
_worldVelocityXFilter.Update(
rawVelocityXInWorld,
deltaTimeSeconds),
_worldVelocityYFilter.Update(
rawVelocityYInWorld,
deltaTimeSeconds),
_angularVelocityFilter.Update(
rawAngularVelocity,
deltaTimeSeconds));
_previousPoseInWorld =
normalizedPoseInWorld;
_previousTimestampSeconds =
sampleTimestampSeconds;
return new VehicleState(
sampleTimestampSeconds,
normalizedPoseInWorld,
filteredTwistInWorld,
true);
}
/// <summary>
/// 更新位姿差分基准但保留当前滤波速度,避免定位跳变形成虚假速度尖峰。
/// </summary>
public VehicleState RebasePreservingVelocity(
Pose2D poseInWorld,
double sampleTimestampSeconds)
{
EnsureFinitePose(
poseInWorld,
nameof(poseInWorld));
EnsureFiniteNonNegative(
sampleTimestampSeconds,
nameof(sampleTimestampSeconds));
var normalizedPoseInWorld =
new Pose2D(
poseInWorld.XMeters,
poseInWorld.YMeters,
AngleMath.NormalizeRadians(
poseInWorld.YawRadians));
_previousPoseInWorld =
normalizedPoseInWorld;
_previousTimestampSeconds =
sampleTimestampSeconds;
_hasPreviousSample = true;
var hasValidVelocityEstimate =
_worldVelocityXFilter.IsInitialized &&
_worldVelocityYFilter.IsInitialized &&
_angularVelocityFilter.IsInitialized;
var retainedTwistInWorld =
hasValidVelocityEstimate
? new Twist2D(
_worldVelocityXFilter.Value,
_worldVelocityYFilter.Value,
_angularVelocityFilter.Value)
: Twist2D.Zero;
return new VehicleState(
sampleTimestampSeconds,
normalizedPoseInWorld,
retainedTwistInWorld,
hasValidVelocityEstimate);
}
/// <summary>
/// 使用当前定位重新建立差分基准,并返回速度无效的零速状态。
/// </summary>
public VehicleState Reset(
Pose2D poseInWorld,
double sampleTimestampSeconds)
{
EnsureFinitePose(
poseInWorld,
nameof(poseInWorld));
EnsureFiniteNonNegative(
sampleTimestampSeconds,
nameof(sampleTimestampSeconds));
_previousPoseInWorld =
new Pose2D(
poseInWorld.XMeters,
poseInWorld.YMeters,
AngleMath.NormalizeRadians(
poseInWorld.YawRadians));
_previousTimestampSeconds =
sampleTimestampSeconds;
_hasPreviousSample = true;
_worldVelocityXFilter.Reset();
_worldVelocityYFilter.Reset();
_angularVelocityFilter.Reset();
return new VehicleState(
sampleTimestampSeconds,
_previousPoseInWorld,
Twist2D.Zero,
false);
}
/// <summary>
/// 清除差分基准和全部滤波历史,使下一帧重新初始化估计器。
/// </summary>
public void Reset()
{
_hasPreviousSample = false;
_previousPoseInWorld = Pose2D.Identity;
_previousTimestampSeconds = 0.0;
_worldVelocityXFilter.Reset();
_worldVelocityYFilter.Reset();
_angularVelocityFilter.Reset();
}
/// <summary>
/// 检查位姿是否由有限数值组成。
/// </summary>
private static void EnsureFinitePose(
Pose2D pose,
string parameterName)
{
if (!IsFinite(pose.XMeters) ||
!IsFinite(pose.YMeters) ||
!IsFinite(pose.YawRadians))
{
throw new ArgumentOutOfRangeException(
parameterName,
"速度估计使用的车辆位姿必须由有限数值组成。");
}
}
/// <summary>
/// 检查数值是否为非负有限值。
/// </summary>
private static void EnsureFiniteNonNegative(
double value,
string parameterName)
{
if (!IsFinite(value) || value < 0.0)
{
throw new ArgumentOutOfRangeException(
parameterName,
"速度估计使用的采样时刻必须是非负有限值。");
}
}
/// <summary>
/// 判断数值是否可用于速度估计。
/// </summary>
private static bool IsFinite(double value)
{
return !double.IsNaN(value) &&
!double.IsInfinity(value);
}
}
}