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