Files
CapMachine/CapMachine.Wpf/CanDrive/ZlgCan/ZlgDbcDatabase.cs

729 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CapMachine.Wpf.CanDrive;
using CapMachine.Wpf.Services;
using 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>
/// 基于周立功 ZDBCzdbc.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);
}
var fileSize = 0L;
try
{
fileSize = new FileInfo(dbcPath).Length;
}
catch
{
}
var fullPath = dbcPath;
try
{
fullPath = Path.GetFullPath(dbcPath);
}
catch
{
}
var shortPath = TryGetShortPath(fullPath);
var candidatePaths = new List<string>();
if (!string.IsNullOrWhiteSpace(fullPath)) candidatePaths.Add(fullPath);
if (!string.IsNullOrWhiteSpace(shortPath)
&& !string.Equals(shortPath, fullPath, StringComparison.OrdinalIgnoreCase))
{
candidatePaths.Add(shortPath);
}
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 loadOk = false;
foreach (var path in candidatePaths)
{
var fileInfo = new ZDBC.FileInfo
{
strFilePath = BuildFixedPathBytes(path, 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);
loadOk = ZDBC.ZDBC_LoadFile(_dbcHandle, pFile);
if (loadOk)
{
break;
}
}
finally
{
Marshal.FreeHGlobal(pFile);
}
}
if (!loadOk)
{
// fallback直接按内容加载绕开路径编码问题同时尝试多种编码以提升兼容性。
loadOk = TryLoadContentFallback_NoLock(fullPath, merge);
}
if (!loadOk)
{
var msg = $"ZDBC_LoadFile 失败。DbcPath={fullPath}ShortPath={shortPath ?? string.Empty}Size={fileSize}。请检查 DBC 文件格式,或将 DBC 放到纯英文路径后重试。";
throw new InvalidOperationException(msg);
}
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=0FT_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=0FT_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.Default.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;
}
/// <summary>
/// 当 ZDBC_LoadFile 失败时的兜底加载策略:读取文件内容并调用 ZDBC_LoadContent。
/// </summary>
/// <param name="fullPath">DBC 完整路径。</param>
/// <param name="merge">是否合并。</param>
/// <returns>加载成功返回 true。</returns>
private bool TryLoadContentFallback_NoLock(string fullPath, bool merge)
{
// zdbc.dll 入口为 ANSI 字符串,因此这里优先使用 ASCII将非 ASCII 字符替换为 ?
// 避免中文注释/特殊字符导致原生库解析失败;同时需要兼容 UTF-16 BOM否则用 ASCII 读会出现大量 \0 造成内容截断。
var encodings = new List<Encoding>();
try
{
var bytes = File.ReadAllBytes(fullPath);
if (bytes.Length >= 2)
{
// UTF-16 LE BOM: FF FE
if (bytes[0] == 0xFF && bytes[1] == 0xFE)
{
encodings.Add(Encoding.Unicode);
}
// UTF-16 BE BOM: FE FF
else if (bytes[0] == 0xFE && bytes[1] == 0xFF)
{
encodings.Add(Encoding.BigEndianUnicode);
}
}
if (bytes.Length >= 3)
{
// UTF-8 BOM: EF BB BF
if (bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
{
encodings.Add(Encoding.UTF8);
}
}
}
catch
{
}
// 常规兜底编码序列
encodings.Add(new UTF8Encoding(false, true));
encodings.Add(Encoding.UTF8);
encodings.Add(Encoding.Default);
encodings.Add(Encoding.ASCII);
foreach (var enc in encodings.Distinct())
{
IntPtr pContent = IntPtr.Zero;
try
{
var content = File.ReadAllText(fullPath, enc);
pContent = Marshal.StringToHGlobalAnsi(content);
var ok = ZDBC.ZDBC_LoadContent(_dbcHandle, pContent, merge ? 1u : 0u);
if (ok)
{
return true;
}
}
catch
{
}
finally
{
if (pContent != IntPtr.Zero)
{
Marshal.FreeHGlobal(pContent);
}
}
}
return false;
}
/// <summary>
/// 获取 Windows 8.3 短路径。
/// </summary>
/// <param name="lpszLongPath">长路径。</param>
/// <param name="lpszShortPath">短路径缓冲。</param>
/// <param name="cchBuffer">缓冲区大小。</param>
/// <returns>返回写入的字符数量0 表示失败。</returns>
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, uint cchBuffer);
/// <summary>
/// 尝试将长路径转换为短路径8.3),用于兼容部分原生库对中文/特殊字符路径处理不完整的问题。
/// </summary>
/// <param name="fullPath">长路径。</param>
/// <returns>短路径;失败返回 null。</returns>
private static string? TryGetShortPath(string fullPath)
{
try
{
if (string.IsNullOrWhiteSpace(fullPath))
{
return null;
}
var sb = new StringBuilder(1024);
var ret = GetShortPathName(fullPath, sb, (uint)sb.Capacity);
if (ret == 0)
{
return null;
}
return sb.ToString();
}
catch
{
return null;
}
}
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.Default.GetString(bytes, 0, len).Trim();
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ZlgDbcDatabase));
}
}
}
}