// 将统一命令转换为原 Chassis API 调用 using System; using CommonUsage.Chassis; namespace MyParking.Shared { /// /// 将统一的单车车体速度命令转换为旧版MultiWheelChassis调用。 /// 车体坐标系固定为X向前、Y向左、逆时针为正。 /// public sealed class MultiWheelChassisAdapter { #region 辅助内容 private const float BiasTolerance = 0.001f; private const double MotionDeadband = 1e-6; private readonly MultiWheelChassis _chassis; private double _activeMotionDirectionRadians; /// /// 当前适配器对应的车辆编号。 /// public int VehicleId { get; } /// /// Maximum distance from the body origin to a wheel center, in metres. /// public double MaximumWheelRadiusMeters { get; } /// /// Maximum longitudinal wheel offset from the body origin, in metres. /// For a symmetric four-wheel-steering chassis this is half the wheelbase. /// public double HalfWheelBaseMeters { get; } /// /// 车体原点到最外侧舵轮中心的最大横向距离,单位为米。 /// 对称四舵轮底盘中,它通常等于物理轮距的一半。 /// public double HalfTrackWidthMeters { get; } /// /// 获取旧版SendMotion使用的对称前后GCP半径,单位为m。 /// public double ControlPointRadiusMeters => _chassis.ControlPointRadius / 1000.0; /// /// 获取当前已经准备并激活的滚动运动系X轴在真实车体系中的方向,单位为rad。 /// public double ActiveMotionDirectionRadians => _activeMotionDirectionRadians; /// /// Width of the steering-alignment speed gate, in degrees. /// public double SteeringAlignmentSigmaDegrees { get => _chassis.SteeringAlignmentSigmaDegrees; set { NumericGuard.EnsureFinitePositive( value, nameof(value)); EnsureRepresentableAsSingle( value, nameof(value)); _chassis.SteeringAlignmentSigmaDegrees = (float)value; } } /// /// 检查旧底盘是否仍处于无偏置的真实车体坐标系。 /// private void EnsureBodyFrameIsActive() { EnsureMotionFrameIsActive(0.0); } /// /// 检查旧底盘当前是否处于指定的运动坐标系。 /// motionDirectionRadians表示该运动系X轴在真实车体坐标系中的方向。 /// private void EnsureMotionFrameIsActive( double motionDirectionRadians) { NumericGuard.EnsureFinite( motionDirectionRadians, nameof(motionDirectionRadians)); var expectedBiasDegrees = ConvertRadiansToSingleDegrees( -AngleMath.NormalizeRadians( motionDirectionRadians), nameof(motionDirectionRadians)); var bias = _chassis.GetOriginBias(); var angleErrorDegrees = AngleMath.NormalizeDegrees( bias.Z - expectedBiasDegrees); if (Math.Abs(bias.X) <= BiasTolerance && Math.Abs(bias.Y) <= BiasTolerance && Math.Abs(angleErrorDegrees) <= BiasTolerance) { return; } throw new InvalidOperationException( "MultiWheelChassis当前运动坐标系与命令不一致。" + $"当前偏置为X={bias.X}, Y={bias.Y}, Th={bias.Z}°," + $"期望Th={expectedBiasDegrees}°。"); } /// /// 检查底盘命令是否包含无效数值。 /// private static void ValidateTwist(Twist2D twist) { EnsureRepresentableAsSingle( twist.VxMetersPerSecond, nameof(twist.VxMetersPerSecond)); EnsureRepresentableAsSingle( twist.VyMetersPerSecond, nameof(twist.VyMetersPerSecond)); EnsureRepresentableAsSingle( AngleMath.RadiansToDegrees( twist.OmegaRadiansPerSecond), nameof(twist.OmegaRadiansPerSecond)); } /// /// 检查数值是否为有限值且可安全转换为float。 /// private static void EnsureRepresentableAsSingle( double value, string parameterName) { NumericGuard.EnsureFinite(value, parameterName); if (value > float.MaxValue || value < -float.MaxValue) { throw new ArgumentOutOfRangeException( parameterName, "底盘速度命令超过float可表示范围。"); } } /// /// 将有限弧度值转换为float可表示的角度值。 /// private static float ConvertRadiansToSingleDegrees( double angleRadians, string parameterName) { var angleDegrees = AngleMath.RadiansToDegrees(angleRadians); EnsureRepresentableAsSingle( angleDegrees, parameterName); return (float)angleDegrees; } /// /// 获取最近一次底盘运动分解失败原因。 /// public string LastFailureReason => _chassis.LastMotionDecomposeFailureReason; #endregion /// /// 将旧底盘的原点偏置恢复为真实单车车体坐标系。 /// public void ResetToBodyFrame() { ActivateMotionFrame(0.0); } /// /// 激活指定运动方向对应的SendMotion坐标系。 /// 0表示真实车头,正90度表示将车体左侧作为虚拟车头。 /// 调用方必须先停车,并确认舵轮已经按该方向完成预对齐。 /// public void ActivateMotionFrame( double motionDirectionRadians) { NumericGuard.EnsureFinite( motionDirectionRadians, nameof(motionDirectionRadians)); var normalizedDirectionRadians = AngleMath.NormalizeRadians( motionDirectionRadians); var biasDegrees = ConvertRadiansToSingleDegrees( -normalizedDirectionRadians, nameof(motionDirectionRadians)); var currentBias = _chassis.GetOriginBias(); if (Math.Abs(currentBias.X) <= BiasTolerance && Math.Abs(currentBias.Y) <= BiasTolerance && Math.Abs( AngleMath.NormalizeDegrees( currentBias.Z - biasDegrees)) <= BiasTolerance) { SetActiveMotionDirection( normalizedDirectionRadians); return; } _chassis.SetOriginBias( x: 0.0f, y: 0.0f, th: biasDegrees); SetActiveMotionDirection( normalizedDirectionRadians); } /// /// 缓存当前运动坐标系方向,供控制周期内转换车体速度。 /// private void SetActiveMotionDirection( double motionDirectionRadians) { _activeMotionDirectionRadians = AngleMath.NormalizeRadians( motionDirectionRadians); } public MultiWheelChassisAdapter(MultiWheelChassis chassis, int vehicleId) { _chassis = chassis ?? throw new ArgumentNullException(nameof(chassis)); if (vehicleId <= 0) { throw new ArgumentOutOfRangeException( nameof(vehicleId), "车辆编号必须大于零。"); } VehicleId = vehicleId; #pragma warning disable CS0612, CS0618 var wheels = _chassis.GetSteerWheels(); #pragma warning restore CS0612, CS0618 if (wheels.Count == 0) { throw new InvalidOperationException( "MultiWheelChassis尚未完成舵轮初始化," + "不能创建底盘适配器。"); } // 禁用旧版DirectionAngle/ZeroDirection坐标偏置, // 保证SendXYThSpeed直接使用真实车体坐标系。 var maximumWheelRadiusMillimeters = 0.0; var maximumLongitudinalOffsetMillimeters = 0.0; var maximumLateralOffsetMillimeters = 0.0; foreach (var wheel in wheels) { maximumWheelRadiusMillimeters = Math.Max( maximumWheelRadiusMillimeters, wheel.PhysicalPosition.Length()); maximumLongitudinalOffsetMillimeters = Math.Max( maximumLongitudinalOffsetMillimeters, Math.Abs(wheel.PhysicalPosition.X)); maximumLateralOffsetMillimeters = Math.Max( maximumLateralOffsetMillimeters, Math.Abs(wheel.PhysicalPosition.Y)); } MaximumWheelRadiusMeters = maximumWheelRadiusMillimeters / 1000.0; HalfWheelBaseMeters = maximumLongitudinalOffsetMillimeters / 1000.0; HalfTrackWidthMeters = maximumLateralOffsetMillimeters / 1000.0; if (MaximumWheelRadiusMeters <= 0.0 || HalfWheelBaseMeters <= 0.0 || HalfTrackWidthMeters <= 0.0 || ControlPointRadiusMeters <= 0.0) { throw new InvalidOperationException( "Wheel positions and ControlPointRadius must produce valid chassis dimensions."); } var initialBias = _chassis.GetOriginBias(); SetActiveMotionDirection( -AngleMath.DegreesToRadians( initialBias.Z)); // 通过反转轮速表达反向运动,避免蟹行正反切换时舵轮无意义地旋转180°。 _chassis.PreferMinimumSteeringTravel = true; } /// /// 将车体坐标系刚体速度统一转换为滚动SendMotion、原地自转或停车命令。 /// 非零平移命令使用调用方在运动段开始前已经准备并激活的β运动坐标系。 /// public bool SendBodyTwist( Twist2D bodyTwist, TimeSpan? interval = null) { ValidateTwist(bodyTwist); var linearSpeedMetersPerSecond = Math.Sqrt( bodyTwist.VxMetersPerSecond * bodyTwist.VxMetersPerSecond + bodyTwist.VyMetersPerSecond * bodyTwist.VyMetersPerSecond); if (linearSpeedMetersPerSecond <= MotionDeadband) { if (Math.Abs( bodyTwist.OmegaRadiansPerSecond) <= MotionDeadband) { StopImmediately(); return true; } return SendPureRotation( bodyTwist.OmegaRadiansPerSecond, interval); } return SendRollingTwistInActiveMotionFrame( bodyTwist, linearSpeedMetersPerSecond, interval); } /// /// 将车体刚体速度转换到已准备的运动坐标系,并生成该坐标系中的前后GCP方向。 /// private bool SendRollingTwistInActiveMotionFrame( Twist2D bodyTwist, double linearSpeedMetersPerSecond, TimeSpan? interval) { EnsureMotionFrameIsActive( _activeMotionDirectionRadians); var bodyPoseInMotionFrame = new Pose2D( 0.0, 0.0, -_activeMotionDirectionRadians); var motionTwist = FrameTransform2D.TransformTwistAtSamePoint( bodyPoseInMotionFrame, bodyTwist); var motionVxMetersPerSecond = motionTwist.VxMetersPerSecond; var motionVyMetersPerSecond = motionTwist.VyMetersPerSecond; if (Math.Abs(motionVxMetersPerSecond) <= MotionDeadband) { StopImmediately(); throw new InvalidOperationException( "当前车体速度几乎垂直于已经准备的运动坐标系," + "无法由方向型前后GCP稳定表示。请停车后按目标主运动方向重新准备并激活β。"); } var travelDirection = Math.Sign( motionVxMetersPerSecond); var signedCenterSpeedMetersPerSecond = travelDirection * linearSpeedMetersPerSecond; var frontVelocityYMetersPerSecond = motionVyMetersPerSecond + bodyTwist.OmegaRadiansPerSecond * ControlPointRadiusMeters; var rearVelocityYMetersPerSecond = motionVyMetersPerSecond - bodyTwist.OmegaRadiansPerSecond * ControlPointRadiusMeters; var directedVxMetersPerSecond = travelDirection * motionVxMetersPerSecond; var frontAngleRadians = Math.Atan2( travelDirection * frontVelocityYMetersPerSecond, directedVxMetersPerSecond); var rearAngleRadians = Math.Atan2( travelDirection * rearVelocityYMetersPerSecond, directedVxMetersPerSecond); return SendGcpMotionInActiveFrame( signedCenterSpeedMetersPerSecond, frontAngleRadians, rearAngleRadians, interval); } /// /// 将已经完成自转舵轮准备的纯角速度命令交给XYTh底盘解算。 /// private bool SendPureRotation( double omegaRadiansPerSecond, TimeSpan? interval) { EnsureBodyFrameIsActive(); var success = _chassis.SendXYThSpeed( 0.0f, 0.0f, ConvertRadiansToSingleDegrees( omegaRadiansPerSecond, nameof(omegaRadiansPerSecond)), interval, enableDifferentialSteerFeedforward: true); if (!success) { _chassis.PredefinedDriveStop(); } return success; } /// /// 在当前已激活的运动坐标系中将有符号速度和前后GCP角度发送给旧版SendMotion。 /// private bool SendGcpMotionInActiveFrame( double speedMetersPerSecond, double frontAngleRadians, double rearAngleRadians, TimeSpan? interval = null) { EnsureRepresentableAsSingle( speedMetersPerSecond, nameof(speedMetersPerSecond)); NumericGuard.EnsureFinite( frontAngleRadians, nameof(frontAngleRadians)); NumericGuard.EnsureFinite( rearAngleRadians, nameof(rearAngleRadians)); EnsureMotionFrameIsActive( _activeMotionDirectionRadians); if (Math.Abs(frontAngleRadians) >= Math.PI / 2.0 || Math.Abs(rearAngleRadians) >= Math.PI / 2.0) { throw new ArgumentOutOfRangeException( nameof(frontAngleRadians), "前后GCP角度必须位于正负90度以内,避免四轮几何解算出现奇异值。"); } var success = _chassis.SendMotion( (float)speedMetersPerSecond, ConvertRadiansToSingleDegrees( frontAngleRadians, nameof(frontAngleRadians)), ConvertRadiansToSingleDegrees( rearAngleRadians, nameof(rearAngleRadians)), interval); if (!success) { // 分解失败后立即清除上一条驱动速度,避免车辆继续执行陈旧命令。 _chassis.PredefinedDriveStop(); } return success; } /// /// 立即将所有驱动轮速度下发为零。 /// public void StopImmediately() { _chassis.PredefinedDriveStop(); } /// /// 清零XYTh驱动速度,但保留已经准备好的自转舵角和轮速方向。 /// public void StopXYThDrivePreserveSteeringState() { _chassis.StopXYThDrivePreserveSteeringState(); } /// /// 停车并将所有舵轮转到指定的车体角度。 /// 只调整舵轮角度,不产生车辆线速度。 /// public bool PrepareParallelDirection( double directionRadians) { EnsureBodyFrameIsActive(); var targetDegrees = ConvertRadiansToSingleDegrees( AngleMath.NormalizeRadians( directionRadians), nameof(directionRadians)); #pragma warning disable CS0612, CS0618 var wheels = _chassis.GetSteerWheels(); #pragma warning restore CS0612, CS0618 // 没有舵轮时不能认为预对齐成功。 if (wheels.Count == 0) { return false; } // 先检查所有舵轮能否到达目标机械角度。 foreach (var wheel in wheels) { if (targetDegrees < wheel.AngleLowerLimit || targetDegrees > wheel.AngleUpperLimit) { return false; } } // 模式切换前立即停止驱动轮。 _chassis.PredefinedDriveStop(); // 检查完成后再统一下发,避免只转动一部分舵轮。 foreach (var wheel in wheels) { wheel.WriteAngle(targetDegrees); } return true; } /// /// 检查所有舵轮是否已经对准给定方向。 /// public bool AreParallelWheelsAligned( double directionRadians, double toleranceRadians) { NumericGuard.EnsureFiniteNonNegative( toleranceRadians, nameof(toleranceRadians)); EnsureBodyFrameIsActive(); var targetDegrees = ConvertRadiansToSingleDegrees( AngleMath.NormalizeRadians( directionRadians), nameof(directionRadians)); var toleranceDegrees = ConvertRadiansToSingleDegrees( toleranceRadians, nameof(toleranceRadians)); #pragma warning disable CS0612, CS0618 var wheels = _chassis.GetSteerWheels(); #pragma warning restore CS0612, CS0618 foreach (var wheel in wheels) { var angleErrorDegrees = targetDegrees - wheel.ReadAngle(); if (Math.Abs(angleErrorDegrees) > toleranceDegrees) { return false; } } return true; } /// /// 停车并将舵轮预对齐到原地自转方向。 /// 返回是否成功生成舵轮目标。 /// public bool PrepareSpin( TimeSpan? interval = null, double alignmentToleranceDegrees = 2.0) { NumericGuard.EnsureFiniteNonNegative( alignmentToleranceDegrees, nameof(alignmentToleranceDegrees)); EnsureRepresentableAsSingle( alignmentToleranceDegrees, nameof(alignmentToleranceDegrees)); EnsureBodyFrameIsActive(); var success = _chassis.PrepareRotateWheels( alignmentToleranceDegrees: (float)alignmentToleranceDegrees); if (!success) { _chassis.PredefinedDriveStop(); } return success; } /// /// 将已到位的自转舵角和轮速方向一次性交接给XYTh, /// 防止普通SendXYThSpeed正式运动首帧重新初始化运动状态。 /// public bool AdoptPreparedSpinForXYTh( double toleranceRadians = 2.0 * Math.PI / 180.0) { NumericGuard.EnsureFiniteNonNegative( toleranceRadians, nameof(toleranceRadians)); EnsureBodyFrameIsActive(); var success = _chassis .AdoptPreparedRotateWheelsForXYTh( ConvertRadiansToSingleDegrees( toleranceRadians, nameof(toleranceRadians))); if (!success) { _chassis.PredefinedDriveStop(); } return success; } /// /// 所有舵轮是否已对齐到原地自转方向。 /// public bool AreSpinWheelsAligned => _chassis.LastRotateAligned; } }