完善蟹行虚拟阿克曼与SendMotion运动坐标系,并添加轮速诊断日志
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -70,6 +70,9 @@ namespace MedullaAdapter
|
||||
[AsInitParam(desc = "手动控制夹臂速度系数")] public float ManualArmSpeedFac = 1.0f;
|
||||
[AsInitParam(desc = "遥控转弯舵角同步限速宽度,单位为度")]
|
||||
public float ManualSteeringAlignmentSigmaDegrees = 8.0f;
|
||||
[AsInitParam(desc = "轮速诊断日志相对目录")]
|
||||
public string WheelSpeedDiagnosticDirectory =
|
||||
@"logs\wheel-speed";
|
||||
[AsInitParam(desc = "左夹臂低限位")][AsLowerIO] public int LeftArmLowerPos = -10000;
|
||||
[AsInitParam(desc = "左夹臂高限位")][AsLowerIO] public int LeftArmUpperPos = 5927610;
|
||||
[AsInitParam(desc = "右夹臂低限位")][AsLowerIO] public int RightArmLowerPos = -17295;
|
||||
@@ -91,6 +94,10 @@ namespace MedullaAdapter
|
||||
[IOObjectMonitor(desc = "右后右轮PID修正后速度")] public float SpeedRRR;
|
||||
[IOObjectMonitor(desc = "灯光模式")] public int LightMode = 0;
|
||||
[IOObjectMonitor(desc = "实体遥控器当前速度倍率")] public float TransmitterSpeed = 0.3f;
|
||||
[IOObjectMonitor(desc = "轮速诊断记录已启用")]
|
||||
public bool WheelSpeedDiagnosticEnabled;
|
||||
[IOObjectMonitor(desc = "轮速诊断记录状态")]
|
||||
public string WheelSpeedDiagnosticStatus = "未启动";
|
||||
[IOObjectMonitor(desc = "左前左驱动器远程帧701")] public byte LFLRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "左前右驱动器远程帧702")] public byte LFRRemoteCode = 0;
|
||||
[IOObjectMonitor(desc = "右前左驱动器远程帧703")] public byte RFLRemoteCode = 0;
|
||||
@@ -116,6 +123,22 @@ namespace MedullaAdapter
|
||||
{
|
||||
DisableFromM = true;
|
||||
}
|
||||
|
||||
// M层诊断:请求开始保存CAN轮速事件和底盘周期快照。
|
||||
[IOObjectUtility]
|
||||
public void StartWheelSpeedDiagnostic()
|
||||
{
|
||||
WheelSpeedDiagnosticEnabled = true;
|
||||
WheelSpeedDiagnosticStatus = "等待创建日志文件";
|
||||
}
|
||||
|
||||
// M层诊断:请求停止轮速记录并刷新CSV文件。
|
||||
[IOObjectUtility]
|
||||
public void StopWheelSpeedDiagnostic()
|
||||
{
|
||||
WheelSpeedDiagnosticEnabled = false;
|
||||
WheelSpeedDiagnosticStatus = "等待停止并刷新日志";
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override void CommunicationInit()
|
||||
@@ -234,10 +257,8 @@ namespace MedullaAdapter
|
||||
Math.Sign(x);
|
||||
var steeringDegrees =
|
||||
-normalizedSteering * MaxManualTheta;
|
||||
var omega = CalculateManualOmega(
|
||||
speed,
|
||||
steeringDegrees,
|
||||
adapter.HalfWheelBaseMeters);
|
||||
var frontTh = steeringDegrees;
|
||||
var rearTh = -steeringDegrees;
|
||||
ManualMode = (int)mode;
|
||||
|
||||
switch (mode)
|
||||
@@ -245,27 +266,96 @@ namespace MedullaAdapter
|
||||
case ManualControlMode.Normal:
|
||||
// 普通模式统一使用车体速度命令:
|
||||
// X向前,行驶中连续改变角速度时舵轮边转、车辆边走。
|
||||
SendBodyCommand(
|
||||
vx: speed,
|
||||
vy: 0.0,
|
||||
omegaRadiansPerSecond: omega,
|
||||
interval);
|
||||
// SendBodyCommand(
|
||||
// vx: speed,
|
||||
// vy: 0.0,
|
||||
// omegaRadiansPerSecond: omega,
|
||||
// interval);
|
||||
Chassis.SendMotion(
|
||||
speed,
|
||||
frontTh,
|
||||
rearTh,
|
||||
interval);
|
||||
break;
|
||||
case ManualControlMode.Crab:
|
||||
SendBodyCommand(vx: 0.0, vy: speed, omegaRadiansPerSecond: omega, interval);
|
||||
// 舵轮机械范围为[-120°,120°]。
|
||||
// 蟹行后虚拟轴距由原车宽度决定,比正常模式轴距短。
|
||||
// 按几何比例缩小转角,使相同摇杆输入获得接近一致的曲率。
|
||||
var normalSteeringRadians =
|
||||
steeringDegrees *
|
||||
Math.PI / 180.0;
|
||||
var geometryRatio =
|
||||
adapter.HalfTrackWidthMeters /
|
||||
adapter.HalfWheelBaseMeters;
|
||||
|
||||
// +90°运动坐标系已经把虚拟左侧映射为车体后方,
|
||||
// 此处保持普通模式的转向符号,避免再次取反导致左右颠倒。
|
||||
var crabSteeringRadians =
|
||||
Math.Atan(
|
||||
geometryRatio *
|
||||
Math.Tan(
|
||||
normalSteeringRadians));
|
||||
|
||||
// 蟹行转角最终限制为±30°,为±120°机械舵角保留余量。
|
||||
var maximumCrabSteeringRadians =
|
||||
30.0 * Math.PI / 180.0;
|
||||
crabSteeringRadians = Math.Max(
|
||||
-maximumCrabSteeringRadians,
|
||||
Math.Min(
|
||||
maximumCrabSteeringRadians,
|
||||
crabSteeringRadians));
|
||||
|
||||
// 将车体左侧作为虚拟阿克曼车头,并在该运动坐标系中
|
||||
// 复用与普通模式相同的SendMotion前后控制点解算。
|
||||
if (!adapter.SendVirtualAckermannMotion(
|
||||
motionDirectionRadians:
|
||||
Math.PI / 2.0,
|
||||
speedMetersPerSecond:
|
||||
speed,
|
||||
steeringRadians:
|
||||
crabSteeringRadians,
|
||||
interval))
|
||||
{
|
||||
adapter.StopImmediately();
|
||||
|
||||
Console.WriteLine(
|
||||
"蟹行SendMotion命令分解失败,车辆已经停车:" +
|
||||
adapter.LastFailureReason);
|
||||
}
|
||||
break;
|
||||
case ManualControlMode.Spin:
|
||||
// 自转时speed表示最外侧舵轮的目标切向速度。
|
||||
// 根据v=omega*r换算角速度,不能把m/s与deg/s直接相乘。
|
||||
var spinOmega =
|
||||
// 摇杆处于中位时只清零驱动速度,保持已经准备好的
|
||||
// 自转舵角;下次推动摇杆时仍会重新检查实际舵角。
|
||||
if (Math.Abs(speed) < 1e-6f)
|
||||
{
|
||||
adapter
|
||||
.StopXYThDrivePreserveSteeringState();
|
||||
break;
|
||||
}
|
||||
|
||||
// 自转时speed表示最外侧舵轮中心的目标切向速度,
|
||||
// 根据v=omega*r换算为SendXYThSpeed需要的角速度。
|
||||
var spinOmegaRadiansPerSecond =
|
||||
speed /
|
||||
adapter.MaximumWheelRadiusMeters;
|
||||
|
||||
SendBodyCommand(
|
||||
vx: 0.0,
|
||||
vy: 0.0,
|
||||
omegaRadiansPerSecond: spinOmega,
|
||||
interval);
|
||||
// 普通安全版SendXYThSpeed只下发角速度,
|
||||
// 四轮实际舵角未到位时不会开放驱动速度。
|
||||
if (!adapter.Send(
|
||||
new ChassisCommand(
|
||||
CarNum,
|
||||
new Twist2D(
|
||||
0.0,
|
||||
0.0,
|
||||
spinOmegaRadiansPerSecond)),
|
||||
interval))
|
||||
{
|
||||
adapter.StopImmediately();
|
||||
|
||||
Console.WriteLine(
|
||||
"SendXYThSpeed原地自转命令分解失败,车辆已经停车:" +
|
||||
adapter.LastFailureReason);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ManualMode = -1;
|
||||
@@ -294,6 +384,10 @@ namespace MedullaAdapter
|
||||
if (_pendingManualMode != mode)
|
||||
{
|
||||
adapter.StopImmediately();
|
||||
// 所有模式的准备角度均按真实机械舵角表达;
|
||||
// 先退出上一模式的虚拟运动坐标系,再执行预对齐。
|
||||
adapter.ResetToBodyFrame();
|
||||
_activeManualMode = null;
|
||||
|
||||
var preparationAccepted = mode switch
|
||||
{
|
||||
@@ -354,24 +448,33 @@ namespace MedullaAdapter
|
||||
if (!aligned)
|
||||
return false;
|
||||
|
||||
if (mode == ManualControlMode.Spin)
|
||||
{
|
||||
// 四轮实际舵角确认到位后只交接一次,保留PrepareSpin
|
||||
// 选定的机械舵角和轮速方向,避免首条XYTh命令重新选角。
|
||||
if (!adapter.AdoptPreparedSpinForXYTh(
|
||||
toleranceRadians))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 蟹行轮子在真实车体系中到达机械+90°后,
|
||||
// 再将车体左侧激活为SendMotion的虚拟X正方向。
|
||||
else if (mode == ManualControlMode.Crab)
|
||||
{
|
||||
adapter.ActivateMotionFrame(
|
||||
Math.PI / 2.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
adapter.ResetToBodyFrame();
|
||||
}
|
||||
|
||||
_activeManualMode = mode;
|
||||
_pendingManualMode = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private double CalculateManualOmega(
|
||||
float speed,
|
||||
float steeringDegrees,
|
||||
double halfWheelBaseMeters)
|
||||
{
|
||||
var steeringRadians =
|
||||
steeringDegrees * Math.PI / 180.0;
|
||||
|
||||
return speed *
|
||||
Math.Tan(steeringRadians) /
|
||||
Math.Max(halfWheelBaseMeters, 0.01);
|
||||
}
|
||||
|
||||
internal void SendBodyCommand(double vx, double vy, double omegaRadiansPerSecond, TimeSpan? interval = null)
|
||||
{
|
||||
var adapter = GetChassisAdapter();
|
||||
|
||||
@@ -4,6 +4,7 @@ using FundamentalLib;
|
||||
using MCUSerialBridgeCLR;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace MedullaAdapter
|
||||
{
|
||||
@@ -26,6 +27,9 @@ namespace MedullaAdapter
|
||||
private bool io_bit4 = false;//黄灯
|
||||
private const byte BatteryPortIndex = 3;
|
||||
private static readonly byte[] BatteryRequest = BuildBatteryRequest();
|
||||
private readonly WheelSpeedDiagnosticLogger
|
||||
_wheelSpeedLogger =
|
||||
new WheelSpeedDiagnosticLogger();
|
||||
|
||||
// M层单车底盘:将车轮线速度换算为驱动电机转速。
|
||||
private static float ConvertMps2Rpm(float mps)
|
||||
@@ -50,6 +54,8 @@ namespace MedullaAdapter
|
||||
// M层硬件主循环:交换IO、发送轮组指令并更新车辆反馈状态。
|
||||
public override void Operation(int iteration)
|
||||
{
|
||||
UpdateWheelSpeedDiagnosticState();
|
||||
|
||||
if (_lastIteration != iteration)
|
||||
{
|
||||
_lastIteration = iteration;
|
||||
@@ -158,6 +164,7 @@ namespace MedullaAdapter
|
||||
cart.ActualSpeedLeftRear = (cart.ActualSpeedLeftRearLeft + cart.ActualSpeedLeftRearRight) / 2;
|
||||
cart.ActualSpeedRightFront = (cart.ActualSpeedRightFrontLeft + cart.ActualSpeedRightFrontRight) / 2;
|
||||
cart.ActualSpeedRightRear = (cart.ActualSpeedRightRearLeft + cart.ActualSpeedRightRearRight) / 2;
|
||||
_wheelSpeedLogger.RecordSnapshot(cart);
|
||||
// M层CAN辅助:封装本周期驱动器CAN发送参数。
|
||||
MCUSerialBridgeError SendCan(byte port, ushort standardId, byte[] payload, bool RTR = false, uint timeout = 2)
|
||||
{
|
||||
@@ -264,8 +271,9 @@ namespace MedullaAdapter
|
||||
{
|
||||
_operationTime++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 没有错误 没有节点保护 就发06 07 0F使能
|
||||
else if (_operationTime == 4)
|
||||
{
|
||||
@@ -428,6 +436,79 @@ namespace MedullaAdapter
|
||||
|
||||
}
|
||||
|
||||
// M层诊断:根据界面开关创建或关闭本次轮速CSV记录。
|
||||
private void UpdateWheelSpeedDiagnosticState()
|
||||
{
|
||||
if (cart == null)
|
||||
return;
|
||||
|
||||
if (cart.WheelSpeedDiagnosticEnabled)
|
||||
{
|
||||
if (_wheelSpeedLogger.IsRunning)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var configuredDirectory =
|
||||
string.IsNullOrWhiteSpace(
|
||||
cart.WheelSpeedDiagnosticDirectory)
|
||||
? @"logs\wheel-speed"
|
||||
: cart.WheelSpeedDiagnosticDirectory;
|
||||
|
||||
var logDirectory =
|
||||
Path.IsPathRooted(configuredDirectory)
|
||||
? configuredDirectory
|
||||
: Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
configuredDirectory);
|
||||
|
||||
_wheelSpeedLogger.Start(
|
||||
logDirectory,
|
||||
cart.CarNum);
|
||||
|
||||
cart.WheelSpeedDiagnosticStatus =
|
||||
"记录中:" +
|
||||
_wheelSpeedLogger.SnapshotLogPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
cart.WheelSpeedDiagnosticEnabled =
|
||||
false;
|
||||
cart.WheelSpeedDiagnosticStatus =
|
||||
"启动失败:" + ex.Message;
|
||||
|
||||
Console.WriteLine(
|
||||
"轮速诊断启动失败:" +
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_wheelSpeedLogger.IsRunning)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var snapshotPath =
|
||||
_wheelSpeedLogger.SnapshotLogPath;
|
||||
|
||||
_wheelSpeedLogger.Stop();
|
||||
|
||||
cart.WheelSpeedDiagnosticStatus =
|
||||
"已保存:" + snapshotPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
cart.WheelSpeedDiagnosticStatus =
|
||||
"停止失败:" + ex.Message;
|
||||
|
||||
Console.WriteLine(
|
||||
"轮速诊断停止失败:" +
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// M层CAN安全:判断单个驱动节点是否处于可运行状态。
|
||||
private static bool IsNodeOperational(byte remoteCode)
|
||||
{
|
||||
@@ -601,7 +682,13 @@ namespace MedullaAdapter
|
||||
var payload = msg.Payload;
|
||||
if (payload == null || payload.Length < 8) return;
|
||||
var rpm = DecodeRpmFromPayload(payload);
|
||||
cart.ActualSpeedLeftFrontLeft = ConvertRpm2Mps(rpm);
|
||||
var speed = ConvertRpm2Mps(rpm);
|
||||
cart.ActualSpeedLeftFrontLeft = speed;
|
||||
_wheelSpeedLogger.RecordCanFeedback(
|
||||
0x281,
|
||||
"LFL",
|
||||
rpm,
|
||||
speed);
|
||||
cart.LFLActualPos = ConvertR2MM(BitConverter.ToInt32(payload, 0) / 10000f);
|
||||
},
|
||||
[0x282] = (msg) =>
|
||||
@@ -610,7 +697,13 @@ namespace MedullaAdapter
|
||||
var payload = msg.Payload;
|
||||
if (payload == null || payload.Length < 8) return;
|
||||
var rpm = DecodeRpmFromPayload(payload);
|
||||
cart.ActualSpeedLeftFrontRight = -ConvertRpm2Mps(rpm);
|
||||
var speed = -ConvertRpm2Mps(rpm);
|
||||
cart.ActualSpeedLeftFrontRight = speed;
|
||||
_wheelSpeedLogger.RecordCanFeedback(
|
||||
0x282,
|
||||
"LFR",
|
||||
rpm,
|
||||
speed);
|
||||
cart.LFRActualPos = ConvertR2MM(-BitConverter.ToInt32(payload, 0) / 10000f);
|
||||
},
|
||||
[0x283] = (msg) =>
|
||||
@@ -619,7 +712,13 @@ namespace MedullaAdapter
|
||||
var payload = msg.Payload;
|
||||
if (payload == null || payload.Length < 8) return;
|
||||
var rpm = DecodeRpmFromPayload(payload);
|
||||
cart.ActualSpeedRightFrontLeft = ConvertRpm2Mps(rpm);
|
||||
var speed = ConvertRpm2Mps(rpm);
|
||||
cart.ActualSpeedRightFrontLeft = speed;
|
||||
_wheelSpeedLogger.RecordCanFeedback(
|
||||
0x283,
|
||||
"RFL",
|
||||
rpm,
|
||||
speed);
|
||||
cart.RFLActualPos = ConvertR2MM(BitConverter.ToInt32(payload, 0) / 10000f);
|
||||
},
|
||||
[0x284] = (msg) =>
|
||||
@@ -628,7 +727,13 @@ namespace MedullaAdapter
|
||||
var payload = msg.Payload;
|
||||
if (payload == null || payload.Length < 8) return;
|
||||
var rpm = DecodeRpmFromPayload(payload);
|
||||
cart.ActualSpeedRightFrontRight = -ConvertRpm2Mps(rpm);
|
||||
var speed = -ConvertRpm2Mps(rpm);
|
||||
cart.ActualSpeedRightFrontRight = speed;
|
||||
_wheelSpeedLogger.RecordCanFeedback(
|
||||
0x284,
|
||||
"RFR",
|
||||
rpm,
|
||||
speed);
|
||||
cart.RFRActualPos = ConvertR2MM(-BitConverter.ToInt32(payload, 0) / 10000f);
|
||||
},
|
||||
[0x285] = (msg) =>
|
||||
@@ -637,7 +742,13 @@ namespace MedullaAdapter
|
||||
var payload = msg.Payload;
|
||||
if (payload == null || payload.Length < 8) return;
|
||||
var rpm = DecodeRpmFromPayload(payload);
|
||||
cart.ActualSpeedLeftRearLeft = ConvertRpm2Mps(rpm);
|
||||
var speed = ConvertRpm2Mps(rpm);
|
||||
cart.ActualSpeedLeftRearLeft = speed;
|
||||
_wheelSpeedLogger.RecordCanFeedback(
|
||||
0x285,
|
||||
"LRL",
|
||||
rpm,
|
||||
speed);
|
||||
cart.LRLActualPos = ConvertR2MM(BitConverter.ToInt32(payload, 0) / 10000f);
|
||||
},
|
||||
[0x286] = (msg) =>
|
||||
@@ -646,7 +757,13 @@ namespace MedullaAdapter
|
||||
var payload = msg.Payload;
|
||||
if (payload == null || payload.Length < 8) return;
|
||||
var rpm = DecodeRpmFromPayload(payload);
|
||||
cart.ActualSpeedLeftRearRight = -ConvertRpm2Mps(rpm);
|
||||
var speed = -ConvertRpm2Mps(rpm);
|
||||
cart.ActualSpeedLeftRearRight = speed;
|
||||
_wheelSpeedLogger.RecordCanFeedback(
|
||||
0x286,
|
||||
"LRR",
|
||||
rpm,
|
||||
speed);
|
||||
cart.LRRActualPos = ConvertR2MM(-BitConverter.ToInt32(payload, 0) / 10000f);
|
||||
},
|
||||
[0x287] = (msg) =>
|
||||
@@ -655,7 +772,13 @@ namespace MedullaAdapter
|
||||
var payload = msg.Payload;
|
||||
if (payload == null || payload.Length < 8) return;
|
||||
var rpm = DecodeRpmFromPayload(payload);
|
||||
cart.ActualSpeedRightRearLeft = ConvertRpm2Mps(rpm);
|
||||
var speed = ConvertRpm2Mps(rpm);
|
||||
cart.ActualSpeedRightRearLeft = speed;
|
||||
_wheelSpeedLogger.RecordCanFeedback(
|
||||
0x287,
|
||||
"RRL",
|
||||
rpm,
|
||||
speed);
|
||||
cart.RRLActualPos = ConvertR2MM(BitConverter.ToInt32(payload, 0) / 10000f);
|
||||
},
|
||||
[0x288] = (msg) =>
|
||||
@@ -664,7 +787,13 @@ namespace MedullaAdapter
|
||||
var payload = msg.Payload;
|
||||
if (payload == null || payload.Length < 8) return;
|
||||
var rpm = DecodeRpmFromPayload(payload);
|
||||
cart.ActualSpeedRightRearRight = -ConvertRpm2Mps(rpm);
|
||||
var speed = -ConvertRpm2Mps(rpm);
|
||||
cart.ActualSpeedRightRearRight = speed;
|
||||
_wheelSpeedLogger.RecordCanFeedback(
|
||||
0x288,
|
||||
"RRR",
|
||||
rpm,
|
||||
speed);
|
||||
cart.RRRActualPos = ConvertR2MM(-BitConverter.ToInt32(payload, 0) / 10000f);
|
||||
},
|
||||
[0x289] = (msg) =>
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace MedullaAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// 在后台保存驱动器CAN速度事件和底盘周期快照,避免文件IO阻塞CAN回调。
|
||||
/// </summary>
|
||||
internal sealed class WheelSpeedDiagnosticLogger : IDisposable
|
||||
{
|
||||
private readonly struct LogRecord
|
||||
{
|
||||
public LogRecord(bool isCanEvent, string line)
|
||||
{
|
||||
IsCanEvent = isCanEvent;
|
||||
Line = line;
|
||||
}
|
||||
|
||||
public bool IsCanEvent { get; }
|
||||
|
||||
public string Line { get; }
|
||||
}
|
||||
|
||||
private const int MaximumQueuedRecords = 100000;
|
||||
private const double SnapshotIntervalMilliseconds = 20.0;
|
||||
private readonly ConcurrentQueue<LogRecord> _records = new();
|
||||
private readonly AutoResetEvent _recordsAvailable = new(false);
|
||||
private readonly object _lifecycleLock = new();
|
||||
private Stopwatch _stopwatch;
|
||||
private Thread _writerThread;
|
||||
private StreamWriter _canWriter;
|
||||
private StreamWriter _snapshotWriter;
|
||||
private volatile bool _isRunning;
|
||||
private int _queuedRecordCount;
|
||||
private long _receiveSequence;
|
||||
private long _droppedRecordCount;
|
||||
private double _lastSnapshotMilliseconds = double.NegativeInfinity;
|
||||
|
||||
public bool IsRunning => _isRunning;
|
||||
|
||||
public string CanLogPath { get; private set; } = "";
|
||||
|
||||
public string SnapshotLogPath { get; private set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 创建本次诊断的两个CSV文件并启动后台写入线程。
|
||||
/// </summary>
|
||||
public void Start(string directory, int carNumber)
|
||||
{
|
||||
lock (_lifecycleLock)
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
throw new ArgumentException(
|
||||
"轮速诊断目录不能为空。",
|
||||
nameof(directory));
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var filePrefix =
|
||||
$"{DateTime.Now:yyyyMMdd_HHmmss_fff}_Car{carNumber}";
|
||||
|
||||
CanLogPath = Path.Combine(
|
||||
directory,
|
||||
$"{filePrefix}_can.csv");
|
||||
|
||||
SnapshotLogPath = Path.Combine(
|
||||
directory,
|
||||
$"{filePrefix}_snapshot.csv");
|
||||
|
||||
_canWriter = CreateWriter(CanLogPath);
|
||||
_snapshotWriter = CreateWriter(SnapshotLogPath);
|
||||
|
||||
_canWriter.WriteLine(
|
||||
"ElapsedMs,ReceiveSequence,CanId,MotorName,RawRpm,SpeedMps");
|
||||
|
||||
_snapshotWriter.WriteLine(
|
||||
"ElapsedMs,CarNum,ManualMode,SendThresSpeed," +
|
||||
"CmdLFL,CmdLFR,CmdLRL,CmdLRR,CmdRFL,CmdRFR,CmdRRL,CmdRRR," +
|
||||
"PidLFL,PidLFR,PidLRL,PidLRR,PidRFL,PidRFR,PidRRL,PidRRR," +
|
||||
"ActualLFL,ActualLFR,ActualLRL,ActualLRR,ActualRFL,ActualRFR,ActualRRL,ActualRRR," +
|
||||
"ActualLeftFront,ActualLeftRear,ActualRightFront,ActualRightRear," +
|
||||
"TargetThLeftFront,TargetThLeftRear,TargetThRightFront,TargetThRightRear," +
|
||||
"ActualThLeftFront,ActualThLeftRear,ActualThRightFront,ActualThRightRear");
|
||||
|
||||
while (_records.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
|
||||
_queuedRecordCount = 0;
|
||||
_receiveSequence = 0;
|
||||
_droppedRecordCount = 0;
|
||||
_lastSnapshotMilliseconds =
|
||||
double.NegativeInfinity;
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
_isRunning = true;
|
||||
|
||||
_writerThread = new Thread(WriterLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "WheelSpeedDiagnosticWriter"
|
||||
};
|
||||
_writerThread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止记录并等待队列中的诊断数据写入磁盘。
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
Thread writerThread;
|
||||
|
||||
lock (_lifecycleLock)
|
||||
{
|
||||
if (!_isRunning &&
|
||||
_writerThread == null)
|
||||
return;
|
||||
|
||||
_isRunning = false;
|
||||
writerThread = _writerThread;
|
||||
_recordsAvailable.Set();
|
||||
}
|
||||
|
||||
writerThread?.Join(3000);
|
||||
|
||||
lock (_lifecycleLock)
|
||||
{
|
||||
_canWriter?.Flush();
|
||||
_snapshotWriter?.Flush();
|
||||
_canWriter?.Dispose();
|
||||
_snapshotWriter?.Dispose();
|
||||
_canWriter = null;
|
||||
_snapshotWriter = null;
|
||||
_writerThread = null;
|
||||
_stopwatch?.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一帧驱动器速度反馈加入内存队列,不在CAN回调中执行文件写入。
|
||||
/// </summary>
|
||||
public void RecordCanFeedback(
|
||||
ushort canId,
|
||||
string motorName,
|
||||
float rawRpm,
|
||||
float speedMetersPerSecond)
|
||||
{
|
||||
if (!_isRunning)
|
||||
return;
|
||||
|
||||
var elapsedMilliseconds =
|
||||
_stopwatch.Elapsed.TotalMilliseconds;
|
||||
var receiveSequence =
|
||||
Interlocked.Increment(
|
||||
ref _receiveSequence);
|
||||
|
||||
var line = string.Join(
|
||||
",",
|
||||
Format(elapsedMilliseconds),
|
||||
receiveSequence.ToString(
|
||||
CultureInfo.InvariantCulture),
|
||||
$"0x{canId:X3}",
|
||||
motorName,
|
||||
Format(rawRpm),
|
||||
Format(speedMetersPerSecond));
|
||||
|
||||
Enqueue(new LogRecord(
|
||||
isCanEvent: true,
|
||||
line));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按最多50Hz记录一帧控制命令、PID输出、CAN反馈和舵角快照。
|
||||
/// </summary>
|
||||
public void RecordSnapshot(
|
||||
DiverCartDefinition cart)
|
||||
{
|
||||
if (!_isRunning || cart == null)
|
||||
return;
|
||||
|
||||
var elapsedMilliseconds =
|
||||
_stopwatch.Elapsed.TotalMilliseconds;
|
||||
|
||||
if (elapsedMilliseconds -
|
||||
_lastSnapshotMilliseconds <
|
||||
SnapshotIntervalMilliseconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSnapshotMilliseconds =
|
||||
elapsedMilliseconds;
|
||||
|
||||
var line = string.Join(
|
||||
",",
|
||||
Format(elapsedMilliseconds),
|
||||
cart.CarNum.ToString(
|
||||
CultureInfo.InvariantCulture),
|
||||
Format(cart.ManualMode),
|
||||
Format(cart.SendThresSpeed),
|
||||
Format(cart.SpeedLeftFrontLeft),
|
||||
Format(cart.SpeedLeftFrontRight),
|
||||
Format(cart.SpeedLeftRearLeft),
|
||||
Format(cart.SpeedLeftRearRight),
|
||||
Format(cart.SpeedRightFrontLeft),
|
||||
Format(cart.SpeedRightFrontRight),
|
||||
Format(cart.SpeedRightRearLeft),
|
||||
Format(cart.SpeedRightRearRight),
|
||||
Format(cart.SpeedLFL),
|
||||
Format(cart.SpeedLFR),
|
||||
Format(cart.SpeedLRL),
|
||||
Format(cart.SpeedLRR),
|
||||
Format(cart.SpeedRFL),
|
||||
Format(cart.SpeedRFR),
|
||||
Format(cart.SpeedRRL),
|
||||
Format(cart.SpeedRRR),
|
||||
Format(cart.ActualSpeedLeftFrontLeft),
|
||||
Format(cart.ActualSpeedLeftFrontRight),
|
||||
Format(cart.ActualSpeedLeftRearLeft),
|
||||
Format(cart.ActualSpeedLeftRearRight),
|
||||
Format(cart.ActualSpeedRightFrontLeft),
|
||||
Format(cart.ActualSpeedRightFrontRight),
|
||||
Format(cart.ActualSpeedRightRearLeft),
|
||||
Format(cart.ActualSpeedRightRearRight),
|
||||
Format(cart.ActualSpeedLeftFront),
|
||||
Format(cart.ActualSpeedLeftRear),
|
||||
Format(cart.ActualSpeedRightFront),
|
||||
Format(cart.ActualSpeedRightRear),
|
||||
Format(cart.ThLeftFront),
|
||||
Format(cart.ThLeftRear),
|
||||
Format(cart.ThRightFront),
|
||||
Format(cart.ThRightRear),
|
||||
Format(cart.ActualThLeftFront),
|
||||
Format(cart.ActualThLeftRear),
|
||||
Format(cart.ActualThRightFront),
|
||||
Format(cart.ActualThRightRear));
|
||||
|
||||
Enqueue(new LogRecord(
|
||||
isCanEvent: false,
|
||||
line));
|
||||
}
|
||||
|
||||
private static StreamWriter CreateWriter(
|
||||
string path)
|
||||
{
|
||||
return new StreamWriter(
|
||||
path,
|
||||
append: false,
|
||||
new UTF8Encoding(
|
||||
encoderShouldEmitUTF8Identifier: true),
|
||||
bufferSize: 64 * 1024);
|
||||
}
|
||||
|
||||
private void Enqueue(LogRecord record)
|
||||
{
|
||||
var queuedCount =
|
||||
Interlocked.Increment(
|
||||
ref _queuedRecordCount);
|
||||
|
||||
if (queuedCount >
|
||||
MaximumQueuedRecords)
|
||||
{
|
||||
Interlocked.Decrement(
|
||||
ref _queuedRecordCount);
|
||||
Interlocked.Increment(
|
||||
ref _droppedRecordCount);
|
||||
return;
|
||||
}
|
||||
|
||||
_records.Enqueue(record);
|
||||
_recordsAvailable.Set();
|
||||
}
|
||||
|
||||
private void WriterLoop()
|
||||
{
|
||||
var lastFlushTime = DateTime.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
while (_isRunning ||
|
||||
!_records.IsEmpty)
|
||||
{
|
||||
var wroteAnyRecord = false;
|
||||
|
||||
while (_records.TryDequeue(
|
||||
out var record))
|
||||
{
|
||||
Interlocked.Decrement(
|
||||
ref _queuedRecordCount);
|
||||
|
||||
if (record.IsCanEvent)
|
||||
_canWriter.WriteLine(record.Line);
|
||||
else
|
||||
_snapshotWriter.WriteLine(record.Line);
|
||||
|
||||
wroteAnyRecord = true;
|
||||
}
|
||||
|
||||
var shouldFlush =
|
||||
wroteAnyRecord &&
|
||||
(DateTime.UtcNow -
|
||||
lastFlushTime)
|
||||
.TotalMilliseconds >= 500.0;
|
||||
|
||||
if (shouldFlush)
|
||||
{
|
||||
_canWriter.Flush();
|
||||
_snapshotWriter.Flush();
|
||||
lastFlushTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (!wroteAnyRecord)
|
||||
_recordsAvailable.WaitOne(100);
|
||||
}
|
||||
|
||||
var dropped =
|
||||
Interlocked.Read(
|
||||
ref _droppedRecordCount);
|
||||
|
||||
if (dropped > 0)
|
||||
{
|
||||
_canWriter.WriteLine(
|
||||
$"# DroppedRecords={dropped}");
|
||||
_snapshotWriter.WriteLine(
|
||||
$"# DroppedRecords={dropped}");
|
||||
}
|
||||
|
||||
_canWriter.Flush();
|
||||
_snapshotWriter.Flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 后台日志失败不能终止车辆控制线程。
|
||||
Console.WriteLine(
|
||||
"轮速诊断后台写入失败:" +
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Format(
|
||||
double value)
|
||||
{
|
||||
return value.ToString(
|
||||
"0.######",
|
||||
CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_recordsAvailable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
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\\admin\\.nuget\\packages\\",
|
||||
"packagesPath": "C:\\Users\\CodexSandboxOffline\\.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)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\admin\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
||||
<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>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.3</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\admin\.nuget\packages\" />
|
||||
<SourceRoot Include="C:\Users\CodexSandboxOffline\.nuget\packages\" />
|
||||
<SourceRoot Include="C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -8,7 +8,7 @@
|
||||
"net8.0": []
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\admin\\.nuget\\packages\\": {},
|
||||
"C:\\Users\\CodexSandboxOffline\\.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\\admin\\.nuget\\packages\\",
|
||||
"packagesPath": "C:\\Users\\CodexSandboxOffline\\.nuget\\packages\\",
|
||||
"outputPath": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "4fABnQtycfA=",
|
||||
"dgSpecHash": "b9v8vkN2ac8=",
|
||||
"success": true,
|
||||
"projectFilePath": "D:\\Users\\Desktop\\入职培训\\停车机器人\\MyParking\\MedullaAdapter\\MedullaAdapter.csproj",
|
||||
"expectedPackageFiles": [],
|
||||
|
||||
Reference in New Issue
Block a user