周立功的CAN /FD实现

This commit is contained in:
2026-02-06 12:34:34 +08:00
parent 2e8ad1cffa
commit 74338fdb3a
13 changed files with 4260 additions and 310 deletions

View File

@@ -88,10 +88,10 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
#endregion
#region
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern IntPtr ZCAN_OpenDevice(uint device_type, uint device_index, uint reserved);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
public static extern IntPtr ZCAN_OpenDeviceByName(uint device_type, string name);
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]

View File

@@ -2,8 +2,10 @@ 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;
@@ -21,6 +23,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
private readonly object _sync = new object();
private readonly ILogService _log;
private static IntPtr _preloadedZlgcanHandle = IntPtr.Zero;
private static string? _preloadedZlgcanError;
private IntPtr _deviceHandle = IntPtr.Zero;
private readonly IntPtr[] _canChannelHandles = new IntPtr[2];
private IntPtr _linChannelHandle = IntPtr.Zero;
@@ -189,16 +194,189 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
EnsureNativeDllExists("zlgcan.dll");
_deviceHandle = ZLGCAN.ZCAN_OpenDevice(ZLGCAN.ZCAN_USBCANFD_200U, deviceIndex, 0);
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)
{
throw new InvalidOperationException("ZCAN_OpenDevice 失败,请确认驱动/设备连接/程序位数与 DLL 匹配。");
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>
@@ -1286,6 +1464,78 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
{
throw new FileNotFoundException($"未找到 {dllName},请将其复制到程序输出目录:{baseDir}", full);
}
TryPreloadNativeDll(full);
if (_preloadedZlgcanHandle == IntPtr.Zero && !string.IsNullOrWhiteSpace(_preloadedZlgcanError))
{
throw new InvalidOperationException($"预加载 {dllName} 失败:{_preloadedZlgcanError}。DLL 路径:{full}");
}
var dllArch = TryGetPeArchitecture(full);
if (!string.IsNullOrWhiteSpace(dllArch))
{
var procArch = Environment.Is64BitProcess ? "x64" : "x86";
if (Environment.Is64BitProcess && !string.Equals(dllArch, "x64", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x64 版本 DLL或将程序改为 x86 运行。DLL 路径:{full}");
}
if (!Environment.Is64BitProcess && !string.Equals(dllArch, "x86", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x86 版本 DLL或将程序改为 x64 运行。DLL 路径:{full}");
}
}
}
/// <summary>
/// 尝试读取 PE 头判断 DLL 架构。
/// </summary>
/// <param name="dllFullPath">DLL 完整路径。</param>
/// <returns>返回 "x86"/"x64"/"arm64"/"unknown(...)";读取失败返回 null。</returns>
private static string? TryGetPeArchitecture(string dllFullPath)
{
try
{
using var fs = new FileStream(dllFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var br = new BinaryReader(fs);
if (fs.Length < 0x40)
{
return null;
}
var mz = br.ReadUInt16();
if (mz != 0x5A4D)
{
return null;
}
fs.Position = 0x3C;
var peOffset = br.ReadInt32();
if (peOffset <= 0 || peOffset > fs.Length - 6)
{
return null;
}
fs.Position = peOffset;
var peSig = br.ReadUInt32();
if (peSig != 0x00004550)
{
return null;
}
var machine = br.ReadUInt16();
return machine switch
{
0x014c => "x86",
0x8664 => "x64",
0xAA64 => "arm64",
_ => $"unknown(0x{machine:X})"
};
}
catch
{
return null;
}
}
private static Dictionary<string, CanDbcModel> BuildDbcModelIndex(IEnumerable<CanDbcModel> models)

View File

@@ -79,6 +79,33 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
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)
@@ -91,26 +118,42 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
}
}
var fileInfo = new ZDBC.FileInfo
var loadOk = false;
foreach (var path in candidatePaths)
{
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)
var fileInfo = new ZDBC.FileInfo
{
throw new InvalidOperationException("ZDBC_LoadFile 失败,请检查 DBC 文件格式。");
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);
}
}
finally
if (!loadOk)
{
Marshal.FreeHGlobal(pFile);
// 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();
@@ -542,13 +585,128 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
private static byte[] BuildFixedPathBytes(string path, int fixedLen)
{
var bytes = Encoding.ASCII.GetBytes(path);
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;
@@ -556,7 +714,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
int len = Array.IndexOf(bytes, (byte)0);
if (len < 0) len = bytes.Length;
return Encoding.ASCII.GetString(bytes, 0, len).Trim();
return Encoding.Default.GetString(bytes, 0, len).Trim();
}
private void ThrowIfDisposed()