diff --git a/MultiWheel/MultiWheelC/AGV.cs b/MultiWheel/MultiWheelC/AGV.cs
index cf35434..7af2057 100644
--- a/MultiWheel/MultiWheelC/AGV.cs
+++ b/MultiWheel/MultiWheelC/AGV.cs
@@ -400,6 +400,102 @@ namespace MultiWheelC
}
}
}
+
+ public void FleetCurveWalk(float srcX, float srcY, int srcId, float dstX, float dstY, int dstId,
+ float speed, params float[] trackTypeInfo)
+ {
+ if (trackTypeInfo == null || trackTypeInfo.Length < 2)
+ {
+ DLog.Log("FleetCurveWalk abort: invalid trackTypeInfo, expected Bezier type info.", "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve invalid trackTypeInfo", "FleetCurve");
+ return;
+ }
+
+ var trackType = (int)trackTypeInfo[0];
+ if (trackType != 2)
+ {
+ DLog.Log($"FleetCurveWalk abort: unsupported trackType={trackType}, only Bezier(type=2) is supported.",
+ "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve only supports Bezier trackType=2", "FleetCurve");
+ return;
+ }
+
+ var controlPointNum = (int)trackTypeInfo[1];
+ var expectedLength = 2 + controlPointNum * 2;
+ if (controlPointNum < 3 || trackTypeInfo.Length < expectedLength)
+ {
+ DLog.Log(
+ $"FleetCurveWalk abort: invalid Bezier trackTypeInfo. controlPointNum={controlPointNum}, " +
+ $"length={trackTypeInfo.Length}, expected>={expectedLength}.",
+ "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve invalid Bezier trackTypeInfo", "FleetCurve");
+ return;
+ }
+
+ BezierTrack track;
+ try
+ {
+ track = ProcessTrackTypeInfo(srcX, srcY, dstX, dstY, trackTypeInfo) as BezierTrack;
+ }
+ catch (Exception ex)
+ {
+ DLog.Log($"FleetCurveWalk abort: failed to process trackTypeInfo. {ex.Message}", "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve failed to process track", "FleetCurve");
+ return;
+ }
+
+ if (track == null)
+ {
+ DLog.Log("FleetCurveWalk abort: ProcessTrackTypeInfo did not return BezierTrack.", "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve requires BezierTrack", "FleetCurve");
+ return;
+ }
+
+ track.Speed = speed;
+ track.CarDirectionBias = PilotDefinition.Conf.FleetCurveCarDirectionBias;
+
+ DLog.Log(
+ $"call FleetCurveWalk(src=({srcX:0},{srcY:0},id:{srcId}), dst=({dstX:0},{dstY:0},id:{dstId}), " +
+ $"speed={speed:0.000}, trackType={trackType}, controls={controlPointNum}, track={track.GetType().Name}, " +
+ $"carDirectionBias={PilotDefinition.Conf.FleetCurveCarDirectionBias:0.0})",
+ "FleetCurveDbg");
+
+ if (dstId != -1)
+ {
+ while (!TryLock(dstId))
+ {
+ Thread.Sleep(50);
+ }
+ DLog.Log($"锁点{dstId}完成", "FleetCurveDbg");
+ }
+
+ var action = new MultiWheelC.FleetCurveWalk
+ {
+ Track = track,
+ CurveSpeed = speed,
+ CarDirectionBias = PilotDefinition.Conf.FleetCurveCarDirectionBias,
+ BezierResolution = PilotDefinition.Conf.FleetCurveBezierResolution,
+ SlowDistance = PilotDefinition.Conf.FleetCurveSlowDistance,
+ FinishDistance = PilotDefinition.Conf.FleetCurveFinishDistance,
+ FinishSpeed = PilotDefinition.Conf.FleetCurveFinishSpeed,
+ SlowingPow = PilotDefinition.Conf.FleetCurveSlowingPow,
+ GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold,
+ StartSyncTimeoutSec = PilotDefinition.Conf.FleetCrabStartSyncTimeoutSec
+ };
+
+ try
+ {
+ new DriveTask(action.Get()).Wait();
+ }
+ finally
+ {
+ if (srcId != -1)
+ {
+ Leave(srcId);
+ DLog.Log($"释放放车点{srcId}", "FleetCurveDbg");
+ }
+ }
+ }
public void ChangeAvoidanceDistance(float stopDistance, float slowDistance)
{
diff --git a/MultiWheel/MultiWheelC/FleetCrabWalk.cs b/MultiWheel/MultiWheelC/FleetCrabWalk.cs
new file mode 100644
index 0000000..8aeccf3
--- /dev/null
+++ b/MultiWheel/MultiWheelC/FleetCrabWalk.cs
@@ -0,0 +1,528 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using ClumsyCore;
+using ClumsyCore.Interfaces;
+using ClumsyCore.Pilot;
+using FundamentalLib;
+using CommonUsage.Chassis;
+using CommonUsage.Mathematics;
+using MDCSToolBox.Clumsy.Movements;
+using MDCSToolBox.Clumsy.Pilot;
+
+namespace MultiWheelC;
+
+// ===== 车队联动-自动蟹行动作 =====
+// 以当前车队中心为起点,构造指定方向和长度的直线路径;
+// 执行侧直接写 MultiVehicleAuto...,由 TickMultiVehicle 自动分支统一下发。
+//
+// 控制思路参考 MDCSToolbox 几何控制器,但实现收在 MultiWheelC 内:
+// 1) 读取主车 Detour 反推车队中心,计算沿直线的进度、横向偏差和车身目标朝向偏差;
+// 2) 根据横向偏差给前后 GCP 同向修正,根据车身目标朝向偏差给前后 GCP 反向修正;
+// 3) 根据终点距离减速,并发布 ideal fleet center 给从车做前馈。
+//
+// 前提:在主车(MultiVehicleMasterEndpoint=="/")运行,且主车有 Detour 定位。
+public class FleetCrabWalk : MovementDefinition
+{
+ /// 路径方向相对启动时车队朝向的夹角(deg,逆时针为正)。
+ public float CrabAngleDeg = 45f;
+
+ /// 路径方向相对车身目标朝向的夹角(deg,逆时针为正)。MovementTest 会设为 CrabAngleDeg,以保持启动时车身朝向。
+ public float BodyToPathAngleDeg = 45f;
+
+ /// 路径长度(mm)。
+ public float CrabLengthMm = 2000f;
+
+ /// 行驶速度(m/s)。
+ public float CrabSpeed = 0.2f;
+
+ /// 速度命令加速度限制(m/s^2),小于等于 0 表示不限制。
+ public float FleetCrabAccel = 0.2f;
+
+ /// 预对齐后正式下发速度前 5 秒加速度限制(m/s^2),小于等于 0 表示不限制。
+ public float FleetCrabStartAccel = 0.01f;
+
+ /// 末端开始减速距离(mm)。
+ public float FleetCrabSlowDistance = 2000f;
+
+ /// 完成距离(mm),低于该剩余距离结束动作。
+ public float FleetCrabFinishDistance = 20f;
+
+ /// 末端最低速度(m/s)。
+ public float FleetCrabFinishSpeed = 0.02f;
+
+ /// 末端减速曲线指数。
+ public float FleetCrabSlowingPow = 0.8f;
+
+ /// 前后 GCP 舵角修正上限(deg)。
+ public float GcpThetaThreshold = 95f;
+
+ private bool _stopping;
+
+ private void Cleanup()
+ {
+ var self = PilotDefinition.Self;
+ self.MultiVehicleScriptVx = 0;
+ self.MultiVehicleScriptVy = 0;
+ self.MultiVehicleScriptVth = 0;
+ self.MultiVehicleScriptMode = 0;
+ self.MultiVehicleScriptEnabled = false;
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoFrontTh = 0;
+ self.MultiVehicleAutoRearTh = 0;
+ self.MultiVehicleAutoHasIdeal = false;
+ self.MultiVehicleAutoEnabled = false;
+ }
+
+ public void Stop()
+ {
+ _stopping = true;
+ Cleanup();
+ }
+
+ private static float Clamp(float value, float min, float max)
+ {
+ if (value < min) return min;
+ if (value > max) return max;
+ return value;
+ }
+
+ private static float ClampAbs(float value, float limit)
+ {
+ var absLimit = Math.Abs(limit);
+ if (absLimit <= 0) return value;
+ if (value > absLimit) return absLimit;
+ if (value < -absLimit) return -absLimit;
+ return value;
+ }
+
+ private static float Slew(float current, float target, float maxDelta)
+ {
+ if (maxDelta <= 0) return target;
+ if (target > current + maxDelta) return current + maxDelta;
+ if (target < current - maxDelta) return current - maxDelta;
+ return target;
+ }
+
+ private static float AverageAngle(float frontTh, float rearTh)
+ {
+ var diff = (float)CommonMath.ThDiff(frontTh, rearTh);
+ return (float)CommonMath.RoundTh(rearTh + diff / 2f);
+ }
+
+ private static void ResolveCrabDriveEquivalent(float speed, float rawFrontTh, float rawRearTh, float steerLimit,
+ out float driveSpeed, out float frontTh, out float rearTh, out bool reverseEquivalent, out float rawBaseTh)
+ {
+ var limit = Math.Min(179f, Math.Max(1f, Math.Abs(steerLimit)));
+ rawBaseTh = AverageAngle(rawFrontTh, rawRearTh);
+ driveSpeed = speed;
+ frontTh = rawFrontTh;
+ rearTh = rawRearTh;
+ reverseEquivalent = false;
+
+ if (rawBaseTh > limit)
+ {
+ frontTh = (float)CommonMath.RoundTh(frontTh - 180f);
+ rearTh = (float)CommonMath.RoundTh(rearTh - 180f);
+ driveSpeed = -driveSpeed;
+ reverseEquivalent = true;
+ }
+ else if (rawBaseTh < -limit)
+ {
+ frontTh = (float)CommonMath.RoundTh(frontTh + 180f);
+ rearTh = (float)CommonMath.RoundTh(rearTh + 180f);
+ driveSpeed = -driveSpeed;
+ reverseEquivalent = true;
+ }
+
+ frontTh = ClampAbs(frontTh, limit);
+ rearTh = ClampAbs(rearTh, limit);
+ }
+
+ private static float ProbeSpeed(float speed)
+ {
+ return Math.Abs(speed) > 1e-4f ? speed : 1f;
+ }
+
+ private static bool TryGetMotionYawSign(float frontTh, float rearTh, float driveSpeed, float controlRadius,
+ out float yawSign)
+ {
+ yawSign = 0f;
+ if (Math.Abs(CommonMath.ThDiff(frontTh, rearTh)) <= 1e-3f)
+ return false;
+
+ var radius = Math.Max(1f, Math.Abs(controlRadius));
+ Vector2 pFront = new(radius, 0), pRear = new(-radius, 0),
+ normFront = CommonMath.Transform2D(pFront, frontTh + 90f, Vector2.UnitX),
+ normRear = CommonMath.Transform2D(pRear, rearTh + 90f, Vector2.UnitX);
+ var (intersect, center) = CommonMath.TwoLinesIntersection(pFront, normFront, pRear, normRear);
+ if (!intersect)
+ return false;
+
+ // Match MultiWheelChassis.SendMotion: the tangent side is selected by
+ // rotCenter.Y > 1, and reverse-equivalent motion flips the yaw direction.
+ var tangentSign = center.Y > 1f ? 1f : -1f;
+ var speedSign = driveSpeed >= 0f ? 1f : -1f;
+ yawSign = speedSign * tangentSign;
+ return true;
+ }
+
+ private static float GetYawSplitSign(float baseTh, float speed, float steerLimit, float controlRadius)
+ {
+ const float probeDth = 1f;
+ ResolveCrabDriveEquivalent(ProbeSpeed(speed), baseTh + probeDth, baseTh - probeDth, steerLimit,
+ out var probeSpeed, out var probeFrontTh, out var probeRearTh, out _, out _);
+ return TryGetMotionYawSign(probeFrontTh, probeRearTh, probeSpeed, controlRadius, out var yawSign)
+ ? yawSign
+ : 1f;
+ }
+
+ private static float EstimateLateralVelocity(float bodyTh, float frontTh, float rearTh, float driveSpeed,
+ Vector2 pathLeft)
+ {
+ var motionTh = (float)CommonMath.RoundTh(bodyTh + AverageAngle(frontTh, rearTh));
+ var rad = motionTh / 180f * Math.PI;
+ var dir = new Vector2((float)Math.Cos(rad), (float)Math.Sin(rad));
+ if (driveSpeed < 0f)
+ dir = -dir;
+ return Vector2.Dot(dir, pathLeft);
+ }
+
+ private static float ScoreBiasSign(float baseTh, float bodyTh, float speed, float steerLimit, Vector2 pathLeft,
+ float lateral, float biasProbe)
+ {
+ ResolveCrabDriveEquivalent(ProbeSpeed(speed), baseTh + biasProbe, baseTh + biasProbe, steerLimit,
+ out var probeSpeed, out var probeFrontTh, out var probeRearTh, out _, out _);
+ var lateralVelocity = EstimateLateralVelocity(bodyTh, probeFrontTh, probeRearTh, probeSpeed, pathLeft);
+ return -Math.Sign(lateral) * lateralVelocity;
+ }
+
+ private static float GetLateralBiasSign(float baseTh, float bodyTh, float speed, float steerLimit, Vector2 pathLeft,
+ float lateral)
+ {
+ if (Math.Abs(lateral) <= 1e-3f)
+ return 1f;
+
+ const float probeBias = 1f;
+ var positiveScore = ScoreBiasSign(baseTh, bodyTh, speed, steerLimit, pathLeft, lateral, probeBias);
+ var negativeScore = ScoreBiasSign(baseTh, bodyTh, speed, steerLimit, pathLeft, lateral, -probeBias);
+ return positiveScore >= negativeScore ? 1f : -1f;
+ }
+
+ private static bool TryGetControlFleetCenter(PilotDefinition self, out float centerX, out float centerY,
+ out float centerTh, out string source)
+ {
+ if (self.TryGetFleetCenterFromMembers(out centerX, out centerY, out centerTh))
+ {
+ source = "fleet";
+ return true;
+ }
+
+ if (self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh))
+ {
+ source = "slam";
+ return true;
+ }
+
+ source = "none";
+ return false;
+ }
+
+ public override IEnumerable Get()
+ {
+ var self = PilotDefinition.Self;
+ var conf = PilotDefinition.Conf;
+ var chassis = BasicPilotBase.Chassis as MultiWheelChassis;
+ if (chassis == null)
+ {
+ DLog.Log("ABORT: FleetCrabWalk requires MultiWheelChassis.", "FleetCrabDbg");
+ yield break;
+ }
+ _stopping = false;
+
+ DLog.Log(
+ $"ENTER master?={conf.MultiVehicleMasterEndpoint == "/"} endpoint={conf.MultiVehicleMasterEndpoint} " +
+ $"fleetNum={conf.MultiVehicleFleetNum} useDetect={conf.MultiVehicleUseDetect} " +
+ $"syncUseDetour={conf.MultiVehicleSyncUseDetour} useIdealCenter={conf.MultiVehicleAutoUseIdealCenter} " +
+ $"autoFields=true pathMode=relative pathAngle={CrabAngleDeg:0.0} " +
+ $"bodyToPath={BodyToPathAngleDeg:0.0} gcpLimit={GcpThetaThreshold:0.0} " +
+ $"biasFac={conf.BiasFac:0.00} fleetCrabDthFac={conf.FleetCrabDthLinearFac:0.00}",
+ "FleetCrabDbg");
+
+ if (conf.MultiVehicleMasterEndpoint != "/")
+ {
+ DLog.Log($"ABORT: 非主车 (endpoint={conf.MultiVehicleMasterEndpoint})", "FleetCrabDbg");
+ Hedingben.ToastText("车队蟹行需在主车(主车端点=\"/\")运行", "FleetCrab");
+ yield break;
+ }
+
+ // 注意:getCartLocation() 在无有效 Detour 定位时会阻塞——若卡在这里且后面看不到 CENTER 日志,即定位未就绪。
+ DLog.Log("主车校验通过,开始读取车队中心 (getCartLocation 无定位会阻塞)…", "FleetCrabDbg");
+ if (!TryGetControlFleetCenter(self, out var x0, out var y0, out var theta, out var initialCenterSource))
+ {
+ DLog.Log("ABORT: TryGetFleetCenterFromSlam 返回 false (无定位)", "FleetCrabDbg");
+ Hedingben.ToastText("车队蟹行需要主车 Detour 定位", "FleetCrab");
+ yield break;
+ }
+ DLog.Log($"CENTER 车队中心=({x0:0},{y0:0},{theta:0.0})", "FleetCrabDbg");
+
+ DLog.Log($"CENTER_SOURCE source={initialCenterSource} center=({x0:0},{y0:0},{theta:0.0})", "FleetCrabDbg");
+
+ var pathStart = new Vector2(x0, y0);
+ var pathLengthMm = CrabLengthMm;
+ var phi = CommonMath.RoundTh(theta + CrabAngleDeg);
+ var dst = CommonMath.Transform2D(pathStart, phi, new Vector2(pathLengthMm, 0));
+ var targetBodyTh = CommonMath.RoundTh(phi - BodyToPathAngleDeg);
+ var phiRad = phi / 180.0 * Math.PI;
+ var pathDir = new Vector2((float)Math.Cos(phiRad), (float)Math.Sin(phiRad));
+ var pathLeft = new Vector2(-pathDir.Y, pathDir.X);
+
+ DLog.Log(
+ $"START center=({x0:0},{y0:0},{theta:0.0}) pathMode=relative " +
+ $"src=({pathStart.X:0},{pathStart.Y:0}) pathAngle={CrabAngleDeg:0.0} bodyToPath={BodyToPathAngleDeg:0.0} " +
+ $"phi={phi:0.0} targetBody={targetBodyTh:0.0} " +
+ $"len={pathLengthMm:0} dst=({dst.X:0},{dst.Y:0}) speed={CrabSpeed:0.000} startAccel={FleetCrabStartAccel:0.000} accel={FleetCrabAccel:0.000} " +
+ $"slow={FleetCrabSlowDistance:0} finishDist={FleetCrabFinishDistance:0} " +
+ $"finishSpeed={FleetCrabFinishSpeed:0.000} slowingPow={FleetCrabSlowingPow:0.00}",
+ "FleetCrabDbg");
+
+ var gcpLimit = Math.Max(1f, Math.Abs(GcpThetaThreshold));
+ var controlRadius = Math.Max(1f, Math.Abs(conf.MultiVehicleControlRadius > 0
+ ? conf.MultiVehicleControlRadius
+ : conf.TestCarSyncDistance / 2f));
+ ResolveCrabDriveEquivalent(0f, (float)CommonMath.ThDiff(phi, theta),
+ (float)CommonMath.ThDiff(phi, theta), gcpLimit, out _, out var holdFrontTh, out var holdRearTh,
+ out _, out _);
+ var warmStart = DateTime.Now;
+ var warmSeqBaseline = self.BeginFleetMotionWarmup();
+
+ self.MultiVehicleScriptEnabled = false;
+ self.MultiVehicleScriptMode = 0;
+ self.MultiVehicleScriptVx = 0;
+ self.MultiVehicleScriptVy = 0;
+ self.MultiVehicleScriptVth = 0;
+ self.MultiVehicleAutoEnabled = true;
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoFrontTh = holdFrontTh;
+ self.MultiVehicleAutoRearTh = holdRearTh;
+ self.MultiVehicleAutoIdealX = pathStart.X;
+ self.MultiVehicleAutoIdealY = pathStart.Y;
+ self.MultiVehicleAutoIdealTh = targetBodyTh;
+ self.MultiVehicleAutoHasIdeal = true;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+ self.PrimeMasterAutoFromSlam();
+ DLog.Log(
+ $"WARMUP auto fields enabled, waiting for fleet startup sync seqBase={warmSeqBaseline} " +
+ $"hold=({holdFrontTh:0.00},{holdRearTh:0.00})",
+ "FleetCrabDbg");
+
+ var warmEnd = warmStart.AddSeconds(Math.Max(1.0f, conf.FleetCrabStartSyncTimeoutSec));
+ var warmIter = 0;
+ var warmReady = false;
+ var warmDetail = "";
+ while (!_stopping && DateTime.Now < warmEnd)
+ {
+ warmIter++;
+ self.MultiVehicleScriptEnabled = false;
+ self.MultiVehicleScriptMode = 0;
+ self.MultiVehicleAutoEnabled = true;
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoFrontTh = holdFrontTh;
+ self.MultiVehicleAutoRearTh = holdRearTh;
+ self.MultiVehicleAutoIdealX = pathStart.X;
+ self.MultiVehicleAutoIdealY = pathStart.Y;
+ self.MultiVehicleAutoIdealTh = targetBodyTh;
+ self.MultiVehicleAutoHasIdeal = true;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+ self.PrimeMasterAutoFromSlam();
+ var snap = self.GetFleetCenterSnapshot();
+ int cnt;
+ lock (self.FleetLock) cnt = self.MultiVehicleFleet.Count;
+ if (warmIter % 5 == 0)
+ DLog.Log(
+ $"WARMUP#{warmIter} 快照=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) tick={snap.Tick} " +
+ $"autoEn={self.MultiVehicleAutoEnabled} scriptEn={self.MultiVehicleScriptEnabled} cnt={cnt}/{conf.MultiVehicleFleetNum} " +
+ $"detail={warmDetail}",
+ "FleetCrabDbg");
+ if (self.IsFleetMotionWarmupReady(warmStart, warmSeqBaseline,
+ conf.TestCarSyncTh, conf.TestCarSyncDistance, out warmDetail))
+ {
+ warmReady = true;
+ DLog.Log(
+ $"WARMUP done iter={warmIter} 快照=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) cnt={cnt} detail={warmDetail}",
+ "FleetCrabDbg");
+ break;
+ }
+ yield return true;
+ }
+ if (!warmReady)
+ {
+ DLog.Log($"WARMUP timeout: fleet startup sync failed, abort action. detail={warmDetail}",
+ "FleetCrabDbg");
+ Hedingben.ToastText("车队蟹行启动同步超时,已取消", "FleetCrab");
+ Cleanup();
+ yield break;
+ }
+
+ Hedingben.ToastText($"车队蟹行 路径{phi:0.0}° 车身夹角{BodyToPathAngleDeg:0.0}° 长度{pathLengthMm:0}mm", "FleetCrab");
+
+ if (warmReady && self.TryGetFleetCenterFromMembers(out var warmX, out var warmY, out var warmTh))
+ {
+ x0 = warmX;
+ y0 = warmY;
+ theta = warmTh;
+ pathStart = new Vector2(x0, y0);
+ phi = CommonMath.RoundTh(theta + CrabAngleDeg);
+ dst = CommonMath.Transform2D(pathStart, phi, new Vector2(pathLengthMm, 0));
+ targetBodyTh = CommonMath.RoundTh(phi - BodyToPathAngleDeg);
+ phiRad = phi / 180.0 * Math.PI;
+ pathDir = new Vector2((float)Math.Cos(phiRad), (float)Math.Sin(phiRad));
+ pathLeft = new Vector2(-pathDir.Y, pathDir.X);
+ self.MultiVehicleAutoIdealX = pathStart.X;
+ self.MultiVehicleAutoIdealY = pathStart.Y;
+ self.MultiVehicleAutoIdealTh = targetBodyTh;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+ DLog.Log(
+ $"WARMUP_REBASE source=fleet center=({x0:0},{y0:0},{theta:0.0}) phi={phi:0.0} targetBody={targetBodyTh:0.0} dst=({dst.X:0},{dst.Y:0})",
+ "FleetCrabDbg");
+ }
+
+ var iter = 0;
+ var lastLog = DateTime.MinValue;
+ var finishDistance = Math.Max(0f, FleetCrabFinishDistance);
+ var slowDistance = Math.Max(finishDistance + 1f, FleetCrabSlowDistance);
+ var baseSpeed = Math.Abs(CrabSpeed);
+ var finishSpeed = Math.Min(baseSpeed, Math.Abs(FleetCrabFinishSpeed));
+ var slowingPow = Math.Max(0.01f, FleetCrabSlowingPow);
+ var accel = Math.Abs(FleetCrabAccel);
+ var startAccel = Math.Abs(FleetCrabStartAccel);
+ var cmdSpeed = 0f;
+ var lastTick = DateTime.Now;
+ var speedRampStart = DateTime.Now;
+ var stopReason = "done";
+
+ while (!_stopping)
+ {
+ iter++;
+
+ if (!TryGetControlFleetCenter(self, out var cx, out var cy, out var cth, out var centerSource))
+ {
+ stopReason = "fleet center invalid";
+ DLog.Log("ABORT: TryGetControlFleetCenter returned false during auto crab.", "FleetCrabDbg");
+ break;
+ }
+ var delta = new Vector2(cx - pathStart.X, cy - pathStart.Y);
+ var along = Vector2.Dot(delta, pathDir);
+ var lateral = Vector2.Dot(delta, pathLeft);
+ var remain = pathLengthMm - along;
+ if (remain <= finishDistance)
+ break;
+
+ var targetSpeed = baseSpeed;
+ var slowRatio = 1f;
+ if (remain < slowDistance)
+ {
+ slowRatio = (float)Math.Pow(Clamp(Math.Max(0, remain) / slowDistance, 0f, 1f), slowingPow);
+ targetSpeed = slowRatio * (baseSpeed - finishSpeed) + finishSpeed;
+ }
+ var now = DateTime.Now;
+ var dt = Math.Max(0.001f, (float)(now - lastTick).TotalSeconds);
+ lastTick = now;
+ var rampElapsed = (now - speedRampStart).TotalSeconds;
+ var activeAccel = rampElapsed < 5.0 ? startAccel : accel;
+ var speed = activeAccel > 0 ? Slew(cmdSpeed, targetSpeed, activeAccel * dt) : targetSpeed;
+ cmdSpeed = speed;
+
+ var baseCrabTh = (float)CommonMath.ThDiff(phi, cth);
+ var headingErr = (float)CommonMath.ThDiff(targetBodyTh, cth);
+ var headingErrReverse = (float)CommonMath.ThDiff(cth, targetBodyTh);
+ var targetBodyToPath = (float)CommonMath.ThDiff(phi, targetBodyTh);
+ var rawBiasMagnitude = (float)(Math.Atan(conf.BiasFac * Math.Abs(lateral) / 1000f /
+ Math.Max(speed, 0.3f)) / Math.PI * 180.0);
+ var biasSign = GetLateralBiasSign(baseCrabTh, cth, speed, gcpLimit, pathLeft, lateral);
+ var rawBiasItem = rawBiasMagnitude * biasSign;
+ var biasItem = ClampAbs(rawBiasItem, conf.BiasThreshold);
+ var yawSplitSign = GetYawSplitSign(baseCrabTh + biasItem, speed, gcpLimit, controlRadius);
+ var rawDthItem = conf.FleetCrabDthLinearFac * headingErr * yawSplitSign;
+ var dthItem = ClampAbs(rawDthItem, conf.FleetCrabDthLinearThreshold);
+ var rawFrontTh = baseCrabTh + biasItem + dthItem;
+ var rawRearTh = baseCrabTh + biasItem - dthItem;
+ ResolveCrabDriveEquivalent(speed, rawFrontTh, rawRearTh, gcpLimit, out var driveSpeed,
+ out var frontTh, out var rearTh, out var reverseEquivalent, out var rawBaseTh);
+ holdFrontTh = frontTh;
+ holdRearTh = rearTh;
+ var idealAlong = Clamp(along, 0f, pathLengthMm);
+ var ideal = pathStart + pathDir * idealAlong;
+
+ self.MultiVehicleScriptEnabled = false;
+ self.MultiVehicleScriptMode = 0;
+ self.MultiVehicleScriptVx = 0;
+ self.MultiVehicleScriptVy = 0;
+ self.MultiVehicleScriptVth = 0;
+ self.MultiVehicleAutoEnabled = true;
+ self.MultiVehicleAutoVx = driveSpeed;
+ self.MultiVehicleAutoFrontTh = frontTh;
+ self.MultiVehicleAutoRearTh = rearTh;
+ self.MultiVehicleAutoIdealX = ideal.X;
+ self.MultiVehicleAutoIdealY = ideal.Y;
+ self.MultiVehicleAutoIdealTh = targetBodyTh;
+ self.MultiVehicleAutoHasIdeal = true;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+
+ if ((DateTime.Now - lastLog).TotalMilliseconds >= 300)
+ {
+ lastLog = DateTime.Now;
+ var snap = self.GetFleetCenterSnapshot();
+ int fleetCnt;
+ lock (self.FleetLock) fleetCnt = self.MultiVehicleFleet.Count;
+ DLog.Log(
+ $"ITER#{iter} centerSrc={centerSource} center=({cx:0},{cy:0},{cth:0.0}) snap=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) " +
+ $"along={along:0} lateral={lateral:0} remain={remain:0} headingErr={headingErr:0.0} " +
+ $"baseTh={baseCrabTh:0.0} bias={biasItem:0.0} dth={dthItem:0.0} " +
+ $"slowRatio={slowRatio:0.000} targetV={targetSpeed:0.000} rampT={rampElapsed:0.0} accel={activeAccel:0.000} auto=(vx:{driveSpeed:0.000},fTh:{frontTh:0.0},rTh:{rearTh:0.0}) " +
+ $"ideal=({ideal.X:0},{ideal.Y:0},{targetBodyTh:0.0}) scriptEn={self.MultiVehicleScriptEnabled} " +
+ $"cnt={fleetCnt}/{conf.MultiVehicleFleetNum}",
+ "FleetCrabDbg");
+ DLog.Log(
+ $"CTRL iter={iter} centerSrc:{centerSource} phi:{phi:0.00} targetBody:{targetBodyTh:0.00} startTheta:{theta:0.00} " +
+ $"cth:{cth:0.00} crabAngle:{CrabAngleDeg:0.00} bodyToPathCfg:{BodyToPathAngleDeg:0.00} " +
+ $"targetBodyToPath:{targetBodyToPath:0.00} bodyToPathNow:{baseCrabTh:0.00} " +
+ $"headingErr(target-current):{headingErr:0.00} reverse(current-target):{headingErrReverse:0.00} yawSign:{yawSplitSign:0} " +
+ $"fleetCrabDthFac:{conf.FleetCrabDthLinearFac:0.000} rawDth:{rawDthItem:0.00} dth:{dthItem:0.00} dthLimit:{conf.FleetCrabDthLinearThreshold:0.00} " +
+ $"lateral:{lateral:0.0} biasFac:{conf.BiasFac:0.000} biasSign:{biasSign:0} rawBias:{rawBiasItem:0.00} bias:{biasItem:0.00} biasLimit:{conf.BiasThreshold:0.00} " +
+ $"baseTh:{baseCrabTh:0.00} rawBase:{rawBaseTh:0.00} rawOut(f:{rawFrontTh:0.00},r:{rawRearTh:0.00}) " +
+ $"out(f:{frontTh:0.00},r:{rearTh:0.00}) gcpLimit:{gcpLimit:0.00} revEq:{reverseEquivalent} " +
+ $"speedRaw:{speed:0.000} speed:{driveSpeed:0.000} rampT:{rampElapsed:0.0} accel:{activeAccel:0.000} along:{along:0.0} remain:{remain:0.0} ideal=({ideal.X:0.0},{ideal.Y:0.0},{targetBodyTh:0.00})",
+ "FleetCrabHeadingDbg");
+ }
+ yield return true;
+ }
+
+ if (_stopping)
+ stopReason = "stop";
+
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoFrontTh = holdFrontTh;
+ self.MultiVehicleAutoRearTh = holdRearTh;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+ DLog.Log(
+ $"STOP_HOLD iter={iter} reason={stopReason} hold=(fTh:{holdFrontTh:0.0},rTh:{holdRearTh:0.0}) cmdSpeed={cmdSpeed:0.000}",
+ "FleetCrabDbg");
+ var settleEnd = DateTime.Now.AddMilliseconds(Math.Max(100, conf.MultiVehicleSyncInterval * 3));
+ while (!_stopping && DateTime.Now < settleEnd)
+ {
+ self.MultiVehicleScriptEnabled = false;
+ self.MultiVehicleScriptMode = 0;
+ self.MultiVehicleAutoEnabled = true;
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoFrontTh = holdFrontTh;
+ self.MultiVehicleAutoRearTh = holdRearTh;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+ yield return true;
+ }
+
+ Cleanup();
+ Hedingben.ToastText("车队蟹行完成", "FleetCrab");
+ DLog.Log($"DONE iter={iter} reason={stopReason}", "FleetCrabDbg");
+ }
+}
diff --git a/MultiWheel/MultiWheelC/FleetCurveWalk.cs b/MultiWheel/MultiWheelC/FleetCurveWalk.cs
new file mode 100644
index 0000000..cb66bcd
--- /dev/null
+++ b/MultiWheel/MultiWheelC/FleetCurveWalk.cs
@@ -0,0 +1,413 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Numerics;
+using ClumsyCore;
+using ClumsyCore.Interfaces;
+using ClumsyCore.Pilot;
+using FundamentalLib;
+using CommonUsage.Chassis;
+using CommonUsage.Mathematics;
+using MDCSToolBox.Clumsy.MotionControllers;
+using MDCSToolBox.Clumsy.Movements;
+using MDCSToolBox.Clumsy.Pilot;
+using MDCSToolBox.Clumsy.Tracks;
+
+namespace MultiWheelC;
+
+public class FleetCurveWalk : MovementDefinition
+{
+ public BezierTrack Track;
+ public List ControlPoints = new();
+ public float CurveSpeed = 0.2f;
+ public float CarDirectionBias = 0f;
+ public int BezierResolution = 100;
+ public float SlowDistance = 2000f;
+ public float FinishDistance = 20f;
+ public float FinishSpeed = 0.02f;
+ public float SlowingPow = 0.8f;
+ public float GcpThetaThreshold = 95f;
+ public float StartSyncTimeoutSec = 8f;
+
+ private bool _stopping;
+ private MultiWheelGeometricController _controller;
+ private MultiWheelChassis _chassis;
+ private bool _savedControlPoints;
+ private float _savedControlRadius;
+ private Vector2 _savedGcp0;
+ private Vector2 _savedGcp1;
+
+ public void Stop()
+ {
+ _stopping = true;
+ if (_controller != null)
+ _controller.BreakAndHold = true;
+ Cleanup();
+ }
+
+ public static bool TryParsePointList(string text, out List points, out string error)
+ {
+ points = new List();
+ error = "";
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ error = "empty control point list";
+ return false;
+ }
+
+ var segments = text.Split(new[] { ';', '|' }, StringSplitOptions.RemoveEmptyEntries);
+ for (var i = 0; i < segments.Length; i++)
+ {
+ var pair = segments[i].Split(new[] { ',', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+ if (pair.Length != 2)
+ {
+ error = $"invalid point #{i + 1}: {segments[i]}";
+ return false;
+ }
+
+ if (!TryParseFloat(pair[0], out var x) || !TryParseFloat(pair[1], out var y))
+ {
+ error = $"invalid number in point #{i + 1}: {segments[i]}";
+ return false;
+ }
+ points.Add(new Vector2(x, y));
+ }
+
+ if (points.Count < 3)
+ {
+ error = "Bezier curve requires at least 3 control points";
+ return false;
+ }
+
+ return true;
+ }
+
+ public static List BuildRelativeControlPoints(Vector2 start, float startTh, List relativePoints)
+ {
+ var source = relativePoints ?? new List();
+ var normalized = new List();
+ if (source.Count == 0 || Vector2.Distance(source[0], Vector2.Zero) > 1f)
+ normalized.Add(Vector2.Zero);
+ for (var i = 0; i < source.Count; i++)
+ normalized.Add(source[i]);
+ if (normalized.Count < 2)
+ normalized.Add(new Vector2(1000f, 0f));
+ if (normalized.Count < 3)
+ normalized.Add(new Vector2(2000f, 0f));
+
+ var result = new List();
+ for (var i = 0; i < normalized.Count; i++)
+ result.Add(CommonMath.Transform2D(start, startTh, normalized[i]));
+ return result;
+ }
+
+ public static List BuildAgvControlPoints(float srcX, float srcY, float dstX, float dstY,
+ params float[] controlPointCoords)
+ {
+ var src = new Vector2(srcX, srcY);
+ var dst = new Vector2(dstX, dstY);
+ var result = new List();
+ if (controlPointCoords == null || controlPointCoords.Length == 0)
+ {
+ result.Add(src);
+ result.Add((src + dst) / 2f);
+ result.Add(dst);
+ return result;
+ }
+
+ if (controlPointCoords.Length % 2 != 0)
+ throw new ArgumentException("FleetCurve controlPointCoords must contain x,y pairs.");
+
+ var supplied = new List();
+ for (var i = 0; i < controlPointCoords.Length; i += 2)
+ supplied.Add(new Vector2(controlPointCoords[i], controlPointCoords[i + 1]));
+
+ if (supplied.Count >= 3 &&
+ Vector2.Distance(supplied[0], src) <= 10f &&
+ Vector2.Distance(supplied[supplied.Count - 1], dst) <= 10f)
+ return supplied;
+
+ result.Add(src);
+ for (var i = 0; i < supplied.Count; i++)
+ result.Add(supplied[i]);
+ result.Add(dst);
+ if (result.Count < 3)
+ result.Insert(1, (src + dst) / 2f);
+ return result;
+ }
+
+ private static bool TryParseFloat(string text, out float value)
+ {
+ return float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value) ||
+ float.TryParse(text, out value);
+ }
+
+ private static float ClampAbs(float value, float limit)
+ {
+ var absLimit = Math.Abs(limit);
+ if (absLimit <= 0) return value;
+ if (value > absLimit) return absLimit;
+ if (value < -absLimit) return -absLimit;
+ return value;
+ }
+
+ private static bool TryGetControlFleetCenter(PilotDefinition self, out float centerX, out float centerY,
+ out float centerTh, out string source)
+ {
+ if (self.TryGetFleetCenterFromMembers(out centerX, out centerY, out centerTh))
+ {
+ source = "fleet";
+ return true;
+ }
+
+ if (self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh))
+ {
+ source = "slam";
+ return true;
+ }
+
+ source = "none";
+ return false;
+ }
+
+ private void Cleanup()
+ {
+ var self = PilotDefinition.Self;
+ self.MultiVehicleScriptVx = 0;
+ self.MultiVehicleScriptVy = 0;
+ self.MultiVehicleScriptVth = 0;
+ self.MultiVehicleScriptMode = 0;
+ self.MultiVehicleScriptEnabled = false;
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoFrontTh = 0;
+ self.MultiVehicleAutoRearTh = 0;
+ self.MultiVehicleAutoHasIdeal = false;
+ self.MultiVehicleAutoEnabled = false;
+ RestoreControlPointRadius();
+ }
+
+ private void ApplyFleetControlPointRadius(MultiWheelChassis chassis, float radius)
+ {
+ if (!_savedControlPoints)
+ {
+ _chassis = chassis;
+ _savedControlRadius = chassis.ControlPointRadius;
+ var gcps = chassis.GetGeometricControlPoints();
+ if (gcps.Count >= 2)
+ {
+ _savedGcp0 = gcps[0].Position;
+ _savedGcp1 = gcps[1].Position;
+ }
+ _savedControlPoints = true;
+ }
+
+ chassis.ControlPointRadius = radius;
+ var points = chassis.GetGeometricControlPoints();
+ if (points.Count >= 2)
+ {
+ points[0].Position = new Vector2(radius, 0);
+ points[1].Position = new Vector2(-radius, 0);
+ }
+ }
+
+ private void RestoreControlPointRadius()
+ {
+ if (!_savedControlPoints || _chassis == null)
+ return;
+
+ _chassis.ControlPointRadius = _savedControlRadius;
+ var points = _chassis.GetGeometricControlPoints();
+ if (points.Count >= 2)
+ {
+ points[0].Position = _savedGcp0;
+ points[1].Position = _savedGcp1;
+ }
+ _savedControlPoints = false;
+ }
+
+ private static void WriteWarmupAuto(PilotDefinition self, Vector2 idealPos, float idealTh,
+ float frontTh, float rearTh)
+ {
+ self.MultiVehicleScriptEnabled = false;
+ self.MultiVehicleScriptMode = 0;
+ self.MultiVehicleScriptVx = 0;
+ self.MultiVehicleScriptVy = 0;
+ self.MultiVehicleScriptVth = 0;
+ self.MultiVehicleAutoEnabled = true;
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoFrontTh = frontTh;
+ self.MultiVehicleAutoRearTh = rearTh;
+ self.MultiVehicleAutoIdealX = idealPos.X;
+ self.MultiVehicleAutoIdealY = idealPos.Y;
+ self.MultiVehicleAutoIdealTh = idealTh;
+ self.MultiVehicleAutoHasIdeal = true;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+ }
+
+ public override IEnumerable Get()
+ {
+ var self = PilotDefinition.Self;
+ var conf = PilotDefinition.Conf;
+ var chassis = BasicPilotBase.Chassis as MultiWheelChassis;
+ _stopping = false;
+
+ if (chassis == null)
+ {
+ DLog.Log("ABORT: FleetCurveWalk requires MultiWheelChassis.", "FleetCurveDbg");
+ yield break;
+ }
+
+ if (conf.MultiVehicleMasterEndpoint != "/")
+ {
+ DLog.Log($"ABORT: FleetCurveWalk must run on master endpoint, endpoint={conf.MultiVehicleMasterEndpoint}",
+ "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve requires master vehicle", "FleetCurve");
+ yield break;
+ }
+
+ if (Track == null && (ControlPoints == null || ControlPoints.Count < 3))
+ {
+ DLog.Log("ABORT: FleetCurveWalk requires a BezierTrack or at least 3 control points.", "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve requires track or >=3 control points", "FleetCurve");
+ yield break;
+ }
+
+ if (!TryGetControlFleetCenter(self, out var x0, out var y0, out var theta, out var initialCenterSource))
+ {
+ DLog.Log("ABORT: FleetCurveWalk failed to read fleet center.", "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve requires master localization", "FleetCurve");
+ yield break;
+ }
+
+ var baseSpeed = Math.Abs(CurveSpeed);
+ if (baseSpeed <= 1e-4f)
+ {
+ DLog.Log("ABORT: FleetCurveWalk speed is zero.", "FleetCurveDbg");
+ yield break;
+ }
+
+ var resolution = Math.Max(2, BezierResolution);
+ var speedFinish = Math.Min(baseSpeed, Math.Abs(FinishSpeed));
+ var gcpLimit = Math.Max(1f, Math.Abs(GcpThetaThreshold));
+ var controlRadius = Math.Max(1f, Math.Abs(conf.MultiVehicleControlRadius > 0
+ ? conf.MultiVehicleControlRadius
+ : conf.TestCarSyncDistance / 2f));
+
+ ApplyFleetControlPointRadius(chassis, controlRadius);
+
+ try
+ {
+ var track = Track;
+ var trackSource = "external";
+ if (track == null)
+ {
+ var points = new List(ControlPoints);
+ track = new BezierTrack(points, resolution);
+ trackSource = "controlPoints";
+ }
+ track.CarDirectionBias = CarDirectionBias;
+ track.Speed = baseSpeed;
+
+ var center = new Vector2(x0, y0);
+ var (idealPos, idealAngle, bias, pd) = track.QueryTangentPoint(center);
+ var carDirection = (float)CommonMath.ThDiff(theta, CarDirectionBias);
+ var holdTh = ClampAbs((float)CommonMath.ThDiff(idealAngle, carDirection), gcpLimit);
+ var targetBodyTh = (float)CommonMath.RoundTh(idealAngle + CarDirectionBias);
+
+ DLog.Log(
+ $"START center=({x0:0},{y0:0},{theta:0.0}) source={initialCenterSource} " +
+ $"track={track.GetType().Name} trackSource={trackSource} controls={ControlPoints?.Count ?? 0} " +
+ $"len={track.Length():0} speed={baseSpeed:0.000} bias={CarDirectionBias:0.0} " +
+ $"query=({idealPos.X:0},{idealPos.Y:0}) tangent={idealAngle:0.0} targetBody={targetBodyTh:0.0} " +
+ $"pathBias={bias:0.0} pd={pd:0.0} hold={holdTh:0.0} radius={controlRadius:0}",
+ "FleetCurveDbg");
+
+ var warmStart = DateTime.Now;
+ var warmSeqBaseline = self.BeginFleetMotionWarmup();
+ WriteWarmupAuto(self, idealPos, targetBodyTh, holdTh, holdTh);
+ self.PrimeMasterAutoFromSlam();
+
+ var warmEnd = warmStart.AddSeconds(Math.Max(1.0f, StartSyncTimeoutSec));
+ var warmIter = 0;
+ var warmReady = false;
+ var warmDetail = "";
+ while (!_stopping && DateTime.Now < warmEnd)
+ {
+ warmIter++;
+ WriteWarmupAuto(self, idealPos, targetBodyTh, holdTh, holdTh);
+ self.PrimeMasterAutoFromSlam();
+
+ if (warmIter % 5 == 0)
+ {
+ var snap = self.GetFleetCenterSnapshot();
+ int cnt;
+ lock (self.FleetLock) cnt = self.MultiVehicleFleet.Count;
+ DLog.Log(
+ $"WARMUP#{warmIter} snap=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) " +
+ $"cnt={cnt}/{conf.MultiVehicleFleetNum} detail={warmDetail}",
+ "FleetCurveDbg");
+ }
+
+ if (self.IsFleetMotionWarmupReady(warmStart, warmSeqBaseline,
+ conf.TestCarSyncTh, conf.TestCarSyncDistance, out warmDetail))
+ {
+ warmReady = true;
+ DLog.Log($"WARMUP done iter={warmIter} detail={warmDetail}", "FleetCurveDbg");
+ break;
+ }
+ yield return true;
+ }
+
+ if (!warmReady)
+ {
+ DLog.Log($"WARMUP timeout: fleet startup sync failed, abort curve action. detail={warmDetail}",
+ "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve startup sync timeout", "FleetCurve");
+ Cleanup();
+ yield break;
+ }
+
+ _controller = new ChassisController { BaseSpeed = baseSpeed }.Get();
+ _controller.MultiVehicleSync = true;
+ _controller.BaseSpeed = baseSpeed;
+ _controller.SlowDistance = Math.Max(FinishDistance + 1f, SlowDistance);
+ _controller.FinishDistance = Math.Max(0f, FinishDistance);
+ _controller.FinishSpeed = speedFinish;
+ _controller.SlowingPow = Math.Max(0.01f, SlowingPow);
+ _controller.GcpThetaThreshold = gcpLimit;
+ _controller.AddTrack(track, "FleetCurve");
+
+ Hedingben.ToastText($"FleetCurve len {track.Length():0}mm speed {baseSpeed:0.00}", "FleetCurve");
+
+ foreach (var running in _controller.Track())
+ {
+ if (_stopping)
+ break;
+ if (!running)
+ break;
+ yield return true;
+ }
+
+ if (!_stopping)
+ {
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+ var settleEnd = DateTime.Now.AddMilliseconds(Math.Max(100, conf.MultiVehicleSyncInterval * 3));
+ while (!_stopping && DateTime.Now < settleEnd)
+ {
+ self.MultiVehicleAutoEnabled = true;
+ self.MultiVehicleAutoVx = 0;
+ self.MultiVehicleAutoCmdTime = DateTime.Now;
+ yield return true;
+ }
+ }
+
+ DLog.Log($"DONE stopping={_stopping}", "FleetCurveDbg");
+ }
+ finally
+ {
+ Cleanup();
+ _controller = null;
+ }
+ }
+}
diff --git a/MultiWheel/MultiWheelC/MovementTests.cs b/MultiWheel/MultiWheelC/MovementTests.cs
index a10ce79..dc8e2c3 100644
--- a/MultiWheel/MultiWheelC/MovementTests.cs
+++ b/MultiWheel/MultiWheelC/MovementTests.cs
@@ -7,10 +7,8 @@ using ClumsyCore.Pilot;
using FundamentalLib;
using CommonUsage.Chassis;
using CommonUsage.Mathematics;
-using MDCSToolBox.Clumsy.MotionControllers;
using MDCSToolBox.Clumsy.Movements;
using MDCSToolBox.Clumsy.Pilot;
-using MDCSToolBox.Clumsy.Tracks;
namespace MultiWheelC;
@@ -426,518 +424,53 @@ public class FleetRotateInPlaceTest : MovementTest
}
}
-// ===== 车队联动-自动蟹行动作 =====
-// 以当前车队中心为起点,构造指定方向和长度的直线路径;
-// 执行侧直接写 MultiVehicleAuto...,由 TickMultiVehicle 自动分支统一下发。
-//
-// 控制思路参考 MDCSToolbox 几何控制器,但实现收在 MultiWheelC 内:
-// 1) 读取主车 Detour 反推车队中心,计算沿直线的进度、横向偏差和车身目标朝向偏差;
-// 2) 根据横向偏差给前后 GCP 同向修正,根据车身目标朝向偏差给前后 GCP 反向修正;
-// 3) 根据终点距离减速,并发布 ideal fleet center 给从车做前馈。
-//
-// 前提:在主车(MultiVehicleMasterEndpoint=="/")运行,且主车有 Detour 定位。
-public class FleetCrabWalk : MovementDefinition
+[MovementTest(name = "车队联动-曲线行走")]
+public class FleetCurveWalkTest : MovementTest
{
- /// 路径方向相对启动时车队朝向的夹角(deg,逆时针为正)。
- public float CrabAngleDeg = 45f;
+ private FleetCurveWalk _proc;
+ private DriveTask _task;
- /// 路径方向相对车身目标朝向的夹角(deg,逆时针为正)。MovementTest 会设为 CrabAngleDeg,以保持启动时车身朝向。
- public float BodyToPathAngleDeg = 45f;
-
- /// 路径长度(mm)。
- public float CrabLengthMm = 2000f;
-
- /// 行驶速度(m/s)。
- public float CrabSpeed = 0.2f;
-
- /// 速度命令加速度限制(m/s^2),小于等于 0 表示不限制。
- public float FleetCrabAccel = 0.2f;
-
- /// 预对齐后正式下发速度前 5 秒加速度限制(m/s^2),小于等于 0 表示不限制。
- public float FleetCrabStartAccel = 0.01f;
-
- /// 末端开始减速距离(mm)。
- public float FleetCrabSlowDistance = 2000f;
-
- /// 完成距离(mm),低于该剩余距离结束动作。
- public float FleetCrabFinishDistance = 20f;
-
- /// 末端最低速度(m/s)。
- public float FleetCrabFinishSpeed = 0.02f;
-
- /// 末端减速曲线指数。
- public float FleetCrabSlowingPow = 0.8f;
-
- /// 前后 GCP 舵角修正上限(deg)。
- public float GcpThetaThreshold = 95f;
-
- private bool _stopping;
-
- private void Cleanup()
+ public override void Test()
{
var self = PilotDefinition.Self;
- self.MultiVehicleScriptVx = 0;
- self.MultiVehicleScriptVy = 0;
- self.MultiVehicleScriptVth = 0;
- self.MultiVehicleScriptMode = 0;
- self.MultiVehicleScriptEnabled = false;
- self.MultiVehicleAutoVx = 0;
- self.MultiVehicleAutoFrontTh = 0;
- self.MultiVehicleAutoRearTh = 0;
- self.MultiVehicleAutoHasIdeal = false;
- self.MultiVehicleAutoEnabled = false;
- }
-
- public void Stop()
- {
- _stopping = true;
- Cleanup();
- }
-
- private static float Clamp(float value, float min, float max)
- {
- if (value < min) return min;
- if (value > max) return max;
- return value;
- }
-
- private static float ClampAbs(float value, float limit)
- {
- var absLimit = Math.Abs(limit);
- if (absLimit <= 0) return value;
- if (value > absLimit) return absLimit;
- if (value < -absLimit) return -absLimit;
- return value;
- }
-
- private static float Slew(float current, float target, float maxDelta)
- {
- if (maxDelta <= 0) return target;
- if (target > current + maxDelta) return current + maxDelta;
- if (target < current - maxDelta) return current - maxDelta;
- return target;
- }
-
- private static float AverageAngle(float frontTh, float rearTh)
- {
- var diff = (float)CommonMath.ThDiff(frontTh, rearTh);
- return (float)CommonMath.RoundTh(rearTh + diff / 2f);
- }
-
- private static void ResolveCrabDriveEquivalent(float speed, float rawFrontTh, float rawRearTh, float steerLimit,
- out float driveSpeed, out float frontTh, out float rearTh, out bool reverseEquivalent, out float rawBaseTh)
- {
- var limit = Math.Min(179f, Math.Max(1f, Math.Abs(steerLimit)));
- rawBaseTh = AverageAngle(rawFrontTh, rawRearTh);
- driveSpeed = speed;
- frontTh = rawFrontTh;
- rearTh = rawRearTh;
- reverseEquivalent = false;
-
- if (rawBaseTh > limit)
+ if (!self.TryGetFleetCenterFromMembers(out var x, out var y, out var th) &&
+ !self.TryGetFleetCenterFromSlam(out x, out y, out th))
{
- frontTh = (float)CommonMath.RoundTh(frontTh - 180f);
- rearTh = (float)CommonMath.RoundTh(rearTh - 180f);
- driveSpeed = -driveSpeed;
- reverseEquivalent = true;
- }
- else if (rawBaseTh < -limit)
- {
- frontTh = (float)CommonMath.RoundTh(frontTh + 180f);
- rearTh = (float)CommonMath.RoundTh(rearTh + 180f);
- driveSpeed = -driveSpeed;
- reverseEquivalent = true;
+ DLog.Log("FleetCurveWalkTest abort: failed to read fleet center.", "FleetCurveDbg");
+ Hedingben.ToastText("FleetCurve requires master localization", "FleetCurve");
+ return;
}
- frontTh = ClampAbs(frontTh, limit);
- rearTh = ClampAbs(rearTh, limit);
+ if (!FleetCurveWalk.TryParsePointList(PilotDefinition.Conf.FleetCurveControlPoints,
+ out var relativePoints, out var error))
+ {
+ DLog.Log($"FleetCurveWalkTest abort: {error}", "FleetCurveDbg");
+ Hedingben.ToastText(error, "FleetCurve");
+ return;
+ }
+
+ var controlPoints = FleetCurveWalk.BuildRelativeControlPoints(new Vector2(x, y), th, relativePoints);
+ _proc = new FleetCurveWalk
+ {
+ ControlPoints = controlPoints,
+ CurveSpeed = PilotDefinition.Conf.FleetCurveSpeed,
+ CarDirectionBias = PilotDefinition.Conf.FleetCurveCarDirectionBias,
+ BezierResolution = PilotDefinition.Conf.FleetCurveBezierResolution,
+ SlowDistance = PilotDefinition.Conf.FleetCurveSlowDistance,
+ FinishDistance = PilotDefinition.Conf.FleetCurveFinishDistance,
+ FinishSpeed = PilotDefinition.Conf.FleetCurveFinishSpeed,
+ SlowingPow = PilotDefinition.Conf.FleetCurveSlowingPow,
+ GcpThetaThreshold = PilotDefinition.Conf.FleetCrabGcpThetaThreshold,
+ StartSyncTimeoutSec = PilotDefinition.Conf.FleetCrabStartSyncTimeoutSec
+ };
+ _task = new DriveTask(_proc.Get());
+ _task.Wait();
}
- private static float ProbeSpeed(float speed)
+ public override void TestStop()
{
- return Math.Abs(speed) > 1e-4f ? speed : 1f;
- }
-
- private static bool TryGetMotionYawSign(float frontTh, float rearTh, float driveSpeed, float controlRadius,
- out float yawSign)
- {
- yawSign = 0f;
- if (Math.Abs(CommonMath.ThDiff(frontTh, rearTh)) <= 1e-3f)
- return false;
-
- var radius = Math.Max(1f, Math.Abs(controlRadius));
- Vector2 pFront = new(radius, 0), pRear = new(-radius, 0),
- normFront = CommonMath.Transform2D(pFront, frontTh + 90f, Vector2.UnitX),
- normRear = CommonMath.Transform2D(pRear, rearTh + 90f, Vector2.UnitX);
- var (intersect, center) = CommonMath.TwoLinesIntersection(pFront, normFront, pRear, normRear);
- if (!intersect)
- return false;
-
- // Match MultiWheelChassis.SendMotion: the tangent side is selected by
- // rotCenter.Y > 1, and reverse-equivalent motion flips the yaw direction.
- var tangentSign = center.Y > 1f ? 1f : -1f;
- var speedSign = driveSpeed >= 0f ? 1f : -1f;
- yawSign = speedSign * tangentSign;
- return true;
- }
-
- private static float GetYawSplitSign(float baseTh, float speed, float steerLimit, float controlRadius)
- {
- const float probeDth = 1f;
- ResolveCrabDriveEquivalent(ProbeSpeed(speed), baseTh + probeDth, baseTh - probeDth, steerLimit,
- out var probeSpeed, out var probeFrontTh, out var probeRearTh, out _, out _);
- return TryGetMotionYawSign(probeFrontTh, probeRearTh, probeSpeed, controlRadius, out var yawSign)
- ? yawSign
- : 1f;
- }
-
- private static float EstimateLateralVelocity(float bodyTh, float frontTh, float rearTh, float driveSpeed,
- Vector2 pathLeft)
- {
- var motionTh = (float)CommonMath.RoundTh(bodyTh + AverageAngle(frontTh, rearTh));
- var rad = motionTh / 180f * Math.PI;
- var dir = new Vector2((float)Math.Cos(rad), (float)Math.Sin(rad));
- if (driveSpeed < 0f)
- dir = -dir;
- return Vector2.Dot(dir, pathLeft);
- }
-
- private static float ScoreBiasSign(float baseTh, float bodyTh, float speed, float steerLimit, Vector2 pathLeft,
- float lateral, float biasProbe)
- {
- ResolveCrabDriveEquivalent(ProbeSpeed(speed), baseTh + biasProbe, baseTh + biasProbe, steerLimit,
- out var probeSpeed, out var probeFrontTh, out var probeRearTh, out _, out _);
- var lateralVelocity = EstimateLateralVelocity(bodyTh, probeFrontTh, probeRearTh, probeSpeed, pathLeft);
- return -Math.Sign(lateral) * lateralVelocity;
- }
-
- private static float GetLateralBiasSign(float baseTh, float bodyTh, float speed, float steerLimit, Vector2 pathLeft,
- float lateral)
- {
- if (Math.Abs(lateral) <= 1e-3f)
- return 1f;
-
- const float probeBias = 1f;
- var positiveScore = ScoreBiasSign(baseTh, bodyTh, speed, steerLimit, pathLeft, lateral, probeBias);
- var negativeScore = ScoreBiasSign(baseTh, bodyTh, speed, steerLimit, pathLeft, lateral, -probeBias);
- return positiveScore >= negativeScore ? 1f : -1f;
- }
-
- private static bool TryGetControlFleetCenter(PilotDefinition self, out float centerX, out float centerY,
- out float centerTh, out string source)
- {
- if (self.TryGetFleetCenterFromMembers(out centerX, out centerY, out centerTh))
- {
- source = "fleet";
- return true;
- }
-
- if (self.TryGetFleetCenterFromSlam(out centerX, out centerY, out centerTh))
- {
- source = "slam";
- return true;
- }
-
- source = "none";
- return false;
- }
-
- public override IEnumerable Get()
- {
- var self = PilotDefinition.Self;
- var conf = PilotDefinition.Conf;
- var chassis = BasicPilotBase.Chassis as MultiWheelChassis;
- if (chassis == null)
- {
- DLog.Log("ABORT: FleetCrabWalk requires MultiWheelChassis.", "FleetCrabDbg");
- yield break;
- }
- _stopping = false;
-
- DLog.Log(
- $"ENTER master?={conf.MultiVehicleMasterEndpoint == "/"} endpoint={conf.MultiVehicleMasterEndpoint} " +
- $"fleetNum={conf.MultiVehicleFleetNum} useDetect={conf.MultiVehicleUseDetect} " +
- $"syncUseDetour={conf.MultiVehicleSyncUseDetour} useIdealCenter={conf.MultiVehicleAutoUseIdealCenter} " +
- $"autoFields=true pathMode=relative pathAngle={CrabAngleDeg:0.0} " +
- $"bodyToPath={BodyToPathAngleDeg:0.0} gcpLimit={GcpThetaThreshold:0.0} " +
- $"biasFac={conf.BiasFac:0.00} fleetCrabDthFac={conf.FleetCrabDthLinearFac:0.00}",
- "FleetCrabDbg");
-
- if (conf.MultiVehicleMasterEndpoint != "/")
- {
- DLog.Log($"ABORT: 非主车 (endpoint={conf.MultiVehicleMasterEndpoint})", "FleetCrabDbg");
- Hedingben.ToastText("车队蟹行需在主车(主车端点=\"/\")运行", "FleetCrab");
- yield break;
- }
-
- // 注意:getCartLocation() 在无有效 Detour 定位时会阻塞——若卡在这里且后面看不到 CENTER 日志,即定位未就绪。
- DLog.Log("主车校验通过,开始读取车队中心 (getCartLocation 无定位会阻塞)…", "FleetCrabDbg");
- if (!TryGetControlFleetCenter(self, out var x0, out var y0, out var theta, out var initialCenterSource))
- {
- DLog.Log("ABORT: TryGetFleetCenterFromSlam 返回 false (无定位)", "FleetCrabDbg");
- Hedingben.ToastText("车队蟹行需要主车 Detour 定位", "FleetCrab");
- yield break;
- }
- DLog.Log($"CENTER 车队中心=({x0:0},{y0:0},{theta:0.0})", "FleetCrabDbg");
-
- DLog.Log($"CENTER_SOURCE source={initialCenterSource} center=({x0:0},{y0:0},{theta:0.0})", "FleetCrabDbg");
-
- var pathStart = new Vector2(x0, y0);
- var pathLengthMm = CrabLengthMm;
- var phi = CommonMath.RoundTh(theta + CrabAngleDeg);
- var dst = CommonMath.Transform2D(pathStart, phi, new Vector2(pathLengthMm, 0));
- var targetBodyTh = CommonMath.RoundTh(phi - BodyToPathAngleDeg);
- var phiRad = phi / 180.0 * Math.PI;
- var pathDir = new Vector2((float)Math.Cos(phiRad), (float)Math.Sin(phiRad));
- var pathLeft = new Vector2(-pathDir.Y, pathDir.X);
-
- DLog.Log(
- $"START center=({x0:0},{y0:0},{theta:0.0}) pathMode=relative " +
- $"src=({pathStart.X:0},{pathStart.Y:0}) pathAngle={CrabAngleDeg:0.0} bodyToPath={BodyToPathAngleDeg:0.0} " +
- $"phi={phi:0.0} targetBody={targetBodyTh:0.0} " +
- $"len={pathLengthMm:0} dst=({dst.X:0},{dst.Y:0}) speed={CrabSpeed:0.000} startAccel={FleetCrabStartAccel:0.000} accel={FleetCrabAccel:0.000} " +
- $"slow={FleetCrabSlowDistance:0} finishDist={FleetCrabFinishDistance:0} " +
- $"finishSpeed={FleetCrabFinishSpeed:0.000} slowingPow={FleetCrabSlowingPow:0.00}",
- "FleetCrabDbg");
-
- var gcpLimit = Math.Max(1f, Math.Abs(GcpThetaThreshold));
- var controlRadius = Math.Max(1f, Math.Abs(conf.MultiVehicleControlRadius > 0
- ? conf.MultiVehicleControlRadius
- : conf.TestCarSyncDistance / 2f));
- ResolveCrabDriveEquivalent(0f, (float)CommonMath.ThDiff(phi, theta),
- (float)CommonMath.ThDiff(phi, theta), gcpLimit, out _, out var holdFrontTh, out var holdRearTh,
- out _, out _);
- var warmStart = DateTime.Now;
- var warmSeqBaseline = self.BeginFleetMotionWarmup();
-
- self.MultiVehicleScriptEnabled = false;
- self.MultiVehicleScriptMode = 0;
- self.MultiVehicleScriptVx = 0;
- self.MultiVehicleScriptVy = 0;
- self.MultiVehicleScriptVth = 0;
- self.MultiVehicleAutoEnabled = true;
- self.MultiVehicleAutoVx = 0;
- self.MultiVehicleAutoFrontTh = holdFrontTh;
- self.MultiVehicleAutoRearTh = holdRearTh;
- self.MultiVehicleAutoIdealX = pathStart.X;
- self.MultiVehicleAutoIdealY = pathStart.Y;
- self.MultiVehicleAutoIdealTh = targetBodyTh;
- self.MultiVehicleAutoHasIdeal = true;
- self.MultiVehicleAutoCmdTime = DateTime.Now;
- self.PrimeMasterAutoFromSlam();
- DLog.Log(
- $"WARMUP auto fields enabled, waiting for fleet startup sync seqBase={warmSeqBaseline} " +
- $"hold=({holdFrontTh:0.00},{holdRearTh:0.00})",
- "FleetCrabDbg");
-
- var warmEnd = warmStart.AddSeconds(Math.Max(1.0f, conf.FleetCrabStartSyncTimeoutSec));
- var warmIter = 0;
- var warmReady = false;
- var warmDetail = "";
- while (!_stopping && DateTime.Now < warmEnd)
- {
- warmIter++;
- self.MultiVehicleScriptEnabled = false;
- self.MultiVehicleScriptMode = 0;
- self.MultiVehicleAutoEnabled = true;
- self.MultiVehicleAutoVx = 0;
- self.MultiVehicleAutoFrontTh = holdFrontTh;
- self.MultiVehicleAutoRearTh = holdRearTh;
- self.MultiVehicleAutoIdealX = pathStart.X;
- self.MultiVehicleAutoIdealY = pathStart.Y;
- self.MultiVehicleAutoIdealTh = targetBodyTh;
- self.MultiVehicleAutoHasIdeal = true;
- self.MultiVehicleAutoCmdTime = DateTime.Now;
- self.PrimeMasterAutoFromSlam();
- var snap = self.GetFleetCenterSnapshot();
- int cnt;
- lock (self.FleetLock) cnt = self.MultiVehicleFleet.Count;
- if (warmIter % 5 == 0)
- DLog.Log(
- $"WARMUP#{warmIter} 快照=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) tick={snap.Tick} " +
- $"autoEn={self.MultiVehicleAutoEnabled} scriptEn={self.MultiVehicleScriptEnabled} cnt={cnt}/{conf.MultiVehicleFleetNum} " +
- $"detail={warmDetail}",
- "FleetCrabDbg");
- if (self.IsFleetMotionWarmupReady(warmStart, warmSeqBaseline,
- conf.TestCarSyncTh, conf.TestCarSyncDistance, out warmDetail))
- {
- warmReady = true;
- DLog.Log(
- $"WARMUP done iter={warmIter} 快照=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) cnt={cnt} detail={warmDetail}",
- "FleetCrabDbg");
- break;
- }
- yield return true;
- }
- if (!warmReady)
- {
- DLog.Log($"WARMUP timeout: fleet startup sync failed, abort action. detail={warmDetail}",
- "FleetCrabDbg");
- Hedingben.ToastText("车队蟹行启动同步超时,已取消", "FleetCrab");
- Cleanup();
- yield break;
- }
-
- Hedingben.ToastText($"车队蟹行 路径{phi:0.0}° 车身夹角{BodyToPathAngleDeg:0.0}° 长度{pathLengthMm:0}mm", "FleetCrab");
-
- if (warmReady && self.TryGetFleetCenterFromMembers(out var warmX, out var warmY, out var warmTh))
- {
- x0 = warmX;
- y0 = warmY;
- theta = warmTh;
- pathStart = new Vector2(x0, y0);
- phi = CommonMath.RoundTh(theta + CrabAngleDeg);
- dst = CommonMath.Transform2D(pathStart, phi, new Vector2(pathLengthMm, 0));
- targetBodyTh = CommonMath.RoundTh(phi - BodyToPathAngleDeg);
- phiRad = phi / 180.0 * Math.PI;
- pathDir = new Vector2((float)Math.Cos(phiRad), (float)Math.Sin(phiRad));
- pathLeft = new Vector2(-pathDir.Y, pathDir.X);
- self.MultiVehicleAutoIdealX = pathStart.X;
- self.MultiVehicleAutoIdealY = pathStart.Y;
- self.MultiVehicleAutoIdealTh = targetBodyTh;
- self.MultiVehicleAutoCmdTime = DateTime.Now;
- DLog.Log(
- $"WARMUP_REBASE source=fleet center=({x0:0},{y0:0},{theta:0.0}) phi={phi:0.0} targetBody={targetBodyTh:0.0} dst=({dst.X:0},{dst.Y:0})",
- "FleetCrabDbg");
- }
-
- var iter = 0;
- var lastLog = DateTime.MinValue;
- var finishDistance = Math.Max(0f, FleetCrabFinishDistance);
- var slowDistance = Math.Max(finishDistance + 1f, FleetCrabSlowDistance);
- var baseSpeed = Math.Abs(CrabSpeed);
- var finishSpeed = Math.Min(baseSpeed, Math.Abs(FleetCrabFinishSpeed));
- var slowingPow = Math.Max(0.01f, FleetCrabSlowingPow);
- var accel = Math.Abs(FleetCrabAccel);
- var startAccel = Math.Abs(FleetCrabStartAccel);
- var cmdSpeed = 0f;
- var lastTick = DateTime.Now;
- var speedRampStart = DateTime.Now;
- var stopReason = "done";
-
- while (!_stopping)
- {
- iter++;
-
- if (!TryGetControlFleetCenter(self, out var cx, out var cy, out var cth, out var centerSource))
- {
- stopReason = "fleet center invalid";
- DLog.Log("ABORT: TryGetControlFleetCenter returned false during auto crab.", "FleetCrabDbg");
- break;
- }
- var delta = new Vector2(cx - pathStart.X, cy - pathStart.Y);
- var along = Vector2.Dot(delta, pathDir);
- var lateral = Vector2.Dot(delta, pathLeft);
- var remain = pathLengthMm - along;
- if (remain <= finishDistance)
- break;
-
- var targetSpeed = baseSpeed;
- var slowRatio = 1f;
- if (remain < slowDistance)
- {
- slowRatio = (float)Math.Pow(Clamp(Math.Max(0, remain) / slowDistance, 0f, 1f), slowingPow);
- targetSpeed = slowRatio * (baseSpeed - finishSpeed) + finishSpeed;
- }
- var now = DateTime.Now;
- var dt = Math.Max(0.001f, (float)(now - lastTick).TotalSeconds);
- lastTick = now;
- var rampElapsed = (now - speedRampStart).TotalSeconds;
- var activeAccel = rampElapsed < 5.0 ? startAccel : accel;
- var speed = activeAccel > 0 ? Slew(cmdSpeed, targetSpeed, activeAccel * dt) : targetSpeed;
- cmdSpeed = speed;
-
- var baseCrabTh = (float)CommonMath.ThDiff(phi, cth);
- var headingErr = (float)CommonMath.ThDiff(targetBodyTh, cth);
- var headingErrReverse = (float)CommonMath.ThDiff(cth, targetBodyTh);
- var targetBodyToPath = (float)CommonMath.ThDiff(phi, targetBodyTh);
- var rawBiasMagnitude = (float)(Math.Atan(conf.BiasFac * Math.Abs(lateral) / 1000f /
- Math.Max(speed, 0.3f)) / Math.PI * 180.0);
- var biasSign = GetLateralBiasSign(baseCrabTh, cth, speed, gcpLimit, pathLeft, lateral);
- var rawBiasItem = rawBiasMagnitude * biasSign;
- var biasItem = ClampAbs(rawBiasItem, conf.BiasThreshold);
- var yawSplitSign = GetYawSplitSign(baseCrabTh + biasItem, speed, gcpLimit, controlRadius);
- var rawDthItem = conf.FleetCrabDthLinearFac * headingErr * yawSplitSign;
- var dthItem = ClampAbs(rawDthItem, conf.FleetCrabDthLinearThreshold);
- var rawFrontTh = baseCrabTh + biasItem + dthItem;
- var rawRearTh = baseCrabTh + biasItem - dthItem;
- ResolveCrabDriveEquivalent(speed, rawFrontTh, rawRearTh, gcpLimit, out var driveSpeed,
- out var frontTh, out var rearTh, out var reverseEquivalent, out var rawBaseTh);
- holdFrontTh = frontTh;
- holdRearTh = rearTh;
- var idealAlong = Clamp(along, 0f, pathLengthMm);
- var ideal = pathStart + pathDir * idealAlong;
-
- self.MultiVehicleScriptEnabled = false;
- self.MultiVehicleScriptMode = 0;
- self.MultiVehicleScriptVx = 0;
- self.MultiVehicleScriptVy = 0;
- self.MultiVehicleScriptVth = 0;
- self.MultiVehicleAutoEnabled = true;
- self.MultiVehicleAutoVx = driveSpeed;
- self.MultiVehicleAutoFrontTh = frontTh;
- self.MultiVehicleAutoRearTh = rearTh;
- self.MultiVehicleAutoIdealX = ideal.X;
- self.MultiVehicleAutoIdealY = ideal.Y;
- self.MultiVehicleAutoIdealTh = targetBodyTh;
- self.MultiVehicleAutoHasIdeal = true;
- self.MultiVehicleAutoCmdTime = DateTime.Now;
-
- if ((DateTime.Now - lastLog).TotalMilliseconds >= 300)
- {
- lastLog = DateTime.Now;
- var snap = self.GetFleetCenterSnapshot();
- int fleetCnt;
- lock (self.FleetLock) fleetCnt = self.MultiVehicleFleet.Count;
- DLog.Log(
- $"ITER#{iter} centerSrc={centerSource} center=({cx:0},{cy:0},{cth:0.0}) snap=({snap.X:0},{snap.Y:0},{snap.Th:0.0}) " +
- $"along={along:0} lateral={lateral:0} remain={remain:0} headingErr={headingErr:0.0} " +
- $"baseTh={baseCrabTh:0.0} bias={biasItem:0.0} dth={dthItem:0.0} " +
- $"slowRatio={slowRatio:0.000} targetV={targetSpeed:0.000} rampT={rampElapsed:0.0} accel={activeAccel:0.000} auto=(vx:{driveSpeed:0.000},fTh:{frontTh:0.0},rTh:{rearTh:0.0}) " +
- $"ideal=({ideal.X:0},{ideal.Y:0},{targetBodyTh:0.0}) scriptEn={self.MultiVehicleScriptEnabled} " +
- $"cnt={fleetCnt}/{conf.MultiVehicleFleetNum}",
- "FleetCrabDbg");
- DLog.Log(
- $"CTRL iter={iter} centerSrc:{centerSource} phi:{phi:0.00} targetBody:{targetBodyTh:0.00} startTheta:{theta:0.00} " +
- $"cth:{cth:0.00} crabAngle:{CrabAngleDeg:0.00} bodyToPathCfg:{BodyToPathAngleDeg:0.00} " +
- $"targetBodyToPath:{targetBodyToPath:0.00} bodyToPathNow:{baseCrabTh:0.00} " +
- $"headingErr(target-current):{headingErr:0.00} reverse(current-target):{headingErrReverse:0.00} yawSign:{yawSplitSign:0} " +
- $"fleetCrabDthFac:{conf.FleetCrabDthLinearFac:0.000} rawDth:{rawDthItem:0.00} dth:{dthItem:0.00} dthLimit:{conf.FleetCrabDthLinearThreshold:0.00} " +
- $"lateral:{lateral:0.0} biasFac:{conf.BiasFac:0.000} biasSign:{biasSign:0} rawBias:{rawBiasItem:0.00} bias:{biasItem:0.00} biasLimit:{conf.BiasThreshold:0.00} " +
- $"baseTh:{baseCrabTh:0.00} rawBase:{rawBaseTh:0.00} rawOut(f:{rawFrontTh:0.00},r:{rawRearTh:0.00}) " +
- $"out(f:{frontTh:0.00},r:{rearTh:0.00}) gcpLimit:{gcpLimit:0.00} revEq:{reverseEquivalent} " +
- $"speedRaw:{speed:0.000} speed:{driveSpeed:0.000} rampT:{rampElapsed:0.0} accel:{activeAccel:0.000} along:{along:0.0} remain:{remain:0.0} ideal=({ideal.X:0.0},{ideal.Y:0.0},{targetBodyTh:0.00})",
- "FleetCrabHeadingDbg");
- }
- yield return true;
- }
-
- if (_stopping)
- stopReason = "stop";
-
- self.MultiVehicleAutoVx = 0;
- self.MultiVehicleAutoFrontTh = holdFrontTh;
- self.MultiVehicleAutoRearTh = holdRearTh;
- self.MultiVehicleAutoCmdTime = DateTime.Now;
- DLog.Log(
- $"STOP_HOLD iter={iter} reason={stopReason} hold=(fTh:{holdFrontTh:0.0},rTh:{holdRearTh:0.0}) cmdSpeed={cmdSpeed:0.000}",
- "FleetCrabDbg");
- var settleEnd = DateTime.Now.AddMilliseconds(Math.Max(100, conf.MultiVehicleSyncInterval * 3));
- while (!_stopping && DateTime.Now < settleEnd)
- {
- self.MultiVehicleScriptEnabled = false;
- self.MultiVehicleScriptMode = 0;
- self.MultiVehicleAutoEnabled = true;
- self.MultiVehicleAutoVx = 0;
- self.MultiVehicleAutoFrontTh = holdFrontTh;
- self.MultiVehicleAutoRearTh = holdRearTh;
- self.MultiVehicleAutoCmdTime = DateTime.Now;
- yield return true;
- }
-
- Cleanup();
- Hedingben.ToastText("车队蟹行完成", "FleetCrab");
- DLog.Log($"DONE iter={iter} reason={stopReason}", "FleetCrabDbg");
+ _proc?.Stop();
+ _task?.Stop();
}
}
diff --git a/MultiWheel/MultiWheelC/PilotConfig.cs b/MultiWheel/MultiWheelC/PilotConfig.cs
index 2a82bdd..0887b5b 100644
--- a/MultiWheel/MultiWheelC/PilotConfig.cs
+++ b/MultiWheel/MultiWheelC/PilotConfig.cs
@@ -203,6 +203,31 @@ public class PilotConfig : MultiWheelPilotConfig
[FieldMember(desc = "FleetCrab startup wheel alignment tolerance(deg)")]
public float FleetCrabStartWheelAlignDeg = 2f;
+ // ===== Fleet linked Bezier curve walk =====
+ [FieldMember(desc = "FleetCurve MovementTest control points relative to fleet start; format x,y;x,y;...")]
+ public string FleetCurveControlPoints = "0,0;1000,800;2000,800;3000,0";
+
+ [FieldMember(desc = "FleetCurve Bezier resolution")]
+ public int FleetCurveBezierResolution = 100;
+
+ [FieldMember(desc = "FleetCurve speed(m/s)")]
+ public float FleetCurveSpeed = 0.2f;
+
+ [FieldMember(desc = "FleetCurve car direction bias(deg); body target follows tangent+bias")]
+ public float FleetCurveCarDirectionBias = 0f;
+
+ [FieldMember(desc = "FleetCurve slow distance(mm)")]
+ public float FleetCurveSlowDistance = 2000f;
+
+ [FieldMember(desc = "FleetCurve finish distance(mm)")]
+ public float FleetCurveFinishDistance = 20f;
+
+ [FieldMember(desc = "FleetCurve finish speed(m/s)")]
+ public float FleetCurveFinishSpeed = 0.02f;
+
+ [FieldMember(desc = "FleetCurve slowing curve exponent")]
+ public float FleetCurveSlowingPow = 0.8f;
+
// ===== 2腿检测(单线雷达识别两腿托盘 / 轮胎)=====
[FieldMember(desc = "2腿检测:雷达名(逗号分隔可多个)")]
public string TwoLegLidarName = "rear_left_lidar_1,rear_right_lidar_1";