1679 lines
59 KiB
C#
1679 lines
59 KiB
C#
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>
|
||
/// 合并接收模式:通过设备级 API(ZCAN_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; }
|
||
}
|
||
}
|