697 lines
26 KiB
C#
697 lines
26 KiB
C#
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("\"", "\"\"") +
|
|||
|
|
"\"";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|