diff --git a/.windsurf/rules/zlgcan.md b/.windsurf/rules/zlgcan.md
new file mode 100644
index 0000000..1861d74
--- /dev/null
+++ b/.windsurf/rules/zlgcan.md
@@ -0,0 +1,36 @@
+---
+())trigger: manual
+---
+### 目标:根据周立功的CAN卡提供的官方资料,进行封装成C#类库供使用,主要是CAN,CAN FD,LIN三个通信,其中CAN和CAN FD使用DBC配置,LIN使用LDF配置,
+
+### 周立功CAN卡型号:USBCANFD 200U
+
+### 官网网络资源地址
+
+函数库简介:https://manual.zlg.cn/web/#/152/5333
+开发流程:https://manual.zlg.cn/web/#/152/5334
+数据结构定义:https://manual.zlg.cn/web/#/152/5335
+函数说明:https://manual.zlg.cn/web/#/152/5336
+设备类型号:https://manual.zlg.cn/web/#/152/6361
+
+### 官方ZLGAPI
+
+CapMachine.Wpf\CanDrive\ZlgCan\ZLGAPI.cs
+
+### 官方接口函数使用方法:
+
+CapMachine.Wpf\CanDrive\ZlgCan\接口函数使用手册.pdf
+
+### 要求:
+
+相关的封装内容放到CapMachine.Wpf\CanDrive\ZlgCan下,封装要体现工程化能力,需要考虑性能,后期长时间运行
+
+功能要全面,我之前用的图莫斯的CAN卡,是另外一套函数,在CapMachine.Wpf\CanDrive 和CapMachine.Wpf\LinDrive 中封装实现,你可以查看
+
+官方的样例在\CapMachine\Sample\C#_USBCANFD_251215,可借鉴使用
+
+可以联网查询信息
+
+需要详细的注释分析过程
+
+当前提供的CAN和LIN的基础能力,我最终需要的是使用DBC(CAN/CAN FD)和LDF(LIN)配置文件进行读取数据和控制发送
diff --git a/CapMachine.Model/CANLIN/CANFdConfigExd.cs b/CapMachine.Model/CANLIN/CANFdConfigExd.cs
index 4425e27..bbb670d 100644
--- a/CapMachine.Model/CANLIN/CANFdConfigExd.cs
+++ b/CapMachine.Model/CANLIN/CANFdConfigExd.cs
@@ -49,5 +49,11 @@ namespace CapMachine.Model.CANLIN
///
[Column(Name = "DbcPath", IsNullable = false, StringLength = 500)]
public string? DbcPath { get; set; }
+
+ ///
+ /// 调度表是否启用
+ ///
+ [Column(Name = "SchEnable")]
+ public bool SchEnable { get; set; }
}
}
diff --git a/CapMachine.Model/CANLIN/CANFdScheduleConfig.cs b/CapMachine.Model/CANLIN/CANFdScheduleConfig.cs
new file mode 100644
index 0000000..5789e15
--- /dev/null
+++ b/CapMachine.Model/CANLIN/CANFdScheduleConfig.cs
@@ -0,0 +1,60 @@
+using FreeSql.DataAnnotations;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CapMachine.Model.CANLIN
+{
+ ///
+ /// 调度表的配置
+ /// 其实这些调度表是在DBC中有的,但是图莫斯的驱动没有读取到这些信息
+ /// 那么我们在系统层面进行操作和保存这些信息
+ ///
+ [Table(Name = "CANFdScheduleConfig")]
+ public class CANFdScheduleConfig
+ {
+ ///
+ /// 主键
+ ///
+ [Column(IsPrimary = true, IsIdentity = true)]
+ public long Id { get; set; }
+
+ ///
+ /// 消息名称
+ ///
+ [Column(Name = "MsgName")]
+ public string? MsgName { get; set; }
+
+ ///
+ /// 消息的周期
+ ///
+ [Column(Name = "Cycle")]
+ public int Cycle { get; set; }
+
+ ///
+ /// 发送方式
+ ///
+ [Column(Name = "OrderSend")]
+ public int OrderSend { get; set; }
+
+ ///
+ /// 调度表的Index
+ /// //约定每帧对应一个调度表,预设5个调度表,每个调度表对应一个帧
+ /// 0-4这个范围的设置Index
+ ///
+ [Column(Name = "SchTabIndex")]
+ public int SchTabIndex { get; set; }
+
+
+
+
+ ///
+ /// ///////////////////////////////////////////导航属性///////////////////////////////////////////////////////
+ ///
+
+ public long CanLinConfigProId { get; set; }
+ public CanLinConfigPro? CanLinConfigPro { get; set; }
+ }
+}
diff --git a/CapMachine.Model/CANLIN/CanLinConfigPro.cs b/CapMachine.Model/CANLIN/CanLinConfigPro.cs
index 48f1f48..1756c75 100644
--- a/CapMachine.Model/CANLIN/CanLinConfigPro.cs
+++ b/CapMachine.Model/CANLIN/CanLinConfigPro.cs
@@ -50,6 +50,12 @@ namespace CapMachine.Model.CANLIN
///CAN 的调度表配置模式
public List? CanScheduleConfigs { get; set; }
+ ///
+ /// ///////////////////////////////////////////导航属性///////////////////////////////////////////////////////
+ ///
+ ///CAN 的调度表配置模式
+ public List? CanFdScheduleConfigs { get; set; }
+
///
/// ///////////////////////////////////////////导航属性///////////////////////////////////////////////////////
///
diff --git a/CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs b/CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs
index 2e3dce0..281b631 100644
--- a/CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs
+++ b/CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs
@@ -1,6 +1,8 @@
using CapMachine.Wpf.CanDrive.CanFD;
+using CapMachine.Wpf.Dtos;
using CapMachine.Wpf.Models.Tag;
using CapMachine.Wpf.Services;
+using ImTools;
using NPOI.OpenXmlFormats.Wordprocessing;
using Prism.Ioc;
using Prism.Mvvm;
@@ -20,20 +22,41 @@ using System.Windows.Interop;
namespace CapMachine.Wpf.CanDrive
{
///
- /// Toomoss CANFD
+ /// 图莫斯(Toomoss)CAN FD 驱动封装。
///
+ ///
+ /// 职责边界:
+ /// - 负责图莫斯 USB2XXX(CANFD)SDK 的设备扫描/打开/初始化/关闭;
+ /// - 负责 DBC 文件解析,并提供“信号值 <-> CANFD 帧”的互转(通过 SDK 的 DBCParserByFD 接口);
+ /// - 提供三类发送能力:
+ /// - 单次发送();
+ /// - 软件侧精确周期发送(,以及并行版本 );
+ /// - 硬件侧调度表发送( / ,以及 增量更新);
+ /// - 提供接收能力:后台轮询读取报文并同步到 DBC 信号实时值()。
+ ///
+ /// 线程与资源:
+ /// - 发送/接收/调度表更新均可能在后台线程运行;
+ /// - 多处使用 分配非托管内存,必须在 finally 中释放,避免长期运行内存上涨。
+ ///
+ /// 重要约束:
+ /// - 为 0 表示 DBC 未解析成功,任何 DBC_* 调用都应视为不可用;
+ /// - / 表示 CANFD 通道索引(0=CAN1,1=CAN2)。
+ ///
public class ToomossCanFD : BindableBase
{
private readonly IContainerProvider ContainerProvider;
///
- /// 实例化函数
+ /// 构造函数。
///
- public ToomossCanFD(IContainerProvider containerProvider)
+ /// DI 容器。
+ /// 日志服务。
+ public ToomossCanFD(IContainerProvider containerProvider, ILogService logService)
{
ContainerProvider = containerProvider;
+ LoggerService = logService;
HighSpeedDataService = ContainerProvider.Resolve();
- LogService = ContainerProvider.Resolve();
+ //LogService = ContainerProvider.Resolve();
//Stopwatch.Frequency表示高精度计时器每秒的计数次数(ticks/秒)每毫秒的ticks数 = 每秒的ticks数 ÷ 1000
TicksPerMs = Stopwatch.Frequency / 1000.0;
}
@@ -43,11 +66,22 @@ namespace CapMachine.Wpf.CanDrive
///
public HighSpeedDataService HighSpeedDataService { get; set; }
- public ILogService LogService { get; set; }
+ public ILogService LoggerService { get; set; }
///
- /// 开始CAN的驱动
+ /// 启动 CANFD 驱动(设备准备流程)。
///
+ ///
+ /// 执行顺序与 SDK 依赖一致:
+ /// 1) 校验 DLL 是否存在;
+ /// 2) 扫描设备并选取句柄;
+ /// 3) 打开设备;
+ /// 4) 读取设备信息;
+ /// 5) 获取 CANFD 初始化配置(仲裁/数据波特率);
+ /// 6) 初始化 CANFD。
+ ///
+ /// 注意:该方法不解析 DBC;DBC 解析由 显式触发。
+ ///
public void StartCanDrive()
{
try
@@ -61,14 +95,16 @@ namespace CapMachine.Wpf.CanDrive
}
catch (Exception ex)
{
- LogService.Error(ex.Message);
+ LoggerService.Error(ex.Message);
System.Windows.MessageBox.Show($"{ex.Message}", "提示", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Hand);
}
}
///
- /// 开始Dbc文件写入
+ /// 解析并加载 DBC 文件。
///
+ /// DBC 文件路径。
+ /// 解析得到的信号列表(用于 UI 展示与实时值刷新)。
public ObservableCollection StartDbc(string DbcPath)
{
DBC_Parser(DbcPath);
@@ -76,43 +112,51 @@ namespace CapMachine.Wpf.CanDrive
}
///
- /// Dbc消息集合
+ /// DBC 解析得到的信号集合。
///
+ ///
+ /// 接收线程会持续更新 。
+ ///
public ObservableCollection ListCanFdDbcModel { get; set; } = new ObservableCollection();
#region 公共变量
///
- /// 仲裁波特率
+ /// 仲裁波特率(Arbitration Bit Rate)。
///
public uint ArbBaudRate { get; set; } = 500000;
///
- /// 数据波特率
+ /// 数据波特率(Data Bit Rate)。
///
public uint DataBaudRate { get; set; } = 2000000;
///
- /// CAN FD标准 ISO是否启用
+ /// 是否启用 ISO CRC(CAN FD ISO 标准)。
///
public bool ISOEnable { get; set; } = true;
///
- /// 终端电阻 是否启用
+ /// 终端电阻是否启用。
///
public bool ResEnable { get; set; } = true;
///
- /// 更新配置
+ /// 更新 CANFD 参数配置(仅更新内存变量,实际生效需重新 InitCAN)。
///
- public void UpdateConfig(uint arbBaudRate, uint dataBaudRate, bool iSOEnable, bool resEnable,ushort sendCycle)
+ /// 仲裁波特率。
+ /// 数据波特率。
+ /// ISO CRC 使能。
+ /// 终端电阻使能。
+ /// 软件循环发送周期(ms)。
+ public void UpdateConfig(uint arbBaudRate, uint dataBaudRate, bool iSOEnable, bool resEnable, ushort sendCycle)
{
ArbBaudRate = arbBaudRate;
DataBaudRate = dataBaudRate;
ISOEnable = iSOEnable;
ResEnable = resEnable;
- SendCycle=sendCycle;
+ SendCycle = sendCycle;
}
///
@@ -136,8 +180,11 @@ namespace CapMachine.Wpf.CanDrive
public USB2CANFD.CANFD_BUS_ERROR CurCANFD_BUS_ERROR = new USB2CANFD.CANFD_BUS_ERROR();
///
- /// DBC Handle
+ /// DBC 解析成功后得到的句柄。
///
+ ///
+ /// 为 0 表示 DBC 未解析/解析失败。
+ ///
public UInt64 DBCHandle { get; set; }
///
@@ -151,19 +198,19 @@ namespace CapMachine.Wpf.CanDrive
public Int32 DevHandle { get; set; } = 0;
///
- /// Write CAN Index
+ /// 发送通道索引(0=CAN1,1=CAN2)。
///
public Byte WriteCANIndex { get; set; } = 0;
///
- /// Read CAN Index
+ /// 接收通道索引(0=CAN1,1=CAN2)。
///
public Byte ReadCANIndex { get; set; } = 0;
private bool _OpenState;
///
- /// 打开设备的状态
+ /// 设备打开状态。
///
public bool OpenState
{
@@ -173,7 +220,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _DbcParserState;
///
- /// DBC解析的状态
+ /// DBC 解析状态。
///
public bool DbcParserState
{
@@ -402,8 +449,12 @@ namespace CapMachine.Wpf.CanDrive
}
///
- /// 发送CAN数据
+ /// 单次发送:根据 CmdData 中的信号值构建 CANFD 报文并发送。
///
+ /// 待发送的信号指令集合(按 MsgName 分组后组成帧)。
+ ///
+ /// 注意:该方法会分配临时非托管内存 msgPt,并在结束前释放。
+ ///
public void SendCanMsg(List CmdData)
{
var GroupMsg = CmdData.GroupBy(x => x.MsgName);
@@ -414,6 +465,7 @@ namespace CapMachine.Wpf.CanDrive
CanMsg[i].Data = new Byte[64];
}
+ // 非托管缓冲:SDK API 以指针形式写入帧结构体
IntPtr msgPt = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CANFD.CANFD_MSG)));
int Index = 0;
//循环给MSG赋值数据
@@ -447,7 +499,7 @@ namespace CapMachine.Wpf.CanDrive
//DBCParser.DBC_SyncValueToCANFDMsg(DBCHandle, new StringBuilder("msg_speed_can"), msgPt);
//CanMsg[2] = (USB2CANFD.CANFD_MSG)Marshal.PtrToStructure(msgPt, typeof(USB2CANFD.CANFD_MSG));
- //释放申请的临时缓冲区
+ // 释放临时非托管缓冲区,避免内存泄漏
Marshal.FreeHGlobal(msgPt);
Console.WriteLine("");
//发送CAN数据
@@ -484,8 +536,18 @@ namespace CapMachine.Wpf.CanDrive
private static readonly Random _random = new Random();
///
- /// 精确周期发送CAN数据
+ /// 定时扫描更新数据 扫描Task
///
+ private static Task CycleUpdateCmdTask { get; set; }
+
+ ///
+ /// 软件侧精确周期发送 CANFD 数据。
+ ///
+ ///
+ /// - 使用 tick 作为时间基准,减少 DateTime 抖动;
+ /// - 使用 Task.Delay + SpinWait 组合等待;
+ /// - 使用 支持外部取消。
+ ///
public void StartPrecisionCycleSendMsg()
{
// 创建取消标记源 用于控制任务的取消 允许在需要时通过取消令牌来优雅停止任务
@@ -550,7 +612,7 @@ namespace CapMachine.Wpf.CanDrive
//此时重置NextExecutionTime为当前时间,避免连续的延迟累积
// 严重延迟,重新校准
NextExecutionTime = Stopwatcher.ElapsedTicks;
- LogService.Info("定时发送延迟过大,重新校准时间");
+ LoggerService.Info("定时发送延迟过大,重新校准时间");
Console.WriteLine("定时发送延迟过大,重新校准时间");
}
@@ -572,6 +634,7 @@ namespace CapMachine.Wpf.CanDrive
CanMsg[i].Data = new Byte[64];
}
+ // 发送构帧临时缓冲:每轮申请/释放,避免与其他线程共享同一指针导致并发问题
IntPtr msgPtSend = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CANFD.CANFD_MSG)));
int Index = 0;
@@ -590,7 +653,7 @@ namespace CapMachine.Wpf.CanDrive
//通过DBC写入数据后生成CanMsg
//将信号值填入CAN消息里面
- //释放申请的临时缓冲区
+ // 释放非托管缓冲区(务必释放,否则长时间运行会造成内存泄漏)
Marshal.FreeHGlobal(msgPtSend);
//CanMsg[0].Flags = 5;
@@ -618,7 +681,7 @@ namespace CapMachine.Wpf.CanDrive
}
catch (Exception ex)
{
- LogService.Error(ex.Message);
+ LoggerService.Error(ex.Message);
Console.WriteLine($"CAN周期发送异常: {ex.Message}");
// 短暂暂停避免异常情况下CPU占用过高
IsSendOk = false;
@@ -632,7 +695,7 @@ namespace CapMachine.Wpf.CanDrive
{
// 确保在任何情况下(正常退出、异常、取消)都会停止计时器
Stopwatcher.Stop();
- LogService.Error(ex.Message);
+ LoggerService.Error(ex.Message);
// 清理其他可能的资源
Console.WriteLine("CAN周期发送任务已结束,资源已清理");
IsSendOk = false;
@@ -674,8 +737,12 @@ namespace CapMachine.Wpf.CanDrive
}
///
- /// 修改停止发送的方法
+ /// 停止软件循环发送。
///
+ ///
+ /// - 置 为 false,循环自然退出;
+ /// - 触发 取消,用于尽快唤醒 Delay 并退出。
+ ///
public void StopCycleSendMsg()
{
IsCycleSend = false;
@@ -765,7 +832,7 @@ namespace CapMachine.Wpf.CanDrive
if (messageStopwatch.ElapsedTicks >= nextExecutionTime + cycleInTicks)
{
nextExecutionTime = messageStopwatch.ElapsedTicks;
- LogService.Info($"消息{msgName}定时发送延迟过大,重新校准时间");
+ LoggerService.Info($"消息{msgName}定时发送延迟过大,重新校准时间");
}
// 使用锁确保DBC操作和发送的线程安全
@@ -779,12 +846,12 @@ namespace CapMachine.Wpf.CanDrive
// 为信号赋值
foreach (var signal in signals)
{
- CAN_DBCParser.DBC_SetSignalValue(DBCHandle, new StringBuilder(msgName),
+ DBCParserByFD.DBC_SetSignalValue(DBCHandle, new StringBuilder(msgName),
new StringBuilder(signal.SignalName), signal.SignalCmdValue);
}
// 同步值到CAN消息
- CAN_DBCParser.DBC_SyncValueToCANMsg(DBCHandle, new StringBuilder(msgName), msgPtSend);
+ DBCParserByFD.DBC_SyncValueToCANFDMsg(DBCHandle, new StringBuilder(msgName), msgPtSend);
var canMsg = (USB2CAN.CAN_MSG)Marshal.PtrToStructure(msgPtSend, typeof(USB2CAN.CAN_MSG));
// 直接发送此消息,无需通过队列
@@ -800,7 +867,7 @@ namespace CapMachine.Wpf.CanDrive
else
{
IsSendOk = false;
- LogService.Info($"消息{msgName}发送失败: {sendedNum}");
+ LoggerService.Info($"消息{msgName}发送失败: {sendedNum}");
}
}
finally
@@ -812,23 +879,23 @@ namespace CapMachine.Wpf.CanDrive
}
catch (TaskCanceledException)
{
- LogService.Info($"消息{msgName}发送任务被取消");
+ LoggerService.Info($"消息{msgName}发送任务被取消");
break;
}
catch (Exception ex)
{
- LogService.Info($"消息{msgName}发送异常: {ex.Message}");
+ LoggerService.Info($"消息{msgName}发送异常: {ex.Message}");
await Task.Delay(10, token);
}
}
}
catch (Exception ex)
{
- LogService.Info($"消息{msgName}发送线程异常: {ex.Message}");
+ LoggerService.Info($"消息{msgName}发送线程异常: {ex.Message}");
}
finally
{
- LogService.Info($"消息{msgName}发送线程已退出");
+ LoggerService.Info($"消息{msgName}发送线程已退出");
}
}, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
@@ -845,11 +912,11 @@ namespace CapMachine.Wpf.CanDrive
}
catch (Exception ex)
{
- LogService.Info($"并行发送监控任务异常: {ex.Message}");
+ LoggerService.Info($"并行发送监控任务异常: {ex.Message}");
}
finally
{
- LogService.Info("并行发送任务已全部结束");
+ LoggerService.Info("并行发送任务已全部结束");
IsSendOk = false;
}
}, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
@@ -859,9 +926,405 @@ namespace CapMachine.Wpf.CanDrive
+ #region 调度表发送报文
+
+ private bool _SchEnable;
+ ///
+ /// 调度表使能开关。
+ ///
+ ///
+ /// 该开关用于控制“硬件调度表发送”及“CmdData 变化时是否增量更新调度表”。
+ ///
+ public bool SchEnable
+ {
+ get { return _SchEnable; }
+ set
+ {
+ _SchEnable = value;
+ RaisePropertyChanged();
+ }
+ }
+
+
+ ///
+ /// 调度表的发送的报文数据
+ ///
+ private USB2CANFD.CANFD_MSG[] SchCanMsg { get; set; }
+
+ ///
+ /// 依据消息的分组
+ ///
+ private IEnumerable> GroupMsg { get; set; }
+
+ ///
+ /// 调度表集合数据
+ /// 总共3个调度表,第一个表里面包含3帧数据,第二个调度表包含6帧数据,第三个调度表包含11帧数据
+ /// Byte[] MsgTabNum = new Byte[3] { 3, 6, 11 };
+ ///
+ private Byte[] MsgTabNum { get; set; }
+
+ ///
+ /// 调度表发送的次数集合
+ /// 第一个调度表循环发送数据,第二个调度表循环发送数据,第三个调度表只发送3次
+ /// UInt16[] SendTimes = new UInt16[3] { 0xFFFF, 0xFFFF, 3 };
+ ///
+ private UInt16[] SendTimes { get; set; }
+
+ ///
+ /// 预设的调度表的个数 常值
+ ///
+ private const int MsgTabCount = 5;
+
+ ///
+ /// 定时更新时间
+ ///
+ private int UpdateCycle { get; set; } = 100;
+
+ ///
+ /// CNA 调度表的配置信息
+ ///
+ public List ListCANScheduleConfig { get; set; }
+
+ Random random = new Random();
+
+ ///
+ /// 监控数据
+ /// 查找问题用,平时不用
+ ///
+ //public MonitorValueLog monitorValueLog { get; set; }
+
+ ///
+ /// 更新数据 测试用废弃了
+ ///
+ public void UpdateValue()
+ {
+ //通过DBC进行对消息赋值
+ IntPtr msgPtSend = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CANFD.CANFD_MSG)));
+ int Index = 0;
+ //循环给MSG赋值数据
+ foreach (var itemMsg in GroupMsg)
+ {
+ foreach (var itemSignal in itemMsg)
+ {
+ itemSignal.SignalCmdValue = random.Next(0, 100);
+ DBCParserByFD.DBC_SetSignalValue(DBCHandle, new StringBuilder(itemMsg.Key), new StringBuilder(itemSignal.SignalName), itemSignal.SignalCmdValue);
+ }
+ DBCParserByFD.DBC_SyncValueToCANFDMsg(DBCHandle, new StringBuilder(itemMsg.Key), msgPtSend);
+ SchCanMsg[Index] = (USB2CANFD.CANFD_MSG)Marshal.PtrToStructure(msgPtSend, typeof(USB2CANFD.CANFD_MSG));
+ Index++;
+ }
+
+ //通过DBC写入数据后生成CanMsg
+ //将信号值填入CAN消息里面
+
+ //释放申请的临时缓冲区
+ Marshal.FreeHGlobal(msgPtSend);
+
+
+ ////总共3个调度表,第一个表里面包含3帧数据,第二个调度表包含6帧数据,第三个调度表包含11帧数据
+ //MsgTabNum = new Byte[1] { 1 };
+ ////第一个调度表循环发送数据,第二个调度表循环发送数据,第三个调度表只发送3次
+ //SendTimes = new UInt16[1] { 0xFFFF };
+ //var ret = USB2CANFD.CAN_SetSchedule(DevHandle, WriteCANIndex, SchCanMsg, MsgTabNum, SendTimes, 1);//配置调度表,该函数耗时可能会比较长,但是只需要执行一次即可
+ var ret = USB2CANFD.CANFD_UpdateSchedule(DevHandle, WriteCANIndex, 0, 0, SchCanMsg, 1);//配置调度表,该函数耗时可能会比较长,但是只需要执行一次即可
+ if (ret == USB2CANFD.CANFD_SUCCESS)
+ {
+ Console.WriteLine("Update CAN Schedule Success");
+ }
+ else
+ {
+ Console.WriteLine("Update CAN Schedule Error ret = {0}", ret);
+ return;
+ }
+ }
+
+ ///
+ /// 启动硬件调度表(CANFD)。
+ ///
+ ///
+ /// 流程与 CAN 类似:
+ /// 1) 基于 分组构建帧数组;
+ /// 2) 使用 DBC 将信号值同步到帧;
+ /// 3) 将调度周期写入帧 ;
+ /// 4) 调用 CANFD_SetSchedule 下发;
+ /// 5) 调用 CANFD_StartSchedule 启动调度器 0。
+ ///
+ public void StartSchedule()
+ {
+ if (CmdData.Count() == 0) return;
+
+ //依据报文进行分组
+ GroupMsg = CmdData.GroupBy(x => x.MsgName)!;
+ //初始化调度表要发送的消息结构
+ SchCanMsg = new USB2CANFD.CANFD_MSG[GroupMsg.Count()];
+ for (int i = 0; i < GroupMsg.Count(); i++)
+ {
+ SchCanMsg[i] = new USB2CANFD.CANFD_MSG();
+ SchCanMsg[i].Data = new Byte[64];
+ }
+
+ //通过DBC进行对消息赋值
+ IntPtr msgPtSend = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CANFD.CANFD_MSG)));
+ int Index = 0;
+ //循环给MSG赋值数据
+ foreach (var itemMsg in GroupMsg)
+ {
+ foreach (var itemSignal in itemMsg)
+ {
+ DBCParserByFD.DBC_SetSignalValue(DBCHandle, new StringBuilder(itemMsg.Key), new StringBuilder(itemSignal.SignalName), itemSignal.SignalCmdValue);
+ }
+ DBCParserByFD.DBC_SyncValueToCANFDMsg(DBCHandle, new StringBuilder(itemMsg.Key), msgPtSend);
+ //每个分组就是一个帧指令/消息数据
+ SchCanMsg[Index] = (USB2CANFD.CANFD_MSG)Marshal.PtrToStructure(msgPtSend, typeof(USB2CANFD.CANFD_MSG));
+ // 将“MsgName -> 帧 index”的映射写回调度表配置,用于调试/定位(当前更新实现是全量更新,不依赖 MsgIndex)
+ if (ListCANScheduleConfig.Any(a => a.MsgName == itemMsg.Key))
+ {
+ //把帧的位置给ListCANScheduleConfig,方便更新时定位数据
+ ListCANScheduleConfig.FindFirst(a => a.MsgName == itemMsg.Key).MsgIndex = Index;
+
+ // 设备 SDK 约定:TimeStamp 字段用于调度表发送周期(单位毫秒)
+ SchCanMsg[Index].TimeStamp = (uint)ListCANScheduleConfig.FindFirst(a => a.MsgName == itemMsg.Key).Cycle;
+ }
+ Index++;
+ }
+ //通过DBC写入数据后生成CanMsg
+ //将信号值填入CAN消息里面
+ //释放申请的临时缓冲区
+ Marshal.FreeHGlobal(msgPtSend);
+
+ //********就是可以设置多个调度表放那里,但是运行时同一个时刻只能运行调度表其中的一个 ********
+ //****** 控制报文SchCanMsg和调度器中第一个调度器中的报文集合和要更新的报文集合都是同一个变量SchCanMsg, ********
+ // *** SchCanMsg的Index序号和ListCANScheduleConfig的MsgIndex是一样的 ***
+
+ //图莫斯的Sample:总共3个调度表,第一个表里面包含3帧数据,第二个调度表包含6帧数据,第三个调度表包含11帧数据
+ //预设5个调度表,但是我们只用其中第一个调度表,第一个调度表中包括多少消息帧,由系统的控制指令的帧的分布决定,SchCanMsg.Count()是所需要的控制发送的帧,都放到第一个调度表中
+
+ MsgTabNum = new Byte[MsgTabCount] { (byte)SchCanMsg.Count(), 1, 1, 1, 1 };
+ //0xFFFF:调度表循环发送数据,X:调度表循环发送的次数
+ //设置每个调度表的发送方式,约定全部为循环发送
+ SendTimes = new UInt16[MsgTabCount] { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF };
+
+ //SchCanMsg:需要发送的消息集合;MsgTabNum:调度表集合数据;SendTimes:发送次数集合数据;调度表个数:MsgTabCount
+ var ret = USB2CANFD.CANFD_SetSchedule(DevHandle, WriteCANIndex, SchCanMsg, MsgTabNum, SendTimes, MsgTabCount);//配置调度表,该函数耗时可能会比较长,但是只需要执行一次即可
+ if (ret == USB2CANFD.CANFD_SUCCESS)
+ {
+ Console.WriteLine("Set CAN Schedule Success");
+ }
+ else
+ {
+ Console.WriteLine("Set CAN Schedule Error ret = {0}", ret);
+ LoggerService.Info($"Set CAN Schedule Error; 返回错误代码:{ret}");
+ return;
+ }
+
+ // 约定使用调度器索引 0:同一时刻只能运行一个调度器,且本项目把所有控制帧都集中在同一个调度器里
+ //CAN_MSG的TimeStamp就是这个报文发送的周期,由调度器协调
+ ret = USB2CANFD.CANFD_StartSchedule(DevHandle, WriteCANIndex, (byte)0, (byte)100, (byte)ListCANScheduleConfig.FirstOrDefault()!.OrderSend);
+ if (ret == USB2CANFD.CANFD_SUCCESS)
+ {
+ Console.WriteLine($"Start CAN Schedule 1 Success,SchTabIndex:{(byte)0} - Cycle:{(byte)100} - OrderSend:{(byte)1}");
+ }
+ else
+ {
+ Console.WriteLine("Start CAN Schedule 1 Error ret = {0}", ret);
+ LoggerService.Info($"Start CAN Schedule 1 Error;");
+ return;
+ }
+
+
+ //foreach (var itemGroupMsg in GroupMsg)
+ //{
+ // if (itemGroupMsg == null) continue;
+ // if (ListCANScheduleConfig.Any(a => a.MsgName!.Contains(itemGroupMsg.Key)))
+ // {
+ // var CANScheduleConfig = ListCANScheduleConfig.FindFirst(a => a.MsgName!.Contains(itemGroupMsg.Key));
+ // //配置表里面包括这个报文消息内容
+ // ret = USB2CANFD.CAN_StartSchedule(DevHandle, WriteCANIndex, (byte)CANScheduleConfig.SchTabIndex, (byte)CANScheduleConfig.Cycle, (byte)CANScheduleConfig.OrderSend);
+ // if (ret == USB2CANFD.CAN_SUCCESS)
+ // {
+ // Console.WriteLine($"Start CAN Schedule 1 Success,SchTabIndex:{(byte)CANScheduleConfig.SchTabIndex} - Cycle:{(byte)CANScheduleConfig.Cycle} - OrderSend:{(byte)CANScheduleConfig.OrderSend}");
+ // }
+ // else
+ // {
+ // Console.WriteLine("Start CAN Schedule 1 Error ret = {0}", ret);
+ // LoggerService.Info($"Start CAN Schedule 1 Error;消息名称:{CANScheduleConfig.MsgName}");
+ // return;
+ // }
+ // }
+ // else
+ // {
+ // LoggerService.Info($"调度表配置未发现对应的消息报文信息;报文信息{itemGroupMsg.Key}");
+ // }
+ //}
+
+ //走到这里说明调度表执行的是OK的
+ //IsSendOk = true;
+
+ }
+
+
+ ///
+ /// 停止硬件调度表。
+ ///
+ ///
+ /// 停止后设备侧将不再按周期自动发送。
+ ///
+ public void StopSchedule()
+ {
+ ret = USB2CANFD.CANFD_StopSchedule(DevHandle, WriteCANIndex);//启动第一个调度表,表里面的CAN帧并行发送
+ if (ret == USB2CANFD.CANFD_SUCCESS)
+ {
+ IsSendOk = false;
+ Console.WriteLine("Stop CAN Schedule");
+ LoggerService.Info($"Stop CAN Schedule");
+ }
+ else
+ {
+ Console.WriteLine("Start CAN Schedule Error ret = {0}", ret);
+ LoggerService.Info($"Stop CAN Schedule");
+ return;
+ }
+
+
+ }
+
+ ///
+ /// 循环使用的
+ ///
+ private int CycleUpdateIndex = 0;
+
+ ///
+ /// 绑定/刷新驱动侧要发送的 CmdData,并自动维护“值变化事件”的订阅。
+ ///
+ /// 新的指令集合。
+ ///
+ /// 注意:必须先对旧集合取消订阅,否则会造成重复触发和内存泄漏。
+ ///
+ public void LoadCmdDataToDrive(List cmdData)
+ {
+ // Unsubscribe from events on the old CmdData items
+ if (CmdData != null && CmdData.Count > 0)
+ {
+ foreach (var cmd in CmdData)
+ {
+ cmd.CanCmdDataChangedHandler -= CmdData_CanCmdDataChangedHandler;
+ }
+ }
+
+ // Set the new data and subscribe to events
+ CmdData = cmdData;
+ foreach (var cmd in cmdData)
+ {
+ cmd.CanCmdDataChangedHandler += CmdData_CanCmdDataChangedHandler;
+ }
+ }
+
+ ///
+ /// CmdData 中任意信号值变化事件处理。
+ ///
+ /// 事件源。
+ /// 变化的 MsgName(约定为报文名)。
+ private void CmdData_CanCmdDataChangedHandler(object? sender, string e)
+ {
+ UpdateSchDataByCmdDataChanged();
+ }
+
+ ///
+ /// 调度表更新互斥锁。
+ ///
+ private readonly object SchUpdateLock = new object();
+
+ ///
+ /// 当 CmdData 中信号值变化时,增量刷新调度表的帧数据。
+ ///
+ ///
+ /// 触发条件: 与 同时为 true。
+ /// 当前实现为“全量覆盖更新”(CANFD_UpdateSchedule)。
+ ///
+ public void UpdateSchDataByCmdDataChanged()
+ {
+ try
+ {
+ if (!IsCycleSend) return;
+ if (!SchEnable) return;
+
+ // 基础防御:确保 DBC/ 调度表 / 分组已经初始化
+ if (DBCHandle == 0 || SchCanMsg == null || GroupMsg == null)
+ {
+ return;
+ }
+
+ lock (SchUpdateLock)
+ {
+ // 重新构建每一帧的 Data,并覆盖更新到设备调度表
+ IntPtr msgPtSend = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CANFD.CANFD_MSG)));
+ int CycleUpdateIndex = 0;
+ //循环给MSG赋值数据,顺序是固定的,跟初始时设置是一样的
+ foreach (var itemMsg in GroupMsg)
+ {
+ foreach (var itemSignal in itemMsg)
+ {
+ //itemSignal.SignalCmdValue = random.Next(0, 100); //仿真测试数据使用
+ var SetSignalValue = DBCParserByFD.DBC_SetSignalValue(DBCHandle, new StringBuilder(itemMsg.Key), new StringBuilder(itemSignal.SignalName), itemSignal.SignalCmdValue);
+ //monitorValueLog.UpdateValue1(SetSignalValue);
+ }
+ var SyncValueToCanMsg = DBCParserByFD.DBC_SyncValueToCANFDMsg(DBCHandle, new StringBuilder(itemMsg.Key), msgPtSend);
+ //monitorValueLog.UpdateValue2(SyncValueToCanMsg);
+ SchCanMsg[CycleUpdateIndex] = (USB2CANFD.CANFD_MSG)Marshal.PtrToStructure(msgPtSend, typeof(USB2CANFD.CANFD_MSG));
+ CycleUpdateIndex++;
+ }
+
+ //通过DBC写入数据后生成CanMsg
+ //将信号值填入CAN消息里面
+
+ // 释放非托管缓冲
+ Marshal.FreeHGlobal(msgPtSend);
+
+ //CAN_UpdateSchedule 官网解释
+ // ---MsgTabIndex CAN调度表索引号
+ // ---MsgIndex 开始更新帧起始索引,若起始索引大于调度表帧数,则将帧添加到调度表后面
+ // ---pCanMsg 需要更新的CAN帧指针
+ // ---MsgNum pCanMsgTab里面包含的有效帧数
+
+ //CAN_UpdateSchedule中的MsgIndex表示当前的调度器中的帧Index序号
+ //因为调度表中的帧集合和控制帧的集合和要更新的帧集合都是同一个集合SchCanMsg
+ // 默认调度表索引 0:一次性覆盖更新所有帧(MsgIndex=0, MsgNum=帧数)
+ var ret = USB2CANFD.CANFD_UpdateSchedule(DevHandle, WriteCANIndex, (byte)0, (byte)(0), SchCanMsg, (byte)SchCanMsg.Count());//配置调度表,该函数耗时可能会比较长,但是只需要执行一次即可
+ if (ret == USB2CANFD.CANFD_SUCCESS)
+ {
+ IsSendOk = true;
+ Console.WriteLine($"Update CAN Schedule Success -- SchTabIndex:{(byte)0} -- MsgIndex:{(byte)(0)}-- SchCanMsg.Count():{(SchCanMsg.Count())} ");
+ //monitorValueLog.UpdateValue3(ret);
+ }
+ else
+ {
+ IsSendOk = false;
+ //Console.WriteLine($"Update CAN Schedule Error ret = {ret} -- SchTabIndex:{(byte)0} -- MsgIndex:{(byte)(0)}");
+ //monitorValueLog.UpdateValue3(ret);
+ LoggerService.Info($"更新调度表失败,错误码:{ret}");
+ //return;
+ }
+ }
+
+ }
+ catch (Exception ex)
+ {
+ IsSendOk = false;
+ LoggerService.Info($"时间:{DateTime.Now.ToString()}-【MSG】-{ex.Message}");
+ }
+
+ }
+
+
+
+ #endregion
+
+
+
+
private bool _IsCycleRevice;
///
- /// 是否循环接收数据
+ /// 是否循环接收数据。
///
public bool IsCycleRevice
{
@@ -872,7 +1335,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsCycleSend;
///
- /// 是否循环发送数据
+ /// 是否循环发送数据。
///
public bool IsCycleSend
{
@@ -881,12 +1344,12 @@ namespace CapMachine.Wpf.CanDrive
}
///
- /// 循环发送数据
+ /// 软件循环发送周期(毫秒)。
///
public ushort SendCycle { get; set; } = 200;
///
- /// 循环接受数据
+ /// 接收轮询周期(毫秒)。
///
public ushort ReviceCycle { get; set; } = 200;
@@ -906,7 +1369,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsSendOk;
///
- /// 发送报文是否OK
+ /// 最近一次发送是否成功。
///
public bool IsSendOk
{
@@ -924,7 +1387,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsReviceOk;
///
- /// 接收报文是否OK
+ /// 最近一次接收是否成功。
///
public bool IsReviceOk
{
@@ -941,18 +1404,22 @@ namespace CapMachine.Wpf.CanDrive
///
- /// 要发送的数据
+ /// 当前激活的指令集合(由 Service/UI 下发)。
///
public List CmdData { get; set; } = new List();
///
- /// 循环获取CAN消息
+ /// 启动后台循环接收 CANFD 报文,并同步到 DBC 信号实时值。
///
+ ///
+ /// 注意:当前实现每轮接收都会分配 msgPtRead 并释放,长时间运行会造成 GC 压力和潜在碎片;
+ /// 若后续需要优化,可参考 CAN 版本的“接收缓冲池复用 + CloseDevice 互斥释放”。
+ ///
public void StartCycleReviceCanMsg()
{
CycleReviceTask = Task.Run(async () =>
{
- //var ret = USB2CANFD.CANFD_StartGetMsg(DevHandle, ReadCANIndex);
+ //var ret = USB2CANFDFD.CANFD_StartGetMsg(DevHandle, ReadCANIndex);
while (IsCycleRevice)
{
await Task.Delay(ReviceCycle);
@@ -990,14 +1457,14 @@ namespace CapMachine.Wpf.CanDrive
else if (CanNum == 0)
{
IsReviceOk = false;
- Console.WriteLine("No CAN data!");
+ //Console.WriteLine("No CAN data!");
}
else
{
IsReviceOk = false;
Console.WriteLine("Get CAN data error!");
}
- Console.WriteLine("");
+ //Console.WriteLine("");
//将CAN消息数据填充到信号里面
DBCParserByFD.DBC_SyncCANFDMsgToValue(DBCHandle, msgPtRead, CanNum);
@@ -1008,7 +1475,7 @@ namespace CapMachine.Wpf.CanDrive
//有配置的名称的,认为是有用的,则需要读取数据
//if (!string.IsNullOrEmpty(item.Name))
//{
- //CAN_DBCParser.DBC_GetSignalValueStr(DBCHandle, new StringBuilder(item.MsgName), new StringBuilder(item.SignalName), ValueSb);
+ //DBCParserByFD.DBC_GetSignalValueStr(DBCHandle, new StringBuilder(item.MsgName), new StringBuilder(item.SignalName), ValueSb);
//double[] ValueDouble;
DBCParserByFD.DBC_GetSignalValue(DBCHandle, new StringBuilder(item.MsgName), new StringBuilder(item.SignalName), ValueDouble);
//item.SignalRtValueSb = ValueSb;
@@ -1017,7 +1484,7 @@ namespace CapMachine.Wpf.CanDrive
//}
}
- //释放数据缓冲区,必须释放,否则程序运行一段时间后会报内存不足
+ // 释放数据缓冲区,必须释放,否则程序运行一段时间后会报内存不足
Marshal.FreeHGlobal(msgPtRead);
Thread.Sleep(10);
@@ -1031,8 +1498,8 @@ namespace CapMachine.Wpf.CanDrive
catch (Exception ex)
{
IsReviceOk = false;
- LogService.Error(ex.Message);
- //LogService.Info($"时间:{DateTime.Now.ToString()}-【Meter】-{ex.Message}");
+ LoggerService.Error(ex.Message);
+ //LoggerService.Info($"时间:{DateTime.Now.ToString()}-【Meter】-{ex.Message}");
}
}
@@ -1091,8 +1558,14 @@ namespace CapMachine.Wpf.CanDrive
///
- /// 关闭设备
+ /// 关闭设备并释放资源。
///
+ ///
+ /// 释放顺序要点:
+ /// - 关闭设备并下置状态位;
+ /// - 若启用调度表则先停止;
+ /// - 等待接收任务退出(短等待,避免 UI 卡死)。
+ ///
public void CloseDevice()
{
//关闭设备
@@ -1101,6 +1574,25 @@ namespace CapMachine.Wpf.CanDrive
DbcParserState = false;
IsCycleRevice = false;
IsCycleSend = false;
+ if (SchEnable)
+ {
+ StopSchedule();
+ }
+
+ // 确保定时器发送被停止并释放资源
+ //try { StopTimerCycleSendMsg(); } catch { }
+
+ // 等待接收任务结束后释放非托管缓冲区,避免并发释放
+ try
+ {
+ var task = CycleReviceTask;
+ if (task != null && !task.IsCompleted)
+ {
+ task.Wait(TimeSpan.FromMilliseconds(ReviceCycle + 500));
+ }
+ }
+ catch { }
+
}
}
diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs
new file mode 100644
index 0000000..90e2d76
--- /dev/null
+++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs
@@ -0,0 +1,1250 @@
+using System.Runtime.InteropServices;
+
+namespace CapMachine.Wpf.CanDrive.ZlgCan
+{
+ ///
+ /// ZLG 原生驱动(zlgcan.dll)P/Invoke 映射层。
+ /// 说明:
+ /// - 本类仅负责将厂商 SDK 的函数/常量/结构体映射到 C#,不包含任何业务逻辑;上层 Driver/Service 会对这些 API 进行封装并提供线程安全与资源管理。
+ /// - 互操作注意事项:
+ /// 1) zlgcan.dll 必须随程序部署且位数与进程一致(x86/x64),否则会在加载/调用时失败。
+ /// 2) 结构体需严格按 SDK 定义设置 与 Pack;字段顺序与大小必须一致。
+ /// 3) DllImport 的 CallingConvention 与 SetLastError 需与 SDK 匹配,否则可能出现栈不平衡或错误码不可用。
+ /// - 约定:当前工程中通常由 ZlgCanFd200uDriver 负责检查 DLL 存在与架构匹配,并在打开设备前完成预检查。
+ ///
+ public class ZLGCAN
+ {
+
+ #region 设备类型
+ // 设备类型常量:用于 ZCAN_OpenDevice/ZCAN_OpenDeviceByName。
+ // 说明:常量值来自厂商 SDK;此处保持原值以便与文档/示例对照。
+ public static UInt32 ZCAN_PCI9810 = 2;
+ public static UInt32 ZCAN_USBCAN1 = 3;
+ public static UInt32 ZCAN_USBCAN2 = 4;
+ public static UInt32 ZCAN_PCI9820 = 5;
+ public static UInt32 ZCAN_CANETUDP = 12;
+ public static UInt32 ZCAN_PCI9840 = 14;
+ public static UInt32 ZCAN_PCI9820I = 16;
+ public static UInt32 ZCAN_CANETTCP = 17;
+ public static UInt32 ZCAN_PCI5010U = 19;
+ public static UInt32 ZCAN_USBCAN_E_U = 20;
+ public static UInt32 ZCAN_USBCAN_2E_U = 21;
+ public static UInt32 ZCAN_PCI5020U = 22;
+ public static UInt32 ZCAN_PCIE9221 = 24;
+ public static UInt32 ZCAN_WIFICAN_TCP = 25;
+ public static UInt32 ZCAN_WIFICAN_UDP = 26;
+ public static UInt32 ZCAN_PCIe9120 = 27;
+ public static UInt32 ZCAN_PCIe9110 = 28;
+ public static UInt32 ZCAN_PCIe9140 = 29;
+ public static UInt32 ZCAN_USBCAN_4E_U = 31;
+ public static UInt32 ZCAN_CANDTU_200UR = 32;
+ public static UInt32 ZCAN_USBCAN_8E_U = 34;
+ public static UInt32 ZCAN_CANDTU_NET = 36;
+ public static UInt32 ZCAN_CANDTU_100UR = 37;
+ public static UInt32 ZCAN_PCIE_CANFD_200U = 39;
+ public static UInt32 ZCAN_PCIE_CANFD_400U = 40;
+ public static UInt32 ZCAN_USBCANFD_200U = 41;
+ public static UInt32 ZCAN_USBCANFD_100U = 42;
+ public static UInt32 ZCAN_USBCANFD_MINI = 43;
+ public static UInt32 ZCAN_CANSCOPE = 45;
+ public static UInt32 ZCAN_CLOUD = 46;
+ public static UInt32 ZCAN_CANDTU_NET_400 = 47;
+ public static UInt32 ZCAN_CANFDNET_TCP = 48;
+ public static UInt32 ZCAN_CANFDNET_200U_TCP = 48;
+ public static UInt32 ZCAN_CANFDNET_UDP = 49;
+ public static UInt32 ZCAN_CANFDNET_200U_UDP = 49;
+ public static UInt32 ZCAN_CANFDWIFI_TCP = 50;
+ public static UInt32 ZCAN_CANFDWIFI_100U_TCP = 50;
+ public static UInt32 ZCAN_CANFDWIFI_UDP = 51;
+ public static UInt32 ZCAN_CANFDWIFI_100U_UDP = 51;
+ public static UInt32 ZCAN_CANFDNET_400U_TCP = 52;
+ public static UInt32 ZCAN_CANFDNET_400U_UDP = 53;
+ public static UInt32 ZCAN_CANFDNET_100U_TCP = 55;
+ public static UInt32 ZCAN_CANFDNET_100U_UDP = 56;
+ public static UInt32 ZCAN_CANFDNET_800U_TCP = 57;
+ public static UInt32 ZCAN_CANFDNET_800U_UDP = 58;
+ public static UInt32 ZCAN_USBCANFD_800U = 59;
+ public static UInt32 ZCAN_PCIE_CANFD_100U_EX = 60;
+ public static UInt32 ZCAN_PCIE_CANFD_400U_EX = 61;
+ public static UInt32 ZCAN_PCIE_CANFD_200U_MINI = 62;
+ public static UInt32 ZCAN_PCIE_CANFD_200U_EX = 63;
+ public static UInt32 ZCAN_PCIE_CANFD_200U_M2 = 63;
+ public static UInt32 ZCAN_CANFDDTU_400_TCP = 64;
+ public static UInt32 ZCAN_CANFDDTU_400_UDP = 65;
+ public static UInt32 ZCAN_CANFDWIFI_200U_TCP = 66;
+ public static UInt32 ZCAN_CANFDWIFI_200U_UDP = 67;
+ public static UInt32 ZCAN_CANFDDTU_800ER_TCP = 68;
+ public static UInt32 ZCAN_CANFDDTU_800ER_UDP = 69;
+ public static UInt32 ZCAN_CANFDDTU_800EWGR_TCP = 70;
+ public static UInt32 ZCAN_CANFDDTU_800EWGR_UDP = 71;
+ public static UInt32 ZCAN_CANFDDTU_600EWGR_TCP = 72;
+ public static UInt32 ZCAN_CANFDDTU_600EWGR_UDP = 73;
+ public static UInt32 ZCAN_CANFDDTU_CASCADE_TCP = 74;
+ public static UInt32 ZCAN_CANFDDTU_CASCADE_UDP = 75;
+ public static UInt32 ZCAN_USBCANFD_400U = 76;
+ public static UInt32 ZCAN_CANFDDTU_200U = 77;
+ public static UInt32 ZCAN_ZPSCANFD_TCP = 78;
+ public static UInt32 ZCAN_ZPSCANFD_USB = 79;
+ public static UInt32 ZCAN_CANFDBRIDGE_PLUS = 80;
+ public static UInt32 ZCAN_CANFDDTU_300U = 81;
+ public static UInt32 ZCAN_PCIE_CANFD_800U = 82;
+ public static UInt32 ZCAN_PCIE_CANFD_1200U = 83;
+ public static UInt32 ZCAN_MINI_PCIE_CANFD = 84;
+ public static UInt32 ZCAN_USBCANFD_800H = 85;
+ #endregion
+
+ #region LIN事件
+ // LIN 事件常量:用于接收/状态通知。
+ public static UInt32 ZCAN_LIN_WAKE_UP = 1;
+ public static UInt32 ZCAN_LIN_ENTERED_SLEEP_MODE = 2;
+ public static UInt32 ZCAN_LIN_EXITED_SLEEP_MODE = 3;
+ #endregion
+
+ #region 函数
+ // 原生函数导入:
+ // - 这里不逐个函数详注,具体语义以 SDK 文档为准;
+ // - 上层 Driver 会封装为更易用的 C# API,并处理错误码、重试、线程模型等。
+ [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, SetLastError = true)]
+ public static extern IntPtr ZCAN_OpenDeviceByName(uint device_type, string name);
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_CloseDevice(IntPtr device_handle);
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_IsDeviceOnLine(IntPtr device_handle);
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_GetDeviceInf(IntPtr device_handle, IntPtr pInfo);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern IntPtr ZCAN_InitCAN(IntPtr device_handle, uint can_index, IntPtr pInitConfig);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern IntPtr ZCAN_InitCAN(IntPtr device_handle, uint can_index, ref ZCAN_CHANNEL_INIT_CONFIG config);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_StartCAN(IntPtr chn_handle);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_ResetCAN(IntPtr chn_handle);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_ClearBuffer(IntPtr chn_handle);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_ReadChannelErrInfo(IntPtr chn_handle, ref ZCAN_CHANNEL_ERR_INFO pErrInfo);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_GetReceiveNum(IntPtr channel_handle, byte type);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_Transmit(IntPtr channel_handle, IntPtr pTransmit, uint len);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_TransmitFD(IntPtr channel_handle, IntPtr pTransmit, uint len);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_TransmitData(IntPtr device_handle, IntPtr pTransmit, uint len);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_Receive(IntPtr channel_handle, IntPtr pReceive, uint len, int wait_time = -1);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_ReceiveFD(IntPtr channel_handle, IntPtr pReceive, uint len, int wait_time = -1);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_ReceiveData(IntPtr device_handle, IntPtr pReceive, uint len, int wait_time);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_SetValue(IntPtr device_handle, string path, string value);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_SetValue(IntPtr device_handle, string path, IntPtr value);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_SetValue(IntPtr device_handle, string path, ref ZCAN_AUTO_TRANSMIT_OBJ value);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_SetValue(IntPtr device_handle, string path, ref ZCANFD_AUTO_TRANSMIT_OBJ value);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern IntPtr ZCAN_GetValue(IntPtr device_handle, string path);
+
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern IntPtr ZCAN_InitLIN(IntPtr device_handle, uint lin_index, IntPtr pLINInitConfig);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_StartLIN(IntPtr channel_handle);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_ResetLIN(IntPtr channel_handle);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_TransmitLIN(IntPtr channel_handle, IntPtr pSend, uint Len);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_GetLINReceiveNum(IntPtr channel_handle);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_ReceiveLIN(IntPtr channel_handle, IntPtr pReceive, uint Len, int WaitTime);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_SetLINPublish(IntPtr channel_handle, IntPtr pSend, uint nPublishCount);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_SetLINSubscribe(IntPtr channel_handle, IntPtr pSend, uint nSubscribeCount);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_WakeUpLIN(IntPtr channel_handle);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_UDS_Request(IntPtr device_handle, IntPtr req, IntPtr resp, IntPtr dataBuf, uint dataBufSize);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_UDS_Control(IntPtr device_handle, IntPtr ctrl, IntPtr resp);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_UDS_RequestEX(IntPtr device_handle, IntPtr requestData, IntPtr resp, IntPtr dataBuf, uint dataBufSize);
+
+
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZCAN_UDS_ControlEX(IntPtr device_handle, uint dataType, IntPtr ctrl, IntPtr resp);
+ #endregion
+
+ #region 结构体
+ // SDK 结构体定义:用于与原生内存布局一致地传参/收参。
+ // 说明:字段注释多数来自 SDK 示例或历史代码;这里主要保证布局与 Pack 正确。
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_DEVICE_INFO
+ {
+ public ushort hw_Version;
+ public ushort fw_Version;
+ public ushort dr_Version;
+ public ushort in_Version;
+ public ushort irq_Num;
+ public byte can_Num;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
+ public char[] str_Serial_Num;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 40)]
+ public char[] str_hw_Type;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public byte[] reserved;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_CHANNEL_INIT_CONFIG
+ {
+ public uint can_type;
+ public _ZCAN_CHANNEL_INIT_CONFIG config;
+ }
+
+
+ [StructLayout(LayoutKind.Explicit, Pack = 1)]
+ public struct _ZCAN_CHANNEL_INIT_CONFIG
+ {
+ [FieldOffset(0)]
+ public _ZCAN_CHANNEL_CAN_INIT_CONFIG can;
+ [FieldOffset(0)]
+ public _ZCAN_CHANNEL_CANFD_INIT_CONFIG canfd;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct _ZCAN_CHANNEL_CAN_INIT_CONFIG
+ {
+ public uint acc_code;
+ public uint acc_mask;
+ public uint reserved;
+ public byte filter;
+ public byte timing0;
+ public byte timing1;
+ public byte mode;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct _ZCAN_CHANNEL_CANFD_INIT_CONFIG
+ {
+ public uint acc_code;
+ public uint acc_mask;
+ public uint abit_timing;
+ public uint dbit_timing;
+ public uint brp;
+ public byte filter;
+ public byte mode;
+ public ushort pad;
+ public uint reserved;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_CHANNEL_ERR_INFO
+ {
+ public uint error_code;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
+ public byte[] passive_ErrData;
+ public byte arLost_ErrData;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_Transmit_Data
+ {
+ public can_frame frame;
+ public uint transmit_type;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public class can_frame
+ {
+ public uint can_id;
+ public byte can_dlc; // frame payload length in byte (0 .. CAN_MAX_DLEN)
+ public byte __pad; // padding
+ public byte __res0; // reserved / padding
+ public byte __res1; // reserved / padding
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] data; // frame data
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public class canfd_frame
+ {
+ public uint can_id;
+ public byte len;
+ public byte flags;
+ public byte __res0;
+ public byte __res1; /* reserved / padding */
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
+ public byte[] data;
+ };
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public class ZCAN_Receive_Data
+ {
+ public can_frame frame;
+ public UInt64 timestamp;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public class ZCAN_ReceiveFD_Data
+ {
+ public canfd_frame frame;
+ public UInt64 timestamp;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_AUTO_TRANSMIT_OBJ //CANFD定时发送帧结构体
+ {
+ public ushort enable; //0-禁用,1-使能
+ public ushort index; //定时报文索引
+ public uint interval; //定时周期
+ public ZCAN_Transmit_Data obj;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANFD_AUTO_TRANSMIT_OBJ //CANFD定时发送帧结构体
+ {
+ public ushort enable; //0-禁用,1-使能
+ public ushort index; //定时报文索引
+ public uint interval; //定时周期
+ public ZCAN_TransmitFD_Data obj;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_TransmitFD_Data
+ {
+ public canfd_frame frame; // 报文数据信息,详见 canfd_frame 结构说明。
+ public uint transmit_type; // 发送方式,0=正常发送,1=单次发送,2=自发自收,3=单次自发自收。
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANCANFDData
+ {
+ public UInt64 timeStamp;
+ public UInt32 flag; // flag用于设置一些参数,内部结构可以通过以下函数实现设置和取值
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public byte[] extraData; //未使用
+ public canfd_frame frame; //实际报文结构体
+
+ // frameType 帧类型 0-CAN 1-CANFD
+ public uint frameType
+ {
+ get { return (flag & 0x03); }
+ set { flag = (uint)((flag & ~0x03) | (value & 0x03)); }
+ }
+
+ // txDelay队列发送延时,延时时间存放在 timeStamp 字段;0-不启用延时,1-启用延时,单位1ms,2-启用延时,单位100us
+ public uint txDelay
+ {
+ get { return ((flag >> 2) & 0x03); }
+ set { flag = (uint)((flag & ~0x0C) | (value & 0x03) << 2); }
+ }
+
+ public uint transmitType
+ {
+ get { return ((flag >> 4) & 0x0F); }
+ set { flag = (uint)((flag & ~0x0F) | (value & 0x0F) << 4); }
+ }
+
+ public uint txEchoRequest
+ {
+ get { return ((flag >> 8) & 0x01); }
+ set { flag = (uint)(flag | (value & 0x01) << 8); }
+ }
+
+ public uint txEchoed
+ {
+ get { return ((flag >> 9) & 0x01); } // bit9
+ set { flag = (uint)((flag & ~0x200) | (value & 0x01) << 9); }
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct PID
+ {
+ public byte rawVal;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct RxData
+ {
+ public UInt64 timeStamp;
+ public byte datalen;
+ public byte dir;
+ public byte chkSum;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 13)]
+ public byte[] reserved;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] data;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANDataObj
+ {
+ public byte dataType; // 1-CAN/CANFD数据, 4-LIN数据
+ public byte chnl; // 数据通道
+ public UInt16 flag; // 未使用
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public byte[] extraData; // 未使用
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 92)]
+ public byte[] data; // 报文结构体
+ }
+
+
+ // LIN
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANLINData
+ {
+ public PID pid; // 受保护的ID
+ public RxData rxData; // 数据
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
+ public byte[] reserved;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANLINErrData
+ {
+ public UInt64 timeStamp; // 时间戳,单位微秒(us)
+ public PID pid; // 受保护的ID
+
+ public byte dataLen; // 数据长度
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] data; // 数据
+
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
+ public byte[] errData; // 错误信息
+
+ public byte dir; // 传输方向
+ public byte chkSum; // 数据校验,部分设备不支持校验数据的获取
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
+ public byte[] reserved;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANLINEventData
+ {
+ public UInt64 timeStamp; // 时间戳,单位微秒(us)
+ public byte type; // 数据长度
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
+ public byte[] reserved;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_LIN_MSG
+ {
+ public byte chnl; // 数据通道
+ public byte dataType; // 0-LIN,1-ErrLIN
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 46)]
+ public byte[] data;
+ };
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_LIN_INIT_CONFIG
+ {
+ public byte linMode; // 0-slave,1-master
+ public byte chkSumMode; // 1-经典校验,2-增强校验 3-自动(对应eZLINChkSumMode的模式)
+ public byte maxLength; // 最大数据长度,8~64
+ public byte reserved;
+ public uint libBaud; // 波特率,取值1000~20000
+
+ };
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_LIN_PUBLISH_CFG
+ {
+ public byte ID; // 受保护的ID(ID取值范围为0-63)
+ public byte datelen; // 范围1~8
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] // 数据段内容
+ public byte[] data;
+ public byte chkSumMode; // 校验方式:0-默认,启动时配置 1-经典校验 2-增强校验(对应eZLINChkSumMode的模式)
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
+ public byte[] reserved;
+ };
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_LIN_SUBSCIBE_CFG
+ {
+ public byte ID; // 受保护的ID(ID取值范围为0-63)
+ public byte datelen; // dataLen范围为1-8 当为255(0xff)则表示设备自动识别报文长度
+ public byte chkSumMode; // 校验方式:0-默认,启动时配置 1-经典校验 2-增强校验(对应eZLINChkSumMode的模式)
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
+ public byte[] reserved;
+ };
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct SESSION_PARAM
+ {
+ public uint timeout; // 响应超时时间(ms)。因PC定时器误差,建议设置不小于200ms
+ public uint enhanced_timeout; // 收到消极响应错误码为0x78后的超时时间(ms)。因PC定时器误差,建议设置不小于200ms
+ public byte threeInOne; //三合一,把下面三个变量写进这一个变量
+
+ // threeInOne 包含以下三个变量
+ // BYTE check_any_negative_response : 1; // 接收到非本次请求服务的消极响应时是否需要判定为响应错误
+ // BYTE wait_if_suppress_response : 1; // 抑制响应时是否需要等待消极响应,等待时长为响应超时时间
+ // BYTE flag : 6; // 保留
+ public byte check_any_negative_response
+ {
+ get { return (byte)(threeInOne & 0x0001); }
+ set { threeInOne = (byte)((threeInOne & ~0x0001) | (value & 0x0001)); }
+ }
+ public byte wait_if_suppress_response
+ {
+ get { return (byte)((threeInOne & 0x0002) >> 1); }
+ set { threeInOne = (byte)((threeInOne & ~0x0002) | (value & 0x0002)); }
+ }
+ public byte flag
+ {
+ get { return (byte)((threeInOne & 0x00FC) >> 2); }
+ set { threeInOne = (byte)((threeInOne & ~0x00FC) | (value & 0x00FC)); }
+ }
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
+ public byte[] reserved0;
+ };
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct TRANS_PARAM
+ {
+ public byte version; // 传输协议版本,VERSION_0,VERSION_1
+ public byte max_data_len; // 单帧最大数据长度,can:8,canfd:64
+ public byte local_st_min; // 本程序发送流控时用,连续帧之间的最小间隔,0x00-0x7F(0ms~127ms),0xF1-0xF9(100us~900us)
+ public byte block_size; // 流控帧的块大小
+ public byte fill_byte; // 无效字节的填充数据
+ public byte ext_frame; // 0:标准帧 1:扩展帧
+ public byte is_modify_ecu_st_min; // 是否忽略ECU 返回流控的STmin,强制使用本程序设置的remote_st_min 参数代替
+ public byte remote_st_min; // 发送多帧时用, is_modify_ecu_st_min = 1 时有效,0x00 - 0x7F(0ms~127ms), 0xF1 - 0xF9(100us~900us)
+ public uint fc_timeout; // 接收流控超时时间(ms),如发送首帧后需要等待回应流控帧
+ public byte fill_mode; // 数据长度填充模式 0-就近填充 1-不填充 2-最大填充
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
+ public byte[] reserved0;
+ };
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct TRANS_PARAM_LIN
+ {
+ public byte fill_byte; // 无效字节的填充数据
+ public byte st_min; // 从节点准备接收诊断请求的下一帧或传输诊断响应的下一帧所需的最小时间
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
+ public byte[] reserved0;
+ };
+
+
+ // CAN UDS请求数据
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_UDS_REQUEST // 硬件UDS接口构体
+ {
+ public uint req_id; // 请求事务索引ID,范围0~65535
+ public byte channel; // 设备通道索引
+ public byte frame_type; // 帧类型
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
+ public byte[] reserved0;
+ public uint src_addr; // 请求地址
+ public uint dst_addr; // 响应地址
+ public byte suppress_response; // 1-抑制响应
+ public byte sid; // 请求服务id
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
+ public byte[] reserved1;
+
+ public SESSION_PARAM session_param;
+ public TRANS_PARAM trans_param;
+ public IntPtr data; // 数据数组(不包含SID)
+ public uint data_len; // 数据数组的长度
+ public uint reserved2;
+ }
+
+
+ // LIN UDS请求数据
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZLIN_UDS_REQUEST // 硬件UDS接口构体
+ {
+ public uint req_id; // 请求事务索引ID,范围0~65535
+ public byte channel; // 设备通道索引
+ public byte suppress_response; // 1:抑制响应 0:不抑制
+ public byte sid; // 请求服务id
+ public byte Nad; // 节点地址
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] reserved1;
+
+
+ public SESSION_PARAM session_param;
+ public TRANS_PARAM_LIN trans_param;
+ public IntPtr data; // 数据数组(不包含SID)
+ public uint data_len; // 数据数组的长度
+ public uint reserved2;
+ }
+
+
+ // UDS响应数据
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_UDS_RESPONSE
+ {
+ public byte status; // 响应状态
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
+ public byte[] reserved;
+ public byte type; // 响应类型
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] // 大小为8
+ public byte[] raw;
+ };
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct UDS_RESPONSE_Positive
+ {
+ public byte sid; // 响应服务id
+ public uint data_len; // 数据长度(不包含SID), 数据存放在接口传入的dataBuf中
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct UDS_RESPONSE_Negative
+ {
+ public byte neg_code; // 固定为0x7F
+ public byte sid; // 请求服务id
+ public byte error_code; // 错误码
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct UDS_RESPONSE_raw
+ {
+ public ulong raw;
+ }
+
+
+ [StructLayout(LayoutKind.Explicit)]
+ public struct _UDS_RESPONSE_union
+ {
+ [FieldOffset(0)]
+ public UDS_RESPONSE_Positive zudsPositive;
+
+ [FieldOffset(0)]
+ public UDS_RESPONSE_Negative zudsNegative;
+
+ [FieldOffset(0)]
+ public UDS_RESPONSE_raw raw;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANCANFDUdsData
+ {
+ public IntPtr req; // 请求信息
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)]
+ public byte[] reserved; // 保留位
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANLINUdsData
+ {
+ public IntPtr req; // 请求信息
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)]
+ public byte[] reserved; // 保留位
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCANUdsRequestDataObj
+ {
+ public uint dataType; // 数据类型
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 63)]
+ public byte[] data;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
+ public byte[] reserved; // 保留位
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_DYNAMIC_CONFIG_DATA
+ {
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
+ public char[] key;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
+ public char[] value;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct BusUsage
+ {
+ public ulong nTimeStampBegin; // 测量起始时间戳,单位us
+ public ulong nTimeStampEnd; // 测量结束时间戳,单位us
+ public byte nChnl; // 通道
+ public byte nReserved; // 保留
+ public ushort nBusUsage; // 总线利用率(%),总线利用率*100展示。取值0~10000,如8050表示80.50%
+ public uint nFrameCount; // 帧数量
+ }
+ #endregion
+ }
+
+
+ public class ZDBC
+ {
+ #region 常量定义
+ public const int _MAX_FILE_PATH_ = 260; // 最长文件路径
+ public const int _DBC_NAME_LENGTH_ = 127; // 名称最长长度
+ public const int _DBC_COMMENT_MAX_LENGTH_ = 127; // 注释最长长度
+ public const int _DBC_UNIT_MAX_LENGTH_ = 23; // 单位最长长度
+ public const int _DBC_SIGNAL_MAX_COUNT_ = 256; // 一个消息含有的信号的最大数目
+
+ public const int MUTIPLEXER_NONE = 0; // 不使用复用器
+ public const int MUTIPLEXER_M_VALUE = 1; // 复用信号,当复用器开关的值为multiplexer_value时,该信号有效
+ public const int MUTIPLEXER_M_SWITCH = 2; // 复用器开关,一个DBC消息只能有一个信号为开关
+
+ public const int FT_CAN = 0; // CAN
+ public const int FT_CANFD = 1; // CANFD
+
+ public const int PROTOCOL_J1939 = 0;
+ public const int PROTOCOL_OTHER = 1;
+ public const uint INVALID_DBC_HANDLE = 0xffffffff; // 无效的DBC句柄
+ #endregion
+
+ #region 函数部分
+
+ // ZDBC.dll
+ public delegate bool OnSend(IntPtr ctx, IntPtr pObj);
+ public delegate void OnMultiTransDone(IntPtr ctx, IntPtr pMsg, IntPtr data, UInt16 nLen, byte nDirection);
+ public static OnSend onSend;
+ public static OnMultiTransDone onMultiTransDone;
+
+ ///
+ /// 此函数用于初始化解析模块,只需要初始化一次。
+ ///
+ /// 是否关闭多帧发送,为 1 时不支持多帧的消息发送。
+ /// 是否开启异步解析;0-不启动,ZDBC_AsyncAnalyse 接口无效;1-启动, 独立线程解析出消息。
+ /// 为 INVALID_DBC_HANDLE 表示初始化失败,其他表示初始化成功,保存该返回值,之后的函数调用都要用到该句柄。
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZDBC_Init(byte disableMultiSend = 0, byte enableAsyncAnalyse = 1);
+
+ ///
+ /// 释放资源, 与DBC_Init配对使用
+ ///
+ /// hDBC-句柄, ZDBC_Init的返回值
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZDBC_Release(uint DBCHandle);
+
+ ///
+ /// 此函数用以加载 DBC 格式文件。
+ ///
+ /// 句柄;ZDBC_Init的返回值
+ /// 结构体 FileInfo的指针
+ /// 为 true 表示加载成功,false 表示失败。
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_LoadFile(uint DBCHandle, IntPtr pFileInfo);
+
+ ///
+ /// 从字符串加载DBC
+ ///
+ /// hDBC-句柄, DBC_Load的返回值
+ /// pFileContent-文件内容字符串
+ /// merge-是否合并到当前数据库; 1:不清除现有的数据, 即支持加载多个文件;0:清除原来的数据
+ ///
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_LoadContent(uint DBCHandle, IntPtr pFileContent, uint merge);
+
+ ///
+ /// 获取文件的第一条消息。
+ ///
+ /// hDBC-句柄, DBC_Load的返回值
+ /// pMsg 存储消息的信息
+ /// true表示成功
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_GetFirstMessage(uint DBCHandle, IntPtr pMsg);
+
+ ///
+ /// 获取下一条消息。
+ ///
+ /// hDBC-句柄, DBC_Load的返回值
+ /// pMsg 存储消息的信息
+ /// true表示成功
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_GetNextMessage(uint DBCHandle, IntPtr pMsg);
+
+ ///
+ /// 此函数用以根据 ID 获取消息数据。
+ ///
+ /// 句柄;
+ /// 帧 ID;
+ /// 消息信息结构体
+ ///
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_GetMessageById(uint DBCHandle, uint nID, IntPtr pMsg);
+
+ ///
+ /// 此函数用以获取 DBC 文件中含有的消息数目。
+ ///
+ /// DBC句柄
+ /// DBC 文件中含有的消息数目
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern uint ZDBC_GetMessageCount(uint DBCHandle);
+
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_Analyse(uint DBCHandle, IntPtr pObj, IntPtr pMsg);
+
+
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_AsyncAnalyse(uint DBCHandle, IntPtr pObj, uint frame_type, UInt64 extraData);
+
+
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZDBC_OnReceive(uint DBCHandle, IntPtr pObj, uint frame_type);
+
+
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZDBC_SetSender(uint hDBC, OnSend sender, IntPtr ctx);
+
+
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZDBC_SetOnMultiTransDoneFunc(uint hDBC, OnMultiTransDone func, IntPtr ctx);
+
+
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern byte ZDBC_Send(uint hDBC, IntPtr pMsg);
+
+ ///
+ /// 根据原始数据解码为 DBCMessage。
+ ///
+ /// DBC句柄
+ /// 输出参数,解析结果。
+ /// 帧数据数组, ControlCAN 传入 VCI_CAN_OBJ, zlgcan 传入 can_frame。
+ /// 原始帧数据个数, 即数组大小。
+ /// frame_type 帧类型, 参考FT_CAN=0、FT_CANFD=1,ControlCAN不支持CANFD。
+ /// 是否成功。
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_Decode(uint DBCHandle, IntPtr P2DBCMessage, IntPtr P2Obj, uint nCount, byte frame_type);
+
+ ///
+ /// 根据 DBCMessage 编码为原始数据。
+ ///
+ /// DBC句柄;
+ /// 编码的原始数据缓冲区数组, ControlCAN 传入 VCI_CAN_OBJ, zlgcan 传入 can_frame。
+ /// 输出参数,pObj 缓冲区大小, 返回时为实际原始数据个数。
+ /// 输入参数,DBC 消息。
+ /// frame_type 帧类型, FT_CAN=0、FT_CANFD=1,ControlCAN不支持CANFD。
+ /// 是否成功。
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_Encode(uint DBCHandle, IntPtr P2Obj, IntPtr P2nCount, IntPtr pMsg, byte frame_type);
+
+ ///
+ /// 信号原始值转换为实际值
+ ///
+ /// sgl 信号
+ /// rawVal 原始值, 如果该值超出信号长度可表示范围,会被截断。
+ /// 实际值
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern double ZDBC_CalcActualValue(IntPtr sgl, IntPtr rawVal); //原始值通过计算转为实际值,实际值会传入rawVal的地址
+
+ ///
+ /// 信号实际值转换为原始值
+ ///
+ /// sgl 信号
+ /// actualVal 实际值, 超出可表示范围时会被修正
+ /// 原始值
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt64 ZDBC_CalcRawValue(IntPtr sgl, IntPtr actualVal);
+
+ ///
+ /// 获取网络节点数量
+ ///
+ /// ZDBC_Init的返回值
+ /// 网络节点总数量
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZDBC_GetNetworkNodeCount(uint DBCHandle);
+
+ ///
+ ///
+ ///
+ /// ZDBC_Init的返回值
+ /// index 位置索引
+ /// DBCNetworkNode * node 网络节点信息
+ ///
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern bool ZDBC_GetNetworkNodeAt(uint DBCHandle, UInt32 index, IntPtr node);
+
+
+ ///
+ /// 获取具体信号的值与含义对个数
+ ///
+ /// ZDBC_Init的返回值
+ /// message的ID
+ /// signal的名字
+ ///
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZDBC_GetValDescPairCount(uint DBCHandle, UInt32 mag_id, string signal_name);
+
+
+ ///
+ /// 获取具体信号的值与含义对
+ ///
+ /// ZDBC_Init的返回值
+ /// message的ID
+ /// signal的名字
+ /// ValDescPair结构体类型
+ [DllImport("zdbc.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZDBC_GetValDescPair(uint DBCHandle, UInt32 mag_id, string signal_name, IntPtr pair);
+
+
+ #endregion
+
+ #region DBC 结构体部分
+
+ public struct FileInfo
+ {
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _MAX_FILE_PATH_ + 1)]
+ public byte[] strFilePath; // dbc文件路径
+ public byte type; // dbc的协议类型, j1939选择PROTOCOL_J1939, 其他协议选择PROTOCOL_OTHER
+ public byte merge; // 1:不清除现有的数据, 即支持加载多个文件 0:清除原来的数据
+ };
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
+ public struct DBCSignal
+ {
+
+ public UInt32 nStartBit; // 起始位
+ public UInt32 nLen; // 位长度
+ public double nFactor; // 转换因子
+ public double nOffset; // 转换偏移实际值=原始值*nFactor+nOffset
+ public double nMin; // 最小值
+ public double nMax; // 最大值
+ public UInt64 nRawvalue; // 原始值
+ public byte is_signed; // 1:有符号数据, 0:无符号
+ public byte is_motorola; // 是否摩托罗拉格式
+ public byte multiplexer_type; // 复用器类型
+ public byte val_type; // 0:integer, 1:float, 2:double
+ public UInt32 multiplexer_value; // 复用器开关值为此值时信号有效
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _DBC_UNIT_MAX_LENGTH_ + 1)]
+ public byte[] unit; //单位
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _DBC_NAME_LENGTH_ + 1)]
+ public byte[] strName; //名称
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _DBC_COMMENT_MAX_LENGTH_ + 1)]
+ public byte[] strComment; //注释
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _DBC_NAME_LENGTH_ + 1)]
+ public byte[] strValDesc; //值描述
+
+ public double initialValue; // 初始化值(原始值)
+ public uint initialValueValid; // 初始值是否有效
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
+ public struct DBCMessage
+ {
+ public UInt32 nSignalCount; // 信号数量
+ public UInt32 nID; // ID
+ public UInt32 nSize; // 消息占的字节数目
+ public double nCycleTime; // 发送周期
+ public byte nExtend; // 1:扩展帧, 0:标准帧
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _DBC_SIGNAL_MAX_COUNT_)]
+ public DBCSignal[] vSignals; // 信号集合
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _DBC_NAME_LENGTH_ + 1)]
+ public byte[] strName; // 名称
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _DBC_COMMENT_MAX_LENGTH_ + 1)]
+ public byte[] strComment; // 注释
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
+ public struct ValDescPair
+ {
+ public double value; // 信号值
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = _DBC_NAME_LENGTH_ + 1)]
+ public byte[] strName; // 对应的值描述
+ }
+ #endregion
+ }
+
+
+ public class ZUDS
+ {
+ #region 参数定义
+
+ public static uint udsRTR = 0x40000000; // Remote Transmission Request
+ public static uint udsEFF = 0x80000000; // Extend Frame Flag
+ public static uint udsERR = 0x20000000; // Err flag
+
+ #endregion
+
+ #region 函数部分
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ public delegate uint OnUDSTransmitDelegate(IntPtr ctx, IntPtr frame, uint count);
+
+
+ ///
+ /// 该函数用于初始化 UDS 函数库,返回操作句柄,用于后续的操作,与 ZUDS_Release
+ /// 配对使用。
+ /// typedef uint32 TP_TYPE; // transport protocol
+ /// #define DoCAN 0
+ ///
+ ///
+ /// 操作句柄,= ZUDS_INVALID_HANDLE 为无效句柄,其他值为有效句柄。
+ [DllImport("zuds.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZUDS_Init(uint type);
+
+
+ ///
+ /// 该函数用于释放资源,与 ZUDS_Init 配对使用。
+ ///
+ ///
+ ///
+ [DllImport("zuds.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZUDS_Release(uint type);
+
+
+ ///
+ /// 该函数用于设置函数库的参数。
+ ///
+ ///
+ /// 参数类型,= PARAM_TYPE_SESSION 0 用 于 设 置 会 话 层 参 数 , =
+ ///PARAM_TYPE_ISO15765 1 用于设置 ISO15765 的通信参数;
+ /// 参数值,type =PARAM_TYPE_SESSION 0 时为 ZUDS_SESSION_PARAM,
+ ///type= PARAM_TYPE_ISO15765 1 时为 ZUDS_ISO15765_PARAM。
+ [DllImport("zuds.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZUDS_SetParam(UInt32 ZUDS_HANDLE, byte type, IntPtr param);
+
+
+ ///
+ /// 该函数用于设置发送回调函数。函数库自身并不发送帧数据,把打包的帧数据通过回调
+ ///函数传出给用户发送,用户可通过 zlgcan 函数库进行帧数据发送。
+ ///
+ ///
+ /// ctx 上下文参数, 在回调函数中传出, 库内部不会处理该参数;
+ /// :回调函数原型;typedef uint32 (*OnUDSTransmit)(void* ctx, const ZUDS_FRAME* frame, uint32 count);
+ [DllImport("zuds.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZUDS_SetTransmitHandler(UInt32 ZUDS_HANDLE, IntPtr ctx, OnUDSTransmitDelegate onUDSTransmit);
+
+
+ [DllImport("zuds.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZUDS_OnReceive(UInt32 ZUDS_HANDLE, IntPtr ZUDS_FRAME);
+
+
+ [DllImport("zuds.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZUDS_Request(UInt32 ZUDS_HANDLE, IntPtr ZUDS_REQUEST, IntPtr ZUDS_RESPONSE);
+
+
+ [DllImport("zuds.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZUDS_Stop(UInt32 ZUDS_HANDLE);
+
+
+ [DllImport("zuds.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern void ZUDS_SetTesterPresent(UInt32 ZUDS_HANDLE, byte enable, IntPtr param);
+
+ #endregion
+
+ #region 结构体部分
+ ///
+ /// 会话层面参数;即一应一答传输时的通讯参数。
+ ///
+ public struct ZUDS_SESSION_PARAM
+ {
+ public UInt16 timeout;// ms, timeout to wait the response of the server
+ public UInt16 enhanced_timeout; // ms,timeout to wait after negative response: error code 0x78
+ public UInt32 reserved0; // 保留
+ public UInt32 reserved1; // 保留
+ }
+
+
+ ///
+ /// 传输数据部分的参数,例如传输时侯每帧报文的字节数。
+ ///
+ public struct ZUDS_ISO15765_PARAM
+ {
+ public byte version; // VERSION_0, VERSION_1格式版本,为 VERSION_0 时符合 ISO15765-2 的 2004 版本格式要求;为
+ //hVERSION_1 是符合 ISO15765-2 的 2016 版本新增的格式要求,如下图所示
+ public byte max_data_len; // max data length, can:8, canfd:64
+ public byte local_st_min; // ms, min time between two consecutive frames
+ public byte block_size;
+ public byte fill_byte; // fill to invalid byte
+ public byte frame_type; // 0:std 1:ext
+ public byte is_modify_ecu_st_min; //是否忽略 ECU 返回流控的 STmin,强制使用本程序设置的
+ //remote_st_min 参数代替
+
+ public byte remote_st_min; //发 送 多 帧 时 用, is_ignore_ecu_st_min = 1 时 有 效 ,
+ //0x00-0x7F(0ms ~127ms), 0xF1-0xF9(100us ~900us)
+ public UInt16 fc_timeout; //接收流控超时时间(ms), 如发送首帧后需要等待回应流控帧。
+ public byte fill_mode;//字节填充模式。FILL_MODE_NONE-不填充0;FILL_MODE_SHORT- 小于 8 字节填充至 8 字节,大于 8 字节时按 DLC 就近填充1;FILL_MODE_MAX- 始终填充至最大数据长度 (不建议)2。
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
+ public byte[] reserved;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZUDS_TESTER_PRESENT_PARAM
+ {
+ public UInt32 addr;//会话保持的请求地址;
+ public UInt16 cycle;//发送周期,单位毫秒;
+ public byte suppress_response; // 1:suppress是否抑制响应,建议设置为 1;
+ public UInt32 reserved;//:保留,忽略即可。
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZUDS_REQUEST
+ {
+ public uint src_addr; // 请求地址
+ public uint dst_addr; // 响应地址
+ public byte suppress_response; // 1:抑制响应
+ public byte sid; //service id of request
+ public ushort reserve0;
+ public IntPtr param; //array,params of the service
+ public uint param_len; //参数数组的长度
+ public uint reserved;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public class ZUDS_RESPONSE
+ {
+ public byte status;
+ public byte type; // RT_POSITIVE, RT_NEGATIVE
+ public _ZUDS_Union union;
+ public uint reserved;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZUDS_positive
+ {
+ public byte sid; // service id of response
+ public IntPtr param; // array, params of the service, don't free
+ public uint param_len;
+
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct _ZUDS_negative
+ {
+ public byte neg_code; // 0x7F
+ public byte sid; //service id of response
+ public byte error_code;//消极响应的错误码
+
+ }
+
+
+ [StructLayout(LayoutKind.Explicit)]
+ public struct _ZUDS_Union
+ {
+ [FieldOffset(0)]
+ public ZUDS_positive zudsPositive;
+
+ [FieldOffset(0)]
+ public _ZUDS_negative zudsNegative;
+ }
+
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public class ZUDS_FRAME
+ {
+ public uint id;
+ public byte extend;
+ public byte remote;
+ public byte data_len;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
+ public byte[] data;
+ public uint reserved;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public class ZUDS_CTX
+ {
+ public IntPtr can_type; // 0-CAN 1-CANFD 2-CANFD加速
+ public IntPtr chn_handle; // 通道句柄
+ }
+ #endregion
+ }
+}
diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanChannelOptions.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanChannelOptions.cs
new file mode 100644
index 0000000..508084e
--- /dev/null
+++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanChannelOptions.cs
@@ -0,0 +1,82 @@
+using System;
+
+namespace CapMachine.Wpf.CanDrive.ZlgCan
+{
+ ///
+ /// ZLG CANFD 通道初始化参数。
+ ///
+ public sealed class ZlgCanFdChannelOptions
+ {
+ ///
+ /// 仲裁域波特率(单位:bps)。例如 500000。
+ ///
+ public uint ArbitrationBaudRate { get; set; } = 500000;
+
+ ///
+ /// 数据域波特率(单位:bps)。例如 2000000。
+ ///
+ public uint DataBaudRate { get; set; } = 2000000;
+
+ ///
+ /// 终端电阻。
+ ///
+ public bool EnableInternalResistance { get; set; } = true;
+
+ ///
+ /// 仅监听模式。
+ ///
+ public bool ListenOnly { get; set; } = false;
+
+ ///
+ /// 是否启用总线利用率上报。
+ ///
+ public bool EnableBusUsage { get; set; } = false;
+
+ ///
+ /// 总线利用率上报周期(单位:ms)。
+ ///
+ public int BusUsagePeriodMs { get; set; } = 500;
+
+ ///
+ /// 是否启用设备层“合并接收”(ZCAN_ReceiveData)。
+ ///
+ public bool EnableMergeReceive { get; set; } = false;
+
+ ///
+ /// 合并接收缓冲区最大帧数量。
+ ///
+ public int MergeReceiveBufferFrames { get; set; } = 100;
+ }
+
+ ///
+ /// ZLG LIN 通道初始化参数。
+ ///
+ public sealed class ZlgLinChannelOptions
+ {
+ ///
+ /// LIN 模式:true=主节点,false=从节点。
+ ///
+ public bool IsMaster { get; set; } = true;
+
+ ///
+ /// 校验模式。
+ /// 1-经典校验,2-增强校验,3-自动。
+ ///
+ public byte ChecksumMode { get; set; } = 3;
+
+ ///
+ /// 最大数据长度(8~64)。
+ ///
+ public byte MaxLength { get; set; } = 8;
+
+ ///
+ /// 波特率(1000~20000)。
+ ///
+ public uint BaudRate { get; set; } = 19200;
+
+ ///
+ /// LIN 接收轮询等待时间(ms)。
+ ///
+ public int ReceiveWaitMs { get; set; } = 10;
+ }
+}
diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs
new file mode 100644
index 0000000..219e2ff
--- /dev/null
+++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs
@@ -0,0 +1,2593 @@
+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
+{
+ ///
+ /// 周立功 USBCANFD-200U 工程化封装(CAN/CANFD/LIN)。
+ ///
+ ///
+ /// 设计目标:
+ /// - 把 ZLG 原生 DLL(zlgcan.dll/zdbc.dll 等)调用封装为可被 WPF/Service 层直接消费的 .NET API;
+ /// - 统一承载 CAN/CANFD/LIN 三类通道的“打开/初始化/接收/发送/关闭”生命周期;
+ /// - 支持两类发送:
+ /// - 普通发送: / / ;
+ /// - 事件驱动发送:由 订阅 ,在信号值变化时增量下发;
+ /// - 支持硬件定时发送(调度表):
+ /// - 通过 下发 auto_send 列表并 apply;
+ /// - 运行中通过 MsgName->Index 映射实现“值变化时覆盖更新同 index 的帧数据”(不改变周期)。
+ ///
+ /// 线程与资源(务必理解):
+ /// - 接收线程由 创建,回调事件在后台线程触发;
+ /// - DBC 编码/解码与 CmdData 注入受 保护,避免接收线程与 UI 线程并发访问同一 DBC 对象;
+ /// - 设备句柄/通道句柄/接收线程生命周期受 保护,避免并发 Open/Close 导致句柄泄漏或 AccessViolation;
+ /// - auto_send(硬件定时发送)状态受 保护,避免 UI 启停与事件覆盖更新并发冲突。
+ ///
+ public sealed class ZlgCanFd200uDriver : BindableBase, IDisposable
+ {
+ ///
+ /// 驱动全局互斥锁。
+ ///
+ ///
+ /// 用于保护“设备句柄/通道句柄/接收线程”的生命周期操作:
+ /// - Open/Init/Start/Reset/Close
+ /// - StartReceiveLoop/StopReceiveLoop
+ /// 避免多线程同时开关导致句柄泄漏、AccessViolation 或状态错乱。
+ ///
+ private readonly object _sync = new object();
+
+ ///
+ /// 日志服务(用于输出驱动调用、异常、诊断信息)。
+ ///
+ private readonly ILogService _log;
+
+ ///
+ /// 硬件定时发送(auto_send)相关状态互斥锁。
+ ///
+ ///
+ /// auto_send 的配置/覆盖更新可能来自不同线程:
+ /// - UI 线程点击“循环发送/调度表”触发 StartAutoSendSchedule
+ /// - CmdDataChanged 事件线程触发覆盖更新
+ /// 因此必须对“是否启用、映射表、通道/帧类型”等共享状态加锁。
+ ///
+ private readonly object _autoSendSync = new object();
+
+ ///
+ /// 当前是否处于硬件定时发送(auto_send)任务运行态。
+ ///
+ private bool _autoSendActive;
+
+ ///
+ /// 当前硬件定时发送使用的通道索引(0/1)。
+ ///
+ private int _autoSendChannelIndex;
+
+ ///
+ /// 当前硬件定时发送使用的帧类型( 或 )。
+ ///
+ private byte _autoSendFrameType;
+
+ ///
+ /// MsgName -> auto_send 任务索引列表的映射。
+ ///
+ ///
+ /// 用于“信号值变化时覆盖更新对应定时发送任务”。
+ /// - 一个 MsgName 可能对应多个条目(例如配置了多个相同 MsgName 的调度表行)。
+ /// - 覆盖更新时通过 重新写入同 index 的 auto_send 结构体达到更新效果。
+ ///
+ private readonly Dictionary> _autoSendMsgIndex = new Dictionary>(StringComparer.Ordinal);
+
+ ///
+ /// 预加载的 zlgcan.dll 模块句柄。
+ ///
+ ///
+ /// 用于在 中强制从输出目录加载指定 DLL,减少被 PATH 中同名 DLL 干扰的风险。
+ ///
+ private static IntPtr _preloadedZlgcanHandle = IntPtr.Zero;
+
+ ///
+ /// 预加载 zlgcan.dll 失败时记录的错误信息。
+ ///
+ ///
+ /// - 若预加载失败,后续 OpenDevice 失败时会把该信息拼接到异常提示中,便于定位依赖缺失/位数不匹配等问题。
+ ///
+ private static string? _preloadedZlgcanError;
+
+ ///
+ /// 设备句柄(ZCAN_OpenDevice 返回)。
+ ///
+ ///
+ /// - 设备句柄是 CAN/CANFD/LIN 共用的“根句柄”;
+ /// - auto_send 配置也通过 device_handle + path 进行 SetValue。
+ ///
+ private IntPtr _deviceHandle = IntPtr.Zero;
+
+ ///
+ /// CAN/CANFD 通道句柄数组(最多 2 通道)。
+ ///
+ private readonly IntPtr[] _canChannelHandles = new IntPtr[2];
+
+ ///
+ /// LIN 通道句柄。
+ ///
+ private IntPtr _linChannelHandle = IntPtr.Zero;
+
+ ///
+ /// 接收线程取消令牌。
+ ///
+ private CancellationTokenSource? _recvCts;
+
+ ///
+ /// 接收循环任务(后台线程)。
+ ///
+ private Task? _recvTask;
+
+ ///
+ /// 对象是否已释放。
+ ///
+ private volatile bool _disposed;
+
+ ///
+ /// DBC 数据结构互斥锁。
+ ///
+ ///
+ /// 用于保护以下共享状态的并发读写:
+ /// - 的加载/释放
+ /// - 与 的替换
+ /// - 的注入与事件订阅/退订
+ ///
+ /// 接收线程在解码时会读取 DBC 与索引,因此这里必须保证读取到的是一致快照。
+ ///
+ private readonly object _dbcSync = new object();
+
+ ///
+ /// DBC 数据库实例。
+ ///
+ ///
+ /// 该对象封装 zdbc.dll 的加载/解析能力,用于:
+ /// - 接收帧解析为信号值(更新 UI 的 )
+ /// - 发送时把 CmdData 的信号值编码为原始帧数据(见 )
+ ///
+ private ZlgDbcDatabase? _dbc;
+
+ ///
+ /// DBC 解析后的“信号模型集合”(用于 UI 绑定)。
+ ///
+ private ObservableCollection? _dbcModels;
+
+ ///
+ /// SignalName -> 信号模型的索引,加速更新(接收解码更新时 O(1) 查找)。
+ ///
+ private Dictionary? _dbcModelIndex;
+
+ ///
+ /// 当前用于发送的指令集合(来源于配置程序+读写设置,外部通过 LoadCmdDataToDrive 注入)。
+ ///
+ ///
+ /// 该集合的每个元素通常对应一个 Signal(含 MsgName/SignalName/SignalCmdValue)。
+ /// - 事件驱动发送:订阅每个元素的 CanCmdDataChangedHandler,在信号变化时增量下发。
+ /// - 编码发送:按 MsgName 聚合所有 SignalName/SignalCmdValue,再交给 DBC 编码。
+ ///
+ private List? _cmdData;
+
+ ///
+ /// CmdData 编码并发送时使用的通道索引。
+ ///
+ private int _cmdSendChannelIndex;
+
+ ///
+ /// CmdData 编码并发送时使用的帧类型(CAN/CANFD)。
+ ///
+ private byte _cmdSendFrameType;
+
+ ///
+ /// 构造函数。
+ ///
+ /// 日志服务。
+ public ZlgCanFd200uDriver(ILogService logService)
+ {
+ _log = logService;
+ }
+
+ ///
+ /// 设备打开状态的 backing 字段。
+ ///
+ ///
+ /// 对外通过 暴露。
+ /// - 仅在驱动内部状态切换时写入(例如 /);
+ /// - 使用 通知 UI 刷新。
+ ///
+ private bool _openState;
+ ///
+ /// 设备打开状态。
+ ///
+ public bool OpenState
+ {
+ get { return _openState; }
+ private set { _openState = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 接收线程运行状态的 backing 字段。
+ ///
+ private bool _isReceiving;
+ ///
+ /// 是否正在接收。
+ ///
+ public bool IsReceiving
+ {
+ get { return _isReceiving; }
+ private set { _isReceiving = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 发送指示灯状态的 backing 字段。
+ ///
+ private bool _isSendOk;
+ ///
+ /// 最近是否发生过“发送成功”(用于 UI 指示灯)。
+ ///
+ public bool IsSendOk
+ {
+ get { return _isSendOk; }
+ private set { _isSendOk = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 接收指示灯状态的 backing 字段。
+ ///
+ private bool _isReviceOk;
+ ///
+ /// 最近是否发生过“接收成功”(用于 UI 指示灯)。
+ ///
+ public bool IsReviceOk
+ {
+ get { return _isReviceOk; }
+ private set { _isReviceOk = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 发送指示灯更新 token。
+ ///
+ ///
+ /// 用于解决“短时间内多次 MarkSendOk 导致的延迟回落互相覆盖”问题:
+ /// - 每次触发都会递增 token;
+ /// - 延迟回落时仅当 token 未变化才会把 置回 false。
+ ///
+ private int _sendOkToken;
+
+ ///
+ /// 接收指示灯更新 token。
+ ///
+ ///
+ /// 语义同 ,用于 的延迟回落保护。
+ ///
+ private int _reviceOkToken;
+
+ ///
+ /// 将 置为 true 并在一段时间后自动恢复为 false。
+ ///
+ /// 保持 true 的时间(毫秒)。
+ 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;
+ }
+ });
+ }
+
+ ///
+ /// 将 置为 true 并在一段时间后自动恢复为 false。
+ ///
+ /// 保持 true 的时间(毫秒)。
+ 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;
+ }
+ });
+ }
+
+ ///
+ /// CAN/CANFD 原始帧接收事件。
+ ///
+ ///
+ /// - 该事件在接收线程中触发(非 UI 线程);
+ /// - 订阅方若需要更新 UI,请自行切回 UI 线程;
+ /// - 即使启用了 DBC 解码,该事件仍会原样抛出原始帧,便于外部做抓包/记录。
+ ///
+ public event Action? CanFrameReceived;
+
+ ///
+ /// LIN 原始帧接收事件。
+ ///
+ ///
+ /// 同 :在接收线程触发,订阅方自行处理线程切换。
+ ///
+ public event Action? LinFrameReceived;
+
+ ///
+ /// 的 backing 字段。
+ ///
+ private bool _dbcDecodeEnabled;
+ ///
+ /// 是否启用 DBC 解码更新(接收帧触发)。
+ ///
+ public bool DbcDecodeEnabled
+ {
+ get { return _dbcDecodeEnabled; }
+ set { _dbcDecodeEnabled = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 的 backing 字段。
+ ///
+ private bool _isCycleSend;
+ ///
+ /// 是否启用“事件驱动发送”。
+ ///
+ ///
+ /// 与现有 ToomossCan/ToomossCanFD 行为对齐:只有 IsCycleSend=true 且 SchEnable=true 时,
+ /// 才会在 CmdDataChanged 事件中触发增量下发。
+ ///
+ public bool IsCycleSend
+ {
+ get { return _isCycleSend; }
+ set { _isCycleSend = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 的 backing 字段。
+ ///
+ private bool _schEnable;
+ ///
+ /// 发送使能(与 UI 的调度表使能语义对齐)。
+ ///
+ public bool SchEnable
+ {
+ get { return _schEnable; }
+ set { _schEnable = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 打开设备(不初始化 CAN/LIN 通道)。
+ ///
+ /// 设备索引(通常 0)。
+ ///
+ /// 说明:
+ /// - 该方法只负责获取 device_handle(根句柄);通道初始化由 / 完成;
+ /// - 若 OpenDevice 失败,为了便于现场定位,代码会输出:
+ /// - 输出目录 DLL 依赖诊断(是否缺失、位数是否匹配、版本信息);
+ /// - 当前进程架构与已加载模块路径;
+ /// - 该方法是幂等的:若设备已打开则直接返回。
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// 预加载指定路径的原生 DLL,尽量避免被 PATH 中的同名 DLL 干扰。
+ ///
+ /// DLL 完整路径。
+ 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}";
+ }
+ }
+
+ ///
+ /// 构造输出目录下某个 DLL 的诊断信息(架构/大小/版本)。
+ ///
+ /// 输出目录。
+ /// DLL 文件名。
+ /// 诊断信息字符串;不存在返回 "xxx:not_found";异常返回 null。
+ 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;
+ }
+ }
+
+ ///
+ /// 获取当前进程中已加载模块的完整路径。
+ ///
+ /// 模块文件名(如 zlgcan.dll)。
+ /// 完整路径;获取失败返回 null。
+ 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;
+ }
+ }
+
+ ///
+ /// 打开设备并初始化 CANFD 通道。
+ ///
+ /// 设备索引(通常 0)。
+ /// 通道0配置。
+ /// 通道1配置(可为 null 表示不初始化)。
+ ///
+ /// 生命周期语义:
+ /// - 本方法会执行:OpenDevice -> InitCAN(通道参数) -> StartCAN;
+ /// - 并根据通道0配置写入“设备级合并接收开关”(0/set_device_recv_merge);
+ /// - 初始化过程中如发生异常,会调用 尽量回收已打开的句柄,避免资源泄漏。
+ ///
+ /// 注意:
+ /// - 合并接收开关属于 device 级能力,因此以通道0配置为准;
+ /// - 该方法不启动接收线程,接收线程由 单独启动,便于 UI 控制。
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// 初始化并启动 LIN 通道。
+ ///
+ /// LIN 通道索引(通常 0)。
+ /// LIN 初始化参数。
+ ///
+ /// 说明:
+ /// - LIN 与 CAN/CANFD 共用同一个 device_handle;
+ /// - 本工程在 UI/Service 层通常会做 CAN/LIN 互斥校验,避免同时占用同一硬件。
+ ///
+ 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 失败。");
+ }
+ }
+ }
+
+ ///
+ /// 启动后台接收线程。
+ ///
+ /// 是否启用合并接收(ZCAN_ReceiveData)。建议与通道初始化时的 EnableMergeReceive 保持一致。
+ /// 接收缓冲最大帧数(合并接收固定数组大小,过小会丢帧)。
+ ///
+ /// 线程与回调说明:
+ /// - 本方法会创建一个后台 Task 持续读取驱动接收缓冲区;
+ /// - 收到帧后会触发 / (在后台线程触发,非 UI 线程);
+ /// - 若 为 true,会在接收线程中对帧做 DBC 解码并更新 集合。
+ ///
+ /// 注意:
+ /// - 若你在 UI 层直接绑定 集合并要求线程安全更新,请在订阅事件或更新集合时切回 UI 线程。
+ ///
+ /// 设备未打开或接收线程已启动。
+ 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);
+ }
+ }
+
+ ///
+ /// 加载 DBC 文件,并生成 UI 绑定用的信号集合。
+ ///
+ /// DBC 文件路径。
+ /// 是否启用异步解析。
+ /// 是否合并加载。
+ /// 协议类型。
+ /// CanDbcModel 集合。
+ ///
+ /// 成功加载后会:
+ /// - 重新创建并替换内部 DBC 数据库对象;
+ /// - 构建用于 UI 展示的 列表;
+ /// - 建立 SignalName 索引(用于接收线程解码时快速定位模型并更新值);
+ /// - 默认开启 ,使接收线程能驱动实时值更新。
+ ///
+ public ObservableCollection 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;
+ }
+ }
+
+ ///
+ /// 设置并订阅要发送的指令集合(事件驱动)。
+ ///
+ /// 指令集合。
+ /// 发送通道(0/1)。
+ /// 帧类型:ZDBC.FT_CAN 或 ZDBC.FT_CANFD。
+ ///
+ /// 本方法用于建立“信号值变化 => 增量下发”的事件驱动链路:
+ /// - 会先取消订阅旧集合的变更事件,再订阅新集合;
+ /// - 仅当 与 同时为 true 时,变更事件才会触发实际发送;
+ /// - 发送时仍会通过 DBC 对 MsgName 进行编码(见 )。
+ ///
+ public void LoadCmdDataToDrive(List 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;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 事件驱动回调:某个 MsgName 的信号值变更时,仅增量编码并下发该消息。
+ ///
+ /// 发送方。
+ /// 发生变化的消息名称。
+ ///
+ /// 发送策略:
+ /// - 调度表(硬件 auto_send)运行时:优先覆盖更新设备定时发送列表中的对应条目(见 );
+ /// - 否则:立即编码并发送 1 帧(见 )。
+ ///
+ /// 说明:该方法由 的变更事件触发,可能不在 UI 线程。
+ ///
+ 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}");
+ }
+ }
+
+ ///
+ /// 按 MsgName 尝试覆盖更新硬件定时发送列表中的对应任务。
+ ///
+ /// 消息名称。
+ /// true=已更新 auto_send;false=未更新(可能未启动 auto_send 或未命中任务索引)。
+ ///
+ /// 覆盖更新原理:
+ /// - auto_send 的每条任务由 index 唯一标识;
+ /// - 重新对同 index 调用 SetValue 写入新的帧数据,即可实现“后续周期发送新值”;
+ /// - 本方法仅覆盖帧数据,不改变周期(周期由启动调度表时写入)。
+ ///
+ private bool TryUpdateAutoSendByMsgName(string msgName)
+ {
+ if (string.IsNullOrWhiteSpace(msgName)) return false;
+
+ bool active;
+ int channelIndex;
+ byte frameType;
+ List? 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;
+ }
+
+ ///
+ /// 根据 MsgName 从当前 CmdData 聚合信号值,并通过 DBC 编码为原始 CAN/CANFD 帧。
+ ///
+ /// 消息名称(DBC Message Name)。
+ /// 帧类型(CAN/CANFD)。
+ ///
+ /// 成功返回 (CanId, Payload, PayloadLen);失败返回 null(如 DBC 未加载、CmdData 为空或该 MsgName 无任何信号值)。
+ ///
+ private (uint CanId, byte[] Payload, int PayloadLen)? TryBuildRawFrameFromCmdData(string msgName, byte frameType)
+ {
+ ZlgDbcDatabase? dbc;
+ List? 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(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(), dataLen);
+ }
+
+ ///
+ /// 清空并按调度表批量配置硬件定时发送列表,然后 apply_auto_send 使能。
+ ///
+ /// 调度表条目集合。
+ /// 通道索引(0/1)。
+ /// 帧类型(CAN/CANFD)。
+ ///
+ /// 对应周立功定时发送能力:
+ /// - 通过 {channel}/auto_send 或 {channel}/auto_send_canfd 写入待发送帧与周期;
+ /// - 再通过 {channel}/apply_auto_send 使能后,由设备侧硬件定时器保证发送周期精度。
+ ///
+ /// 本工程约定:
+ /// - 调度表中的 MsgName 必须能在 DBC 中找到 message 定义,否则无法编码并会被跳过;
+ /// - 运行期间信号值变化会触发覆盖更新(不影响周期)。
+ ///
+ 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>(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();
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// 停止硬件定时发送(clear_auto_send)。
+ ///
+ /// 通道索引(0/1)。
+ ///
+ /// - 会清空设备侧定时发送列表并停止发送;
+ /// - 同时清空内存态 MsgName->Index 映射,避免后续误覆盖写入。
+ ///
+ 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();
+
+ // 根据帧类型选择写入 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() : 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() : 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。");
+ }
+
+ ///
+ /// 根据 MsgName,从当前 CmdData 中取出信号值,编码并发送一帧。
+ ///
+ /// 消息名称。
+ /// 发送通道。
+ /// 帧类型:ZDBC.FT_CAN 或 ZDBC.FT_CANFD。
+ public void SendOneMsgByCmdData(string msgName, int channelIndex, byte frameType)
+ {
+ ThrowIfDisposed();
+
+ ZlgDbcDatabase? dbc;
+ List? 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(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。");
+ }
+
+ ///
+ /// 停止后台接收线程。
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// 发送 CAN 报文。
+ ///
+ /// 通道索引(0/1)。
+ /// 包含扩展帧标志位的 can_id(可用 ZlgCanIdHelper.MakeCanId 生成)。
+ /// 数据(0~8字节)。
+ /// 是否请求发送回显。
+ /// 发送方式,0=正常发送,1=单次发送,2=自发自收,3=单次自发自收。
+ 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);
+ }
+ }
+
+ ///
+ /// 发送 CANFD 报文。
+ ///
+ /// 通道索引(0/1)。
+ /// 包含扩展帧标志位的 can_id(可用 ZlgCanIdHelper.MakeCanId 生成)。
+ /// 数据(0~64字节)。
+ /// 是否请求发送回显。
+ /// 发送方式,0=正常发送,1=单次发送,2=自发自收,3=单次自发自收。
+ /// 驱动返回的实际发送帧数(通常为 0 或 1)。
+ ///
+ /// - CANFD 有效长度由 决定,内部会自动截断到 64 字节;
+ /// - 若 为 true,会在 flags 中设置回显位,使设备回传一帧 Tx 事件。
+ /// 在 merge 接收模式下,这种 Tx 回显会以 标识。
+ ///
+ /// 通道未初始化或句柄无效。
+ 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);
+ }
+ }
+
+ ///
+ /// 设置设备定时发送(CAN)。
+ ///
+ /// 通道索引。
+ /// 定时任务索引。
+ /// 是否使能。
+ /// 周期(ms)。
+ /// can_id。
+ /// 数据(0~8)。
+ ///
+ /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/auto_send。
+ ///
+ /// 关键点:
+ /// - 为设备侧列表索引;同一索引再次写入会“覆盖更新”;
+ /// - 为周期(毫秒),设备按该周期自动发送;
+ /// - 本实现默认设置发送回显位(__pad=0x20),便于在接收线程中区分 Tx/Rx 并刷新发送指示灯。
+ ///
+ /// 设备未打开或底层 SetValue 失败。
+ 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}");
+ }
+ }
+
+ ///
+ /// 设置设备定时发送(CANFD)。
+ ///
+ /// 通道索引。
+ /// 定时任务索引。
+ /// 是否使能。
+ /// 周期(ms)。
+ /// can_id。
+ /// 数据(0~64)。
+ ///
+ /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/auto_send_canfd。
+ ///
+ /// 与 一致:
+ /// - 同 index 写入即覆盖更新;
+ /// - interval 为周期(ms);
+ /// - 默认设置发送回显(flags=0x20)。
+ ///
+ /// 设备未打开或底层 SetValue 失败。
+ 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}");
+ }
+ }
+
+ ///
+ /// 启动通道定时发送任务(apply_auto_send)。
+ ///
+ /// 通道索引。
+ ///
+ /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/apply_auto_send。
+ ///
+ /// 调用时机:
+ /// - 在完成若干条 auto_send/auto_send_canfd 配置后调用;
+ /// - 设备侧开始按列表条目周期自动发送。
+ ///
+ /// 设备未打开或底层 SetValue 失败。
+ 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}");
+ }
+ }
+
+ ///
+ /// 清空通道定时发送任务(clear_auto_send)。
+ ///
+ /// 通道索引。
+ ///
+ /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/clear_auto_send。
+ ///
+ /// 说明:
+ /// - clear 会清空硬件列表并停止该通道的定时发送;
+ /// - 若你要重新配置调度表,建议先 clear,再从 index=0 重新写入并 apply。
+ ///
+ /// 设备未打开或底层 SetValue 失败。
+ 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}");
+ }
+ }
+
+ ///
+ /// 配置 LIN 发布表。
+ ///
+ /// 发布配置集合。
+ ///
+ /// - LIN 发布表用于“周期性/按条件发布”LIN 数据(由设备侧完成具体发送时机,取决于驱动能力与配置)。
+ /// - 本方法会将托管结构体数组拷贝到非托管内存,再调用底层 API。
+ /// - 若集合为空则直接返回。
+ ///
+ /// 线程说明:
+ /// - 该方法为同步调用;建议在设备已初始化 LIN 通道后调用。
+ ///
+ /// LIN 未初始化或底层配置失败。
+ public void SetLinPublish(IEnumerable publishCfg)
+ {
+ ThrowIfDisposed();
+ if (_linChannelHandle == IntPtr.Zero) throw new InvalidOperationException("LIN 未初始化。");
+
+ var list = publishCfg?.ToList() ?? new List();
+ 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);
+ }
+ }
+
+ ///
+ /// 配置 LIN 订阅表。
+ ///
+ /// 订阅配置集合。
+ ///
+ /// - LIN 订阅表用于配置希望接收/关注的 LIN 帧。
+ /// - 本方法会将托管结构体数组拷贝到非托管内存,再调用底层 API。
+ /// - 若集合为空则直接返回。
+ ///
+ /// LIN 未初始化或底层配置失败。
+ public void SetLinSubscribe(IEnumerable subscribeCfg)
+ {
+ ThrowIfDisposed();
+ if (_linChannelHandle == IntPtr.Zero) throw new InvalidOperationException("LIN 未初始化。");
+
+ var list = subscribeCfg?.ToList() ?? new List();
+ 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);
+ }
+ }
+
+ ///
+ /// 发送一帧 LIN 报文。
+ ///
+ /// 通道号(通常为 0)。
+ /// 受保护 ID(PID,包含奇偶校验位)。
+ /// 数据区(0-8字节)。
+ /// 方向(通常 0;具体含义以 ZLG 文档为准)。
+ ///
+ /// 说明:
+ /// - 当前工程使用 merge 接收()读取 LIN 帧,因此这里的发送只负责向设备下发原始帧;
+ /// - ZLG 的 LIN 发送 API 以 数组形式接收数据;本方法将 PID+数据按 的内存布局写入。
+ ///
+ public void TransmitLin(int channel, byte pid, ReadOnlySpan 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);
+ }
+ }
+
+ ///
+ /// 关闭设备。
+ ///
+ ///
+ /// 资源释放顺序(概览):
+ /// - 停止接收线程();
+ /// - Reset LIN/CAN 通道并关闭设备句柄(见 );
+ /// - 取消 CmdData 事件订阅并释放 DBC 资源。
+ ///
+ /// 线程安全:
+ /// - 该方法通过 与 保护,避免并发 close/load/receive 导致状态错乱。
+ ///
+ 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;
+ }
+ }
+
+ ///
+ ///
+ /// 遵循 .NET Dispose 语义:
+ /// - 多次调用是安全的(通过 做幂等保护);
+ /// - Dispose 内部会调用 释放驱动句柄、停止接收线程并释放 DBC 资源。
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ // 说明:Dispose 不抛异常,避免在 using/终结路径打断上层释放流程。
+ // Close 内部已经尽量记录日志并吞掉部分关闭异常,保证释放流程走完。
+ Close();
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ ///
+ /// 关闭设备与通道句柄(不加锁版本)。
+ ///
+ ///
+ /// 该方法假定调用方已经持有 :
+ /// - 用于 Close 流程中按顺序 Reset LIN、Reset CAN、CloseDevice;
+ /// - 任何异常都会被吞掉并记录 Warn,确保关闭流程尽量走完,避免句柄泄漏。
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// 初始化指定 CANFD 通道(仅设置参数并 InitCAN,不负责 StartCAN)。
+ ///
+ /// 通道索引。
+ /// 通道初始化参数。
+ ///
+ /// 该方法主要完成:
+ /// - 设置仲裁域/数据域波特率;
+ /// - 设置终端电阻与总线利用率统计开关;
+ /// - 调用 ZCAN_InitCAN 获取通道句柄并保存到 。
+ ///
+ /// 注意:
+ /// - 这里仅负责 Init,不负责 Start;Start 由 完成。
+ ///
+ /// 任一 SetValue/InitCAN 失败。
+
+ 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;
+ }
+
+ ///
+ /// 启动指定 CAN 通道。
+ ///
+ /// 通道索引。
+ /// 通道句柄。
+ ///
+ /// 调用前置条件:
+ /// - 已成功执行并产生通道句柄。
+ ///
+ /// 通道句柄为空或启动失败。
+
+ 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;
+ }
+
+ ///
+ /// 获取 CAN 通道句柄(不存在则抛异常)。
+ ///
+ /// 通道索引。
+ /// 通道句柄。
+ /// 通道索引超范围。
+ /// 通道未初始化。
+
+ 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;
+ }
+
+ ///
+ /// 普通接收模式:按通道轮询 CAN/CANFD 缓冲区读取。
+ ///
+ /// 取消令牌。
+ /// 每次接收的最大帧数。
+ ///
+ /// - 该模式分别调用:
+ /// - ZCAN_GetReceiveNum/ZCAN_Receive 接收 CAN
+ /// - ZCAN_GetReceiveNum/ZCAN_ReceiveFD 接收 CANFD
+ /// - 每次接收会在非托管堆上分配固定大小缓冲,循环中复用,退出时释放。
+ ///
+
+ 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(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(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);
+ }
+ }
+
+ ///
+ /// 合并接收模式:通过设备级 API(ZCAN_ReceiveData)统一接收 CAN/CANFD/LIN。
+ ///
+ /// 取消令牌。
+ /// 接收缓存容量。
+ ///
+ /// - merge 接收模式通常用于高吞吐场景:由驱动统一将各类数据封装为 DataObj;
+ /// - 本方法根据 dataType 分流:
+ /// - 1:CAN/CANFD
+ /// - 4:LIN
+ /// - 触发事件仍通过 / 。
+ ///
+
+ 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(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(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(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);
+ }
+ }
+
+ ///
+ /// 将接收到的 CAN/CANFD 帧转换为托管对象并触发事件。
+ ///
+ /// 通道号。
+ /// 时间戳(us)。
+ /// 是否 CANFD。
+ /// can_id(含标志位)。
+ /// 原始数据缓冲区。
+ /// 数据长度。
+ /// 是否为发送回显(Tx)。
+ ///
+ /// 事件派发与 DBC 解码:
+ /// - 始终先触发 抛出原始帧;
+ /// - 再依据 决定是否进行 DBC 解码并刷新 。
+ ///
+ /// 发送/接收指示:
+ /// - 若 为 true,调用 ;否则调用 。
+ ///
+
+ 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}");
+ }
+ }
+
+ ///
+ /// 将接收到的 LIN 帧转换为托管对象并触发事件。
+ ///
+ /// 通道号。
+ /// 时间戳(us)。
+ /// PID。
+ /// 原始数据缓冲区。
+ /// 数据长度。
+ /// 方向(由设备回传)。
+ ///
+ /// - LIN 数据长度最大 8 字节,本方法会按 截断并复制;
+ /// - 事件在接收线程触发,订阅方若更新 UI,请自行切换线程。
+ ///
+
+ 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}");
+ }
+ }
+
+ ///
+ /// 校验指定原生 DLL 是否存在于程序输出目录(AppContext.BaseDirectory)。
+ ///
+ /// DLL 文件名。
+ /// 找不到 DLL。
+
+ 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}");
+ }
+ }
+ }
+
+ ///
+ /// 尝试读取 PE 头判断 DLL 架构。
+ ///
+ /// DLL 完整路径。
+ /// 返回 "x86"/"x64"/"arm64"/"unknown(...)";读取失败返回 null。
+ 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;
+ }
+ }
+
+ ///
+ /// 构建 DBC 解码更新用的索引表。
+ ///
+ /// 所有信号模型。
+ /// 键为 (MsgName, SignalName) 的组合键,值为对应的 。
+ ///
+ /// - 接收线程解码得到 (msgName, signalName) 后,可 O(1) 定位到 UI 模型进行更新;
+ /// - 组合键使用 ,避免字符串拼接歧义。
+ ///
+ private static Dictionary BuildDbcModelIndex(IEnumerable models)
+ {
+ var dict = new Dictionary(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;
+ }
+
+ ///
+ /// 构建 (MsgName, SignalName) 的唯一组合键。
+ ///
+ /// 消息名。
+ /// 信号名。
+ /// 组合键字符串。
+ ///
+ /// 使用 '\0' 作为分隔符,降低与业务字符串冲突的风险。
+ ///
+ private static string BuildMsgSigKey(string msgName, string signalName)
+ {
+ return msgName + "\u0000" + signalName;
+ }
+
+ ///
+ /// 尝试对原始帧做 DBC 解码,并将解码结果写回到对应的 实例。
+ ///
+ /// CAN ID(含标志位)。
+ /// 原始数据。
+ /// 帧类型(CAN/CANFD)。
+ ///
+ /// - 本方法被 在接收线程中调用;
+ /// - 若 DBC 未加载或索引表为空,会直接返回;
+ /// - 解码后按 signalName 更新 (字符串形式)。
+ ///
+ private void TryDecodeAndUpdateModels(uint canId, byte[] payload, byte frameType)
+ {
+ ZlgDbcDatabase? dbc;
+ Dictionary? 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}");
+ }
+ }
+
+ ///
+ /// 若对象已释放则抛异常。
+ ///
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(ZlgCanFd200uDriver));
+ }
+ }
+ }
+
+ ///
+ /// CAN/CANFD 接收帧。
+ ///
+ public readonly struct ZlgCanRxFrame
+ {
+ ///
+ /// 构造 CAN/CANFD 接收帧。
+ ///
+ /// 通道号。
+ /// 是否 CANFD。
+ /// can_id(含标志位)。
+ /// 数据(已截断为实际长度)。
+ /// 时间戳(us)。
+ /// 是否为发送回显。
+ 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;
+ }
+
+ ///
+ /// 通道号(0/1)。
+ ///
+ public int Channel { get; }
+
+ ///
+ /// 是否为 CANFD 帧。
+ ///
+ public bool IsCanFd { get; }
+
+ ///
+ /// can_id(包含扩展帧/错误帧等标志位,具体位定义见 ZLG 文档)。
+ ///
+ public uint CanId { get; }
+
+ ///
+ /// 数据区(已按实际长度截断拷贝)。
+ ///
+ public byte[] Data { get; }
+
+ ///
+ /// 时间戳(微秒)。
+ ///
+ public ulong TimestampUs { get; }
+
+ ///
+ /// 是否为发送回显(Tx)。
+ ///
+ public bool IsTx { get; }
+ }
+
+ ///
+ /// LIN 接收帧。
+ ///
+ public readonly struct ZlgLinRxFrame
+ {
+ ///
+ /// 构造 LIN 接收帧。
+ ///
+ /// 通道号。
+ /// PID。
+ /// 数据(已截断为实际长度)。
+ /// 时间戳(us)。
+ /// 方向(由设备回传)。
+ public ZlgLinRxFrame(int channel, byte pid, byte[] data, ulong timestampUs, byte dir)
+ {
+ Channel = channel;
+ Pid = pid;
+ Data = data;
+ TimestampUs = timestampUs;
+ Dir = dir;
+ }
+
+ ///
+ /// 通道号。
+ ///
+ public int Channel { get; }
+
+ ///
+ /// PID。
+ ///
+ public byte Pid { get; }
+
+ ///
+ /// 数据区(已按实际长度截断拷贝)。
+ ///
+ public byte[] Data { get; }
+
+ ///
+ /// 时间戳(微秒)。
+ ///
+ public ulong TimestampUs { get; }
+
+ ///
+ /// 方向:由设备回传,0/1 具体含义以 ZLG 文档为准。
+ ///
+ public byte Dir { get; }
+ }
+}
diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanIdHelper.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanIdHelper.cs
new file mode 100644
index 0000000..8d0a9cd
--- /dev/null
+++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanIdHelper.cs
@@ -0,0 +1,62 @@
+using System;
+
+namespace CapMachine.Wpf.CanDrive.ZlgCan
+{
+ ///
+ /// ZLG CAN ID 帮助类。
+ ///
+ public static class ZlgCanIdHelper
+ {
+ ///
+ /// 生成 ZLG/SocketCAN 风格的 can_id(包含扩展帧/远程帧/错误帧标志位)。
+ ///
+ /// CAN 标识符(标准帧 11bit:0~0x7FF;扩展帧 29bit:0~0x1FFFFFFF)。
+ /// 是否为扩展帧。
+ /// 是否为远程帧(RTR)。
+ /// 是否为错误帧。
+ /// 包含标志位的 can_id。
+ public static uint MakeCanId(uint id, bool isExtended, bool isRemote, bool isError)
+ {
+ // 兼容官方样例写法:bit31 扩展帧,bit30 RTR,bit29 ERR
+ // 注意:该函数不做强约束校验(例如扩展帧 29bit),上层可按需要增加校验。
+ uint canId = id & 0x1FFFFFFF;
+
+ if (isExtended)
+ {
+ canId |= 1u << 31;
+ }
+
+ if (isRemote)
+ {
+ canId |= 1u << 30;
+ }
+
+ if (isError)
+ {
+ canId |= 1u << 29;
+ }
+
+ return canId;
+ }
+
+ ///
+ /// 从 can_id 中提取 29bit 标识符。
+ ///
+ /// 包含标志位的 can_id。
+ /// 29bit 标识符。
+ public static uint GetArbitrationId(uint canId)
+ {
+ return canId & 0x1FFFFFFF;
+ }
+
+ ///
+ /// 判断 can_id 是否为扩展帧。
+ ///
+ /// 包含标志位的 can_id。
+ /// 是否扩展帧。
+ public static bool IsExtended(uint canId)
+ {
+ return (canId & (1u << 31)) != 0;
+ }
+ }
+}
diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgDbcDatabase.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgDbcDatabase.cs
new file mode 100644
index 0000000..c573902
--- /dev/null
+++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgDbcDatabase.cs
@@ -0,0 +1,728 @@
+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
+{
+ ///
+ /// 基于周立功 ZDBC(zdbc.dll)的 DBC 数据库封装。
+ ///
+ ///
+ /// 设计要点:
+ /// - zdbc.dll 为原生依赖,必须放置在程序输出目录(AppContext.BaseDirectory)。
+ /// - 本类封装 DBC 文件加载、消息/信号枚举、以及基于 DBCMessage 的编码/解码能力。
+ /// - 运行稳定性:实现 IDisposable,确保 ZDBC_Release 与相关非托管内存释放。
+ ///
+ 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 _messageByName = new Dictionary(StringComparer.Ordinal);
+ private readonly Dictionary _messageIdByName = new Dictionary(StringComparer.Ordinal);
+
+ ///
+ /// 构造函数。
+ ///
+ /// 日志服务。
+ public ZlgDbcDatabase(ILogService logService)
+ {
+ _log = logService;
+ }
+
+ ///
+ /// 是否已加载。
+ ///
+ public bool IsLoaded
+ {
+ get
+ {
+ return _dbcHandle != 0 && _dbcHandle != ZDBC.INVALID_DBC_HANDLE;
+ }
+ }
+
+ ///
+ /// 加载 DBC 文件。
+ ///
+ /// DBC 文件路径。
+ /// 是否启用异步解析(库内部线程)。
+ /// 是否合并到当前数据库(支持加载多个文件)。
+ /// 协议类型:J1939=0,其他=1。
+ 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();
+ 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();
+ }
+ }
+
+ ///
+ /// 枚举 DBC 内全部 Message/Signal 元信息,并转换为 UI 使用的 CanDbcModel 集合。
+ ///
+ /// 信号集合。
+ public ObservableCollection BuildCanDbcModels()
+ {
+ ThrowIfDisposed();
+
+ if (!IsLoaded)
+ {
+ return new ObservableCollection();
+ }
+
+ lock (_sync)
+ {
+ var list = new List();
+
+ 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(list);
+ }
+ }
+
+ ///
+ /// 根据 MsgName 获取 Message ID。
+ ///
+ /// 消息名称。
+ /// Message ID。
+ public uint GetMessageIdByName(string msgName)
+ {
+ ThrowIfDisposed();
+ if (string.IsNullOrWhiteSpace(msgName)) return 0;
+
+ lock (_sync)
+ {
+ return _messageIdByName.TryGetValue(msgName, out var id) ? id : 0;
+ }
+ }
+
+ ///
+ /// 根据 MsgName + 信号值集合编码为 CAN/CANFD 原始帧。
+ ///
+ /// 消息名称。
+ /// 信号值(SignalName -> 实际值)。
+ /// 帧类型:FT_CAN=0,FT_CANFD=1。
+ /// 编码后的 can_id 与数据。
+ public (uint CanId, byte[] Data, int DataLen) EncodeToRawFrame(string msgName, IDictionary 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。");
+ }
+ }
+
+ ///
+ /// 将原始 CAN/CANFD 帧解码为 DBCMessage,并返回消息名称与信号实际值。
+ ///
+ /// can_id。
+ /// payload。
+ /// 帧类型:FT_CAN=0,FT_CANFD=1。
+ /// 解码结果。
+ public (string MsgName, Dictionary 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(), requestTxEcho: false);
+ Marshal.StructureToPtr(frame, _canFramePtr, false);
+
+ var ok = ZDBC.ZDBC_Decode(_dbcHandle, _msgPtr, _canFramePtr, 1, frameType);
+ if (!ok)
+ {
+ return (string.Empty, new Dictionary());
+ }
+
+ var msg = Marshal.PtrToStructure(_msgPtr);
+ return ExtractActualSignals(msg);
+ }
+
+ if (frameType == ZDBC.FT_CANFD)
+ {
+ var frame = ZlgNativeCanFdFrame.Create(canId, data ?? Array.Empty(), requestTxEcho: false);
+ Marshal.StructureToPtr(frame, _canFdFramePtr, false);
+
+ var ok = ZDBC.ZDBC_Decode(_dbcHandle, _msgPtr, _canFdFramePtr, 1, frameType);
+ if (!ok)
+ {
+ return (string.Empty, new Dictionary());
+ }
+
+ var msg = Marshal.PtrToStructure(_msgPtr);
+ return ExtractActualSignals(msg);
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(frameType), "frameType 仅支持 FT_CAN=0 或 FT_CANFD=1。");
+ }
+ }
+
+ ///
+ 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(_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 Signals) ExtractActualSignals(ZDBC.DBCMessage msg)
+ {
+ var msgName = ByteArrayToAsciiString(msg.strName);
+ var dict = new Dictionary(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;
+ }
+
+ ///
+ /// 当 ZDBC_LoadFile 失败时的兜底加载策略:读取文件内容并调用 ZDBC_LoadContent。
+ ///
+ /// DBC 完整路径。
+ /// 是否合并。
+ /// 加载成功返回 true。
+ private bool TryLoadContentFallback_NoLock(string fullPath, bool merge)
+ {
+ // zdbc.dll 入口为 ANSI 字符串,因此这里优先使用 ASCII(将非 ASCII 字符替换为 ?),
+ // 避免中文注释/特殊字符导致原生库解析失败;同时需要兼容 UTF-16 BOM,否则用 ASCII 读会出现大量 \0 造成内容截断。
+
+ var encodings = new List();
+
+ 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;
+ }
+
+ ///
+ /// 获取 Windows 8.3 短路径。
+ ///
+ /// 长路径。
+ /// 短路径缓冲。
+ /// 缓冲区大小。
+ /// 返回写入的字符数量;0 表示失败。
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ private static extern uint GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, uint cchBuffer);
+
+ ///
+ /// 尝试将长路径转换为短路径(8.3),用于兼容部分原生库对中文/特殊字符路径处理不完整的问题。
+ ///
+ /// 长路径。
+ /// 短路径;失败返回 null。
+ 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));
+ }
+ }
+ }
+}
diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgNativeFrames.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgNativeFrames.cs
new file mode 100644
index 0000000..0aedf4f
--- /dev/null
+++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgNativeFrames.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace CapMachine.Wpf.CanDrive.ZlgCan
+{
+ ///
+ /// 供 DBC 编解码/Marshal 使用的原生 CAN 帧结构(与 zlgcan can_frame 二进制布局兼容)。
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZlgNativeCanFrame
+ {
+ public uint can_id;
+ public byte can_dlc;
+ public byte __pad;
+ public byte __res0;
+ public byte __res1;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] data;
+
+ ///
+ /// 创建 CAN 帧。
+ ///
+ /// can_id(包含扩展帧等标志位)。
+ /// 数据(0~8)。
+ /// 是否请求发送回显。
+ /// 原生 CAN 帧。
+ public static ZlgNativeCanFrame Create(uint canId, byte[] payload, bool requestTxEcho)
+ {
+ var len = Math.Min(8, payload?.Length ?? 0);
+ var frame = new ZlgNativeCanFrame
+ {
+ can_id = canId,
+ can_dlc = (byte)len,
+ __pad = 0,
+ __res0 = 0,
+ __res1 = 0,
+ data = new byte[8]
+ };
+
+ if (len > 0 && payload != null)
+ {
+ Array.Copy(payload, frame.data, len);
+ }
+
+ if (requestTxEcho)
+ {
+ frame.__pad |= 0x20;
+ }
+
+ return frame;
+ }
+ }
+
+ ///
+ /// 供 DBC 编解码/Marshal 使用的原生 CANFD 帧结构(与 zlgcan canfd_frame 二进制布局兼容)。
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZlgNativeCanFdFrame
+ {
+ public uint can_id;
+ public byte len;
+ public byte flags;
+ public byte __res0;
+ public byte __res1;
+
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
+ public byte[] data;
+
+ ///
+ /// 创建 CANFD 帧。
+ ///
+ /// can_id(包含扩展帧等标志位)。
+ /// 数据(0~64)。
+ /// 是否请求发送回显。
+ /// 原生 CANFD 帧。
+ public static ZlgNativeCanFdFrame Create(uint canId, byte[] payload, bool requestTxEcho)
+ {
+ var len = Math.Min(64, payload?.Length ?? 0);
+ var frame = new ZlgNativeCanFdFrame
+ {
+ can_id = canId,
+ len = (byte)len,
+ flags = 0,
+ __res0 = 0,
+ __res1 = 0,
+ data = new byte[64]
+ };
+
+ if (len > 0 && payload != null)
+ {
+ Array.Copy(payload, frame.data, len);
+ }
+
+ if (requestTxEcho)
+ {
+ frame.flags |= 0x20;
+ }
+
+ return frame;
+ }
+ }
+}
diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/接口函数使用手册.pdf b/CapMachine.Wpf/CanDrive/ZlgCan/接口函数使用手册.pdf
new file mode 100644
index 0000000..4e68bec
Binary files /dev/null and b/CapMachine.Wpf/CanDrive/ZlgCan/接口函数使用手册.pdf differ
diff --git a/CapMachine.Wpf/Dtos/CANFdConfigExdDto.cs b/CapMachine.Wpf/Dtos/CANFdConfigExdDto.cs
index 8d4d873..078912b 100644
--- a/CapMachine.Wpf/Dtos/CANFdConfigExdDto.cs
+++ b/CapMachine.Wpf/Dtos/CANFdConfigExdDto.cs
@@ -73,5 +73,16 @@ namespace CapMachine.Wpf.Dtos
get { return _DbcPath; }
set { _DbcPath = value; RaisePropertyChanged(); }
}
+
+
+ private bool _SchEnable;
+ ///
+ /// 调度表是否启用
+ ///
+ public bool SchEnable
+ {
+ get { return _SchEnable; }
+ set { _SchEnable = value; RaisePropertyChanged(); }
+ }
}
}
diff --git a/CapMachine.Wpf/Dtos/CANFdScheduleConfigDto.cs b/CapMachine.Wpf/Dtos/CANFdScheduleConfigDto.cs
new file mode 100644
index 0000000..2ff33ad
--- /dev/null
+++ b/CapMachine.Wpf/Dtos/CANFdScheduleConfigDto.cs
@@ -0,0 +1,83 @@
+using CapMachine.Model.CANLIN;
+using Prism.Mvvm;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CapMachine.Wpf.Dtos
+{
+ ///
+ /// CANFdScheduleConfigDto
+ ///
+ public class CANFdScheduleConfigDto : BindableBase
+ {
+ ///
+ /// 主键
+ ///
+ public long Id { get; set; }
+
+ private string? _MsgName;
+ ///
+ /// 消息名称/帧名称
+ ///
+ public string? MsgName
+ {
+ get { return _MsgName; }
+ set { _MsgName = value; RaisePropertyChanged(); }
+ }
+
+ private int _Cycle;
+ ///
+ /// 周期
+ ///
+ public int Cycle
+ {
+ get { return _Cycle; }
+ set { _Cycle = value; RaisePropertyChanged(); }
+ }
+
+ private int _OrderSend;
+ ///
+ /// 发送方式
+ ///
+ public int OrderSend
+ {
+ get { return _OrderSend; }
+ set { _OrderSend = value; RaisePropertyChanged(); }
+ }
+
+ private int _SchTabIndex;
+ ///
+ /// 调度表的Index序列
+ ///
+ public int SchTabIndex
+ {
+ get { return _SchTabIndex; }
+ set { _SchTabIndex = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 在更新调度表数据时,我们有一个整体的帧数据指令集合,但是这些帧数据集合,分属于不同的调度表,
+ /// 这个在开始时生成整体的帧数据指令集合才会把这个MsgIndex分配上,这个不需要保存到数据库中,对接使用
+ ///
+ public int MsgIndex { get; set; }
+
+ ///
+ /// 程序的ID
+ ///
+ public long CanLinConfigProId { get; set; }
+
+
+ private CanLinConfigPro _CanLinConfigPro;
+ ///
+ /// 所属的程序
+ ///
+ public CanLinConfigPro CanLinConfigPro
+ {
+ get { return _CanLinConfigPro; }
+ set { _CanLinConfigPro = value; RaisePropertyChanged(); }
+ }
+ }
+}
diff --git a/CapMachine.Wpf/Services/CanFdDriveService.cs b/CapMachine.Wpf/Services/CanFdDriveService.cs
index 7d3844f..c28b0ec 100644
--- a/CapMachine.Wpf/Services/CanFdDriveService.cs
+++ b/CapMachine.Wpf/Services/CanFdDriveService.cs
@@ -1,5 +1,6 @@
using CapMachine.Model.CANLIN;
using CapMachine.Wpf.CanDrive;
+using CapMachine.Wpf.Dtos;
using CapMachine.Wpf.Models;
using CapMachine.Wpf.Models.ProModelPars;
using ImTools;
@@ -25,9 +26,9 @@ namespace CapMachine.Wpf.Services
///
/// 实例化函数
///
- public CanFdDriveService(HighSpeedDataService highSpeedDataService, IContainerProvider containerProvider, LogicRuleService logicRuleService)
+ public CanFdDriveService(HighSpeedDataService highSpeedDataService, IContainerProvider containerProvider, LogicRuleService logicRuleService, ILogService logService)
{
- ToomossCanFDDrive = new ToomossCanFD(containerProvider);
+ ToomossCanFDDrive = new ToomossCanFD(containerProvider, logService);
//高速数据服务
HighSpeedDataService = highSpeedDataService;
LogicRuleService = logicRuleService;
@@ -126,6 +127,11 @@ namespace CapMachine.Wpf.Services
///
public List CmdData { get; set; } = new List();
+ ///
+ /// CNA 调度表的配置信息
+ ///
+ public List ListCANFdScheduleConfig { get; set; }
+
///
/// 增加发送的指令数据
///
@@ -212,7 +218,7 @@ namespace CapMachine.Wpf.Services
{
if (PwLimitCanCmdData != null)
{
- if (PwLimitCanCmdData.LogicRuleDto==null)
+ if (PwLimitCanCmdData.LogicRuleDto == null)
{
PwLimitCanCmdData.SignalCmdValue = PwLimit;
return;
@@ -264,7 +270,7 @@ namespace CapMachine.Wpf.Services
PTCFlowCanCmdData.SignalCmdValue = Flow;
return;
}
- PTCFlowCanCmdData.SignalCmdValue = LogicRuleService.ApplyExpressionFast(Flow, PTCFlowCanCmdData.LogicRuleDto!);
+ PTCFlowCanCmdData.SignalCmdValue = LogicRuleService.ApplyExpressionFast(Flow, PTCFlowCanCmdData.LogicRuleDto!);
}
}
@@ -322,10 +328,39 @@ namespace CapMachine.Wpf.Services
{
if (CmdData.Count > 0)
{
- ToomossCanFDDrive.IsCycleSend = true;
+ //把指令数据赋值给CAN 驱动
ToomossCanFDDrive.CmdData = CmdData;
- //ToomossCanFDDrive.StartCycleSendMsg();
- ToomossCanFDDrive.StartPrecisionCycleSendMsg();
+
+ if (ToomossCanFDDrive.SchEnable)
+ {
+
+ //使用调度表的话,需要在调度表中配置信息后才可以进行操作
+ var GroupMsg = ToomossCanFDDrive.CmdData.GroupBy(a => a.MsgName).ToList();
+ foreach (var itemMsg in GroupMsg)
+ {
+ if (!ListCANFdScheduleConfig.ToList().Any(a => a.MsgName == itemMsg.Key))
+ {
+ System.Windows.MessageBox.Show($"你使能了调度表,但是调度表中没有设置【{itemMsg.Key}】信息,请设置后再操作", "提示", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Hand);
+ return;
+ }
+ }
+
+ if (ListCANFdScheduleConfig == null && ListCANFdScheduleConfig!.Count() == 0)
+ {
+ System.Windows.MessageBox.Show("调度表配置为空数据,请检查!将无法发送数据", "提示", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Hand);
+ return;
+ }
+ ToomossCanFDDrive.ListCANScheduleConfig = ListCANFdScheduleConfig!;
+ ToomossCanFDDrive.StartSchedule();
+ //ToomossCanFDDrive.StartCycleUpdateCmd();
+ }
+ else
+ {
+ ToomossCanFDDrive.StartPrecisionCycleSendMsg();
+ //ToomossCanFDDrive.StartCycleSendMsg();
+ }
+
+ ToomossCanFDDrive.IsCycleSend = true;
}
else
{
@@ -334,7 +369,15 @@ namespace CapMachine.Wpf.Services
}
else
{
- ToomossCanFDDrive.IsCycleSend = false;
+ if (ToomossCanFDDrive.SchEnable)
+ {
+ ToomossCanFDDrive.IsCycleSend = false;
+ ToomossCanFDDrive.StopSchedule();
+ }
+ else
+ {
+ ToomossCanFDDrive.IsCycleSend = false;
+ }
}
}
diff --git a/CapMachine.Wpf/Services/ComActionService.cs b/CapMachine.Wpf/Services/ComActionService.cs
index f72b49b..374c146 100644
--- a/CapMachine.Wpf/Services/ComActionService.cs
+++ b/CapMachine.Wpf/Services/ComActionService.cs
@@ -1,4 +1,4 @@
-using CapMachine.Wpf.PrismEvent;
+using CapMachine.Wpf.PrismEvent;
using Prism.Events;
using Prism.Services.Dialogs;
using System;
@@ -105,6 +105,7 @@ namespace CapMachine.Wpf.Services
}
+
///
/// 过热度和过冷度配置弹窗
///
@@ -155,6 +156,7 @@ namespace CapMachine.Wpf.Services
+
#endregion
diff --git a/CapMachine.Wpf/Services/NavigationMenuService.cs b/CapMachine.Wpf/Services/NavigationMenuService.cs
index b5a9b8d..3e9aca6 100644
--- a/CapMachine.Wpf/Services/NavigationMenuService.cs
+++ b/CapMachine.Wpf/Services/NavigationMenuService.cs
@@ -44,14 +44,10 @@ namespace CapMachine.Wpf.Services
{
new NavigationItem("Rule","规则转换","DialogLogicRuleView"),
}),
- new NavigationItem("","配置导出","",new ObservableCollection()
+ new NavigationItem("", "配置导出","",new ObservableCollection()
{
new NavigationItem("","CAN/CANFD/LIN配置导入导出","DialogCanLinConfigImExport"),
}),
- //new NavigationItem("", "PID设置","",new ObservableCollection()
- //{
- // new NavigationItem("Circle","转速PID",""),
- //}),
//new NavigationItem("", "通信配置","",new ObservableCollection()
//{
// new NavigationItem("Circle","CAN配置","CANConfigView"),
@@ -81,8 +77,10 @@ namespace CapMachine.Wpf.Services
//}));
MenuItems.Add(new NavigationItem("", "工艺过程", "MonitorView"));
MenuItems.Add(new NavigationItem("", "CAN配置", "CANConfigView"));
- MenuItems.Add(new NavigationItem("", "CANFD配置", "CANFDConfigView"));
+ //MenuItems.Add(new NavigationItem("", "CANFD配置", "CANFDConfigView"));
MenuItems.Add(new NavigationItem("", "LIN配置", "LINConfigView"));
+ //MenuItems.Add(new NavigationItem("", "ZLG CAN配置", "ZlgCanDriveConfigView"));
+ //MenuItems.Add(new NavigationItem("", "ZLG LIN配置", "ZlgLinDriveConfigView"));
MenuItems.Add(new NavigationItem("", "工艺参数", "ProConfigView"));
MenuItems.Add(new NavigationItem("", "工艺曲线", "RealTimeChartView"));
MenuItems.Add(new NavigationItem("", "动作日志", "ActionLogView"));
diff --git a/CapMachine.Wpf/Services/ZlgCanDriveService.cs b/CapMachine.Wpf/Services/ZlgCanDriveService.cs
new file mode 100644
index 0000000..d1a9c09
--- /dev/null
+++ b/CapMachine.Wpf/Services/ZlgCanDriveService.cs
@@ -0,0 +1,1008 @@
+using CapMachine.Model.CANLIN;
+using CapMachine.Wpf.CanDrive;
+using CapMachine.Wpf.CanDrive.ZlgCan;
+using CapMachine.Wpf.Dtos;
+using ImTools;
+using Prism.Mvvm;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace CapMachine.Wpf.Services
+{
+ ///
+ /// ZLG CAN/CANFD 驱动服务(共享设备句柄)。
+ ///
+ ///
+ /// 该类属于“服务层/编排层”,主要职责:
+ /// - 管理 的生命周期(打开、接收线程、关闭释放);
+ /// - 管理 DBC 加载与信号模型集合();
+ /// - 管理写入指令集合()并将其注入驱动形成“DBC 编码发送”的闭环;
+ /// - 提供两种循环发送实现:
+ /// 1) 硬件定时发送(调度表):通过周立功 auto_send 列表 + apply_auto_send(精度高)
+ /// 2) 软件精确定时循环发送:通过后台 Task 在 PC 侧按周期触发 (用于不依赖硬件调度或调试场景)
+ ///
+ /// 线程模型:
+ /// - 接收线程由 内部创建;
+ /// - 软件调度发送由本类创建后台 Task(见 );
+ /// - UI 绑定的属性变化通过 的 RaisePropertyChanged 通知。
+ ///
+ public sealed class ZlgCanDriveService : BindableBase
+ {
+ ///
+ /// 软件调度条目(仅在软件调度线程内使用)。
+ ///
+ ///
+ /// 该结构体承担“纯运行态状态”的职责:
+ /// - 启动时由 构建并排序;
+ /// - 运行中由 在单线程内更新 等字段;
+ /// - 不对外暴露,也不需要线程安全的读写(因为不会跨线程访问)。
+ ///
+ private sealed class SoftwareScheduleItem
+ {
+ ///
+ /// 构造。
+ ///
+ /// DBC 报文名称。
+ /// 周期(Stopwatch ticks)。
+ /// 发送顺序(用于排序)。
+ public SoftwareScheduleItem(string msgName, long periodTicks, int orderSend)
+ {
+ MsgName = msgName;
+ PeriodTicks = periodTicks;
+ OrderSend = orderSend;
+ }
+
+ ///
+ /// DBC 报文名称(MsgName)。
+ ///
+ public string MsgName { get; }
+
+ ///
+ /// 周期(以 ticks 表示)。
+ ///
+ public long PeriodTicks { get; }
+
+ ///
+ /// 发送顺序(用于同 tick 内的确定性排序)。
+ ///
+ public int OrderSend { get; }
+
+ ///
+ /// 下次计划发送的到期时间(sw.ElapsedTicks)。
+ ///
+ public long NextDueTicks;
+
+ ///
+ /// 上一次记录 warn 的时间(用于日志节流)。
+ ///
+ public long LastWarnTicks;
+ }
+
+ ///
+ /// 日志服务。
+ ///
+ private readonly ILogService _log;
+
+ ///
+ /// 共享的 ZLG 驱动实例(CAN/CANFD/LIN)。
+ ///
+ public ZlgCanFd200uDriver Driver { get; }
+
+ ///
+ /// 当前选中的配置程序(沿用原有 FreeSql 模型)。
+ ///
+ ///
+ /// 该属性用于与 UI“配置程序选择”联动。
+ /// - 本类不负责持久化;
+ /// - 只在 中用于把配置中的显示名称映射到 DBC 信号集合。
+ ///
+ public CanLinConfigPro? SelectedCanLinConfigPro { get; set; }
+
+ ///
+ /// Dbc 消息集合(用于 UI 绑定)。
+ ///
+ ///
+ /// 该集合由 / 构建并替换,通常用于界面显示:
+ /// - MsgName/SignalName/实时值等;
+ /// - 也会被 用于把“配置程序中的显示名称”映射到信号上。
+ ///
+ /// 注意:
+ /// - DBC 解码更新发生在 Driver 的接收线程中,若 UI 对集合元素的属性变化敏感,请确保 UI 侧的线程切换策略。
+ ///
+ public ObservableCollection ListCanDbcModel { get; private set; } = new ObservableCollection();
+
+ ///
+ /// 的 backing 字段。
+ ///
+ private bool _dbcParserState;
+ ///
+ /// DBC 解析状态。
+ ///
+ ///
+ /// 该状态仅表示“DBC 已加载且有信号集合”,常用于 UI 控件的 enable/disable。
+ ///
+ public bool DbcParserState
+ {
+ get { return _dbcParserState; }
+ private set { _dbcParserState = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 的 backing 字段。
+ ///
+ private bool _openState;
+ ///
+ /// 设备打开状态。
+ ///
+ ///
+ /// 该属性与 保持一致,由构造函数中订阅 Driver.PropertyChanged 同步刷新。
+ ///
+ public bool OpenState
+ {
+ get { return _openState; }
+ private set { _openState = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 的 backing 字段。
+ ///
+ private ZlgCanMode _mode;
+ ///
+ /// CAN 模式(单选:CAN 或 CANFD)。
+ ///
+ ///
+ /// - 该属性影响发送/调度表下发时使用的 frameType(CAN=8字节,CANFD=64字节);
+ /// - 切换 Mode 后若要立即生效,通常应重新调用 (使 Driver 侧 frameType 更新)。
+ ///
+ public ZlgCanMode Mode
+ {
+ get { return _mode; }
+ set { _mode = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 发送使能(与 UI 的调度表使能语义对齐)。
+ ///
+ ///
+ /// 该属性透传到 :
+ /// - true:允许“事件驱动发送”(CmdData 变化时发送/覆盖 auto_send)
+ /// - false:仅允许手动发送或软件调度任务主动调用发送接口
+ ///
+ public bool SchEnable
+ {
+ get { return Driver.SchEnable; }
+ set { Driver.SchEnable = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 是否启用事件驱动发送。
+ ///
+ ///
+ /// 该属性透传到 。
+ /// - true 且 为 true 时:CmdData 变化会触发增量发送/覆盖更新;
+ /// - false:CmdData 变化不会触发任何发送(由外部显式调用发送方法)。
+ ///
+ public bool IsCycleSend
+ {
+ get { return Driver.IsCycleSend; }
+ set { Driver.IsCycleSend = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 是否正在循环接收(对齐 Toomoss:IsCycleRevice)。
+ ///
+ ///
+ /// 该属性为只读透传:接收线程由 创建。
+ ///
+ public bool IsCycleRevice
+ {
+ get { return Driver.IsReceiving; }
+ }
+
+ ///
+ /// 最近是否发送成功(用于 UI 指示)。
+ ///
+ ///
+ /// 该标志由 Driver 的发送调用或 Tx 回显事件触发,短时间保持 true 后自动回落。
+ ///
+ public bool IsSendOk
+ {
+ get { return Driver.IsSendOk; }
+ }
+
+ ///
+ /// 最近是否接收成功(用于 UI 指示)。
+ ///
+ ///
+ /// 该标志由 Driver 接收线程收到 Rx 帧触发,短时间保持 true 后自动回落。
+ ///
+ public bool IsReviceOk
+ {
+ get { return Driver.IsReviceOk; }
+ }
+
+ ///
+ /// 要发送的 CAN 指令数据。
+ ///
+ ///
+ /// 该集合通常来源于“读写设置/配置程序”生成的信号写入项。
+ /// - 发送时并不是逐条直接写信号,而是按 MsgName 聚合后通过 DBC 编码为帧(由 Driver 完成)。
+ /// - 本类在 中将集合注入 Driver,并建立信号变化事件订阅。
+ ///
+ public List CmdData { get; } = new List();
+
+ ///
+ /// 调度表(软件调度任务)互斥锁。
+ ///
+ ///
+ /// 保护以下共享状态的并发访问:
+ /// - 调度表条目快照
+ /// - / 生命周期切换
+ ///
+ /// 说明:
+ /// - 硬件 auto_send 的生命周期主要由 Driver 内部管理;
+ /// - 本类在 中同时清理软件任务与硬件 auto_send,确保“停止=设备不再发”。
+ ///
+ private readonly object _scheduleLock = new object();
+
+ ///
+ /// 当前调度表条目快照。
+ ///
+ ///
+ /// 元组含义:
+ /// - MsgName:DBC Message Name
+ /// - CycleMs:周期(ms)
+ /// - OrderSend/SchTabIndex:用于排序,保证硬件列表 index 的确定性
+ ///
+ private List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> _scheduleItems = new List<(string, int, int, int)>();
+
+ ///
+ /// 软件调度任务取消令牌。
+ ///
+ private CancellationTokenSource? _scheduleCts;
+
+ ///
+ /// 软件调度后台任务。
+ ///
+ private Task? _scheduleTask;
+
+ ///
+ /// 标记“当前循环发送使用配置的调度表项”。
+ ///
+ ///
+ /// 该字段当前仅用于区分启动来源(配置调度表 or 精确定时循环),未来可用于 UI 展示或行为分支。
+ ///
+ private bool _scheduleUseConfigItems;
+
+ ///
+ /// 要发送的速度指令数据。
+ ///
+ ///
+ /// 该字段用于兼容旧接口 的“手动发送转速”行为:
+ /// - 通过 从 中按 ConfigName=转速 选取;
+ /// - 若未配置该项,则手动发送会记录 warn 并忽略。
+ ///
+ private CanCmdData? SpeedCanCmdData { get; set; }
+
+ ///
+ /// 当前打开的设备索引(默认 0)。
+ ///
+ ///
+ /// 由 更新,在 时作为打开参数。
+ ///
+ private uint _deviceIndex = 0;
+
+ ///
+ /// 通道0配置快照。
+ ///
+ ///
+ /// 目前服务层仅初始化通道0,并将合并接收等设备级开关按通道0的配置写入设备。
+ ///
+ private ZlgCanFdChannelOptions _channel0 = new ZlgCanFdChannelOptions();
+
+ ///
+ /// 构造。
+ ///
+ /// 日志服务。
+ ///
+ /// 构造时会:
+ /// - 创建 ;
+ /// - 订阅 Driver 的 ,并同步更新本服务的派生属性:
+ /// / / / 。
+ ///
+ public ZlgCanDriveService(ILogService logService)
+ {
+ _log = logService;
+ Driver = new ZlgCanFd200uDriver(logService);
+
+ Driver.PropertyChanged += (_, __) =>
+ {
+ OpenState = Driver.OpenState;
+ RaisePropertyChanged(nameof(IsCycleRevice));
+ RaisePropertyChanged(nameof(IsSendOk));
+ RaisePropertyChanged(nameof(IsReviceOk));
+ };
+
+ OpenState = Driver.OpenState;
+ Mode = ZlgCanMode.Can;
+ }
+
+ ///
+ /// 初始化 CAN 配置信息,并将配置中的名称映射到 DBC 信号集合(用于 UI 显示)。
+ ///
+ /// 选中的配置。
+ ///
+ /// 该方法不与设备连接状态强绑定:
+ /// - 只要 已有信号集合,就可以进行“显示名称映射”。
+ ///
+ /// 映射规则:
+ /// - 以 SignalName 为键,在 DBC 模型集合中找到对应信号,并把配置项的 Name 写回 。
+ ///
+ public void InitCanConfig(CanLinConfigPro selectedCanLinConfigPro)
+ {
+ SelectedCanLinConfigPro = selectedCanLinConfigPro;
+ if (SelectedCanLinConfigPro?.CanLinConfigContents == null) return;
+
+ // 先清空旧映射,避免配置项删除/切换后中文名称残留。
+ if (ListCanDbcModel != null && ListCanDbcModel.Count > 0)
+ {
+ foreach (var m in ListCanDbcModel)
+ {
+ m.Name = null;
+ }
+ }
+
+ foreach (var item in SelectedCanLinConfigPro.CanLinConfigContents)
+ {
+ CanDbcModel? find;
+ if (!string.IsNullOrWhiteSpace(item.MsgFrameName))
+ {
+ find = ListCanDbcModel.FindFirst(a => a.SignalName == item.SignalName && a.MsgName == item.MsgFrameName);
+ }
+ else
+ {
+ find = ListCanDbcModel.FindFirst(a => a.SignalName == item.SignalName);
+ }
+ if (find != null)
+ {
+ find.Name = item.Name;
+ }
+ }
+ }
+
+ ///
+ /// 更新配置(从 DTO/DB 同步到驱动)。
+ ///
+ /// 设备索引。
+ /// 仲裁波特率(bps)。
+ /// 数据波特率(bps)。
+ /// 终端电阻使能。
+ /// 是否合并接收。
+ /// 合并接收缓冲帧数。
+ ///
+ /// 该方法仅更新服务层保存的“配置快照”,不会立即触发驱动重连。
+ /// - 生效时机:下一次调用 打开设备并初始化通道。
+ /// - 当前实现仅使用通道0配置。
+ ///
+ public void UpdateConfig(uint deviceIndex, uint arbBaudRate, uint dataBaudRate, bool resEnable, bool mergeReceive = false, int mergeReceiveBufferFrames = 100)
+ {
+ _deviceIndex = deviceIndex;
+ _channel0.ArbitrationBaudRate = arbBaudRate;
+ _channel0.DataBaudRate = dataBaudRate;
+ _channel0.EnableInternalResistance = resEnable;
+ _channel0.EnableMergeReceive = mergeReceive;
+ _channel0.MergeReceiveBufferFrames = mergeReceiveBufferFrames;
+ }
+
+ ///
+ /// 打开 CAN/CANFD(按 Mode)。
+ ///
+ ///
+ /// - 该方法是幂等的:若 为 true,则直接返回;
+ /// - 成功打开后会初始化通道并启动接收线程(见 )。
+ ///
+ /// 注意:
+ /// - 本方法不会自动加载 DBC,也不会自动注入 CmdData;
+ /// - 建议调用顺序:UpdateConfig -> StartCanDrive -> StartDbc -> LoadCmdDataToDrive。
+ ///
+ public void StartCanDrive()
+ {
+ if (OpenState)
+ {
+ return;
+ }
+
+ Driver.OpenAndInitCan(_deviceIndex, _channel0);
+ Driver.StartReceiveLoop(_channel0.EnableMergeReceive, _channel0.MergeReceiveBufferFrames);
+ OpenState = Driver.OpenState;
+ }
+
+ ///
+ /// 使能/停止循环接收。
+ ///
+ /// true=启动接收线程,false=停止接收线程。
+ ///
+ /// 说明:
+ /// - 接收线程属于 Driver 内部资源;
+ /// - 若开启合并接收,接收线程会按通道0配置决定采用 merge 或 per-channel 接收。
+ ///
+ /// 设备未连接。
+ public void SetReceiveEnabled(bool enable)
+ {
+ if (!OpenState)
+ {
+ throw new InvalidOperationException("设备未连接,无法切换循环接收。");
+ }
+
+ if (enable)
+ {
+ if (!Driver.IsReceiving)
+ {
+ Driver.StartReceiveLoop(_channel0.EnableMergeReceive, _channel0.MergeReceiveBufferFrames);
+ }
+ }
+ else
+ {
+ if (Driver.IsReceiving)
+ {
+ Driver.StopReceiveLoop();
+ }
+ }
+
+ RaisePropertyChanged(nameof(IsCycleRevice));
+ }
+
+ ///
+ /// 关闭设备(会同时关闭共享的 LIN 通道)。
+ ///
+ ///
+ /// Close 语义强调“彻底停止后台活动”:
+ /// - 停止调度表:取消软件调度任务,并清理硬件 auto_send 列表(避免设备侧继续发);
+ /// - 停止事件驱动发送:将 置为 false;
+ /// - 停止接收: 内部会停止接收线程并释放句柄。
+ ///
+ public void CloseDevice()
+ {
+ try
+ {
+ // Close 语义:关闭时必须停止循环发送与循环接收。
+ // - 循环发送:停止软件调度,并关闭事件驱动发送标志。
+ // - 循环接收:Driver.Close 内部会 StopReceiveLoop。
+ StopSchedule();
+ IsCycleSend = false;
+
+ Driver.Close();
+ }
+ finally
+ {
+ OpenState = Driver.OpenState;
+ DbcParserState = false;
+ }
+ }
+
+ ///
+ /// 设置调度表配置(CAN)。
+ ///
+ /// 调度表配置项。
+ ///
+ /// 该方法仅保存调度表条目快照,不会立即启动发送。
+ /// - 真正启动由 触发。
+ /// - MsgName 为空的条目会被过滤。
+ ///
+ public void SetScheduleConfigs(IEnumerable configs)
+ {
+ var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List();
+ lock (_scheduleLock)
+ {
+ _scheduleItems = list
+ .Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
+ .ToList();
+ }
+ }
+
+ ///
+ /// 设置调度表配置(CANFD)。
+ ///
+ /// 调度表配置项。
+ ///
+ /// 语义同 CAN 版本 ,差异仅在于配置来源 DTO 类型。
+ ///
+ public void SetScheduleConfigs(IEnumerable configs)
+ {
+ var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List();
+ lock (_scheduleLock)
+ {
+ _scheduleItems = list
+ .Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
+ .ToList();
+ }
+ }
+
+ ///
+ /// 启动调度表(硬件定时发送列表 auto_send)。
+ ///
+ ///
+ /// 本方法选择“硬件调度表”实现:
+ /// - 将调度表条目转换为设备 auto_send 列表并 apply,使设备按周期自动发送;
+ /// - 运行时若 CmdData 对应信号值变化,Driver 会覆盖更新对应 index 的帧数据。
+ ///
+ /// 前置条件:
+ /// - 设备已连接( true);
+ /// - 调度表不为空;
+ /// - DBC 已加载且 CmdData 可编码(否则对应 MsgName 会被 Driver 跳过并记录 warn)。
+ ///
+ /// 设备未连接或调度表为空。
+ public void StartSchedule()
+ {
+ if (!OpenState)
+ {
+ throw new InvalidOperationException("设备未连接,无法启动调度表。");
+ }
+
+ List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items;
+ lock (_scheduleLock)
+ {
+ // 取快照:
+ // - 避免启动过程中与 UI 线程并发修改调度表集合;
+ // - 后续 Driver.StartAutoSendSchedule 会在内部再次排序并建立 MsgName->index 映射。
+ items = _scheduleItems.ToList();
+ }
+
+ if (items.Count == 0)
+ {
+ throw new InvalidOperationException("调度表为空,无法启动调度表。");
+ }
+
+ // 周立功:调度表=硬件定时发送列表(auto_send)
+ // - 周期精度高,由设备侧时钟保障
+ // - PC 仍可同时调用发送接口发送其它数据
+ _scheduleUseConfigItems = true;
+
+ // 启动前先停止已有任务(软件调度 + 硬件 auto_send)
+ StopSchedule();
+
+ // 统一:启用调度表后,等同“循环发送开启”
+ // 说明:这里的 IsCycleSend 指的是“允许事件驱动发送/覆盖更新”。
+ // - 硬件 auto_send 本身不依赖 IsCycleSend;
+ // - 但运行中信号值变化要想覆盖更新到硬件列表,需要 IsCycleSend=true 且 SchEnable=true。
+ IsCycleSend = true;
+
+ // 使用硬件定时发送列表:按 scheduleItems 下发 auto_send 并 apply
+ Driver.StartAutoSendSchedule(
+ items.Select(a => (a.MsgName, a.CycleMs, a.OrderSend, a.SchTabIndex)),
+ channelIndex: 0,
+ frameType: Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
+ }
+
+ ///
+ /// 启动软件精确定时循环发送。
+ ///
+ /// 周期(ms)。
+ ///
+ /// 与 的区别:
+ /// - 本方法完全在 PC 侧调度,周期精度受系统调度与负载影响;
+ /// - 适用于不依赖硬件 auto_send 的场景,或作为调试/兼容方案。
+ ///
+ /// 发送对象:
+ /// - 按当前 中出现的 MsgName 去重后,按统一周期循环发送。
+ ///
+ /// 设备未连接或 CmdData 为空。
+ public void StartPrecisionCycleSend(int cycleMs)
+ {
+ if (!OpenState)
+ {
+ throw new InvalidOperationException("设备未连接,无法启动循环发送。");
+ }
+
+ var ms = Math.Max(1, cycleMs);
+ var msgNames = CmdData.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).Select(a => a.MsgName!).Distinct(StringComparer.Ordinal).ToList();
+ if (msgNames.Count == 0)
+ {
+ throw new InvalidOperationException("CmdData 为空,无法启动循环发送。");
+ }
+
+ // 软件调度:把 CmdData 中出现的 MsgName 去重后按统一周期循环发送。
+ // 注意:这与“配置调度表”的区别是:
+ // - 配置调度表:每条消息有自己的 Cycle/Order/SchTabIndex
+ // - 软件精确循环:所有消息同周期,OrderSend 固定为 1,SchTabIndex 固定为 0
+ var items = msgNames.Select(n => (n, ms, 1, 0)).ToList();
+ _scheduleUseConfigItems = false;
+ StartSoftwareScheduler(items);
+ }
+
+ ///
+ /// 停止调度表/循环发送。
+ ///
+ ///
+ /// 该方法会同时停止两类发送资源:
+ /// - 软件调度:取消 Task 并等待短时间退出;
+ /// - 硬件调度:调用 Driver.StopAutoSendSchedule 清空设备 auto_send 列表,防止设备继续发送。
+ ///
+ /// 该方法不会自动将 置为 false:
+ /// - 保留给上层决定(例如仅暂停调度,但仍允许事件驱动发送)。
+ ///
+ public void StopSchedule()
+ {
+ CancellationTokenSource? cts;
+ Task? task;
+ lock (_scheduleLock)
+ {
+ cts = _scheduleCts;
+ task = _scheduleTask;
+ _scheduleCts = null;
+ _scheduleTask = null;
+ }
+
+ try
+ {
+ cts?.Cancel();
+ if (task != null)
+ {
+ // 等待软件调度线程退出:
+ // - 这里不做无限等待,避免 UI 卡死;
+ // - 超时/异常会被吞掉(与旧代码风格保持一致)。
+ task.Wait(TimeSpan.FromSeconds(2));
+ }
+ }
+ catch
+ {
+ // 说明:
+ // - task.Wait 可能抛 AggregateException/TaskCanceledException;
+ // - StopSchedule 属于“停止/清理”语义,不应因等待异常影响后续硬件清理。
+ }
+ finally
+ {
+ cts?.Dispose();
+ }
+
+ // 无论当前是否处于硬件定时发送,都做一次清理(clear_auto_send)保证关闭/停止时设备侧不再发。
+ try
+ {
+ if (OpenState)
+ {
+ // 防御性清理:
+ // - 若此前是硬件调度表,必须 clear_auto_send,否则设备会继续按周期发送;
+ // - 若此前是软件调度,该调用也不会破坏状态(最多是清空一个空表)。
+ Driver.StopAutoSendSchedule(channelIndex: 0);
+ }
+ }
+ catch
+ {
+ // ignored:停止流程不因硬件清理失败而中断。
+ }
+ }
+
+ ///
+ /// 启动软件调度后台任务。
+ ///
+ /// 调度表条目集合(由调用方准备好快照)。
+ ///
+ /// 该任务使用“下一次到期时间 due”字典进行调度:
+ /// - 优先 sleep 到最早 due,减少 CPU 占用;
+ /// - tick 内对 ready 的消息按字典序依次发送;
+ /// - 每次发送后更新下一次 due。
+ ///
+ /// 注意:
+ /// - 这是软件调度,不保证绝对周期精度;
+ /// - 发送异常会记录 warn,但不会终止调度循环。
+ ///
+ private void StartSoftwareScheduler(List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items)
+ {
+ StopSchedule();
+
+ var cts = new CancellationTokenSource();
+ lock (_scheduleLock)
+ {
+ _scheduleCts = cts;
+ }
+
+ // 统一:软件调度开启后,等同“循环发送开启”
+ IsCycleSend = true;
+
+ var frequency = Stopwatch.Frequency;
+ long ToTicks(int ms)
+ {
+ var safeMs = Math.Max(1, ms);
+ // 使用 long 避免溢出;周期最小为 1 tick。
+ var ticks = (long)(safeMs * (double)frequency / 1000d);
+ return Math.Max(1, ticks);
+ }
+
+ // 只在启动时做一次去重/排序;循环内不分配。
+ var unique = new Dictionary(StringComparer.Ordinal);
+ foreach (var it in items)
+ {
+ if (string.IsNullOrWhiteSpace(it.MsgName)) continue;
+ var name = it.MsgName.Trim();
+ if (!unique.ContainsKey(name))
+ {
+ unique[name] = (Math.Max(1, it.CycleMs), it.OrderSend);
+ }
+ }
+
+ var list = new List(unique.Count);
+ foreach (var kv in unique)
+ {
+ list.Add(new SoftwareScheduleItem(kv.Key, ToTicks(kv.Value.CycleMs), kv.Value.OrderSend));
+ }
+
+ list.Sort((a, b) =>
+ {
+ var byOrder = a.OrderSend.CompareTo(b.OrderSend);
+ return byOrder != 0 ? byOrder : string.CompareOrdinal(a.MsgName, b.MsgName);
+ });
+
+ var scheduleItems = list.ToArray();
+
+ // 使用单调时钟:避免系统时间跳变导致周期紊乱。
+ var sw = Stopwatch.StartNew();
+ for (var i = 0; i < scheduleItems.Length; i++)
+ {
+ // 首次启动:立即发送一轮(NextDueTicks=0)。
+ scheduleItems[i].NextDueTicks = 0;
+ scheduleItems[i].LastWarnTicks = long.MinValue;
+ }
+
+ lock (_scheduleLock)
+ {
+ _scheduleTask = Task.Factory.StartNew(() =>
+ {
+ // LongRunning:为调度线程提供独立线程,减少线程池饥饿导致的周期抖动。
+ RunSoftwareScheduler(sw, scheduleItems, cts.Token, frequency);
+ }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ }
+ }
+
+ ///
+ /// 软件调度循环(后台线程)。
+ ///
+ /// 单调计时器(启动时创建,贯穿整个调度生命周期)。
+ /// 调度条目数组(启动时构建;运行中会更新每项的到期时间)。
+ /// 取消令牌(由 触发取消)。
+ /// (用于 ticks/ms 换算)。
+ ///
+ /// 设计目标:
+ /// - 尽量降低 CPU 占用:优先等待到最早 due 附近;
+ /// - 尽量降低抖动:临近到期使用短等待 + 自旋;
+ /// - 避免“积压补发”:如果错过多个周期,只发送一次并将 due 跳到 now+period。
+ ///
+ /// 线程安全说明:
+ /// - 该方法仅由 创建的 LongRunning 任务调用;
+ /// - 在该线程内独占读写(不会跨线程访问),因此不需要额外锁。
+ ///
+ private void RunSoftwareScheduler(Stopwatch sw, SoftwareScheduleItem[] items, CancellationToken token, long frequency)
+ {
+ // 日志节流:同一条消息每 10 秒最多记录一次发送异常。
+ var warnIntervalTicks = frequency * 10;
+ var spin = new SpinWait();
+
+ while (!token.IsCancellationRequested)
+ {
+ if (!OpenState)
+ {
+ // 设备未打开时不发送:
+ // - 避免在 Close/重连窗口持续抛异常;
+ // - 这里仍然响应取消,保证 StopSchedule 不会“卡住等待”。
+ if (token.WaitHandle.WaitOne(50))
+ {
+ break;
+ }
+ continue;
+ }
+
+ var nowTicks = sw.ElapsedTicks;
+ long minDue = long.MaxValue;
+ for (var i = 0; i < items.Length; i++)
+ {
+ var d = items[i].NextDueTicks;
+ if (d < minDue)
+ {
+ minDue = d;
+ }
+ }
+
+ var waitTicks = minDue - nowTicks;
+ if (waitTicks > 0)
+ {
+ // 混合等待:
+ // - 剩余时间较长:WaitOne(<=200ms) 降低 CPU 占用
+ // - 临近到期:1ms 级等待 + 短自旋提升边界精度
+ var waitMs = (int)(waitTicks * 1000 / frequency);
+ if (waitMs > 5)
+ {
+ var ms = Math.Min(waitMs - 1, 200);
+ if (token.WaitHandle.WaitOne(ms))
+ {
+ break;
+ }
+ continue;
+ }
+
+ if (waitMs > 1)
+ {
+ if (token.WaitHandle.WaitOne(1))
+ {
+ break;
+ }
+ continue;
+ }
+
+ spin.SpinOnce();
+ continue;
+ }
+
+ // 到期发送:
+ // - 同一条消息“错过多个周期”时,只发送一次(避免 burst),然后尽快恢复节拍。
+ nowTicks = sw.ElapsedTicks;
+ for (var i = 0; i < items.Length; i++)
+ {
+ var it = items[i];
+ if (it.NextDueTicks > nowTicks)
+ {
+ continue;
+ }
+
+ try
+ {
+ // 软件调度最终仍复用 Driver 的“按 MsgName 编码并发送一帧”的能力。
+ // 这意味着:
+ // - 发送数据来源仍是 CmdData 当前值;
+ // - 若 DBC 未加载/MsgName 无法编码,Driver 内部会快速返回或抛异常。
+ Driver.SendOneMsgByCmdData(it.MsgName, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
+ }
+ catch (Exception ex)
+ {
+ var t = sw.ElapsedTicks;
+ if (t - it.LastWarnTicks >= warnIntervalTicks)
+ {
+ it.LastWarnTicks = t;
+ _log.Warn($"软件循环发送失败:{it.MsgName},{ex.Message}");
+ }
+ }
+ finally
+ {
+ // 漂移控制:正常情况下按“计划时间”累加;若落后过多,则跳到 now+period(只补发一次)。
+ var next = it.NextDueTicks + it.PeriodTicks;
+ if (next <= nowTicks)
+ {
+ next = nowTicks + it.PeriodTicks;
+ }
+ it.NextDueTicks = next;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 加载 DBC。
+ ///
+ /// DBC 路径。
+ /// DBC 解析得到的信号集合。
+ ///
+ /// - DBC 加载成功后会更新 与 ;
+ /// - 后续发送(调度表/事件驱动/手动发送)都依赖 DBC 编码能力。
+ ///
+ /// 设备未连接。
+ public ObservableCollection StartDbc(string path)
+ {
+ if (!OpenState)
+ {
+ throw new InvalidOperationException("请先打开设备后再加载 DBC。");
+ }
+
+ // DBC 解析与信号集合构建由 Driver 负责:
+ // - 该过程通常涉及读取文件、解析 DBC、构建 Msg/Signal 元数据;
+ // - Driver 侧也会建立“收到帧 -> DBC 解码 -> 更新模型值”的解码链路。
+ ListCanDbcModel = Driver.StartDbc(path);
+
+ // 这里的状态仅用于 UI enable/disable:
+ // - true 代表“已有可用信号集合”;
+ // - 并不代表后续每次编码/发送一定成功(仍受 CmdData 是否完整、MsgName 是否匹配影响)。
+ DbcParserState = ListCanDbcModel != null && ListCanDbcModel.Count > 0;
+ return ListCanDbcModel;
+ }
+
+ ///
+ /// 加载要发送的数据(订阅数据变化事件)。
+ ///
+ /// 指令数据集合。
+ ///
+ /// 该方法会:
+ /// - 更新本服务的 集合;
+ /// - 从集合中选取“转速”项缓存到 (用于兼容旧的手动发送接口);
+ /// - 调用 注入 Driver,并建立事件驱动链路。
+ ///
+ /// 注意:
+ /// - frameType 由 决定;若运行时切换 Mode,建议重新调用本方法。
+ ///
+ public void LoadCmdDataToDrive(List cmdData)
+ {
+ var list = cmdData ?? new List();
+
+ // 以“清空 + AddRange”的方式更新:
+ // - 保持 CmdData 引用不变,避免上层若持有该 List 引用时出现替换不可见;
+ // - 也便于在 Driver 层按引用订阅 CanCmdDataChanged 事件(其内部会自行做快照)。
+ CmdData.Clear();
+ CmdData.AddRange(list);
+
+ // 兼容旧接口:从 CmdData 中缓存“转速”项。
+ // - SendMsgToCanDrive 仅维护这一个信号;
+ // - 若业务后续扩展为多信号手动发送,应改为按 MsgName/SignalName 精确定位。
+ foreach (var item in CmdData)
+ {
+ if (string.Equals(item.ConfigName, "转速", StringComparison.Ordinal))
+ {
+ SpeedCanCmdData = item;
+ }
+ }
+
+ // 将 CmdData 注入 Driver:
+ // - Driver 会订阅每个 CanCmdData 的变化事件,用于“事件驱动发送/覆盖更新 auto_send”;
+ // - frameType 必须与 Mode 对齐:CAN 与 CANFD 的长度/编码规则不同。
+ Driver.LoadCmdDataToDrive(CmdData, channelIndex: 0, frameType: Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
+ }
+
+ ///
+ /// 手动发送(目前对齐原服务:仅发送转速)。
+ ///
+ /// 转速。
+ ///
+ /// 行为说明:
+ /// - 优先将 speedData 写入 的 SignalCmdValue;
+ /// - 若当前未启用事件驱动发送( 或 为 false),则主动触发一次编码发送,
+ /// 以维持旧版本“点击发送即下发”的体验。
+ ///
+ public void SendMsgToCanDrive(double speedData)
+ {
+ if (!OpenState)
+ {
+ return;
+ }
+
+ if (SpeedCanCmdData != null)
+ {
+ // 先更新写入值:
+ // - 若当前启用了事件驱动发送(IsCycleSend && SchEnable),该赋值本身会触发 Driver 的增量发送/覆盖更新;
+ // - 若未启用事件驱动发送,下方会主动补发一次,保证“点一下就发”的旧行为。
+ SpeedCanCmdData.SignalCmdValue = speedData;
+ }
+ else
+ {
+ _log.Warn("未配置转速指令项(ConfigName=转速),忽略手动发送。");
+ }
+
+ // 若未启用事件驱动发送,则这里主动发送一次(与旧行为兼容)
+ if (!IsCycleSend || !SchEnable)
+ {
+ // 历史行为:选取 CmdData 的第一条 MsgName 进行发送。
+ // - 由于本方法本质上是“转速”单信号写入,严格来说应按 SpeedCanCmdData.MsgName 发送;
+ // - 但为避免改动业务逻辑,这里保持旧逻辑,仅通过注释明确其局限。
+ var firstMsg = CmdData.FirstOrDefault()?.MsgName;
+ if (!string.IsNullOrWhiteSpace(firstMsg))
+ {
+ Driver.SendOneMsgByCmdData(firstMsg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
+ }
+ }
+ }
+ }
+
+ ///
+ /// ZLG CAN 工作模式。
+ ///
+ public enum ZlgCanMode
+ {
+ ///
+ /// CAN 经典帧。
+ ///
+ Can = 0,
+ ///
+ /// CAN FD。
+ ///
+ CanFd = 1
+ }
+}
diff --git a/CapMachine.Wpf/Services/ZlgLinDriveService.cs b/CapMachine.Wpf/Services/ZlgLinDriveService.cs
new file mode 100644
index 0000000..7a33a3a
--- /dev/null
+++ b/CapMachine.Wpf/Services/ZlgLinDriveService.cs
@@ -0,0 +1,1314 @@
+using CapMachine.Model.CANLIN;
+using CapMachine.Wpf.LinDrive;
+using CapMachine.Wpf.Dtos;
+using Prism.Mvvm;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace CapMachine.Wpf.Services
+{
+ ///
+ /// ZLG LIN 驱动服务(共享设备句柄)。
+ /// 说明:
+ /// - 该服务不直接持有/管理底层原生句柄,而是复用 中的 Driver(设备级句柄与接收线程均由 Driver 统一管理)。
+ /// - LIN 的“调度表/循环发送”在当前实现中使用软件精确定时( + )完成;
+ /// 与 ZLG CAN/CANFD 的硬件 auto_send 不同,LIN 侧没有调用硬件调度表能力。
+ /// - LDF 解析:此处实现了一个尽量容错的轻量解析器,用于建立“帧/信号 -> 位定义”的运行时索引,供 LIN 收发编解码使用。
+ /// 线程与绑定:
+ /// - LIN 接收回调通常来自 Driver 的接收线程;更新 WPF 绑定对象时会切换到 UI 线程(见 )。
+ /// - CmdData / LDF 索引 / 调度表快照均通过独立锁对象保护,避免 UI 线程与后台线程并发修改引发的竞态。
+ ///
+ public sealed class ZlgLinDriveService : BindableBase
+ {
+ ///
+ /// LIN 信号定义(运行时索引条目)。
+ /// 说明:目前仅使用 StartBit/BitLength 做 bit 级读写;Factor/Offset/IsSigned 预留用于后续扩展(物理量换算)。
+ ///
+ private sealed class LinSignalDef
+ {
+ public string SignalName { get; set; } = string.Empty;
+ public int StartBit { get; set; }
+ public int BitLength { get; set; }
+ public bool IsSigned { get; set; }
+ public double Factor { get; set; } = 1;
+ public double Offset { get; set; }
+ }
+
+ ///
+ /// LIN 帧定义(运行时索引条目)。
+ /// 说明:
+ /// - 同时缓存裸 FrameId(0-63)与受保护 PID(含奇偶校验位)。
+ /// - Signals 字典仅包含在 LDF 的 Frames 里声明且在 Signals 区能找到 bit 定义的信号。
+ ///
+ private sealed class LinFrameDef
+ {
+ public string FrameName { get; set; } = string.Empty;
+ public byte FrameId { get; set; }
+ public byte Pid { get; set; }
+ public int DataLen { get; set; } = 8;
+ public string? Publisher { get; set; }
+ public bool IsMasterFrame { get; set; }
+ public Dictionary Signals { get; } = new Dictionary(StringComparer.Ordinal);
+ }
+
+ ///
+ /// 软件调度项(以 ticks 为最小单位进行精确定时)。
+ /// 说明:
+ /// - NextDueTicks/LastWarnTicks 会在调度线程中更新,因此不对外暴露,仅在调度线程内使用。
+ /// - PeriodTicks 是基于 换算而来,与系统时间无关,抗时间跳变。
+ ///
+ private sealed class SoftwareScheduleItem
+ {
+ public SoftwareScheduleItem(string msgName, long periodTicks)
+ {
+ MsgName = msgName;
+ PeriodTicks = periodTicks;
+ }
+
+ public string MsgName { get; }
+ public long PeriodTicks { get; }
+ public long NextDueTicks;
+ public long LastWarnTicks;
+ }
+
+ private readonly ILogService _log;
+ private readonly ZlgCanDriveService _zlgCanDriveService;
+
+ ///
+ /// LDF 索引锁:保护帧/信号定义索引与 UI 绑定模型映射(_framesByName/_framesByPid/_modelIndex 等)。
+ ///
+ private readonly object _ldfLock = new object();
+
+ ///
+ /// CmdData 锁:保护 的替换与事件订阅关系,避免并发枚举/修改。
+ ///
+ private readonly object _cmdLock = new object();
+
+ ///
+ /// 调度锁:保护调度配置快照与调度线程的启动/停止(_scheduleCts/_scheduleTask/_scheduleRunning)。
+ ///
+ private readonly object _scheduleLock = new object();
+
+ ///
+ /// 帧名 -> 帧定义索引(用于发送:按 MsgName 找到 PID/信号位定义)。
+ ///
+ private Dictionary _framesByName = new Dictionary(StringComparer.Ordinal);
+
+ ///
+ /// PID/ID -> 帧定义索引(用于接收:按 pid/6bit id 回查帧定义并解码信号)。
+ ///
+ private Dictionary _framesByPid = new Dictionary();
+
+ ///
+ /// “帧名+信号名” -> UI 模型索引,用于接收线程快速定位绑定对象并更新实时值。
+ ///
+ private Dictionary _modelIndex = new Dictionary(StringComparer.Ordinal);
+
+ ///
+ /// 未知 PID 的日志节流表:避免接收线程在 LDF 缺失时刷屏。
+ ///
+ private readonly Dictionary _unknownPidLastLogTicks = new Dictionary();
+
+ ///
+ /// 调度取消源(置空表示未运行)。
+ ///
+ private CancellationTokenSource? _scheduleCts;
+
+ ///
+ /// 调度后台任务(LongRunning)。
+ ///
+ private Task? _scheduleTask;
+
+ ///
+ /// 调度运行标记(与 _scheduleCts 配合;主要用于可读性与状态表达)。
+ ///
+ private bool _scheduleRunning;
+
+ ///
+ /// LIN 调度表 DTO 缓存(由 ViewModel/上层写入;启动调度时会取快照)。
+ ///
+ private List _linScheduleConfigs = new List();
+
+ ///
+ /// 当前选中的配置程序(沿用原有 FreeSql 模型)。
+ ///
+ public CanLinConfigPro? SelectedCanLinConfigPro { get; set; }
+
+ private bool _openState;
+ ///
+ /// LIN 打开状态。
+ ///
+ public bool OpenState
+ {
+ get { return _openState; }
+ private set { _openState = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 解析 LDF 文本,提取 Frames/Signals/Nodes(Master)信息。
+ /// 说明:这是一个轻量解析器:
+ /// - 目标是构建“帧-信号位定义”的索引,并支持 UI 展示;
+ /// - 不追求覆盖所有 LDF 语法,仅尽量兼容常见格式。
+ ///
+ /// LDF 原始文本(建议已剔除注释)。
+ /// Frames 列表、Signals 位定义字典、Master 节点名(可能为空)。
+ private static (List<(string FrameName, int FrameId, int DataLen, string? Publisher, List Signals)>, Dictionary, string? MasterNodeName) ParseLdf(string ldfText)
+ {
+ var master = TryExtractMasterNodeName(ldfText);
+
+ var framesBlock = TryExtractNamedBlock(ldfText, "Frames");
+ var signalsBlock = TryExtractNamedBlock(ldfText, "Signals");
+
+ var signals = ParseSignalsBlock(signalsBlock);
+ var frames = ParseFramesBlock(framesBlock);
+ return (frames, signals, master);
+ }
+
+ private static string? TryExtractMasterNodeName(string ldfText)
+ {
+ var nodesBlock = TryExtractNamedBlock(ldfText, "Nodes");
+ if (string.IsNullOrWhiteSpace(nodesBlock)) return null;
+
+ // 常见格式:Master: MasterName;
+ var m = Regex.Match(nodesBlock, @"(?im)^\s*Master\s*:\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*;", RegexOptions.Compiled);
+ if (m.Success)
+ {
+ return m.Groups["name"].Value;
+ }
+
+ return null;
+ }
+
+ private static Dictionary ParseSignalsBlock(string? signalsBlock)
+ {
+ var dict = new Dictionary(StringComparer.Ordinal);
+ if (string.IsNullOrWhiteSpace(signalsBlock))
+ {
+ return dict;
+ }
+
+ // 兼容常见格式:SigName: 0, 8;
+ var sigRegex = new Regex(@"(?im)^\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?\d+)\s*,\s*(?\d+)", RegexOptions.Compiled);
+ foreach (Match m in sigRegex.Matches(signalsBlock))
+ {
+ var name = m.Groups["name"].Value;
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ continue;
+ }
+
+ if (!int.TryParse(m.Groups["start"].Value, out var startBit))
+ {
+ continue;
+ }
+
+ if (!int.TryParse(m.Groups["len"].Value, out var bitLen))
+ {
+ bitLen = 1;
+ }
+
+ if (bitLen <= 0)
+ {
+ bitLen = 1;
+ }
+
+ dict[name] = (startBit, bitLen);
+ }
+
+ return dict;
+ }
+
+ private static List<(string FrameName, int FrameId, int DataLen, string? Publisher, List Signals)> ParseFramesBlock(string? framesBlock)
+ {
+ var list = new List<(string FrameName, int FrameId, int DataLen, string? Publisher, List Signals)>();
+ if (string.IsNullOrWhiteSpace(framesBlock))
+ {
+ return list;
+ }
+
+ // 常见格式:FrameName : 0x10, Publisher, 8 { SigA, SigB };
+ var frameRegex = new Regex(@"(?s)(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?0x[0-9A-Fa-f]+|\d+)\s*,\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*,\s*(?\d+)\s*\{(?.*?)\}\s*;?", RegexOptions.Compiled);
+ var sigRegex = new Regex(@"(?m)^[\s\t]*(?[A-Za-z_][A-Za-z0-9_]*)\s*[,;]", RegexOptions.Compiled);
+
+ foreach (Match fm in frameRegex.Matches(framesBlock))
+ {
+ var frameName = fm.Groups["name"].Value;
+ var idText = fm.Groups["id"].Value;
+ var pub = fm.Groups["pub"].Value;
+ var lenText = fm.Groups["len"].Value;
+ var body = fm.Groups["body"].Value;
+
+ if (string.IsNullOrWhiteSpace(frameName) || string.IsNullOrWhiteSpace(idText) || string.IsNullOrWhiteSpace(lenText))
+ {
+ continue;
+ }
+
+ if (!TryParseIntAuto(idText, out var frameId))
+ {
+ continue;
+ }
+
+ if (!int.TryParse(lenText, out var dataLen))
+ {
+ dataLen = 8;
+ }
+
+ dataLen = Math.Max(1, Math.Min(8, dataLen));
+
+ var sigs = new List();
+ foreach (Match sm in sigRegex.Matches(body))
+ {
+ var sigName = sm.Groups["sig"].Value;
+ if (string.IsNullOrWhiteSpace(sigName))
+ {
+ continue;
+ }
+ if (IsReservedKeyword(sigName))
+ {
+ continue;
+ }
+ sigs.Add(sigName);
+ }
+
+ list.Add((frameName, frameId, dataLen, string.IsNullOrWhiteSpace(pub) ? null : pub, sigs));
+ }
+
+ // 兜底:若正则未匹配到帧(格式差异),退回到只提取 frameName 与 signals
+ if (list.Count == 0)
+ {
+ var fallbackFrameRegex = new Regex(@"(?s)(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?.*?)\}\s*;?", RegexOptions.Compiled);
+ foreach (Match fm in fallbackFrameRegex.Matches(framesBlock))
+ {
+ var frameName = fm.Groups["name"].Value;
+ var body = fm.Groups["body"].Value;
+ if (string.IsNullOrWhiteSpace(frameName) || string.IsNullOrWhiteSpace(body))
+ {
+ continue;
+ }
+
+ var sigs = new List();
+ foreach (Match sm in sigRegex.Matches(body))
+ {
+ var sigName = sm.Groups["sig"].Value;
+ if (string.IsNullOrWhiteSpace(sigName))
+ {
+ continue;
+ }
+ if (IsReservedKeyword(sigName))
+ {
+ continue;
+ }
+ sigs.Add(sigName);
+ }
+
+ list.Add((frameName, 0, 8, null, sigs));
+ }
+ }
+
+ return list;
+ }
+
+ private static bool TryParseIntAuto(string s, out int value)
+ {
+ s = s.Trim();
+ if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ {
+ return int.TryParse(s.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out value);
+ }
+ return int.TryParse(s, out value);
+ }
+
+ ///
+ /// 基于解析结果构建运行时索引:
+ /// - _framesByName/_framesByPid:用于发送与接收定位帧定义;
+ /// - ListLinLdfModel/_modelIndex:用于 UI 展示与实时值更新。
+ ///
+ /// 帧定义集合。
+ /// 信号位定义字典。
+ /// Master 节点名(可能为空)。
+ private void BuildRuntimeIndex(
+ List<(string FrameName, int FrameId, int DataLen, string? Publisher, List Signals)> frames,
+ Dictionary signals,
+ string? masterNodeName)
+ {
+ lock (_ldfLock)
+ {
+ _framesByName = new Dictionary(StringComparer.Ordinal);
+ _framesByPid = new Dictionary();
+ _unknownPidLastLogTicks.Clear();
+
+ foreach (var f in frames)
+ {
+ var pid = BuildProtectedId((byte)f.FrameId);
+ var isMasterFrame = !string.IsNullOrWhiteSpace(masterNodeName) && string.Equals(f.Publisher, masterNodeName, StringComparison.Ordinal);
+ var frame = new LinFrameDef
+ {
+ FrameName = f.FrameName,
+ FrameId = (byte)f.FrameId,
+ Pid = pid,
+ DataLen = f.DataLen,
+ Publisher = f.Publisher,
+ IsMasterFrame = isMasterFrame,
+ };
+
+ foreach (var sigName in f.Signals)
+ {
+ if (!signals.TryGetValue(sigName, out var def))
+ {
+ continue;
+ }
+
+ frame.Signals[sigName] = new LinSignalDef
+ {
+ SignalName = sigName,
+ StartBit = def.StartBit,
+ BitLength = def.BitLen,
+ IsSigned = false,
+ Factor = 1,
+ Offset = 0,
+ };
+ }
+
+ _framesByName[frame.FrameName] = frame;
+ // 兼容:
+ // - 有些设备/固件回传 pid 为“受保护 PID(含奇偶校验位)”
+ // - 有些回传 pid 为“裸 ID(0-63,仅 6bit)”
+ // 因此两种 key 都建立索引,确保接收解码稳定。
+ _framesByPid[frame.Pid] = frame;
+ _framesByPid[(byte)(frame.FrameId & 0x3F)] = frame;
+ }
+
+ ListLinLdfModel.Clear();
+ _modelIndex = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var frame in _framesByName.Values.OrderBy(a => a.FrameName, StringComparer.Ordinal))
+ {
+ foreach (var sig in frame.Signals.Values.OrderBy(a => a.SignalName, StringComparer.Ordinal))
+ {
+ var model = new LinLdfModel
+ {
+ MsgName = frame.FrameName,
+ SignalName = sig.SignalName,
+ Publisher = frame.Publisher,
+ IsMasterFrame = frame.IsMasterFrame ? "是" : "否",
+ SignalDesc = null,
+ SignalUnit = null,
+ Name = null,
+ IsSeletedInfo = 0,
+ };
+ ListLinLdfModel.Add(model);
+ _modelIndex[BuildMsgSigKey(frame.FrameName, sig.SignalName)] = model;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 处理一帧 LIN 接收数据:按 pid 匹配帧定义,按信号位定义解码并更新 UI 实时值。
+ /// 说明:
+ /// - 该方法可能在 Driver 的接收线程中被调用;
+ /// - 不做阻塞操作(如 IO/长时间锁持有),避免拖慢接收线程。
+ ///
+ /// 原始 LIN 接收帧。
+ private void HandleLinFrame(CanDrive.ZlgCan.ZlgLinRxFrame frame)
+ {
+ if (!LdfParserState)
+ {
+ return;
+ }
+
+ LinFrameDef? def;
+ lock (_ldfLock)
+ {
+ if (!_framesByPid.TryGetValue(frame.Pid, out def))
+ {
+ // pid 可能是 6bit ID 或 8bit PID,做回退匹配
+ var id6 = (byte)(frame.Pid & 0x3F);
+ if (!_framesByPid.TryGetValue(id6, out def))
+ {
+ var pidProtected = BuildProtectedId(id6);
+ _framesByPid.TryGetValue(pidProtected, out def);
+ }
+
+ if (def == null)
+ {
+ // 节流日志:每个 pid 至少间隔 5s 输出一次,避免刷屏
+ var now = Environment.TickCount64;
+ if (!_unknownPidLastLogTicks.TryGetValue(frame.Pid, out var last) || now - last >= 5000)
+ {
+ _unknownPidLastLogTicks[frame.Pid] = now;
+ _log.Debug($"ZLG LIN 收到未知 PID=0x{frame.Pid:X2}(id6=0x{id6:X2}),LDF 中未找到对应帧,已忽略。DataLen={frame.Data?.Length ?? 0}。");
+ }
+ return;
+ }
+ }
+ }
+
+ if (def == null)
+ {
+ return;
+ }
+
+ var bytes = frame.Data;
+ foreach (var sig in def.Signals.Values)
+ {
+ // LIN 信号位序按 Intel/Little-Endian 方式读取(与现有 CmdData 写入一致)。
+ var raw = ReadBitsIntel(bytes, sig.StartBit, sig.BitLength, sig.IsSigned);
+ UpdateSignalRtValue(def.FrameName, sig.SignalName, raw.ToString());
+ }
+ }
+
+ private void UpdateSignalRtValue(string msgName, string signalName, string value)
+ {
+ var key = BuildMsgSigKey(msgName, signalName);
+ LinLdfModel? model;
+ lock (_ldfLock)
+ {
+ _modelIndex.TryGetValue(key, out model);
+ }
+
+ if (model == null)
+ {
+ return;
+ }
+
+ // WPF:从接收线程更新 UI 绑定对象时,尽量切到 UI 线程。
+ var app = Application.Current;
+ if (app != null && !app.Dispatcher.CheckAccess())
+ {
+ app.Dispatcher.BeginInvoke(new Action(() => model.SignalRtValue = value));
+ return;
+ }
+
+ model.SignalRtValue = value;
+ }
+
+ ///
+ /// 发送一帧(按当前 CmdData 聚合编码)。
+ ///
+ /// 帧名。
+ public void SendOneFrameByMsgName(string msgName)
+ {
+ if (!OpenState)
+ {
+ throw new InvalidOperationException("设备未连接,无法发送。");
+ }
+
+ LinFrameDef? def;
+ lock (_ldfLock)
+ {
+ if (!_framesByName.TryGetValue(msgName, out def))
+ {
+ throw new InvalidOperationException($"未找到帧定义:{msgName}。请先解析 LDF。");
+ }
+ }
+
+ if (def == null)
+ {
+ return;
+ }
+
+ var data = new byte[8];
+
+ List cmds;
+ lock (_cmdLock)
+ {
+ cmds = CmdData.Where(a => string.Equals(a.MsgName, msgName, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(a.SignalName)).ToList();
+ }
+
+ foreach (var cmd in cmds)
+ {
+ if (cmd.SignalName == null)
+ {
+ continue;
+ }
+
+ if (!def.Signals.TryGetValue(cmd.SignalName, out var sigDef))
+ {
+ continue;
+ }
+
+ var raw = ConvertPhysicalToRaw(cmd.SignalCmdValue, sigDef);
+ WriteBitsIntel(data, sigDef.StartBit, sigDef.BitLength, raw);
+ }
+
+ // LIN 发送:复用 CAN Service 中的 Driver 发送接口。
+ // 说明:
+ // - channelIndex 当前固定为 0(项目约定)。
+ // - dataLen 取 LDF 帧定义中的长度,避免发送多余填充字节。
+ _zlgCanDriveService.Driver.TransmitLin(0, def.Pid, data.AsSpan(0, def.DataLen), 0);
+ }
+
+ private static ulong ConvertPhysicalToRaw(double physical, LinSignalDef sigDef)
+ {
+ var v = (physical - sigDef.Offset) / (Math.Abs(sigDef.Factor) < double.Epsilon ? 1 : sigDef.Factor);
+ if (sigDef.IsSigned)
+ {
+ var max = (1L << (sigDef.BitLength - 1)) - 1;
+ var min = -(1L << (sigDef.BitLength - 1));
+ var val = (long)Math.Round(v);
+ if (val > max) val = max;
+ if (val < min) val = min;
+ unchecked
+ {
+ return (ulong)val;
+ }
+ }
+
+ var umax = sigDef.BitLength >= 64 ? ulong.MaxValue : ((1UL << sigDef.BitLength) - 1UL);
+ var uval = (ulong)Math.Max(0, Math.Round(v));
+ if (uval > umax) uval = umax;
+ return uval;
+ }
+
+ private static void WriteBitsIntel(byte[] data, int startBit, int bitLen, ulong value)
+ {
+ if (data == null || data.Length == 0) return;
+ if (bitLen <= 0) return;
+
+ for (int i = 0; i < bitLen; i++)
+ {
+ var bitPos = startBit + i;
+ var byteIndex = bitPos / 8;
+ var bitIndex = bitPos % 8;
+ if (byteIndex < 0 || byteIndex >= data.Length) break;
+
+ var mask = (byte)(1 << bitIndex);
+ if (((value >> i) & 0x1) == 1)
+ {
+ data[byteIndex] |= mask;
+ }
+ else
+ {
+ data[byteIndex] &= (byte)~mask;
+ }
+ }
+ }
+
+ private static long ReadBitsIntel(byte[] data, int startBit, int bitLen, bool isSigned)
+ {
+ if (data == null || data.Length == 0) return 0;
+ if (bitLen <= 0) return 0;
+
+ ulong raw = 0;
+ for (int i = 0; i < bitLen; i++)
+ {
+ var bitPos = startBit + i;
+ var byteIndex = bitPos / 8;
+ var bitIndex = bitPos % 8;
+ if (byteIndex < 0 || byteIndex >= data.Length) break;
+ var bit = (data[byteIndex] >> bitIndex) & 0x1;
+ raw |= ((ulong)bit << i);
+ }
+
+ if (!isSigned)
+ {
+ return (long)raw;
+ }
+
+ if (bitLen >= 64)
+ {
+ unchecked
+ {
+ return (long)raw;
+ }
+ }
+
+ var signBit = 1UL << (bitLen - 1);
+ if ((raw & signBit) == 0)
+ {
+ return (long)raw;
+ }
+
+ var mask = (1UL << bitLen) - 1;
+ var twos = (~raw + 1) & mask;
+ return -(long)twos;
+ }
+
+ private static byte BuildProtectedId(byte frameId)
+ {
+ var id = (byte)(frameId & 0x3F);
+ var id0 = (id >> 0) & 1;
+ var id1 = (id >> 1) & 1;
+ var id2 = (id >> 2) & 1;
+ var id3 = (id >> 3) & 1;
+ var id4 = (id >> 4) & 1;
+ var id5 = (id >> 5) & 1;
+
+ var p0 = (id0 ^ id1 ^ id2 ^ id4) & 1;
+ var p1 = (~(id1 ^ id3 ^ id4 ^ id5)) & 1;
+
+ return (byte)(id | (p0 << 6) | (p1 << 7));
+ }
+
+ private static string BuildMsgSigKey(string msgName, string signalName)
+ {
+ return $"{msgName}\0{signalName}";
+ }
+
+ ///
+ /// 启动软件精确定时循环发送(按统一周期发送当前 CmdData 中出现的帧)。
+ ///
+ /// 周期(ms)。
+ public void StartPrecisionCycleSend(int cycleMs)
+ {
+ if (!OpenState)
+ {
+ throw new InvalidOperationException("设备未连接,无法启动循环发送。");
+ }
+
+ var ms = Math.Max(1, cycleMs);
+ List msgNames;
+ lock (_cmdLock)
+ {
+ msgNames = CmdData.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).Select(a => a.MsgName!).Distinct(StringComparer.Ordinal).ToList();
+ }
+
+ if (msgNames.Count == 0)
+ {
+ throw new InvalidOperationException("CmdData 为空,无法启动循环发送。");
+ }
+
+ var sw = Stopwatch.StartNew();
+ var freq = Stopwatch.Frequency;
+ var periodTicks = (long)(ms * (double)freq / 1000);
+ if (periodTicks <= 0) periodTicks = 1;
+
+ var items = msgNames.Select(n => new SoftwareScheduleItem(n, periodTicks)).ToArray();
+ var now = sw.ElapsedTicks;
+ for (int i = 0; i < items.Length; i++)
+ {
+ items[i].NextDueTicks = now + items[i].PeriodTicks;
+ }
+
+ lock (_scheduleLock)
+ {
+ StopSchedule();
+ _scheduleRunning = true;
+ _scheduleCts = new CancellationTokenSource();
+ var token = _scheduleCts.Token;
+
+ _scheduleTask = Task.Factory.StartNew(
+ () => RunSoftwareScheduler(sw, items, token, freq),
+ token,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+ }
+ }
+
+ ///
+ /// 停止循环发送。
+ ///
+ public void StopSchedule()
+ {
+ lock (_scheduleLock)
+ {
+ _scheduleRunning = false;
+ try
+ {
+ _scheduleCts?.Cancel();
+ }
+ catch
+ {
+ // ignore
+ }
+
+ _scheduleCts = null;
+ _scheduleTask = null;
+ }
+ }
+
+ private void RunSoftwareScheduler(Stopwatch sw, SoftwareScheduleItem[] items, CancellationToken token, long frequency)
+ {
+ // 说明:
+ // - 采用“下一次到期 ticks”驱动调度,避免 Thread.Sleep 累积误差。
+ // - waitMs>5 时先 WaitOne 较长时间以降低 CPU;临近触发时用 1ms 等待+SpinWait 降低抖动。
+ // - 对单帧发送失败做节流告警,避免循环中刷屏。
+ var warnIntervalTicks = frequency * 10;
+ var spin = new SpinWait();
+
+ while (!token.IsCancellationRequested)
+ {
+ if (!OpenState)
+ {
+ if (token.WaitHandle.WaitOne(50))
+ {
+ break;
+ }
+ continue;
+ }
+
+ var nowTicks = sw.ElapsedTicks;
+ long minDue = long.MaxValue;
+ for (var i = 0; i < items.Length; i++)
+ {
+ var d = items[i].NextDueTicks;
+ if (d < minDue)
+ {
+ minDue = d;
+ }
+ }
+
+ var waitTicks = minDue - nowTicks;
+ if (waitTicks > 0)
+ {
+ var waitMs = (int)(waitTicks * 1000 / frequency);
+ if (waitMs > 5)
+ {
+ var ms = Math.Min(waitMs - 1, 200);
+ if (token.WaitHandle.WaitOne(ms))
+ {
+ break;
+ }
+ continue;
+ }
+
+ if (waitMs > 1)
+ {
+ if (token.WaitHandle.WaitOne(1))
+ {
+ break;
+ }
+ continue;
+ }
+
+ spin.SpinOnce();
+ continue;
+ }
+
+ nowTicks = sw.ElapsedTicks;
+ for (var i = 0; i < items.Length; i++)
+ {
+ var it = items[i];
+ if (it.NextDueTicks > nowTicks)
+ {
+ continue;
+ }
+
+ try
+ {
+ SendOneFrameByMsgName(it.MsgName);
+ }
+ catch (Exception ex)
+ {
+ var t = sw.ElapsedTicks;
+ if (t - it.LastWarnTicks >= warnIntervalTicks)
+ {
+ it.LastWarnTicks = t;
+ _log.Warn($"LIN 软件循环发送失败:{it.MsgName},{ex.Message}");
+ }
+ }
+ finally
+ {
+ // 以“上一次计划时间 + 周期”推进 next,保证周期稳定;
+ // 若调度发生严重滞后(next 已过期),则直接以当前时间重新对齐。
+ var next = it.NextDueTicks + it.PeriodTicks;
+ if (next <= nowTicks)
+ {
+ next = nowTicks + it.PeriodTicks;
+ }
+ it.NextDueTicks = next;
+ }
+ }
+ }
+ }
+
+ private bool _ldfParserState;
+ ///
+ /// LDF 解析状态。
+ /// 说明:
+ /// - 当 成功解析并建立运行时索引后为 true;
+ /// - 若未解析/解析失败/关闭设备则为 false。
+ /// - LIN 收发编解码依赖该状态(未解析时接收帧会被忽略,发送会在找不到帧定义时抛出异常)。
+ ///
+ public bool LdfParserState
+ {
+ get { return _ldfParserState; }
+ private set { _ldfParserState = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// LDF 消息集合(UI 绑定)。
+ /// 说明:
+ /// - 该集合由 解析并构建,用于 UI 展示“帧-信号”全集与实时值;
+ /// - 未解析 LDF 时保持为空。
+ ///
+ public ObservableCollection ListLinLdfModel { get; private set; } = new ObservableCollection();
+
+ ///
+ /// 要发送的 LIN 指令集合(来源于配置程序+读写设置)。
+ ///
+ public List CmdData { get; } = new List();
+
+ ///
+ /// 是否启用调度发送(与 UI 的调度表使能语义对齐)。
+ ///
+ public bool SchEnable
+ {
+ get { return _zlgCanDriveService.Driver.SchEnable; }
+ set { _zlgCanDriveService.Driver.SchEnable = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 是否启用事件驱动发送。
+ ///
+ public bool IsCycleSend
+ {
+ get { return _zlgCanDriveService.Driver.IsCycleSend; }
+ set { _zlgCanDriveService.Driver.IsCycleSend = value; RaisePropertyChanged(); }
+ }
+
+ ///
+ /// 构造。
+ ///
+ /// 共享 CAN 服务。
+ /// 日志服务。
+ public ZlgLinDriveService(ZlgCanDriveService zlgCanDriveService, ILogService logService)
+ {
+ _zlgCanDriveService = zlgCanDriveService;
+ _log = logService;
+
+ // 透传 CAN Service/Driver 的状态指示灯属性(用于 UI 绑定)。
+ _zlgCanDriveService.PropertyChanged += (_, __) =>
+ {
+ RaisePropertyChanged(nameof(IsCycleRevice));
+ RaisePropertyChanged(nameof(IsSendOk));
+ RaisePropertyChanged(nameof(IsReviceOk));
+ };
+
+ // 订阅 LIN 帧接收事件:回调线程由 Driver 的接收线程决定(通常为后台线程)。
+ _zlgCanDriveService.Driver.LinFrameReceived += frame =>
+ {
+ try
+ {
+ HandleLinFrame(frame);
+ }
+ catch (Exception ex)
+ {
+ _log.Warn($"ZLG LIN 接收处理异常:{ex.Message}");
+ }
+ };
+ }
+
+ ///
+ /// 是否正在循环接收(透传底层 Driver 接收线程状态)。
+ ///
+ public bool IsCycleRevice
+ {
+ get { return _zlgCanDriveService.IsCycleRevice; }
+ }
+
+ ///
+ /// 发送状态指示(短时间保持 true 后回落,透传 Driver)。
+ ///
+ public bool IsSendOk
+ {
+ get { return _zlgCanDriveService.IsSendOk; }
+ }
+
+ ///
+ /// 接收状态指示(短时间保持 true 后回落,透传 Driver)。
+ ///
+ public bool IsReviceOk
+ {
+ get { return _zlgCanDriveService.IsReviceOk; }
+ }
+
+ ///
+ /// 开关接收线程(共享设备接收线程)。
+ ///
+ /// 是否启用。
+ public void SetReceiveEnabled(bool enable)
+ {
+ _zlgCanDriveService.SetReceiveEnabled(enable);
+ RaisePropertyChanged(nameof(IsCycleRevice));
+ }
+
+ ///
+ /// 下发调度表配置(系统层调度表,支持每帧不同周期)。
+ ///
+ /// 调度表配置集合。
+ public void SetScheduleConfigs(List configs)
+ {
+ lock (_scheduleLock)
+ {
+ // 仅缓存配置快照:真正启动调度时会再取一次快照,避免 UI 边编辑边启动导致的并发问题。
+ _linScheduleConfigs = configs ?? new List();
+ }
+ }
+
+ ///
+ /// 启动软件调度(按调度表中每帧的 Cycle 分别调度发送)。
+ /// 说明:ZLG LIN 当前未接入硬件调度表/auto_send,因此用软件精确定时实现。
+ ///
+ public void StartSchedule()
+ {
+ if (!OpenState)
+ {
+ throw new InvalidOperationException("设备未连接,无法启动调度发送。");
+ }
+
+ List snapshot;
+ lock (_scheduleLock)
+ {
+ snapshot = _linScheduleConfigs?.ToList() ?? new List();
+ }
+
+ // 仅使用“被激活的调度表”中的“被选中消息”
+ var msgItems = snapshot
+ .Where(a => a != null)
+ .Where(a => a.IsActive)
+ .Where(a => a.IsMsgActived)
+ .Where(a => !string.IsNullOrWhiteSpace(a.MsgName))
+ .GroupBy(a => a.MsgName!, StringComparer.Ordinal)
+ .Select(g =>
+ {
+ // 同一 MsgName 若出现多个周期,以最小周期为准(更安全)
+ var cycle = g.Select(x => x.Cycle).Where(x => x > 0).DefaultIfEmpty(100).Min();
+ return (MsgName: g.Key, Cycle: cycle);
+ })
+ .ToList();
+
+ if (msgItems.Count == 0)
+ {
+ throw new InvalidOperationException("调度表为空或未选择任何消息帧,无法启动调度。");
+ }
+
+ var sw = Stopwatch.StartNew();
+ var freq = Stopwatch.Frequency;
+
+ var items = msgItems
+ .Select(x =>
+ {
+ var ms = Math.Max(1, x.Cycle);
+ var ticks = (long)(ms * (double)freq / 1000);
+ if (ticks <= 0) ticks = 1;
+ return new SoftwareScheduleItem(x.MsgName, ticks);
+ })
+ .ToArray();
+
+ var now = sw.ElapsedTicks;
+ for (int i = 0; i < items.Length; i++)
+ {
+ items[i].NextDueTicks = now + items[i].PeriodTicks;
+ }
+
+ lock (_scheduleLock)
+ {
+ StopSchedule();
+ _scheduleRunning = true;
+ _scheduleCts = new CancellationTokenSource();
+ var token = _scheduleCts.Token;
+
+ _scheduleTask = Task.Factory.StartNew(
+ () => RunSoftwareScheduler(sw, items, token, freq),
+ token,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+ }
+ }
+
+ ///
+ /// 设置并订阅要发送的指令集合(事件驱动)。
+ ///
+ /// 指令集合。
+ public void LoadCmdDataToDrive(IEnumerable cmdData)
+ {
+ lock (_cmdLock)
+ {
+ if (CmdData.Count > 0)
+ {
+ foreach (var cmd in CmdData)
+ {
+ // 先退订旧对象事件,避免 UI/服务重复绑定导致“同一变化触发多次发送”。
+ cmd.LinCmdDataChangedHandler -= CmdData_LinCmdDataChangedHandler;
+ }
+ }
+
+ CmdData.Clear();
+ if (cmdData != null)
+ {
+ CmdData.AddRange(cmdData);
+ }
+
+ foreach (var cmd in CmdData)
+ {
+ // 订阅变化事件:用于“值变化增量发送”(由 + 共同控制)。
+ cmd.LinCmdDataChangedHandler += CmdData_LinCmdDataChangedHandler;
+ }
+ }
+ }
+
+ private void CmdData_LinCmdDataChangedHandler(object? sender, string msgName)
+ {
+ if (!OpenState)
+ {
+ return;
+ }
+
+ if (!IsCycleSend || !SchEnable)
+ {
+ // 语义:
+ // - IsCycleSend:是否允许“事件驱动发送”;
+ // - SchEnable:是否启用发送(与 UI 的调度表使能开关对齐)。
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(msgName))
+ {
+ return;
+ }
+
+ try
+ {
+ // 事件触发只发送当前 MsgName 对应帧,避免每次改动都全量发送。
+ SendOneFrameByMsgName(msgName.Trim());
+ }
+ catch (Exception ex)
+ {
+ _log.Warn($"ZLG LIN 事件驱动发送失败:{msgName},{ex.Message}");
+ }
+ }
+
+ ///
+ /// 初始化 LIN 配置信息(目前仅缓存)。
+ ///
+ /// 选中的配置。
+ public void InitLinConfig(CanLinConfigPro selectedLinConfigPro)
+ {
+ SelectedCanLinConfigPro = selectedLinConfigPro;
+ }
+
+ ///
+ /// 打开 LIN(共享设备句柄)。
+ ///
+ /// 设备索引。
+ /// 波特率。
+ /// 是否主节点。
+ public void StartLinDrive(uint deviceIndex, uint baudRate, bool isMaster)
+ {
+ if (OpenState)
+ {
+ return;
+ }
+
+ try
+ {
+ // 先确保设备打开(不影响 CAN 后续 Init)
+ _zlgCanDriveService.Driver.OpenDevice(deviceIndex);
+
+ // 初始化 LIN 通道
+ _zlgCanDriveService.Driver.OpenAndInitLin(0, new CanDrive.ZlgCan.ZlgLinChannelOptions
+ {
+ BaudRate = baudRate,
+ IsMaster = isMaster,
+ MaxLength = 8,
+ ChecksumMode = 3
+ });
+
+ // 统一由 CAN 服务侧启动接收线程(设备级 merge 接收可以同时收 CAN/LIN)
+ if (!_zlgCanDriveService.Driver.IsReceiving)
+ {
+ // mergeReceive=true:走合并接收,减少线程与句柄数量(与 CAN 侧默认策略一致)。
+ _zlgCanDriveService.Driver.StartReceiveLoop(mergeReceive: true, bufferFrames: 200);
+ }
+
+ OpenState = true;
+ LdfParserState = false;
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"ZLG LIN 打开失败:{ex.Message}");
+ OpenState = false;
+ throw;
+ }
+ }
+
+ ///
+ /// 关闭 LIN(共享设备句柄下,当前实现以 CloseDevice 为准:关闭将同时关闭 CAN/LIN)。
+ ///
+ public void CloseDevice()
+ {
+ // ZLG 的通道句柄都在 Driver 内部;当前 Close 会关闭所有通道,保持与旧系统“同一时刻只有一种驱动工作”的原则一致。
+ _zlgCanDriveService.CloseDevice();
+ OpenState = false;
+ LdfParserState = false;
+ }
+
+ ///
+ /// 加载并解析 LDF 文件,建立“帧/信号”的运行时索引,并生成 UI 可绑定的 集合。
+ /// 说明:
+ /// - 这里使用轻量解析器解析常见 LDF 结构(Frames/Signals/Nodes),目的是满足当前项目的 LIN 编解码与 UI 展示需求。
+ /// - 若 LDF 格式差异较大,解析可能失败并抛出异常;调用方(通常为 ViewModel)应捕获并提示。
+ ///
+ /// LDF 路径。
+ public ObservableCollection StartLdf(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentException("LDF 路径为空", nameof(path));
+ }
+
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"LDF 文件不存在:{path}", path);
+ }
+
+ try
+ {
+ // 读取文本:项目约定 UTF-8;若现场文件是 ANSI/GBK,可能需要外部统一转码。
+ var text = File.ReadAllText(path, Encoding.UTF8);
+
+ // 去除单行注释,简化解析
+ text = Regex.Replace(text, @"//.*?$", string.Empty, RegexOptions.Multiline);
+
+ // 解析并构建索引(在 _ldfLock 下更新全量索引,避免并发读到半更新状态)。
+ var (frames, signals, masterNodeName) = ParseLdf(text);
+ BuildRuntimeIndex(frames, signals, masterNodeName);
+
+ LdfParserState = true;
+ return ListLinLdfModel;
+ }
+ catch (Exception ex)
+ {
+ _log.Error($"ZLG LIN 解析 LDF 失败:{ex.Message}");
+ LdfParserState = false;
+ throw;
+ }
+ }
+
+ private static List ParseLdfFramesAndSignals(string ldfText)
+ {
+ // 说明:此解析器只用于生成“帧-信号全集池”,不做位宽/缩放等语义解析。
+ // 目标:尽可能容错地从 Frames 区域提取 FrameName 与其包含的 SignalName 列表。
+
+ var framesBlock = TryExtractNamedBlock(ldfText, "Frames");
+ if (string.IsNullOrWhiteSpace(framesBlock))
+ {
+ return new List();
+ }
+
+ var result = new List();
+ var exists = new HashSet(StringComparer.Ordinal);
+
+ // Frame 定义一般形式:FrameName : ... { ... }
+ // 这里以非贪婪匹配提取每个 Frame 的 body
+ var frameRegex = new Regex(@"(?s)(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?.*?)\}\s*;?", RegexOptions.Compiled);
+ var sigRegex = new Regex(@"(?m)^\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*[,;]", RegexOptions.Compiled);
+
+ foreach (Match fm in frameRegex.Matches(framesBlock))
+ {
+ var frameName = fm.Groups["name"].Value;
+ var body = fm.Groups["body"].Value;
+ if (string.IsNullOrWhiteSpace(frameName) || string.IsNullOrWhiteSpace(body))
+ {
+ continue;
+ }
+
+ foreach (Match sm in sigRegex.Matches(body))
+ {
+ var sigName = sm.Groups["sig"].Value;
+ if (string.IsNullOrWhiteSpace(sigName))
+ {
+ continue;
+ }
+
+ // 排除明显的关键字(避免误采集)
+ if (IsReservedKeyword(sigName))
+ {
+ continue;
+ }
+
+ var key = $"{frameName}:{sigName}";
+ if (!exists.Add(key))
+ {
+ continue;
+ }
+
+ result.Add(new LinLdfModel
+ {
+ MsgName = frameName,
+ SignalName = sigName,
+ Name = null,
+ SignalDesc = null,
+ SignalUnit = null,
+ IsSeletedInfo = 0,
+ });
+ }
+ }
+
+ return result;
+ }
+
+ private static string? TryExtractNamedBlock(string text, string blockName)
+ {
+ // 提取形如:blockName { ... } 的块内容(不包含外层大括号)。
+ // 基于括号深度扫描,避免正则在嵌套结构上失效。
+ var idx = CultureInvariantIndexOf(text, blockName);
+ if (idx < 0) return null;
+
+ var braceIdx = text.IndexOf('{', idx);
+ if (braceIdx < 0) return null;
+
+ int depth = 0;
+ for (int i = braceIdx; i < text.Length; i++)
+ {
+ var ch = text[i];
+ if (ch == '{') depth++;
+ else if (ch == '}')
+ {
+ depth--;
+ if (depth == 0)
+ {
+ return text.Substring(braceIdx + 1, i - braceIdx - 1);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static int CultureInvariantIndexOf(string text, string value)
+ {
+ return text.IndexOf(value, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsReservedKeyword(string token)
+ {
+ // LDF 常见关键字/区块名(用于降低误匹配概率)
+ switch (token)
+ {
+ case "Frames":
+ case "Signals":
+ case "Signal":
+ case "Nodes":
+ case "Master":
+ case "Slaves":
+ case "Diagnostic":
+ case "Diagnostics":
+ case "Checksum":
+ case "Event_triggered_frames":
+ case "Sporadic_frames":
+ case "Schedule_tables":
+ case "Node_attributes":
+ case "Node_composition":
+ case "LIN_protocol":
+ case "LIN_protocol_version":
+ case "LIN_speed":
+ case "Protocol_version":
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/CapMachine.Wpf/ViewModels/CANConfigViewModel.cs b/CapMachine.Wpf/ViewModels/CANConfigViewModel.cs
index c2643c6..cb0e6bc 100644
--- a/CapMachine.Wpf/ViewModels/CANConfigViewModel.cs
+++ b/CapMachine.Wpf/ViewModels/CANConfigViewModel.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using AutoMapper;
using CapMachine.Core;
using CapMachine.Model;
using CapMachine.Model.CANLIN;
@@ -152,12 +152,13 @@ namespace CapMachine.Wpf.ViewModels
}
///
- /// CAN/LIN配置变更事件处理
+ /// CAN/LIN 配置导入后刷新当前页面。
///
- ///
+ /// 事件消息。
private void CanLinConfigChangedEventCall(string msg)
{
- RefreshConfigList();
+ InitLoadCanConfigPro();
+ InitWriteRuleCbx();
}
///
@@ -948,18 +949,12 @@ namespace CapMachine.Wpf.ViewModels
///
private void SchEnableCmdCall(object par)
{
- var dd = SelectedCANConfigExdDto.SchEnable;
- var Result = (bool)par;
- if (Result)
+ if (SelectedCANConfigExdDto == null)
{
- CanDriveService.ToomossCanDrive.SchEnable = true;
-
+ return;
}
- else
- {
- CanDriveService.ToomossCanDrive.SchEnable = false;
- }
+ CanDriveService.ToomossCanDrive.SchEnable = SelectedCANConfigExdDto.SchEnable;
}
@@ -1362,22 +1357,22 @@ namespace CapMachine.Wpf.ViewModels
{ "SelectCanLinConfigProId",SelectCanLinConfigPro.Id},
}, (par) =>
- {
- if (par.Result == ButtonResult.OK)
- {
- ////程序名称
- ListCANScheduleConfigDto = par.Parameters.GetValue>("ReturnValue");
- //把更新后的最新值给当前的主集合中
- SelectCanLinConfigPro.CanScheduleConfigs = Mapper.Map>(ListCANScheduleConfigDto.ToList());
+ {
+ if (par.Result == ButtonResult.OK)
+ {
+ ////程序名称
+ ListCANScheduleConfigDto = par.Parameters.GetValue>("ReturnValue");
+ //把更新后的最新值给当前的主集合中
+ SelectCanLinConfigPro.CanScheduleConfigs = Mapper.Map>(ListCANScheduleConfigDto.ToList());
- }
- else if (par.Result == ButtonResult.Cancel)
- {
- //取消
+ }
+ else if (par.Result == ButtonResult.Cancel)
+ {
+ //取消
- }
+ }
- });
+ });
}
else
{
diff --git a/CapMachine.Wpf/ViewModels/CANFDConfigViewModel.cs b/CapMachine.Wpf/ViewModels/CANFDConfigViewModel.cs
index 7dbc90c..ea27dc2 100644
--- a/CapMachine.Wpf/ViewModels/CANFDConfigViewModel.cs
+++ b/CapMachine.Wpf/ViewModels/CANFDConfigViewModel.cs
@@ -172,12 +172,13 @@ namespace CapMachine.Wpf.ViewModels
}
///
- /// CAN/LIN配置变更事件处理
+ /// CAN/LIN 配置导入后刷新当前页面。
///
- ///
+ /// 事件消息。
private void CanLinConfigChangedEventCall(string msg)
{
- RefreshConfigList();
+ InitLoadCanConfigPro();
+ InitWriteRuleCbx();
}
///
@@ -212,7 +213,7 @@ namespace CapMachine.Wpf.ViewModels
canLinConfigPros = FreeSql.Select().Where(a => a.CANLINInfo == CANLIN.CANFD)
.Include(a => a.CANFdConfigExd)
.IncludeMany(a => a.CanLinConfigContents, then => then.Include(b => b.LogicRule))//,then=> then.Include(b=>b.LogicRule)
- //.IncludeMany(a => a.CanLinConfigContents)
+ .IncludeMany(a => a.CanFdScheduleConfigs)//
.ToList();
ListCanLinConfigPro = new ObservableCollection(canLinConfigPros);
@@ -271,6 +272,12 @@ namespace CapMachine.Wpf.ViewModels
ListReadCanLinRWConfigDto = new ObservableCollection();
}
+ //调度表配置信息
+ if (SelectCanLinConfigPro.CanFdScheduleConfigs != null && SelectCanLinConfigPro.CanFdScheduleConfigs.Count() > 0)
+ {
+ ListCANFdScheduleConfigDto = new ObservableCollection(Mapper.Map>(SelectCanLinConfigPro.CanFdScheduleConfigs));
+ }
+
//匹配选中的SelectCanLinConfigPro.CanLinConfigContents和ListCanDbcModel
MatchSeletedAndCanDbcModel();
}
@@ -519,6 +526,8 @@ namespace CapMachine.Wpf.ViewModels
CanFdDriveService.InitCanConfig(SelectCanLinConfigPro);
InitLoadCanConfigPro();
+
+ CanFdDriveService.ToomossCanFDDrive.LoadCmdDataToDrive(CanFdDriveService.CmdData);
}
else
{
@@ -636,6 +645,18 @@ namespace CapMachine.Wpf.ViewModels
SelectCanLinConfigProConfigName = SelectCanLinConfigPro.ConfigName;
+
+ //调度表配置信息
+ if (SelectCanLinConfigPro.CanFdScheduleConfigs != null && SelectCanLinConfigPro.CanFdScheduleConfigs.Count() > 0)
+ {
+ ListCANFdScheduleConfigDto = new ObservableCollection(Mapper.Map>(SelectCanLinConfigPro.CanFdScheduleConfigs));
+ }
+ else
+ {
+ ListCANFdScheduleConfigDto = new ObservableCollection();
+ }
+
+
return;
}
//先判断是否是正确的集合数据,防止DataGrid的数据源刷新导致的触发事件
@@ -705,6 +726,33 @@ namespace CapMachine.Wpf.ViewModels
#endregion
+
+ private DelegateCommand _btnTestPLC;
+ ///
+ /// 临时测试的方法
+ ///
+ public DelegateCommand btnTestPLC
+ {
+ set
+ {
+ _btnTestPLC = value;
+ }
+ get
+ {
+ if (_btnTestPLC == null)
+ {
+ _btnTestPLC = new DelegateCommand(() => btnTestPLCCall());
+ }
+ return _btnTestPLC;
+ }
+ }
+ //临时测试的方法
+ private void btnTestPLCCall()
+ {
+ CanFdDriveService.ToomossCanFDDrive.UpdateSchDataByCmdDataChanged();
+ }
+
+
#region Dbc操作
private ObservableCollection _ListCanDbcModel;
@@ -938,6 +986,43 @@ namespace CapMachine.Wpf.ViewModels
set { _ArbBaudRateCbxItems = value; RaisePropertyChanged(); }
}
+
+ private DelegateCommand