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 { /// /// 将四个舵轮准备到自转姿态并按世界航向闭环旋转,正常完成后等待舵轮回正。 /// public class MultiWheelRotateInPlace : MovementDefinition { /// /// 旋转目标角度 /// public float AngleTarget; // 留作标定或单元测试时显式替换;为空时使用配置化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 targetAngle = (float)AngleMath.NormalizeDegrees(AngleTarget); var currentAngle = 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; while (true) { if ((DateTime.Now - rotationStarted) .TotalSeconds > rotationTimeoutSeconds) { throw new TimeoutException( $"原地自转超过{rotationTimeoutSeconds:F1}s仍未到位。"); } currentAngle = ReadCurrentAngleDegrees(stateProvider); cachedCurrentAngle = currentAngle; var s = thPid.GetResponse(targetAngle, true); var angleErrorDegrees = (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命令统一使用rad/s。 // adapter.Send最终调用普通安全版SendXYThSpeed。 var omegaRadiansPerSecond = (float)AngleMath.DegreesToRadians(s); if (!adapter.Send( new ChassisCommand( PilotDefinition.Self.CarNum, new Twist2D( 0.0, 0.0, omegaRadiansPerSecond)), interval)) { throw new InvalidOperationException( "安全XYTh原地旋转底盘解算失败:" + adapter.LastFailureReason); } yield return true; } CommandAngularSpeedObserver?.Invoke(0f); adapter.StopXYThDrivePreserveSteeringState(); // 航向正常到位后复用统一回正动作;异常或取消会直接进入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( $"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 (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 == null || !stateProvider.TryGetState(out var state)) { throw new InvalidOperationException( "无法从Detour状态源读取有效车辆航向。" + GetStateProviderFailureReason( stateProvider)); } return (float)AngleMath.RadiansToDegrees( state.PoseInWorld.YawRadians); } /// /// 获取已知停车状态源最近一次失败原因,未知实现返回空字符串。 /// 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; } /// /// 检查原地自转参数是否为正有限值,部分时间和容差参数允许为零。 /// 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, "原地自转参数必须是有效的正数。"); } } } }