571 lines
20 KiB
C#
571 lines
20 KiB
C#
using CapMachine.Wpf.CanDrive;
|
||
using CapMachine.Wpf.Services;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Collections.ObjectModel;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
|
||
namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||
{
|
||
/// <summary>
|
||
/// 基于周立功 ZDBC(zdbc.dll)的 DBC 数据库封装。
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// 设计要点:
|
||
/// - zdbc.dll 为原生依赖,必须放置在程序输出目录(AppContext.BaseDirectory)。
|
||
/// - 本类封装 DBC 文件加载、消息/信号枚举、以及基于 DBCMessage 的编码/解码能力。
|
||
/// - 运行稳定性:实现 IDisposable,确保 ZDBC_Release 与相关非托管内存释放。
|
||
/// </remarks>
|
||
public sealed class ZlgDbcDatabase : IDisposable
|
||
{
|
||
private readonly ILogService _log;
|
||
private readonly object _sync = new object();
|
||
|
||
private uint _dbcHandle;
|
||
private bool _disposed;
|
||
|
||
// 复用非托管缓冲,避免高频 Allocate/Free
|
||
private IntPtr _msgPtr = IntPtr.Zero;
|
||
private IntPtr _countPtr = IntPtr.Zero;
|
||
private IntPtr _canFramePtr = IntPtr.Zero;
|
||
private IntPtr _canFdFramePtr = IntPtr.Zero;
|
||
|
||
private readonly Dictionary<string, ZDBC.DBCMessage> _messageByName = new Dictionary<string, ZDBC.DBCMessage>(StringComparer.Ordinal);
|
||
private readonly Dictionary<string, uint> _messageIdByName = new Dictionary<string, uint>(StringComparer.Ordinal);
|
||
|
||
/// <summary>
|
||
/// 构造函数。
|
||
/// </summary>
|
||
/// <param name="logService">日志服务。</param>
|
||
public ZlgDbcDatabase(ILogService logService)
|
||
{
|
||
_log = logService;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否已加载。
|
||
/// </summary>
|
||
public bool IsLoaded
|
||
{
|
||
get
|
||
{
|
||
return _dbcHandle != 0 && _dbcHandle != ZDBC.INVALID_DBC_HANDLE;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载 DBC 文件。
|
||
/// </summary>
|
||
/// <param name="dbcPath">DBC 文件路径。</param>
|
||
/// <param name="enableAsyncAnalyse">是否启用异步解析(库内部线程)。</param>
|
||
/// <param name="merge">是否合并到当前数据库(支持加载多个文件)。</param>
|
||
/// <param name="protocolType">协议类型:J1939=0,其他=1。</param>
|
||
public void Load(string dbcPath, bool enableAsyncAnalyse = true, bool merge = false, byte protocolType = ZDBC.PROTOCOL_OTHER)
|
||
{
|
||
ThrowIfDisposed();
|
||
|
||
if (string.IsNullOrWhiteSpace(dbcPath))
|
||
{
|
||
throw new ArgumentException("dbcPath 不能为空。", nameof(dbcPath));
|
||
}
|
||
|
||
EnsureNativeDllExists("zdbc.dll");
|
||
|
||
if (!File.Exists(dbcPath))
|
||
{
|
||
throw new FileNotFoundException("DBC 文件不存在。", dbcPath);
|
||
}
|
||
|
||
lock (_sync)
|
||
{
|
||
if (_dbcHandle == 0 || _dbcHandle == ZDBC.INVALID_DBC_HANDLE)
|
||
{
|
||
_dbcHandle = ZDBC.ZDBC_Init(0, enableAsyncAnalyse ? (byte)1 : (byte)0);
|
||
if (_dbcHandle == ZDBC.INVALID_DBC_HANDLE)
|
||
{
|
||
_dbcHandle = 0;
|
||
throw new InvalidOperationException("ZDBC_Init 初始化失败。");
|
||
}
|
||
}
|
||
|
||
var fileInfo = new ZDBC.FileInfo
|
||
{
|
||
strFilePath = BuildFixedPathBytes(dbcPath, ZDBC._MAX_FILE_PATH_ + 1),
|
||
type = protocolType,
|
||
merge = (byte)(merge ? 1 : 0)
|
||
};
|
||
|
||
IntPtr pFile = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.FileInfo)));
|
||
try
|
||
{
|
||
Marshal.StructureToPtr(fileInfo, pFile, false);
|
||
var ok = ZDBC.ZDBC_LoadFile(_dbcHandle, pFile);
|
||
if (!ok)
|
||
{
|
||
throw new InvalidOperationException("ZDBC_LoadFile 失败,请检查 DBC 文件格式。");
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
Marshal.FreeHGlobal(pFile);
|
||
}
|
||
|
||
RefreshMessageCache_NoLock();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 枚举 DBC 内全部 Message/Signal 元信息,并转换为 UI 使用的 CanDbcModel 集合。
|
||
/// </summary>
|
||
/// <returns>信号集合。</returns>
|
||
public ObservableCollection<CanDbcModel> BuildCanDbcModels()
|
||
{
|
||
ThrowIfDisposed();
|
||
|
||
if (!IsLoaded)
|
||
{
|
||
return new ObservableCollection<CanDbcModel>();
|
||
}
|
||
|
||
lock (_sync)
|
||
{
|
||
var list = new List<CanDbcModel>();
|
||
|
||
foreach (var kv in _messageByName)
|
||
{
|
||
var msg = kv.Value;
|
||
var msgName = ByteArrayToAsciiString(msg.strName);
|
||
var msgIdStr = "0x" + msg.nID.ToString("X8");
|
||
|
||
var signals = msg.vSignals;
|
||
if (signals == null || signals.Length == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var signalCount = (int)Math.Min(msg.nSignalCount, (uint)signals.Length);
|
||
for (int i = 0; i < signalCount; i++)
|
||
{
|
||
var s = signals[i];
|
||
var sigName = ByteArrayToAsciiString(s.strName);
|
||
if (string.IsNullOrWhiteSpace(sigName))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
list.Add(new CanDbcModel
|
||
{
|
||
MsgName = msgName,
|
||
MsgId = msgIdStr,
|
||
SignalName = sigName,
|
||
SignalDesc = ByteArrayToAsciiString(s.strComment),
|
||
SignalUnit = ByteArrayToAsciiString(s.unit),
|
||
SignalRtValue = "--",
|
||
Publisher = string.Empty
|
||
});
|
||
}
|
||
}
|
||
|
||
return new ObservableCollection<CanDbcModel>(list);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据 MsgName 获取 Message ID。
|
||
/// </summary>
|
||
/// <param name="msgName">消息名称。</param>
|
||
/// <returns>Message ID。</returns>
|
||
public uint GetMessageIdByName(string msgName)
|
||
{
|
||
ThrowIfDisposed();
|
||
if (string.IsNullOrWhiteSpace(msgName)) return 0;
|
||
|
||
lock (_sync)
|
||
{
|
||
return _messageIdByName.TryGetValue(msgName, out var id) ? id : 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据 MsgName + 信号值集合编码为 CAN/CANFD 原始帧。
|
||
/// </summary>
|
||
/// <param name="msgName">消息名称。</param>
|
||
/// <param name="signals">信号值(SignalName -> 实际值)。</param>
|
||
/// <param name="frameType">帧类型:FT_CAN=0,FT_CANFD=1。</param>
|
||
/// <returns>编码后的 can_id 与数据。</returns>
|
||
public (uint CanId, byte[] Data, int DataLen) EncodeToRawFrame(string msgName, IDictionary<string, double> signals, byte frameType)
|
||
{
|
||
ThrowIfDisposed();
|
||
|
||
if (!IsLoaded)
|
||
{
|
||
throw new InvalidOperationException("DBC 未加载。");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(msgName))
|
||
{
|
||
throw new ArgumentException("msgName 不能为空。", nameof(msgName));
|
||
}
|
||
|
||
lock (_sync)
|
||
{
|
||
if (!_messageByName.TryGetValue(msgName, out var msg))
|
||
{
|
||
throw new InvalidOperationException($"DBC 中未找到消息:{msgName}");
|
||
}
|
||
|
||
// 在副本上修改信号,避免污染缓存
|
||
var workingMsg = msg;
|
||
if (workingMsg.vSignals == null)
|
||
{
|
||
workingMsg.vSignals = new ZDBC.DBCSignal[ZDBC._DBC_SIGNAL_MAX_COUNT_];
|
||
}
|
||
|
||
if (signals != null)
|
||
{
|
||
foreach (var kv in signals)
|
||
{
|
||
SetSignalActualValue(ref workingMsg, kv.Key, kv.Value);
|
||
}
|
||
}
|
||
|
||
EnsureMarshalBuffers_NoLock();
|
||
|
||
// 写入 msg 到非托管
|
||
Marshal.StructureToPtr(workingMsg, _msgPtr, false);
|
||
|
||
// nCount 输入输出参数,准备 1 帧
|
||
Marshal.WriteInt32(_countPtr, 1);
|
||
|
||
if (frameType == ZDBC.FT_CAN)
|
||
{
|
||
var ok = ZDBC.ZDBC_Encode(_dbcHandle, _canFramePtr, _countPtr, _msgPtr, frameType);
|
||
if (!ok)
|
||
{
|
||
throw new InvalidOperationException("ZDBC_Encode(CAN) 失败。");
|
||
}
|
||
|
||
var frame = (ZlgNativeCanFrame)Marshal.PtrToStructure(_canFramePtr, typeof(ZlgNativeCanFrame));
|
||
var len = Math.Min(8, (int)frame.can_dlc);
|
||
var data = new byte[len];
|
||
if (len > 0 && frame.data != null)
|
||
{
|
||
Array.Copy(frame.data, data, len);
|
||
}
|
||
|
||
return (frame.can_id, data, len);
|
||
}
|
||
|
||
if (frameType == ZDBC.FT_CANFD)
|
||
{
|
||
var ok = ZDBC.ZDBC_Encode(_dbcHandle, _canFdFramePtr, _countPtr, _msgPtr, frameType);
|
||
if (!ok)
|
||
{
|
||
throw new InvalidOperationException("ZDBC_Encode(CANFD) 失败。");
|
||
}
|
||
|
||
var frame = (ZlgNativeCanFdFrame)Marshal.PtrToStructure(_canFdFramePtr, typeof(ZlgNativeCanFdFrame));
|
||
var len = Math.Min(64, (int)frame.len);
|
||
var data = new byte[len];
|
||
if (len > 0 && frame.data != null)
|
||
{
|
||
Array.Copy(frame.data, data, len);
|
||
}
|
||
|
||
return (frame.can_id, data, len);
|
||
}
|
||
|
||
throw new ArgumentOutOfRangeException(nameof(frameType), "frameType 仅支持 FT_CAN=0 或 FT_CANFD=1。");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将原始 CAN/CANFD 帧解码为 DBCMessage,并返回消息名称与信号实际值。
|
||
/// </summary>
|
||
/// <param name="canId">can_id。</param>
|
||
/// <param name="data">payload。</param>
|
||
/// <param name="frameType">帧类型:FT_CAN=0,FT_CANFD=1。</param>
|
||
/// <returns>解码结果。</returns>
|
||
public (string MsgName, Dictionary<string, double> Signals) DecodeRawFrame(uint canId, byte[] data, byte frameType)
|
||
{
|
||
ThrowIfDisposed();
|
||
|
||
if (!IsLoaded)
|
||
{
|
||
throw new InvalidOperationException("DBC 未加载。");
|
||
}
|
||
|
||
lock (_sync)
|
||
{
|
||
EnsureMarshalBuffers_NoLock();
|
||
|
||
if (frameType == ZDBC.FT_CAN)
|
||
{
|
||
var frame = ZlgNativeCanFrame.Create(canId, data ?? Array.Empty<byte>(), requestTxEcho: false);
|
||
Marshal.StructureToPtr(frame, _canFramePtr, false);
|
||
|
||
var ok = ZDBC.ZDBC_Decode(_dbcHandle, _msgPtr, _canFramePtr, 1, frameType);
|
||
if (!ok)
|
||
{
|
||
return (string.Empty, new Dictionary<string, double>());
|
||
}
|
||
|
||
var msg = Marshal.PtrToStructure<ZDBC.DBCMessage>(_msgPtr);
|
||
return ExtractActualSignals(msg);
|
||
}
|
||
|
||
if (frameType == ZDBC.FT_CANFD)
|
||
{
|
||
var frame = ZlgNativeCanFdFrame.Create(canId, data ?? Array.Empty<byte>(), requestTxEcho: false);
|
||
Marshal.StructureToPtr(frame, _canFdFramePtr, false);
|
||
|
||
var ok = ZDBC.ZDBC_Decode(_dbcHandle, _msgPtr, _canFdFramePtr, 1, frameType);
|
||
if (!ok)
|
||
{
|
||
return (string.Empty, new Dictionary<string, double>());
|
||
}
|
||
|
||
var msg = Marshal.PtrToStructure<ZDBC.DBCMessage>(_msgPtr);
|
||
return ExtractActualSignals(msg);
|
||
}
|
||
|
||
throw new ArgumentOutOfRangeException(nameof(frameType), "frameType 仅支持 FT_CAN=0 或 FT_CANFD=1。");
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public void Dispose()
|
||
{
|
||
if (_disposed) return;
|
||
_disposed = true;
|
||
|
||
lock (_sync)
|
||
{
|
||
try
|
||
{
|
||
if (_dbcHandle != 0 && _dbcHandle != ZDBC.INVALID_DBC_HANDLE)
|
||
{
|
||
ZDBC.ZDBC_Release(_dbcHandle);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Warn($"ZDBC_Release 异常:{ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
_dbcHandle = 0;
|
||
}
|
||
|
||
FreeMarshalBuffers_NoLock();
|
||
_messageByName.Clear();
|
||
_messageIdByName.Clear();
|
||
}
|
||
}
|
||
|
||
private void RefreshMessageCache_NoLock()
|
||
{
|
||
_messageByName.Clear();
|
||
_messageIdByName.Clear();
|
||
|
||
if (!IsLoaded)
|
||
{
|
||
return;
|
||
}
|
||
|
||
EnsureMarshalBuffers_NoLock();
|
||
|
||
// 遍历消息
|
||
var msg = new ZDBC.DBCMessage
|
||
{
|
||
vSignals = new ZDBC.DBCSignal[ZDBC._DBC_SIGNAL_MAX_COUNT_],
|
||
strName = new byte[ZDBC._DBC_NAME_LENGTH_ + 1],
|
||
strComment = new byte[ZDBC._DBC_COMMENT_MAX_LENGTH_ + 1]
|
||
};
|
||
|
||
Marshal.StructureToPtr(msg, _msgPtr, false);
|
||
var ok = ZDBC.ZDBC_GetFirstMessage(_dbcHandle, _msgPtr);
|
||
while (ok)
|
||
{
|
||
var m = Marshal.PtrToStructure<ZDBC.DBCMessage>(_msgPtr);
|
||
var name = ByteArrayToAsciiString(m.strName);
|
||
if (!string.IsNullOrWhiteSpace(name))
|
||
{
|
||
_messageByName[name] = m;
|
||
_messageIdByName[name] = m.nID;
|
||
}
|
||
|
||
Marshal.StructureToPtr(msg, _msgPtr, false);
|
||
ok = ZDBC.ZDBC_GetNextMessage(_dbcHandle, _msgPtr);
|
||
}
|
||
}
|
||
|
||
private void EnsureMarshalBuffers_NoLock()
|
||
{
|
||
if (_msgPtr == IntPtr.Zero)
|
||
{
|
||
_msgPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.DBCMessage)));
|
||
}
|
||
|
||
if (_countPtr == IntPtr.Zero)
|
||
{
|
||
_countPtr = Marshal.AllocHGlobal(sizeof(int));
|
||
}
|
||
|
||
if (_canFramePtr == IntPtr.Zero)
|
||
{
|
||
_canFramePtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZlgNativeCanFrame)));
|
||
}
|
||
|
||
if (_canFdFramePtr == IntPtr.Zero)
|
||
{
|
||
_canFdFramePtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZlgNativeCanFdFrame)));
|
||
}
|
||
}
|
||
|
||
private void FreeMarshalBuffers_NoLock()
|
||
{
|
||
if (_canFdFramePtr != IntPtr.Zero)
|
||
{
|
||
Marshal.FreeHGlobal(_canFdFramePtr);
|
||
_canFdFramePtr = IntPtr.Zero;
|
||
}
|
||
|
||
if (_canFramePtr != IntPtr.Zero)
|
||
{
|
||
Marshal.FreeHGlobal(_canFramePtr);
|
||
_canFramePtr = IntPtr.Zero;
|
||
}
|
||
|
||
if (_countPtr != IntPtr.Zero)
|
||
{
|
||
Marshal.FreeHGlobal(_countPtr);
|
||
_countPtr = IntPtr.Zero;
|
||
}
|
||
|
||
if (_msgPtr != IntPtr.Zero)
|
||
{
|
||
Marshal.FreeHGlobal(_msgPtr);
|
||
_msgPtr = IntPtr.Zero;
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
private void SetSignalActualValue(ref ZDBC.DBCMessage msg, string signalName, double actualValue)
|
||
{
|
||
if (msg.vSignals == null || msg.vSignals.Length == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var count = (int)Math.Min(msg.nSignalCount, (uint)msg.vSignals.Length);
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var s = msg.vSignals[i];
|
||
var name = ByteArrayToAsciiString(s.strName);
|
||
if (!string.Equals(name, signalName, StringComparison.Ordinal))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 使用 ZDBC_CalcRawValue 做实际值->原始值转换
|
||
IntPtr pSgl = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.DBCSignal)));
|
||
IntPtr pVal = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(double)));
|
||
try
|
||
{
|
||
Marshal.StructureToPtr(s, pSgl, false);
|
||
Marshal.StructureToPtr(actualValue, pVal, false);
|
||
var raw = ZDBC.ZDBC_CalcRawValue(pSgl, pVal);
|
||
msg.vSignals[i].nRawvalue = raw;
|
||
}
|
||
finally
|
||
{
|
||
Marshal.FreeHGlobal(pVal);
|
||
Marshal.FreeHGlobal(pSgl);
|
||
}
|
||
|
||
return;
|
||
}
|
||
}
|
||
|
||
private (string MsgName, Dictionary<string, double> Signals) ExtractActualSignals(ZDBC.DBCMessage msg)
|
||
{
|
||
var msgName = ByteArrayToAsciiString(msg.strName);
|
||
var dict = new Dictionary<string, double>(StringComparer.Ordinal);
|
||
|
||
if (msg.vSignals == null)
|
||
{
|
||
return (msgName, dict);
|
||
}
|
||
|
||
var count = (int)Math.Min(msg.nSignalCount, (uint)msg.vSignals.Length);
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var s = msg.vSignals[i];
|
||
var name = ByteArrayToAsciiString(s.strName);
|
||
if (string.IsNullOrWhiteSpace(name))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 使用 ZDBC_CalcActualValue 做原始值->实际值转换
|
||
IntPtr pSgl = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.DBCSignal)));
|
||
IntPtr pRaw = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ulong)));
|
||
try
|
||
{
|
||
Marshal.StructureToPtr(s, pSgl, false);
|
||
Marshal.StructureToPtr(s.nRawvalue, pRaw, false);
|
||
var actual = ZDBC.ZDBC_CalcActualValue(pSgl, pRaw);
|
||
dict[name] = actual;
|
||
}
|
||
finally
|
||
{
|
||
Marshal.FreeHGlobal(pRaw);
|
||
Marshal.FreeHGlobal(pSgl);
|
||
}
|
||
}
|
||
|
||
return (msgName, dict);
|
||
}
|
||
|
||
private static byte[] BuildFixedPathBytes(string path, int fixedLen)
|
||
{
|
||
var bytes = Encoding.ASCII.GetBytes(path);
|
||
var dst = new byte[fixedLen];
|
||
Array.Clear(dst, 0, dst.Length);
|
||
Array.Copy(bytes, 0, dst, 0, Math.Min(bytes.Length, fixedLen - 1));
|
||
return dst;
|
||
}
|
||
|
||
private static string ByteArrayToAsciiString(byte[]? bytes)
|
||
{
|
||
if (bytes == null || bytes.Length == 0) return string.Empty;
|
||
|
||
int len = Array.IndexOf(bytes, (byte)0);
|
||
if (len < 0) len = bytes.Length;
|
||
|
||
return Encoding.ASCII.GetString(bytes, 0, len).Trim();
|
||
}
|
||
|
||
private void ThrowIfDisposed()
|
||
{
|
||
if (_disposed)
|
||
{
|
||
throw new ObjectDisposedException(nameof(ZlgDbcDatabase));
|
||
}
|
||
}
|
||
}
|
||
}
|