using System; using System.Collections.Generic; using ClumsyCore.Interfaces; using ClumsyCore.Pilot; using CommonUsage.Chassis; using MDCSToolBox.Commons.Controllers; using MyParking.Shared; using MultiWheelC.StateEstimation; namespace MultiWheelC { /// /// 指定原地自转使用Detour绝对航向或轮组相对角度反馈。 /// public enum InPlaceRotationFeedbackMode { DetourAbsoluteHeading, RelativeWheelOdometry } /// /// 将四个舵轮准备到自转姿态,按所选反馈旋转并在完成后等待舵轮回正。 /// public class MultiWheelRotateInPlace : MovementDefinition { /// /// Detour模式表示世界目标航向,轮组模式表示有符号相对旋转角度,单位deg。 /// public float AngleTarget; /// /// 获取或设置原地自转反馈模式;默认保持现有Detour绝对航向闭环。 /// public InPlaceRotationFeedbackMode FeedbackMode = InPlaceRotationFeedbackMode.DetourAbsoluteHeading; // 留作标定或单元测试时显式替换;为空时使用配置化Detour与电机反馈组合状态源。 public Func ThetaReader; public IVehicleStateProvider StateProvider; public MultiWheelChassis Chassis = PilotDefinition.Chassis as MultiWheelChassis; /// /// 获取或设置本次动作的PID参数读取覆盖;为空时读取车辆配置。 /// public Func PidparamsRead; public PIDController thPid; // 将本周期PID角速度输出提供给实验记录器,单位deg/s。 public Action CommandAngularSpeedObserver; // 自转前舵轮实际角度允许误差覆盖值,单位deg;为空时读取车辆配置。 public float? WheelAlignmentToleranceDegrees; // 自转舵轮连续保持到位的时间,单位s。 public float WheelAlignmentStableSeconds = 0.3f; // 自转舵轮准备超时时间,单位s。 public float WheelAlignmentTimeoutSeconds = 10f; // 航向尚未到位时允许下发的最小有效角速度覆盖值,单位deg/s;为空时读取车辆配置。 public float? MinimumAngularSpeedDegreesPerSecond; // 舵轮到位后执行航向闭环允许的最长时间覆盖值,单位s;为空时读取车辆配置。 public float? RotationTimeoutSeconds; /// /// 读取一次有效配置,闭环旋转到目标航向并在正常完成后等待舵轮稳定回正。 /// public override IEnumerable Get() { if (Chassis == null) throw new InvalidOperationException( "当前底盘不是MultiWheelChassis,无法执行原地自转。"); var config = PilotDefinition.Conf; var pidParameters = PidparamsRead == null ? new PIDParams { Kp = config.InPlaceRotateKp, Ki = config.InPlaceRotateKi, Kd = config.InPlaceRotateKd, MaxI = config.InPlaceRotateMaxI, DeadZone = config.InPlaceRotateArriveDeg, SpeedAccPerSec = config.InPlaceRotateAcc, OutputUpperThreshold = config.InPlaceRotateMaxSpeed } : PidparamsRead(); var wheelAlignmentToleranceDegrees = WheelAlignmentToleranceDegrees ?? config.InPlaceRotateWheelAlignDeg; var minimumAngularSpeedDegreesPerSecond = MinimumAngularSpeedDegreesPerSecond ?? config.InPlaceRotateMinimumSpeed; var rotationTimeoutSeconds = RotationTimeoutSeconds ?? config.InPlaceRotateTimeoutSec; var stateProvider = StateProvider ?? ParkingVehicleStateProviderFactory.Create( Chassis, config); ValidateParameters( pidParameters, wheelAlignmentToleranceDegrees, minimumAngularSpeedDegreesPerSecond, rotationTimeoutSeconds); var adapter = new MultiWheelChassisAdapter( Chassis, PilotDefinition.Self.CarNum); adapter.ResetToBodyFrame(); try { var alignmentStarted = DateTime.Now; DateTime? alignedSince = null; while (true) { if (!adapter.PrepareSpin( alignmentToleranceDegrees: wheelAlignmentToleranceDegrees)) throw new InvalidOperationException( "无法生成原地自转舵轮目标:" + adapter.LastFailureReason); if (adapter.AreSpinWheelsAligned) { if (alignedSince == null) alignedSince = DateTime.Now; if ((DateTime.Now - alignedSince.Value) .TotalSeconds >= WheelAlignmentStableSeconds) break; } else { alignedSince = null; } if ((DateTime.Now - alignmentStarted) .TotalSeconds > WheelAlignmentTimeoutSeconds) throw new TimeoutException( "原地自转舵轮在限定时间内未稳定到位。"); yield return true; } var alignmentToleranceRadians = AngleMath.DegreesToRadians( wheelAlignmentToleranceDegrees); if (!adapter.AdoptPreparedSpinForXYTh( alignmentToleranceRadians)) { throw new InvalidOperationException( "无法将已到位的自转舵角交接给XYTh:" + adapter.LastFailureReason); } var useRelativeWheelOdometry = FeedbackMode == InPlaceRotationFeedbackMode .RelativeWheelOdometry; var wheelStateProvider = useRelativeWheelOdometry ? stateProvider as WheelFeedbackVehicleStateProvider : null; if (useRelativeWheelOdometry && wheelStateProvider == null) { throw new InvalidOperationException( "轮组相对角度自转需要" + "WheelFeedbackVehicleStateProvider。"); } var targetAngle = useRelativeWheelOdometry ? AngleTarget : (float)AngleMath.NormalizeDegrees( AngleTarget); var currentAngle = useRelativeWheelOdometry ? 0f : ReadCurrentAngleDegrees(stateProvider); var cachedCurrentAngle = currentAngle; thPid = new PIDController( () => cachedCurrentAngle, pidParameters.Kp); thPid.ChangeParameters( pidParameters.Kp, pidParameters.Ki, pidParameters.Kd, pidParameters.MaxI, pidParameters.DeadZone, pidParameters.OutputUpperThreshold, pidParameters.SpeedAccPerSec); var lastCommandTime = DateTime.Now; var rotationStarted = DateTime.Now; var accumulatedWheelAngleRadians = 0.0; var previousWheelOmegaRadiansPerSecond = 0.0; var previousWheelTimestampSeconds = 0.0; var hasPreviousWheelSample = false; while (true) { if ((DateTime.Now - rotationStarted) .TotalSeconds > rotationTimeoutSeconds) { throw new TimeoutException( $"原地自转超过{rotationTimeoutSeconds:F1}s仍未到位。"); } if (useRelativeWheelOdometry) { if (!wheelStateProvider.TryGetWheelTwist( out var wheelTwist, out var wheelTimestampSeconds)) { CommandAngularSpeedObserver?.Invoke(0f); adapter .StopXYThDrivePreserveSteeringState(); if (hasPreviousWheelSample) { throw new InvalidOperationException( "原地自转期间轮组角速度不可用:" + wheelStateProvider.LastFailureReason); } yield return true; continue; } if (hasPreviousWheelSample) { var wheelDeltaTimeSeconds = wheelTimestampSeconds - previousWheelTimestampSeconds; if (!NumericGuard.IsFinite( wheelDeltaTimeSeconds) || wheelDeltaTimeSeconds <= 0.0) { throw new InvalidOperationException( "轮组角速度采样时间没有单调递增。"); } accumulatedWheelAngleRadians += 0.5 * (previousWheelOmegaRadiansPerSecond + wheelTwist .OmegaRadiansPerSecond) * wheelDeltaTimeSeconds; } previousWheelOmegaRadiansPerSecond = wheelTwist.OmegaRadiansPerSecond; previousWheelTimestampSeconds = wheelTimestampSeconds; hasPreviousWheelSample = true; currentAngle = (float)AngleMath.RadiansToDegrees( accumulatedWheelAngleRadians); } else { currentAngle = ReadCurrentAngleDegrees(stateProvider); } cachedCurrentAngle = currentAngle; var s = thPid.GetResponse( targetAngle, !useRelativeWheelOdometry); var angleErrorDegrees = useRelativeWheelOdometry ? targetAngle - currentAngle : (float)AngleMath .ShortestDifferenceDegrees( targetAngle, currentAngle); // PID进入到位死区后等待其0.3s稳定确认;等待期间 // 只清零驱动速度,不清除已经准备好的自转舵角状态。 if (Math.Abs(angleErrorDegrees) <= pidParameters.DeadZone) { CommandAngularSpeedObserver?.Invoke(0f); adapter .StopXYThDrivePreserveSteeringState(); // 到位稳定只依赖已经单独校验的航向。 // 位置候选留到停车后处理,避免位置抖动中断航向闭环。 if (thPid.IsArrived()) break; yield return true; continue; } // PID输出低于底盘有效轮速范围时提高到最小可执行值, // 避免接近目标时反复出现微小命令但车辆实际不动。 if (Math.Abs(s) > 1e-6f && Math.Abs(s) < minimumAngularSpeedDegreesPerSecond) { s = Math.Sign(angleErrorDegrees) * minimumAngularSpeedDegreesPerSecond; } // PID加速限制在首周期可能暂时输出零;此时保留 // 已交接的自转状态,等待下一周期产生有效角速度。 if (Math.Abs(s) <= 1e-6f) { CommandAngularSpeedObserver?.Invoke(0f); adapter .StopXYThDrivePreserveSteeringState(); yield return true; continue; } CommandAngularSpeedObserver?.Invoke(s); var now = DateTime.Now; var interval = now - lastCommandTime; lastCommandTime = now; // PID输出s为deg/s,Shared统一使用车体坐标系Twist2D和rad/s。 var omegaRadiansPerSecond = (float)AngleMath.DegreesToRadians(s); if (!adapter.SendBodyTwist( new Twist2D( 0.0, 0.0, omegaRadiansPerSecond), interval)) { throw new InvalidOperationException( "安全XYTh原地旋转底盘解算失败:" + adapter.LastFailureReason); } yield return true; } CommandAngularSpeedObserver?.Invoke(0f); adapter.StopXYThDrivePreserveSteeringState(); if (!useRelativeWheelOdometry && IsLocalizationRecoveryPending( stateProvider)) { BeginPostRotationPositionRecovery( stateProvider); var recoveryStarted = DateTime.Now; while (true) { adapter .StopXYThDrivePreserveSteeringState(); if (stateProvider.TryGetState(out _) && !IsLocalizationRecoveryPending( stateProvider)) { break; } if ((DateTime.Now - recoveryStarted) .TotalSeconds > config .ParkingDetourJumpConfirmationTimeoutSeconds) { throw new InvalidOperationException( "原地自转完成后Detour位置在限定时间内未恢复。" + GetStateProviderFailureReason( stateProvider)); } yield return true; } } // 航向正常到位后复用统一回正动作;异常或取消会直接进入finally停车。 var wheelPreparation = new PrepareWheelsForward(); foreach (var keepRunning in wheelPreparation.Get()) { if (!keepRunning) { break; } yield return true; } if (!wheelPreparation.Completed) { throw new InvalidOperationException( "原地自转完成后舵轮未能稳定回到车头方向。"); } Console.WriteLine( useRelativeWheelOdometry ? "final relative wheel rotate to " + $"{currentAngle:F2}deg, wheels forward" : $"final rotate to {targetAngle}, wheels forward"); } finally { CommandAngularSpeedObserver?.Invoke(0f); adapter.StopImmediately(); } } /// /// 检查原地自转的舵轮准备、最小速度和超时参数是否可执行。 /// private void ValidateParameters( PIDParams pidParameters, float wheelAlignmentToleranceDegrees, float minimumAngularSpeedDegreesPerSecond, float rotationTimeoutSeconds) { EnsureFinitePositive( wheelAlignmentToleranceDegrees, nameof(WheelAlignmentToleranceDegrees), allowZero: true); EnsureFinitePositive( WheelAlignmentStableSeconds, nameof(WheelAlignmentStableSeconds), allowZero: true); EnsureFinitePositive( WheelAlignmentTimeoutSeconds, nameof(WheelAlignmentTimeoutSeconds)); EnsureFinitePositive( minimumAngularSpeedDegreesPerSecond, nameof(MinimumAngularSpeedDegreesPerSecond)); EnsureFinitePositive( rotationTimeoutSeconds, nameof(RotationTimeoutSeconds)); if (float.IsNaN(AngleTarget) || float.IsInfinity(AngleTarget)) { throw new ArgumentOutOfRangeException( nameof(AngleTarget), "原地自转目标角度必须是有限值。"); } if (!Enum.IsDefined( typeof(InPlaceRotationFeedbackMode), FeedbackMode)) { throw new ArgumentOutOfRangeException( nameof(FeedbackMode), "原地自转反馈模式无效。"); } if (FeedbackMode == InPlaceRotationFeedbackMode .RelativeWheelOdometry && Math.Abs(AngleTarget) >= 180f) { throw new ArgumentOutOfRangeException( nameof(AngleTarget), "轮组相对自转角度必须满足-180° < angle < 180°。"); } if (pidParameters == null) { throw new InvalidOperationException( "原地自转PID参数读取结果为空。"); } EnsureFinitePositive( pidParameters.DeadZone, "PidparamsRead.DeadZone"); EnsureFinitePositive( pidParameters.OutputUpperThreshold, "PidparamsRead.OutputUpperThreshold"); EnsureFinitePositive( pidParameters.SpeedAccPerSec, "PidparamsRead.SpeedAccPerSec"); EnsureFinitePositive( pidParameters.Kp, "PidparamsRead.Kp"); if (minimumAngularSpeedDegreesPerSecond > pidParameters.OutputUpperThreshold) { throw new InvalidOperationException( "原地自转最小有效角速度不能大于最大角速度。"); } } /// /// 读取经过状态源校验的世界航向,显式设置ThetaReader时优先使用替代读数。 /// private float ReadCurrentAngleDegrees( IVehicleStateProvider stateProvider) { if (ThetaReader != null) { var angleDegrees = ThetaReader(); if (float.IsNaN(angleDegrees) || float.IsInfinity(angleDegrees)) { throw new InvalidOperationException( "自定义航向读取结果不是有效角度。"); } return (float)AngleMath.NormalizeDegrees( angleDegrees); } if (stateProvider is WheelFeedbackVehicleStateProvider wheelProvider) { if (!wheelProvider.TryGetHeadingRadians( out var wheelHeadingRadians)) { throw new InvalidOperationException( "无法从Detour状态源读取有效车辆航向。" + wheelProvider.LastHeadingFailureReason); } return (float)AngleMath.RadiansToDegrees( wheelHeadingRadians); } if (stateProvider is DetourVehicleStateProvider detourProvider) { if (!detourProvider.TryGetHeadingRadians( out var detourHeadingRadians)) { throw new InvalidOperationException( "无法从Detour状态源读取有效车辆航向。" + detourProvider.LastHeadingFailureReason); } return (float)AngleMath.RadiansToDegrees( detourHeadingRadians); } if (stateProvider == null || !stateProvider.TryGetState(out var state)) { throw new InvalidOperationException( "无法从Detour状态源读取有效车辆航向。" + GetStateProviderFailureReason( stateProvider)); } return (float)AngleMath.RadiansToDegrees( state.PoseInWorld.YawRadians); } /// /// 通知配置化状态源:车辆已经停车,可以重新确认旋转期间的位置候选。 /// private static void BeginPostRotationPositionRecovery( IVehicleStateProvider stateProvider) { if (stateProvider is WheelFeedbackVehicleStateProvider wheelProvider) { wheelProvider.BeginPostRotationPositionRecovery(); return; } if (stateProvider is DetourVehicleStateProvider detourProvider) { detourProvider.BeginPostRotationPositionRecovery(); } } /// /// 获取已知停车状态源最近一次失败原因,未知实现返回空字符串。 /// private static string GetStateProviderFailureReason( IVehicleStateProvider stateProvider) { if (stateProvider is WheelFeedbackVehicleStateProvider wheelProvider) { return wheelProvider.LastFailureReason; } if (stateProvider is DetourVehicleStateProvider detourProvider) { return detourProvider.LastFailureReason; } return string.Empty; } /// /// 判断Detour是否仍在使用轮组预测确认疑似位姿不连续。 /// private static bool IsLocalizationRecoveryPending( IVehicleStateProvider stateProvider) { if (stateProvider is WheelFeedbackVehicleStateProvider wheelProvider) { return wheelProvider.TryGetLatestDetourDiagnostics( out var jumpCandidateActive, out _, out _, out _, out _, out _, out _) && jumpCandidateActive; } return stateProvider is DetourVehicleStateProvider detourProvider && detourProvider.IsJumpCandidateActive; } /// /// 检查原地自转参数是否为正有限值,部分时间和容差参数允许为零。 /// private static void EnsureFinitePositive( float value, string parameterName, bool allowZero = false) { if (float.IsNaN(value) || float.IsInfinity(value) || (allowZero ? value < 0f : value <= 0f)) { throw new ArgumentOutOfRangeException( parameterName, "原地自转参数必须是有效的正数。"); } } } }