using CapMachine.Wpf.CanDrive; using CapMachine.Wpf.Services; using Prism.Mvvm; using System; using System.ComponentModel; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; namespace CapMachine.Wpf.CanDrive.ZlgCan { /// /// 周立功 USBCANFD-200U 工程化封装(CAN/CANFD/LIN)。 /// /// /// 设计目标: /// - 把 ZLG 原生 DLL(zlgcan.dll/zdbc.dll 等)调用封装为可被 WPF/Service 层直接消费的 .NET API; /// - 统一承载 CAN/CANFD/LIN 三类通道的“打开/初始化/接收/发送/关闭”生命周期; /// - 支持两类发送: /// - 普通发送: / / ; /// - 事件驱动发送:由 订阅 ,在信号值变化时增量下发; /// - 支持硬件定时发送(调度表): /// - 通过 下发 auto_send 列表并 apply; /// - 运行中通过 MsgName->Index 映射实现“值变化时覆盖更新同 index 的帧数据”(不改变周期)。 /// /// 线程与资源(务必理解): /// - 接收线程由 创建,回调事件在后台线程触发; /// - DBC 编码/解码与 CmdData 注入受 保护,避免接收线程与 UI 线程并发访问同一 DBC 对象; /// - 设备句柄/通道句柄/接收线程生命周期受 保护,避免并发 Open/Close 导致句柄泄漏或 AccessViolation; /// - auto_send(硬件定时发送)状态受 保护,避免 UI 启停与事件覆盖更新并发冲突。 /// public sealed class ZlgCanFd200uDriver : BindableBase, IDisposable { /// /// 驱动全局互斥锁。 /// /// /// 用于保护“设备句柄/通道句柄/接收线程”的生命周期操作: /// - Open/Init/Start/Reset/Close /// - StartReceiveLoop/StopReceiveLoop /// 避免多线程同时开关导致句柄泄漏、AccessViolation 或状态错乱。 /// private readonly object _sync = new object(); /// /// 日志服务(用于输出驱动调用、异常、诊断信息)。 /// private readonly ILogService _log; /// /// 硬件定时发送(auto_send)相关状态互斥锁。 /// /// /// auto_send 的配置/覆盖更新可能来自不同线程: /// - UI 线程点击“循环发送/调度表”触发 StartAutoSendSchedule /// - CmdDataChanged 事件线程触发覆盖更新 /// 因此必须对“是否启用、映射表、通道/帧类型”等共享状态加锁。 /// private readonly object _autoSendSync = new object(); /// /// 当前是否处于硬件定时发送(auto_send)任务运行态。 /// private bool _autoSendActive; /// /// 当前硬件定时发送使用的通道索引(0/1)。 /// private int _autoSendChannelIndex; /// /// 当前硬件定时发送使用的帧类型()。 /// private byte _autoSendFrameType; /// /// MsgName -> auto_send 任务索引列表的映射。 /// /// /// 用于“信号值变化时覆盖更新对应定时发送任务”。 /// - 一个 MsgName 可能对应多个条目(例如配置了多个相同 MsgName 的调度表行)。 /// - 覆盖更新时通过 重新写入同 index 的 auto_send 结构体达到更新效果。 /// private readonly Dictionary> _autoSendMsgIndex = new Dictionary>(StringComparer.Ordinal); /// /// 预加载的 zlgcan.dll 模块句柄。 /// /// /// 用于在 中强制从输出目录加载指定 DLL,减少被 PATH 中同名 DLL 干扰的风险。 /// private static IntPtr _preloadedZlgcanHandle = IntPtr.Zero; /// /// 预加载 zlgcan.dll 失败时记录的错误信息。 /// /// /// - 若预加载失败,后续 OpenDevice 失败时会把该信息拼接到异常提示中,便于定位依赖缺失/位数不匹配等问题。 /// private static string? _preloadedZlgcanError; /// /// 设备句柄(ZCAN_OpenDevice 返回)。 /// /// /// - 设备句柄是 CAN/CANFD/LIN 共用的“根句柄”; /// - auto_send 配置也通过 device_handle + path 进行 SetValue。 /// private IntPtr _deviceHandle = IntPtr.Zero; /// /// CAN/CANFD 通道句柄数组(最多 2 通道)。 /// private readonly IntPtr[] _canChannelHandles = new IntPtr[2]; /// /// LIN 通道句柄。 /// private IntPtr _linChannelHandle = IntPtr.Zero; /// /// 接收线程取消令牌。 /// private CancellationTokenSource? _recvCts; /// /// 接收循环任务(后台线程)。 /// private Task? _recvTask; /// /// 对象是否已释放。 /// private volatile bool _disposed; /// /// DBC 数据结构互斥锁。 /// /// /// 用于保护以下共享状态的并发读写: /// - 的加载/释放 /// - 的替换 /// - 的注入与事件订阅/退订 /// /// 接收线程在解码时会读取 DBC 与索引,因此这里必须保证读取到的是一致快照。 /// private readonly object _dbcSync = new object(); /// /// DBC 数据库实例。 /// /// /// 该对象封装 zdbc.dll 的加载/解析能力,用于: /// - 接收帧解析为信号值(更新 UI 的 ) /// - 发送时把 CmdData 的信号值编码为原始帧数据(见 ) /// private ZlgDbcDatabase? _dbc; /// /// DBC 解析后的“信号模型集合”(用于 UI 绑定)。 /// private ObservableCollection? _dbcModels; /// /// SignalName -> 信号模型的索引,加速更新(接收解码更新时 O(1) 查找)。 /// private Dictionary? _dbcModelIndex; /// /// 当前用于发送的指令集合(来源于配置程序+读写设置,外部通过 LoadCmdDataToDrive 注入)。 /// /// /// 该集合的每个元素通常对应一个 Signal(含 MsgName/SignalName/SignalCmdValue)。 /// - 事件驱动发送:订阅每个元素的 CanCmdDataChangedHandler,在信号变化时增量下发。 /// - 编码发送:按 MsgName 聚合所有 SignalName/SignalCmdValue,再交给 DBC 编码。 /// private List? _cmdData; /// /// CmdData 编码并发送时使用的通道索引。 /// private int _cmdSendChannelIndex; /// /// CmdData 编码并发送时使用的帧类型(CAN/CANFD)。 /// private byte _cmdSendFrameType; /// /// 构造函数。 /// /// 日志服务。 public ZlgCanFd200uDriver(ILogService logService) { _log = logService; } /// /// 设备打开状态的 backing 字段。 /// /// /// 对外通过 暴露。 /// - 仅在驱动内部状态切换时写入(例如 /); /// - 使用 通知 UI 刷新。 /// private bool _openState; /// /// 设备打开状态。 /// public bool OpenState { get { return _openState; } private set { _openState = value; RaisePropertyChanged(); } } /// /// 接收线程运行状态的 backing 字段。 /// private bool _isReceiving; /// /// 是否正在接收。 /// public bool IsReceiving { get { return _isReceiving; } private set { _isReceiving = value; RaisePropertyChanged(); } } /// /// 发送指示灯状态的 backing 字段。 /// private bool _isSendOk; /// /// 最近是否发生过“发送成功”(用于 UI 指示灯)。 /// public bool IsSendOk { get { return _isSendOk; } private set { _isSendOk = value; RaisePropertyChanged(); } } /// /// 接收指示灯状态的 backing 字段。 /// private bool _isReviceOk; /// /// 最近是否发生过“接收成功”(用于 UI 指示灯)。 /// public bool IsReviceOk { get { return _isReviceOk; } private set { _isReviceOk = value; RaisePropertyChanged(); } } /// /// 发送指示灯更新 token。 /// /// /// 用于解决“短时间内多次 MarkSendOk 导致的延迟回落互相覆盖”问题: /// - 每次触发都会递增 token; /// - 延迟回落时仅当 token 未变化才会把 置回 false。 /// private int _sendOkToken; /// /// 接收指示灯更新 token。 /// /// /// 语义同 ,用于 的延迟回落保护。 /// private int _reviceOkToken; /// /// 将 置为 true 并在一段时间后自动恢复为 false。 /// /// 保持 true 的时间(毫秒)。 private void MarkSendOk(int holdMs = 800) { var token = Interlocked.Increment(ref _sendOkToken); IsSendOk = true; Task.Run(async () => { await Task.Delay(Math.Max(50, holdMs)); if (token == _sendOkToken) { IsSendOk = false; } }); } /// /// 将 置为 true 并在一段时间后自动恢复为 false。 /// /// 保持 true 的时间(毫秒)。 private void MarkReviceOk(int holdMs = 800) { var token = Interlocked.Increment(ref _reviceOkToken); IsReviceOk = true; Task.Run(async () => { await Task.Delay(Math.Max(50, holdMs)); if (token == _reviceOkToken) { IsReviceOk = false; } }); } /// /// CAN/CANFD 原始帧接收事件。 /// /// /// - 该事件在接收线程中触发(非 UI 线程); /// - 订阅方若需要更新 UI,请自行切回 UI 线程; /// - 即使启用了 DBC 解码,该事件仍会原样抛出原始帧,便于外部做抓包/记录。 /// public event Action? CanFrameReceived; /// /// LIN 原始帧接收事件。 /// /// /// 同 :在接收线程触发,订阅方自行处理线程切换。 /// public event Action? LinFrameReceived; /// /// 的 backing 字段。 /// private bool _dbcDecodeEnabled; /// /// 是否启用 DBC 解码更新(接收帧触发)。 /// public bool DbcDecodeEnabled { get { return _dbcDecodeEnabled; } set { _dbcDecodeEnabled = value; RaisePropertyChanged(); } } /// /// 的 backing 字段。 /// private bool _isCycleSend; /// /// 是否启用“事件驱动发送”。 /// /// /// 与现有 ToomossCan/ToomossCanFD 行为对齐:只有 IsCycleSend=true 且 SchEnable=true 时, /// 才会在 CmdDataChanged 事件中触发增量下发。 /// public bool IsCycleSend { get { return _isCycleSend; } set { _isCycleSend = value; RaisePropertyChanged(); } } /// /// 的 backing 字段。 /// private bool _schEnable; /// /// 发送使能(与 UI 的调度表使能语义对齐)。 /// public bool SchEnable { get { return _schEnable; } set { _schEnable = value; RaisePropertyChanged(); } } /// /// 打开设备(不初始化 CAN/LIN 通道)。 /// /// 设备索引(通常 0)。 /// /// 说明: /// - 该方法只负责获取 device_handle(根句柄);通道初始化由 / 完成; /// - 若 OpenDevice 失败,为了便于现场定位,代码会输出: /// - 输出目录 DLL 依赖诊断(是否缺失、位数是否匹配、版本信息); /// - 当前进程架构与已加载模块路径; /// - 该方法是幂等的:若设备已打开则直接返回。 /// public void OpenDevice(uint deviceIndex) { ThrowIfDisposed(); lock (_sync) { if (_deviceHandle != IntPtr.Zero) { OpenState = true; return; } EnsureNativeDllExists("zlgcan.dll"); // 设备类型固定为 USBCANFD-200U。若后续扩展其它型号,应把 deviceType 抽为配置参数。 var deviceType = ZLGCAN.ZCAN_USBCANFD_200U; try { _deviceHandle = ZLGCAN.ZCAN_OpenDevice(deviceType, deviceIndex, 0); } catch (Exception ex) { var baseDir = AppContext.BaseDirectory; var dllFullPath = Path.Combine(baseDir, "zlgcan.dll"); var msg = $"ZCAN_OpenDevice 调用异常。deviceType={deviceType}, deviceIndex={deviceIndex}, " + $"Is64BitProcess={Environment.Is64BitProcess}, ProcessArch={RuntimeInformation.ProcessArchitecture}, " + $"DllPath={dllFullPath}, BaseDir={baseDir}。异常:{ex.Message}"; _log.Error(msg); throw new InvalidOperationException(msg, ex); } if (_deviceHandle == IntPtr.Zero) { var lastError = Marshal.GetLastWin32Error(); var lastErrorMsg = string.Empty; try { lastErrorMsg = new Win32Exception(lastError).Message; } catch { } var baseDir = AppContext.BaseDirectory; var dllFullPath = Path.Combine(baseDir, "zlgcan.dll"); var loadedModulePath = GetLoadedModulePath("zlgcan.dll") ?? string.Empty; var depDiag = string.Join(" | ", new[] { BuildDllDiag(baseDir, "zlgcan.dll"), BuildDllDiag(baseDir, "USB2XXX.dll"), BuildDllDiag(baseDir, "libusb-1.0.dll"), BuildDllDiag(baseDir, "zdbc.dll"), BuildDllDiag(baseDir, "kerneldlls\\USBCANFD.dll"), BuildDllDiag(baseDir, "kerneldlls\\CANDevCore.dll"), BuildDllDiag(baseDir, "kerneldlls\\CANDevice.dll"), BuildDllDiag(baseDir, "kerneldlls\\CANFDCOM.dll"), BuildDllDiag(baseDir, "kerneldlls\\CANFDNET.dll"), BuildDllDiag(baseDir, "kerneldlls\\ZPSCANFD.dll"), BuildDllDiag(baseDir, "kerneldlls\\USBCANFD800U.dll"), BuildDllDiag(baseDir, "kerneldlls\\USBCAN.dll"), BuildDllDiag(baseDir, "kerneldlls\\usbcan.dll"), }.Where(s => !string.IsNullOrWhiteSpace(s))); string dllVer = string.Empty; try { if (File.Exists(dllFullPath)) { var vi = FileVersionInfo.GetVersionInfo(dllFullPath); dllVer = vi?.FileVersion ?? string.Empty; } } catch { } var msg = $"ZCAN_OpenDevice 失败。deviceType={deviceType}, deviceIndex={deviceIndex}。" + $"Win32LastError={lastError}(0x{lastError:X}){(string.IsNullOrWhiteSpace(lastErrorMsg) ? string.Empty : $"({lastErrorMsg})")}。" + $"Is64BitProcess={Environment.Is64BitProcess}, ProcessArch={RuntimeInformation.ProcessArchitecture}。" + $"DllPath={dllFullPath}, DllVersion={dllVer}, LoadedModulePath={loadedModulePath}, BaseDir={baseDir}。" + $"PreloadError={_preloadedZlgcanError ?? string.Empty}。" + $"DirDllDiag={depDiag}。" + "请确认:1) 已安装ZLG驱动;2) 设备已连接且未被其它程序占用;3) 程序位数与 zlgcan.dll 匹配;4) 设备类型/索引正确。"; _log.Error(msg); throw new InvalidOperationException(msg); } OpenState = true; } } /// /// 预加载指定路径的原生 DLL,尽量避免被 PATH 中的同名 DLL 干扰。 /// /// DLL 完整路径。 private static void TryPreloadNativeDll(string dllFullPath) { try { if (_preloadedZlgcanHandle != IntPtr.Zero) { return; } if (!string.IsNullOrWhiteSpace(_preloadedZlgcanError)) { return; } _preloadedZlgcanHandle = NativeLibrary.Load(dllFullPath); } catch (Exception ex) { _preloadedZlgcanError = $"{ex.GetType().Name}: {ex.Message}"; } } /// /// 构造输出目录下某个 DLL 的诊断信息(架构/大小/版本)。 /// /// 输出目录。 /// DLL 文件名。 /// 诊断信息字符串;不存在返回 "xxx:not_found";异常返回 null。 private static string? BuildDllDiag(string baseDir, string fileName) { try { if (string.IsNullOrWhiteSpace(baseDir) || string.IsNullOrWhiteSpace(fileName)) { return null; } var full = Path.Combine(baseDir, fileName); if (!File.Exists(full)) { return $"{fileName}:not_found"; } var arch = TryGetPeArchitecture(full) ?? string.Empty; long size = 0; try { size = new FileInfo(full).Length; } catch { } var ver = string.Empty; try { var vi = FileVersionInfo.GetVersionInfo(full); ver = vi?.FileVersion ?? string.Empty; } catch { } return $"{fileName}:arch={arch},size={size},ver={ver}"; } catch { return null; } } /// /// 获取当前进程中已加载模块的完整路径。 /// /// 模块文件名(如 zlgcan.dll)。 /// 完整路径;获取失败返回 null。 private static string? GetLoadedModulePath(string moduleFileName) { try { if (string.IsNullOrWhiteSpace(moduleFileName)) { return null; } using var p = Process.GetCurrentProcess(); foreach (ProcessModule m in p.Modules) { if (string.Equals(m.ModuleName, moduleFileName, StringComparison.OrdinalIgnoreCase)) { return m.FileName; } } return null; } catch { return null; } } /// /// 打开设备并初始化 CANFD 通道。 /// /// 设备索引(通常 0)。 /// 通道0配置。 /// 通道1配置(可为 null 表示不初始化)。 /// /// 生命周期语义: /// - 本方法会执行:OpenDevice -> InitCAN(通道参数) -> StartCAN; /// - 并根据通道0配置写入“设备级合并接收开关”(0/set_device_recv_merge); /// - 初始化过程中如发生异常,会调用 尽量回收已打开的句柄,避免资源泄漏。 /// /// 注意: /// - 合并接收开关属于 device 级能力,因此以通道0配置为准; /// - 该方法不启动接收线程,接收线程由 单独启动,便于 UI 控制。 /// public void OpenAndInitCan(uint deviceIndex, ZlgCanFdChannelOptions channel0, ZlgCanFdChannelOptions? channel1 = null) { ThrowIfDisposed(); lock (_sync) { if (_deviceHandle == IntPtr.Zero) { OpenDevice(deviceIndex); } try { InitCanChannelInternal(0, channel0); _canChannelHandles[0] = StartCanChannelInternal(0); if (channel1 != null) { InitCanChannelInternal(1, channel1); _canChannelHandles[1] = StartCanChannelInternal(1); } // 合并接收:这是设备级能力,按通道0的配置为准(两通道同时开启/关闭) var mergeEnable = channel0.EnableMergeReceive; var mergeRet = ZLGCAN.ZCAN_SetValue(_deviceHandle, "0/set_device_recv_merge", mergeEnable ? "1" : "0"); if (mergeRet != 1) { _log.Warn("设置合并接收失败(0/set_device_recv_merge)。将继续运行,但接收模式可能不符合预期。"); } } catch { SafeClose_NoLock(); throw; } } } /// /// 初始化并启动 LIN 通道。 /// /// LIN 通道索引(通常 0)。 /// LIN 初始化参数。 /// /// 说明: /// - LIN 与 CAN/CANFD 共用同一个 device_handle; /// - 本工程在 UI/Service 层通常会做 CAN/LIN 互斥校验,避免同时占用同一硬件。 /// public void OpenAndInitLin(uint linIndex, ZlgLinChannelOptions options) { ThrowIfDisposed(); lock (_sync) { if (_deviceHandle == IntPtr.Zero) { OpenDevice(0); } if (_linChannelHandle != IntPtr.Zero) { throw new InvalidOperationException("LIN 通道已初始化。"); } var cfg = new ZLGCAN.ZCAN_LIN_INIT_CONFIG { linMode = (byte)(options.IsMaster ? 1 : 0), chkSumMode = options.ChecksumMode, maxLength = options.MaxLength, reserved = 0, libBaud = options.BaudRate }; IntPtr pCfg = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZLGCAN.ZCAN_LIN_INIT_CONFIG))); try { Marshal.StructureToPtr(cfg, pCfg, false); _linChannelHandle = ZLGCAN.ZCAN_InitLIN(_deviceHandle, linIndex, pCfg); } finally { Marshal.FreeHGlobal(pCfg); } if (_linChannelHandle == IntPtr.Zero) { throw new InvalidOperationException("ZCAN_InitLIN 失败。"); } var ret = ZLGCAN.ZCAN_StartLIN(_linChannelHandle); if (ret != 1) { throw new InvalidOperationException("ZCAN_StartLIN 失败。"); } } } /// /// 启动后台接收线程。 /// /// 是否启用合并接收(ZCAN_ReceiveData)。建议与通道初始化时的 EnableMergeReceive 保持一致。 /// 接收缓冲最大帧数(合并接收固定数组大小,过小会丢帧)。 /// /// 线程与回调说明: /// - 本方法会创建一个后台 Task 持续读取驱动接收缓冲区; /// - 收到帧后会触发 / (在后台线程触发,非 UI 线程); /// - 若 为 true,会在接收线程中对帧做 DBC 解码并更新 集合。 /// /// 注意: /// - 若你在 UI 层直接绑定 集合并要求线程安全更新,请在订阅事件或更新集合时切回 UI 线程。 /// /// 设备未打开或接收线程已启动。 public void StartReceiveLoop(bool mergeReceive, int bufferFrames = 100) { ThrowIfDisposed(); lock (_sync) { if (_deviceHandle == IntPtr.Zero) { throw new InvalidOperationException("设备未打开。"); } if (_recvTask != null) { throw new InvalidOperationException("接收线程已启动。"); } if (bufferFrames <= 0) bufferFrames = 1; _recvCts = new CancellationTokenSource(); var token = _recvCts.Token; _recvTask = Task.Run(() => { IsReceiving = true; try { if (mergeReceive) { ReceiveLoop_Merge(token, bufferFrames); } else { ReceiveLoop_PerChannel(token, bufferFrames); } } catch (Exception ex) { _log.Error($"ZLG 接收线程异常退出:{ex.Message}"); } finally { IsReceiving = false; } }, token); } } /// /// 加载 DBC 文件,并生成 UI 绑定用的信号集合。 /// /// DBC 文件路径。 /// 是否启用异步解析。 /// 是否合并加载。 /// 协议类型。 /// CanDbcModel 集合。 /// /// 成功加载后会: /// - 重新创建并替换内部 DBC 数据库对象; /// - 构建用于 UI 展示的 列表; /// - 建立 SignalName 索引(用于接收线程解码时快速定位模型并更新值); /// - 默认开启 ,使接收线程能驱动实时值更新。 /// public ObservableCollection StartDbc(string dbcPath, bool enableAsyncAnalyse = true, bool merge = false, byte protocolType = ZDBC.PROTOCOL_OTHER) { ThrowIfDisposed(); lock (_dbcSync) { _dbc?.Dispose(); _dbc = new ZlgDbcDatabase(_log); _dbc.Load(dbcPath, enableAsyncAnalyse, merge, protocolType); _dbcModels = _dbc.BuildCanDbcModels(); _dbcModelIndex = BuildDbcModelIndex(_dbcModels); DbcDecodeEnabled = true; return _dbcModels; } } /// /// 设置并订阅要发送的指令集合(事件驱动)。 /// /// 指令集合。 /// 发送通道(0/1)。 /// 帧类型:ZDBC.FT_CAN 或 ZDBC.FT_CANFD。 /// /// 本方法用于建立“信号值变化 => 增量下发”的事件驱动链路: /// - 会先取消订阅旧集合的变更事件,再订阅新集合; /// - 仅当 同时为 true 时,变更事件才会触发实际发送; /// - 发送时仍会通过 DBC 对 MsgName 进行编码(见 )。 /// public void LoadCmdDataToDrive(List cmdData, int channelIndex, byte frameType) { ThrowIfDisposed(); lock (_dbcSync) { // 说明:CmdData 的变更事件回调可能与接收线程/调度表线程并发发生。 // 这里使用 _dbcSync 统一保护: // - _cmdData 集合引用替换 // - 事件订阅/退订 // - _cmdSendChannelIndex/_cmdSendFrameType 写入 // 以避免事件在“退订一半/替换一半”的中间状态触发导致空引用或错用旧集合。 if (_cmdData != null) { foreach (var old in _cmdData) { old.CanCmdDataChangedHandler -= CmdData_CanCmdDataChangedHandler; } } // 保存最新 CmdData 快照:后续 // - 事件驱动发送会按 MsgName 聚合信号并编码; // - 启动硬件调度表也会从 CmdData/DBC 生成原始帧。 _cmdData = cmdData; _cmdSendChannelIndex = channelIndex; _cmdSendFrameType = frameType; if (_cmdData != null) { foreach (var item in _cmdData) { item.CanCmdDataChangedHandler += CmdData_CanCmdDataChangedHandler; } } } } /// /// 事件驱动回调:某个 MsgName 的信号值变更时,仅增量编码并下发该消息。 /// /// 发送方。 /// 发生变化的消息名称。 /// /// 发送策略: /// - 调度表(硬件 auto_send)运行时:优先覆盖更新设备定时发送列表中的对应条目(见 ); /// - 否则:立即编码并发送 1 帧(见 )。 /// /// 说明:该方法由 的变更事件触发,可能不在 UI 线程。 /// private void CmdData_CanCmdDataChangedHandler(object? sender, string msgName) { try { if (!IsCycleSend) return; if (!SchEnable) return; // 注意:该回调可能在非 UI 线程触发,也可能被频繁触发(例如 UI 滑条连续变化)。 // 这里的实现目标是“尽量少做事”: // - 若已启用硬件 auto_send:只覆盖更新对应条目帧数据,不做 apply/clear(避免抖动); // - 否则直接发送 1 帧,维持信号变化的即时反馈。 // 调度表使能时:优先使用硬件定时发送(auto_send)进行增量覆盖; // 若当前未处于 auto_send 任务状态,则回退为直接发送一帧。 if (TryUpdateAutoSendByMsgName(msgName)) { return; } SendOneMsgByCmdData(msgName, _cmdSendChannelIndex, _cmdSendFrameType); } catch (Exception ex) { _log.Warn($"事件驱动发送异常:{ex.Message}"); } } /// /// 按 MsgName 尝试覆盖更新硬件定时发送列表中的对应任务。 /// /// 消息名称。 /// true=已更新 auto_send;false=未更新(可能未启动 auto_send 或未命中任务索引)。 /// /// 覆盖更新原理: /// - auto_send 的每条任务由 index 唯一标识; /// - 重新对同 index 调用 SetValue 写入新的帧数据,即可实现“后续周期发送新值”; /// - 本方法仅覆盖帧数据,不改变周期(周期由启动调度表时写入)。 /// private bool TryUpdateAutoSendByMsgName(string msgName) { if (string.IsNullOrWhiteSpace(msgName)) return false; bool active; int channelIndex; byte frameType; List? taskIndexes; lock (_autoSendSync) { // 从 auto_send 运行态读取一个“快照”: // - active/channelIndex/frameType:当前硬件调度表的运行参数 // - taskIndexes:该 MsgName 在硬件列表中对应的任务 index(可能映射到多个条目) // 之所以拷贝 ToList():避免后续遍历时与 Start/StopSchedule 并发清空集合产生枚举异常。 active = _autoSendActive; channelIndex = _autoSendChannelIndex; frameType = _autoSendFrameType; if (!_autoSendMsgIndex.TryGetValue(msgName, out taskIndexes)) { taskIndexes = null; } else { taskIndexes = taskIndexes.ToList(); } } if (!active || taskIndexes == null || taskIndexes.Count == 0) { return false; } var built = TryBuildRawFrameFromCmdData(msgName, frameType); if (built == null) { // 常见原因: // - DBC 未加载/加载失败 // - CmdData 中不存在该 MsgName 的任何信号 // - 该 MsgName 在 DBC 中找不到 message 定义,导致编码失败 return false; } foreach (var idx in taskIndexes) { try { // intervalMs 传 null:表示“只覆盖帧内容,不修改周期”。 // 周期由 StartAutoSendSchedule 首次写入,运行中保持不变。 ConfigureAutoSendRawFrame(channelIndex, idx, enable: true, intervalMs: null, canId: built.Value.CanId, payload: built.Value.Payload, payloadLen: built.Value.PayloadLen, frameType: frameType); } catch (Exception ex) { _log.Warn($"覆盖定时发送失败:Msg={msgName}, Index={idx}, {ex.Message}"); } } return true; } /// /// 根据 MsgName 从当前 CmdData 聚合信号值,并通过 DBC 编码为原始 CAN/CANFD 帧。 /// /// 消息名称(DBC Message Name)。 /// 帧类型(CAN/CANFD)。 /// /// 成功返回 (CanId, Payload, PayloadLen);失败返回 null(如 DBC 未加载、CmdData 为空或该 MsgName 无任何信号值)。 /// private (uint CanId, byte[] Payload, int PayloadLen)? TryBuildRawFrameFromCmdData(string msgName, byte frameType) { ZlgDbcDatabase? dbc; List? cmdData; lock (_dbcSync) { // 读取 DBC 与 CmdData 引用快照: // - 避免在锁内做编码(编码可能较慢,会阻塞接收线程解码等操作); // - 允许并发切换 DBC/重载 CmdData:即使快照稍旧,也只会影响一次编码结果。 dbc = _dbc; cmdData = _cmdData; } if (dbc == null || !dbc.IsLoaded) return null; if (cmdData == null || cmdData.Count == 0) return null; var dict = new Dictionary(StringComparer.Ordinal); foreach (var item in cmdData) { if (!string.Equals(item.MsgName, msgName, StringComparison.Ordinal)) { continue; } if (string.IsNullOrWhiteSpace(item.SignalName)) { continue; } // 同一 SignalName 若在 CmdData 中重复出现,这里按“最后一次覆盖”处理。 dict[item.SignalName] = item.SignalCmdValue; } if (dict.Count == 0) return null; var (canId, data, dataLen) = dbc.EncodeToRawFrame(msgName, dict, frameType); return (canId, data ?? Array.Empty(), dataLen); } /// /// 清空并按调度表批量配置硬件定时发送列表,然后 apply_auto_send 使能。 /// /// 调度表条目集合。 /// 通道索引(0/1)。 /// 帧类型(CAN/CANFD)。 /// /// 对应周立功定时发送能力: /// - 通过 {channel}/auto_send 或 {channel}/auto_send_canfd 写入待发送帧与周期; /// - 再通过 {channel}/apply_auto_send 使能后,由设备侧硬件定时器保证发送周期精度。 /// /// 本工程约定: /// - 调度表中的 MsgName 必须能在 DBC 中找到 message 定义,否则无法编码并会被跳过; /// - 运行期间信号值变化会触发覆盖更新(不影响周期)。 /// public void StartAutoSendSchedule(IEnumerable<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> scheduleItems, int channelIndex, byte frameType) { ThrowIfDisposed(); if (_deviceHandle == IntPtr.Zero) throw new InvalidOperationException("设备未打开。"); var items = scheduleItems?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)>(); if (items.Count == 0) { throw new InvalidOperationException("调度表为空,无法启动硬件定时发送。"); } // 硬件侧列表索引顺序即发送启动顺序:按 SchTabIndex、OrderSend 排序以保持可控。 items = items .OrderBy(a => a.SchTabIndex) .ThenBy(a => a.OrderSend) .ThenBy(a => a.MsgName, StringComparer.Ordinal) .ToList(); // 周立功说明:每通道最大 100 条(部分型号更小)。这里按 100 做上限保护。 if (items.Count > 100) { throw new InvalidOperationException($"调度表条目过多({items.Count}),超过硬件定时发送列表上限 100。"); } // 重要:重新配置调度表时必须先 clear_auto_send。 // - 防止旧条目残留导致“设备仍发送旧帧/旧周期”; // - 也确保本次配置从 index=0 连续写入,便于 MsgName->index 映射稳定。 ClearAutoSend(channelIndex); var msgIndex = new Dictionary>(StringComparer.Ordinal); for (ushort i = 0; i < items.Count; i++) { var it = items[i]; var cycle = (uint)Math.Max(1, it.CycleMs); var built = TryBuildRawFrameFromCmdData(it.MsgName, frameType); if (built == null) { _log.Warn($"定时发送跳过:MsgName={it.MsgName} 未能从 CmdData/DBC 生成原始帧。"); continue; } ConfigureAutoSendRawFrame(channelIndex, i, enable: true, intervalMs: cycle, canId: built.Value.CanId, payload: built.Value.Payload, payloadLen: built.Value.PayloadLen, frameType: frameType); if (!msgIndex.TryGetValue(it.MsgName, out var list)) { list = new List(); msgIndex[it.MsgName] = list; } list.Add(i); } // apply_auto_send:告诉设备“开始执行定时发送列表”。 // 注意:仅在配置完所有条目后调用一次即可。 ApplyAutoSend(channelIndex); lock (_autoSendSync) { // 记录运行态与映射表:供 CmdDataChanged 事件在运行中做“覆盖更新”。 _autoSendActive = true; _autoSendChannelIndex = channelIndex; _autoSendFrameType = frameType; _autoSendMsgIndex.Clear(); foreach (var kv in msgIndex) { _autoSendMsgIndex[kv.Key] = kv.Value; } } } /// /// 停止硬件定时发送(clear_auto_send)。 /// /// 通道索引(0/1)。 /// /// - 会清空设备侧定时发送列表并停止发送; /// - 同时清空内存态 MsgName->Index 映射,避免后续误覆盖写入。 /// public void StopAutoSendSchedule(int channelIndex) { ThrowIfDisposed(); if (_deviceHandle == IntPtr.Zero) { lock (_autoSendSync) { _autoSendActive = false; _autoSendMsgIndex.Clear(); } return; } // clear_auto_send:停止设备侧定时发送并清空列表。 // 该调用非常关键:否则即使 PC 侧不再发送,硬件仍会按周期继续发。 ClearAutoSend(channelIndex); lock (_autoSendSync) { _autoSendActive = false; _autoSendMsgIndex.Clear(); } } private void ConfigureAutoSendRawFrame(int channelIndex, ushort taskIndex, bool enable, uint? intervalMs, uint canId, byte[] payload, int payloadLen, byte frameType) { var interval = intervalMs ?? 1; var len = Math.Max(0, payloadLen); var data = payload ?? Array.Empty(); // 根据帧类型选择写入 auto_send 的结构体: // - CAN:{channel}/auto_send + ZCAN_AUTO_TRANSMIT_OBJ // - CANFD:{channel}/auto_send_canfd + ZCANFD_AUTO_TRANSMIT_OBJ // 覆盖更新的关键:对同一 taskIndex 再次 SetValue 会替换设备侧缓存的帧数据。 if (frameType == ZDBC.FT_CAN) { var canBytes = len <= 0 ? Array.Empty() : data.Take(Math.Min(8, len)).ToArray(); ConfigureAutoSendCan(channelIndex, taskIndex, enable, interval, canId, canBytes); return; } if (frameType == ZDBC.FT_CANFD) { var fdBytes = len <= 0 ? Array.Empty() : data.Take(Math.Min(64, len)).ToArray(); ConfigureAutoSendCanFd(channelIndex, taskIndex, enable, interval, canId, fdBytes); return; } throw new ArgumentOutOfRangeException(nameof(frameType), "frameType 仅支持 ZDBC.FT_CAN=0 或 ZDBC.FT_CANFD=1。"); } /// /// 根据 MsgName,从当前 CmdData 中取出信号值,编码并发送一帧。 /// /// 消息名称。 /// 发送通道。 /// 帧类型:ZDBC.FT_CAN 或 ZDBC.FT_CANFD。 public void SendOneMsgByCmdData(string msgName, int channelIndex, byte frameType) { ThrowIfDisposed(); ZlgDbcDatabase? dbc; List? cmdData; lock (_dbcSync) { // 仅在锁内拿到 DBC/CmdData 的引用快照,避免编码时长时间占用 _dbcSync。 // 编码可能会分配内存/做查表,若在锁内执行会阻塞接收线程的 DBC 解码。 dbc = _dbc; cmdData = _cmdData; } if (dbc == null || !dbc.IsLoaded) { // DBC 未加载时无法做“信号值->原始帧”的编码,因此直接忽略(上层会通过 DbcParserState 提示)。 return; } if (cmdData == null || cmdData.Count == 0) { // 没有 CmdData 也就没有要写入的信号值,直接返回。 return; } if (string.IsNullOrWhiteSpace(msgName)) { return; } var dict = new Dictionary(StringComparer.Ordinal); foreach (var item in cmdData) { if (!string.Equals(item.MsgName, msgName, StringComparison.Ordinal)) { continue; } if (string.IsNullOrWhiteSpace(item.SignalName)) { continue; } dict[item.SignalName] = item.SignalCmdValue; } if (dict.Count == 0) { // MsgName 未命中任何信号:常见于 CmdData 尚未注入或 UI 选择的消息为空。 return; } // DBC 编码:将“信号名->物理值”转换为可发送的 raw frame。 // 注意:frameType 决定 payload 长度上限(CAN=8, CANFD=64)。 var (canId, data, dataLen) = dbc.EncodeToRawFrame(msgName, dict, frameType); if (frameType == ZDBC.FT_CAN) { // requestTxEcho=true:请求设备回传一帧 Tx echo。 // - merge 接收模式下会以 IsTx 标识; // - per-channel 接收模式下会走 __pad/flags 判断(见接收循环)。 SendCan(channelIndex, canId, data, requestTxEcho: true); return; } if (frameType == ZDBC.FT_CANFD) { SendCanFd(channelIndex, canId, data, requestTxEcho: true); return; } throw new ArgumentOutOfRangeException(nameof(frameType), "frameType 仅支持 ZDBC.FT_CAN=0 或 ZDBC.FT_CANFD=1。"); } /// /// 停止后台接收线程。 /// public void StopReceiveLoop() { lock (_sync) { // StopReceiveLoop 与 StartReceiveLoop/Open/Close 同受 _sync 保护: // - 防止接收线程创建/销毁与通道 Reset/Close 并发; // - 保证 _recvCts/_recvTask 的可见性与一致性。 if (_recvCts == null || _recvTask == null) { return; } try { _recvCts.Cancel(); _recvTask.Wait(TimeSpan.FromSeconds(2)); } catch (Exception ex) { _log.Warn($"停止接收线程等待超时或异常:{ex.Message}"); } finally { // 无论等待是否超时,都要清理引用并释放 CTS,避免下次 StartReceiveLoop 认为线程仍在运行。 _recvCts.Dispose(); _recvCts = null; _recvTask = null; } } } /// /// 发送 CAN 报文。 /// /// 通道索引(0/1)。 /// 包含扩展帧标志位的 can_id(可用 ZlgCanIdHelper.MakeCanId 生成)。 /// 数据(0~8字节)。 /// 是否请求发送回显。 /// 发送方式,0=正常发送,1=单次发送,2=自发自收,3=单次自发自收。 public uint SendCan(int channelIndex, uint canId, byte[] data, bool requestTxEcho = false, uint transmitType = 0) { ThrowIfDisposed(); var chn = GetCanChannelHandleOrThrow(channelIndex); var frame = new ZLGCAN.can_frame { can_id = canId, can_dlc = (byte)Math.Min(8, data?.Length ?? 0), __pad = 0, __res0 = 0, __res1 = 0, data = new byte[8] }; if (data != null && data.Length > 0) { Array.Copy(data, frame.data, Math.Min(8, data.Length)); } if (requestTxEcho) { frame.__pad |= 0x20; } var tx = new ZLGCAN.ZCAN_Transmit_Data { frame = frame, transmit_type = transmitType }; IntPtr pTx = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZLGCAN.ZCAN_Transmit_Data))); try { Marshal.StructureToPtr(tx, pTx, false); var ret = ZLGCAN.ZCAN_Transmit(chn, pTx, 1); if (ret > 0) { MarkSendOk(); } return ret; } finally { Marshal.FreeHGlobal(pTx); } } /// /// 发送 CANFD 报文。 /// /// 通道索引(0/1)。 /// 包含扩展帧标志位的 can_id(可用 ZlgCanIdHelper.MakeCanId 生成)。 /// 数据(0~64字节)。 /// 是否请求发送回显。 /// 发送方式,0=正常发送,1=单次发送,2=自发自收,3=单次自发自收。 /// 驱动返回的实际发送帧数(通常为 0 或 1)。 /// /// - CANFD 有效长度由 决定,内部会自动截断到 64 字节; /// - 若 为 true,会在 flags 中设置回显位,使设备回传一帧 Tx 事件。 /// 在 merge 接收模式下,这种 Tx 回显会以 标识。 /// /// 通道未初始化或句柄无效。 public uint SendCanFd(int channelIndex, uint canId, byte[] data, bool requestTxEcho = false, uint transmitType = 0) { ThrowIfDisposed(); var chn = GetCanChannelHandleOrThrow(channelIndex); var len = (byte)Math.Min(64, data?.Length ?? 0); var frame = new ZLGCAN.canfd_frame { can_id = canId, len = len, flags = 0, __res0 = 0, __res1 = 0, data = new byte[64] }; if (data != null && data.Length > 0) { Array.Copy(data, frame.data, Math.Min(64, data.Length)); } if (requestTxEcho) { frame.flags |= 0x20; } var tx = new ZLGCAN.ZCAN_TransmitFD_Data { frame = frame, transmit_type = transmitType }; IntPtr pTx = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZLGCAN.ZCAN_TransmitFD_Data))); try { Marshal.StructureToPtr(tx, pTx, false); var ret = ZLGCAN.ZCAN_TransmitFD(chn, pTx, 1); if (ret > 0) { MarkSendOk(); } return ret; } finally { Marshal.FreeHGlobal(pTx); } } /// /// 设置设备定时发送(CAN)。 /// /// 通道索引。 /// 定时任务索引。 /// 是否使能。 /// 周期(ms)。 /// can_id。 /// 数据(0~8)。 /// /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/auto_send。 /// /// 关键点: /// - 为设备侧列表索引;同一索引再次写入会“覆盖更新”; /// - 为周期(毫秒),设备按该周期自动发送; /// - 本实现默认设置发送回显位(__pad=0x20),便于在接收线程中区分 Tx/Rx 并刷新发送指示灯。 /// /// 设备未打开或底层 SetValue 失败。 public void ConfigureAutoSendCan(int channelIndex, ushort taskIndex, bool enable, uint intervalMs, uint canId, byte[] data) { ThrowIfDisposed(); if (_deviceHandle == IntPtr.Zero) { throw new InvalidOperationException("设备未打开。"); } var frame = new ZLGCAN.can_frame { can_id = canId, can_dlc = (byte)Math.Min(8, data?.Length ?? 0), __pad = 0x20, // 默认发送回显 __res0 = 0, __res1 = 0, data = new byte[8] }; if (data != null && data.Length > 0) { Array.Copy(data, frame.data, Math.Min(8, data.Length)); } var obj = new ZLGCAN.ZCAN_AUTO_TRANSMIT_OBJ { enable = (ushort)(enable ? 1 : 0), index = taskIndex, interval = intervalMs, obj = new ZLGCAN.ZCAN_Transmit_Data { frame = frame, transmit_type = 0 } }; var path = string.Format("{0}/auto_send", channelIndex); var ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, ref obj); if (ret != 1) { throw new InvalidOperationException($"配置定时发送失败:{path}"); } } /// /// 设置设备定时发送(CANFD)。 /// /// 通道索引。 /// 定时任务索引。 /// 是否使能。 /// 周期(ms)。 /// can_id。 /// 数据(0~64)。 /// /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/auto_send_canfd。 /// /// 与 一致: /// - 同 index 写入即覆盖更新; /// - interval 为周期(ms); /// - 默认设置发送回显(flags=0x20)。 /// /// 设备未打开或底层 SetValue 失败。 public void ConfigureAutoSendCanFd(int channelIndex, ushort taskIndex, bool enable, uint intervalMs, uint canId, byte[] data) { ThrowIfDisposed(); if (_deviceHandle == IntPtr.Zero) { throw new InvalidOperationException("设备未打开。"); } var frame = new ZLGCAN.canfd_frame { can_id = canId, len = (byte)Math.Min(64, data?.Length ?? 0), flags = 0x20, // 默认发送回显 __res0 = 0, __res1 = 0, data = new byte[64] }; if (data != null && data.Length > 0) { Array.Copy(data, frame.data, Math.Min(64, data.Length)); } var obj = new ZLGCAN.ZCANFD_AUTO_TRANSMIT_OBJ { enable = (ushort)(enable ? 1 : 0), index = taskIndex, interval = intervalMs, obj = new ZLGCAN.ZCAN_TransmitFD_Data { frame = frame, transmit_type = 0 } }; var path = string.Format("{0}/auto_send_canfd", channelIndex); var ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, ref obj); if (ret != 1) { throw new InvalidOperationException($"配置定时发送失败:{path}"); } } /// /// 启动通道定时发送任务(apply_auto_send)。 /// /// 通道索引。 /// /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/apply_auto_send。 /// /// 调用时机: /// - 在完成若干条 auto_send/auto_send_canfd 配置后调用; /// - 设备侧开始按列表条目周期自动发送。 /// /// 设备未打开或底层 SetValue 失败。 public void ApplyAutoSend(int channelIndex) { ThrowIfDisposed(); if (_deviceHandle == IntPtr.Zero) throw new InvalidOperationException("设备未打开。"); var path = string.Format("{0}/apply_auto_send", channelIndex); var ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, "0"); if (ret != 1) { throw new InvalidOperationException($"启动定时发送失败:{path}"); } } /// /// 清空通道定时发送任务(clear_auto_send)。 /// /// 通道索引。 /// /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/clear_auto_send。 /// /// 说明: /// - clear 会清空硬件列表并停止该通道的定时发送; /// - 若你要重新配置调度表,建议先 clear,再从 index=0 重新写入并 apply。 /// /// 设备未打开或底层 SetValue 失败。 public void ClearAutoSend(int channelIndex) { ThrowIfDisposed(); if (_deviceHandle == IntPtr.Zero) throw new InvalidOperationException("设备未打开。"); var path = string.Format("{0}/clear_auto_send", channelIndex); var ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, "0"); if (ret != 1) { throw new InvalidOperationException($"清空定时发送失败:{path}"); } } /// /// 配置 LIN 发布表。 /// /// 发布配置集合。 /// /// - LIN 发布表用于“周期性/按条件发布”LIN 数据(由设备侧完成具体发送时机,取决于驱动能力与配置)。 /// - 本方法会将托管结构体数组拷贝到非托管内存,再调用底层 API。 /// - 若集合为空则直接返回。 /// /// 线程说明: /// - 该方法为同步调用;建议在设备已初始化 LIN 通道后调用。 /// /// LIN 未初始化或底层配置失败。 public void SetLinPublish(IEnumerable publishCfg) { ThrowIfDisposed(); if (_linChannelHandle == IntPtr.Zero) throw new InvalidOperationException("LIN 未初始化。"); var list = publishCfg?.ToList() ?? new List(); if (list.Count == 0) { return; } int size = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_LIN_PUBLISH_CFG)); IntPtr pArr = Marshal.AllocHGlobal(size * list.Count); try { for (int i = 0; i < list.Count; i++) { var item = list[i]; if (item.data == null || item.data.Length != 8) { item.data = new byte[8]; } if (item.reserved == null || item.reserved.Length != 5) { item.reserved = new byte[5]; } Marshal.StructureToPtr(item, IntPtr.Add(pArr, i * size), false); } var ret = ZLGCAN.ZCAN_SetLINPublish(_linChannelHandle, pArr, (uint)list.Count); if (ret != 1) { throw new InvalidOperationException("ZCAN_SetLINPublish 失败。"); } } finally { Marshal.FreeHGlobal(pArr); } } /// /// 配置 LIN 订阅表。 /// /// 订阅配置集合。 /// /// - LIN 订阅表用于配置希望接收/关注的 LIN 帧。 /// - 本方法会将托管结构体数组拷贝到非托管内存,再调用底层 API。 /// - 若集合为空则直接返回。 /// /// LIN 未初始化或底层配置失败。 public void SetLinSubscribe(IEnumerable subscribeCfg) { ThrowIfDisposed(); if (_linChannelHandle == IntPtr.Zero) throw new InvalidOperationException("LIN 未初始化。"); var list = subscribeCfg?.ToList() ?? new List(); if (list.Count == 0) { return; } int size = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_LIN_SUBSCIBE_CFG)); IntPtr pArr = Marshal.AllocHGlobal(size * list.Count); try { for (int i = 0; i < list.Count; i++) { var item = list[i]; if (item.reserved == null || item.reserved.Length != 5) { item.reserved = new byte[5]; } Marshal.StructureToPtr(item, IntPtr.Add(pArr, i * size), false); } var ret = ZLGCAN.ZCAN_SetLINSubscribe(_linChannelHandle, pArr, (uint)list.Count); if (ret != 1) { throw new InvalidOperationException("ZCAN_SetLINSubscribe 失败。"); } } finally { Marshal.FreeHGlobal(pArr); } } /// /// 发送一帧 LIN 报文。 /// /// 通道号(通常为 0)。 /// 受保护 ID(PID,包含奇偶校验位)。 /// 数据区(0-8字节)。 /// 方向(通常 0;具体含义以 ZLG 文档为准)。 /// /// 说明: /// - 当前工程使用 merge 接收()读取 LIN 帧,因此这里的发送只负责向设备下发原始帧; /// - ZLG 的 LIN 发送 API 以 数组形式接收数据;本方法将 PID+数据按 的内存布局写入。 /// public void TransmitLin(int channel, byte pid, ReadOnlySpan data, byte dir = 0) { ThrowIfDisposed(); if (_linChannelHandle == IntPtr.Zero) throw new InvalidOperationException("LIN 未初始化。"); var len = Math.Min(8, data.Length); var buf8 = new byte[8]; if (len > 0) { data.Slice(0, len).CopyTo(buf8); } var linData = new ZLGCAN.ZCANLINData { pid = new ZLGCAN.PID { rawVal = pid }, rxData = new ZLGCAN.RxData { timeStamp = 0, datalen = (byte)len, dir = dir, chkSum = 0, reserved = new byte[13], data = buf8 }, reserved = new byte[7] }; var msg = new ZLGCAN.ZCAN_LIN_MSG { chnl = (byte)channel, dataType = 0, data = new byte[46] }; var linSize = Marshal.SizeOf(typeof(ZLGCAN.ZCANLINData)); IntPtr pLin = Marshal.AllocHGlobal(linSize); try { Marshal.StructureToPtr(linData, pLin, false); var copy = Math.Min(msg.data.Length, linSize); Marshal.Copy(pLin, msg.data, 0, copy); } finally { Marshal.FreeHGlobal(pLin); } var msgSize = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_LIN_MSG)); IntPtr pMsg = Marshal.AllocHGlobal(msgSize); try { Marshal.StructureToPtr(msg, pMsg, false); var ret = ZLGCAN.ZCAN_TransmitLIN(_linChannelHandle, pMsg, 1); if (ret == 0) { throw new InvalidOperationException("ZCAN_TransmitLIN 失败。"); } MarkSendOk(); } finally { Marshal.FreeHGlobal(pMsg); } } /// /// 关闭设备。 /// /// /// 资源释放顺序(概览): /// - 停止接收线程(); /// - Reset LIN/CAN 通道并关闭设备句柄(见 ); /// - 取消 CmdData 事件订阅并释放 DBC 资源。 /// /// 线程安全: /// - 该方法通过 保护,避免并发 close/load/receive 导致状态错乱。 /// public void Close() { lock (_sync) { // 先停接收线程: // - 接收线程内部会访问 device_handle/channel_handle; // - 若不先停线程就 Reset/Close 句柄,极易触发访问已释放句柄导致异常或崩溃。 StopReceiveLoop(); // 再按通道/设备顺序释放句柄: // - ResetLIN/ResetCAN 让驱动停止通道并回收内部资源 // - CloseDevice 释放 device_handle SafeClose_NoLock(); } lock (_dbcSync) { // Close 过程同时断开“事件驱动发送”链路: // - 避免 UI 仍在更新 SignalCmdValue 时触发驱动调用(此时句柄可能已关闭); // - 也避免 _cmdData 持有 Driver 事件导致对象无法被 GC 回收。 if (_cmdData != null) { foreach (var item in _cmdData) { item.CanCmdDataChangedHandler -= CmdData_CanCmdDataChangedHandler; } } _cmdData = null; _dbcModels = null; _dbcModelIndex = null; _dbc?.Dispose(); _dbc = null; } } /// /// /// 遵循 .NET Dispose 语义: /// - 多次调用是安全的(通过 做幂等保护); /// - Dispose 内部会调用 释放驱动句柄、停止接收线程并释放 DBC 资源。 /// public void Dispose() { if (_disposed) return; _disposed = true; try { // 说明:Dispose 不抛异常,避免在 using/终结路径打断上层释放流程。 // Close 内部已经尽量记录日志并吞掉部分关闭异常,保证释放流程走完。 Close(); } catch { // ignored } } /// /// 关闭设备与通道句柄(不加锁版本)。 /// /// /// 该方法假定调用方已经持有 : /// - 用于 Close 流程中按顺序 Reset LIN、Reset CAN、CloseDevice; /// - 任何异常都会被吞掉并记录 Warn,确保关闭流程尽量走完,避免句柄泄漏。 /// private void SafeClose_NoLock() { try { if (_linChannelHandle != IntPtr.Zero) { // ResetLIN 语义:停止 LIN 通道并释放其内部缓存/线程。 var ret = ZLGCAN.ZCAN_ResetLIN(_linChannelHandle); if (ret != 1) { _log.Warn("ZCAN_ResetLIN 失败。"); } } } catch (Exception ex) { _log.Warn($"关闭 LIN 异常:{ex.Message}"); } finally { _linChannelHandle = IntPtr.Zero; } for (int i = 0; i < _canChannelHandles.Length; i++) { try { if (_canChannelHandles[i] != IntPtr.Zero) { // ResetCAN 语义:停止 CAN/CANFD 通道。 // 注意:若硬件 auto_send 正在发送,建议在服务层先调用 StopAutoSendSchedule(clear_auto_send)。 // 当前 Close 不主动 clear_auto_send 的原因: // - auto_send 是 per-channel 的“设备侧任务”,通常由上层 Start/StopSchedule 管控; // - Close 仍会 ResetCAN/CloseDevice,通常也会导致任务失效,但具体行为依赖 SDK/硬件。 var ret = ZLGCAN.ZCAN_ResetCAN(_canChannelHandles[i]); if (ret != 1) { _log.Warn($"ZCAN_ResetCAN 失败,通道{i}。"); } } } catch (Exception ex) { _log.Warn($"关闭 CAN 通道{i}异常:{ex.Message}"); } finally { _canChannelHandles[i] = IntPtr.Zero; } } try { if (_deviceHandle != IntPtr.Zero) { var ret = ZLGCAN.ZCAN_CloseDevice(_deviceHandle); if (ret != 1) { _log.Warn("ZCAN_CloseDevice 失败。"); } } } catch (Exception ex) { _log.Warn($"关闭设备异常:{ex.Message}"); } finally { _deviceHandle = IntPtr.Zero; OpenState = false; } } /// /// 初始化指定 CANFD 通道(仅设置参数并 InitCAN,不负责 StartCAN)。 /// /// 通道索引。 /// 通道初始化参数。 /// /// 该方法主要完成: /// - 设置仲裁域/数据域波特率; /// - 设置终端电阻与总线利用率统计开关; /// - 调用 ZCAN_InitCAN 获取通道句柄并保存到 。 /// /// 注意: /// - 这里仅负责 Init,不负责 Start;Start 由 完成。 /// /// 任一 SetValue/InitCAN 失败。 private void InitCanChannelInternal(int chnIdx, ZlgCanFdChannelOptions options) { var path = string.Format("{0}/canfd_abit_baud_rate", chnIdx); var ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, options.ArbitrationBaudRate.ToString()); if (ret != 1) { throw new InvalidOperationException($"设置仲裁域波特率失败:{path}"); } path = string.Format("{0}/canfd_dbit_baud_rate", chnIdx); ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, options.DataBaudRate.ToString()); if (ret != 1) { throw new InvalidOperationException($"设置数据域波特率失败:{path}"); } path = string.Format("{0}/initenal_resistance", chnIdx); ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, options.EnableInternalResistance ? "1" : "0"); if (ret != 1) { throw new InvalidOperationException($"设置终端电阻失败:{path}"); } if (options.EnableBusUsage) { path = string.Format("{0}/set_bus_usage_enable", chnIdx); ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, "1"); if (ret != 1) { _log.Warn($"启用总线利用率失败:{path}"); } path = string.Format("{0}/set_bus_usage_period", chnIdx); ret = ZLGCAN.ZCAN_SetValue(_deviceHandle, path, options.BusUsagePeriodMs.ToString()); if (ret != 1) { _log.Warn($"设置总线利用率周期失败:{path}"); } } var initCfg = new ZLGCAN.ZCAN_CHANNEL_INIT_CONFIG { can_type = 1, config = new ZLGCAN._ZCAN_CHANNEL_INIT_CONFIG { canfd = new ZLGCAN._ZCAN_CHANNEL_CANFD_INIT_CONFIG { acc_code = 0, acc_mask = 0xFFFFFFFF, abit_timing = 0, dbit_timing = 0, brp = 0, filter = 0, mode = (byte)(options.ListenOnly ? 1 : 0), pad = 0, reserved = 0 } } }; var chnHandle = ZLGCAN.ZCAN_InitCAN(_deviceHandle, (uint)chnIdx, ref initCfg); if (chnHandle == IntPtr.Zero) { throw new InvalidOperationException($"初始化 CANFD 通道失败:{chnIdx}"); } _canChannelHandles[chnIdx] = chnHandle; } /// /// 启动指定 CAN 通道。 /// /// 通道索引。 /// 通道句柄。 /// /// 调用前置条件: /// - 已成功执行并产生通道句柄。 /// /// 通道句柄为空或启动失败。 private IntPtr StartCanChannelInternal(int chnIdx) { var h = _canChannelHandles[chnIdx]; if (h == IntPtr.Zero) { throw new InvalidOperationException("CAN 通道句柄为空。"); } var ret = ZLGCAN.ZCAN_StartCAN(h); if (ret != 1) { throw new InvalidOperationException($"启动 CAN 通道失败:{chnIdx}"); } return h; } /// /// 获取 CAN 通道句柄(不存在则抛异常)。 /// /// 通道索引。 /// 通道句柄。 /// 通道索引超范围。 /// 通道未初始化。 private IntPtr GetCanChannelHandleOrThrow(int channelIndex) { if (channelIndex < 0 || channelIndex >= _canChannelHandles.Length) { throw new ArgumentOutOfRangeException(nameof(channelIndex)); } var h = _canChannelHandles[channelIndex]; if (h == IntPtr.Zero) { throw new InvalidOperationException($"CAN 通道{channelIndex}未初始化。"); } return h; } /// /// 普通接收模式:按通道轮询 CAN/CANFD 缓冲区读取。 /// /// 取消令牌。 /// 每次接收的最大帧数。 /// /// - 该模式分别调用: /// - ZCAN_GetReceiveNum/ZCAN_Receive 接收 CAN /// - ZCAN_GetReceiveNum/ZCAN_ReceiveFD 接收 CANFD /// - 每次接收会在非托管堆上分配固定大小缓冲,循环中复用,退出时释放。 /// private void ReceiveLoop_PerChannel(CancellationToken token, int bufferFrames) { const int WaitMs = 10; // 说明:ZLG SDK 的 Receive/ReceiveFD 需要调用方提供一段连续的非托管内存作为数组缓冲区。 // 因为接收线程会长期运行,所以这里采用“循环外一次性分配 + 循环内复用 + finally 释放”。 int canStructSize = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_Receive_Data)); int canfdStructSize = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_ReceiveFD_Data)); IntPtr pCanBuffer = Marshal.AllocHGlobal(canStructSize * bufferFrames); IntPtr pCanfdBuffer = Marshal.AllocHGlobal(canfdStructSize * bufferFrames); try { while (!token.IsCancellationRequested) { for (int ch = 0; ch < _canChannelHandles.Length; ch++) { var handle = _canChannelHandles[ch]; if (handle == IntPtr.Zero) continue; // 0:CAN 经典帧缓冲区;1:CANFD 缓冲区(ZLG SDK 约定)。 // 注意:GetReceiveNum 返回的是“当前缓存中可读数量”,但真正 Receive 返回的 actual // 可能小于请求值(例如 bufferFrames 太小或并发到达)。 uint canNum = ZLGCAN.ZCAN_GetReceiveNum(handle, 0); if (canNum > 0) { uint actual = ZLGCAN.ZCAN_Receive(handle, pCanBuffer, (uint)bufferFrames, WaitMs); for (int i = 0; i < actual; i++) { IntPtr pCur = IntPtr.Add(pCanBuffer, i * canStructSize); var rec = Marshal.PtrToStructure(pCur); // Tx 回显判断:当前实现沿用项目既有逻辑,使用 __pad 的 0x20 位。 // - 该位含义取决于 ZLG SDK 结构体定义/版本;若未来 SDK 变更,该判断可能需要同步调整。 RaiseCanFrame(ch, rec.timestamp, false, rec.frame.can_id, rec.frame.data, rec.frame.can_dlc, (rec.frame.__pad & 0x20) == 0x20); } } uint canfdNum = ZLGCAN.ZCAN_GetReceiveNum(handle, 1); if (canfdNum > 0) { uint actual = ZLGCAN.ZCAN_ReceiveFD(handle, pCanfdBuffer, (uint)bufferFrames, WaitMs); for (int i = 0; i < actual; i++) { IntPtr pCur = IntPtr.Add(pCanfdBuffer, i * canfdStructSize); var rec = Marshal.PtrToStructure(pCur); // Tx 回显判断:使用 flags 的 0x20 位(同样依赖 SDK 定义)。 RaiseCanFrame(ch, rec.timestamp, true, rec.frame.can_id, rec.frame.data, rec.frame.len, (rec.frame.flags & 0x20) == 0x20); } } ZLGCAN.ZCAN_CHANNEL_ERR_INFO err = new ZLGCAN.ZCAN_CHANNEL_ERR_INFO { error_code = 0, passive_ErrData = new byte[3], arLost_ErrData = 0 }; try { ZLGCAN.ZCAN_ReadChannelErrInfo(handle, ref err); if (err.error_code != 0) { _log.Warn($"CAN通道{ch}错误码:0x{err.error_code:X}"); } } catch { // ignore } } // 轻量节流:避免无数据时空转占满 CPU。 // 注意:这会引入 10ms 级额外接收延迟;若有更高实时性需求,应改为事件/阻塞式等待(SDK 支持时)。 Thread.Sleep(10); } } finally { Marshal.FreeHGlobal(pCanfdBuffer); Marshal.FreeHGlobal(pCanBuffer); } } /// /// 合并接收模式:通过设备级 API(ZCAN_ReceiveData)统一接收 CAN/CANFD/LIN。 /// /// 取消令牌。 /// 接收缓存容量。 /// /// - merge 接收模式通常用于高吞吐场景:由驱动统一将各类数据封装为 DataObj; /// - 本方法根据 dataType 分流: /// - 1:CAN/CANFD /// - 4:LIN /// - 触发事件仍通过 / 。 /// private void ReceiveLoop_Merge(CancellationToken token, int bufferFrames) { int dataObjSize = Marshal.SizeOf(typeof(ZLGCAN.ZCANDataObj)); int canfdSize = Marshal.SizeOf(typeof(ZLGCAN.ZCANCANFDData)); int linSize = Marshal.SizeOf(typeof(ZLGCAN.ZCANLINData)); // 合并接收模式的内存模型: // - pDataObjs:ZCANDataObj 数组(每个元素描述一条数据,含 dataType/chnl/data)。 // - obj.data 为托管 byte[],为了用 PtrToStructure 解析为结构体,这里做一次 Marshal.Copy 到临时非托管缓冲。 // 这样可以避免 unsafe/pin,但会增加一次拷贝开销;在高吞吐场景下 bufferFrames 应适当调大。 IntPtr pDataObjs = Marshal.AllocHGlobal(dataObjSize * bufferFrames); IntPtr pCanfdBuffer = Marshal.AllocHGlobal(canfdSize); IntPtr pLinBuffer = Marshal.AllocHGlobal(linSize); try { while (!token.IsCancellationRequested) { // 2:device 级合并接收缓冲区(ZLG SDK 约定)。 // 注意:合并接收开关需要在通道初始化时通过 0/set_device_recv_merge 开启(见 OpenAndInitCan)。 uint recvNum = ZLGCAN.ZCAN_GetReceiveNum(_deviceHandle, 2); if (recvNum == 0) { Thread.Sleep(10); continue; } uint actualRecv = ZLGCAN.ZCAN_ReceiveData(_deviceHandle, pDataObjs, (uint)bufferFrames, 10); if (actualRecv == 0) { continue; } for (int i = 0; i < actualRecv; i++) { IntPtr pCur = IntPtr.Add(pDataObjs, i * dataObjSize); var obj = Marshal.PtrToStructure(pCur); switch (obj.dataType) { case 1: // dataType=1:CAN/CANFD 统一封装(ZCANCANFDData)。 // - flag bit0:是否为 CANFD // - flag bit9:是否为 Tx echo(项目中用来点亮发送指示灯) Marshal.Copy(obj.data, 0, pCanfdBuffer, canfdSize); var canfdData = Marshal.PtrToStructure(pCanfdBuffer); bool isFd = (canfdData.flag & 1) == 1; bool isTx = (canfdData.flag & (1 << 9)) != 0; RaiseCanFrame(obj.chnl, canfdData.timeStamp, isFd, canfdData.frame.can_id, canfdData.frame.data, canfdData.frame.len, isTx); break; case 4: // dataType=4:LIN(ZCANLINData)。 if (obj.data == null || obj.data.Length < linSize) { break; } Marshal.Copy(obj.data, 0, pLinBuffer, linSize); var linData = Marshal.PtrToStructure(pLinBuffer); RaiseLinFrame(obj.chnl, linData.rxData.timeStamp, linData.pid.rawVal, linData.rxData.data, linData.rxData.datalen, linData.rxData.dir); break; } } // 微节流:在持续高流量下仍可能较占 CPU;该 sleep 也会影响“批次间”延迟。 Thread.Sleep(1); } } finally { Marshal.FreeHGlobal(pLinBuffer); Marshal.FreeHGlobal(pCanfdBuffer); Marshal.FreeHGlobal(pDataObjs); } } /// /// 将接收到的 CAN/CANFD 帧转换为托管对象并触发事件。 /// /// 通道号。 /// 时间戳(us)。 /// 是否 CANFD。 /// can_id(含标志位)。 /// 原始数据缓冲区。 /// 数据长度。 /// 是否为发送回显(Tx)。 /// /// 事件派发与 DBC 解码: /// - 始终先触发 抛出原始帧; /// - 再依据 决定是否进行 DBC 解码并刷新 。 /// /// 发送/接收指示: /// - 若 为 true,调用 ;否则调用 。 /// private void RaiseCanFrame(int channel, ulong timestamp, bool isCanFd, uint canId, byte[] data, byte dlc, bool isTx) { try { var len = Math.Min(isCanFd ? 64 : 8, Math.Min((int)dlc, data?.Length ?? 0)); var bytes = new byte[len]; if (len > 0 && data != null) { Array.Copy(data, bytes, len); } CanFrameReceived?.Invoke(new ZlgCanRxFrame(channel, isCanFd, canId, bytes, timestamp, isTx)); // 说明: // - UI 的“接收”指示灯通常表达“收到了总线帧/驱动回调在工作”,因此包括 Tx echo 在内的任意帧均点亮接收; // - Tx echo 仍额外点亮“发送”指示灯。 MarkReviceOk(); if (isTx) { MarkSendOk(); } if (DbcDecodeEnabled) { TryDecodeAndUpdateModels(canId, bytes, isCanFd ? (byte)ZDBC.FT_CANFD : (byte)ZDBC.FT_CAN); } } catch (Exception ex) { _log.Warn($"派发 CAN 帧事件异常:{ex.Message}"); } } /// /// 将接收到的 LIN 帧转换为托管对象并触发事件。 /// /// 通道号。 /// 时间戳(us)。 /// PID。 /// 原始数据缓冲区。 /// 数据长度。 /// 方向(由设备回传)。 /// /// - LIN 数据长度最大 8 字节,本方法会按 截断并复制; /// - 事件在接收线程触发,订阅方若更新 UI,请自行切换线程。 /// private void RaiseLinFrame(int channel, ulong timestamp, byte pid, byte[] data, byte datalen, byte dir) { try { var len = Math.Min(8, Math.Min((int)datalen, data?.Length ?? 0)); var bytes = new byte[len]; if (len > 0 && data != null) { Array.Copy(data, bytes, len); } LinFrameReceived?.Invoke(new ZlgLinRxFrame(channel, pid, bytes, timestamp, dir)); } catch (Exception ex) { _log.Warn($"派发 LIN 帧事件异常:{ex.Message}"); } } /// /// 校验指定原生 DLL 是否存在于程序输出目录(AppContext.BaseDirectory)。 /// /// DLL 文件名。 /// 找不到 DLL。 private static void EnsureNativeDllExists(string dllName) { var baseDir = AppContext.BaseDirectory; var full = Path.Combine(baseDir, dllName); if (!File.Exists(full)) { throw new FileNotFoundException($"未找到 {dllName},请将其复制到程序输出目录:{baseDir}", full); } // 预加载的目的: // - 明确从输出目录加载指定 DLL,尽量避免被 PATH 中其它目录的同名 DLL“劫持”; // - 发生依赖缺失/入口点不存在等异常时,记录在 _preloadedZlgcanError,便于 OpenDevice 失败时诊断。 TryPreloadNativeDll(full); if (_preloadedZlgcanHandle == IntPtr.Zero && !string.IsNullOrWhiteSpace(_preloadedZlgcanError)) { throw new InvalidOperationException($"预加载 {dllName} 失败:{_preloadedZlgcanError}。DLL 路径:{full}"); } // 位数匹配检查: // - 许多现场问题来自 x86/x64 不匹配; // - 这里通过读取 PE 头 machine 字段做静态判断,避免到 P/Invoke 时才抛 BadImageFormatException。 // 注意:这只能检查“主 DLL”的架构,不能保证其依赖 DLL(kerneldlls 下的其它库)都齐全且位数一致。 var dllArch = TryGetPeArchitecture(full); if (!string.IsNullOrWhiteSpace(dllArch)) { var procArch = Environment.Is64BitProcess ? "x64" : "x86"; if (Environment.Is64BitProcess && !string.Equals(dllArch, "x64", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x64 版本 DLL,或将程序改为 x86 运行。DLL 路径:{full}"); } if (!Environment.Is64BitProcess && !string.Equals(dllArch, "x86", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x86 版本 DLL,或将程序改为 x64 运行。DLL 路径:{full}"); } } } /// /// 尝试读取 PE 头判断 DLL 架构。 /// /// DLL 完整路径。 /// 返回 "x86"/"x64"/"arm64"/"unknown(...)";读取失败返回 null。 private static string? TryGetPeArchitecture(string dllFullPath) { try { using var fs = new FileStream(dllFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var br = new BinaryReader(fs); if (fs.Length < 0x40) { return null; } var mz = br.ReadUInt16(); if (mz != 0x5A4D) { return null; } // e_lfanew:DOS Header + 0x3C,指向 PE Header 起始偏移。 fs.Position = 0x3C; var peOffset = br.ReadInt32(); if (peOffset <= 0 || peOffset > fs.Length - 6) { return null; } // PE signature:"PE\0\0" => 0x00004550。 fs.Position = peOffset; var peSig = br.ReadUInt32(); if (peSig != 0x00004550) { return null; } // IMAGE_FILE_HEADER.Machine: // - 0x014c = x86 // - 0x8664 = x64 // - 0xAA64 = arm64 var machine = br.ReadUInt16(); return machine switch { 0x014c => "x86", 0x8664 => "x64", 0xAA64 => "arm64", _ => $"unknown(0x{machine:X})" }; } catch { return null; } } /// /// 构建 DBC 解码更新用的索引表。 /// /// 所有信号模型。 /// 键为 (MsgName, SignalName) 的组合键,值为对应的 /// /// - 接收线程解码得到 (msgName, signalName) 后,可 O(1) 定位到 UI 模型进行更新; /// - 组合键使用 ,避免字符串拼接歧义。 /// private static Dictionary BuildDbcModelIndex(IEnumerable models) { var dict = new Dictionary(StringComparer.Ordinal); foreach (var m in models) { if (string.IsNullOrWhiteSpace(m.MsgName) || string.IsNullOrWhiteSpace(m.SignalName)) { continue; } dict[BuildMsgSigKey(m.MsgName, m.SignalName)] = m; } return dict; } /// /// 构建 (MsgName, SignalName) 的唯一组合键。 /// /// 消息名。 /// 信号名。 /// 组合键字符串。 /// /// 使用 '\0' 作为分隔符,降低与业务字符串冲突的风险。 /// private static string BuildMsgSigKey(string msgName, string signalName) { return msgName + "\u0000" + signalName; } /// /// 尝试对原始帧做 DBC 解码,并将解码结果写回到对应的 实例。 /// /// CAN ID(含标志位)。 /// 原始数据。 /// 帧类型(CAN/CANFD)。 /// /// - 本方法被 在接收线程中调用; /// - 若 DBC 未加载或索引表为空,会直接返回; /// - 解码后按 signalName 更新 (字符串形式)。 /// private void TryDecodeAndUpdateModels(uint canId, byte[] payload, byte frameType) { ZlgDbcDatabase? dbc; Dictionary? index; lock (_dbcSync) { dbc = _dbc; index = _dbcModelIndex; } if (dbc == null || !dbc.IsLoaded || index == null) { return; } try { var (msgName, signals) = dbc.DecodeRawFrame(canId, payload, frameType); if (string.IsNullOrWhiteSpace(msgName) || signals == null || signals.Count == 0) { return; } foreach (var kv in signals) { var key = BuildMsgSigKey(msgName, kv.Key); if (index.TryGetValue(key, out var model)) { model.SignalRtValue = kv.Value.ToString(); } } } catch (Exception ex) { _log.Warn($"DBC 解码异常:{ex.Message}"); } } /// /// 若对象已释放则抛异常。 /// private void ThrowIfDisposed() { if (_disposed) { throw new ObjectDisposedException(nameof(ZlgCanFd200uDriver)); } } } /// /// CAN/CANFD 接收帧。 /// public readonly struct ZlgCanRxFrame { /// /// 构造 CAN/CANFD 接收帧。 /// /// 通道号。 /// 是否 CANFD。 /// can_id(含标志位)。 /// 数据(已截断为实际长度)。 /// 时间戳(us)。 /// 是否为发送回显。 public ZlgCanRxFrame(int channel, bool isCanFd, uint canId, byte[] data, ulong timestampUs, bool isTx) { Channel = channel; IsCanFd = isCanFd; CanId = canId; Data = data; TimestampUs = timestampUs; IsTx = isTx; } /// /// 通道号(0/1)。 /// public int Channel { get; } /// /// 是否为 CANFD 帧。 /// public bool IsCanFd { get; } /// /// can_id(包含扩展帧/错误帧等标志位,具体位定义见 ZLG 文档)。 /// public uint CanId { get; } /// /// 数据区(已按实际长度截断拷贝)。 /// public byte[] Data { get; } /// /// 时间戳(微秒)。 /// public ulong TimestampUs { get; } /// /// 是否为发送回显(Tx)。 /// public bool IsTx { get; } } /// /// LIN 接收帧。 /// public readonly struct ZlgLinRxFrame { /// /// 构造 LIN 接收帧。 /// /// 通道号。 /// PID。 /// 数据(已截断为实际长度)。 /// 时间戳(us)。 /// 方向(由设备回传)。 public ZlgLinRxFrame(int channel, byte pid, byte[] data, ulong timestampUs, byte dir) { Channel = channel; Pid = pid; Data = data; TimestampUs = timestampUs; Dir = dir; } /// /// 通道号。 /// public int Channel { get; } /// /// PID。 /// public byte Pid { get; } /// /// 数据区(已按实际长度截断拷贝)。 /// public byte[] Data { get; } /// /// 时间戳(微秒)。 /// public ulong TimestampUs { get; } /// /// 方向:由设备回传,0/1 具体含义以 ZLG 文档为准。 /// public byte Dir { get; } } }