Files
ParkingRobot/MultiWheelC/Experiments/DetourStaticDiagnosticTest.cs
T

697 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using ClumsyCore;
using ClumsyCore.Interfaces;
using ClumsyCore.Pilot;
using CommonUsage.Chassis;
using FundamentalLib;
using MyParking.Shared;
namespace MultiWheelC
{
/// <summary>
/// 从C层测试界面启动定时的Detour静态定位与轮组反馈诊断记录。
/// </summary>
[MovementTest(name = "诊断:Detour静态定位记录")]
public sealed class DetourStaticDiagnosticTest : MovementTest
{
private sealed class DiagnosticSample
{
public double ElapsedSeconds;
public string LocalTimestamp;
public bool DetourReadSucceeded;
public double DetourCallDurationMilliseconds;
public long? DetourTickRaw;
public string DetourTimestamp;
public bool DetourTimestampValid;
public double? DetourDataAgeMilliseconds;
public double? DetourLStep;
public double? DetourXMillimeters;
public double? DetourYMillimeters;
public double? DetourYawDegrees;
public double? LocalDeltaMilliseconds;
public double? DetourTickDeltaMilliseconds;
public double? DeltaXMillimeters;
public double? DeltaYMillimeters;
public double? DeltaYawDegrees;
public double? DeltaPositionMillimeters;
public bool IsRepeatedTick;
public bool IsRepeatedPose;
public bool IsOutOfOrderTick;
public bool WheelReadSucceeded;
public double? WheelBodyVxMetersPerSecond;
public double? WheelBodyVyMetersPerSecond;
public double? WheelBodyOmegaDegreesPerSecond;
public double? ActualSteerLeftFrontDegrees;
public double? ActualSteerLeftRearDegrees;
public double? ActualSteerRightFrontDegrees;
public double? ActualSteerRightRearDegrees;
public double? ActualSpeedLeftFrontMetersPerSecond;
public double? ActualSpeedLeftRearMetersPerSecond;
public double? ActualSpeedRightFrontMetersPerSecond;
public double? ActualSpeedRightRearMetersPerSecond;
public string FailureReason;
}
private readonly object _sampleSyncRoot = new object();
private readonly List<DiagnosticSample> _samples =
new List<DiagnosticSample>();
private readonly Stopwatch _clock = new Stopwatch();
private MultiWheelChassis _chassis;
private Thread _samplingThread;
private volatile bool _sampling;
private int _testRunning;
private int _stopRequested;
private int _sessionId;
private bool _hasPreviousDetourSample;
private double _previousElapsedSeconds;
private long _previousDetourTick;
private double _previousDetourXMillimeters;
private double _previousDetourYMillimeters;
private double _previousDetourYawDegrees;
/// <summary>
/// 获取或设置自动结束前的记录时长,单位为min。
/// </summary>
public double DurationMinutes = 20.0;
/// <summary>
/// 获取或设置本机主动读取Detour的周期,单位为ms。
/// </summary>
public int SampleIntervalMilliseconds = 50;
/// <summary>
/// 获取最近一次静态诊断CSV的完整路径。
/// </summary>
public string SavedFilePath { get; private set; } =
string.Empty;
/// <summary>
/// 停车后开始静态采样,并在到达设定时长时自动保存CSV。
/// </summary>
public override void Test()
{
if (Interlocked.CompareExchange(
ref _testRunning,
1,
0) != 0)
{
Console.WriteLine("Detour静态诊断已经在运行。");
return;
}
var samplingStarted = false;
var completedAutomatically = false;
Exception testFailure = null;
try
{
ValidateSettings();
_chassis =
PilotDefinition.Chassis as MultiWheelChassis;
if (_chassis == null)
{
throw new InvalidOperationException(
"当前底盘不是MultiWheelChassis,无法读取四轮反馈。");
}
var sessionId = ResetSession();
_chassis.PredefinedDriveStop();
_sampling = true;
_clock.Restart();
samplingStarted = true;
_samplingThread = new Thread(
() => SamplingLoop(sessionId))
{
IsBackground = true,
Name = "DetourStaticDiagnostic"
};
_samplingThread.Start();
Console.WriteLine(
$"Detour静态诊断开始:时长={DurationMinutes:F1}min" +
$"主动读取周期={SampleIntervalMilliseconds}ms" +
"车辆必须保持静止。");
Hedingben.ToastText(
$"Detour静态诊断开始,预计{DurationMinutes:F1}分钟后自动结束。");
var durationSeconds = DurationMinutes * 60.0;
while (Volatile.Read(ref _stopRequested) == 0 &&
_clock.Elapsed.TotalSeconds < durationSeconds)
{
Thread.Sleep(100);
}
completedAutomatically =
Volatile.Read(ref _stopRequested) == 0;
}
catch (Exception exception)
{
testFailure = exception;
Console.WriteLine(
"Detour静态诊断失败:" +
exception.Message);
}
finally
{
_sampling = false;
_chassis?.PredefinedDriveStop();
if (_samplingThread != null &&
_samplingThread != Thread.CurrentThread)
{
_samplingThread.Join(
Math.Max(
1000,
SampleIntervalMilliseconds * 4));
}
_clock.Stop();
if (samplingStarted)
{
try
{
SaveCsvAndReport(
completedAutomatically,
testFailure);
}
catch (Exception exception)
{
Console.WriteLine(
"Detour静态诊断CSV保存失败:" +
exception.Message);
Hedingben.ToastText(
"Detour静态诊断CSV保存失败:" +
exception.Message);
}
}
_samplingThread = null;
_chassis = null;
Interlocked.Exchange(ref _testRunning, 0);
}
}
/// <summary>
/// 请求提前停止采样;测试线程随后保存已经采集的数据。
/// </summary>
public override void TestStop()
{
Interlocked.Exchange(ref _stopRequested, 1);
_sampling = false;
_chassis?.PredefinedDriveStop();
}
/// <summary>
/// 清除上一次测试的样本、时间基准和输出路径。
/// </summary>
private int ResetSession()
{
lock (_sampleSyncRoot)
{
_samples.Clear();
}
Interlocked.Exchange(ref _stopRequested, 0);
_hasPreviousDetourSample = false;
_previousElapsedSeconds = 0.0;
_previousDetourTick = 0;
_previousDetourXMillimeters = 0.0;
_previousDetourYMillimeters = 0.0;
_previousDetourYawDegrees = 0.0;
SavedFilePath = string.Empty;
return Interlocked.Increment(ref _sessionId);
}
/// <summary>
/// 检查测试时长和主动采样周期是否适合执行。
/// </summary>
private void ValidateSettings()
{
NumericGuard.EnsureFinitePositive(
DurationMinutes,
nameof(DurationMinutes));
if (SampleIntervalMilliseconds < 20 ||
SampleIntervalMilliseconds > 5000)
{
throw new ArgumentOutOfRangeException(
nameof(SampleIntervalMilliseconds),
"Detour主动读取周期必须在20ms到5000ms之间。");
}
}
/// <summary>
/// 按设定周期持续采样,直至测试到时或收到停止请求。
/// </summary>
private void SamplingLoop(int sessionId)
{
while (_sampling &&
sessionId == Volatile.Read(ref _sessionId))
{
CaptureSample(sessionId);
Thread.Sleep(SampleIntervalMilliseconds);
}
}
/// <summary>
/// 采集一帧原始Detour定位、接口耗时和四轮实际反馈。
/// </summary>
private void CaptureSample(int sessionId)
{
var sample = new DiagnosticSample
{
ElapsedSeconds = _clock.Elapsed.TotalSeconds,
LocalTimestamp =
DateTimeOffset.Now.ToString(
"O",
CultureInfo.InvariantCulture),
FailureReason = string.Empty
};
CaptureDetour(sample, sessionId);
if (sessionId != Volatile.Read(ref _sessionId))
{
return;
}
CaptureWheelFeedback(sample);
if (!_sampling ||
sessionId != Volatile.Read(ref _sessionId))
{
return;
}
lock (_sampleSyncRoot)
{
_samples.Add(sample);
}
}
/// <summary>
/// 读取Detour原始字段并计算与上一成功读取之间的时间和位姿差。
/// </summary>
private void CaptureDetour(
DiagnosticSample sample,
int sessionId)
{
var callClock = Stopwatch.StartNew();
try
{
var location =
DetourInterface.getCartLocation();
callClock.Stop();
sample.DetourCallDurationMilliseconds =
callClock.Elapsed.TotalMilliseconds;
sample.DetourReadSucceeded = true;
sample.DetourTickRaw = Convert.ToInt64(
location.tick,
CultureInfo.InvariantCulture);
sample.DetourLStep = Convert.ToDouble(
location.l_step,
CultureInfo.InvariantCulture);
sample.DetourXMillimeters = Convert.ToDouble(
location.x,
CultureInfo.InvariantCulture);
sample.DetourYMillimeters = Convert.ToDouble(
location.y,
CultureInfo.InvariantCulture);
sample.DetourYawDegrees = Convert.ToDouble(
location.th,
CultureInfo.InvariantCulture);
if (sessionId != Volatile.Read(ref _sessionId))
{
return;
}
CaptureDetourTimestamp(sample);
CaptureDetourDelta(sample);
}
catch (Exception exception)
{
callClock.Stop();
sample.DetourCallDurationMilliseconds =
callClock.Elapsed.TotalMilliseconds;
AppendFailure(
sample,
"Detour读取失败:" +
exception.Message);
}
}
/// <summary>
/// 将Detour原始tick按.NET DateTime ticks解释并记录数据年龄。
/// </summary>
private static void CaptureDetourTimestamp(
DiagnosticSample sample)
{
try
{
var detourTime = new DateTime(
sample.DetourTickRaw.Value,
DateTimeKind.Local);
sample.DetourTimestamp =
detourTime.ToString(
"O",
CultureInfo.InvariantCulture);
sample.DetourDataAgeMilliseconds =
(DateTime.Now - detourTime)
.TotalMilliseconds;
sample.DetourTimestampValid = true;
}
catch (ArgumentOutOfRangeException)
{
sample.DetourTimestamp = string.Empty;
}
}
/// <summary>
/// 计算Detour帧间差并更新下一帧使用的原始基准。
/// </summary>
private void CaptureDetourDelta(
DiagnosticSample sample)
{
var tick = sample.DetourTickRaw.Value;
var xMillimeters = sample.DetourXMillimeters.Value;
var yMillimeters = sample.DetourYMillimeters.Value;
var yawDegrees = sample.DetourYawDegrees.Value;
if (_hasPreviousDetourSample)
{
sample.LocalDeltaMilliseconds =
(sample.ElapsedSeconds -
_previousElapsedSeconds) * 1000.0;
sample.DetourTickDeltaMilliseconds =
(tick - _previousDetourTick) /
(double)TimeSpan.TicksPerMillisecond;
sample.DeltaXMillimeters =
xMillimeters - _previousDetourXMillimeters;
sample.DeltaYMillimeters =
yMillimeters - _previousDetourYMillimeters;
sample.DeltaYawDegrees =
AngleMath.ShortestDifferenceDegrees(
yawDegrees,
_previousDetourYawDegrees);
sample.DeltaPositionMillimeters = Math.Sqrt(
sample.DeltaXMillimeters.Value *
sample.DeltaXMillimeters.Value +
sample.DeltaYMillimeters.Value *
sample.DeltaYMillimeters.Value);
sample.IsRepeatedTick =
tick == _previousDetourTick;
sample.IsOutOfOrderTick =
tick < _previousDetourTick;
sample.IsRepeatedPose =
sample.DeltaPositionMillimeters.Value <= 1e-6 &&
Math.Abs(sample.DeltaYawDegrees.Value) <= 1e-9;
}
_hasPreviousDetourSample = true;
_previousElapsedSeconds = sample.ElapsedSeconds;
_previousDetourTick = tick;
_previousDetourXMillimeters = xMillimeters;
_previousDetourYMillimeters = yMillimeters;
_previousDetourYawDegrees = yawDegrees;
}
/// <summary>
/// 读取底盘反算速度与按物理安装位置识别的四轮实际反馈。
/// </summary>
private void CaptureWheelFeedback(
DiagnosticSample sample)
{
try
{
var carSpeed = _chassis.GetCarSpeed(true);
sample.WheelBodyVxMetersPerSecond = carSpeed.Vx;
sample.WheelBodyVyMetersPerSecond = carSpeed.Vy;
// CommonUsage的CarSpeed.Vw以deg/s表达。
sample.WheelBodyOmegaDegreesPerSecond = carSpeed.Vw;
#pragma warning disable CS0612, CS0618
var wheels = _chassis.GetSteerWheels();
#pragma warning restore CS0612, CS0618
var leftFront = FindWheel(wheels, true, true);
var leftRear = FindWheel(wheels, false, true);
var rightFront = FindWheel(wheels, true, false);
var rightRear = FindWheel(wheels, false, false);
if (leftFront == null || leftRear == null ||
rightFront == null || rightRear == null)
{
throw new InvalidOperationException(
"未能按物理安装位置识别四个舵轮。");
}
sample.ActualSteerLeftFrontDegrees =
leftFront.ReadAngle();
sample.ActualSteerLeftRearDegrees =
leftRear.ReadAngle();
sample.ActualSteerRightFrontDegrees =
rightFront.ReadAngle();
sample.ActualSteerRightRearDegrees =
rightRear.ReadAngle();
sample.ActualSpeedLeftFrontMetersPerSecond =
leftFront.ReadSpeed();
sample.ActualSpeedLeftRearMetersPerSecond =
leftRear.ReadSpeed();
sample.ActualSpeedRightFrontMetersPerSecond =
rightFront.ReadSpeed();
sample.ActualSpeedRightRearMetersPerSecond =
rightRear.ReadSpeed();
sample.WheelReadSucceeded = true;
}
catch (Exception exception)
{
AppendFailure(
sample,
"四轮反馈读取失败:" +
exception.Message);
}
}
/// <summary>
/// 根据真实车体X向前、Y向左的物理安装位置查找指定舵轮。
/// </summary>
private static SteerWheel FindWheel(
IReadOnlyList<SteerWheel> wheels,
bool requireFront,
bool requireLeft)
{
foreach (var wheel in wheels)
{
var isFront = wheel.PhysicalPosition.X >= 0f;
var isLeft = wheel.PhysicalPosition.Y >= 0f;
if (isFront == requireFront &&
isLeft == requireLeft)
{
return wheel;
}
}
return null;
}
/// <summary>
/// 追加本帧诊断失败原因且保留先前错误信息。
/// </summary>
private static void AppendFailure(
DiagnosticSample sample,
string reason)
{
sample.FailureReason =
string.IsNullOrWhiteSpace(sample.FailureReason)
? reason
: sample.FailureReason + "" + reason;
}
/// <summary>
/// 保存采样快照并输出自动结束或手动停止后的摘要提示。
/// </summary>
private void SaveCsvAndReport(
bool completedAutomatically,
Exception testFailure)
{
List<DiagnosticSample> snapshot;
lock (_sampleSyncRoot)
{
snapshot =
new List<DiagnosticSample>(_samples);
}
var outputDirectory = Path.Combine(
AppContext.BaseDirectory,
"DetourStaticDiagnostics");
Directory.CreateDirectory(outputDirectory);
SavedFilePath = Path.Combine(
outputDirectory,
$"{DateTime.Now:yyyyMMdd_HHmmss_fff}_" +
"DetourStaticDiagnostic.csv");
using (var writer = new StreamWriter(
SavedFilePath,
false,
new UTF8Encoding(true)))
{
WriteCsvRow(writer,
"ElapsedSeconds", "LocalTimestamp",
"DetourReadSucceeded", "DetourCallDurationMilliseconds",
"DetourTickRaw", "DetourTimestamp",
"DetourTimestampValid", "DetourDataAgeMilliseconds",
"DetourLStep", "DetourXMillimeters",
"DetourYMillimeters", "DetourYawDegrees",
"LocalDeltaMilliseconds", "DetourTickDeltaMilliseconds",
"DeltaXMillimeters", "DeltaYMillimeters",
"DeltaYawDegrees", "DeltaPositionMillimeters",
"IsRepeatedTick", "IsRepeatedPose", "IsOutOfOrderTick",
"WheelReadSucceeded", "WheelBodyVxMetersPerSecond",
"WheelBodyVyMetersPerSecond",
"WheelBodyOmegaDegreesPerSecond",
"ActualSteerLeftFrontDegrees",
"ActualSteerLeftRearDegrees",
"ActualSteerRightFrontDegrees",
"ActualSteerRightRearDegrees",
"ActualSpeedLeftFrontMetersPerSecond",
"ActualSpeedLeftRearMetersPerSecond",
"ActualSpeedRightFrontMetersPerSecond",
"ActualSpeedRightRearMetersPerSecond",
"FailureReason");
foreach (var sample in snapshot)
{
WriteCsvRow(writer,
sample.ElapsedSeconds, sample.LocalTimestamp,
sample.DetourReadSucceeded,
sample.DetourCallDurationMilliseconds,
sample.DetourTickRaw, sample.DetourTimestamp,
sample.DetourTimestampValid,
sample.DetourDataAgeMilliseconds,
sample.DetourLStep, sample.DetourXMillimeters,
sample.DetourYMillimeters, sample.DetourYawDegrees,
sample.LocalDeltaMilliseconds,
sample.DetourTickDeltaMilliseconds,
sample.DeltaXMillimeters, sample.DeltaYMillimeters,
sample.DeltaYawDegrees,
sample.DeltaPositionMillimeters,
sample.IsRepeatedTick, sample.IsRepeatedPose,
sample.IsOutOfOrderTick,
sample.WheelReadSucceeded,
sample.WheelBodyVxMetersPerSecond,
sample.WheelBodyVyMetersPerSecond,
sample.WheelBodyOmegaDegreesPerSecond,
sample.ActualSteerLeftFrontDegrees,
sample.ActualSteerLeftRearDegrees,
sample.ActualSteerRightFrontDegrees,
sample.ActualSteerRightRearDegrees,
sample.ActualSpeedLeftFrontMetersPerSecond,
sample.ActualSpeedLeftRearMetersPerSecond,
sample.ActualSpeedRightFrontMetersPerSecond,
sample.ActualSpeedRightRearMetersPerSecond,
sample.FailureReason);
}
}
var successfulSamples = 0;
var maximumPositionStepMillimeters = 0.0;
var maximumAbsoluteYawStepDegrees = 0.0;
foreach (var sample in snapshot)
{
if (sample.DetourReadSucceeded)
{
successfulSamples++;
}
maximumPositionStepMillimeters = Math.Max(
maximumPositionStepMillimeters,
sample.DeltaPositionMillimeters ?? 0.0);
maximumAbsoluteYawStepDegrees = Math.Max(
maximumAbsoluteYawStepDegrees,
Math.Abs(sample.DeltaYawDegrees ?? 0.0));
}
var completionReason = testFailure != null
? "因异常提前结束"
: completedAutomatically
? "到达设定时长,已自动结束"
: "收到手动停止请求";
var message =
$"Detour静态诊断{completionReason}" +
$"样本={snapshot.Count},有效Detour样本={successfulSamples}" +
$"最大位置阶跃={maximumPositionStepMillimeters:F2}mm" +
$"最大航向阶跃={maximumAbsoluteYawStepDegrees:F3}°;" +
$"CSV={SavedFilePath}";
Console.WriteLine(message);
Hedingben.ToastText(message);
}
/// <summary>
/// 使用InvariantCulture格式化并转义一行CSV字段。
/// </summary>
private static void WriteCsvRow(
TextWriter writer,
params object[] values)
{
var fields = new string[values.Length];
for (var index = 0; index < values.Length; index++)
{
fields[index] = FormatCsvValue(values[index]);
}
writer.WriteLine(string.Join(",", fields));
}
/// <summary>
/// 将单个值转换为区域无关且符合CSV转义规则的文本。
/// </summary>
private static string FormatCsvValue(object value)
{
if (value == null)
{
return string.Empty;
}
string text;
if (value is bool boolean)
{
text = boolean ? "1" : "0";
}
else if (value is IFormattable formattable)
{
text = formattable.ToString(
null,
CultureInfo.InvariantCulture);
}
else
{
text = value.ToString();
}
if (text.IndexOfAny(
new[] { ',', '"', '\r', '\n' }) < 0)
{
return text;
}
return "\"" +
text.Replace("\"", "\"\"") +
"\"";
}
}
}