优化差速转舵并增加纵向辨识测试

This commit is contained in:
2026-08-27 12:39:22 +08:00
parent 17092c2766
commit 1c5181b327
24 changed files with 1748 additions and 266 deletions
@@ -0,0 +1,562 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Threading;
using ClumsyCore;
using ClumsyCore.Pilot;
using CommonUsage.Chassis;
using FundamentalLib;
using MultiWheelC.StateEstimation;
using MyParking.Shared;
namespace MultiWheelC
{
/// <summary>
/// 提供纵向开环辨识测试共用的舵轮准备、速度下发、采样和安全停车流程。
/// </summary>
public abstract class LongitudinalIdentificationTestBase
: MovementTest
{
private const double MaximumTargetSpeedMetersPerSecond =
0.70;
private const double MinimumTargetSpeedMetersPerSecond =
0.02;
private const float MillimetersPerMeter = 1000f;
private DriveTask _preparationTask;
private MultiWheelChassis _chassis;
private MultiWheelChassisAdapter _adapter;
private WheelFeedbackVehicleStateProvider _stateProvider;
private TrackingExperimentRecorder _recorder;
private int _testRunning;
private int _stopRequested;
/// <summary>
/// 获取或设置本次重复实验编号。
/// </summary>
public int TrialNumber = 1;
/// <summary>
/// 获取或设置速度阶跃前的静止记录时间,单位为s。
/// </summary>
public double BaselineSeconds = 1.0;
/// <summary>
/// 获取或设置目标速度保持时间,单位为s。
/// </summary>
public double CommandHoldSeconds = 5.0;
/// <summary>
/// 获取或设置停车后的继续记录时间,单位为s。
/// </summary>
public double PostStopSeconds = 2.0;
/// <summary>
/// 获取或设置速度命令循环周期,单位为ms。
/// </summary>
public int ControlIntervalMilliseconds = 50;
/// <summary>
/// 派生测试选择是否使用底盘DeAccPerSecond完成正常减速。
/// </summary>
protected abstract bool UseConfiguredDeceleration { get; }
/// <summary>
/// 获取用于CSV文件名区分停车方式的标识。
/// </summary>
protected abstract string StopModeName { get; }
/// <summary>
/// 读取目标速度,完成舵轮回正后执行纵向开环命令并保存CSV。
/// </summary>
public override void Test()
{
if (Interlocked.CompareExchange(
ref _testRunning,
1,
0) != 0)
{
Console.WriteLine(
"纵向辨识测试已经在运行,请先停止当前测试。");
return;
}
Interlocked.Exchange(ref _stopRequested, 0);
try
{
ValidateSettings();
if (!TryReadTargetSpeed(
out var targetSpeedMetersPerSecond))
{
return;
}
_chassis =
PilotDefinition.Chassis as MultiWheelChassis;
if (_chassis == null)
{
throw new InvalidOperationException(
"当前底盘不是MultiWheelChassis,无法执行纵向辨识。");
}
ValidateChassisAndTarget(
targetSpeedMetersPerSecond);
Console.WriteLine(
"纵向辨识开始前请确认M层轮速诊断记录已经开启。");
Hedingben.ToastText(
"请确认M层轮速诊断记录已开启。",
"LongitudinalIdentification");
if (!MovementTestPreparation.AlignWheelsForward(
ref _preparationTask))
{
throw new InvalidOperationException(
"四个舵轮未能稳定回正,纵向辨识已经取消。");
}
ThrowIfStopRequested();
_adapter = new MultiWheelChassisAdapter(
_chassis,
PilotDefinition.Self.CarNum);
_adapter.ResetToBodyFrame();
_adapter.StopImmediately();
_stateProvider =
ParkingVehicleStateProviderFactory.Create(
_chassis);
if (!_stateProvider.TryGetState(
out var initialState))
{
throw new InvalidOperationException(
"无法读取纵向辨识起点位姿:" +
_stateProvider.LastFailureReason);
}
_recorder = CreateRecorder(
targetSpeedMetersPerSecond,
initialState.PoseInWorld);
_recorder.Start();
Console.WriteLine(
$"纵向辨识参数:目标速度={targetSpeedMetersPerSecond:F3}m/s" +
$"AccPerSecond={_chassis.AccPerSecond:F3}m/s²," +
$"DeAccPerSecond={_chassis.DeAccPerSecond:F3}m/s²," +
$"保持={CommandHoldSeconds:F1}s,停车方式={StopModeName}。");
RunStationaryPhase(
BaselineSeconds,
"静止基线");
RunTargetSpeedPhase(
targetSpeedMetersPerSecond);
if (UseConfiguredDeceleration)
{
RunConfiguredDecelerationPhase(
targetSpeedMetersPerSecond);
}
else
{
_adapter.StopImmediately();
RunStationaryPhase(
PostStopSeconds,
"立即停车后记录");
}
Console.WriteLine(
"纵向辨识测试完成。请同时保存对应的M层轮速诊断CSV。");
Hedingben.ToastText(
"纵向辨识完成,请停止并保存M层轮速诊断记录。",
"LongitudinalIdentification");
}
catch (OperationCanceledException)
{
Console.WriteLine("纵向辨识已由用户停止。");
}
catch (Exception exception)
{
Console.WriteLine(
"纵向辨识失败:" +
exception.Message);
Hedingben.ToastText(
"纵向辨识失败:" +
exception.Message,
"LongitudinalIdentification");
}
finally
{
_adapter?.StopImmediately();
_chassis?.PredefinedDriveStop();
_recorder?.UpdateBodyCommand(0f, 0f, 0f);
_recorder?.StopAndSave();
_preparationTask?.Stop();
_preparationTask = null;
_recorder = null;
_stateProvider = null;
_adapter = null;
_chassis = null;
Interlocked.Exchange(ref _testRunning, 0);
}
}
/// <summary>
/// 请求停止当前辨识并立即清零底盘驱动速度。
/// </summary>
public override void TestStop()
{
Interlocked.Exchange(ref _stopRequested, 1);
_preparationTask?.Stop();
_adapter?.StopImmediately();
_chassis?.PredefinedDriveStop();
}
/// <summary>
/// 在固定周期内保持零命令并记录静止数据。
/// </summary>
private void RunStationaryPhase(
double durationSeconds,
string phaseName)
{
_recorder.UpdateBodyCommand(0f, 0f, 0f);
RunPeriodicPhase(
durationSeconds,
phaseName,
_ => UpdateWheelDiagnostics());
}
/// <summary>
/// 通过标准车体速度适配器持续下发直线目标速度。
/// </summary>
private void RunTargetSpeedPhase(
double targetSpeedMetersPerSecond)
{
_recorder.UpdateBodyCommand(
(float)targetSpeedMetersPerSecond,
0f,
0f);
RunPeriodicPhase(
CommandHoldSeconds,
"目标速度保持",
interval =>
{
var accepted = _adapter.SendBodyTwist(
new Twist2D(
targetSpeedMetersPerSecond,
0.0,
0.0),
interval);
if (!accepted)
{
throw new InvalidOperationException(
"底盘拒绝纵向速度命令:" +
_adapter.LastFailureReason);
}
UpdateWheelDiagnostics();
});
}
/// <summary>
/// 重复发送零速SendMotion,使底盘内部DeAccPerSecond斜坡真实参与减速。
/// </summary>
private void RunConfiguredDecelerationPhase(
double targetSpeedMetersPerSecond)
{
_recorder.UpdateBodyCommand(0f, 0f, 0f);
var rampDurationSeconds =
Math.Abs(targetSpeedMetersPerSecond) /
_chassis.DeAccPerSecond;
var totalDurationSeconds =
rampDurationSeconds +
PostStopSeconds;
RunPeriodicPhase(
totalDurationSeconds,
"配置化正常减速",
interval =>
{
// SendBodyTwist(Zero)会立即停车;辨识DeAccPerSecond时必须
// 直接保持SendMotion零目标,让底盘内部发送速度逐周期下降。
var accepted = _chassis.SendMotion(
0f,
0f,
0f,
interval);
if (!accepted)
{
throw new InvalidOperationException(
"底盘拒绝正常减速命令:" +
_chassis.LastMotionDecomposeFailureReason);
}
UpdateWheelDiagnostics();
});
}
/// <summary>
/// 按实际循环间隔运行一个阶段,并响应测试界面的停止请求。
/// </summary>
private void RunPeriodicPhase(
double durationSeconds,
string phaseName,
Action<TimeSpan> cycleAction)
{
Console.WriteLine(
$"纵向辨识阶段:{phaseName},预计{durationSeconds:F2}s。");
var periodSeconds =
ControlIntervalMilliseconds / 1000.0;
var phaseClock = Stopwatch.StartNew();
var previousCycleSeconds = -periodSeconds;
while (phaseClock.Elapsed.TotalSeconds <
durationSeconds)
{
ThrowIfStopRequested();
var cycleStartSeconds =
phaseClock.Elapsed.TotalSeconds;
var deltaTimeSeconds =
cycleStartSeconds -
previousCycleSeconds;
previousCycleSeconds = cycleStartSeconds;
cycleAction(
TimeSpan.FromSeconds(
deltaTimeSeconds));
var elapsedMilliseconds =
(phaseClock.Elapsed.TotalSeconds -
cycleStartSeconds) * 1000.0;
var remainingMilliseconds =
ControlIntervalMilliseconds -
elapsedMilliseconds;
if (remainingMilliseconds > 1.0)
{
Thread.Sleep(
(int)Math.Floor(
remainingMilliseconds));
}
}
}
/// <summary>
/// 刷新轮组原始与滤波速度,供后台CSV记录器读取最新诊断值。
/// </summary>
private void UpdateWheelDiagnostics()
{
_stateProvider.TryGetWheelTwist(
out _,
out _);
}
/// <summary>
/// 创建复用现有字段格式的纵向辨识CSV记录器。
/// </summary>
private TrackingExperimentRecorder CreateRecorder(
double targetSpeedMetersPerSecond,
Pose2D initialPoseInWorld)
{
var expectedTravelMeters =
targetSpeedMetersPerSecond *
CommandHoldSeconds;
var referenceStart = new Vector2(
(float)(
initialPoseInWorld.XMeters *
MillimetersPerMeter),
(float)(
initialPoseInWorld.YMeters *
MillimetersPerMeter));
var referenceEnd = new Vector2(
referenceStart.X +
(float)(
Math.Cos(initialPoseInWorld.YawRadians) *
expectedTravelMeters *
MillimetersPerMeter),
referenceStart.Y +
(float)(
Math.Sin(initialPoseInWorld.YawRadians) *
expectedTravelMeters *
MillimetersPerMeter));
var speedMillimetersPerSecond =
targetSpeedMetersPerSecond *
MillimetersPerMeter;
var trajectoryName =
"LongitudinalStep_" +
speedMillimetersPerSecond.ToString(
"+0;-0;0",
CultureInfo.InvariantCulture) +
"mmps_" +
StopModeName;
return new TrackingExperimentRecorder(
controllerName:
"OpenLoopLongitudinalIdentification",
trajectoryName: trajectoryName,
trialNumber: TrialNumber,
referenceStart: referenceStart,
referenceEnd: referenceEnd,
referenceSpeed:
(float)targetSpeedMetersPerSecond,
sampleIntervalMs:
ControlIntervalMilliseconds,
referenceMotionFrameYawDegrees: 0f,
referenceAccelerationMetersPerSecondSquared:
_chassis.AccPerSecond,
referenceDecelerationMetersPerSecondSquared:
_chassis.DeAccPerSecond,
diagnosticChassis: _chassis,
diagnosticStateProvider: _stateProvider);
}
/// <summary>
/// 从测试界面读取带方向的纵向目标速度,正值前进、负值倒车。
/// </summary>
private static bool TryReadTargetSpeed(
out double targetSpeedMetersPerSecond)
{
targetSpeedMetersPerSecond = 0.0;
var input = UI.GetInput(
"输入纵向目标速度(m/s,正数前进、负数倒车," +
$"范围-{MaximumTargetSpeedMetersPerSecond:F1}" +
$"{MaximumTargetSpeedMetersPerSecond:F1}且不能为0):");
var parsed = double.TryParse(
input,
NumberStyles.Float,
CultureInfo.CurrentCulture,
out targetSpeedMetersPerSecond) ||
double.TryParse(
input,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out targetSpeedMetersPerSecond);
if (!parsed ||
double.IsNaN(targetSpeedMetersPerSecond) ||
double.IsInfinity(targetSpeedMetersPerSecond) ||
Math.Abs(targetSpeedMetersPerSecond) <
MinimumTargetSpeedMetersPerSecond ||
Math.Abs(targetSpeedMetersPerSecond) >
MaximumTargetSpeedMetersPerSecond)
{
Console.WriteLine(
"目标速度必须是绝对值位于" +
$"{MinimumTargetSpeedMetersPerSecond:F2}" +
$"{MaximumTargetSpeedMetersPerSecond:F2}m/s之间的有限数值。");
return false;
}
return true;
}
/// <summary>
/// 验证实验时间、循环周期和编号配置。
/// </summary>
private void ValidateSettings()
{
NumericGuard.EnsureFiniteNonNegative(
BaselineSeconds,
nameof(BaselineSeconds));
NumericGuard.EnsureFinitePositive(
CommandHoldSeconds,
nameof(CommandHoldSeconds));
NumericGuard.EnsureFiniteNonNegative(
PostStopSeconds,
nameof(PostStopSeconds));
if (CommandHoldSeconds > 30.0 ||
BaselineSeconds > 10.0 ||
PostStopSeconds > 10.0)
{
throw new ArgumentOutOfRangeException(
nameof(CommandHoldSeconds),
"辨识阶段时间超出测试允许范围。");
}
if (ControlIntervalMilliseconds < 20 ||
ControlIntervalMilliseconds > 200)
{
throw new ArgumentOutOfRangeException(
nameof(ControlIntervalMilliseconds),
"纵向辨识命令周期必须在20200ms之间。");
}
if (TrialNumber <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(TrialNumber),
"测试编号必须大于零。");
}
}
/// <summary>
/// 验证底盘运行时加载的速度、加速度和减速度配置。
/// </summary>
private void ValidateChassisAndTarget(
double targetSpeedMetersPerSecond)
{
NumericGuard.EnsureFinitePositive(
_chassis.MaxSpeed,
nameof(_chassis.MaxSpeed));
NumericGuard.EnsureFinitePositive(
_chassis.AccPerSecond,
nameof(_chassis.AccPerSecond));
NumericGuard.EnsureFinitePositive(
_chassis.DeAccPerSecond,
nameof(_chassis.DeAccPerSecond));
if (Math.Abs(targetSpeedMetersPerSecond) >
_chassis.MaxSpeed)
{
throw new ArgumentOutOfRangeException(
nameof(targetSpeedMetersPerSecond),
"目标速度超过底盘当前MaxSpeed配置。");
}
}
/// <summary>
/// 在收到停止请求时中断当前阶段并转入finally安全停车。
/// </summary>
private void ThrowIfStopRequested()
{
if (Volatile.Read(ref _stopRequested) != 0)
{
throw new OperationCanceledException();
}
}
}
/// <summary>
/// 测量速度阶跃、加速与稳态,并在保持结束后立即清零驱动速度。
/// </summary>
[MovementTest(name = "纵向辨识:速度阶跃与立即停车")]
public sealed class LongitudinalStepImmediateStopTest
: LongitudinalIdentificationTestBase
{
protected override bool UseConfiguredDeceleration =>
false;
protected override string StopModeName =>
"ImmediateStop";
}
/// <summary>
/// 测量速度阶跃、稳态以及由底盘DeAccPerSecond形成的正常减速过程。
/// </summary>
[MovementTest(name = "纵向辨识:速度阶跃与正常减速")]
public sealed class LongitudinalStepConfiguredDecelerationTest
: LongitudinalIdentificationTestBase
{
protected override bool UseConfiguredDeceleration =>
true;
protected override string StopModeName =>
"ConfiguredDeceleration";
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.