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; }
}
}