Files
CapMachine/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs
2026-02-06 12:34:34 +08:00

1679 lines
59 KiB
C#
Raw 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>
public sealed class ZlgCanFd200uDriver : BindableBase, IDisposable
{
private readonly object _sync = new object();
private readonly ILogService _log;
private static IntPtr _preloadedZlgcanHandle = IntPtr.Zero;
private static string? _preloadedZlgcanError;
private IntPtr _deviceHandle = IntPtr.Zero;
private readonly IntPtr[] _canChannelHandles = new IntPtr[2];
private IntPtr _linChannelHandle = IntPtr.Zero;
private CancellationTokenSource? _recvCts;
private Task? _recvTask;
private volatile bool _disposed;
private readonly object _dbcSync = new object();
private ZlgDbcDatabase? _dbc;
private ObservableCollection<CanDbcModel>? _dbcModels;
private Dictionary<string, CanDbcModel>? _dbcModelIndex;
private List<CanCmdData>? _cmdData;
private int _cmdSendChannelIndex;
private byte _cmdSendFrameType;
/// <summary>
/// 构造函数。
/// </summary>
/// <param name="logService">日志服务。</param>
public ZlgCanFd200uDriver(ILogService logService)
{
_log = logService;
}
/// <summary>
/// 安全关闭设备与通道(需在 _sync 锁内调用)。
/// </summary>
/// <remarks>
/// - 该方法会尽力调用 Reset/Close并在异常时记录日志且继续执行释放流程
/// - 该方法不会停止接收线程,调用方应优先 StopReceiveLoop。
/// </remarks>
private bool _openState;
/// <summary>
/// 设备打开状态。
/// </summary>
public bool OpenState
{
get { return _openState; }
private set { _openState = value; RaisePropertyChanged(); }
}
private bool _isReceiving;
/// <summary>
/// 是否正在接收。
/// </summary>
public bool IsReceiving
{
get { return _isReceiving; }
private set { _isReceiving = value; RaisePropertyChanged(); }
}
private bool _isSendOk;
/// <summary>
/// 最近是否发生过“发送成功”(用于 UI 指示灯)。
/// </summary>
public bool IsSendOk
{
get { return _isSendOk; }
private set { _isSendOk = value; RaisePropertyChanged(); }
}
private bool _isReviceOk;
/// <summary>
/// 最近是否发生过“接收成功”(用于 UI 指示灯)。
/// </summary>
public bool IsReviceOk
{
get { return _isReviceOk; }
private set { _isReviceOk = value; RaisePropertyChanged(); }
}
private int _sendOkToken;
private int _reviceOkToken;
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;
}
});
}
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>
public event Action<ZlgCanRxFrame>? CanFrameReceived;
/// <summary>
/// LIN 原始帧接收事件。
/// </summary>
public event Action<ZlgLinRxFrame>? LinFrameReceived;
private bool _dbcDecodeEnabled;
/// <summary>
/// 是否启用 DBC 解码更新(接收帧触发)。
/// </summary>
public bool DbcDecodeEnabled
{
get { return _dbcDecodeEnabled; }
set { _dbcDecodeEnabled = value; RaisePropertyChanged(); }
}
private bool _isCycleSend;
/// <summary>
/// 是否启用“事件驱动发送”。
/// </summary>
/// <remarks>
/// 与现有 ToomossCan/ToomossCanFD 行为对齐:只有 IsCycleSend=true 且 SchEnable=true 时,
/// 才会在 CmdDataChanged 事件中触发增量下发。
/// </remarks>
public bool IsCycleSend
{
get { return _isCycleSend; }
set { _isCycleSend = value; RaisePropertyChanged(); }
}
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>
public void OpenDevice(uint deviceIndex)
{
ThrowIfDisposed();
lock (_sync)
{
if (_deviceHandle != IntPtr.Zero)
{
OpenState = true;
return;
}
EnsureNativeDllExists("zlgcan.dll");
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>
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>
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>
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>
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>
public void LoadCmdDataToDrive(List<CanCmdData> cmdData, int channelIndex, byte frameType)
{
ThrowIfDisposed();
lock (_dbcSync)
{
if (_cmdData != null)
{
foreach (var old in _cmdData)
{
old.CanCmdDataChangedHandler -= CmdData_CanCmdDataChangedHandler;
}
}
_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>
private void CmdData_CanCmdDataChangedHandler(object? sender, string msgName)
{
try
{
if (!IsCycleSend) return;
if (!SchEnable) return;
SendOneMsgByCmdData(msgName, _cmdSendChannelIndex, _cmdSendFrameType);
}
catch (Exception ex)
{
_log.Warn($"事件驱动发送异常:{ex.Message}");
}
}
/// <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 = _dbc;
cmdData = _cmdData;
}
if (dbc == null || !dbc.IsLoaded)
{
return;
}
if (cmdData == null || cmdData.Count == 0)
{
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)
{
return;
}
var (canId, data, dataLen) = dbc.EncodeToRawFrame(msgName, dict, frameType);
if (frameType == ZDBC.FT_CAN)
{
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)
{
if (_recvCts == null || _recvTask == null)
{
return;
}
try
{
_recvCts.Cancel();
_recvTask.Wait(TimeSpan.FromSeconds(2));
}
catch (Exception ex)
{
_log.Warn($"停止接收线程等待超时或异常:{ex.Message}");
}
finally
{
_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>
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>
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>
/// 启动通道定时发送任务apply_auto_send
/// </summary>
/// <param name="channelIndex">通道索引。</param>
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>
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>
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>
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>
/// 关闭设备。
/// </summary>
public void Close()
{
lock (_sync)
{
StopReceiveLoop();
SafeClose_NoLock();
}
lock (_dbcSync)
{
if (_cmdData != null)
{
foreach (var item in _cmdData)
{
item.CanCmdDataChangedHandler -= CmdData_CanCmdDataChangedHandler;
}
}
_cmdData = null;
_dbcModels = null;
_dbcModelIndex = null;
_dbc?.Dispose();
_dbc = null;
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed) return;
_disposed = true;
try
{
Close();
}
catch
{
// ignored
}
}
private void SafeClose_NoLock()
{
try
{
if (_linChannelHandle != IntPtr.Zero)
{
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)
{
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>
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>
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>
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>
private void ReceiveLoop_PerChannel(CancellationToken token, int bufferFrames)
{
const int WaitMs = 10;
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;
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);
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);
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
}
}
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>
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));
IntPtr pDataObjs = Marshal.AllocHGlobal(dataObjSize * bufferFrames);
IntPtr pCanfdBuffer = Marshal.AllocHGlobal(canfdSize);
IntPtr pLinBuffer = Marshal.AllocHGlobal(linSize);
try
{
while (!token.IsCancellationRequested)
{
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:
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:
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;
}
}
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>
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));
if (isTx)
{
MarkSendOk();
}
else
{
MarkReviceOk();
}
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>
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);
}
TryPreloadNativeDll(full);
if (_preloadedZlgcanHandle == IntPtr.Zero && !string.IsNullOrWhiteSpace(_preloadedZlgcanError))
{
throw new InvalidOperationException($"预加载 {dllName} 失败:{_preloadedZlgcanError}。DLL 路径:{full}");
}
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;
}
fs.Position = 0x3C;
var peOffset = br.ReadInt32();
if (peOffset <= 0 || peOffset > fs.Length - 6)
{
return null;
}
fs.Position = peOffset;
var peSig = br.ReadUInt32();
if (peSig != 0x00004550)
{
return null;
}
var machine = br.ReadUInt16();
return machine switch
{
0x014c => "x86",
0x8664 => "x64",
0xAA64 => "arm64",
_ => $"unknown(0x{machine:X})"
};
}
catch
{
return null;
}
}
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;
}
private static string BuildMsgSigKey(string msgName, string signalName)
{
return msgName + "\u0000" + signalName;
}
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;
}
public int Channel { get; }
public bool IsCanFd { get; }
public uint CanId { get; }
public byte[] Data { get; }
public ulong TimestampUs { get; }
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;
}
public int Channel { get; }
public byte Pid { get; }
public byte[] Data { get; }
public ulong TimestampUs { get; }
/// <summary>
/// 方向由设备回传0/1 具体含义以 ZLG 文档为准。
/// </summary>
public byte Dir { get; }
}
}