备份
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -80,25 +80,25 @@ namespace MultiWheelC.StateEstimation
|
|||||||
throw new ArgumentNullException(
|
throw new ArgumentNullException(
|
||||||
nameof(velocityEstimator));
|
nameof(velocityEstimator));
|
||||||
|
|
||||||
EnsureFinitePositive(
|
NumericGuard.EnsureFinitePositive(
|
||||||
maximumLinearSpeedMetersPerSecond,
|
maximumLinearSpeedMetersPerSecond,
|
||||||
nameof(maximumLinearSpeedMetersPerSecond));
|
nameof(maximumLinearSpeedMetersPerSecond));
|
||||||
EnsureFinitePositive(
|
NumericGuard.EnsureFinitePositive(
|
||||||
maximumAngularSpeedRadiansPerSecond,
|
maximumAngularSpeedRadiansPerSecond,
|
||||||
nameof(maximumAngularSpeedRadiansPerSecond));
|
nameof(maximumAngularSpeedRadiansPerSecond));
|
||||||
EnsureFiniteNonNegative(
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
positionJumpMarginMeters,
|
positionJumpMarginMeters,
|
||||||
nameof(positionJumpMarginMeters));
|
nameof(positionJumpMarginMeters));
|
||||||
EnsureFiniteNonNegative(
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
headingJumpMarginRadians,
|
headingJumpMarginRadians,
|
||||||
nameof(headingJumpMarginRadians));
|
nameof(headingJumpMarginRadians));
|
||||||
EnsureFinitePositive(
|
NumericGuard.EnsureFinitePositive(
|
||||||
velocityPositionResidualMeters,
|
velocityPositionResidualMeters,
|
||||||
nameof(velocityPositionResidualMeters));
|
nameof(velocityPositionResidualMeters));
|
||||||
EnsureFinitePositive(
|
NumericGuard.EnsureFinitePositive(
|
||||||
velocityHeadingResidualRadians,
|
velocityHeadingResidualRadians,
|
||||||
nameof(velocityHeadingResidualRadians));
|
nameof(velocityHeadingResidualRadians));
|
||||||
EnsureFinitePositive(
|
NumericGuard.EnsureFinitePositive(
|
||||||
stationaryConfirmationSeconds,
|
stationaryConfirmationSeconds,
|
||||||
nameof(stationaryConfirmationSeconds));
|
nameof(stationaryConfirmationSeconds));
|
||||||
|
|
||||||
@@ -238,9 +238,9 @@ namespace MultiWheelC.StateEstimation
|
|||||||
var location =
|
var location =
|
||||||
DetourInterface.getCartLocation();
|
DetourInterface.getCartLocation();
|
||||||
|
|
||||||
EnsureFinite(location.x, "DetourX");
|
NumericGuard.EnsureFinite(location.x, "DetourX");
|
||||||
EnsureFinite(location.y, "DetourY");
|
NumericGuard.EnsureFinite(location.y, "DetourY");
|
||||||
EnsureFinite(location.th, "DetourTheta");
|
NumericGuard.EnsureFinite(location.th, "DetourTheta");
|
||||||
|
|
||||||
return new Pose2D(
|
return new Pose2D(
|
||||||
location.x / MillimetersPerMeter,
|
location.x / MillimetersPerMeter,
|
||||||
@@ -268,25 +268,6 @@ namespace MultiWheelC.StateEstimation
|
|||||||
return _latestState;
|
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>
|
||||||
/// 接受首帧或静止后的首个新位姿并重新建立零速差分基准。
|
/// 接受首帧或静止后的首个新位姿并重新建立零速差分基准。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -340,7 +321,7 @@ namespace MultiWheelC.StateEstimation
|
|||||||
Pose2D endPoseInWorld,
|
Pose2D endPoseInWorld,
|
||||||
double deltaTimeSeconds)
|
double deltaTimeSeconds)
|
||||||
{
|
{
|
||||||
if (!IsFinite(deltaTimeSeconds) ||
|
if (!NumericGuard.IsFinite(deltaTimeSeconds) ||
|
||||||
deltaTimeSeconds <= 0.0)
|
deltaTimeSeconds <= 0.0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -448,62 +429,5 @@ namespace MultiWheelC.StateEstimation
|
|||||||
HeadingEqualityToleranceRadians;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using MyParking.Shared;
|
||||||
|
|
||||||
namespace MultiWheelC.StateEstimation
|
namespace MultiWheelC.StateEstimation
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@ namespace MultiWheelC.StateEstimation
|
|||||||
public FirstOrderLowPassFilter(
|
public FirstOrderLowPassFilter(
|
||||||
double timeConstantSeconds)
|
double timeConstantSeconds)
|
||||||
{
|
{
|
||||||
EnsureFinitePositive(
|
NumericGuard.EnsureFinitePositive(
|
||||||
timeConstantSeconds,
|
timeConstantSeconds,
|
||||||
nameof(timeConstantSeconds));
|
nameof(timeConstantSeconds));
|
||||||
|
|
||||||
@@ -25,35 +25,6 @@ namespace MultiWheelC.StateEstimation
|
|||||||
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>
|
||||||
/// 使用当前输入和真实采样间隔更新滤波结果。
|
/// 使用当前输入和真实采样间隔更新滤波结果。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -61,10 +32,10 @@ namespace MultiWheelC.StateEstimation
|
|||||||
double input,
|
double input,
|
||||||
double deltaTimeSeconds)
|
double deltaTimeSeconds)
|
||||||
{
|
{
|
||||||
EnsureFinite(
|
NumericGuard.EnsureFinite(
|
||||||
input,
|
input,
|
||||||
nameof(input));
|
nameof(input));
|
||||||
EnsureFinitePositive(
|
NumericGuard.EnsureFinitePositive(
|
||||||
deltaTimeSeconds,
|
deltaTimeSeconds,
|
||||||
nameof(deltaTimeSeconds));
|
nameof(deltaTimeSeconds));
|
||||||
|
|
||||||
@@ -98,7 +69,7 @@ namespace MultiWheelC.StateEstimation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset(double initialValue)
|
public void Reset(double initialValue)
|
||||||
{
|
{
|
||||||
EnsureFinite(
|
NumericGuard.EnsureFinite(
|
||||||
initialValue,
|
initialValue,
|
||||||
nameof(initialValue));
|
nameof(initialValue));
|
||||||
|
|
||||||
@@ -106,37 +77,5 @@ namespace MultiWheelC.StateEstimation
|
|||||||
_isInitialized = true;
|
_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,
|
|
||||||
"滤波输入必须是有限值。");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using MyParking.Shared;
|
using MyParking.Shared;
|
||||||
|
|
||||||
namespace MultiWheelC.StateEstimation
|
namespace MultiWheelC.StateEstimation
|
||||||
@@ -17,13 +16,13 @@ namespace MultiWheelC.StateEstimation
|
|||||||
Twist2D twistInWorld,
|
Twist2D twistInWorld,
|
||||||
bool hasValidVelocityEstimate)
|
bool hasValidVelocityEstimate)
|
||||||
{
|
{
|
||||||
EnsureFiniteNonNegative(
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
sampleTimestampSeconds,
|
sampleTimestampSeconds,
|
||||||
nameof(sampleTimestampSeconds));
|
nameof(sampleTimestampSeconds));
|
||||||
EnsureFinitePose(
|
NumericGuard.EnsureFinite(
|
||||||
poseInWorld,
|
poseInWorld,
|
||||||
nameof(poseInWorld));
|
nameof(poseInWorld));
|
||||||
EnsureFiniteTwist(
|
NumericGuard.EnsureFinite(
|
||||||
twistInWorld,
|
twistInWorld,
|
||||||
nameof(twistInWorld));
|
nameof(twistInWorld));
|
||||||
|
|
||||||
@@ -73,66 +72,9 @@ namespace MultiWheelC.StateEstimation
|
|||||||
public Twist2D TwistInBody { get; }
|
public Twist2D TwistInBody { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前速度是否已由至少两个连续有效定位样本估算得到。
|
/// 获取当前速度估计是否已经初始化并可用于闭环控制。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasValidVelocityEstimate { get; }
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,6 @@ namespace MultiWheelC.StateEstimation
|
|||||||
angularFilterTimeConstantSeconds);
|
angularFilterTimeConstantSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取是否已经保存了可用于下一次差分的位姿基准。
|
|
||||||
/// </summary>
|
|
||||||
public bool HasPreviousSample =>
|
|
||||||
_hasPreviousSample;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用一个新的有效定位样本更新并返回车辆状态。
|
/// 使用一个新的有效定位样本更新并返回车辆状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -65,10 +59,10 @@ namespace MultiWheelC.StateEstimation
|
|||||||
Pose2D poseInWorld,
|
Pose2D poseInWorld,
|
||||||
double sampleTimestampSeconds)
|
double sampleTimestampSeconds)
|
||||||
{
|
{
|
||||||
EnsureFinitePose(
|
NumericGuard.EnsureFinite(
|
||||||
poseInWorld,
|
poseInWorld,
|
||||||
nameof(poseInWorld));
|
nameof(poseInWorld));
|
||||||
EnsureFiniteNonNegative(
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
sampleTimestampSeconds,
|
sampleTimestampSeconds,
|
||||||
nameof(sampleTimestampSeconds));
|
nameof(sampleTimestampSeconds));
|
||||||
|
|
||||||
@@ -135,53 +129,6 @@ namespace MultiWheelC.StateEstimation
|
|||||||
true);
|
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>
|
||||||
/// 使用当前定位重新建立差分基准,并返回速度无效的零速状态。
|
/// 使用当前定位重新建立差分基准,并返回速度无效的零速状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -189,10 +136,10 @@ namespace MultiWheelC.StateEstimation
|
|||||||
Pose2D poseInWorld,
|
Pose2D poseInWorld,
|
||||||
double sampleTimestampSeconds)
|
double sampleTimestampSeconds)
|
||||||
{
|
{
|
||||||
EnsureFinitePose(
|
NumericGuard.EnsureFinite(
|
||||||
poseInWorld,
|
poseInWorld,
|
||||||
nameof(poseInWorld));
|
nameof(poseInWorld));
|
||||||
EnsureFiniteNonNegative(
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
sampleTimestampSeconds,
|
sampleTimestampSeconds,
|
||||||
nameof(sampleTimestampSeconds));
|
nameof(sampleTimestampSeconds));
|
||||||
|
|
||||||
@@ -231,45 +178,5 @@ namespace MultiWheelC.StateEstimation
|
|||||||
_angularVelocityFilter.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ namespace MultiWheelC.StateEstimation
|
|||||||
var rawLongitudinalSpeedMetersPerSecond =
|
var rawLongitudinalSpeedMetersPerSecond =
|
||||||
(double)actualCarSpeed.Vx;
|
(double)actualCarSpeed.Vx;
|
||||||
|
|
||||||
EnsureFinite(
|
NumericGuard.EnsureFinite(
|
||||||
rawLongitudinalSpeedMetersPerSecond,
|
rawLongitudinalSpeedMetersPerSecond,
|
||||||
"电机反馈车体纵向速度");
|
"电机反馈车体纵向速度");
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ namespace MultiWheelC.StateEstimation
|
|||||||
double timestampSeconds,
|
double timestampSeconds,
|
||||||
out bool hasValidWheelSpeedEstimate)
|
out bool hasValidWheelSpeedEstimate)
|
||||||
{
|
{
|
||||||
EnsureFiniteNonNegative(
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
timestampSeconds,
|
timestampSeconds,
|
||||||
nameof(timestampSeconds));
|
nameof(timestampSeconds));
|
||||||
|
|
||||||
@@ -238,37 +238,5 @@ namespace MultiWheelC.StateEstimation
|
|||||||
deltaTimeSeconds);
|
deltaTimeSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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 (double.IsNaN(value) ||
|
|
||||||
double.IsInfinity(value))
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
parameterName,
|
|
||||||
"车辆状态输入必须是有限值。");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,7 +11,6 @@ namespace MyParking.Shared
|
|||||||
public sealed class MultiWheelChassisAdapter
|
public sealed class MultiWheelChassisAdapter
|
||||||
{
|
{
|
||||||
#region 辅助内容
|
#region 辅助内容
|
||||||
private const double RadiansToDegrees = 180.0 / Math.PI;
|
|
||||||
private const float BiasTolerance = 0.001f;
|
private const float BiasTolerance = 0.001f;
|
||||||
private readonly MultiWheelChassis _chassis;
|
private readonly MultiWheelChassis _chassis;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -44,15 +43,12 @@ namespace MyParking.Shared
|
|||||||
get => _chassis.SteeringAlignmentSigmaDegrees;
|
get => _chassis.SteeringAlignmentSigmaDegrees;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (double.IsNaN(value) ||
|
NumericGuard.EnsureFinitePositive(
|
||||||
double.IsInfinity(value) ||
|
value,
|
||||||
value <= 0.0 ||
|
nameof(value));
|
||||||
value > float.MaxValue)
|
EnsureRepresentableAsSingle(
|
||||||
{
|
value,
|
||||||
throw new ArgumentOutOfRangeException(
|
nameof(value));
|
||||||
nameof(value),
|
|
||||||
"Steering alignment sigma must be a positive finite value.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_chassis.SteeringAlignmentSigmaDegrees =
|
_chassis.SteeringAlignmentSigmaDegrees =
|
||||||
(float)value;
|
(float)value;
|
||||||
@@ -74,18 +70,18 @@ namespace MyParking.Shared
|
|||||||
private void EnsureMotionFrameIsActive(
|
private void EnsureMotionFrameIsActive(
|
||||||
double motionDirectionRadians)
|
double motionDirectionRadians)
|
||||||
{
|
{
|
||||||
ValidateFinite(
|
NumericGuard.EnsureFinite(
|
||||||
motionDirectionRadians,
|
motionDirectionRadians,
|
||||||
nameof(motionDirectionRadians));
|
nameof(motionDirectionRadians));
|
||||||
|
|
||||||
var expectedBiasDegrees =
|
var expectedBiasDegrees =
|
||||||
(float)(
|
ConvertRadiansToSingleDegrees(
|
||||||
-FrameTransform2D.NormalizeAngle(
|
-AngleMath.NormalizeRadians(
|
||||||
motionDirectionRadians) *
|
motionDirectionRadians),
|
||||||
RadiansToDegrees);
|
nameof(motionDirectionRadians));
|
||||||
var bias = _chassis.GetOriginBias();
|
var bias = _chassis.GetOriginBias();
|
||||||
var angleErrorDegrees =
|
var angleErrorDegrees =
|
||||||
NormalizeDegrees(
|
AngleMath.NormalizeDegrees(
|
||||||
bias.Z - expectedBiasDegrees);
|
bias.Z - expectedBiasDegrees);
|
||||||
|
|
||||||
if (Math.Abs(bias.X) <= BiasTolerance &&
|
if (Math.Abs(bias.X) <= BiasTolerance &&
|
||||||
@@ -102,46 +98,32 @@ namespace MyParking.Shared
|
|||||||
$"期望Th={expectedBiasDegrees}°。");
|
$"期望Th={expectedBiasDegrees}°。");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将角度归一化到[-180°,180°]附近。
|
|
||||||
/// </summary>
|
|
||||||
private static float NormalizeDegrees(float degrees)
|
|
||||||
{
|
|
||||||
return (float)(
|
|
||||||
degrees -
|
|
||||||
Math.Round(degrees / 360.0) * 360.0);
|
|
||||||
}
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 检查底盘命令是否包含无效数值。
|
/// 检查底盘命令是否包含无效数值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ValidateTwist(Twist2D twist)
|
private static void ValidateTwist(Twist2D twist)
|
||||||
{
|
{
|
||||||
ValidateFinite(
|
EnsureRepresentableAsSingle(
|
||||||
twist.VxMetersPerSecond,
|
twist.VxMetersPerSecond,
|
||||||
nameof(twist.VxMetersPerSecond));
|
nameof(twist.VxMetersPerSecond));
|
||||||
|
|
||||||
ValidateFinite(
|
EnsureRepresentableAsSingle(
|
||||||
twist.VyMetersPerSecond,
|
twist.VyMetersPerSecond,
|
||||||
nameof(twist.VyMetersPerSecond));
|
nameof(twist.VyMetersPerSecond));
|
||||||
|
|
||||||
ValidateFinite(
|
EnsureRepresentableAsSingle(
|
||||||
twist.OmegaRadiansPerSecond,
|
AngleMath.RadiansToDegrees(
|
||||||
|
twist.OmegaRadiansPerSecond),
|
||||||
nameof(twist.OmegaRadiansPerSecond));
|
nameof(twist.OmegaRadiansPerSecond));
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 检查数值是否为有限值。
|
/// 检查数值是否为有限值且可安全转换为float。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ValidateFinite(
|
private static void EnsureRepresentableAsSingle(
|
||||||
double value,
|
double value,
|
||||||
string parameterName)
|
string parameterName)
|
||||||
{
|
{
|
||||||
if (double.IsNaN(value) ||
|
NumericGuard.EnsureFinite(value, parameterName);
|
||||||
double.IsInfinity(value))
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
parameterName,
|
|
||||||
"底盘速度命令不能是NaN或无穷大。");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value > float.MaxValue ||
|
if (value > float.MaxValue ||
|
||||||
value < -float.MaxValue)
|
value < -float.MaxValue)
|
||||||
@@ -152,6 +134,21 @@ namespace MyParking.Shared
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将有限弧度值转换为float可表示的角度值。
|
||||||
|
/// </summary>
|
||||||
|
private static float ConvertRadiansToSingleDegrees(
|
||||||
|
double angleRadians,
|
||||||
|
string parameterName)
|
||||||
|
{
|
||||||
|
var angleDegrees =
|
||||||
|
AngleMath.RadiansToDegrees(angleRadians);
|
||||||
|
EnsureRepresentableAsSingle(
|
||||||
|
angleDegrees,
|
||||||
|
parameterName);
|
||||||
|
return (float)angleDegrees;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取最近一次底盘运动分解失败原因。
|
/// 获取最近一次底盘运动分解失败原因。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -175,15 +172,15 @@ namespace MyParking.Shared
|
|||||||
public void ActivateMotionFrame(
|
public void ActivateMotionFrame(
|
||||||
double motionDirectionRadians)
|
double motionDirectionRadians)
|
||||||
{
|
{
|
||||||
ValidateFinite(
|
NumericGuard.EnsureFinite(
|
||||||
motionDirectionRadians,
|
motionDirectionRadians,
|
||||||
nameof(motionDirectionRadians));
|
nameof(motionDirectionRadians));
|
||||||
|
|
||||||
var biasDegrees =
|
var biasDegrees =
|
||||||
(float)(
|
ConvertRadiansToSingleDegrees(
|
||||||
-FrameTransform2D.NormalizeAngle(
|
-AngleMath.NormalizeRadians(
|
||||||
motionDirectionRadians) *
|
motionDirectionRadians),
|
||||||
RadiansToDegrees);
|
nameof(motionDirectionRadians));
|
||||||
var currentBias =
|
var currentBias =
|
||||||
_chassis.GetOriginBias();
|
_chassis.GetOriginBias();
|
||||||
|
|
||||||
@@ -192,7 +189,7 @@ namespace MyParking.Shared
|
|||||||
Math.Abs(currentBias.Y) <=
|
Math.Abs(currentBias.Y) <=
|
||||||
BiasTolerance &&
|
BiasTolerance &&
|
||||||
Math.Abs(
|
Math.Abs(
|
||||||
NormalizeDegrees(
|
AngleMath.NormalizeDegrees(
|
||||||
currentBias.Z -
|
currentBias.Z -
|
||||||
biasDegrees)) <=
|
biasDegrees)) <=
|
||||||
BiasTolerance)
|
BiasTolerance)
|
||||||
@@ -284,9 +281,9 @@ namespace MyParking.Shared
|
|||||||
var vyMetersPerSecond =
|
var vyMetersPerSecond =
|
||||||
(float)command.BodyTwist.VyMetersPerSecond;
|
(float)command.BodyTwist.VyMetersPerSecond;
|
||||||
var omegaDegreesPerSecond =
|
var omegaDegreesPerSecond =
|
||||||
(float)(
|
ConvertRadiansToSingleDegrees(
|
||||||
command.BodyTwist.OmegaRadiansPerSecond *
|
command.BodyTwist.OmegaRadiansPerSecond,
|
||||||
RadiansToDegrees);
|
nameof(command.BodyTwist.OmegaRadiansPerSecond));
|
||||||
var success = _chassis.SendXYThSpeed(
|
var success = _chassis.SendXYThSpeed(
|
||||||
vxMetersPerSecond,
|
vxMetersPerSecond,
|
||||||
vyMetersPerSecond,
|
vyMetersPerSecond,
|
||||||
@@ -312,13 +309,13 @@ namespace MyParking.Shared
|
|||||||
double steeringRadians,
|
double steeringRadians,
|
||||||
TimeSpan? interval = null)
|
TimeSpan? interval = null)
|
||||||
{
|
{
|
||||||
ValidateFinite(
|
NumericGuard.EnsureFinite(
|
||||||
motionDirectionRadians,
|
motionDirectionRadians,
|
||||||
nameof(motionDirectionRadians));
|
nameof(motionDirectionRadians));
|
||||||
ValidateFinite(
|
EnsureRepresentableAsSingle(
|
||||||
speedMetersPerSecond,
|
speedMetersPerSecond,
|
||||||
nameof(speedMetersPerSecond));
|
nameof(speedMetersPerSecond));
|
||||||
ValidateFinite(
|
NumericGuard.EnsureFinite(
|
||||||
steeringRadians,
|
steeringRadians,
|
||||||
nameof(steeringRadians));
|
nameof(steeringRadians));
|
||||||
EnsureMotionFrameIsActive(
|
EnsureMotionFrameIsActive(
|
||||||
@@ -333,9 +330,9 @@ namespace MyParking.Shared
|
|||||||
}
|
}
|
||||||
|
|
||||||
var steeringDegrees =
|
var steeringDegrees =
|
||||||
(float)(
|
ConvertRadiansToSingleDegrees(
|
||||||
steeringRadians *
|
steeringRadians,
|
||||||
RadiansToDegrees);
|
nameof(steeringRadians));
|
||||||
var success =
|
var success =
|
||||||
_chassis.SendMotion(
|
_chassis.SendMotion(
|
||||||
(float)speedMetersPerSecond,
|
(float)speedMetersPerSecond,
|
||||||
@@ -360,13 +357,13 @@ namespace MyParking.Shared
|
|||||||
double rearAngleRadians,
|
double rearAngleRadians,
|
||||||
TimeSpan? interval = null)
|
TimeSpan? interval = null)
|
||||||
{
|
{
|
||||||
ValidateFinite(
|
EnsureRepresentableAsSingle(
|
||||||
speedMetersPerSecond,
|
speedMetersPerSecond,
|
||||||
nameof(speedMetersPerSecond));
|
nameof(speedMetersPerSecond));
|
||||||
ValidateFinite(
|
NumericGuard.EnsureFinite(
|
||||||
frontAngleRadians,
|
frontAngleRadians,
|
||||||
nameof(frontAngleRadians));
|
nameof(frontAngleRadians));
|
||||||
ValidateFinite(
|
NumericGuard.EnsureFinite(
|
||||||
rearAngleRadians,
|
rearAngleRadians,
|
||||||
nameof(rearAngleRadians));
|
nameof(rearAngleRadians));
|
||||||
EnsureBodyFrameIsActive();
|
EnsureBodyFrameIsActive();
|
||||||
@@ -383,10 +380,12 @@ namespace MyParking.Shared
|
|||||||
|
|
||||||
var success = _chassis.SendMotion(
|
var success = _chassis.SendMotion(
|
||||||
(float)speedMetersPerSecond,
|
(float)speedMetersPerSecond,
|
||||||
(float)(frontAngleRadians *
|
ConvertRadiansToSingleDegrees(
|
||||||
RadiansToDegrees),
|
frontAngleRadians,
|
||||||
(float)(rearAngleRadians *
|
nameof(frontAngleRadians)),
|
||||||
RadiansToDegrees),
|
ConvertRadiansToSingleDegrees(
|
||||||
|
rearAngleRadians,
|
||||||
|
nameof(rearAngleRadians)),
|
||||||
interval);
|
interval);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
@@ -422,8 +421,11 @@ namespace MyParking.Shared
|
|||||||
double directionRadians)
|
double directionRadians)
|
||||||
{
|
{
|
||||||
EnsureBodyFrameIsActive();
|
EnsureBodyFrameIsActive();
|
||||||
var targetDegrees = (float)(FrameTransform2D.NormalizeAngle(directionRadians) *
|
var targetDegrees =
|
||||||
RadiansToDegrees);
|
ConvertRadiansToSingleDegrees(
|
||||||
|
AngleMath.NormalizeRadians(
|
||||||
|
directionRadians),
|
||||||
|
nameof(directionRadians));
|
||||||
|
|
||||||
#pragma warning disable CS0612, CS0618
|
#pragma warning disable CS0612, CS0618
|
||||||
var wheels = _chassis.GetSteerWheels();
|
var wheels = _chassis.GetSteerWheels();
|
||||||
@@ -463,23 +465,21 @@ namespace MyParking.Shared
|
|||||||
double directionRadians,
|
double directionRadians,
|
||||||
double toleranceRadians)
|
double toleranceRadians)
|
||||||
{
|
{
|
||||||
if (double.IsNaN(toleranceRadians) ||
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
double.IsInfinity(toleranceRadians) ||
|
toleranceRadians,
|
||||||
toleranceRadians < 0.0)
|
nameof(toleranceRadians));
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(toleranceRadians),
|
|
||||||
"舵轮到位容差必须是非负有限值。");
|
|
||||||
}
|
|
||||||
|
|
||||||
EnsureBodyFrameIsActive();
|
EnsureBodyFrameIsActive();
|
||||||
var targetDegrees = (float)(
|
var targetDegrees =
|
||||||
FrameTransform2D.NormalizeAngle(directionRadians) *
|
ConvertRadiansToSingleDegrees(
|
||||||
180.0 / Math.PI);
|
AngleMath.NormalizeRadians(
|
||||||
|
directionRadians),
|
||||||
|
nameof(directionRadians));
|
||||||
|
|
||||||
var toleranceDegrees = (float)(
|
var toleranceDegrees =
|
||||||
Math.Abs(toleranceRadians) *
|
ConvertRadiansToSingleDegrees(
|
||||||
180.0 / Math.PI);
|
toleranceRadians,
|
||||||
|
nameof(toleranceRadians));
|
||||||
|
|
||||||
#pragma warning disable CS0612, CS0618
|
#pragma warning disable CS0612, CS0618
|
||||||
var wheels = _chassis.GetSteerWheels();
|
var wheels = _chassis.GetSteerWheels();
|
||||||
@@ -507,16 +507,12 @@ namespace MyParking.Shared
|
|||||||
TimeSpan? interval = null,
|
TimeSpan? interval = null,
|
||||||
double alignmentToleranceDegrees = 2.0)
|
double alignmentToleranceDegrees = 2.0)
|
||||||
{
|
{
|
||||||
ValidateFinite(
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
|
alignmentToleranceDegrees,
|
||||||
|
nameof(alignmentToleranceDegrees));
|
||||||
|
EnsureRepresentableAsSingle(
|
||||||
alignmentToleranceDegrees,
|
alignmentToleranceDegrees,
|
||||||
nameof(alignmentToleranceDegrees));
|
nameof(alignmentToleranceDegrees));
|
||||||
|
|
||||||
if (alignmentToleranceDegrees < 0.0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(alignmentToleranceDegrees),
|
|
||||||
"自转舵轮到位容差必须是非负有限值。");
|
|
||||||
}
|
|
||||||
|
|
||||||
EnsureBodyFrameIsActive();
|
EnsureBodyFrameIsActive();
|
||||||
|
|
||||||
@@ -540,23 +536,18 @@ namespace MyParking.Shared
|
|||||||
double toleranceRadians =
|
double toleranceRadians =
|
||||||
2.0 * Math.PI / 180.0)
|
2.0 * Math.PI / 180.0)
|
||||||
{
|
{
|
||||||
if (double.IsNaN(toleranceRadians) ||
|
NumericGuard.EnsureFiniteNonNegative(
|
||||||
double.IsInfinity(toleranceRadians) ||
|
toleranceRadians,
|
||||||
toleranceRadians < 0.0)
|
nameof(toleranceRadians));
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(toleranceRadians),
|
|
||||||
"自转状态交接容差必须是非负有限值。");
|
|
||||||
}
|
|
||||||
|
|
||||||
EnsureBodyFrameIsActive();
|
EnsureBodyFrameIsActive();
|
||||||
|
|
||||||
var success =
|
var success =
|
||||||
_chassis
|
_chassis
|
||||||
.AdoptPreparedRotateWheelsForXYTh(
|
.AdoptPreparedRotateWheelsForXYTh(
|
||||||
(float)(
|
ConvertRadiansToSingleDegrees(
|
||||||
toleranceRadians *
|
toleranceRadians,
|
||||||
RadiansToDegrees));
|
nameof(toleranceRadians)));
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,27 +9,6 @@ namespace MyParking.Shared
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class FrameTransform2D
|
public static class FrameTransform2D
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 将角度归一化到[-π, π)范围。
|
|
||||||
/// </summary>
|
|
||||||
public static double NormalizeAngle(double angleRadians)
|
|
||||||
{
|
|
||||||
return AngleMath.NormalizeRadians(angleRadians);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 计算从current到target的最短角度差。
|
|
||||||
/// 返回正值表示逆时针旋转。
|
|
||||||
/// </summary>
|
|
||||||
public static double ShortestAngleDifference(
|
|
||||||
double targetRadians,
|
|
||||||
double currentRadians)
|
|
||||||
{
|
|
||||||
return AngleMath.ShortestDifferenceRadians(
|
|
||||||
targetRadians,
|
|
||||||
currentRadians);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将源坐标系中的点变换到目标坐标系。
|
/// 将源坐标系中的点变换到目标坐标系。
|
||||||
/// sourcePoseInTarget表示源坐标系在目标坐标系中的位姿。
|
/// sourcePoseInTarget表示源坐标系在目标坐标系中的位姿。
|
||||||
@@ -106,7 +85,7 @@ namespace MyParking.Shared
|
|||||||
return new Pose2D(
|
return new Pose2D(
|
||||||
childPositionInParent.XMeters,
|
childPositionInParent.XMeters,
|
||||||
childPositionInParent.YMeters,
|
childPositionInParent.YMeters,
|
||||||
NormalizeAngle(
|
AngleMath.NormalizeRadians(
|
||||||
parentFromMiddle.YawRadians +
|
parentFromMiddle.YawRadians +
|
||||||
middleFromChild.YawRadians));
|
middleFromChild.YawRadians));
|
||||||
}
|
}
|
||||||
@@ -127,7 +106,7 @@ namespace MyParking.Shared
|
|||||||
sin * childPoseInParent.XMeters -
|
sin * childPoseInParent.XMeters -
|
||||||
cos * childPoseInParent.YMeters,
|
cos * childPoseInParent.YMeters,
|
||||||
|
|
||||||
NormalizeAngle(
|
AngleMath.NormalizeRadians(
|
||||||
-childPoseInParent.YawRadians));
|
-childPoseInParent.YawRadians));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,36 +81,6 @@ namespace MyParking.Shared
|
|||||||
new Twist2D(0.0, 0.0, 0.0);
|
new Twist2D(0.0, 0.0, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 发送给单辆车的车体坐标系速度命令。
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct ChassisCommand
|
|
||||||
{
|
|
||||||
public ChassisCommand(
|
|
||||||
int vehicleId,
|
|
||||||
Twist2D bodyTwist)
|
|
||||||
{
|
|
||||||
VehicleId = vehicleId;
|
|
||||||
BodyTwist = bodyTwist;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int VehicleId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 单车车体坐标系速度:X向前、Y向左、逆时针旋转为正。
|
|
||||||
/// </summary>
|
|
||||||
public Twist2D BodyTwist { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建指定车辆的停止命令。
|
|
||||||
/// </summary>
|
|
||||||
public static ChassisCommand Stop(int vehicleId)
|
|
||||||
{
|
|
||||||
return new ChassisCommand(
|
|
||||||
vehicleId,
|
|
||||||
Twist2D.Zero);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单辆车的车体坐标系在车队坐标系中的位姿。
|
/// 单辆车的车体坐标系在车队坐标系中的位姿。
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ namespace MyParking.Shared
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class NumericGuard
|
public static class NumericGuard
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 判断指定浮点数是否既不是NaN也不是无穷大。
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsFinite(double value)
|
||||||
|
{
|
||||||
|
return !double.IsNaN(value) &&
|
||||||
|
!double.IsInfinity(value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 确保指定浮点数不是NaN或无穷大。
|
/// 确保指定浮点数不是NaN或无穷大。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -14,8 +23,7 @@ namespace MyParking.Shared
|
|||||||
double value,
|
double value,
|
||||||
string parameterName)
|
string parameterName)
|
||||||
{
|
{
|
||||||
if (double.IsNaN(value) ||
|
if (!IsFinite(value))
|
||||||
double.IsInfinity(value))
|
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(
|
throw new ArgumentOutOfRangeException(
|
||||||
parameterName,
|
parameterName,
|
||||||
@@ -64,17 +72,31 @@ namespace MyParking.Shared
|
|||||||
Pose2D pose,
|
Pose2D pose,
|
||||||
string parameterName)
|
string parameterName)
|
||||||
{
|
{
|
||||||
if (double.IsNaN(pose.XMeters) ||
|
if (!IsFinite(pose.XMeters) ||
|
||||||
double.IsInfinity(pose.XMeters) ||
|
!IsFinite(pose.YMeters) ||
|
||||||
double.IsNaN(pose.YMeters) ||
|
!IsFinite(pose.YawRadians))
|
||||||
double.IsInfinity(pose.YMeters) ||
|
|
||||||
double.IsNaN(pose.YawRadians) ||
|
|
||||||
double.IsInfinity(pose.YawRadians))
|
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(
|
throw new ArgumentOutOfRangeException(
|
||||||
parameterName,
|
parameterName,
|
||||||
"二维位姿必须由有限值组成。");
|
"二维位姿必须由有限值组成。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确保二维刚体速度的两个线速度分量和角速度均为有限值。
|
||||||
|
/// </summary>
|
||||||
|
public static void EnsureFinite(
|
||||||
|
Twist2D twist,
|
||||||
|
string parameterName)
|
||||||
|
{
|
||||||
|
if (!IsFinite(twist.VxMetersPerSecond) ||
|
||||||
|
!IsFinite(twist.VyMetersPerSecond) ||
|
||||||
|
!IsFinite(twist.OmegaRadiansPerSecond))
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
parameterName,
|
||||||
|
"二维刚体速度必须由有限值组成。");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
是的,强烈建议做系统辨识,尤其是你这种要把电机反馈和 SLAM 融合的场景。
|
|
||||||
为什么需要系统辨识?
|
|
||||||
卡尔曼滤波(或 EKF)的效果很大程度上取决于过程模型有多准。模型不准的话,会出现:
|
|
||||||
|
|
||||||
预测步持续往错误方向跑
|
|
||||||
滤波器过度依赖测量(SLAM),或者反过来过度信任错误的模型
|
|
||||||
速度估计系统性偏大/偏小
|
|
||||||
原地自转时航向纠正效果变差
|
|
||||||
|
|
||||||
你现在已经知道电机反馈“偏大”,这本身就是典型的模型参数问题(可能是轮胎半径、减速比、编码器标定、打滑补偿等)。
|
|
||||||
建议辨识的主要参数
|
|
||||||
针对四轮差速车,优先考虑这些:
|
|
||||||
|
|
||||||
运动学参数
|
|
||||||
有效轮半径(或左右轮半径)
|
|
||||||
轮距(左右轮中心距离)
|
|
||||||
可能的轴距相关项(如果不是纯差速)
|
|
||||||
|
|
||||||
速度相关
|
|
||||||
电机速度到实际车速的缩放系数(你提到的“偏大”问题)
|
|
||||||
左右轮速度的不对称性(如果存在)
|
|
||||||
低速段的非线性(静摩擦、死区)
|
|
||||||
|
|
||||||
可选但有用的
|
|
||||||
速度偏置 / 零点漂移
|
|
||||||
简单的打滑系数(如果经常在特定地面跑)
|
|
||||||
|
|
||||||
|
|
||||||
怎么做比较实际?
|
|
||||||
不需要一开始就上很复杂的在线辨识,可以分两步:
|
|
||||||
离线标定(推荐先做)
|
|
||||||
|
|
||||||
让车在平坦地面以不同速度直线行驶、原地旋转,记录:
|
|
||||||
四个电机反馈速度
|
|
||||||
SLAM 给出的位置和航向变化
|
|
||||||
|
|
||||||
用最小二乘或简单优化,拟合出轮半径、轮距、速度缩放系数等
|
|
||||||
原地自转特别适合标定轮距和左右轮差异
|
|
||||||
|
|
||||||
在线微调(可选)
|
|
||||||
|
|
||||||
把关键参数(比如速度缩放系数、轮半径)也放进状态向量里一起估计(Augmented EKF)
|
|
||||||
或者用简单的自适应方法,定期用 SLAM 和电机速度的一致性去微调缩放因子
|
|
||||||
|
|
||||||
实际建议顺序
|
|
||||||
|
|
||||||
先建立基本运动学模型 + EKF 框架
|
|
||||||
做一轮离线系统辨识,把明显偏差(尤其是速度偏大)修正掉
|
|
||||||
再跑融合,看残差(innovation)是否变小、速度估计是否更合理
|
|
||||||
如果还有残余误差,再考虑把个别参数做成在线估计
|
|
||||||
|
|
||||||
总结:建完模型后做系统辨识,是让卡尔曼滤波真正好用的关键一步。不做的话,即使融合了电机和 SLAM,效果也容易打折扣。需要的话,我可以给你一个针对差速车的简单离线标定思路(需要记录哪些数据、怎么拟合)。
|
|
||||||
|
|
||||||
有没有必要做卡尔曼/传感器融合?
|
|
||||||
分两层看:
|
|
||||||
|
|
||||||
如果只是为了速度闭环,且当前绿色线已经能让车跑得比较稳
|
|
||||||
→ 性价比一般。
|
|
||||||
你现在的处理已经解决了最严重的问题,继续花时间做完整的 EKF + 电机融合,短期内收益可能没有那么大。可以先把绿色线再优化一下(比如调整低通截止频率、加一点自适应滤波,或者用更温和的滑动平均),看能不能把残余抖动再压低一点。
|
|
||||||
如果你对速度精度、平滑度要求比较高,或者后面还要做更精细的控制(轨迹跟踪、力控、高精度定位等)
|
|
||||||
→ 值得做,性价比会变高。
|
|
||||||
原因是:
|
|
||||||
绿色线本质还是“从位置差分出来的速度”,天然会有延迟和残余噪声。
|
|
||||||
电机反馈是更直接的速度源,频率高、相位更好。
|
|
||||||
把电机速度和经过处理的 SLAM 差分速度做融合(哪怕是简单的互补滤波或一阶卡尔曼),通常能同时得到:
|
|
||||||
更低的噪声
|
|
||||||
更小的滞后
|
|
||||||
对电机“偏大”问题的一定校正能力
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
实际建议(按性价比排序)
|
|
||||||
|
|
||||||
先低成本优化现有绿色线(最快)
|
|
||||||
检查低通滤波的截止频率是否合适(太低会滞后,太高会留噪声)。
|
|
||||||
可以尝试一阶低通 + 中值滤波组合,或者简单的互补滤波(位置差分 + 电机速度)。
|
|
||||||
看看闭环时车的实际表现,如果抖动已经可接受,就先用着。
|
|
||||||
|
|
||||||
中等成本:做简单融合(推荐优先考虑)
|
|
||||||
不用完整 EKF,先做一个互补滤波或一维卡尔曼只融速度:
|
|
||||||
高频信任电机反馈速度
|
|
||||||
低频用处理后的 SLAM 差分速度去校正偏差
|
|
||||||
|
|
||||||
实现简单,效果通常比单纯低通好一截,滞后也更小。
|
|
||||||
|
|
||||||
完整 EKF + 系统辨识
|
|
||||||
只有在你需要同时提升位置、航向、速度的整体一致性,或者后面要上更复杂的控制时,再上这个。
|
|
||||||
工作量明显更大,但长期收益最高。
|
|
||||||
|
|
||||||
|
|
||||||
总结
|
|
||||||
从你这张图来看,目前绿色线已经能用,不是“必须立刻上卡尔曼”的紧急情况。
|
|
||||||
但如果你觉得绿色线的残余抖动还是影响闭环效果,或者想要更干净、滞后更小的速度,那么把四个电机反馈融进去(哪怕先做简单融合)是性价比不错的下一步,比继续死磕低通滤波更有前途。
|
|
||||||
你现在速度闭环时,车实际表现如何?是觉得绿色线的抖动已经导致控制不稳,还是只是看着不舒服?这能帮助判断要不要马上上融合。
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user