using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Threading; namespace MedullaAdapter { /// /// 在后台保存驱动器CAN速度事件和底盘周期快照,避免文件IO阻塞CAN回调。 /// 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 _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; } = ""; /// /// 创建本次诊断的两个CSV文件并启动后台写入线程。 /// 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,ManualControlMode,ManualMode,SendThresSpeed," + "DiffSteerKp,DiffSteerKi,DiffSteerKd,DiffSteerMaxI,DiffSteerDeadZone,DiffSteerThresh,DiffSteerSpeedAcc," + "PidOutLeftFront,PidOutLeftRear,PidOutRightFront,PidOutRightRear," + "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," + "ErrorThLeftFront,ErrorThLeftRear,ErrorThRightFront,ErrorThRightRear"); 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(); } } /// /// 停止记录并等待队列中的诊断数据写入磁盘。 /// 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(); } } /// /// 将一帧驱动器速度反馈加入内存队列,不在CAN回调中执行文件写入。 /// 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)); } /// /// 按最多50Hz记录一帧控制命令、PID输出、CAN反馈和舵角快照。 /// 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((int)cart.TransmitterControlMode), Format(cart.ManualMode), Format(cart.SendThresSpeed), Format(cart.DiffSteerKp), Format(cart.DiffSteerKi), Format(cart.DiffSteerKd), Format(cart.DiffSteerMaxI), Format(cart.DiffSteerDeadZone), Format(cart.DiffSteerThresh), Format(cart.DiffSteerSpeedAcc), Format(cart.DiffSteerOutputLeftFront), Format(cart.DiffSteerOutputLeftRear), Format(cart.DiffSteerOutputRightFront), Format(cart.DiffSteerOutputRightRear), 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), Format(cart.ThLeftFront - cart.ActualThLeftFront), Format(cart.ThLeftRear - cart.ActualThLeftRear), Format(cart.ThRightFront - cart.ActualThRightFront), Format(cart.ThRightRear - 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(); } } }