实现蟹行轨迹跟踪测试并优化底盘XYTh与原地旋转舵角控制
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-1
@@ -89,4 +89,5 @@ _ReSharper*/
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
*.csv
|
||||
*.csv
|
||||
*.png
|
||||
@@ -0,0 +1,667 @@
|
||||
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 ReferencePathKind PathKind;
|
||||
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 =
|
||||
30.0 * Math.PI / 180.0;
|
||||
public float WheelAlignmentToleranceDegrees = 2f;
|
||||
public float WheelAlignmentStableSeconds = 0.3f;
|
||||
public float WheelAlignmentTimeoutSeconds = 10f;
|
||||
public float TrackingTimeoutSeconds = 60f;
|
||||
public Action<float, float, float> CommandObserver;
|
||||
|
||||
// 运动坐标系相对车体坐标系的朝向:普通模式为0,蟹行为π/2。
|
||||
public double MotionFrameYawInBodyRadians = Math.PI / 2.0;
|
||||
private double _lastSCurveProgress;
|
||||
|
||||
public override IEnumerable<bool> Get()
|
||||
{
|
||||
ValidateParameters();
|
||||
|
||||
var chassis =
|
||||
PilotDefinition.Chassis as MultiWheelChassis;
|
||||
if (chassis == null)
|
||||
throw new InvalidOperationException(
|
||||
"当前底盘不是MultiWheelChassis,无法执行运动坐标系轨迹测试。");
|
||||
|
||||
var adapter = new MultiWheelChassisAdapter(
|
||||
chassis,
|
||||
PilotDefinition.Self.CarNum);
|
||||
|
||||
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,
|
||||
WheelAlignmentToleranceDegrees *
|
||||
Math.PI / 180.0);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 =
|
||||
location.th * Math.PI / 180.0;
|
||||
|
||||
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 =
|
||||
FrameTransform2D
|
||||
.ShortestAngleDifference(
|
||||
desiredBodyYaw,
|
||||
currentBodyYaw);
|
||||
var omega =
|
||||
speed * referenceCurvature +
|
||||
HeadingGainPerSecond * headingError;
|
||||
omega = Limit(
|
||||
omega,
|
||||
MaximumAngularSpeedRadiansPerSecond);
|
||||
|
||||
// 运动坐标系相对车体系旋转+90°:
|
||||
// 运动系正向速度会转换成车体系+Y速度。
|
||||
var bodyTwist =
|
||||
FrameTransform2D
|
||||
.TransformTwistAtSamePoint(
|
||||
new Pose2D(
|
||||
0.0,
|
||||
0.0,
|
||||
MotionFrameYawInBodyRadians),
|
||||
new Twist2D(
|
||||
vxInMotion,
|
||||
vyInMotion,
|
||||
omega));
|
||||
|
||||
var now = DateTime.Now;
|
||||
var interval = now - lastCommandTime;
|
||||
lastCommandTime = now;
|
||||
var command = new ChassisCommand(
|
||||
PilotDefinition.Self.CarNum,
|
||||
bodyTwist);
|
||||
|
||||
if (!adapter.Send(command, interval))
|
||||
throw new InvalidOperationException(
|
||||
"蟹行轨迹底盘解算失败:" +
|
||||
adapter.LastFailureReason);
|
||||
|
||||
CommandObserver?.Invoke(
|
||||
(float)bodyTwist.VxMetersPerSecond,
|
||||
(float)bodyTwist.VyMetersPerSecond,
|
||||
(float)bodyTwist
|
||||
.OmegaRadiansPerSecond);
|
||||
|
||||
yield return true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
adapter.StopImmediately();
|
||||
CommandObserver?.Invoke(0f, 0f, 0f);
|
||||
}
|
||||
|
||||
yield return false;
|
||||
}
|
||||
|
||||
// 计算当前点在直线或圆弧上的参考点、切线和剩余距离。
|
||||
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 =
|
||||
FrameTransform2D.NormalizeAngle(
|
||||
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))
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+738
-10
@@ -7,6 +7,7 @@ using MDCSToolBox.Commons.Controllers;
|
||||
using MDCSToolBox.Clumsy.Tracks;
|
||||
using MyParking.Shared;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
|
||||
@@ -102,7 +103,7 @@ namespace MultiWheelC
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试连续前进4m")]
|
||||
[MovementTest(name = "旧版SendMotion:连续前进4m")]
|
||||
public class TestForward4m : MovementTest
|
||||
{
|
||||
public float DistanceMillimeters = 4000f; // 测试距离,单位mm。
|
||||
@@ -138,8 +139,8 @@ namespace MultiWheelC
|
||||
source.Y + DistanceMillimeters * (float)Math.Sin(headingRadians));
|
||||
_recorder =
|
||||
new TrackingExperimentRecorder(
|
||||
controllerName: "Stanley",
|
||||
trajectoryName: "Straight4m",
|
||||
controllerName: "LegacyGeometricController",
|
||||
trajectoryName: "LegacyStraight4m",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: destination,
|
||||
@@ -225,8 +226,10 @@ namespace MultiWheelC
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: rotationCenter,
|
||||
referenceEnd: rotationCenter,
|
||||
referenceSpeed:
|
||||
MaxAngularSpeedDegreesPerSecond);
|
||||
referenceSpeed: 0f,
|
||||
referenceAngularSpeed:
|
||||
MaxAngularSpeedDegreesPerSecond *
|
||||
(float)Math.PI / 180f);
|
||||
_recorder.Start();
|
||||
|
||||
try
|
||||
@@ -258,7 +261,8 @@ namespace MultiWheelC
|
||||
commandAngularSpeed =>
|
||||
_recorder?.UpdateCommand(
|
||||
0f,
|
||||
commandAngularSpeed)
|
||||
commandAngularSpeed *
|
||||
(float)Math.PI / 180f)
|
||||
}.Get());
|
||||
|
||||
_task.Wait();
|
||||
@@ -293,7 +297,7 @@ namespace MultiWheelC
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试左转90°圆弧")]
|
||||
[MovementTest(name = "旧版SendMotion:左转90°圆弧")]
|
||||
public class TestArcMovement : MovementTest
|
||||
{
|
||||
public float RadiusMillimeters = 2000f; // 左转圆的半径,单位mm。
|
||||
@@ -370,6 +374,13 @@ namespace MultiWheelC
|
||||
CarDirectionBias = 0f
|
||||
};
|
||||
|
||||
// 左转90°后,圆心到终点的径向方向等于起始车头方向。
|
||||
var destination = center + new Vector2(
|
||||
RadiusMillimeters *
|
||||
(float)Math.Cos(headingRadians),
|
||||
RadiusMillimeters *
|
||||
(float)Math.Sin(headingRadians));
|
||||
|
||||
if (!controller.AddTrack(arc, "LeftArc90Degrees"))
|
||||
{
|
||||
Console.WriteLine(
|
||||
@@ -378,12 +389,12 @@ namespace MultiWheelC
|
||||
}
|
||||
|
||||
_recorder = new TrackingExperimentRecorder(
|
||||
controllerName: "GeometricController",
|
||||
controllerName: "LegacyGeometricController",
|
||||
trajectoryName:
|
||||
$"LeftArc90_R{RadiusMillimeters:0}mm",
|
||||
$"LegacyLeftArc90_R{RadiusMillimeters:0}mm",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: source,
|
||||
referenceEnd: destination,
|
||||
referenceSpeed: CruiseSpeed);
|
||||
_recorder.Start();
|
||||
|
||||
@@ -414,6 +425,723 @@ namespace MultiWheelC
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试蟹行前进4m")]
|
||||
public class TestCrabForward4m : MovementTest
|
||||
{
|
||||
public float DistanceMillimeters = 4000f;
|
||||
public float CruiseSpeed = 0.2f;
|
||||
public int TrialNumber = 1;
|
||||
|
||||
private DriveTask _task;
|
||||
private TrackingExperimentRecorder _recorder;
|
||||
|
||||
// 将车体左侧作为运动前向,沿直线蟹行4m并记录Detour实验数据。
|
||||
public override void Test()
|
||||
{
|
||||
if (!TryReadStartPose(
|
||||
out var source,
|
||||
out var bodyYawRadians))
|
||||
return;
|
||||
|
||||
var motionYaw =
|
||||
bodyYawRadians + Math.PI / 2.0;
|
||||
var destination = new Vector2(
|
||||
source.X +
|
||||
DistanceMillimeters *
|
||||
(float)Math.Cos(motionYaw),
|
||||
source.Y +
|
||||
DistanceMillimeters *
|
||||
(float)Math.Sin(motionYaw));
|
||||
|
||||
var tracker = new CrabMotionFrameTracker
|
||||
{
|
||||
PathKind =
|
||||
CrabMotionFrameTracker
|
||||
.ReferencePathKind.Straight,
|
||||
StartPosition = source,
|
||||
InitialBodyYawRadians =
|
||||
bodyYawRadians,
|
||||
LengthMillimeters =
|
||||
DistanceMillimeters,
|
||||
CruiseSpeed = CruiseSpeed
|
||||
};
|
||||
|
||||
_recorder = new TrackingExperimentRecorder(
|
||||
controllerName:
|
||||
"CrabMotionFrameTracker",
|
||||
trajectoryName:
|
||||
"CrabStraight4m",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: destination,
|
||||
referenceSpeed: CruiseSpeed,
|
||||
referenceMotionFrameYawDegrees: 90f);
|
||||
tracker.CommandObserver =
|
||||
(vx, vy, omega) =>
|
||||
_recorder?.UpdateBodyCommand(
|
||||
vx,
|
||||
vy,
|
||||
omega);
|
||||
_recorder.Start();
|
||||
|
||||
try
|
||||
{
|
||||
_task = new DriveTask(tracker.Get());
|
||||
_task.Wait();
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateBodyCommand(
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
_recorder?.StopAndSave();
|
||||
_task = null;
|
||||
_recorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateBodyCommand(
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
_recorder?.StopAndSave();
|
||||
}
|
||||
|
||||
// 读取并校验测试开始时的Detour世界位姿。
|
||||
private static bool TryReadStartPose(
|
||||
out Vector2 source,
|
||||
out double bodyYawRadians)
|
||||
{
|
||||
var location =
|
||||
DetourInterface.getCartLocation();
|
||||
if (double.IsNaN(location.x) ||
|
||||
double.IsInfinity(location.x) ||
|
||||
double.IsNaN(location.y) ||
|
||||
double.IsInfinity(location.y) ||
|
||||
double.IsNaN(location.th) ||
|
||||
double.IsInfinity(location.th))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"Detour当前位姿无效,取消蟹行直线测试。");
|
||||
source = Vector2.Zero;
|
||||
bodyYawRadians = 0.0;
|
||||
return false;
|
||||
}
|
||||
|
||||
source = new Vector2(
|
||||
(float)location.x,
|
||||
(float)location.y);
|
||||
bodyYawRadians =
|
||||
location.th * Math.PI / 180.0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试蟹行左转90°圆弧")]
|
||||
public class TestCrabLeftArc90 : MovementTest
|
||||
{
|
||||
public float RadiusMillimeters = 2000f;
|
||||
public float CruiseSpeed = 0.2f;
|
||||
public int TrialNumber = 1;
|
||||
|
||||
private DriveTask _task;
|
||||
private TrackingExperimentRecorder _recorder;
|
||||
|
||||
// 将车体左侧作为运动前向,沿半径2m的左转圆弧运动90°。
|
||||
public override void Test()
|
||||
{
|
||||
var location =
|
||||
DetourInterface.getCartLocation();
|
||||
if (double.IsNaN(location.x) ||
|
||||
double.IsInfinity(location.x) ||
|
||||
double.IsNaN(location.y) ||
|
||||
double.IsInfinity(location.y) ||
|
||||
double.IsNaN(location.th) ||
|
||||
double.IsInfinity(location.th))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"Detour当前位姿无效,取消蟹行圆弧测试。");
|
||||
return;
|
||||
}
|
||||
|
||||
var source = new Vector2(
|
||||
(float)location.x,
|
||||
(float)location.y);
|
||||
var bodyYawRadians =
|
||||
location.th * Math.PI / 180.0;
|
||||
var tracker = new CrabMotionFrameTracker
|
||||
{
|
||||
PathKind =
|
||||
CrabMotionFrameTracker
|
||||
.ReferencePathKind.LeftArc,
|
||||
StartPosition = source,
|
||||
InitialBodyYawRadians =
|
||||
bodyYawRadians,
|
||||
RadiusMillimeters =
|
||||
RadiusMillimeters,
|
||||
ArcSweepRadians = Math.PI / 2.0,
|
||||
CruiseSpeed = CruiseSpeed
|
||||
};
|
||||
var destination =
|
||||
tracker.GetArcDestination();
|
||||
|
||||
_recorder = new TrackingExperimentRecorder(
|
||||
controllerName:
|
||||
"CrabMotionFrameTracker",
|
||||
trajectoryName:
|
||||
$"CrabLeftArc90_R{RadiusMillimeters:0}mm",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: destination,
|
||||
referenceSpeed: CruiseSpeed,
|
||||
referenceMotionFrameYawDegrees: 90f);
|
||||
tracker.CommandObserver =
|
||||
(vx, vy, omega) =>
|
||||
_recorder?.UpdateBodyCommand(
|
||||
vx,
|
||||
vy,
|
||||
omega);
|
||||
_recorder.Start();
|
||||
|
||||
try
|
||||
{
|
||||
_task = new DriveTask(tracker.Get());
|
||||
_task.Wait();
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateBodyCommand(
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
_recorder?.StopAndSave();
|
||||
_task = null;
|
||||
_recorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateBodyCommand(
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
_recorder?.StopAndSave();
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "测试蟹行4m S型曲线")]
|
||||
public class TestCrabSCurve4m : MovementTest
|
||||
{
|
||||
public float LengthMillimeters = 4000f;
|
||||
public float LateralOffsetMillimeters = 400f;
|
||||
public float CruiseSpeed = 0.2f;
|
||||
public int TrialNumber = 1;
|
||||
|
||||
private DriveTask _task;
|
||||
private TrackingExperimentRecorder _recorder;
|
||||
|
||||
// 将车体左侧作为运动前向,跟踪与普通测试参数一致的4m S型曲线。
|
||||
public override void Test()
|
||||
{
|
||||
if (float.IsNaN(LengthMillimeters) ||
|
||||
float.IsInfinity(LengthMillimeters) ||
|
||||
LengthMillimeters <= 0f ||
|
||||
float.IsNaN(
|
||||
LateralOffsetMillimeters) ||
|
||||
float.IsInfinity(
|
||||
LateralOffsetMillimeters) ||
|
||||
LateralOffsetMillimeters <= 0f ||
|
||||
float.IsNaN(CruiseSpeed) ||
|
||||
float.IsInfinity(CruiseSpeed) ||
|
||||
CruiseSpeed <= 0f)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"蟹行S型曲线测试参数无效。");
|
||||
return;
|
||||
}
|
||||
|
||||
var location =
|
||||
DetourInterface.getCartLocation();
|
||||
if (double.IsNaN(location.x) ||
|
||||
double.IsInfinity(location.x) ||
|
||||
double.IsNaN(location.y) ||
|
||||
double.IsInfinity(location.y) ||
|
||||
double.IsNaN(location.th) ||
|
||||
double.IsInfinity(location.th))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"Detour当前位姿无效,取消蟹行S型曲线测试。");
|
||||
return;
|
||||
}
|
||||
|
||||
var source = new Vector2(
|
||||
(float)location.x,
|
||||
(float)location.y);
|
||||
var bodyYawRadians =
|
||||
location.th * Math.PI / 180.0;
|
||||
var initialMotionYaw =
|
||||
bodyYawRadians + Math.PI / 2.0;
|
||||
var destination = new Vector2(
|
||||
source.X +
|
||||
LengthMillimeters *
|
||||
(float)Math.Cos(
|
||||
initialMotionYaw),
|
||||
source.Y +
|
||||
LengthMillimeters *
|
||||
(float)Math.Sin(
|
||||
initialMotionYaw));
|
||||
var tracker = new CrabMotionFrameTracker
|
||||
{
|
||||
PathKind =
|
||||
CrabMotionFrameTracker
|
||||
.ReferencePathKind.SCurve,
|
||||
StartPosition = source,
|
||||
InitialBodyYawRadians =
|
||||
bodyYawRadians,
|
||||
LengthMillimeters =
|
||||
LengthMillimeters,
|
||||
SCurveLateralOffsetMillimeters =
|
||||
LateralOffsetMillimeters,
|
||||
CruiseSpeed = CruiseSpeed
|
||||
};
|
||||
|
||||
_recorder = new TrackingExperimentRecorder(
|
||||
controllerName:
|
||||
"CrabMotionFrameTracker",
|
||||
trajectoryName:
|
||||
$"CrabSCurve4m_A{LateralOffsetMillimeters:0}mm",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: destination,
|
||||
referenceSpeed: CruiseSpeed,
|
||||
referenceMotionFrameYawDegrees: 90f);
|
||||
tracker.CommandObserver =
|
||||
(vx, vy, omega) =>
|
||||
_recorder?.UpdateBodyCommand(
|
||||
vx,
|
||||
vy,
|
||||
omega);
|
||||
_recorder.Start();
|
||||
|
||||
try
|
||||
{
|
||||
_task = new DriveTask(tracker.Get());
|
||||
_task.Wait();
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateBodyCommand(
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
_recorder?.StopAndSave();
|
||||
_task = null;
|
||||
_recorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateBodyCommand(
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
_recorder?.StopAndSave();
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "旧版SendMotion:4m S型曲线")]
|
||||
public class TestSCurve4m : MovementTest
|
||||
{
|
||||
public float LengthMillimeters = 4000f; // S型曲线纵向长度,单位mm。
|
||||
public float LateralOffsetMillimeters = 400f; // S型曲线左右两侧的最大偏移,单位mm。
|
||||
public float CruiseSpeed = 0.3f; // 首次实车测试建议使用0.3m/s。
|
||||
public int TrialNumber = 1; // 重复实验编号。
|
||||
|
||||
private DriveTask _task;
|
||||
private TrackingExperimentRecorder _recorder;
|
||||
|
||||
// 从当前Detour位姿开始,沿车头方向跟踪先左偏、再右偏并最终回中的完整S型曲线。
|
||||
public override void Test()
|
||||
{
|
||||
if (float.IsNaN(LengthMillimeters) ||
|
||||
float.IsInfinity(LengthMillimeters) ||
|
||||
LengthMillimeters <= 0f ||
|
||||
float.IsNaN(LateralOffsetMillimeters) ||
|
||||
float.IsInfinity(LateralOffsetMillimeters) ||
|
||||
LateralOffsetMillimeters <= 0f ||
|
||||
float.IsNaN(CruiseSpeed) ||
|
||||
float.IsInfinity(CruiseSpeed) ||
|
||||
CruiseSpeed <= 0f)
|
||||
{
|
||||
Console.WriteLine("S型曲线测试参数无效。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MovementTestPreparation.AreWheelsForward())
|
||||
return;
|
||||
|
||||
var location = DetourInterface.getCartLocation();
|
||||
if (double.IsNaN(location.x) ||
|
||||
double.IsInfinity(location.x) ||
|
||||
double.IsNaN(location.y) ||
|
||||
double.IsInfinity(location.y) ||
|
||||
double.IsNaN(location.th) ||
|
||||
double.IsInfinity(location.th))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"Detour当前位姿无效,取消4m S型曲线测试。");
|
||||
return;
|
||||
}
|
||||
|
||||
var source =
|
||||
new Vector2((float)location.x, (float)location.y);
|
||||
var headingRadians =
|
||||
location.th * Math.PI / 180.0;
|
||||
var length = LengthMillimeters;
|
||||
var offset = LateralOffsetMillimeters;
|
||||
|
||||
// 三段三次贝塞尔依次经过左侧峰值、中心线和右侧峰值,
|
||||
// 起点、两个峰值和终点的切线均沿初始前向,连接处没有折角。
|
||||
var firstControlPoints = new List<Vector2>
|
||||
{
|
||||
LocalToWorld(source, headingRadians, 0f, 0f),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length / 12f, 0f),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length / 6f, offset),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length * 0.25f, offset)
|
||||
};
|
||||
var secondControlPoints = new List<Vector2>
|
||||
{
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length * 0.25f, offset),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length / 3f, offset),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length * 2f / 3f, -offset),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length * 0.75f, -offset)
|
||||
};
|
||||
var thirdControlPoints = new List<Vector2>
|
||||
{
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length * 0.75f, -offset),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length * 5f / 6f, -offset),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length * 11f / 12f, 0f),
|
||||
LocalToWorld(
|
||||
source, headingRadians,
|
||||
length, 0f)
|
||||
};
|
||||
|
||||
var firstTrack = new BezierTrack(firstControlPoints)
|
||||
{
|
||||
Speed = CruiseSpeed,
|
||||
CarDirectionBias = 0f
|
||||
};
|
||||
var secondTrack = new BezierTrack(secondControlPoints)
|
||||
{
|
||||
Speed = CruiseSpeed,
|
||||
CarDirectionBias = 0f
|
||||
};
|
||||
var thirdTrack = new BezierTrack(thirdControlPoints)
|
||||
{
|
||||
Speed = CruiseSpeed,
|
||||
CarDirectionBias = 0f
|
||||
};
|
||||
|
||||
var controller = new ChassisController
|
||||
{
|
||||
BaseSpeed = CruiseSpeed
|
||||
}.Get();
|
||||
controller.FinishSpeed = 0f;
|
||||
|
||||
if (!controller.AddTrack(
|
||||
firstTrack,
|
||||
"SCurve4m-Part1") ||
|
||||
!controller.AddTrack(
|
||||
secondTrack,
|
||||
"SCurve4m-Part2") ||
|
||||
!controller.AddTrack(
|
||||
thirdTrack,
|
||||
"SCurve4m-Part3"))
|
||||
{
|
||||
Console.WriteLine(
|
||||
"4m S型曲线轨迹添加失败,取消测试。");
|
||||
return;
|
||||
}
|
||||
|
||||
var destination =
|
||||
LocalToWorld(
|
||||
source,
|
||||
headingRadians,
|
||||
length,
|
||||
0f);
|
||||
_recorder = new TrackingExperimentRecorder(
|
||||
controllerName: "LegacyGeometricController",
|
||||
trajectoryName:
|
||||
$"LegacySCurve4m_A{LateralOffsetMillimeters:0}mm",
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: destination,
|
||||
referenceSpeed: CruiseSpeed);
|
||||
_recorder.Start();
|
||||
|
||||
try
|
||||
{
|
||||
_task = new DriveTask(controller.Track());
|
||||
_task.Wait();
|
||||
|
||||
// 保留少量停止后的数据,用于观察速度是否回到零。
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateCommand(0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
_task = null;
|
||||
_recorder = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止S型曲线测试并保存当前已经采集的数据。
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateCommand(0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
}
|
||||
|
||||
// 将车体起点局部坐标转换为Detour世界坐标,X向前、Y向左。
|
||||
private static Vector2 LocalToWorld(
|
||||
Vector2 origin,
|
||||
double headingRadians,
|
||||
float localX,
|
||||
float localY)
|
||||
{
|
||||
var cos = (float)Math.Cos(headingRadians);
|
||||
var sin = (float)Math.Sin(headingRadians);
|
||||
|
||||
return new Vector2(
|
||||
origin.X + localX * cos - localY * sin,
|
||||
origin.Y + localX * sin + localY * cos);
|
||||
}
|
||||
}
|
||||
|
||||
// C层单车测试:统一使用车体速度命令和SendXYThSpeed跟踪普通模式轨迹。
|
||||
public abstract class XYThNormalTrajectoryTestBase : MovementTest
|
||||
{
|
||||
public float LengthMillimeters = 4000f;
|
||||
public float RadiusMillimeters = 2000f;
|
||||
public float LateralOffsetMillimeters = 400f;
|
||||
public float CruiseSpeed = 0.3f;
|
||||
public int TrialNumber = 1;
|
||||
|
||||
private DriveTask _task;
|
||||
private TrackingExperimentRecorder _recorder;
|
||||
|
||||
protected abstract CrabMotionFrameTracker.ReferencePathKind ReferencePath { get; }
|
||||
|
||||
protected abstract string TrajectoryName { get; }
|
||||
|
||||
// C层单车测试:读取Detour起点并执行普通模式SendXYThSpeed轨迹。
|
||||
public override void Test()
|
||||
{
|
||||
ValidateParameters();
|
||||
|
||||
if (!TryReadDetourPose(
|
||||
out var source,
|
||||
out var initialBodyYawRadians))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Detour当前位置或航向无效,无法开始新版SendXYThSpeed测试。");
|
||||
}
|
||||
|
||||
var tracker = new CrabMotionFrameTracker
|
||||
{
|
||||
PathKind = ReferencePath,
|
||||
// 普通模式的运动坐标系与车体坐标系重合。
|
||||
MotionFrameYawInBodyRadians = 0.0,
|
||||
StartPosition = source,
|
||||
InitialBodyYawRadians = initialBodyYawRadians,
|
||||
LengthMillimeters = LengthMillimeters,
|
||||
RadiusMillimeters = RadiusMillimeters,
|
||||
ArcSweepRadians = Math.PI / 2.0,
|
||||
SCurveLateralOffsetMillimeters =
|
||||
LateralOffsetMillimeters,
|
||||
CruiseSpeed = CruiseSpeed,
|
||||
};
|
||||
|
||||
var destination = ReferencePath ==
|
||||
CrabMotionFrameTracker.ReferencePathKind
|
||||
.LeftArc
|
||||
? tracker.GetArcDestination()
|
||||
: new Vector2(
|
||||
source.X +
|
||||
LengthMillimeters *
|
||||
(float)Math.Cos(initialBodyYawRadians),
|
||||
source.Y +
|
||||
LengthMillimeters *
|
||||
(float)Math.Sin(initialBodyYawRadians));
|
||||
|
||||
_recorder = new TrackingExperimentRecorder(
|
||||
controllerName: "UnifiedXYThTracker",
|
||||
trajectoryName: TrajectoryName,
|
||||
trialNumber: TrialNumber,
|
||||
referenceStart: source,
|
||||
referenceEnd: destination,
|
||||
referenceSpeed: CruiseSpeed);
|
||||
|
||||
tracker.CommandObserver =
|
||||
(vx, vy, omegaRadiansPerSecond) =>
|
||||
_recorder?.UpdateBodyCommand(
|
||||
vx,
|
||||
vy,
|
||||
omegaRadiansPerSecond);
|
||||
|
||||
_recorder.Start();
|
||||
_task = new DriveTask(tracker.Get());
|
||||
|
||||
try
|
||||
{
|
||||
_task.Wait();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_recorder?.UpdateBodyCommand(0f, 0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
_recorder = null;
|
||||
_task = null;
|
||||
}
|
||||
}
|
||||
|
||||
// C层单车测试:停止新版SendXYThSpeed轨迹并保存已有记录。
|
||||
public override void TestStop()
|
||||
{
|
||||
_task?.Stop();
|
||||
_recorder?.UpdateBodyCommand(0f, 0f, 0f);
|
||||
_recorder?.StopAndSave();
|
||||
}
|
||||
|
||||
// C层单车测试:检查新版轨迹的长度、半径、偏移和速度参数。
|
||||
private void ValidateParameters()
|
||||
{
|
||||
if (!IsPositiveFinite(LengthMillimeters))
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(LengthMillimeters),
|
||||
"轨迹长度必须是正有限值。");
|
||||
|
||||
if (!IsPositiveFinite(RadiusMillimeters))
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(RadiusMillimeters),
|
||||
"圆弧半径必须是正有限值。");
|
||||
|
||||
if (!IsPositiveFinite(LateralOffsetMillimeters))
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(LateralOffsetMillimeters),
|
||||
"S型曲线横向偏移必须是正有限值。");
|
||||
|
||||
if (!IsPositiveFinite(CruiseSpeed))
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(CruiseSpeed),
|
||||
"巡航速度必须是正有限值。");
|
||||
}
|
||||
|
||||
// C层单车测试:读取并验证Detour毫米坐标和角度制航向。
|
||||
private static bool TryReadDetourPose(
|
||||
out Vector2 position,
|
||||
out double yawRadians)
|
||||
{
|
||||
var location = DetourInterface.getCartLocation();
|
||||
var x = location.x;
|
||||
var y = location.y;
|
||||
var thetaDegrees = location.th;
|
||||
|
||||
position = new Vector2((float)x, (float)y);
|
||||
yawRadians = thetaDegrees * Math.PI / 180.0;
|
||||
|
||||
return IsFinite(x) &&
|
||||
IsFinite(y) &&
|
||||
IsFinite(thetaDegrees);
|
||||
}
|
||||
|
||||
// C层单车测试:判断浮点参数是否为有限值。
|
||||
private static bool IsFinite(double value)
|
||||
{
|
||||
return !double.IsNaN(value) &&
|
||||
!double.IsInfinity(value);
|
||||
}
|
||||
|
||||
// C层单车测试:判断浮点参数是否为正有限值。
|
||||
private static bool IsPositiveFinite(double value)
|
||||
{
|
||||
return IsFinite(value) && value > 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
[MovementTest(name = "新版SendXYThSpeed:连续前进4m")]
|
||||
public sealed class TestXYThForward4m :
|
||||
XYThNormalTrajectoryTestBase
|
||||
{
|
||||
protected override CrabMotionFrameTracker.ReferencePathKind
|
||||
ReferencePath =>
|
||||
CrabMotionFrameTracker.ReferencePathKind.Straight;
|
||||
|
||||
protected override string TrajectoryName =>
|
||||
"XYThStraight4m";
|
||||
}
|
||||
|
||||
[MovementTest(name = "新版SendXYThSpeed:左转90°圆弧")]
|
||||
public sealed class TestXYThLeftArc90 :
|
||||
XYThNormalTrajectoryTestBase
|
||||
{
|
||||
protected override CrabMotionFrameTracker.ReferencePathKind
|
||||
ReferencePath =>
|
||||
CrabMotionFrameTracker.ReferencePathKind.LeftArc;
|
||||
|
||||
protected override string TrajectoryName =>
|
||||
$"XYThLeftArc90_R{RadiusMillimeters:0}mm";
|
||||
}
|
||||
|
||||
[MovementTest(name = "新版SendXYThSpeed:4m S型曲线")]
|
||||
public sealed class TestXYThSCurve4m :
|
||||
XYThNormalTrajectoryTestBase
|
||||
{
|
||||
protected override CrabMotionFrameTracker.ReferencePathKind
|
||||
ReferencePath =>
|
||||
CrabMotionFrameTracker.ReferencePathKind.SCurve;
|
||||
|
||||
protected override string TrajectoryName =>
|
||||
$"XYThSCurve4m_A{LateralOffsetMillimeters:0}mm";
|
||||
}
|
||||
|
||||
public abstract class ClampMovementTestBase : MovementTest
|
||||
{
|
||||
public float TimeoutSeconds = 30f; // 动作超时时间,单位s。
|
||||
|
||||
@@ -251,7 +251,7 @@ namespace MultiWheelC
|
||||
finally
|
||||
{
|
||||
task?.Stop();
|
||||
chassis.SendXYThSpeed(0f, 0f, 0f);
|
||||
chassis.PredefinedDriveStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +458,12 @@ namespace MultiWheelC
|
||||
var s = thPid.GetResponse(targetAngle, true);
|
||||
Console.WriteLine($"s:{s} AngleTarget:{AngleTarget}");
|
||||
CommandAngularSpeedObserver?.Invoke(s);
|
||||
Chassis.SendXYThSpeed(0, 0, s);
|
||||
if (!Chassis.SendRotateMotion(s))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"原地旋转底盘解算失败:" +
|
||||
Chassis.LastMotionDecomposeFailureReason);
|
||||
}
|
||||
if (thPid.IsArrived()) break;
|
||||
yield return true;
|
||||
}
|
||||
@@ -468,7 +473,7 @@ namespace MultiWheelC
|
||||
finally
|
||||
{
|
||||
CommandAngularSpeedObserver?.Invoke(0f);
|
||||
Chassis.SendXYThSpeed(0, 0, 0);
|
||||
Chassis.PredefinedDriveStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,11 @@ public class PilotConfig : MultiWheelPilotConfig
|
||||
#region 单车-临时
|
||||
[FieldMember(desc = "原地旋转Kp")]
|
||||
public float InPlaceRotateKp = 0.2f;
|
||||
// public float InPlaceRotateKp = 0.2f;
|
||||
|
||||
[FieldMember(desc = "原地旋转Ki")]
|
||||
public float InPlaceRotateKi = 0.01f;
|
||||
// public float InPlaceRotateKi = 0.01f;
|
||||
|
||||
[FieldMember(desc = "原地旋转Kd")]
|
||||
public float InPlaceRotateKd = 0f;
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace MultiWheelC
|
||||
public double DetourY;
|
||||
public double DetourTheta;
|
||||
|
||||
// 车体速度单位为m/s,角速度单位为deg/s。
|
||||
// 车体速度单位为m/s,角速度统一使用rad/s。
|
||||
public float CommandSpeed;
|
||||
public float CommandVx;
|
||||
public float CommandVy;
|
||||
@@ -36,6 +36,8 @@ namespace MultiWheelC
|
||||
private readonly Vector2 _referenceStart;
|
||||
private readonly Vector2 _referenceEnd;
|
||||
private readonly float _referenceSpeed;
|
||||
private readonly float _referenceAngularSpeed;
|
||||
private readonly float _referenceMotionFrameYawDegrees;
|
||||
private readonly int _sampleIntervalMs;
|
||||
|
||||
private readonly List<TrackingSample> _samples =
|
||||
@@ -68,7 +70,9 @@ namespace MultiWheelC
|
||||
Vector2 referenceStart,
|
||||
Vector2 referenceEnd,
|
||||
float referenceSpeed,
|
||||
int sampleIntervalMs = 50)
|
||||
float referenceAngularSpeed = 0f,
|
||||
int sampleIntervalMs = 50,
|
||||
float referenceMotionFrameYawDegrees = 0f)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(controllerName))
|
||||
throw new ArgumentException(
|
||||
@@ -91,6 +95,9 @@ namespace MultiWheelC
|
||||
_referenceStart = referenceStart;
|
||||
_referenceEnd = referenceEnd;
|
||||
_referenceSpeed = referenceSpeed;
|
||||
_referenceAngularSpeed = referenceAngularSpeed;
|
||||
_referenceMotionFrameYawDegrees =
|
||||
referenceMotionFrameYawDegrees;
|
||||
_sampleIntervalMs = sampleIntervalMs;
|
||||
}
|
||||
|
||||
@@ -238,7 +245,11 @@ namespace MultiWheelC
|
||||
|
||||
commandVx = command.Vx;
|
||||
commandVy = command.Vy;
|
||||
commandAngularSpeed = command.Vw;
|
||||
// CommonUsage.GetCarSpeed().Vw的单位为deg/s,
|
||||
// 记录器内部统一转换为rad/s。
|
||||
commandAngularSpeed =
|
||||
command.Vw *
|
||||
(float)Math.PI / 180f;
|
||||
commandSpeed = (float)Math.Sqrt(
|
||||
commandVx * commandVx +
|
||||
commandVy * commandVy);
|
||||
@@ -313,14 +324,18 @@ namespace MultiWheelC
|
||||
"DetourY," +
|
||||
"DetourTheta," +
|
||||
"CommandSpeed," +
|
||||
// 保留旧列(deg/s)供历史Python脚本兼容。
|
||||
"CommandAngularSpeed," +
|
||||
"CommandAngularSpeedRadPerSecond," +
|
||||
"CommandVx," +
|
||||
"CommandVy," +
|
||||
"ReferenceStartX," +
|
||||
"ReferenceStartY," +
|
||||
"ReferenceEndX," +
|
||||
"ReferenceEndY," +
|
||||
"ReferenceSpeed");
|
||||
"ReferenceSpeed," +
|
||||
"ReferenceAngularSpeedRadPerSecond," +
|
||||
"ReferenceMotionFrameYawDegrees");
|
||||
|
||||
foreach (var sample in snapshot)
|
||||
{
|
||||
@@ -335,6 +350,9 @@ namespace MultiWheelC
|
||||
Format(sample.DetourY),
|
||||
Format(sample.DetourTheta),
|
||||
Format(sample.CommandSpeed),
|
||||
Format(
|
||||
sample.CommandAngularSpeed *
|
||||
180.0 / Math.PI),
|
||||
Format(sample.CommandAngularSpeed),
|
||||
Format(sample.CommandVx),
|
||||
Format(sample.CommandVy),
|
||||
@@ -342,7 +360,9 @@ namespace MultiWheelC
|
||||
Format(_referenceStart.Y),
|
||||
Format(_referenceEnd.X),
|
||||
Format(_referenceEnd.Y),
|
||||
Format(_referenceSpeed)));
|
||||
Format(_referenceSpeed),
|
||||
Format(_referenceAngularSpeed),
|
||||
Format(_referenceMotionFrameYawDegrees)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,10 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// 此代码由工具生成。
|
||||
// 运行时版本:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||
// 重新生成代码,这些更改将会丢失。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
e972a413d047c4137a8ce86cbff54a8d2e2558806d9d974d3d6312467ee8ba4d
|
||||
1093d2ea159af831cb6cf39a28abbec1d032f5760b7f90d76a2cda9dbd80e1a4
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -693,6 +693,89 @@ namespace CommonUsage.Chassis
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停车并将四个舵轮转到绕当前坐标原点自转所需的切线方向。
|
||||
/// 只下发舵角,不下发驱动速度。
|
||||
/// </summary>
|
||||
public bool PrepareRotateWheels(float alignmentToleranceDegrees = 2.0f)
|
||||
{
|
||||
if (!Valid)
|
||||
return FailMotionDecomposition(
|
||||
"PrepareRotateWheels",
|
||||
"invalid chassis",
|
||||
null);
|
||||
|
||||
if (float.IsNaN(alignmentToleranceDegrees) ||
|
||||
float.IsInfinity(alignmentToleranceDegrees) ||
|
||||
alignmentToleranceDegrees < 0.0f)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(alignmentToleranceDegrees),
|
||||
"自转舵轮到位容差必须是非负有限值。");
|
||||
|
||||
if (_steerWheels.Count == 0)
|
||||
return FailMotionDecomposition(
|
||||
"PrepareRotateWheels",
|
||||
"no steer wheels",
|
||||
null);
|
||||
|
||||
// 模式切换期间必须保持驱动轮停止。
|
||||
PredefinedDriveStop();
|
||||
|
||||
var targetAngles = new float[_steerWheels.Count];
|
||||
var directions = new int[_steerWheels.Count];
|
||||
|
||||
// 先完成全部舵角解算,再统一下发,避免只转动部分舵轮。
|
||||
for (var i = 0; i < _steerWheels.Count; i++)
|
||||
{
|
||||
var wheel = _steerWheels[i];
|
||||
var px = (double)wheel.Position.X;
|
||||
var py = (double)wheel.Position.Y;
|
||||
|
||||
// 逆时针绕原点旋转时,该舵轮的切向方向为(-py, px)。
|
||||
var tangentDegrees =
|
||||
(float)(Math.Atan2(px, -py) /
|
||||
Math.PI * 180.0);
|
||||
tangentDegrees = CommonMath.ThDiff(
|
||||
tangentDegrees,
|
||||
wheel.ZeroDirection);
|
||||
|
||||
if (!TryResolveWheelAngle(
|
||||
i,
|
||||
tangentDegrees,
|
||||
"PrepareRotateWheels",
|
||||
out targetAngles[i],
|
||||
out directions[i],
|
||||
out var reason))
|
||||
return FailMotionDecomposition(
|
||||
"PrepareRotateWheels",
|
||||
reason,
|
||||
null);
|
||||
}
|
||||
|
||||
for (var i = 0; i < _steerWheels.Count; i++)
|
||||
{
|
||||
_wheelDirs[i] = directions[i];
|
||||
SendTh(i, targetAngles[i]);
|
||||
}
|
||||
|
||||
var allAligned = true;
|
||||
for (var i = 0; i < _steerWheels.Count; i++)
|
||||
{
|
||||
var actualAngle = _steerWheels[i].ReadAngle();
|
||||
var angleError = targetAngles[i] - actualAngle;
|
||||
|
||||
// 这里比较受机械限位约束的真实舵角,不能使用圆周最短角度差。
|
||||
if (float.IsNaN(actualAngle) ||
|
||||
float.IsInfinity(actualAngle) ||
|
||||
Math.Abs(angleError) > alignmentToleranceDegrees)
|
||||
allAligned = false;
|
||||
}
|
||||
|
||||
LastRotateAligned = allAligned;
|
||||
LastMotionDecomposeFailureReason = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绕"已被 SetOriginBias 偏置到车队中心的原点"做原地旋转,可叠加一个车体系小幅纠偏旋量。
|
||||
/// </summary>
|
||||
@@ -781,14 +864,19 @@ namespace CommonUsage.Chassis
|
||||
var maxDth = 0f;
|
||||
for (var i = 0; i < _steerWheels.Count; ++i)
|
||||
{
|
||||
var dth = Math.Abs(CommonMath.ThDiff(ths[i], _steerWheels[i].ReadAngle()));
|
||||
var actualAngle = _steerWheels[i].ReadAngle();
|
||||
var dth = Math.Abs(ths[i] - actualAngle);
|
||||
maxDth = Math.Max(maxDth, dth);
|
||||
slowFac = Math.Min(slowFac, CommonMath.gaussmf(dth, rotSync, 0));
|
||||
// if (dth > 5)
|
||||
// {
|
||||
// allWheelAligned = false;
|
||||
// break;
|
||||
// }
|
||||
slowFac = Math.Min(
|
||||
slowFac,
|
||||
CommonMath.gaussmf(
|
||||
dth,
|
||||
Math.Max(SteeringAlignmentSigmaDegrees, 0.1f),
|
||||
0));
|
||||
if (float.IsNaN(actualAngle) ||
|
||||
float.IsInfinity(actualAngle) ||
|
||||
dth > 2.0f)
|
||||
allWheelAligned = false;
|
||||
}
|
||||
LastRotateAligned = allWheelAligned; // 供上层做积分抗饱和
|
||||
|
||||
@@ -953,8 +1041,6 @@ namespace CommonUsage.Chassis
|
||||
{
|
||||
var vRotX = -vth / 180 * (float)Math.PI * pos.Y / 1000;
|
||||
var vRotY = vth / 180 * (float)Math.PI * pos.X / 1000;
|
||||
Hedingben.ToastText($"vRot:({vRotX:F3},{vRotY:F3}) pos:({pos.X:F1},{pos.Y:F1})",
|
||||
$"SendXYThSpeed-VectorVelocity{i}");
|
||||
return new Vector2(vx + vRotX, vy + vRotY);
|
||||
}
|
||||
/// <summary>
|
||||
@@ -971,38 +1057,61 @@ namespace CommonUsage.Chassis
|
||||
return ((float)(Math.Atan2(v.Y, v.X) / Math.PI * 180), v.Length());
|
||||
}
|
||||
|
||||
private float w = 0;
|
||||
private float wAcc = 0.1f;
|
||||
private float rotSync = 3f;
|
||||
/// <summary>
|
||||
/// 原地旋转时舵角误差对应的速度衰减宽度,单位为度。
|
||||
/// </summary>
|
||||
public float SteeringAlignmentSigmaDegrees { get; set; } = 8f;
|
||||
private bool XYThActive = false;
|
||||
private bool _xyThWheelsAligned = false;
|
||||
private DateTime _xyThDiagnosticsLastTime = DateTime.MinValue;
|
||||
|
||||
public bool SendXYThSpeed(float vx, float vy, float vth, TimeSpan? deltaTime = null)
|
||||
{
|
||||
if (!Valid) return FailMotionDecomposition("SendXYThSpeed", "invalid chassis", deltaTime);
|
||||
|
||||
if (Math.Abs(vx) < 1e-6f && Math.Abs(vy) < 1e-6f && Math.Abs(vth) < 1e-6f)
|
||||
{
|
||||
RampStop(deltaTime);
|
||||
XYThActive = false;
|
||||
_xyThWheelsAligned = false;
|
||||
GoingActive = false;
|
||||
RotatingActive = false;
|
||||
LastMotionDecomposeFailureReason = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!XYThActive)
|
||||
{
|
||||
ResetMotionState();
|
||||
w = 0;
|
||||
_xyThWheelsAligned = false;
|
||||
}
|
||||
XYThActive = true;
|
||||
GoingActive = false;
|
||||
RotatingActive = false;
|
||||
|
||||
if (Math.Abs(vx) < 1e-6f && Math.Abs(vy) < 1e-6f && Math.Abs(vth) < 1e-6f)
|
||||
{
|
||||
RampStop(deltaTime);
|
||||
LastMotionDecomposeFailureReason = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
w = w + wAcc;
|
||||
if (w > 1) w = 1;
|
||||
float[] sendSpeed = new float[_steerWheels.Count];
|
||||
var allWheelsAligned = true;
|
||||
var maximumAngleError = 0f;
|
||||
const float initialAlignmentToleranceDegrees = 2f;
|
||||
var writeDiagnostics =
|
||||
Debug &&
|
||||
(DateTime.Now - _xyThDiagnosticsLastTime)
|
||||
.TotalMilliseconds >= 250.0;
|
||||
|
||||
for (var i = 0; i < _steerWheels.Count; i++)
|
||||
{
|
||||
var sw = _steerWheels[i];
|
||||
var (angle, speed) = AngleAndSpeed(sw.Position, vx, vy, vth, i);
|
||||
var actualTh = sw.ReadAngle();
|
||||
if (float.IsNaN(actualTh) ||
|
||||
float.IsInfinity(actualTh))
|
||||
{
|
||||
return FailMotionDecomposition(
|
||||
"SendXYThSpeed",
|
||||
$"wheel {i} angle feedback is invalid: {actualTh}",
|
||||
deltaTime);
|
||||
}
|
||||
|
||||
if (!TryResolveWheelAngle(i, CommonMath.ThDiff(angle, sw.ZeroDirection), "SendXYThSpeed",
|
||||
out var useAngle, out var dir, out var resolveReason))
|
||||
return FailMotionDecomposition("SendXYThSpeed", resolveReason, deltaTime);
|
||||
@@ -1012,12 +1121,53 @@ namespace CommonUsage.Chassis
|
||||
sendSpeed[i] = speed;
|
||||
if (speed!=0)
|
||||
SendTh(i, useAngle);
|
||||
w = (float)Math.Min(w, CommonMath.gaussmf(CommonMath.ThDiff(actualTh, _sendAngle[i]), rotSync, 0));
|
||||
|
||||
Hedingben.ToastText($"w:{w:F2} s:{speed:F3} th:{_sendAngle[i]:F1} actualTh:{actualTh:F1}", $"SendXYThSpeed-{i}");
|
||||
// 这里比较受机械限位约束的实际舵角,不使用圆周最短角。
|
||||
var angleError =
|
||||
Math.Abs(_sendAngle[i] - actualTh);
|
||||
maximumAngleError =
|
||||
Math.Max(maximumAngleError, angleError);
|
||||
if (angleError >
|
||||
initialAlignmentToleranceDegrees)
|
||||
{
|
||||
allWheelsAligned = false;
|
||||
}
|
||||
|
||||
if (writeDiagnostics)
|
||||
{
|
||||
Hedingben.ToastText(
|
||||
$"ready:{_xyThWheelsAligned} err:{angleError:F1} " +
|
||||
$"s:{speed:F3} th:{_sendAngle[i]:F1} actualTh:{actualTh:F1}",
|
||||
$"SendXYThSpeed-{i}");
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在一段XYTh运动刚开始时等待舵轮到位。
|
||||
// 连续运动开始后,正常改变vx/vy/vth时允许舵轮边转、车辆边走,
|
||||
// 避免每次打方向都重新把驱动速度压到零。
|
||||
if (!_xyThWheelsAligned &&
|
||||
allWheelsAligned)
|
||||
{
|
||||
_xyThWheelsAligned = true;
|
||||
}
|
||||
|
||||
var driveEnabled = _xyThWheelsAligned;
|
||||
for (var i = 0; i < _steerWheels.Count; i++)
|
||||
AccumulateSpeed(i, w * sendSpeed[i],false,new Vector2(0f,0f), deltaTime);
|
||||
AccumulateSpeed(
|
||||
i,
|
||||
driveEnabled ? sendSpeed[i] : 0f,
|
||||
false,
|
||||
new Vector2(0f,0f),
|
||||
deltaTime);
|
||||
|
||||
if (writeDiagnostics)
|
||||
{
|
||||
_xyThDiagnosticsLastTime = DateTime.Now;
|
||||
Hedingben.ToastText(
|
||||
$"ready:{_xyThWheelsAligned} maxErr:{maximumAngleError:F1} " +
|
||||
$"cmd:({vx:F3},{vy:F3},{vth:F1})",
|
||||
"SendXYThSpeed-alignment");
|
||||
}
|
||||
//todo 计算rotCenter填入
|
||||
LastMoveTime = DateTime.Now;
|
||||
LastMotionDecomposeFailureReason = "";
|
||||
|
||||
@@ -68,6 +68,8 @@ namespace MedullaAdapter
|
||||
[AsInitParam(desc = "遥控器速度上限")] public float TransmitterSpeedUpperLimit = 1.0f;
|
||||
[AsInitParam(desc = "遥控器速度下限")] public float TransmitterSpeedLowerLimit = 0.0f;
|
||||
[AsInitParam(desc = "手动控制夹臂速度系数")] public float ManualArmSpeedFac = 1.0f;
|
||||
[AsInitParam(desc = "遥控转弯舵角同步限速宽度,单位为度")]
|
||||
public float ManualSteeringAlignmentSigmaDegrees = 8.0f;
|
||||
[AsInitParam(desc = "左夹臂低限位")][AsLowerIO] public int LeftArmLowerPos = -10000;
|
||||
[AsInitParam(desc = "左夹臂高限位")][AsLowerIO] public int LeftArmUpperPos = 5927610;
|
||||
[AsInitParam(desc = "右夹臂低限位")][AsLowerIO] public int RightArmLowerPos = -17295;
|
||||
@@ -225,21 +227,39 @@ namespace MedullaAdapter
|
||||
}
|
||||
|
||||
var speed = speedThreshold * y;
|
||||
var omega = CalculateManualOmega(speed, x);
|
||||
var normalizedSteering =
|
||||
(float)Math.Pow(
|
||||
Math.Abs(x),
|
||||
ManualThetaPow) *
|
||||
Math.Sign(x);
|
||||
var steeringDegrees =
|
||||
-normalizedSteering * MaxManualTheta;
|
||||
var omega = CalculateManualOmega(
|
||||
speed,
|
||||
steeringDegrees,
|
||||
adapter.HalfWheelBaseMeters);
|
||||
ManualMode = (int)mode;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case ManualControlMode.Normal:
|
||||
SendBodyCommand(vx: speed, vy: 0.0, omegaRadiansPerSecond: omega, interval);
|
||||
// 普通模式统一使用车体速度命令:
|
||||
// X向前,行驶中连续改变角速度时舵轮边转、车辆边走。
|
||||
SendBodyCommand(
|
||||
vx: speed,
|
||||
vy: 0.0,
|
||||
omegaRadiansPerSecond: omega,
|
||||
interval);
|
||||
break;
|
||||
case ManualControlMode.Crab:
|
||||
SendBodyCommand(vx: 0.0, vy: speed, omegaRadiansPerSecond: omega, interval);
|
||||
break;
|
||||
case ManualControlMode.Spin:
|
||||
// 自转时speed表示最外侧舵轮的目标切向速度。
|
||||
// 根据v=omega*r换算角速度,不能把m/s与deg/s直接相乘。
|
||||
var spinOmega =
|
||||
speed * MaxAngularSpeed *
|
||||
Math.PI / 180.0;
|
||||
speed /
|
||||
adapter.MaximumWheelRadiusMeters;
|
||||
|
||||
SendBodyCommand(
|
||||
vx: 0.0,
|
||||
@@ -341,27 +361,15 @@ namespace MedullaAdapter
|
||||
|
||||
private double CalculateManualOmega(
|
||||
float speed,
|
||||
float steeringInput)
|
||||
float steeringDegrees,
|
||||
double halfWheelBaseMeters)
|
||||
{
|
||||
var normalizedSteering =
|
||||
(float)Math.Pow(
|
||||
Math.Abs(steeringInput),
|
||||
ManualThetaPow) *
|
||||
Math.Sign(steeringInput);
|
||||
|
||||
var steeringDegrees =
|
||||
-normalizedSteering * MaxManualTheta;
|
||||
|
||||
var steeringRadians =
|
||||
steeringDegrees * Math.PI / 180.0;
|
||||
|
||||
// CommonUsage中的ControlPointRadius单位为毫米。
|
||||
var halfWheelBaseMeters =
|
||||
Math.Max(
|
||||
Chassis.ControlPointRadius / 1000.0,
|
||||
0.01);
|
||||
|
||||
return speed * Math.Tan(steeringRadians) / halfWheelBaseMeters;
|
||||
return speed *
|
||||
Math.Tan(steeringRadians) /
|
||||
Math.Max(halfWheelBaseMeters, 0.01);
|
||||
}
|
||||
|
||||
internal void SendBodyCommand(double vx, double vy, double omegaRadiansPerSecond, TimeSpan? interval = null)
|
||||
@@ -398,6 +406,11 @@ namespace MedullaAdapter
|
||||
new MultiWheelChassisAdapter(Chassis, CarNum);
|
||||
}
|
||||
|
||||
_chassisAdapter.SteeringAlignmentSigmaDegrees =
|
||||
Math.Max(
|
||||
ManualSteeringAlignmentSigmaDegrees,
|
||||
0.1f);
|
||||
|
||||
return _chassisAdapter;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,7 +10,7 @@
|
||||
"projectUniqueName": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\MedullaAdapter.csproj",
|
||||
"projectName": "MedullaAdapter",
|
||||
"projectPath": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\MedullaAdapter.csproj",
|
||||
"packagesPath": "C:\\Users\\CodexSandboxOffline\\.nuget\\packages\\",
|
||||
"packagesPath": "C:\\Users\\admin\\.nuget\\packages\\",
|
||||
"outputPath": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">C:\Users\CodexSandboxOffline\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\CodexSandboxOffline\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\admin\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.3</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\CodexSandboxOffline\.nuget\packages\" />
|
||||
<SourceRoot Include="C:\Users\admin\.nuget\packages\" />
|
||||
<SourceRoot Include="C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -8,7 +8,7 @@
|
||||
"net8.0": []
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\CodexSandboxOffline\\.nuget\\packages\\": {},
|
||||
"C:\\Users\\admin\\.nuget\\packages\\": {},
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {}
|
||||
},
|
||||
"project": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"projectUniqueName": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\MedullaAdapter.csproj",
|
||||
"projectName": "MedullaAdapter",
|
||||
"projectPath": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\MedullaAdapter.csproj",
|
||||
"packagesPath": "C:\\Users\\CodexSandboxOffline\\.nuget\\packages\\",
|
||||
"packagesPath": "C:\\Users\\admin\\.nuget\\packages\\",
|
||||
"outputPath": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "b9v8vkN2ac8=",
|
||||
"dgSpecHash": "4fABnQtycfA=",
|
||||
"success": true,
|
||||
"projectFilePath": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\MedullaAdapter.csproj",
|
||||
"expectedPackageFiles": [],
|
||||
|
||||
@@ -19,6 +19,40 @@ namespace MyParking.Shared
|
||||
/// </summary>
|
||||
public int VehicleId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum distance from the body origin to a wheel center, in metres.
|
||||
/// </summary>
|
||||
public double MaximumWheelRadiusMeters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum longitudinal wheel offset from the body origin, in metres.
|
||||
/// For a symmetric four-wheel-steering chassis this is half the wheelbase.
|
||||
/// </summary>
|
||||
public double HalfWheelBaseMeters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Width of the steering-alignment speed gate, in degrees.
|
||||
/// </summary>
|
||||
public double SteeringAlignmentSigmaDegrees
|
||||
{
|
||||
get => _chassis.SteeringAlignmentSigmaDegrees;
|
||||
set
|
||||
{
|
||||
if (double.IsNaN(value) ||
|
||||
double.IsInfinity(value) ||
|
||||
value <= 0.0 ||
|
||||
value > float.MaxValue)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(value),
|
||||
"Steering alignment sigma must be a positive finite value.");
|
||||
}
|
||||
|
||||
_chassis.SteeringAlignmentSigmaDegrees =
|
||||
(float)value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查旧底盘是否仍处于无偏置的真实车体坐标系。
|
||||
/// </summary>
|
||||
@@ -118,6 +152,31 @@ namespace MyParking.Shared
|
||||
}
|
||||
// 禁用旧版DirectionAngle/ZeroDirection坐标偏置,
|
||||
// 保证SendXYThSpeed直接使用真实车体坐标系。
|
||||
var maximumWheelRadiusMillimeters = 0.0;
|
||||
var maximumLongitudinalOffsetMillimeters = 0.0;
|
||||
foreach (var wheel in wheels)
|
||||
{
|
||||
maximumWheelRadiusMillimeters = Math.Max(
|
||||
maximumWheelRadiusMillimeters,
|
||||
wheel.PhysicalPosition.Length());
|
||||
|
||||
maximumLongitudinalOffsetMillimeters = Math.Max(
|
||||
maximumLongitudinalOffsetMillimeters,
|
||||
Math.Abs(wheel.PhysicalPosition.X));
|
||||
}
|
||||
|
||||
MaximumWheelRadiusMeters =
|
||||
maximumWheelRadiusMillimeters / 1000.0;
|
||||
HalfWheelBaseMeters =
|
||||
maximumLongitudinalOffsetMillimeters / 1000.0;
|
||||
|
||||
if (MaximumWheelRadiusMeters <= 0.0 ||
|
||||
HalfWheelBaseMeters <= 0.0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Wheel positions cannot produce valid chassis dimensions.");
|
||||
}
|
||||
|
||||
ResetToBodyFrame();
|
||||
|
||||
// 停车机器人优先保持当前机械舵角,通过反转轮速表达反向运动,
|
||||
@@ -269,12 +328,10 @@ namespace MyParking.Shared
|
||||
TimeSpan? interval = null)
|
||||
{
|
||||
EnsureBodyFrameIsActive();
|
||||
_chassis.PredefinedDriveStop();
|
||||
|
||||
var success =
|
||||
_chassis.SendRotateMotion(
|
||||
0.0f,
|
||||
interval);
|
||||
_chassis.PrepareRotateWheels(
|
||||
alignmentToleranceDegrees: 2.0f);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -34,7 +34,7 @@ def plot_angular_command(
|
||||
)
|
||||
time = frame["TimeSeconds"].to_numpy(dtype=float)
|
||||
angular_command = frame[
|
||||
"CommandAngularSpeedDegPerSec"
|
||||
"CommandAngularSpeedRadPerSec"
|
||||
].to_numpy(dtype=float)
|
||||
maximum = float(np.max(angular_command))
|
||||
minimum = float(np.min(angular_command))
|
||||
@@ -50,12 +50,12 @@ def plot_angular_command(
|
||||
ax.axhline(0.0, color="black", linewidth=0.8)
|
||||
shade_localization_jump_windows(ax, metadata)
|
||||
ax.set_xlabel("时间 / s")
|
||||
ax.set_ylabel("命令角速度 / (°/s)")
|
||||
ax.set_ylabel("命令角速度 / (rad/s)")
|
||||
ax.set_title(
|
||||
f"角速度指令曲线\n"
|
||||
f"{metadata['controller_name']} - "
|
||||
f"{metadata['trajectory_name']},"
|
||||
f"范围=[{minimum:.3f}, {maximum:.3f}]°/s"
|
||||
f"范围=[{minimum:.3f}, {maximum:.3f}]rad/s"
|
||||
)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
|
||||
@@ -76,6 +76,92 @@ def wrap_degrees(angle_degrees: np.ndarray) -> np.ndarray:
|
||||
return (angle_degrees + 180.0) % 360.0 - 180.0
|
||||
|
||||
|
||||
def build_complete_s_curve(
|
||||
start: np.ndarray,
|
||||
end: np.ndarray,
|
||||
offset_mm: float,
|
||||
samples_per_segment: int = 120,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""重建测试使用的三段三次贝塞尔完整S曲线及各点切线航向。"""
|
||||
line = end - start
|
||||
length = float(np.linalg.norm(line))
|
||||
if length <= 1e-6:
|
||||
raise ValueError("S型曲线的起点和终点不能重合。")
|
||||
|
||||
forward = line / length
|
||||
left = np.array([-forward[1], forward[0]])
|
||||
controls = [
|
||||
np.array([
|
||||
[0.0, 0.0],
|
||||
[length / 12.0, 0.0],
|
||||
[length / 6.0, offset_mm],
|
||||
[length * 0.25, offset_mm],
|
||||
]),
|
||||
np.array([
|
||||
[length * 0.25, offset_mm],
|
||||
[length / 3.0, offset_mm],
|
||||
[length * 2.0 / 3.0, -offset_mm],
|
||||
[length * 0.75, -offset_mm],
|
||||
]),
|
||||
np.array([
|
||||
[length * 0.75, -offset_mm],
|
||||
[length * 5.0 / 6.0, -offset_mm],
|
||||
[length * 11.0 / 12.0, 0.0],
|
||||
[length, 0.0],
|
||||
]),
|
||||
]
|
||||
|
||||
local_parts: list[np.ndarray] = []
|
||||
derivative_parts: list[np.ndarray] = []
|
||||
for index, points in enumerate(controls):
|
||||
t = np.linspace(0.0, 1.0, samples_per_segment + 1)
|
||||
if index > 0:
|
||||
t = t[1:]
|
||||
one_minus_t = 1.0 - t
|
||||
local = (
|
||||
one_minus_t[:, None] ** 3 * points[0]
|
||||
+ 3.0
|
||||
* one_minus_t[:, None] ** 2
|
||||
* t[:, None]
|
||||
* points[1]
|
||||
+ 3.0
|
||||
* one_minus_t[:, None]
|
||||
* t[:, None] ** 2
|
||||
* points[2]
|
||||
+ t[:, None] ** 3 * points[3]
|
||||
)
|
||||
derivative = (
|
||||
3.0
|
||||
* one_minus_t[:, None] ** 2
|
||||
* (points[1] - points[0])
|
||||
+ 6.0
|
||||
* one_minus_t[:, None]
|
||||
* t[:, None]
|
||||
* (points[2] - points[1])
|
||||
+ 3.0
|
||||
* t[:, None] ** 2
|
||||
* (points[3] - points[2])
|
||||
)
|
||||
local_parts.append(local)
|
||||
derivative_parts.append(derivative)
|
||||
|
||||
local_points = np.vstack(local_parts)
|
||||
local_derivatives = np.vstack(derivative_parts)
|
||||
world_points = (
|
||||
start
|
||||
+ local_points[:, 0, None] * forward
|
||||
+ local_points[:, 1, None] * left
|
||||
)
|
||||
world_derivatives = (
|
||||
local_derivatives[:, 0, None] * forward
|
||||
+ local_derivatives[:, 1, None] * left
|
||||
)
|
||||
headings = np.rad2deg(
|
||||
np.arctan2(world_derivatives[:, 1], world_derivatives[:, 0])
|
||||
)
|
||||
return world_points, headings
|
||||
|
||||
|
||||
def segmented_savgol(
|
||||
values: np.ndarray,
|
||||
sample_interval: float,
|
||||
@@ -166,6 +252,16 @@ def load_and_resample(
|
||||
"ReferenceEndY",
|
||||
"ReferenceSpeed",
|
||||
]
|
||||
optional_numeric_columns = [
|
||||
"CommandAngularSpeedRadPerSecond",
|
||||
"ReferenceAngularSpeedRadPerSecond",
|
||||
"ReferenceMotionFrameYawDegrees",
|
||||
]
|
||||
numeric_columns.extend(
|
||||
column
|
||||
for column in optional_numeric_columns
|
||||
if column in raw.columns
|
||||
)
|
||||
for column in numeric_columns:
|
||||
raw[column] = pd.to_numeric(raw[column], errors="coerce")
|
||||
|
||||
@@ -220,9 +316,21 @@ def load_and_resample(
|
||||
update_command_speed = np.abs(
|
||||
updates["CommandSpeed"].to_numpy(dtype=float)
|
||||
)
|
||||
update_command_angular = np.abs(
|
||||
updates["CommandAngularSpeed"].to_numpy(dtype=float)
|
||||
)
|
||||
if "CommandAngularSpeedRadPerSecond" in updates.columns:
|
||||
update_command_angular_rad = np.abs(
|
||||
updates[
|
||||
"CommandAngularSpeedRadPerSecond"
|
||||
].to_numpy(dtype=float)
|
||||
)
|
||||
else:
|
||||
# 旧CSV中的CommandAngularSpeed单位为deg/s。
|
||||
update_command_angular_rad = np.deg2rad(
|
||||
np.abs(
|
||||
updates[
|
||||
"CommandAngularSpeed"
|
||||
].to_numpy(dtype=float)
|
||||
)
|
||||
)
|
||||
|
||||
# 自适应跳变阈值:正常移动允许达到参考位移的3倍并保留15mm余量;
|
||||
# 低速阶段仍至少允许30mm,防止把普通定位噪声误判为跳变。
|
||||
@@ -243,8 +351,12 @@ def load_and_resample(
|
||||
)
|
||||
expected_heading_delta = (
|
||||
0.5 *
|
||||
(update_command_angular[1:] + update_command_angular[:-1]) *
|
||||
update_dt
|
||||
(
|
||||
update_command_angular_rad[1:] +
|
||||
update_command_angular_rad[:-1]
|
||||
) *
|
||||
update_dt *
|
||||
180.0 / np.pi
|
||||
)
|
||||
heading_threshold = np.maximum(
|
||||
5.0,
|
||||
@@ -347,6 +459,15 @@ def load_and_resample(
|
||||
(time_uniform >= start) & (time_uniform <= end)
|
||||
)
|
||||
|
||||
if "CommandAngularSpeedRadPerSecond" in raw.columns:
|
||||
angular_command_rad = interpolate_command(
|
||||
"CommandAngularSpeedRadPerSecond"
|
||||
)
|
||||
else:
|
||||
angular_command_rad = np.deg2rad(
|
||||
interpolate_command("CommandAngularSpeed")
|
||||
)
|
||||
|
||||
frame = pd.DataFrame({
|
||||
"TimeSeconds": time_uniform,
|
||||
"DetourXRawMm": x_resampled,
|
||||
@@ -356,8 +477,8 @@ def load_and_resample(
|
||||
"DetourThetaUnwrappedDeg": theta_filtered,
|
||||
"DetourThetaDeg": wrap_degrees(theta_filtered),
|
||||
"CommandSpeedMps": interpolate_command("CommandSpeed"),
|
||||
"CommandAngularSpeedDegPerSec":
|
||||
interpolate_command("CommandAngularSpeed"),
|
||||
"CommandAngularSpeedRadPerSec":
|
||||
angular_command_rad,
|
||||
"InvalidNearLocalizationJump": invalid_near_jump,
|
||||
})
|
||||
|
||||
@@ -367,6 +488,25 @@ def load_and_resample(
|
||||
"trajectory_name": str(first["TrajectoryName"]),
|
||||
"controller_name": str(first.get("ControllerName", "")),
|
||||
"trial_number": str(first.get("TrialNumber", "")),
|
||||
# 蟹行轨迹的运动前向相对车体X轴逆时针偏置90°。
|
||||
# DetourTheta始终是车体航向,计算航向误差时必须扣除该偏置。
|
||||
"motion_frame_yaw_degrees": float(
|
||||
first["ReferenceMotionFrameYawDegrees"]
|
||||
if (
|
||||
"ReferenceMotionFrameYawDegrees" in raw.columns
|
||||
and pd.notna(
|
||||
first["ReferenceMotionFrameYawDegrees"]
|
||||
)
|
||||
)
|
||||
else (
|
||||
90.0
|
||||
if "crab" in (
|
||||
str(first["TrajectoryName"]) +
|
||||
str(first.get("ControllerName", ""))
|
||||
).lower()
|
||||
else 0.0
|
||||
)
|
||||
),
|
||||
"reference_start_mm": np.array(
|
||||
[first["ReferenceStartX"], first["ReferenceStartY"]],
|
||||
dtype=float,
|
||||
@@ -376,6 +516,12 @@ def load_and_resample(
|
||||
dtype=float,
|
||||
),
|
||||
"reference_speed_mps": float(first["ReferenceSpeed"]),
|
||||
"reference_angular_speed_rad_per_second": float(
|
||||
first.get(
|
||||
"ReferenceAngularSpeedRadPerSecond",
|
||||
0.0,
|
||||
)
|
||||
),
|
||||
# 圆弧构造时使用了测试开始处Detour航向,因此这里取首帧航向。
|
||||
"start_heading_degrees": float(first["DetourTheta"]),
|
||||
"sample_interval_seconds": sample_interval,
|
||||
@@ -393,10 +539,13 @@ def build_reference(
|
||||
frame: pd.DataFrame,
|
||||
metadata: dict[str, Any],
|
||||
) -> dict[str, np.ndarray | float | str]:
|
||||
"""根据CSV元数据建立直线、圆弧或原地自转参考及误差。"""
|
||||
"""根据CSV元数据建立直线、圆弧、完整S曲线或原地自转参考及误差。"""
|
||||
trajectory_name = str(metadata["trajectory_name"])
|
||||
start = np.asarray(metadata["reference_start_mm"], dtype=float)
|
||||
end = np.asarray(metadata["reference_end_mm"], dtype=float)
|
||||
motion_frame_yaw_degrees = float(
|
||||
metadata.get("motion_frame_yaw_degrees", 0.0)
|
||||
)
|
||||
actual = frame[
|
||||
["DetourXFilteredMm", "DetourYFilteredMm"]
|
||||
].to_numpy(dtype=float)
|
||||
@@ -410,12 +559,15 @@ def build_reference(
|
||||
if radius_match:
|
||||
radius = float(radius_match.group("radius"))
|
||||
sweep_degrees = float(radius_match.group("sweep"))
|
||||
start_heading = float(metadata["start_heading_degrees"])
|
||||
heading_radians = np.deg2rad(start_heading)
|
||||
start_body_heading = float(metadata["start_heading_degrees"])
|
||||
start_motion_heading = (
|
||||
start_body_heading + motion_frame_yaw_degrees
|
||||
)
|
||||
heading_radians = np.deg2rad(start_motion_heading)
|
||||
center = start + radius * np.array(
|
||||
[-np.sin(heading_radians), np.cos(heading_radians)]
|
||||
)
|
||||
start_radial_degrees = start_heading - 90.0
|
||||
start_radial_degrees = start_motion_heading - 90.0
|
||||
|
||||
radial = actual - center
|
||||
distance_to_center = np.linalg.norm(radial, axis=1)
|
||||
@@ -429,7 +581,10 @@ def build_reference(
|
||||
])
|
||||
# 对逆时针圆弧,正横向误差表示车辆位于轨迹左侧(圆内侧)。
|
||||
lateral_error = radius - distance_to_center
|
||||
reference_heading = radial_angle_degrees + 90.0
|
||||
reference_motion_heading = radial_angle_degrees + 90.0
|
||||
reference_heading = (
|
||||
reference_motion_heading - motion_frame_yaw_degrees
|
||||
)
|
||||
heading_error = wrap_degrees(
|
||||
actual_heading - reference_heading
|
||||
)
|
||||
@@ -450,12 +605,60 @@ def build_reference(
|
||||
"ideal_plot_mm": ideal_plot,
|
||||
"reference_points_mm": reference_points,
|
||||
"reference_heading_degrees": reference_heading,
|
||||
"reference_motion_heading_degrees":
|
||||
reference_motion_heading,
|
||||
"lateral_error_mm": lateral_error,
|
||||
"heading_error_degrees": heading_error,
|
||||
"center_mm": center,
|
||||
"radius_mm": radius,
|
||||
}
|
||||
|
||||
s_curve_match = re.search(
|
||||
r"SCurve(?P<length>[0-9.]+)m_A(?P<offset>[0-9.]+)mm",
|
||||
trajectory_name,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if s_curve_match:
|
||||
offset_mm = float(s_curve_match.group("offset"))
|
||||
ideal_plot, ideal_heading = build_complete_s_curve(
|
||||
start,
|
||||
end,
|
||||
offset_mm,
|
||||
)
|
||||
delta = actual[:, np.newaxis, :] - ideal_plot[np.newaxis, :, :]
|
||||
nearest_indices = np.argmin(
|
||||
np.sum(delta * delta, axis=2),
|
||||
axis=1,
|
||||
)
|
||||
reference_points = ideal_plot[nearest_indices]
|
||||
reference_motion_heading = ideal_heading[nearest_indices]
|
||||
reference_heading = (
|
||||
reference_motion_heading - motion_frame_yaw_degrees
|
||||
)
|
||||
heading_radians = np.deg2rad(reference_motion_heading)
|
||||
left_normals = np.column_stack([
|
||||
-np.sin(heading_radians),
|
||||
np.cos(heading_radians),
|
||||
])
|
||||
lateral_error = np.sum(
|
||||
(actual - reference_points) * left_normals,
|
||||
axis=1,
|
||||
)
|
||||
heading_error = wrap_degrees(
|
||||
actual_heading - reference_heading
|
||||
)
|
||||
return {
|
||||
"kind": "s_curve",
|
||||
"ideal_plot_mm": ideal_plot,
|
||||
"reference_points_mm": reference_points,
|
||||
"reference_heading_degrees": reference_heading,
|
||||
"reference_motion_heading_degrees":
|
||||
reference_motion_heading,
|
||||
"lateral_error_mm": lateral_error,
|
||||
"heading_error_degrees": heading_error,
|
||||
"offset_mm": offset_mm,
|
||||
}
|
||||
|
||||
line = end - start
|
||||
length = float(np.linalg.norm(line))
|
||||
if length <= 1e-6:
|
||||
@@ -516,10 +719,17 @@ def build_reference(
|
||||
progress = np.clip(displacement @ tangent, 0.0, length)
|
||||
reference_points = start + np.outer(progress, tangent)
|
||||
lateral_error = (actual - reference_points) @ left_normal
|
||||
reference_heading_scalar = np.rad2deg(
|
||||
reference_motion_heading_scalar = np.rad2deg(
|
||||
np.arctan2(tangent[1], tangent[0])
|
||||
)
|
||||
reference_heading = np.full(len(frame), reference_heading_scalar)
|
||||
reference_heading_scalar = (
|
||||
reference_motion_heading_scalar -
|
||||
motion_frame_yaw_degrees
|
||||
)
|
||||
reference_heading = np.full(
|
||||
len(frame),
|
||||
reference_heading_scalar,
|
||||
)
|
||||
heading_error = wrap_degrees(
|
||||
actual_heading - reference_heading
|
||||
)
|
||||
@@ -529,6 +739,10 @@ def build_reference(
|
||||
"ideal_plot_mm": ideal_plot,
|
||||
"reference_points_mm": reference_points,
|
||||
"reference_heading_degrees": reference_heading,
|
||||
"reference_motion_heading_degrees": np.full(
|
||||
len(frame),
|
||||
reference_motion_heading_scalar,
|
||||
),
|
||||
"lateral_error_mm": lateral_error,
|
||||
"heading_error_degrees": heading_error,
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -9,6 +9,7 @@
|
||||
### 评价指标
|
||||
横向误差 RMSE、最大横向误差;航向误差 RMSE、最大航向误差;速度误差 RMSE、最大速度偏差;
|
||||
角速度或转角指令的变化曲线;最终位置误差、最终航向误差
|
||||
|
||||
### 展示形式
|
||||
理想轨迹与实际轨迹对比图、横向/航向误差随时间变化图、参考速度与实际速度对比图、角速度指令曲线
|
||||
|
||||
@@ -26,7 +27,6 @@ ALQR:在线估计最新参数实时重新求解
|
||||
3.
|
||||
|
||||
|
||||
|
||||
先用 Bryson’s Rule + 贝叶斯优化在仿真里把 Q/R 调到一个不错的基准。
|
||||
上实车时采用自适应 LQR:在线估计关键参数(尤其是轮胎刚度),实时更新 K。
|
||||
C# 实现的话:
|
||||
|
||||
Reference in New Issue
Block a user