using ClumsyCore; using ClumsyCore.DTools; using ClumsyCore.Interfaces; using ClumsyCore.Pilot; using CommonUsage.Chassis; using MyParking.Shared; using System; using System.Collections.Generic; using System.Numerics; namespace MultiWheelC { // C层单车测试:在可配置的运动坐标系中统一跟踪直线、圆弧或S型曲线。 public sealed class CrabMotionFrameTracker : MovementDefinition { public enum ReferencePathKind { Straight = 0, LeftArc = 1, SCurve = 2 } public enum ChassisCommandBackend { SendXYThSpeed = 0, SendMotion = 1 } public ReferencePathKind PathKind; public ChassisCommandBackend CommandBackend = ChassisCommandBackend.SendMotion; public Vector2 StartPosition; public double InitialBodyYawRadians; public float LengthMillimeters = 4000f; public float RadiusMillimeters = 2000f; public float SCurveLateralOffsetMillimeters = 400f; public double ArcSweepRadians = Math.PI / 2.0; public float CruiseSpeed = 0.2f; public float SlowDistanceMillimeters = 600f; public float FinishDistanceMillimeters = 30f; public float MinimumSpeed = 0.04f; public double LateralGainPerSecond = 0.8; public double MaximumLateralCorrection = 0.12; public double HeadingGainPerSecond = 1.5; public double MaximumAngularSpeedRadiansPerSecond = AngleMath.DegreesToRadians(30.0); public double MaximumVirtualSteeringRadians = AngleMath.DegreesToRadians(30.0); public float WheelAlignmentToleranceDegrees = 2f; public float WheelAlignmentStableSeconds = 0.3f; public float WheelAlignmentTimeoutSeconds = 10f; public float TrackingTimeoutSeconds = 60f; public Action CommandObserver; // 运动坐标系相对车体坐标系的朝向:普通模式为0,蟹行为π/2。 public double MotionFrameYawInBodyRadians = Math.PI / 2.0; private double _lastSCurveProgress; public override IEnumerable Get() { ValidateParameters(); var chassis = PilotDefinition.Chassis as MultiWheelChassis; if (chassis == null) throw new InvalidOperationException( "当前底盘不是MultiWheelChassis,无法执行运动坐标系轨迹测试。"); var adapter = new MultiWheelChassisAdapter( chassis, PilotDefinition.Self.CarNum); adapter.ResetToBodyFrame(); var lastCommandTime = DateTime.Now; try { // 模式切换阶段只转舵轮,驱动速度始终保持为零。 var alignmentStarted = DateTime.Now; DateTime? stableSince = null; while (true) { if (!adapter.PrepareParallelDirection( MotionFrameYawInBodyRadians)) throw new InvalidOperationException( "无法生成运动坐标系对应的舵轮准备姿态。"); var aligned = adapter.AreParallelWheelsAligned( MotionFrameYawInBodyRadians, AngleMath.DegreesToRadians( WheelAlignmentToleranceDegrees)); if (aligned) { if (stableSince == null) stableSince = DateTime.Now; if ((DateTime.Now - stableSince.Value) .TotalSeconds >= WheelAlignmentStableSeconds) break; } else { stableSince = null; } if ((DateTime.Now - alignmentStarted) .TotalSeconds > WheelAlignmentTimeoutSeconds) throw new TimeoutException( "舵轮在限定时间内未稳定到达运动坐标系初始方向。"); yield return true; } if (CommandBackend == ChassisCommandBackend.SendMotion) { // 舵轮已按真实机械角度完成预对齐; // 现在由Shared适配层激活SendMotion虚拟运动坐标系。 adapter.ActivateMotionFrame( MotionFrameYawInBodyRadians); } var trackingStarted = DateTime.Now; while (true) { if ((DateTime.Now - trackingStarted) .TotalSeconds > TrackingTimeoutSeconds) throw new TimeoutException( "蟹行轨迹在限定时间内未完成。"); var location = DetourInterface.getCartLocation(); if (!IsFinite(location.x) || !IsFinite(location.y) || !IsFinite(location.th)) throw new InvalidOperationException( "蟹行轨迹测试期间Detour位姿无效。"); var currentPosition = new Vector2( (float)location.x, (float)location.y); var currentBodyYaw = AngleMath.DegreesToRadians(location.th); CalculateReference( currentPosition, out var tangentYaw, out var referencePoint, out var remainingMillimeters, out var referenceCurvature); if (remainingMillimeters <= FinishDistanceMillimeters) break; var speed = CalculateSpeed(remainingMillimeters); var tangent = new Vector2( (float)Math.Cos(tangentYaw), (float)Math.Sin(tangentYaw)); var leftNormal = new Vector2( -tangent.Y, tangent.X); var positionError = currentPosition - referencePoint; var lateralErrorMeters = Vector2.Dot( positionError, leftNormal) / 1000.0; var normalCorrection = Limit( -LateralGainPerSecond * lateralErrorMeters, MaximumLateralCorrection); // 先在世界坐标中组合切向速度与横向纠偏速度。 var worldVx = tangent.X * speed + leftNormal.X * (float)normalCorrection; var worldVy = tangent.Y * speed + leftNormal.Y * (float)normalCorrection; // 将世界速度表达为当前蟹行运动坐标系速度。 var motionYaw = currentBodyYaw + MotionFrameYawInBodyRadians; var motionCos = Math.Cos(motionYaw); var motionSin = Math.Sin(motionYaw); var vxInMotion = motionCos * worldVx + motionSin * worldVy; var vyInMotion = -motionSin * worldVx + motionCos * worldVy; var desiredBodyYaw = tangentYaw - MotionFrameYawInBodyRadians; var headingError = AngleMath.ShortestDifferenceRadians( desiredBodyYaw, currentBodyYaw); var omega = speed * referenceCurvature + HeadingGainPerSecond * headingError; omega = Limit( omega, MaximumAngularSpeedRadiansPerSecond); var now = DateTime.Now; var interval = now - lastCommandTime; lastCommandTime = now; bool commandAccepted; Twist2D bodyTwist; if (CommandBackend == ChassisCommandBackend.SendMotion) { // 运动坐标系相对车体系旋转+90°: // 运动系正向速度会转换成车体系+Y速度。 bodyTwist = FrameTransform2D .TransformTwistAtSamePoint( new Pose2D( 0.0, 0.0, MotionFrameYawInBodyRadians), new Twist2D( vxInMotion, vyInMotion, omega)); // 将运动坐标系原点和前后几何控制点处的速度, // 转换为SendMotion需要的前后轴方向。 var controlPointRadiusMeters = Math.Max( chassis.ControlPointRadius / 1000.0, 0.001); var frontVelocityY = vyInMotion + omega * controlPointRadiusMeters; var rearVelocityY = vyInMotion - omega * controlPointRadiusMeters; var frontSteeringRadians = Math.Atan2( frontVelocityY, vxInMotion); var rearSteeringRadians = Math.Atan2( rearVelocityY, vxInMotion); // 蟹行测试绕过M层ManualControl并直接调用SendMotion, // 因此需要在C层同步应用蟹行虚拟几何比例和转向符号。 if (IsCrabMotionFrame()) { var geometryRatio = adapter.HalfTrackWidthMeters / adapter.HalfWheelBaseMeters; frontSteeringRadians = ConvertToCrabSteering( frontSteeringRadians, geometryRatio); rearSteeringRadians = ConvertToCrabSteering( rearSteeringRadians, geometryRatio); } var frontThetaDegrees = (float)AngleMath.RadiansToDegrees( frontSteeringRadians); var rearThetaDegrees = (float)AngleMath.RadiansToDegrees( rearSteeringRadians); var motionSpeed = (float)Math.Sqrt( vxInMotion * vxInMotion + vyInMotion * vyInMotion); commandAccepted = chassis.SendMotion( motionSpeed, frontThetaDegrees, rearThetaDegrees, interval); } else if (CommandBackend == ChassisCommandBackend .SendXYThSpeed) { // 安全XYTh后端根据舵角误差统一压低驱动轮速。 bodyTwist = FrameTransform2D .TransformTwistAtSamePoint( new Pose2D( 0.0, 0.0, MotionFrameYawInBodyRadians), new Twist2D( vxInMotion, vyInMotion, omega)); var command = new ChassisCommand( PilotDefinition.Self.CarNum, bodyTwist); commandAccepted = adapter.Send( command, interval); } else { throw new InvalidOperationException( $"不支持的底盘命令后端:{CommandBackend}。"); } if (!commandAccepted) throw new InvalidOperationException( "运动坐标系轨迹底盘解算失败:" + chassis .LastMotionDecomposeFailureReason); CommandObserver?.Invoke( (float)bodyTwist.VxMetersPerSecond, (float)bodyTwist.VyMetersPerSecond, (float)bodyTwist .OmegaRadiansPerSecond); yield return true; } } finally { adapter.StopImmediately(); if (CommandBackend == ChassisCommandBackend.SendMotion) { // 测试退出后恢复真实车体坐标系,避免影响后续测试。 adapter.ResetToBodyFrame(); } CommandObserver?.Invoke(0f, 0f, 0f); } yield return false; } // 判断当前运动坐标系是否为车体左侧朝前的蟹行坐标系。 private bool IsCrabMotionFrame() { return Math.Abs( AngleMath.ShortestDifferenceRadians( Math.PI / 2.0, MotionFrameYawInBodyRadians)) < 1e-6; } // 按车体几何比例缩小蟹行转角。 // +90°运动坐标系已经完成方向映射,此处不能再次反号。 private double ConvertToCrabSteering( double normalSteeringRadians, double geometryRatio) { var crabSteeringRadians = Math.Atan( geometryRatio * Math.Tan( normalSteeringRadians)); return Limit( crabSteeringRadians, MaximumVirtualSteeringRadians); } // 计算当前点在直线或圆弧上的参考点、切线和剩余距离。 private void CalculateReference( Vector2 currentPosition, out double tangentYaw, out Vector2 referencePoint, out float remainingMillimeters, out double curvaturePerMeter) { var initialMotionYaw = InitialBodyYawRadians + MotionFrameYawInBodyRadians; if (PathKind == ReferencePathKind.Straight) { var tangent = new Vector2( (float)Math.Cos(initialMotionYaw), (float)Math.Sin(initialMotionYaw)); var relative = currentPosition - StartPosition; var progress = Vector2.Dot(relative, tangent); var clampedProgress = Math.Max( 0f, Math.Min(progress, LengthMillimeters)); tangentYaw = initialMotionYaw; referencePoint = StartPosition + tangent * clampedProgress; remainingMillimeters = Math.Max( 0f, LengthMillimeters - progress); curvaturePerMeter = 0.0; return; } if (PathKind == ReferencePathKind.SCurve) { CalculateSCurveReference( currentPosition, initialMotionYaw, out tangentYaw, out referencePoint, out remainingMillimeters, out curvaturePerMeter); return; } var center = GetArcCenter(); var startRadialYaw = initialMotionYaw - Math.PI / 2.0; var radial = currentPosition - center; var currentRadialYaw = Math.Atan2(radial.Y, radial.X); var progressRadians = AngleMath.NormalizeRadians( currentRadialYaw - startRadialYaw); // 测试圆弧只有+90°,起点附近的轻微负噪声按0处理。 if (progressRadians < 0.0) progressRadians = 0.0; var clampedProgressRadians = Math.Min( progressRadians, ArcSweepRadians); var referenceRadialYaw = startRadialYaw + clampedProgressRadians; referencePoint = center + new Vector2( RadiusMillimeters * (float)Math.Cos(referenceRadialYaw), RadiusMillimeters * (float)Math.Sin(referenceRadialYaw)); tangentYaw = referenceRadialYaw + Math.PI / 2.0; remainingMillimeters = (float)Math.Max( 0.0, (ArcSweepRadians - progressRadians) * RadiusMillimeters); curvaturePerMeter = 1000.0 / RadiusMillimeters; } // 通过离散最近点和解析导数计算两段三次贝塞尔S曲线的参考状态。 private void CalculateSCurveReference( Vector2 currentPosition, double initialMotionYaw, out double tangentYaw, out Vector2 referencePoint, out float remainingMillimeters, out double curvaturePerMeter) { const int nearestPointSamples = 200; var searchStart = Math.Max( 0.0, _lastSCurveProgress - 0.02); var bestProgress = _lastSCurveProgress; var bestDistanceSquared = double.MaxValue; for (var i = 0; i <= nearestPointSamples; i++) { var progress = searchStart + (1.0 - searchStart) * i / nearestPointSamples; EvaluateSCurve( progress, out var localPoint, out _, out _); var worldPoint = LocalPathPointToWorld( localPoint, initialMotionYaw); var distanceSquared = Vector2.DistanceSquared( currentPosition, worldPoint); if (distanceSquared < bestDistanceSquared) { bestDistanceSquared = distanceSquared; bestProgress = progress; } } // 轨迹进度不允许因定位噪声倒退,防止控制目标跳回上一段曲线。 _lastSCurveProgress = Math.Max( _lastSCurveProgress, bestProgress); EvaluateSCurve( _lastSCurveProgress, out var bestLocalPoint, out var firstDerivative, out var secondDerivative); referencePoint = LocalPathPointToWorld( bestLocalPoint, initialMotionYaw); tangentYaw = initialMotionYaw + Math.Atan2( firstDerivative.Y, firstDerivative.X); var derivativeMagnitude = Math.Sqrt( firstDerivative.X * firstDerivative.X + firstDerivative.Y * firstDerivative.Y); if (derivativeMagnitude < 1e-6) { curvaturePerMeter = 0.0; } else { // 导数单位为mm,乘1000后将曲率从1/mm转换成1/m。 curvaturePerMeter = (firstDerivative.X * secondDerivative.Y - firstDerivative.Y * secondDerivative.X) * 1000.0 / Math.Pow( derivativeMagnitude, 3.0); } remainingMillimeters = ApproximateSCurveRemainingLength( _lastSCurveProgress); } // 计算与普通4m S型测试完全一致的三段三次贝塞尔完整S曲线。 private void EvaluateSCurve( double progress, out Vector2 point, out Vector2 firstDerivative, out Vector2 secondDerivative) { progress = Math.Max( 0.0, Math.Min(progress, 1.0)); Vector2 p0; Vector2 p1; Vector2 p2; Vector2 p3; double t; if (progress <= 0.25) { t = progress * 4.0; p0 = new Vector2(0f, 0f); p1 = new Vector2( LengthMillimeters / 12f, 0f); p2 = new Vector2( LengthMillimeters / 6f, SCurveLateralOffsetMillimeters); p3 = new Vector2( LengthMillimeters * 0.25f, SCurveLateralOffsetMillimeters); } else if (progress <= 0.75) { t = (progress - 0.25) * 2.0; p0 = new Vector2( LengthMillimeters * 0.25f, SCurveLateralOffsetMillimeters); p1 = new Vector2( LengthMillimeters / 3f, SCurveLateralOffsetMillimeters); p2 = new Vector2( LengthMillimeters * 2f / 3f, -SCurveLateralOffsetMillimeters); p3 = new Vector2( LengthMillimeters * 0.75f, -SCurveLateralOffsetMillimeters); } else { t = (progress - 0.75) * 4.0; p0 = new Vector2( LengthMillimeters * 0.75f, -SCurveLateralOffsetMillimeters); p1 = new Vector2( LengthMillimeters * 5f / 6f, -SCurveLateralOffsetMillimeters); p2 = new Vector2( LengthMillimeters * 11f / 12f, 0f); p3 = new Vector2( LengthMillimeters, 0f); } var oneMinusT = 1.0 - t; point = p0 * (float)( oneMinusT * oneMinusT * oneMinusT) + p1 * (float)( 3.0 * oneMinusT * oneMinusT * t) + p2 * (float)( 3.0 * oneMinusT * t * t) + p3 * (float)(t * t * t); firstDerivative = (p1 - p0) * (float)( 3.0 * oneMinusT * oneMinusT) + (p2 - p1) * (float)( 6.0 * oneMinusT * t) + (p3 - p2) * (float)(3.0 * t * t); secondDerivative = (p2 - 2f * p1 + p0) * (float)(6.0 * oneMinusT) + (p3 - 2f * p2 + p1) * (float)(6.0 * t); } // 通过分段采样估算从当前S曲线进度到终点的实际弧长。 private float ApproximateSCurveRemainingLength( double startProgress) { const int lengthSamples = 100; EvaluateSCurve( startProgress, out var previousPoint, out _, out _); var length = 0f; for (var i = 1; i <= lengthSamples; i++) { var progress = startProgress + (1.0 - startProgress) * i / lengthSamples; EvaluateSCurve( progress, out var point, out _, out _); length += Vector2.Distance( previousPoint, point); previousPoint = point; } return length; } // 将以初始蟹行方向为X轴的局部路径点转换到Detour世界坐标。 private Vector2 LocalPathPointToWorld( Vector2 localPoint, double initialMotionYaw) { var cos = (float)Math.Cos(initialMotionYaw); var sin = (float)Math.Sin(initialMotionYaw); return StartPosition + new Vector2( localPoint.X * cos - localPoint.Y * sin, localPoint.X * sin + localPoint.Y * cos); } // 获取蟹行左转圆弧圆心;它位于初始运动方向的左侧。 public Vector2 GetArcCenter() { var initialMotionYaw = InitialBodyYawRadians + MotionFrameYawInBodyRadians; return StartPosition + new Vector2( -RadiusMillimeters * (float)Math.Sin(initialMotionYaw), RadiusMillimeters * (float)Math.Cos(initialMotionYaw)); } // 获取圆弧测试的理论终点。 public Vector2 GetArcDestination() { var initialMotionYaw = InitialBodyYawRadians + MotionFrameYawInBodyRadians; var startRadialYaw = initialMotionYaw - Math.PI / 2.0; var endRadialYaw = startRadialYaw + ArcSweepRadians; var center = GetArcCenter(); return center + new Vector2( RadiusMillimeters * (float)Math.Cos(endRadialYaw), RadiusMillimeters * (float)Math.Sin(endRadialYaw)); } // 根据剩余路径长度生成终点减速速度。 private float CalculateSpeed( float remainingMillimeters) { if (remainingMillimeters >= SlowDistanceMillimeters) return CruiseSpeed; var ratio = remainingMillimeters / Math.Max( SlowDistanceMillimeters, 1f); return Math.Max( MinimumSpeed, CruiseSpeed * ratio); } private void ValidateParameters() { if (CruiseSpeed <= 0f || !IsFinite(CruiseSpeed) || LengthMillimeters <= 0f || !IsFinite(LengthMillimeters) || RadiusMillimeters <= 0f || !IsFinite(RadiusMillimeters) || SCurveLateralOffsetMillimeters <= 0f || !IsFinite( SCurveLateralOffsetMillimeters) || ArcSweepRadians <= 0.0 || !IsFinite(ArcSweepRadians) || SlowDistanceMillimeters <= 0f || !IsFinite(SlowDistanceMillimeters) || FinishDistanceMillimeters < 0f || !IsFinite(FinishDistanceMillimeters) || TrackingTimeoutSeconds <= 0f || !IsFinite(TrackingTimeoutSeconds) || MaximumVirtualSteeringRadians <= 0.0 || MaximumVirtualSteeringRadians >= Math.PI / 2.0 || !IsFinite( MaximumVirtualSteeringRadians)) throw new ArgumentOutOfRangeException( "蟹行轨迹测试参数无效。"); } private static double Limit( double value, double absoluteLimit) { return Math.Max( -absoluteLimit, Math.Min(value, absoluteLimit)); } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } } }