添加直线-半圆组合轨迹测试并调优Stanley参数与夹臂限位类型

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 17:54:09 +08:00
co-authored by Cursor
parent 19b1e49189
commit f8881bc243
5 changed files with 730 additions and 7 deletions
@@ -47,7 +47,7 @@ namespace MultiWheelC
/// <summary>
/// 获取或设置参考速度减速度,单位为m/s²。
/// </summary>
public double DecelerationMetersPerSecondSquared = 0.20;
public double DecelerationMetersPerSecondSquared = 0.12;
/// <summary>
/// 获取或设置离散轨迹点间距,单位为m。
@@ -294,4 +294,314 @@ namespace MultiWheelC
MillimetersPerMeter));
}
}
/// <summary>
/// 从当前Detour位姿开始执行“3m直线—左半圆—3m直线”新版控制器跟踪实验。
/// </summary>
[MovementTest(name = "新版控制器:直线-左半圆-直线轨迹跟踪")]
public sealed class NewControllerStraightSemicircleStraightTest
: MovementTest
{
private const float MillimetersPerMeter = 1000f;
private readonly Painter _painter =
UI.GetPainter(
"NewControllerStraightSemicircleStraight");
private DriveTask _task;
private TrackingExperimentRecorder _recorder;
private DetourVehicleStateProvider _stateProvider;
/// <summary>
/// 获取或设置本次测试编号,用于区分重复实验CSV。
/// </summary>
public int TrialNumber = 1;
/// <summary>
/// 获取或设置半圆前后两段直线的长度,单位为m。
/// </summary>
public double StraightLengthMeters = 3.0;
/// <summary>
/// 获取或设置左转半圆的转弯半径,单位为m。
/// </summary>
public double TurnRadiusMeters = 2.0;
/// <summary>
/// 获取或设置直线与等曲率转弯之间的曲率过渡长度,单位为m。
/// </summary>
public double CurvatureTransitionLengthMeters = 0.60;
/// <summary>
/// 获取或设置两段直线的最大参考速度,单位为m/s。
/// </summary>
public double StraightMaximumSpeedMetersPerSecond = 0.30;
/// <summary>
/// 获取或设置半圆段的最大参考速度,单位为m/s。
/// </summary>
public double SemicircleMaximumSpeedMetersPerSecond = 0.25;
/// <summary>
/// 获取或设置参考速度加速度,单位为m/s²。
/// </summary>
public double AccelerationMetersPerSecondSquared = 0.20;
/// <summary>
/// 获取或设置参考速度减速度,单位为m/s²。
/// </summary>
public double DecelerationMetersPerSecondSquared = 0.12;
/// <summary>
/// 获取或设置离散轨迹点间距,单位为m。
/// </summary>
public double PointSpacingMeters = 0.02;
/// <summary>
/// 读取当前位姿、绘制组合轨迹并启动新版轨迹跟踪动作。
/// </summary>
public override void Test()
{
if (_task != null)
{
Console.WriteLine(
"新版直线-左半圆-直线测试已经在运行,请先停止当前测试。");
return;
}
if (!MovementTestPreparation.AreWheelsForward())
{
return;
}
var chassis =
PilotDefinition.Chassis as MultiWheelChassis;
if (chassis == null)
{
Console.WriteLine(
"当前底盘不是MultiWheelChassis,无法执行新版轨迹测试。");
return;
}
_stateProvider =
new DetourVehicleStateProvider();
if (!_stateProvider.TryGetState(
out var initialState))
{
Console.WriteLine(
"无法读取有效Detour起点位姿:" +
_stateProvider.LastFailureReason);
_stateProvider = null;
return;
}
var trajectory =
TestTrajectoryFactory
.CreateStraightLeftSemicircleStraight(
initialState.PoseInWorld,
StraightLengthMeters,
TurnRadiusMeters,
CurvatureTransitionLengthMeters,
StraightMaximumSpeedMetersPerSecond,
SemicircleMaximumSpeedMetersPerSecond,
AccelerationMetersPerSecondSquared,
DecelerationMetersPerSecondSquared,
PointSpacingMeters);
DrawTrajectory(trajectory);
var referenceStart = ToMillimeterVector(
trajectory.StartPoint.PoseInWorld);
var referenceEnd = ToMillimeterVector(
trajectory.EndPoint.PoseInWorld);
var recorder =
new TrackingExperimentRecorder(
controllerName: "NewStanleyPid",
trajectoryName:
"ProfiledStraightSmoothLeftTurnStraight",
trialNumber: TrialNumber,
referenceStart: referenceStart,
referenceEnd: referenceEnd,
referenceSpeed:
(float)StraightMaximumSpeedMetersPerSecond,
sampleIntervalMs: 50,
referenceAccelerationMetersPerSecondSquared:
(float)AccelerationMetersPerSecondSquared,
referenceDecelerationMetersPerSecondSquared:
(float)DecelerationMetersPerSecondSquared);
_recorder = recorder;
var controlPointRadiusMeters =
chassis.ControlPointRadius /
MillimetersPerMeter;
var movement =
new TrajectoryTrackingMovement
{
Trajectory = trajectory,
StateProvider = _stateProvider,
MaximumCommandSpeedMetersPerSecond =
StraightMaximumSpeedMetersPerSecond,
CycleObserver = controller =>
RecordControlCycle(
recorder,
controller,
controlPointRadiusMeters)
};
recorder.Start();
try
{
_task = new DriveTask(movement.Get());
_task.Wait();
// 保留少量停车后原始Detour数据,并生成一帧处理后的静止状态。
Thread.Sleep(400);
recorder.UpdateCommand(0f, 0f);
if (_stateProvider.TryGetState(
out var stoppedState))
{
recorder.UpdateProcessedState(
stoppedState);
}
}
finally
{
_task?.Stop();
recorder.UpdateCommand(0f, 0f);
recorder.StopAndSave();
_painter.Clear();
_task = null;
_recorder = null;
_stateProvider = null;
}
}
/// <summary>
/// 停止组合轨迹测试、保存已有数据并清除Clumsy轨迹可视化。
/// </summary>
public override void TestStop()
{
_task?.Stop();
_recorder?.UpdateCommand(0f, 0f);
_recorder?.StopAndSave();
_painter.Clear();
_task = null;
_recorder = null;
_stateProvider = null;
}
/// <summary>
/// 将组合轨迹的离散点和相邻线段转换为Clumsy毫米坐标后绘制。
/// </summary>
private void DrawTrajectory(
Trajectory.Trajectory2D trajectory)
{
_painter.Clear();
for (var index = 0;
index < trajectory.Count;
index++)
{
var point = ToMillimeterVector(
trajectory[index].PoseInWorld);
_painter.DrawDot(
Color.Cyan,
point.X,
point.Y,
3f);
if (index == 0)
{
continue;
}
var previousPoint = ToMillimeterVector(
trajectory[index - 1].PoseInWorld);
_painter.DrawLine(
Color.DeepSkyBlue,
previousPoint.X,
previousPoint.Y,
point.X,
point.Y,
width: 2);
}
var start = ToMillimeterVector(
trajectory.StartPoint.PoseInWorld);
var end = ToMillimeterVector(
trajectory.EndPoint.PoseInWorld);
_painter.DrawDot(
Color.LimeGreen,
start.X,
start.Y,
8f);
_painter.DrawDot(
Color.OrangeRed,
end.X,
end.Y,
8f);
}
/// <summary>
/// 将组合轨迹控制周期的状态、参考量和最终GCP命令同步给实验记录器。
/// </summary>
private static void RecordControlCycle(
TrackingExperimentRecorder recorder,
ParkingGeometricController controller,
double controlPointRadiusMeters)
{
if (controller.LastVehicleState.HasValue)
{
recorder.UpdateProcessedState(
controller.LastVehicleState.Value);
}
if (!controller.LastCommand.HasValue)
{
return;
}
if (controller.LastProjection.HasValue &&
controller.LastReferenceSpeedMetersPerSecond.HasValue)
{
var projection =
controller.LastProjection.Value;
recorder.UpdateControlReference(
projection.ArcLengthMeters,
controller.LastReferenceSpeedMetersPerSecond.Value,
projection.LateralErrorMeters,
projection.HeadingErrorRadians,
projection.DistanceToTrajectoryMeters,
projection.RemainingDistanceMeters);
}
var command = controller.LastCommand.Value;
var curvaturePerMeter = Math.Tan(
command.FrontAngleRadians) /
controlPointRadiusMeters;
var angularSpeedRadiansPerSecond =
command.SpeedMetersPerSecond *
curvaturePerMeter;
recorder.UpdateCommand(
(float)command.SpeedMetersPerSecond,
(float)angularSpeedRadiansPerSecond);
}
/// <summary>
/// 将Shared世界坐标系米制位姿转换为Clumsy绘图和记录器使用的毫米坐标。
/// </summary>
private static Vector2 ToMillimeterVector(
Pose2D poseInWorld)
{
return new Vector2(
(float)(
poseInWorld.XMeters *
MillimetersPerMeter),
(float)(
poseInWorld.YMeters *
MillimetersPerMeter));
}
}
}
@@ -92,6 +92,399 @@ namespace MultiWheelC
return new Trajectory2D(points);
}
/// <summary>
/// 从当前位姿生成“3m直线、平滑进入半径2m左转、平滑退出、3m直线”的180°转弯轨迹。
/// </summary>
public static Trajectory2D CreateStraightLeftSemicircleStraight(
Pose2D startPoseInWorld,
double straightLengthMeters = 3.0,
double turnRadiusMeters = 2.0,
double curvatureTransitionLengthMeters = 0.60,
double straightMaximumSpeedMetersPerSecond = 0.30,
double semicircleMaximumSpeedMetersPerSecond = 0.25,
double accelerationMetersPerSecondSquared = 0.20,
double decelerationMetersPerSecondSquared = 0.12,
double pointSpacingMeters = 0.02)
{
EnsureFinitePose(
startPoseInWorld,
nameof(startPoseInWorld));
EnsureFinitePositive(
straightLengthMeters,
nameof(straightLengthMeters));
EnsureFinitePositive(
turnRadiusMeters,
nameof(turnRadiusMeters));
EnsureFinitePositive(
curvatureTransitionLengthMeters,
nameof(curvatureTransitionLengthMeters));
EnsureFinitePositive(
straightMaximumSpeedMetersPerSecond,
nameof(straightMaximumSpeedMetersPerSecond));
EnsureFinitePositive(
semicircleMaximumSpeedMetersPerSecond,
nameof(semicircleMaximumSpeedMetersPerSecond));
EnsureFinitePositive(
accelerationMetersPerSecondSquared,
nameof(accelerationMetersPerSecondSquared));
EnsureFinitePositive(
decelerationMetersPerSecondSquared,
nameof(decelerationMetersPerSecondSquared));
EnsureFinitePositive(
pointSpacingMeters,
nameof(pointSpacingMeters));
var originalSemicircleLengthMeters =
Math.PI * turnRadiusMeters;
var constantCurvatureLengthMeters =
originalSemicircleLengthMeters -
curvatureTransitionLengthMeters;
if (constantCurvatureLengthMeters <= 0.0)
{
throw new ArgumentOutOfRangeException(
nameof(curvatureTransitionLengthMeters),
"曲率过渡段长度必须小于半径对应的原始半圆弧长。");
}
// 两段平滑过渡的平均曲率均为最大曲率的一半;
// 将等曲率段缩短一个过渡长度后,总曲率积分仍严格等于π。
var turnLengthMeters =
2.0 * curvatureTransitionLengthMeters +
constantCurvatureLengthMeters;
var turnStartArcLengthMeters =
straightLengthMeters;
var turnEndArcLengthMeters =
straightLengthMeters +
turnLengthMeters;
var maximumCurvaturePerMeter =
1.0 / turnRadiusMeters;
var sampleArcLengths =
BuildCompositeArcLengthSamples(
straightLengthMeters,
curvatureTransitionLengthMeters,
constantCurvatureLengthMeters,
pointSpacingMeters);
var speedLimits = new double[
sampleArcLengths.Count];
var referenceSpeeds = new double[
sampleArcLengths.Count];
for (var index = 0;
index < sampleArcLengths.Count;
index++)
{
var arcLengthMeters =
sampleArcLengths[index];
// 整个转弯及两侧曲率过渡段采用转弯限速。
speedLimits[index] =
arcLengthMeters >=
turnStartArcLengthMeters &&
arcLengthMeters <=
turnEndArcLengthMeters
? semicircleMaximumSpeedMetersPerSecond
: straightMaximumSpeedMetersPerSecond;
}
ApplyAccelerationAndBrakingLimits(
sampleArcLengths,
speedLimits,
referenceSpeeds,
accelerationMetersPerSecondSquared,
decelerationMetersPerSecondSquared);
var points = new List<TrajectoryPoint>(
sampleArcLengths.Count);
var startCos = Math.Cos(
startPoseInWorld.YawRadians);
var startSin = Math.Sin(
startPoseInWorld.YawRadians);
var localX = 0.0;
var localY = 0.0;
var localYawRadians = 0.0;
var previousArcLengthMeters = 0.0;
for (var index = 0;
index < sampleArcLengths.Count;
index++)
{
var arcLengthMeters =
sampleArcLengths[index];
if (index > 0)
{
var segmentLengthMeters =
arcLengthMeters -
previousArcLengthMeters;
var segmentMiddleArcLengthMeters =
(arcLengthMeters +
previousArcLengthMeters) /
2.0;
var segmentCurvaturePerMeter =
CalculateSmoothTurnCurvature(
segmentMiddleArcLengthMeters -
turnStartArcLengthMeters,
curvatureTransitionLengthMeters,
constantCurvatureLengthMeters,
maximumCurvaturePerMeter);
var segmentYawChangeRadians =
segmentCurvaturePerMeter *
segmentLengthMeters;
if (Math.Abs(segmentCurvaturePerMeter) <=
1e-12)
{
localX +=
Math.Cos(localYawRadians) *
segmentLengthMeters;
localY +=
Math.Sin(localYawRadians) *
segmentLengthMeters;
}
else
{
var nextYawRadians =
localYawRadians +
segmentYawChangeRadians;
localX +=
(Math.Sin(nextYawRadians) -
Math.Sin(localYawRadians)) /
segmentCurvaturePerMeter;
localY +=
(Math.Cos(localYawRadians) -
Math.Cos(nextYawRadians)) /
segmentCurvaturePerMeter;
}
localYawRadians +=
segmentYawChangeRadians;
}
var curvaturePerMeter =
CalculateSmoothTurnCurvature(
arcLengthMeters -
turnStartArcLengthMeters,
curvatureTransitionLengthMeters,
constantCurvatureLengthMeters,
maximumCurvaturePerMeter);
var worldX =
startPoseInWorld.XMeters +
startCos * localX -
startSin * localY;
var worldY =
startPoseInWorld.YMeters +
startSin * localX +
startCos * localY;
var worldYawRadians =
AngleMath.NormalizeRadians(
startPoseInWorld.YawRadians +
localYawRadians);
points.Add(
new TrajectoryPoint(
arcLengthMeters,
new Pose2D(
worldX,
worldY,
worldYawRadians),
curvaturePerMeter,
referenceSpeeds[index]));
previousArcLengthMeters =
arcLengthMeters;
}
return new Trajectory2D(points);
}
/// <summary>
/// 分别采样直线、入弯过渡、等曲率段和出弯过渡,保证所有边界均为精确轨迹点。
/// </summary>
private static List<double> BuildCompositeArcLengthSamples(
double straightLengthMeters,
double curvatureTransitionLengthMeters,
double constantCurvatureLengthMeters,
double pointSpacingMeters)
{
var samples = new List<double> { 0.0 };
var accumulatedArcLengthMeters = 0.0;
AppendSectionArcLengthSamples(
samples,
ref accumulatedArcLengthMeters,
straightLengthMeters,
pointSpacingMeters);
AppendSectionArcLengthSamples(
samples,
ref accumulatedArcLengthMeters,
curvatureTransitionLengthMeters,
pointSpacingMeters);
AppendSectionArcLengthSamples(
samples,
ref accumulatedArcLengthMeters,
constantCurvatureLengthMeters,
pointSpacingMeters);
AppendSectionArcLengthSamples(
samples,
ref accumulatedArcLengthMeters,
curvatureTransitionLengthMeters,
pointSpacingMeters);
AppendSectionArcLengthSamples(
samples,
ref accumulatedArcLengthMeters,
straightLengthMeters,
pointSpacingMeters);
return samples;
}
/// <summary>
/// 计算180°左转中连续变化的参考曲率,过渡段两端的曲率变化率均为零。
/// </summary>
private static double CalculateSmoothTurnCurvature(
double distanceInTurnMeters,
double transitionLengthMeters,
double constantCurvatureLengthMeters,
double maximumCurvaturePerMeter)
{
var totalTurnLengthMeters =
2.0 * transitionLengthMeters +
constantCurvatureLengthMeters;
if (distanceInTurnMeters <= 0.0 ||
distanceInTurnMeters >= totalTurnLengthMeters)
{
return 0.0;
}
if (distanceInTurnMeters < transitionLengthMeters)
{
return maximumCurvaturePerMeter *
SmoothStep01(
distanceInTurnMeters /
transitionLengthMeters);
}
var exitTransitionStartMeters =
transitionLengthMeters +
constantCurvatureLengthMeters;
if (distanceInTurnMeters <=
exitTransitionStartMeters)
{
return maximumCurvaturePerMeter;
}
var exitRatio =
(distanceInTurnMeters -
exitTransitionStartMeters) /
transitionLengthMeters;
return maximumCurvaturePerMeter *
(1.0 - SmoothStep01(exitRatio));
}
/// <summary>
/// 将零到一的比例转换为两端一阶导数均为零的三次平滑比例。
/// </summary>
private static double SmoothStep01(double ratio)
{
var limitedRatio = Math.Max(
0.0,
Math.Min(1.0, ratio));
return limitedRatio *
limitedRatio *
(3.0 - 2.0 * limitedRatio);
}
/// <summary>
/// 将一段指定长度的轨迹追加为均匀弧长采样,并使最后一个点严格落在该段终点。
/// </summary>
private static void AppendSectionArcLengthSamples(
ICollection<double> samples,
ref double accumulatedArcLengthMeters,
double sectionLengthMeters,
double pointSpacingMeters)
{
var sectionStartArcLengthMeters =
accumulatedArcLengthMeters;
var segmentCount = (int)Math.Ceiling(
sectionLengthMeters /
pointSpacingMeters);
for (var index = 1;
index <= segmentCount;
index++)
{
samples.Add(
sectionStartArcLengthMeters +
sectionLengthMeters *
index /
segmentCount);
}
accumulatedArcLengthMeters =
sectionStartArcLengthMeters +
sectionLengthMeters;
}
/// <summary>
/// 对逐点速度上限执行前向加速约束和反向制动约束,生成连续可执行的空间速度曲线。
/// </summary>
private static void ApplyAccelerationAndBrakingLimits(
IReadOnlyList<double> arcLengthsMeters,
IReadOnlyList<double> speedLimitsMetersPerSecond,
double[] referenceSpeedsMetersPerSecond,
double accelerationMetersPerSecondSquared,
double decelerationMetersPerSecondSquared)
{
referenceSpeedsMetersPerSecond[0] = 0.0;
for (var index = 1;
index < arcLengthsMeters.Count;
index++)
{
var segmentLengthMeters =
arcLengthsMeters[index] -
arcLengthsMeters[index - 1];
var accelerationLimitedSpeed = Math.Sqrt(
referenceSpeedsMetersPerSecond[index - 1] *
referenceSpeedsMetersPerSecond[index - 1] +
2.0 *
accelerationMetersPerSecondSquared *
segmentLengthMeters);
referenceSpeedsMetersPerSecond[index] =
Math.Min(
speedLimitsMetersPerSecond[index],
accelerationLimitedSpeed);
}
var finalIndex =
referenceSpeedsMetersPerSecond.Length - 1;
referenceSpeedsMetersPerSecond[finalIndex] = 0.0;
for (var index = finalIndex - 1;
index >= 0;
index--)
{
var segmentLengthMeters =
arcLengthsMeters[index + 1] -
arcLengthsMeters[index];
var brakingLimitedSpeed = Math.Sqrt(
referenceSpeedsMetersPerSecond[index + 1] *
referenceSpeedsMetersPerSecond[index + 1] +
2.0 *
decelerationMetersPerSecondSquared *
segmentLengthMeters);
referenceSpeedsMetersPerSecond[index] =
Math.Min(
referenceSpeedsMetersPerSecond[index],
brakingLimitedSpeed);
}
}
/// <summary>
/// 根据起步、巡航和制动能力计算指定弧长位置允许的参考速度。
/// </summary>
@@ -38,7 +38,7 @@ namespace MultiWheelC
/// <summary>
/// Stanley横向误差增益,单位为1/s。
/// </summary>
public double StanleyCrossTrackGainPerSecond = 1.0;
public double StanleyCrossTrackGainPerSecond = 0.4;
/// <summary>
/// Stanley航向误差增益。
@@ -48,7 +48,7 @@ namespace MultiWheelC
/// <summary>
/// Stanley低速分母保护速度,单位为m/s。
/// </summary>
public double StanleyMinimumSpeedMetersPerSecond = 0.05;
public double StanleyMinimumSpeedMetersPerSecond = 0.15;
/// <summary>
/// 获取或设置Stanley是否优先使用Detour估算的实际速度。
+4 -4
View File
@@ -33,10 +33,10 @@ public class PilotDefinition : MultiWheelPilotDefinition<PilotConfig, PilotDefin
[AsLowerIO(desc = "右后左轮实际位置")] public float RRLActualPos;
[AsLowerIO(desc = "右后右轮实际位置")] public float RRRActualPos;
[AsLowerIO(desc = "左夹臂低限位")] public float LeftArmLowerPos;
[AsLowerIO(desc = "左夹臂高限位")] public float LeftArmUpperPos;
[AsLowerIO(desc = "右夹臂低限位")] public float RightArmLowerPos;
[AsLowerIO(desc = "右夹臂高限位")] public float RightArmUpperPos;
[AsLowerIO(desc = "左夹臂低限位")] public int LeftArmLowerPos;
[AsLowerIO(desc = "左夹臂高限位")] public int LeftArmUpperPos;
[AsLowerIO(desc = "右夹臂低限位")] public int RightArmLowerPos;
[AsLowerIO(desc = "右夹臂高限位")] public int RightArmUpperPos;
[AsUpperIO(desc = "从C往驱动器下使能")] public bool DisableFromC = false;
[AsUpperIO(desc = "从C上复位")] public bool ResetFromC = false;
+20
View File
@@ -35,3 +35,23 @@ StanleyLateralController.cs
PidLongitudinalController.cs
GcpCommandExecutor.cs
ParkingGeometricController.cs
轨迹投影没有进度连续性
[TrajectoryProjector.cs (line 33)](/D:/Users/Desktop/入职培训/停车机器人/MyParking/MultiWheelC/Trajectory/TrajectoryProjector.cs:33) 每个控制周期都会遍历整条轨迹并选择全局最近线段。
当前4 m直线、圆弧、普通 S 曲线一般没有问题;但以后遇到自交、回环、相邻平行路径或 Detour 跳变时,投影可能突然跳到另一段轨迹。
建议在开始复杂曲线测试前增加:
上一次投影线段索引
有限前向搜索窗口
少量允许回退范围
现阶段测试直线不需要马上改。
通用轨迹动作没有起点航向保护
当前直线测试以实时 Detour 位姿作为起点,因此起点位置和航向天然匹配,没有问题。
但 [TrajectoryTrackingMovement.cs (line 119)](/D:/Users/Desktop/入职培训/停车机器人/MyParking/MultiWheelC/Movements/TrajectoryTrackingMovement.cs:119) 本身允许传入任意世界坐标轨迹。如果将来传入的轨迹起点航向和车辆实际航向差别很大,车辆会直接边走边纠正。
后续至少应选择一种:
规划层保证起点位姿和实际车辆一致;
控制器检查初始航向误差,超限则拒绝启动;
增加起步航向对齐状态。
第一阶段建议采用第二种,简单、安全。