2594 lines
105 KiB
C#
2594 lines
105 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>
|
||
/// <remarks>
|
||
/// 设计目标:
|
||
/// - 把 ZLG 原生 DLL(zlgcan.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->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 -> 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 -> 信号模型的索引,加速更新(接收解码更新时 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 -> InitCAN(通道参数) -> 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>
|
||
/// 本方法用于建立“信号值变化 => 增量下发”的事件驱动链路:
|
||
/// - 会先取消订阅旧集合的变更事件,再订阅新集合;
|
||
/// - 仅当 <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_send;false=未更新(可能未启动 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->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->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 未加载时无法做“信号值->原始帧”的编码,因此直接忽略(上层会通过 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 编码:将“信号名->物理值”转换为可发送的 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">受保护 ID(PID,包含奇偶校验位)。</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,不负责 Start;Start 由 <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;
|
||
|
||
// 0:CAN 经典帧缓冲区;1:CANFD 缓冲区(ZLG SDK 约定)。
|
||
// 注意:GetReceiveNum 返回的是“当前缓存中可读数量”,但真正 Receive 返回的 actual
|
||
// 可能小于请求值(例如 bufferFrames 太小或并发到达)。
|
||
uint canNum = ZLGCAN.ZCAN_GetReceiveNum(handle, 0);
|
||
if (canNum > 0)
|
||
{
|
||
uint actual = ZLGCAN.ZCAN_Receive(handle, pCanBuffer, (uint)bufferFrames, WaitMs);
|
||
for (int i = 0; i < actual; i++)
|
||
{
|
||
IntPtr pCur = IntPtr.Add(pCanBuffer, i * canStructSize);
|
||
var rec = Marshal.PtrToStructure<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>
|
||
/// 合并接收模式:通过设备级 API(ZCAN_ReceiveData)统一接收 CAN/CANFD/LIN。
|
||
/// </summary>
|
||
/// <param name="token">取消令牌。</param>
|
||
/// <param name="bufferFrames">接收缓存容量。</param>
|
||
/// <remarks>
|
||
/// - merge 接收模式通常用于高吞吐场景:由驱动统一将各类数据封装为 DataObj;
|
||
/// - 本方法根据 dataType 分流:
|
||
/// - 1:CAN/CANFD
|
||
/// - 4:LIN
|
||
/// - 触发事件仍通过 <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));
|
||
|
||
// 合并接收模式的内存模型:
|
||
// - pDataObjs:ZCANDataObj 数组(每个元素描述一条数据,含 dataType/chnl/data)。
|
||
// - obj.data 为托管 byte[],为了用 PtrToStructure 解析为结构体,这里做一次 Marshal.Copy 到临时非托管缓冲。
|
||
// 这样可以避免 unsafe/pin,但会增加一次拷贝开销;在高吞吐场景下 bufferFrames 应适当调大。
|
||
IntPtr pDataObjs = Marshal.AllocHGlobal(dataObjSize * bufferFrames);
|
||
IntPtr pCanfdBuffer = Marshal.AllocHGlobal(canfdSize);
|
||
IntPtr pLinBuffer = Marshal.AllocHGlobal(linSize);
|
||
|
||
try
|
||
{
|
||
while (!token.IsCancellationRequested)
|
||
{
|
||
// 2:device 级合并接收缓冲区(ZLG SDK 约定)。
|
||
// 注意:合并接收开关需要在通道初始化时通过 0/set_device_recv_merge 开启(见 OpenAndInitCan)。
|
||
uint recvNum = ZLGCAN.ZCAN_GetReceiveNum(_deviceHandle, 2);
|
||
if (recvNum == 0)
|
||
{
|
||
Thread.Sleep(10);
|
||
continue;
|
||
}
|
||
|
||
uint actualRecv = ZLGCAN.ZCAN_ReceiveData(_deviceHandle, pDataObjs, (uint)bufferFrames, 10);
|
||
if (actualRecv == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
for (int i = 0; i < actualRecv; i++)
|
||
{
|
||
IntPtr pCur = IntPtr.Add(pDataObjs, i * dataObjSize);
|
||
var obj = Marshal.PtrToStructure<ZLGCAN.ZCANDataObj>(pCur);
|
||
|
||
switch (obj.dataType)
|
||
{
|
||
case 1:
|
||
// dataType=1:CAN/CANFD 统一封装(ZCANCANFDData)。
|
||
// - flag bit0:是否为 CANFD
|
||
// - flag bit9:是否为 Tx echo(项目中用来点亮发送指示灯)
|
||
Marshal.Copy(obj.data, 0, pCanfdBuffer, canfdSize);
|
||
var canfdData = Marshal.PtrToStructure<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=4:LIN(ZCANLINData)。
|
||
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”的架构,不能保证其依赖 DLL(kerneldlls 下的其它库)都齐全且位数一致。
|
||
var dllArch = TryGetPeArchitecture(full);
|
||
if (!string.IsNullOrWhiteSpace(dllArch))
|
||
{
|
||
var procArch = Environment.Is64BitProcess ? "x64" : "x86";
|
||
if (Environment.Is64BitProcess && !string.Equals(dllArch, "x64", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x64 版本 DLL,或将程序改为 x86 运行。DLL 路径:{full}");
|
||
}
|
||
if (!Environment.Is64BitProcess && !string.Equals(dllArch, "x86", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x86 版本 DLL,或将程序改为 x64 运行。DLL 路径:{full}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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_lfanew:DOS Header + 0x3C,指向 PE Header 起始偏移。
|
||
fs.Position = 0x3C;
|
||
var peOffset = br.ReadInt32();
|
||
if (peOffset <= 0 || peOffset > fs.Length - 6)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// PE signature:"PE\0\0" => 0x00004550。
|
||
fs.Position = peOffset;
|
||
var peSig = br.ReadUInt32();
|
||
if (peSig != 0x00004550)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
// IMAGE_FILE_HEADER.Machine:
|
||
// - 0x014c = x86
|
||
// - 0x8664 = x64
|
||
// - 0xAA64 = arm64
|
||
var machine = br.ReadUInt16();
|
||
return machine switch
|
||
{
|
||
0x014c => "x86",
|
||
0x8664 => "x64",
|
||
0xAA64 => "arm64",
|
||
_ => $"unknown(0x{machine:X})"
|
||
};
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <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; }
|
||
}
|
||
}
|