Files
CapMachine/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs
2026-03-02 11:20:08 +08:00

2594 lines
105 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 周立功 USBCANFD-200U 工程化封装CAN/CANFD/LIN
/// </summary>
/// <remarks>
/// 设计目标:
/// - 把 ZLG 原生 DLLzlgcan.dll/zdbc.dll 等)调用封装为可被 WPF/Service 层直接消费的 .NET API
/// - 统一承载 CAN/CANFD/LIN 三类通道的“打开/初始化/接收/发送/关闭”生命周期;
/// - 支持两类发送:
/// - 普通发送:<see cref="SendCan"/> / <see cref="SendCanFd"/> / <see cref="TransmitLin"/>
/// - 事件驱动发送:由 <see cref="LoadCmdDataToDrive"/> 订阅 <see cref="CanCmdData.CanCmdDataChangedHandler"/>,在信号值变化时增量下发;
/// - 支持硬件定时发送(调度表):
/// - 通过 <see cref="StartAutoSendSchedule"/> 下发 auto_send 列表并 apply
/// - 运行中通过 MsgName-&gt;Index 映射实现“值变化时覆盖更新同 index 的帧数据”(不改变周期)。
///
/// 线程与资源(务必理解):
/// - 接收线程由 <see cref="StartReceiveLoop"/> 创建,回调事件在后台线程触发;
/// - DBC 编码/解码与 CmdData 注入受 <see cref="_dbcSync"/> 保护,避免接收线程与 UI 线程并发访问同一 DBC 对象;
/// - 设备句柄/通道句柄/接收线程生命周期受 <see cref="_sync"/> 保护,避免并发 Open/Close 导致句柄泄漏或 AccessViolation
/// - auto_send硬件定时发送状态受 <see cref="_autoSendSync"/> 保护,避免 UI 启停与事件覆盖更新并发冲突。
/// </remarks>
public sealed class ZlgCanFd200uDriver : BindableBase, IDisposable
{
/// <summary>
/// 驱动全局互斥锁。
/// </summary>
/// <remarks>
/// 用于保护“设备句柄/通道句柄/接收线程”的生命周期操作:
/// - Open/Init/Start/Reset/Close
/// - StartReceiveLoop/StopReceiveLoop
/// 避免多线程同时开关导致句柄泄漏、AccessViolation 或状态错乱。
/// </remarks>
private readonly object _sync = new object();
/// <summary>
/// 日志服务(用于输出驱动调用、异常、诊断信息)。
/// </summary>
private readonly ILogService _log;
/// <summary>
/// 硬件定时发送(auto_send)相关状态互斥锁。
/// </summary>
/// <remarks>
/// auto_send 的配置/覆盖更新可能来自不同线程:
/// - UI 线程点击“循环发送/调度表”触发 StartAutoSendSchedule
/// - CmdDataChanged 事件线程触发覆盖更新
/// 因此必须对“是否启用、映射表、通道/帧类型”等共享状态加锁。
/// </remarks>
private readonly object _autoSendSync = new object();
/// <summary>
/// 当前是否处于硬件定时发送(auto_send)任务运行态。
/// </summary>
private bool _autoSendActive;
/// <summary>
/// 当前硬件定时发送使用的通道索引0/1
/// </summary>
private int _autoSendChannelIndex;
/// <summary>
/// 当前硬件定时发送使用的帧类型(<see cref="ZDBC.FT_CAN"/> 或 <see cref="ZDBC.FT_CANFD"/>)。
/// </summary>
private byte _autoSendFrameType;
/// <summary>
/// MsgName -&gt; auto_send 任务索引列表的映射。
/// </summary>
/// <remarks>
/// 用于“信号值变化时覆盖更新对应定时发送任务”。
/// - 一个 MsgName 可能对应多个条目(例如配置了多个相同 MsgName 的调度表行)。
/// - 覆盖更新时通过 <see cref="ZLGCAN.ZCAN_SetValue"/> 重新写入同 index 的 auto_send 结构体达到更新效果。
/// </remarks>
private readonly Dictionary<string, List<ushort>> _autoSendMsgIndex = new Dictionary<string, List<ushort>>(StringComparer.Ordinal);
/// <summary>
/// 预加载的 zlgcan.dll 模块句柄。
/// </summary>
/// <remarks>
/// 用于在 <see cref="EnsureNativeDllExists"/> 中强制从输出目录加载指定 DLL减少被 PATH 中同名 DLL 干扰的风险。
/// </remarks>
private static IntPtr _preloadedZlgcanHandle = IntPtr.Zero;
/// <summary>
/// 预加载 zlgcan.dll 失败时记录的错误信息。
/// </summary>
/// <remarks>
/// - 若预加载失败,后续 OpenDevice 失败时会把该信息拼接到异常提示中,便于定位依赖缺失/位数不匹配等问题。
/// </remarks>
private static string? _preloadedZlgcanError;
/// <summary>
/// 设备句柄ZCAN_OpenDevice 返回)。
/// </summary>
/// <remarks>
/// - 设备句柄是 CAN/CANFD/LIN 共用的“根句柄”;
/// - auto_send 配置也通过 device_handle + path 进行 SetValue。
/// </remarks>
private IntPtr _deviceHandle = IntPtr.Zero;
/// <summary>
/// CAN/CANFD 通道句柄数组(最多 2 通道)。
/// </summary>
private readonly IntPtr[] _canChannelHandles = new IntPtr[2];
/// <summary>
/// LIN 通道句柄。
/// </summary>
private IntPtr _linChannelHandle = IntPtr.Zero;
/// <summary>
/// 接收线程取消令牌。
/// </summary>
private CancellationTokenSource? _recvCts;
/// <summary>
/// 接收循环任务(后台线程)。
/// </summary>
private Task? _recvTask;
/// <summary>
/// 对象是否已释放。
/// </summary>
private volatile bool _disposed;
/// <summary>
/// DBC 数据结构互斥锁。
/// </summary>
/// <remarks>
/// 用于保护以下共享状态的并发读写:
/// - <see cref="_dbc"/> 的加载/释放
/// - <see cref="_dbcModels"/> 与 <see cref="_dbcModelIndex"/> 的替换
/// - <see cref="_cmdData"/> 的注入与事件订阅/退订
///
/// 接收线程在解码时会读取 DBC 与索引,因此这里必须保证读取到的是一致快照。
/// </remarks>
private readonly object _dbcSync = new object();
/// <summary>
/// DBC 数据库实例。
/// </summary>
/// <remarks>
/// 该对象封装 zdbc.dll 的加载/解析能力,用于:
/// - 接收帧解析为信号值(更新 UI 的 <see cref="CanDbcModel"/>
/// - 发送时把 CmdData 的信号值编码为原始帧数据(见 <see cref="SendOneMsgByCmdData"/>
/// </remarks>
private ZlgDbcDatabase? _dbc;
/// <summary>
/// DBC 解析后的“信号模型集合”(用于 UI 绑定)。
/// </summary>
private ObservableCollection<CanDbcModel>? _dbcModels;
/// <summary>
/// SignalName -&gt; 信号模型的索引,加速更新(接收解码更新时 O(1) 查找)。
/// </summary>
private Dictionary<string, CanDbcModel>? _dbcModelIndex;
/// <summary>
/// 当前用于发送的指令集合(来源于配置程序+读写设置,外部通过 LoadCmdDataToDrive 注入)。
/// </summary>
/// <remarks>
/// 该集合的每个元素通常对应一个 Signal含 MsgName/SignalName/SignalCmdValue
/// - 事件驱动发送:订阅每个元素的 CanCmdDataChangedHandler在信号变化时增量下发。
/// - 编码发送:按 MsgName 聚合所有 SignalName/SignalCmdValue再交给 DBC 编码。
/// </remarks>
private List<CanCmdData>? _cmdData;
/// <summary>
/// CmdData 编码并发送时使用的通道索引。
/// </summary>
private int _cmdSendChannelIndex;
/// <summary>
/// CmdData 编码并发送时使用的帧类型CAN/CANFD
/// </summary>
private byte _cmdSendFrameType;
/// <summary>
/// 构造函数。
/// </summary>
/// <param name="logService">日志服务。</param>
public ZlgCanFd200uDriver(ILogService logService)
{
_log = logService;
}
/// <summary>
/// 设备打开状态的 backing 字段。
/// </summary>
/// <remarks>
/// 对外通过 <see cref="OpenState"/> 暴露。
/// - 仅在驱动内部状态切换时写入(例如 <see cref="OpenDevice"/>/<see cref="Close"/>
/// - 使用 <see cref="RaisePropertyChanged"/> 通知 UI 刷新。
/// </remarks>
private bool _openState;
/// <summary>
/// 设备打开状态。
/// </summary>
public bool OpenState
{
get { return _openState; }
private set { _openState = value; RaisePropertyChanged(); }
}
/// <summary>
/// 接收线程运行状态的 backing 字段。
/// </summary>
private bool _isReceiving;
/// <summary>
/// 是否正在接收。
/// </summary>
public bool IsReceiving
{
get { return _isReceiving; }
private set { _isReceiving = value; RaisePropertyChanged(); }
}
/// <summary>
/// 发送指示灯状态的 backing 字段。
/// </summary>
private bool _isSendOk;
/// <summary>
/// 最近是否发生过“发送成功”(用于 UI 指示灯)。
/// </summary>
public bool IsSendOk
{
get { return _isSendOk; }
private set { _isSendOk = value; RaisePropertyChanged(); }
}
/// <summary>
/// 接收指示灯状态的 backing 字段。
/// </summary>
private bool _isReviceOk;
/// <summary>
/// 最近是否发生过“接收成功”(用于 UI 指示灯)。
/// </summary>
public bool IsReviceOk
{
get { return _isReviceOk; }
private set { _isReviceOk = value; RaisePropertyChanged(); }
}
/// <summary>
/// 发送指示灯更新 token。
/// </summary>
/// <remarks>
/// 用于解决“短时间内多次 MarkSendOk 导致的延迟回落互相覆盖”问题:
/// - 每次触发都会递增 token
/// - 延迟回落时仅当 token 未变化才会把 <see cref="IsSendOk"/> 置回 false。
/// </remarks>
private int _sendOkToken;
/// <summary>
/// 接收指示灯更新 token。
/// </summary>
/// <remarks>
/// 语义同 <see cref="_sendOkToken"/>,用于 <see cref="IsReviceOk"/> 的延迟回落保护。
/// </remarks>
private int _reviceOkToken;
/// <summary>
/// 将 <see cref="IsSendOk"/> 置为 true 并在一段时间后自动恢复为 false。
/// </summary>
/// <param name="holdMs">保持 true 的时间(毫秒)。</param>
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;
}
});
}
/// <summary>
/// 将 <see cref="IsReviceOk"/> 置为 true 并在一段时间后自动恢复为 false。
/// </summary>
/// <param name="holdMs">保持 true 的时间(毫秒)。</param>
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;
}
});
}
/// <summary>
/// CAN/CANFD 原始帧接收事件。
/// </summary>
/// <remarks>
/// - 该事件在接收线程中触发(非 UI 线程);
/// - 订阅方若需要更新 UI请自行切回 UI 线程;
/// - 即使启用了 DBC 解码,该事件仍会原样抛出原始帧,便于外部做抓包/记录。
/// </remarks>
public event Action<ZlgCanRxFrame>? CanFrameReceived;
/// <summary>
/// LIN 原始帧接收事件。
/// </summary>
/// <remarks>
/// 同 <see cref="CanFrameReceived"/>:在接收线程触发,订阅方自行处理线程切换。
/// </remarks>
public event Action<ZlgLinRxFrame>? LinFrameReceived;
/// <summary>
/// <see cref="DbcDecodeEnabled"/> 的 backing 字段。
/// </summary>
private bool _dbcDecodeEnabled;
/// <summary>
/// 是否启用 DBC 解码更新(接收帧触发)。
/// </summary>
public bool DbcDecodeEnabled
{
get { return _dbcDecodeEnabled; }
set { _dbcDecodeEnabled = value; RaisePropertyChanged(); }
}
/// <summary>
/// <see cref="IsCycleSend"/> 的 backing 字段。
/// </summary>
private bool _isCycleSend;
/// <summary>
/// 是否启用“事件驱动发送”。
/// </summary>
/// <remarks>
/// 与现有 ToomossCan/ToomossCanFD 行为对齐:只有 IsCycleSend=true 且 SchEnable=true 时,
/// 才会在 CmdDataChanged 事件中触发增量下发。
/// </remarks>
public bool IsCycleSend
{
get { return _isCycleSend; }
set { _isCycleSend = value; RaisePropertyChanged(); }
}
/// <summary>
/// <see cref="SchEnable"/> 的 backing 字段。
/// </summary>
private bool _schEnable;
/// <summary>
/// 发送使能(与 UI 的调度表使能语义对齐)。
/// </summary>
public bool SchEnable
{
get { return _schEnable; }
set { _schEnable = value; RaisePropertyChanged(); }
}
/// <summary>
/// 打开设备(不初始化 CAN/LIN 通道)。
/// </summary>
/// <param name="deviceIndex">设备索引(通常 0。</param>
/// <remarks>
/// 说明:
/// - 该方法只负责获取 device_handle根句柄通道初始化由 <see cref="OpenAndInitCan"/> / <see cref="OpenAndInitLin"/> 完成;
/// - 若 OpenDevice 失败,为了便于现场定位,代码会输出:
/// - 输出目录 DLL 依赖诊断(是否缺失、位数是否匹配、版本信息);
/// - 当前进程架构与已加载模块路径;
/// - 该方法是幂等的:若设备已打开则直接返回。
/// </remarks>
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;
}
}
/// <summary>
/// 预加载指定路径的原生 DLL尽量避免被 PATH 中的同名 DLL 干扰。
/// </summary>
/// <param name="dllFullPath">DLL 完整路径。</param>
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}";
}
}
/// <summary>
/// 构造输出目录下某个 DLL 的诊断信息(架构/大小/版本)。
/// </summary>
/// <param name="baseDir">输出目录。</param>
/// <param name="fileName">DLL 文件名。</param>
/// <returns>诊断信息字符串;不存在返回 "xxx:not_found";异常返回 null。</returns>
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;
}
}
/// <summary>
/// 获取当前进程中已加载模块的完整路径。
/// </summary>
/// <param name="moduleFileName">模块文件名(如 zlgcan.dll。</param>
/// <returns>完整路径;获取失败返回 null。</returns>
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;
}
}
/// <summary>
/// 打开设备并初始化 CANFD 通道。
/// </summary>
/// <param name="deviceIndex">设备索引(通常 0。</param>
/// <param name="channel0">通道0配置。</param>
/// <param name="channel1">通道1配置可为 null 表示不初始化)。</param>
/// <remarks>
/// 生命周期语义:
/// - 本方法会执行OpenDevice -&gt; InitCAN(通道参数) -&gt; StartCAN
/// - 并根据通道0配置写入“设备级合并接收开关”0/set_device_recv_merge
/// - 初始化过程中如发生异常,会调用 <see cref="SafeClose_NoLock"/> 尽量回收已打开的句柄,避免资源泄漏。
///
/// 注意:
/// - 合并接收开关属于 device 级能力因此以通道0配置为准
/// - 该方法不启动接收线程,接收线程由 <see cref="StartReceiveLoop"/> 单独启动,便于 UI 控制。
/// </remarks>
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;
}
}
}
/// <summary>
/// 初始化并启动 LIN 通道。
/// </summary>
/// <param name="linIndex">LIN 通道索引(通常 0。</param>
/// <param name="options">LIN 初始化参数。</param>
/// <remarks>
/// 说明:
/// - LIN 与 CAN/CANFD 共用同一个 device_handle
/// - 本工程在 UI/Service 层通常会做 CAN/LIN 互斥校验,避免同时占用同一硬件。
/// </remarks>
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 失败。");
}
}
}
/// <summary>
/// 启动后台接收线程。
/// </summary>
/// <param name="mergeReceive">是否启用合并接收ZCAN_ReceiveData。建议与通道初始化时的 EnableMergeReceive 保持一致。</param>
/// <param name="bufferFrames">接收缓冲最大帧数(合并接收固定数组大小,过小会丢帧)。</param>
/// <remarks>
/// 线程与回调说明:
/// - 本方法会创建一个后台 Task 持续读取驱动接收缓冲区;
/// - 收到帧后会触发 <see cref="CanFrameReceived"/> / <see cref="LinFrameReceived"/>(在后台线程触发,非 UI 线程);
/// - 若 <see cref="DbcDecodeEnabled"/> 为 true会在接收线程中对帧做 DBC 解码并更新 <see cref="CanDbcModel"/> 集合。
///
/// 注意:
/// - 若你在 UI 层直接绑定 <see cref="CanDbcModel"/> 集合并要求线程安全更新,请在订阅事件或更新集合时切回 UI 线程。
/// </remarks>
/// <exception cref="InvalidOperationException">设备未打开或接收线程已启动。</exception>
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);
}
}
/// <summary>
/// 加载 DBC 文件,并生成 UI 绑定用的信号集合。
/// </summary>
/// <param name="dbcPath">DBC 文件路径。</param>
/// <param name="enableAsyncAnalyse">是否启用异步解析。</param>
/// <param name="merge">是否合并加载。</param>
/// <param name="protocolType">协议类型。</param>
/// <returns>CanDbcModel 集合。</returns>
/// <remarks>
/// 成功加载后会:
/// - 重新创建并替换内部 DBC 数据库对象;
/// - 构建用于 UI 展示的 <see cref="CanDbcModel"/> 列表;
/// - 建立 SignalName 索引(用于接收线程解码时快速定位模型并更新值);
/// - 默认开启 <see cref="DbcDecodeEnabled"/>,使接收线程能驱动实时值更新。
/// </remarks>
public ObservableCollection<CanDbcModel> 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;
}
}
/// <summary>
/// 设置并订阅要发送的指令集合(事件驱动)。
/// </summary>
/// <param name="cmdData">指令集合。</param>
/// <param name="channelIndex">发送通道0/1。</param>
/// <param name="frameType">帧类型ZDBC.FT_CAN 或 ZDBC.FT_CANFD。</param>
/// <remarks>
/// 本方法用于建立“信号值变化 =&gt; 增量下发”的事件驱动链路:
/// - 会先取消订阅旧集合的变更事件,再订阅新集合;
/// - 仅当 <see cref="IsCycleSend"/> 与 <see cref="SchEnable"/> 同时为 true 时,变更事件才会触发实际发送;
/// - 发送时仍会通过 DBC 对 MsgName 进行编码(见 <see cref="SendOneMsgByCmdData"/>)。
/// </remarks>
public void LoadCmdDataToDrive(List<CanCmdData> 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;
}
}
}
}
/// <summary>
/// 事件驱动回调:某个 MsgName 的信号值变更时,仅增量编码并下发该消息。
/// </summary>
/// <param name="sender">发送方。</param>
/// <param name="msgName">发生变化的消息名称。</param>
/// <remarks>
/// 发送策略:
/// - 调度表(硬件 auto_send)运行时:优先覆盖更新设备定时发送列表中的对应条目(见 <see cref="TryUpdateAutoSendByMsgName"/>
/// - 否则:立即编码并发送 1 帧(见 <see cref="SendOneMsgByCmdData"/>)。
///
/// 说明:该方法由 <see cref="CanCmdData"/> 的变更事件触发,可能不在 UI 线程。
/// </remarks>
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}");
}
}
/// <summary>
/// 按 MsgName 尝试覆盖更新硬件定时发送列表中的对应任务。
/// </summary>
/// <param name="msgName">消息名称。</param>
/// <returns>true=已更新 auto_sendfalse=未更新(可能未启动 auto_send 或未命中任务索引)。</returns>
/// <remarks>
/// 覆盖更新原理:
/// - auto_send 的每条任务由 index 唯一标识;
/// - 重新对同 index 调用 SetValue 写入新的帧数据,即可实现“后续周期发送新值”;
/// - 本方法仅覆盖帧数据,不改变周期(周期由启动调度表时写入)。
/// </remarks>
private bool TryUpdateAutoSendByMsgName(string msgName)
{
if (string.IsNullOrWhiteSpace(msgName)) return false;
bool active;
int channelIndex;
byte frameType;
List<ushort>? 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;
}
/// <summary>
/// 根据 MsgName 从当前 CmdData 聚合信号值,并通过 DBC 编码为原始 CAN/CANFD 帧。
/// </summary>
/// <param name="msgName">消息名称DBC Message Name。</param>
/// <param name="frameType">帧类型CAN/CANFD。</param>
/// <returns>
/// 成功返回 (CanId, Payload, PayloadLen);失败返回 null如 DBC 未加载、CmdData 为空或该 MsgName 无任何信号值)。
/// </returns>
private (uint CanId, byte[] Payload, int PayloadLen)? TryBuildRawFrameFromCmdData(string msgName, byte frameType)
{
ZlgDbcDatabase? dbc;
List<CanCmdData>? 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<string, double>(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<byte>(), dataLen);
}
/// <summary>
/// 清空并按调度表批量配置硬件定时发送列表,然后 apply_auto_send 使能。
/// </summary>
/// <param name="scheduleItems">调度表条目集合。</param>
/// <param name="channelIndex">通道索引0/1。</param>
/// <param name="frameType">帧类型CAN/CANFD。</param>
/// <remarks>
/// 对应周立功定时发送能力:
/// - 通过 {channel}/auto_send 或 {channel}/auto_send_canfd 写入待发送帧与周期;
/// - 再通过 {channel}/apply_auto_send 使能后,由设备侧硬件定时器保证发送周期精度。
///
/// 本工程约定:
/// - 调度表中的 MsgName 必须能在 DBC 中找到 message 定义,否则无法编码并会被跳过;
/// - 运行期间信号值变化会触发覆盖更新(不影响周期)。
/// </remarks>
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-&gt;index 映射稳定。
ClearAutoSend(channelIndex);
var msgIndex = new Dictionary<string, List<ushort>>(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<ushort>();
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;
}
}
}
/// <summary>
/// 停止硬件定时发送clear_auto_send
/// </summary>
/// <param name="channelIndex">通道索引0/1。</param>
/// <remarks>
/// - 会清空设备侧定时发送列表并停止发送;
/// - 同时清空内存态 MsgName-&gt;Index 映射,避免后续误覆盖写入。
/// </remarks>
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<byte>();
// 根据帧类型选择写入 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<byte>() : 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<byte>() : 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。");
}
/// <summary>
/// 根据 MsgName从当前 CmdData 中取出信号值,编码并发送一帧。
/// </summary>
/// <param name="msgName">消息名称。</param>
/// <param name="channelIndex">发送通道。</param>
/// <param name="frameType">帧类型ZDBC.FT_CAN 或 ZDBC.FT_CANFD。</param>
public void SendOneMsgByCmdData(string msgName, int channelIndex, byte frameType)
{
ThrowIfDisposed();
ZlgDbcDatabase? dbc;
List<CanCmdData>? cmdData;
lock (_dbcSync)
{
// 仅在锁内拿到 DBC/CmdData 的引用快照,避免编码时长时间占用 _dbcSync。
// 编码可能会分配内存/做查表,若在锁内执行会阻塞接收线程的 DBC 解码。
dbc = _dbc;
cmdData = _cmdData;
}
if (dbc == null || !dbc.IsLoaded)
{
// DBC 未加载时无法做“信号值-&gt;原始帧”的编码,因此直接忽略(上层会通过 DbcParserState 提示)。
return;
}
if (cmdData == null || cmdData.Count == 0)
{
// 没有 CmdData 也就没有要写入的信号值,直接返回。
return;
}
if (string.IsNullOrWhiteSpace(msgName))
{
return;
}
var dict = new Dictionary<string, double>(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 编码:将“信号名-&gt;物理值”转换为可发送的 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。");
}
/// <summary>
/// 停止后台接收线程。
/// </summary>
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;
}
}
}
/// <summary>
/// 发送 CAN 报文。
/// </summary>
/// <param name="channelIndex">通道索引0/1。</param>
/// <param name="canId">包含扩展帧标志位的 can_id可用 ZlgCanIdHelper.MakeCanId 生成)。</param>
/// <param name="data">数据0~8字节。</param>
/// <param name="requestTxEcho">是否请求发送回显。</param>
/// <param name="transmitType">发送方式0=正常发送1=单次发送2=自发自收3=单次自发自收。</param>
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);
}
}
/// <summary>
/// 发送 CANFD 报文。
/// </summary>
/// <param name="channelIndex">通道索引0/1。</param>
/// <param name="canId">包含扩展帧标志位的 can_id可用 ZlgCanIdHelper.MakeCanId 生成)。</param>
/// <param name="data">数据0~64字节。</param>
/// <param name="requestTxEcho">是否请求发送回显。</param>
/// <param name="transmitType">发送方式0=正常发送1=单次发送2=自发自收3=单次自发自收。</param>
/// <returns>驱动返回的实际发送帧数(通常为 0 或 1。</returns>
/// <remarks>
/// - CANFD 有效长度由 <paramref name="data"/> 决定,内部会自动截断到 64 字节;
/// - 若 <paramref name="requestTxEcho"/> 为 true会在 flags 中设置回显位,使设备回传一帧 Tx 事件。
/// 在 merge 接收模式下,这种 Tx 回显会以 <see cref="ZlgCanRxFrame.IsTx"/> 标识。
/// </remarks>
/// <exception cref="InvalidOperationException">通道未初始化或句柄无效。</exception>
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);
}
}
/// <summary>
/// 设置设备定时发送CAN
/// </summary>
/// <param name="channelIndex">通道索引。</param>
/// <param name="taskIndex">定时任务索引。</param>
/// <param name="enable">是否使能。</param>
/// <param name="intervalMs">周期ms。</param>
/// <param name="canId">can_id。</param>
/// <param name="data">数据0~8。</param>
/// <remarks>
/// 对应 ZLG 驱动 SetValue 路径:<c>{channelIndex}/auto_send</c>。
///
/// 关键点:
/// - <paramref name="taskIndex"/> 为设备侧列表索引;同一索引再次写入会“覆盖更新”;
/// - <paramref name="intervalMs"/> 为周期(毫秒),设备按该周期自动发送;
/// - 本实现默认设置发送回显位__pad=0x20便于在接收线程中区分 Tx/Rx 并刷新发送指示灯。
/// </remarks>
/// <exception cref="InvalidOperationException">设备未打开或底层 SetValue 失败。</exception>
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}");
}
}
/// <summary>
/// 设置设备定时发送CANFD
/// </summary>
/// <param name="channelIndex">通道索引。</param>
/// <param name="taskIndex">定时任务索引。</param>
/// <param name="enable">是否使能。</param>
/// <param name="intervalMs">周期ms。</param>
/// <param name="canId">can_id。</param>
/// <param name="data">数据0~64。</param>
/// <remarks>
/// 对应 ZLG 驱动 SetValue 路径:<c>{channelIndex}/auto_send_canfd</c>。
///
/// 与 <see cref="ConfigureAutoSendCan"/> 一致:
/// - 同 index 写入即覆盖更新;
/// - interval 为周期ms
/// - 默认设置发送回显(flags=0x20)。
/// </remarks>
/// <exception cref="InvalidOperationException">设备未打开或底层 SetValue 失败。</exception>
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}");
}
}
/// <summary>
/// 启动通道定时发送任务apply_auto_send
/// </summary>
/// <param name="channelIndex">通道索引。</param>
/// <remarks>
/// 对应 ZLG 驱动 SetValue 路径:<c>{channelIndex}/apply_auto_send</c>。
///
/// 调用时机:
/// - 在完成若干条 <c>auto_send</c>/<c>auto_send_canfd</c> 配置后调用;
/// - 设备侧开始按列表条目周期自动发送。
/// </remarks>
/// <exception cref="InvalidOperationException">设备未打开或底层 SetValue 失败。</exception>
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}");
}
}
/// <summary>
/// 清空通道定时发送任务clear_auto_send
/// </summary>
/// <param name="channelIndex">通道索引。</param>
/// <remarks>
/// 对应 ZLG 驱动 SetValue 路径:<c>{channelIndex}/clear_auto_send</c>。
///
/// 说明:
/// - clear 会清空硬件列表并停止该通道的定时发送;
/// - 若你要重新配置调度表,建议先 clear再从 index=0 重新写入并 apply。
/// </remarks>
/// <exception cref="InvalidOperationException">设备未打开或底层 SetValue 失败。</exception>
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}");
}
}
/// <summary>
/// 配置 LIN 发布表。
/// </summary>
/// <param name="publishCfg">发布配置集合。</param>
/// <remarks>
/// - LIN 发布表用于“周期性/按条件发布”LIN 数据(由设备侧完成具体发送时机,取决于驱动能力与配置)。
/// - 本方法会将托管结构体数组拷贝到非托管内存,再调用底层 API。
/// - 若集合为空则直接返回。
///
/// 线程说明:
/// - 该方法为同步调用;建议在设备已初始化 LIN 通道后调用。
/// </remarks>
/// <exception cref="InvalidOperationException">LIN 未初始化或底层配置失败。</exception>
public void SetLinPublish(IEnumerable<ZLGCAN.ZCAN_LIN_PUBLISH_CFG> publishCfg)
{
ThrowIfDisposed();
if (_linChannelHandle == IntPtr.Zero) throw new InvalidOperationException("LIN 未初始化。");
var list = publishCfg?.ToList() ?? new List<ZLGCAN.ZCAN_LIN_PUBLISH_CFG>();
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);
}
}
/// <summary>
/// 配置 LIN 订阅表。
/// </summary>
/// <param name="subscribeCfg">订阅配置集合。</param>
/// <remarks>
/// - LIN 订阅表用于配置希望接收/关注的 LIN 帧。
/// - 本方法会将托管结构体数组拷贝到非托管内存,再调用底层 API。
/// - 若集合为空则直接返回。
/// </remarks>
/// <exception cref="InvalidOperationException">LIN 未初始化或底层配置失败。</exception>
public void SetLinSubscribe(IEnumerable<ZLGCAN.ZCAN_LIN_SUBSCIBE_CFG> subscribeCfg)
{
ThrowIfDisposed();
if (_linChannelHandle == IntPtr.Zero) throw new InvalidOperationException("LIN 未初始化。");
var list = subscribeCfg?.ToList() ?? new List<ZLGCAN.ZCAN_LIN_SUBSCIBE_CFG>();
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);
}
}
/// <summary>
/// 发送一帧 LIN 报文。
/// </summary>
/// <param name="channel">通道号(通常为 0。</param>
/// <param name="pid">受保护 IDPID包含奇偶校验位。</param>
/// <param name="data">数据区0-8字节。</param>
/// <param name="dir">方向(通常 0具体含义以 ZLG 文档为准)。</param>
/// <remarks>
/// 说明:
/// - 当前工程使用 merge 接收(<see cref="ReceiveLoop_Merge"/>)读取 LIN 帧,因此这里的发送只负责向设备下发原始帧;
/// - ZLG 的 LIN 发送 API 以 <see cref="ZLGCAN.ZCAN_LIN_MSG"/> 数组形式接收数据;本方法将 PID+数据按 <see cref="ZLGCAN.ZCANLINData"/> 的内存布局写入。
/// </remarks>
public void TransmitLin(int channel, byte pid, ReadOnlySpan<byte> 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);
}
}
/// <summary>
/// 关闭设备。
/// </summary>
/// <remarks>
/// 资源释放顺序(概览):
/// - 停止接收线程(<see cref="StopReceiveLoop"/>
/// - Reset LIN/CAN 通道并关闭设备句柄(见 <see cref="SafeClose_NoLock"/>
/// - 取消 CmdData 事件订阅并释放 DBC 资源。
///
/// 线程安全:
/// - 该方法通过 <see cref="_sync"/> 与 <see cref="_dbcSync"/> 保护,避免并发 close/load/receive 导致状态错乱。
/// </remarks>
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;
}
}
/// <inheritdoc />
/// <remarks>
/// 遵循 .NET Dispose 语义:
/// - 多次调用是安全的(通过 <see cref="_disposed"/> 做幂等保护);
/// - Dispose 内部会调用 <see cref="Close"/> 释放驱动句柄、停止接收线程并释放 DBC 资源。
/// </remarks>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
try
{
// 说明Dispose 不抛异常,避免在 using/终结路径打断上层释放流程。
// Close 内部已经尽量记录日志并吞掉部分关闭异常,保证释放流程走完。
Close();
}
catch
{
// ignored
}
}
/// <summary>
/// 关闭设备与通道句柄(不加锁版本)。
/// </summary>
/// <remarks>
/// 该方法假定调用方已经持有 <see cref="_sync"/>
/// - 用于 Close 流程中按顺序 Reset LIN、Reset CAN、CloseDevice
/// - 任何异常都会被吞掉并记录 Warn确保关闭流程尽量走完避免句柄泄漏。
/// </remarks>
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;
}
}
/// <summary>
/// 初始化指定 CANFD 通道(仅设置参数并 InitCAN不负责 StartCAN
/// </summary>
/// <param name="chnIdx">通道索引。</param>
/// <param name="options">通道初始化参数。</param>
/// <remarks>
/// 该方法主要完成:
/// - 设置仲裁域/数据域波特率;
/// - 设置终端电阻与总线利用率统计开关;
/// - 调用 <c>ZCAN_InitCAN</c> 获取通道句柄并保存到 <see cref="_canChannelHandles"/>。
///
/// 注意:
/// - 这里仅负责 Init不负责 StartStart 由 <see cref="StartCanChannelInternal"/> 完成。
/// </remarks>
/// <exception cref="InvalidOperationException">任一 SetValue/InitCAN 失败。</exception>
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;
}
/// <summary>
/// 启动指定 CAN 通道。
/// </summary>
/// <param name="chnIdx">通道索引。</param>
/// <returns>通道句柄。</returns>
/// <remarks>
/// 调用前置条件:
/// - <see cref="InitCanChannelInternal"/> 已成功执行并产生通道句柄。
/// </remarks>
/// <exception cref="InvalidOperationException">通道句柄为空或启动失败。</exception>
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;
}
/// <summary>
/// 获取 CAN 通道句柄(不存在则抛异常)。
/// </summary>
/// <param name="channelIndex">通道索引。</param>
/// <returns>通道句柄。</returns>
/// <exception cref="ArgumentOutOfRangeException">通道索引超范围。</exception>
/// <exception cref="InvalidOperationException">通道未初始化。</exception>
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;
}
/// <summary>
/// 普通接收模式:按通道轮询 CAN/CANFD 缓冲区读取。
/// </summary>
/// <param name="token">取消令牌。</param>
/// <param name="bufferFrames">每次接收的最大帧数。</param>
/// <remarks>
/// - 该模式分别调用:
/// - <c>ZCAN_GetReceiveNum/ZCAN_Receive</c> 接收 CAN
/// - <c>ZCAN_GetReceiveNum/ZCAN_ReceiveFD</c> 接收 CANFD
/// - 每次接收会在非托管堆上分配固定大小缓冲,循环中复用,退出时释放。
/// </remarks>
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;
// 0CAN 经典帧缓冲区1CANFD 缓冲区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<ZLGCAN.ZCAN_Receive_Data>(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<ZLGCAN.ZCAN_ReceiveFD_Data>(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);
}
}
/// <summary>
/// 合并接收模式:通过设备级 APIZCAN_ReceiveData统一接收 CAN/CANFD/LIN。
/// </summary>
/// <param name="token">取消令牌。</param>
/// <param name="bufferFrames">接收缓存容量。</param>
/// <remarks>
/// - merge 接收模式通常用于高吞吐场景:由驱动统一将各类数据封装为 DataObj
/// - 本方法根据 dataType 分流:
/// - 1CAN/CANFD
/// - 4LIN
/// - 触发事件仍通过 <see cref="RaiseCanFrame"/> / <see cref="RaiseLinFrame"/>。
/// </remarks>
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));
// 合并接收模式的内存模型:
// - pDataObjsZCANDataObj 数组(每个元素描述一条数据,含 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)
{
// 2device 级合并接收缓冲区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<ZLGCAN.ZCANDataObj>(pCur);
switch (obj.dataType)
{
case 1:
// dataType=1CAN/CANFD 统一封装ZCANCANFDData
// - flag bit0是否为 CANFD
// - flag bit9是否为 Tx echo项目中用来点亮发送指示灯
Marshal.Copy(obj.data, 0, pCanfdBuffer, canfdSize);
var canfdData = Marshal.PtrToStructure<ZLGCAN.ZCANCANFDData>(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=4LINZCANLINData
if (obj.data == null || obj.data.Length < linSize)
{
break;
}
Marshal.Copy(obj.data, 0, pLinBuffer, linSize);
var linData = Marshal.PtrToStructure<ZLGCAN.ZCANLINData>(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);
}
}
/// <summary>
/// 将接收到的 CAN/CANFD 帧转换为托管对象并触发事件。
/// </summary>
/// <param name="channel">通道号。</param>
/// <param name="timestamp">时间戳us。</param>
/// <param name="isCanFd">是否 CANFD。</param>
/// <param name="canId">can_id含标志位。</param>
/// <param name="data">原始数据缓冲区。</param>
/// <param name="dlc">数据长度。</param>
/// <param name="isTx">是否为发送回显Tx。</param>
/// <remarks>
/// 事件派发与 DBC 解码:
/// - 始终先触发 <see cref="CanFrameReceived"/> 抛出原始帧;
/// - 再依据 <see cref="DbcDecodeEnabled"/> 决定是否进行 DBC 解码并刷新 <see cref="CanDbcModel"/>。
///
/// 发送/接收指示:
/// - 若 <paramref name="isTx"/> 为 true调用 <see cref="MarkSendOk"/>;否则调用 <see cref="MarkReviceOk"/>。
/// </remarks>
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}");
}
}
/// <summary>
/// 将接收到的 LIN 帧转换为托管对象并触发事件。
/// </summary>
/// <param name="channel">通道号。</param>
/// <param name="timestamp">时间戳us。</param>
/// <param name="pid">PID。</param>
/// <param name="data">原始数据缓冲区。</param>
/// <param name="datalen">数据长度。</param>
/// <param name="dir">方向(由设备回传)。</param>
/// <remarks>
/// - LIN 数据长度最大 8 字节,本方法会按 <paramref name="datalen"/> 截断并复制;
/// - 事件在接收线程触发,订阅方若更新 UI请自行切换线程。
/// </remarks>
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}");
}
}
/// <summary>
/// 校验指定原生 DLL 是否存在于程序输出目录AppContext.BaseDirectory
/// </summary>
/// <param name="dllName">DLL 文件名。</param>
/// <exception cref="FileNotFoundException">找不到 DLL。</exception>
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”的架构不能保证其依赖 DLLkerneldlls 下的其它库)都齐全且位数一致。
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}");
}
}
}
/// <summary>
/// 尝试读取 PE 头判断 DLL 架构。
/// </summary>
/// <param name="dllFullPath">DLL 完整路径。</param>
/// <returns>返回 "x86"/"x64"/"arm64"/"unknown(...)";读取失败返回 null。</returns>
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_lfanewDOS 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;
}
}
/// <summary>
/// 构建 DBC 解码更新用的索引表。
/// </summary>
/// <param name="models">所有信号模型。</param>
/// <returns>键为 (MsgName, SignalName) 的组合键,值为对应的 <see cref="CanDbcModel"/>。</returns>
/// <remarks>
/// - 接收线程解码得到 (msgName, signalName) 后,可 O(1) 定位到 UI 模型进行更新;
/// - 组合键使用 <see cref="BuildMsgSigKey"/>,避免字符串拼接歧义。
/// </remarks>
private static Dictionary<string, CanDbcModel> BuildDbcModelIndex(IEnumerable<CanDbcModel> models)
{
var dict = new Dictionary<string, CanDbcModel>(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;
}
/// <summary>
/// 构建 (MsgName, SignalName) 的唯一组合键。
/// </summary>
/// <param name="msgName">消息名。</param>
/// <param name="signalName">信号名。</param>
/// <returns>组合键字符串。</returns>
/// <remarks>
/// 使用 '\0' 作为分隔符,降低与业务字符串冲突的风险。
/// </remarks>
private static string BuildMsgSigKey(string msgName, string signalName)
{
return msgName + "\u0000" + signalName;
}
/// <summary>
/// 尝试对原始帧做 DBC 解码,并将解码结果写回到对应的 <see cref="CanDbcModel"/> 实例。
/// </summary>
/// <param name="canId">CAN ID含标志位。</param>
/// <param name="payload">原始数据。</param>
/// <param name="frameType">帧类型CAN/CANFD。</param>
/// <remarks>
/// - 本方法被 <see cref="RaiseCanFrame"/> 在接收线程中调用;
/// - 若 DBC 未加载或索引表为空,会直接返回;
/// - 解码后按 signalName 更新 <see cref="CanDbcModel.SignalRtValue"/>(字符串形式)。
/// </remarks>
private void TryDecodeAndUpdateModels(uint canId, byte[] payload, byte frameType)
{
ZlgDbcDatabase? dbc;
Dictionary<string, CanDbcModel>? 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}");
}
}
/// <summary>
/// 若对象已释放则抛异常。
/// </summary>
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZlgCanFd200uDriver));
}
}
}
/// <summary>
/// CAN/CANFD 接收帧。
/// </summary>
public readonly struct ZlgCanRxFrame
{
/// <summary>
/// 构造 CAN/CANFD 接收帧。
/// </summary>
/// <param name="channel">通道号。</param>
/// <param name="isCanFd">是否 CANFD。</param>
/// <param name="canId">can_id含标志位。</param>
/// <param name="data">数据(已截断为实际长度)。</param>
/// <param name="timestampUs">时间戳us。</param>
/// <param name="isTx">是否为发送回显。</param>
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;
}
/// <summary>
/// 通道号0/1
/// </summary>
public int Channel { get; }
/// <summary>
/// 是否为 CANFD 帧。
/// </summary>
public bool IsCanFd { get; }
/// <summary>
/// can_id包含扩展帧/错误帧等标志位,具体位定义见 ZLG 文档)。
/// </summary>
public uint CanId { get; }
/// <summary>
/// 数据区(已按实际长度截断拷贝)。
/// </summary>
public byte[] Data { get; }
/// <summary>
/// 时间戳(微秒)。
/// </summary>
public ulong TimestampUs { get; }
/// <summary>
/// 是否为发送回显Tx
/// </summary>
public bool IsTx { get; }
}
/// <summary>
/// LIN 接收帧。
/// </summary>
public readonly struct ZlgLinRxFrame
{
/// <summary>
/// 构造 LIN 接收帧。
/// </summary>
/// <param name="channel">通道号。</param>
/// <param name="pid">PID。</param>
/// <param name="data">数据(已截断为实际长度)。</param>
/// <param name="timestampUs">时间戳us。</param>
/// <param name="dir">方向(由设备回传)。</param>
public ZlgLinRxFrame(int channel, byte pid, byte[] data, ulong timestampUs, byte dir)
{
Channel = channel;
Pid = pid;
Data = data;
TimestampUs = timestampUs;
Dir = dir;
}
/// <summary>
/// 通道号。
/// </summary>
public int Channel { get; }
/// <summary>
/// PID。
/// </summary>
public byte Pid { get; }
/// <summary>
/// 数据区(已按实际长度截断拷贝)。
/// </summary>
public byte[] Data { get; }
/// <summary>
/// 时间戳(微秒)。
/// </summary>
public ulong TimestampUs { get; }
/// <summary>
/// 方向由设备回传0/1 具体含义以 ZLG 文档为准。
/// </summary>
public byte Dir { get; }
}
}