From 5be18ded2e5df7d70e04294282a4a76bfc2d10ff Mon Sep 17 00:00:00 2001 From: Tyrone CT Date: Mon, 2 Mar 2026 11:20:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CapMachine.Wpf/App.xaml.cs | 2 + CapMachine.Wpf/CanDrive/CanDbcModel.cs | 20 +- CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs | 185 ++- CapMachine.Wpf/CanDrive/ToomossCan.cs | 307 +++-- CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs | 18 + .../CanDrive/ZlgCan/ZlgCanFd200uDriver.cs | 931 ++++++++++++++- .../MapperProfile/LINScheduleConfigProfile.cs | 14 + CapMachine.Wpf/Services/ZlgCanDriveService.cs | 581 +++++++++- CapMachine.Wpf/Services/ZlgLinDriveService.cs | 1019 ++++++++++++++++- .../ViewModels/DialogLINSchConfigViewModel.cs | 42 +- .../DialogZlgCANFDSchConfigViewModel.cs | 358 ++++++ .../DialogZlgCANSchConfigViewModel.cs | 402 +++++++ .../DialogZlgCanLinRwConfigViewModel.cs | 151 +++ .../ViewModels/ZlgCanDriveConfigViewModel.cs | 479 +++++++- .../ViewModels/ZlgLinDriveConfigViewModel.cs | 690 ++++++++++- .../Views/DialogZlgCANFDSchConfigView.xaml | 143 +++ .../Views/DialogZlgCANFDSchConfigView.xaml.cs | 18 + .../Views/DialogZlgCANSchConfigView.xaml | 180 +++ .../Views/DialogZlgCANSchConfigView.xaml.cs | 28 + .../Views/DialogZlgCanLinRwConfigView.xaml | 42 + .../Views/ZlgLinDriveConfigView.xaml | 598 +++++++++- 21 files changed, 5984 insertions(+), 224 deletions(-) create mode 100644 CapMachine.Wpf/MapperProfile/LINScheduleConfigProfile.cs create mode 100644 CapMachine.Wpf/ViewModels/DialogZlgCANFDSchConfigViewModel.cs create mode 100644 CapMachine.Wpf/ViewModels/DialogZlgCANSchConfigViewModel.cs create mode 100644 CapMachine.Wpf/Views/DialogZlgCANFDSchConfigView.xaml create mode 100644 CapMachine.Wpf/Views/DialogZlgCANFDSchConfigView.xaml.cs create mode 100644 CapMachine.Wpf/Views/DialogZlgCANSchConfigView.xaml create mode 100644 CapMachine.Wpf/Views/DialogZlgCANSchConfigView.xaml.cs diff --git a/CapMachine.Wpf/App.xaml.cs b/CapMachine.Wpf/App.xaml.cs index 969b0f7..f0fba45 100644 --- a/CapMachine.Wpf/App.xaml.cs +++ b/CapMachine.Wpf/App.xaml.cs @@ -207,6 +207,8 @@ namespace CapMachine.Wpf containerRegistry.RegisterDialog(); containerRegistry.RegisterDialog(); + containerRegistry.RegisterDialog(); + containerRegistry.RegisterDialog(); containerRegistry.RegisterDialog(); containerRegistry.RegisterDialog(); diff --git a/CapMachine.Wpf/CanDrive/CanDbcModel.cs b/CapMachine.Wpf/CanDrive/CanDbcModel.cs index 62a8a2d..d2273a4 100644 --- a/CapMachine.Wpf/CanDrive/CanDbcModel.cs +++ b/CapMachine.Wpf/CanDrive/CanDbcModel.cs @@ -22,7 +22,25 @@ namespace CapMachine.Wpf.CanDrive /// 但不是所有的SignalName都会配置一个Name,只是需要时才会配置名称 /// 但是CanDbcModel集合会包括所有的SignalName名称的 /// - public string? Name { get; set; } + private string? _name; + + /// + /// 配置的中文名称:速度,转速限制,使能等常用的信息数据 + /// 但不是所有的SignalName都会配置一个Name,只是需要时才会配置名称 + /// 但是CanDbcModel集合会包括所有的SignalName名称的 + /// + public string? Name + { + get { return _name; } + set + { + if (_name != value) + { + _name = value; + RaisePropertyChanged(); + } + } + } /// /// 消息名称 diff --git a/CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs b/CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs index 39f7ee5..281b631 100644 --- a/CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs +++ b/CapMachine.Wpf/CanDrive/CanFD/ToomossCanFD.cs @@ -22,15 +22,35 @@ 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; /// - /// 实例化函数 + /// 构造函数。 /// + /// DI 容器。 + /// 日志服务。 public ToomossCanFD(IContainerProvider containerProvider, ILogService logService) { ContainerProvider = containerProvider; @@ -49,8 +69,19 @@ namespace CapMachine.Wpf.CanDrive public ILogService LoggerService { get; set; } /// - /// 开始CAN的驱动 + /// 启动 CANFD 驱动(设备准备流程)。 /// + /// + /// 执行顺序与 SDK 依赖一致: + /// 1) 校验 DLL 是否存在; + /// 2) 扫描设备并选取句柄; + /// 3) 打开设备; + /// 4) 读取设备信息; + /// 5) 获取 CANFD 初始化配置(仲裁/数据波特率); + /// 6) 初始化 CANFD。 + /// + /// 注意:该方法不解析 DBC;DBC 解析由 显式触发。 + /// public void StartCanDrive() { try @@ -70,8 +101,10 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 开始Dbc文件写入 + /// 解析并加载 DBC 文件。 /// + /// DBC 文件路径。 + /// 解析得到的信号列表(用于 UI 展示与实时值刷新)。 public ObservableCollection StartDbc(string DbcPath) { DBC_Parser(DbcPath); @@ -79,36 +112,44 @@ 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)。 /// + /// 仲裁波特率。 + /// 数据波特率。 + /// ISO CRC 使能。 + /// 终端电阻使能。 + /// 软件循环发送周期(ms)。 public void UpdateConfig(uint arbBaudRate, uint dataBaudRate, bool iSOEnable, bool resEnable, ushort sendCycle) { ArbBaudRate = arbBaudRate; @@ -139,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; } /// @@ -154,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 { @@ -176,7 +220,7 @@ namespace CapMachine.Wpf.CanDrive private bool _DbcParserState; /// - /// DBC解析的状态 + /// DBC 解析状态。 /// public bool DbcParserState { @@ -405,8 +449,12 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 发送CAN数据 + /// 单次发送:根据 CmdData 中的信号值构建 CANFD 报文并发送。 /// + /// 待发送的信号指令集合(按 MsgName 分组后组成帧)。 + /// + /// 注意:该方法会分配临时非托管内存 msgPt,并在结束前释放。 + /// public void SendCanMsg(List CmdData) { var GroupMsg = CmdData.GroupBy(x => x.MsgName); @@ -417,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赋值数据 @@ -450,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数据 @@ -492,8 +541,13 @@ namespace CapMachine.Wpf.CanDrive private static Task CycleUpdateCmdTask { get; set; } /// - /// 精确周期发送CAN数据 + /// 软件侧精确周期发送 CANFD 数据。 /// + /// + /// - 使用 tick 作为时间基准,减少 DateTime 抖动; + /// - 使用 Task.Delay + SpinWait 组合等待; + /// - 使用 支持外部取消。 + /// public void StartPrecisionCycleSendMsg() { // 创建取消标记源 用于控制任务的取消 允许在需要时通过取消令牌来优雅停止任务 @@ -580,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; @@ -598,7 +653,7 @@ namespace CapMachine.Wpf.CanDrive //通过DBC写入数据后生成CanMsg //将信号值填入CAN消息里面 - //释放申请的临时缓冲区 + // 释放非托管缓冲区(务必释放,否则长时间运行会造成内存泄漏) Marshal.FreeHGlobal(msgPtSend); //CanMsg[0].Flags = 5; @@ -682,8 +737,12 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 修改停止发送的方法 + /// 停止软件循环发送。 /// + /// + /// - 置 为 false,循环自然退出; + /// - 触发 取消,用于尽快唤醒 Delay 并退出。 + /// public void StopCycleSendMsg() { IsCycleSend = false; @@ -871,8 +930,11 @@ namespace CapMachine.Wpf.CanDrive private bool _SchEnable; /// - /// 调度表使能 + /// 调度表使能开关。 /// + /// + /// 该开关用于控制“硬件调度表发送”及“CmdData 变化时是否增量更新调度表”。 + /// public bool SchEnable { get { return _SchEnable; } @@ -977,8 +1039,16 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 开始调度表执行 + /// 启动硬件调度表(CANFD)。 /// + /// + /// 流程与 CAN 类似: + /// 1) 基于 分组构建帧数组; + /// 2) 使用 DBC 将信号值同步到帧; + /// 3) 将调度周期写入帧 ; + /// 4) 调用 CANFD_SetSchedule 下发; + /// 5) 调用 CANFD_StartSchedule 启动调度器 0。 + /// public void StartSchedule() { if (CmdData.Count() == 0) return; @@ -1006,13 +1076,13 @@ namespace CapMachine.Wpf.CanDrive DBCParserByFD.DBC_SyncValueToCANFDMsg(DBCHandle, new StringBuilder(itemMsg.Key), msgPtSend); //每个分组就是一个帧指令/消息数据 SchCanMsg[Index] = (USB2CANFD.CANFD_MSG)Marshal.PtrToStructure(msgPtSend, typeof(USB2CANFD.CANFD_MSG)); - //分配当前消息帧在集合中的位置信息到ListCANScheduleConfig中,方便实时更新时定位帧的位置 + // 将“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++; @@ -1047,7 +1117,7 @@ namespace CapMachine.Wpf.CanDrive return; } - //约定使用Index=0 1号调度器,因为同一个时刻只能有一个调度器工作,把所有的报文都要放到这个调度器中 + // 约定使用调度器索引 0:同一时刻只能运行一个调度器,且本项目把所有控制帧都集中在同一个调度器里 //CAN_MSG的TimeStamp就是这个报文发送的周期,由调度器协调 ret = USB2CANFD.CANFD_StartSchedule(DevHandle, WriteCANIndex, (byte)0, (byte)100, (byte)ListCANScheduleConfig.FirstOrDefault()!.OrderSend); if (ret == USB2CANFD.CANFD_SUCCESS) @@ -1094,8 +1164,11 @@ namespace CapMachine.Wpf.CanDrive /// - /// 停止调度表 + /// 停止硬件调度表。 /// + /// + /// 停止后设备侧将不再按周期自动发送。 + /// public void StopSchedule() { ret = USB2CANFD.CANFD_StopSchedule(DevHandle, WriteCANIndex);//启动第一个调度表,表里面的CAN帧并行发送 @@ -1121,10 +1194,12 @@ namespace CapMachine.Wpf.CanDrive private int CycleUpdateIndex = 0; /// - /// 加载要发送的数据 - /// 一般是激活后才注册事件 + /// 绑定/刷新驱动侧要发送的 CmdData,并自动维护“值变化事件”的订阅。 /// - /// + /// 新的指令集合。 + /// + /// 注意:必须先对旧集合取消订阅,否则会造成重复触发和内存泄漏。 + /// public void LoadCmdDataToDrive(List cmdData) { // Unsubscribe from events on the old CmdData items @@ -1145,25 +1220,27 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 指令数据发生变化执行方法 + /// 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 @@ -1179,7 +1256,7 @@ namespace CapMachine.Wpf.CanDrive lock (SchUpdateLock) { - //通过DBC进行对消息赋值 + // 重新构建每一帧的 Data,并覆盖更新到设备调度表 IntPtr msgPtSend = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CANFD.CANFD_MSG))); int CycleUpdateIndex = 0; //循环给MSG赋值数据,顺序是固定的,跟初始时设置是一样的 @@ -1200,7 +1277,7 @@ namespace CapMachine.Wpf.CanDrive //通过DBC写入数据后生成CanMsg //将信号值填入CAN消息里面 - //释放申请的临时缓冲区 + // 释放非托管缓冲 Marshal.FreeHGlobal(msgPtSend); //CAN_UpdateSchedule 官网解释 @@ -1211,7 +1288,7 @@ namespace CapMachine.Wpf.CanDrive //CAN_UpdateSchedule中的MsgIndex表示当前的调度器中的帧Index序号 //因为调度表中的帧集合和控制帧的集合和要更新的帧集合都是同一个集合SchCanMsg - //默认1号调度表,一个更新所有的帧数据 + // 默认调度表索引 0:一次性覆盖更新所有帧(MsgIndex=0, MsgNum=帧数) var ret = USB2CANFD.CANFD_UpdateSchedule(DevHandle, WriteCANIndex, (byte)0, (byte)(0), SchCanMsg, (byte)SchCanMsg.Count());//配置调度表,该函数耗时可能会比较长,但是只需要执行一次即可 if (ret == USB2CANFD.CANFD_SUCCESS) { @@ -1247,7 +1324,7 @@ namespace CapMachine.Wpf.CanDrive private bool _IsCycleRevice; /// - /// 是否循环接收数据 + /// 是否循环接收数据。 /// public bool IsCycleRevice { @@ -1258,7 +1335,7 @@ namespace CapMachine.Wpf.CanDrive private bool _IsCycleSend; /// - /// 是否循环发送数据 + /// 是否循环发送数据。 /// public bool IsCycleSend { @@ -1267,12 +1344,12 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 循环发送数据 + /// 软件循环发送周期(毫秒)。 /// public ushort SendCycle { get; set; } = 200; /// - /// 循环接受数据 + /// 接收轮询周期(毫秒)。 /// public ushort ReviceCycle { get; set; } = 200; @@ -1292,7 +1369,7 @@ namespace CapMachine.Wpf.CanDrive private bool _IsSendOk; /// - /// 发送报文是否OK + /// 最近一次发送是否成功。 /// public bool IsSendOk { @@ -1310,7 +1387,7 @@ namespace CapMachine.Wpf.CanDrive private bool _IsReviceOk; /// - /// 接收报文是否OK + /// 最近一次接收是否成功。 /// public bool IsReviceOk { @@ -1327,13 +1404,17 @@ 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 () => @@ -1403,7 +1484,7 @@ namespace CapMachine.Wpf.CanDrive //} } - //释放数据缓冲区,必须释放,否则程序运行一段时间后会报内存不足 + // 释放数据缓冲区,必须释放,否则程序运行一段时间后会报内存不足 Marshal.FreeHGlobal(msgPtRead); Thread.Sleep(10); @@ -1477,8 +1558,14 @@ namespace CapMachine.Wpf.CanDrive /// - /// 关闭设备 + /// 关闭设备并释放资源。 /// + /// + /// 释放顺序要点: + /// - 关闭设备并下置状态位; + /// - 若启用调度表则先停止; + /// - 等待接收任务退出(短等待,避免 UI 卡死)。 + /// public void CloseDevice() { //关闭设备 diff --git a/CapMachine.Wpf/CanDrive/ToomossCan.cs b/CapMachine.Wpf/CanDrive/ToomossCan.cs index c60b581..b46803d 100644 --- a/CapMachine.Wpf/CanDrive/ToomossCan.cs +++ b/CapMachine.Wpf/CanDrive/ToomossCan.cs @@ -13,15 +13,38 @@ using System.Text; namespace CapMachine.Wpf.CanDrive { /// - /// Toomoss CAN + /// 图莫斯(Toomoss)CAN 驱动封装。 /// + /// + /// 职责边界: + /// - 负责图莫斯 USB2XXX SDK 的设备扫描/打开/初始化/关闭; + /// - 负责 DBC 文件解析,并提供“信号值 <-> CAN 帧”的互转(通过 SDK 的 DBCParser 接口); + /// - 提供三类发送能力: + /// - 单次发送(); + /// - 软件侧精确周期发送(); + /// - 硬件侧调度表发送( / ,以及 增量更新); + /// - 提供接收能力:后台轮询读取报文并同步到 DBC 信号值()。 + /// + /// 线程与资源: + /// - 发送/接收/调度表更新均可能在后台线程运行; + /// - 本类会分配非托管内存(IntPtr 缓冲区)。必须通过 释放,且释放过程需要与接收线程互斥。 + /// + /// 重要约束: + /// - 为 0 表示 DBC 未解析成功,任何 DBC_* 调用都应视为不可用; + /// - / 表示 CAN 通道索引(0=CAN1,1=CAN2),当前实现约定读写通道一致。 + /// public class ToomossCan : BindableBase { + /// + /// Prism 容器,用于解析运行时依赖(如 )。 + /// private readonly IContainerProvider ContainerProvider; /// - /// 实例化函数 + /// 构造函数。 /// + /// DI 容器。 + /// 日志服务(外部注入,避免驱动类自己创建日志对象)。 public ToomossCan(IContainerProvider containerProvider, ILogService logService) { ContainerProvider = containerProvider; @@ -35,8 +58,19 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 开始CAN的驱动 + /// 启动 CAN 驱动(设备准备流程)。 /// + /// + /// 执行顺序与 SDK 依赖一致: + /// 1) 校验 DLL 是否存在; + /// 2) 扫描设备并选取句柄; + /// 3) 打开设备; + /// 4) 读取设备信息; + /// 5) 获取 CAN 初始化配置; + /// 6) 初始化 CAN。 + /// + /// 注意:该方法不解析 DBC;DBC 解析由 显式触发。 + /// public void StartCanDrive() { IsExistsDllFile(); @@ -50,18 +84,20 @@ namespace CapMachine.Wpf.CanDrive } /// - /// HighSpeedDataService 实例 + /// 高速数据服务:用于把接收到的原始报文写入高速记录/显示通道。 /// public HighSpeedDataService HighSpeedDataService { get; set; } /// - /// Logger 实例 + /// 日志服务。 /// public ILogService LoggerService { get; set; } /// - /// 开始Dbc文件写入 + /// 解析并加载 DBC 文件。 /// + /// DBC 文件路径。 + /// 解析得到的信号列表(用于 UI 展示与实时值刷新)。 public ObservableCollection StartDbc(string DbcPath) { DBC_Parser(DbcPath); @@ -77,38 +113,47 @@ namespace CapMachine.Wpf.CanDrive //} /// - /// Dbc消息集合 - /// 包括读取的实时值和数据 + /// DBC 解析得到的信号集合。 /// + /// + /// - 该集合的每个元素代表一个信号(Signal),包含所属报文名、信号名、单位/描述、实时值等。 + /// - 接收线程会持续更新 。 + /// public ObservableCollection ListCanDbcModel { get; set; } = new ObservableCollection(); #region 公共变量 /// - /// 设备固件信息 + /// 设备固件信息(由 SDK 填充)。 /// public USB_DEVICE.DEVICE_INFO DevInfo = new USB_DEVICE.DEVICE_INFO(); /// - /// CAN Config + /// CAN 初始化配置(由 读取或计算后用于 )。 /// USB2CAN.CAN_INIT_CONFIG CANConfig = new USB2CAN.CAN_INIT_CONFIG(); /// - /// DBC加载后获取的Handle - /// DBC Handle + /// DBC 解析成功后得到的句柄。 /// + /// + /// - 为 0 表示 DBC 未解析/解析失败; + /// - 发送与接收解析均依赖该句柄。 + /// public UInt64 DBCHandle { get; set; } /// - /// 扫描设备Handle集合 + /// 扫描到的设备句柄数组(SDK 输出)。 /// public Int32[] DevHandles { get; set; } = new Int32[20]; /// - /// 扫描设备Handle + /// 当前打开的设备句柄。 /// + /// + /// 该值由 选取并在 中打开。 + /// public Int32 DevHandle { get; set; } = 0; /// @@ -149,7 +194,7 @@ namespace CapMachine.Wpf.CanDrive private bool _OpenState; /// - /// 打开设备的状态 + /// 设备打开状态。 /// public bool OpenState { @@ -159,7 +204,7 @@ namespace CapMachine.Wpf.CanDrive private bool _DbcParserState; /// - /// DBC解析的状态 + /// DBC 解析状态。 /// public bool DbcParserState { @@ -174,6 +219,12 @@ namespace CapMachine.Wpf.CanDrive public Int32 ret { get; set; } + /// + /// USB2XXX SDK 的 DLL 文件名。 + /// + /// + /// 注意:此处仅校验 DLL 是否存在,实际的 DllImport 绑定仍由运行时加载机制决定。 + /// public string dllFilePath { get; set; } = "USB2XXX.dll"; @@ -185,10 +236,12 @@ namespace CapMachine.Wpf.CanDrive #endregion /// - /// ******************【1】********************* - /// 是否存在Dll文件 + /// 【1】检查 USB2XXX SDK 的 DLL 是否存在。 /// - /// + /// 存在返回 true,否则返回 false。 + /// + /// 这是最前置的防御性校验,避免后续调用直接触发 DllNotFoundException。 + /// public bool IsExistsDllFile() { if (!File.Exists(dllFilePath)) @@ -202,10 +255,13 @@ namespace CapMachine.Wpf.CanDrive } /// - /// ******************【2】********************* - /// 扫描查找设备,并将每个设备的唯一设备号存放到数组中,后面的函数需要用到 + /// 【2】扫描设备。 /// - /// + /// 扫描到设备返回 true,否则返回 false。 + /// + /// SDK 会把设备句柄写入 ,并返回设备数量。 + /// 当前实现默认选取第一个设备句柄作为 。 + /// public bool ScanDevice() { DevNum = USB_DEVICE.USB_ScanDevice(DevHandles); @@ -330,9 +386,14 @@ namespace CapMachine.Wpf.CanDrive } /// - /// ******************【7】********************* - /// DBC解析 + /// 【7】解析 DBC 文件并构建信号列表。 /// + /// DBC 文件路径。 + /// + /// 解析结果: + /// - :后续信号赋值/取值/同步帧均依赖它; + /// - :用于 UI 列表展示与实时值更新。 + /// public void DBC_Parser(string Path) { //解析DBC文件 @@ -390,9 +451,18 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 发送CAN数据 - /// 发送一次 + /// 单次发送:根据 CmdData 中的信号值构建 CAN 报文并发送。 /// + /// 待发送的信号指令集合(按 MsgName 分组后组成帧)。 + /// + /// 数据流: + /// 1) 按 MsgName 分组(每组代表同一个报文/帧); + /// 2) 对每个分组:逐信号调用 DBC_SetSignalValue 写入 DBC 缓存; + /// 3) 调用 DBC_SyncValueToCANMsg 生成帧; + /// 4) 调用 CAN_SendMsg 一次性批量发送。 + /// + /// 注意:此方法会分配临时非托管内存 msgPt,并在结束前释放。 + /// public void SendCanMsg(List CmdData) { var GroupMsg = CmdData.GroupBy(x => x.MsgName); @@ -403,6 +473,7 @@ namespace CapMachine.Wpf.CanDrive CanMsg[i].Data = new Byte[64]; } + // 非托管缓冲:SDK API 以指针形式写入帧结构体 IntPtr msgPt = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CAN.CAN_MSG))); int Index = 0; //循环给MSG赋值数据 @@ -425,7 +496,7 @@ namespace CapMachine.Wpf.CanDrive //通过DBC写入数据后生成CanMsg //将信号值填入CAN消息里面 - //释放申请的临时缓冲区 + // 释放临时非托管缓冲区,避免内存泄漏 Marshal.FreeHGlobal(msgPt); Console.WriteLine(""); //发送CAN数据 @@ -443,8 +514,11 @@ namespace CapMachine.Wpf.CanDrive private bool _IsCycleRevice; /// - /// 是否循环接收数据 + /// 是否循环接收数据。 /// + /// + /// 由 UI/Service 控制开关,接收任务 以该标记作为退出条件。 + /// public bool IsCycleRevice { get { return _IsCycleRevice; } @@ -454,8 +528,13 @@ namespace CapMachine.Wpf.CanDrive private bool _IsCycleSend; /// - /// 是否循环发送数据 + /// 是否循环发送数据。 /// + /// + /// 由 UI/Service 控制开关: + /// - 软件精确周期发送: / ; + /// - 调度表发送: / 。 + /// public bool IsCycleSend { get { return _IsCycleSend; } @@ -464,28 +543,41 @@ namespace CapMachine.Wpf.CanDrive /// - /// 循环发送数据 + /// 循环发送周期(毫秒)。 /// + /// + /// 该周期用于软件侧循环发送/接收 Task.Delay;硬件调度表发送周期由调度表项的 Cycle 决定。 + /// public ushort SendCycle { get; set; } = 100; /// - /// 循环接受数据 + /// 循环接收轮询周期(毫秒)。 /// public ushort ReviceCycle { get; set; } = 500; /// - /// CycleRevice 扫描Task + /// 循环接收任务。 /// + /// + /// 这里是 static:意味着同进程内多个实例会共享该任务引用(这在多设备场景需要特别注意)。 + /// private static Task CycleReviceTask { get; set; } /// - /// CycleSend 扫描Task + /// 循环发送任务。 /// + /// + /// 这里是 static:意味着同进程内多个实例会共享该任务引用(这在多设备场景需要特别注意)。 + /// private static Task CycleSendTask { get; set; } /// - /// 定时扫描更新数据 扫描Task + /// (旧方案)周期性刷新调度表任务。 /// + /// + /// 当前已引入“CmdData 变化事件 -> 增量更新调度表”的机制(见 ), + /// 该 Task 属于保留兼容路径。 + /// private static Task CycleUpdateCmdTask { get; set; } StringBuilder ValueSb = new StringBuilder(16); @@ -519,8 +611,12 @@ namespace CapMachine.Wpf.CanDrive private bool _IsSendOk; /// - /// 发送报文是否OK + /// 最近一次发送是否成功。 /// + /// + /// - 软件循环发送路径:由 CAN_SendMsg 返回值决定; + /// - 调度表更新路径:由 CAN_UpdateSchedule 返回值决定。 + /// public bool IsSendOk { get { return _IsSendOk; } @@ -537,8 +633,14 @@ namespace CapMachine.Wpf.CanDrive private bool _IsReviceOk; /// - /// 接收报文是否OK + /// 最近一次接收是否成功。 /// + /// + /// 该状态反映最近一次轮询读取 CAN_GetMsgWithSize 的结果: + /// - CanNum > 0:成功收到数据; + /// - CanNum == 0:本轮无数据; + /// - CanNum < 0:调用失败。 + /// public bool IsReviceOk { get { return _IsReviceOk; } @@ -553,13 +655,20 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 要发送的数据 + /// 当前激活的指令集合(由 Service/UI 下发)。 /// + /// + /// - 每个元素代表一个信号的写入指令; + /// - 发送时按 MsgName 分组,同一 MsgName 下的多个信号会被打包到同一帧。 + /// public List CmdData { get; set; } = new List(); /// - /// 循环发送数据 + /// (普通)软件循环发送:以 为周期构建帧并发送。 /// + /// + /// 该方法属于早期实现,精度与线程抖动相关;若追求更高精度建议使用 。 + /// public void StartCycleSendMsg() { CycleSendTask = Task.Run(async () => @@ -865,8 +974,16 @@ namespace CapMachine.Wpf.CanDrive private static readonly Random _random = new Random(); /// - /// 精确周期发送CAN数据 + /// 软件侧精确周期发送 CAN 数据。 /// + /// + /// 关键点: + /// - 使用 tick 作为时间基准,减少 DateTime 带来的抖动; + /// - 使用 Task.Delay + SpinWait 组合:大等待让出 CPU,小等待自旋微调; + /// - 使用 支持外部取消。 + /// + /// 注意:调用方需要先将 置为 true;停止时请调用 。 + /// public void StartPrecisionCycleSendMsg() { // 创建取消标记源 用于控制任务的取消 允许在需要时通过取消令牌来优雅停止任务 @@ -953,6 +1070,7 @@ namespace CapMachine.Wpf.CanDrive CanMsg[i].Data = new Byte[64]; } + // 发送构帧临时缓冲:每轮申请/释放,避免与其他线程共享同一指针导致并发问题 IntPtr msgPtSend = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CAN.CAN_MSG))); int Index = 0; //循环给MSG赋值数据 @@ -970,7 +1088,7 @@ namespace CapMachine.Wpf.CanDrive //通过DBC写入数据后生成CanMsg //将信号值填入CAN消息里面 - //释放申请的临时缓冲区 + // 释放非托管缓冲区(务必释放,否则长时间运行会造成内存泄漏) Marshal.FreeHGlobal(msgPtSend); //发送CAN数据 @@ -1032,8 +1150,12 @@ namespace CapMachine.Wpf.CanDrive /// - /// 修改停止发送的方法 + /// 停止软件循环发送。 /// + /// + /// - 置 为 false,循环自然退出; + /// - 触发 取消,用于尽快唤醒 Delay 并退出。 + /// public void StopCycleSendMsg() { IsCycleSend = false; @@ -1046,8 +1168,11 @@ namespace CapMachine.Wpf.CanDrive private bool _SchEnable; /// - /// 调度表使能 + /// 调度表使能开关。 /// + /// + /// 该开关用于控制“硬件调度表发送”及“CmdData 变化时是否增量更新调度表”。 + /// public bool SchEnable { get { return _SchEnable; } @@ -1060,13 +1185,21 @@ namespace CapMachine.Wpf.CanDrive /// - /// 调度表的发送的报文数据 + /// 当前已下发到设备调度表的帧缓存。 /// + /// + /// - 在 中构建,并通过 CAN_SetSchedule 下发; + /// - 在 中重建并通过 CAN_UpdateSchedule 覆盖更新。 + /// private USB2CAN.CAN_MSG[] SchCanMsg { get; set; } /// - /// 依据消息的分组 + /// CmdData 按 MsgName 分组后的视图。 /// + /// + /// 该分组的遍历顺序决定了 中每个帧的 Index, + /// 因而要求:调度表启动后,更新时必须按相同顺序遍历(当前实现保持一致)。 + /// private IEnumerable> GroupMsg { get; set; } /// @@ -1111,7 +1244,7 @@ namespace CapMachine.Wpf.CanDrive /// public void UpdateValue() { - //通过DBC进行对消息赋值 + // 通过 DBC 生成每一帧的原始数据(Data/DataLen/ID 等) IntPtr msgPtSend = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CAN.CAN_MSG))); int Index = 0; //循环给MSG赋值数据 @@ -1152,8 +1285,18 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 开始调度表执行 + /// 启动硬件调度表。 /// + /// + /// 流程: + /// 1) 基于当前 分组构建帧数组 ; + /// 2) 使用 DBC 将信号值同步到帧; + /// 3) 根据 把周期写入帧的 TimeStamp 字段; + /// 4) 调用 CAN_SetSchedule 下发; + /// 5) 调用 CAN_StartSchedule 启动(当前约定只启动调度器 0)。 + /// + /// 注意:本实现把所有报文都放入调度表 0,因此 UI 上的 SchTabIndex 实际未被使用。 + /// public void StartSchedule() { if (CmdData.Count() == 0) return; @@ -1181,13 +1324,13 @@ namespace CapMachine.Wpf.CanDrive CAN_DBCParser.DBC_SyncValueToCANMsg(DBCHandle, new StringBuilder(itemMsg.Key), msgPtSend); //每个分组就是一个帧指令/消息数据 SchCanMsg[Index] = (USB2CAN.CAN_MSG)Marshal.PtrToStructure(msgPtSend, typeof(USB2CAN.CAN_MSG)); - //分配当前消息帧在集合中的位置信息到ListCANScheduleConfig中,方便实时更新时定位帧的位置 + // 将“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++; @@ -1222,7 +1365,7 @@ namespace CapMachine.Wpf.CanDrive return; } - //约定使用Index=0 1号调度器,因为同一个时刻只能有一个调度器工作,把所有的报文都要放到这个调度器中 + // 约定使用调度器索引 0:同一时刻只能运行一个调度器,且本项目把所有控制帧都集中在同一个调度器里 //CAN_MSG的TimeStamp就是这个报文发送的周期,由调度器协调 ret = USB2CAN.CAN_StartSchedule(DevHandle, WriteCANIndex, (byte)0, (byte)100, (byte)ListCANScheduleConfig.FirstOrDefault()!.OrderSend); if (ret == USB2CAN.CAN_SUCCESS) @@ -1269,8 +1412,11 @@ namespace CapMachine.Wpf.CanDrive /// - /// 停止调度表 + /// 停止硬件调度表。 /// + /// + /// 停止后设备侧将不再按周期自动发送。 + /// public void StopSchedule() { ret = USB2CAN.CAN_StopSchedule(DevHandle, WriteCANIndex);//启动第一个调度表,表里面的CAN帧并行发送 @@ -1389,10 +1535,15 @@ namespace CapMachine.Wpf.CanDrive /// - /// 加载要发送的数据 - /// 一般是激活后才注册事件 + /// 绑定/刷新驱动侧要发送的 CmdData,并自动维护“值变化事件”的订阅。 /// - /// + /// 新的指令集合。 + /// + /// 目的:当某个信号值变化时(CanCmdData.SignalCmdValue setter 触发), + /// 可以通过 触发调度表增量刷新。 + /// + /// 注意:必须先对旧集合取消订阅,否则会造成重复触发和内存泄漏。 + /// public void LoadCmdDataToDrive(List cmdData) { // Unsubscribe from events on the old CmdData items @@ -1413,25 +1564,37 @@ namespace CapMachine.Wpf.CanDrive } /// - /// 指令数据发生变化执行方法 + /// CmdData 中任意信号值变化事件处理。 /// - /// - /// + /// 事件源。 + /// 变化的 MsgName(约定为报文名)。 + /// + /// 当前实现采取“全量覆盖更新调度表”的方式(UpdateSchedule),并未按 MsgName 做局部更新。 + /// private void CmdData_CanCmdDataChangedHandler(object? sender, string e) { UpdateSchDataByCmdDataChanged(); } /// - /// 指令数据发生变化执行更新调度表锁 + /// 调度表更新互斥锁。 /// + /// + /// - CmdData 变化事件可能高频触发; + /// - 同时 UpdateSchedule 调用耗时且涉及非托管内存与 DBC 操作,必须串行化。 + /// private readonly object SchUpdateLock = new object(); /// - /// 指令数据发生变化执行方法 + /// 当 CmdData 中信号值变化时,增量刷新调度表的帧数据。 /// - /// - /// + /// + /// 触发条件: 同时为 true。 + /// + /// 当前实现策略: + /// - 为保证简单与一致性:每次变化都重建所有帧并全量调用 CAN_UpdateSchedule 覆盖更新; + /// - 若后续性能成为瓶颈,可根据变化的 MsgName 实现“按帧更新(MsgIndex + MsgNum=1)”。 + /// private void UpdateSchDataByCmdDataChanged() { try @@ -1447,7 +1610,7 @@ namespace CapMachine.Wpf.CanDrive lock (SchUpdateLock) { - //通过DBC进行对消息赋值 + // 重新构建每一帧的 Data,并覆盖更新到设备调度表 IntPtr msgPtSend = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(USB2CAN.CAN_MSG))); int CycleUpdateIndex = 0; //循环给MSG赋值数据,顺序是固定的,跟初始时设置是一样的 @@ -1468,7 +1631,7 @@ namespace CapMachine.Wpf.CanDrive //通过DBC写入数据后生成CanMsg //将信号值填入CAN消息里面 - //释放申请的临时缓冲区 + // 释放非托管缓冲 Marshal.FreeHGlobal(msgPtSend); //CAN_UpdateSchedule 官网解释 @@ -1479,7 +1642,7 @@ namespace CapMachine.Wpf.CanDrive //CAN_UpdateSchedule中的MsgIndex表示当前的调度器中的帧Index序号 //因为调度表中的帧集合和控制帧的集合和要更新的帧集合都是同一个集合SchCanMsg - //默认1号调度表,一个更新所有的帧数据 + // 默认调度表索引 0:一次性覆盖更新所有帧(MsgIndex=0, MsgNum=帧数) var ret = USB2CAN.CAN_UpdateSchedule(DevHandle, WriteCANIndex, (byte)0, (byte)(0), SchCanMsg, (byte)SchCanMsg.Count());//配置调度表,该函数耗时可能会比较长,但是只需要执行一次即可 if (ret == USB2CAN.CAN_SUCCESS) { @@ -1511,8 +1674,14 @@ namespace CapMachine.Wpf.CanDrive #endregion /// - /// 循环获取CAN消息 + /// 启动后台循环接收 CAN 报文,并同步到 DBC 信号实时值。 /// + /// + /// 关键点: + /// - 使用 CAN_GetMsgWithSize 从设备内部 FIFO 拉取报文; + /// - 使用 作为重用缓冲,避免每轮申请/释放非托管内存导致碎片与性能问题; + /// - 使用 互斥,避免指针并发释放。 + /// public void StartCycleReviceCanMsg() { // 防止重复启动,若已有任务在运行则直接返回 @@ -1694,8 +1863,16 @@ namespace CapMachine.Wpf.CanDrive /// - /// 关闭设备 + /// 关闭设备并释放资源。 /// + /// + /// 释放顺序要点: + /// - 先关闭设备并下置状态位; + /// - 若启用调度表则先停止; + /// - 停止定时器/周期发送; + /// - 等待接收任务退出(短等待,避免 UI 卡死); + /// - 在互斥锁内释放接收缓冲指针,避免接收线程仍在使用。 + /// public void CloseDevice() { //关闭设备 diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs index 967cd09..90e2d76 100644 --- a/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs +++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZLGAPI.cs @@ -2,10 +2,22 @@ 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; @@ -82,12 +94,16 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan #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); @@ -230,6 +246,8 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan #endregion #region 结构体 + // SDK 结构体定义:用于与原生内存布局一致地传参/收参。 + // 说明:字段注释多数来自 SDK 示例或历史代码;这里主要保证布局与 Pack 正确。 [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ZCAN_DEVICE_INFO { diff --git a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs index 5a571ad..219e2ff 100644 --- a/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs +++ b/CapMachine.Wpf/CanDrive/ZlgCan/ZlgCanFd200uDriver.cs @@ -18,30 +18,178 @@ 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; /// @@ -54,13 +202,13 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } /// - /// 安全关闭设备与通道(需在 _sync 锁内调用)。 + /// 设备打开状态的 backing 字段。 /// /// - /// - 该方法会尽力调用 Reset/Close,并在异常时记录日志且继续执行释放流程; - /// - 该方法不会停止接收线程,调用方应优先 StopReceiveLoop。 + /// 对外通过 暴露。 + /// - 仅在驱动内部状态切换时写入(例如 /); + /// - 使用 通知 UI 刷新。 /// - private bool _openState; /// /// 设备打开状态。 @@ -71,6 +219,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan private set { _openState = value; RaisePropertyChanged(); } } + /// + /// 接收线程运行状态的 backing 字段。 + /// private bool _isReceiving; /// /// 是否正在接收。 @@ -81,6 +232,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan private set { _isReceiving = value; RaisePropertyChanged(); } } + /// + /// 发送指示灯状态的 backing 字段。 + /// private bool _isSendOk; /// /// 最近是否发生过“发送成功”(用于 UI 指示灯)。 @@ -91,6 +245,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan private set { _isSendOk = value; RaisePropertyChanged(); } } + /// + /// 接收指示灯状态的 backing 字段。 + /// private bool _isReviceOk; /// /// 最近是否发生过“接收成功”(用于 UI 指示灯)。 @@ -101,9 +258,28 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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); @@ -118,6 +294,10 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan }); } + /// + /// 将 置为 true 并在一段时间后自动恢复为 false。 + /// + /// 保持 true 的时间(毫秒)。 private void MarkReviceOk(int holdMs = 800) { var token = Interlocked.Increment(ref _reviceOkToken); @@ -135,13 +315,24 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// CAN/CANFD 原始帧接收事件。 /// + /// + /// - 该事件在接收线程中触发(非 UI 线程); + /// - 订阅方若需要更新 UI,请自行切回 UI 线程; + /// - 即使启用了 DBC 解码,该事件仍会原样抛出原始帧,便于外部做抓包/记录。 + /// public event Action? CanFrameReceived; /// /// LIN 原始帧接收事件。 /// + /// + /// 同 :在接收线程触发,订阅方自行处理线程切换。 + /// public event Action? LinFrameReceived; + /// + /// 的 backing 字段。 + /// private bool _dbcDecodeEnabled; /// /// 是否启用 DBC 解码更新(接收帧触发)。 @@ -152,6 +343,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan set { _dbcDecodeEnabled = value; RaisePropertyChanged(); } } + /// + /// 的 backing 字段。 + /// private bool _isCycleSend; /// /// 是否启用“事件驱动发送”。 @@ -166,6 +360,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan set { _isCycleSend = value; RaisePropertyChanged(); } } + /// + /// 的 backing 字段。 + /// private bool _schEnable; /// /// 发送使能(与 UI 的调度表使能语义对齐)。 @@ -180,6 +377,14 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 打开设备(不初始化 CAN/LIN 通道)。 /// /// 设备索引(通常 0)。 + /// + /// 说明: + /// - 该方法只负责获取 device_handle(根句柄);通道初始化由 / 完成; + /// - 若 OpenDevice 失败,为了便于现场定位,代码会输出: + /// - 输出目录 DLL 依赖诊断(是否缺失、位数是否匹配、版本信息); + /// - 当前进程架构与已加载模块路径; + /// - 该方法是幂等的:若设备已打开则直接返回。 + /// public void OpenDevice(uint deviceIndex) { ThrowIfDisposed(); @@ -194,6 +399,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan EnsureNativeDllExists("zlgcan.dll"); + // 设备类型固定为 USBCANFD-200U。若后续扩展其它型号,应把 deviceType 抽为配置参数。 var deviceType = ZLGCAN.ZCAN_USBCANFD_200U; try @@ -383,6 +589,16 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 设备索引(通常 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(); @@ -426,6 +642,11 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// LIN 通道索引(通常 0)。 /// LIN 初始化参数。 + /// + /// 说明: + /// - LIN 与 CAN/CANFD 共用同一个 device_handle; + /// - 本工程在 UI/Service 层通常会做 CAN/LIN 互斥校验,避免同时占用同一硬件。 + /// public void OpenAndInitLin(uint linIndex, ZlgLinChannelOptions options) { ThrowIfDisposed(); @@ -480,6 +701,16 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// 是否启用合并接收(ZCAN_ReceiveData)。建议与通道初始化时的 EnableMergeReceive 保持一致。 /// 接收缓冲最大帧数(合并接收固定数组大小,过小会丢帧)。 + /// + /// 线程与回调说明: + /// - 本方法会创建一个后台 Task 持续读取驱动接收缓冲区; + /// - 收到帧后会触发 / (在后台线程触发,非 UI 线程); + /// - 若 为 true,会在接收线程中对帧做 DBC 解码并更新 集合。 + /// + /// 注意: + /// - 若你在 UI 层直接绑定 集合并要求线程安全更新,请在订阅事件或更新集合时切回 UI 线程。 + /// + /// 设备未打开或接收线程已启动。 public void StartReceiveLoop(bool mergeReceive, int bufferFrames = 100) { ThrowIfDisposed(); @@ -535,6 +766,13 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 是否合并加载。 /// 协议类型。 /// CanDbcModel 集合。 + /// + /// 成功加载后会: + /// - 重新创建并替换内部 DBC 数据库对象; + /// - 构建用于 UI 展示的 列表; + /// - 建立 SignalName 索引(用于接收线程解码时快速定位模型并更新值); + /// - 默认开启 ,使接收线程能驱动实时值更新。 + /// public ObservableCollection StartDbc(string dbcPath, bool enableAsyncAnalyse = true, bool merge = false, byte protocolType = ZDBC.PROTOCOL_OTHER) { ThrowIfDisposed(); @@ -560,12 +798,24 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 指令集合。 /// 发送通道(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) @@ -574,6 +824,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } + // 保存最新 CmdData 快照:后续 + // - 事件驱动发送会按 MsgName 聚合信号并编码; + // - 启动硬件调度表也会从 CmdData/DBC 生成原始帧。 _cmdData = cmdData; _cmdSendChannelIndex = channelIndex; _cmdSendFrameType = frameType; @@ -593,6 +846,13 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// 发送方。 /// 发生变化的消息名称。 + /// + /// 发送策略: + /// - 调度表(硬件 auto_send)运行时:优先覆盖更新设备定时发送列表中的对应条目(见 ); + /// - 否则:立即编码并发送 1 帧(见 )。 + /// + /// 说明:该方法由 的变更事件触发,可能不在 UI 线程。 + /// private void CmdData_CanCmdDataChangedHandler(object? sender, string msgName) { try @@ -600,6 +860,17 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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) @@ -608,6 +879,269 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } + /// + /// 按 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 中取出信号值,编码并发送一帧。 /// @@ -622,17 +1156,21 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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; } @@ -659,12 +1197,18 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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; } @@ -685,6 +1229,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan { lock (_sync) { + // StopReceiveLoop 与 StartReceiveLoop/Open/Close 同受 _sync 保护: + // - 防止接收线程创建/销毁与通道 Reset/Close 并发; + // - 保证 _recvCts/_recvTask 的可见性与一致性。 if (_recvCts == null || _recvTask == null) { return; @@ -701,6 +1248,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } finally { + // 无论等待是否超时,都要清理引用并释放 CTS,避免下次 StartReceiveLoop 认为线程仍在运行。 _recvCts.Dispose(); _recvCts = null; _recvTask = null; @@ -772,6 +1320,13 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 数据(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(); @@ -830,6 +1385,15 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 周期(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(); @@ -870,10 +1434,76 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } + /// + /// 设置设备定时发送(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(); @@ -891,6 +1521,14 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 清空通道定时发送任务(clear_auto_send)。 /// /// 通道索引。 + /// + /// 对应 ZLG 驱动 SetValue 路径:{channelIndex}/clear_auto_send。 + /// + /// 说明: + /// - clear 会清空硬件列表并停止该通道的定时发送; + /// - 若你要重新配置调度表,建议先 clear,再从 index=0 重新写入并 apply。 + /// + /// 设备未打开或底层 SetValue 失败。 public void ClearAutoSend(int channelIndex) { ThrowIfDisposed(); @@ -908,6 +1546,15 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 配置 LIN 发布表。 /// /// 发布配置集合。 + /// + /// - LIN 发布表用于“周期性/按条件发布”LIN 数据(由设备侧完成具体发送时机,取决于驱动能力与配置)。 + /// - 本方法会将托管结构体数组拷贝到非托管内存,再调用底层 API。 + /// - 若集合为空则直接返回。 + /// + /// 线程说明: + /// - 该方法为同步调用;建议在设备已初始化 LIN 通道后调用。 + /// + /// LIN 未初始化或底层配置失败。 public void SetLinPublish(IEnumerable publishCfg) { ThrowIfDisposed(); @@ -955,6 +1602,12 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 配置 LIN 订阅表。 /// /// 订阅配置集合。 + /// + /// - LIN 订阅表用于配置希望接收/关注的 LIN 帧。 + /// - 本方法会将托管结构体数组拷贝到非托管内存,再调用底层 API。 + /// - 若集合为空则直接返回。 + /// + /// LIN 未初始化或底层配置失败。 public void SetLinSubscribe(IEnumerable subscribeCfg) { ThrowIfDisposed(); @@ -992,19 +1645,115 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } + /// + /// 发送一帧 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) @@ -1024,6 +1773,11 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } /// + /// + /// 遵循 .NET Dispose 语义: + /// - 多次调用是安全的(通过 做幂等保护); + /// - Dispose 内部会调用 释放驱动句柄、停止接收线程并释放 DBC 资源。 + /// public void Dispose() { if (_disposed) return; @@ -1031,6 +1785,8 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan try { + // 说明:Dispose 不抛异常,避免在 using/终结路径打断上层释放流程。 + // Close 内部已经尽量记录日志并吞掉部分关闭异常,保证释放流程走完。 Close(); } catch @@ -1039,12 +1795,21 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } + /// + /// 关闭设备与通道句柄(不加锁版本)。 + /// + /// + /// 该方法假定调用方已经持有 : + /// - 用于 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) { @@ -1067,6 +1832,11 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan { 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) { @@ -1111,6 +1881,16 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// 通道索引。 /// 通道初始化参数。 + /// + /// 该方法主要完成: + /// - 设置仲裁域/数据域波特率; + /// - 设置终端电阻与总线利用率统计开关; + /// - 调用 ZCAN_InitCAN 获取通道句柄并保存到 。 + /// + /// 注意: + /// - 这里仅负责 Init,不负责 Start;Start 由 完成。 + /// + /// 任一 SetValue/InitCAN 失败。 private void InitCanChannelInternal(int chnIdx, ZlgCanFdChannelOptions options) { @@ -1186,6 +1966,11 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// 通道索引。 /// 通道句柄。 + /// + /// 调用前置条件: + /// - 已成功执行并产生通道句柄。 + /// + /// 通道句柄为空或启动失败。 private IntPtr StartCanChannelInternal(int chnIdx) { @@ -1209,6 +1994,8 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// 通道索引。 /// 通道句柄。 + /// 通道索引超范围。 + /// 通道未初始化。 private IntPtr GetCanChannelHandleOrThrow(int channelIndex) { @@ -1231,11 +2018,19 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// 取消令牌。 /// 每次接收的最大帧数。 + /// + /// - 该模式分别调用: + /// - 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)); @@ -1251,6 +2046,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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) { @@ -1259,6 +2057,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan { 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); } } @@ -1271,6 +2072,8 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan { 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); } } @@ -1296,6 +2099,8 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } + // 轻量节流:避免无数据时空转占满 CPU。 + // 注意:这会引入 10ms 级额外接收延迟;若有更高实时性需求,应改为事件/阻塞式等待(SDK 支持时)。 Thread.Sleep(10); } } @@ -1311,6 +2116,13 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// /// 取消令牌。 /// 接收缓存容量。 + /// + /// - merge 接收模式通常用于高吞吐场景:由驱动统一将各类数据封装为 DataObj; + /// - 本方法根据 dataType 分流: + /// - 1:CAN/CANFD + /// - 4:LIN + /// - 触发事件仍通过 / 。 + /// private void ReceiveLoop_Merge(CancellationToken token, int bufferFrames) { @@ -1318,6 +2130,10 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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); @@ -1326,6 +2142,8 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan { while (!token.IsCancellationRequested) { + // 2:device 级合并接收缓冲区(ZLG SDK 约定)。 + // 注意:合并接收开关需要在通道初始化时通过 0/set_device_recv_merge 开启(见 OpenAndInitCan)。 uint recvNum = ZLGCAN.ZCAN_GetReceiveNum(_deviceHandle, 2); if (recvNum == 0) { @@ -1347,6 +2165,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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; @@ -1355,6 +2176,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan break; case 4: + // dataType=4:LIN(ZCANLINData)。 if (obj.data == null || obj.data.Length < linSize) { break; @@ -1366,6 +2188,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } + // 微节流:在持续高流量下仍可能较占 CPU;该 sleep 也会影响“批次间”延迟。 Thread.Sleep(1); } } @@ -1387,6 +2210,14 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 原始数据缓冲区。 /// 数据长度。 /// 是否为发送回显(Tx)。 + /// + /// 事件派发与 DBC 解码: + /// - 始终先触发 抛出原始帧; + /// - 再依据 决定是否进行 DBC 解码并刷新 。 + /// + /// 发送/接收指示: + /// - 若 为 true,调用 ;否则调用 。 + /// private void RaiseCanFrame(int channel, ulong timestamp, bool isCanFd, uint canId, byte[] data, byte dlc, bool isTx) { @@ -1401,14 +2232,14 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan CanFrameReceived?.Invoke(new ZlgCanRxFrame(channel, isCanFd, canId, bytes, timestamp, isTx)); + // 说明: + // - UI 的“接收”指示灯通常表达“收到了总线帧/驱动回调在工作”,因此包括 Tx echo 在内的任意帧均点亮接收; + // - Tx echo 仍额外点亮“发送”指示灯。 + MarkReviceOk(); if (isTx) { MarkSendOk(); } - else - { - MarkReviceOk(); - } if (DbcDecodeEnabled) { @@ -1430,6 +2261,10 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan /// 原始数据缓冲区。 /// 数据长度。 /// 方向(由设备回传)。 + /// + /// - LIN 数据长度最大 8 字节,本方法会按 截断并复制; + /// - 事件在接收线程触发,订阅方若更新 UI,请自行切换线程。 + /// private void RaiseLinFrame(int channel, ulong timestamp, byte pid, byte[] data, byte datalen, byte dir) { @@ -1465,12 +2300,19 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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)) { @@ -1509,6 +2351,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan return null; } + // e_lfanew:DOS Header + 0x3C,指向 PE Header 起始偏移。 fs.Position = 0x3C; var peOffset = br.ReadInt32(); if (peOffset <= 0 || peOffset > fs.Length - 6) @@ -1516,6 +2359,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan return null; } + // PE signature:"PE\0\0" => 0x00004550。 fs.Position = peOffset; var peSig = br.ReadUInt32(); if (peSig != 0x00004550) @@ -1523,6 +2367,10 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan return null; } + // IMAGE_FILE_HEADER.Machine: + // - 0x014c = x86 + // - 0x8664 = x64 + // - 0xAA64 = arm64 var machine = br.ReadUInt16(); return machine switch { @@ -1538,6 +2386,15 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan } } + /// + /// 构建 DBC 解码更新用的索引表。 + /// + /// 所有信号模型。 + /// 键为 (MsgName, SignalName) 的组合键,值为对应的 + /// + /// - 接收线程解码得到 (msgName, signalName) 后,可 O(1) 定位到 UI 模型进行更新; + /// - 组合键使用 ,避免字符串拼接歧义。 + /// private static Dictionary BuildDbcModelIndex(IEnumerable models) { var dict = new Dictionary(StringComparer.Ordinal); @@ -1555,11 +2412,31 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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; @@ -1635,11 +2512,34 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan 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; } } @@ -1665,9 +2565,24 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan Dir = dir; } + /// + /// 通道号。 + /// public int Channel { get; } + + /// + /// PID。 + /// public byte Pid { get; } + + /// + /// 数据区(已按实际长度截断拷贝)。 + /// public byte[] Data { get; } + + /// + /// 时间戳(微秒)。 + /// public ulong TimestampUs { get; } /// diff --git a/CapMachine.Wpf/MapperProfile/LINScheduleConfigProfile.cs b/CapMachine.Wpf/MapperProfile/LINScheduleConfigProfile.cs new file mode 100644 index 0000000..e4ad8a0 --- /dev/null +++ b/CapMachine.Wpf/MapperProfile/LINScheduleConfigProfile.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using CapMachine.Model.CANLIN; +using CapMachine.Wpf.Dtos; + +namespace CapMachine.Wpf.MapperProfile +{ + public class LINScheduleConfigProfile : Profile + { + public LINScheduleConfigProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/CapMachine.Wpf/Services/ZlgCanDriveService.cs b/CapMachine.Wpf/Services/ZlgCanDriveService.cs index 4dda130..d1a9c09 100644 --- a/CapMachine.Wpf/Services/ZlgCanDriveService.cs +++ b/CapMachine.Wpf/Services/ZlgCanDriveService.cs @@ -7,6 +7,7 @@ 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; @@ -16,8 +17,75 @@ 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; /// @@ -28,37 +96,69 @@ namespace CapMachine.Wpf.Services /// /// 当前选中的配置程序(沿用原有 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; } @@ -68,6 +168,11 @@ namespace CapMachine.Wpf.Services /// /// 发送使能(与 UI 的调度表使能语义对齐)。 /// + /// + /// 该属性透传到 : + /// - true:允许“事件驱动发送”(CmdData 变化时发送/覆盖 auto_send) + /// - false:仅允许手动发送或软件调度任务主动调用发送接口 + /// public bool SchEnable { get { return Driver.SchEnable; } @@ -77,6 +182,11 @@ namespace CapMachine.Wpf.Services /// /// 是否启用事件驱动发送。 /// + /// + /// 该属性透传到 。 + /// - true 且 为 true 时:CmdData 变化会触发增量发送/覆盖更新; + /// - false:CmdData 变化不会触发任何发送(由外部显式调用发送方法)。 + /// public bool IsCycleSend { get { return Driver.IsCycleSend; } @@ -86,6 +196,9 @@ namespace CapMachine.Wpf.Services /// /// 是否正在循环接收(对齐 Toomoss:IsCycleRevice)。 /// + /// + /// 该属性为只读透传:接收线程由 创建。 + /// public bool IsCycleRevice { get { return Driver.IsReceiving; } @@ -94,6 +207,9 @@ namespace CapMachine.Wpf.Services /// /// 最近是否发送成功(用于 UI 指示)。 /// + /// + /// 该标志由 Driver 的发送调用或 Tx 回显事件触发,短时间保持 true 后自动回落。 + /// public bool IsSendOk { get { return Driver.IsSendOk; } @@ -102,6 +218,9 @@ namespace CapMachine.Wpf.Services /// /// 最近是否接收成功(用于 UI 指示)。 /// + /// + /// 该标志由 Driver 接收线程收到 Rx 帧触发,短时间保持 true 后自动回落。 + /// public bool IsReviceOk { get { return Driver.IsReviceOk; } @@ -110,23 +229,92 @@ namespace CapMachine.Wpf.Services /// /// 要发送的 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; @@ -148,14 +336,38 @@ namespace CapMachine.Wpf.Services /// 初始化 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) { - var find = ListCanDbcModel.FindFirst(a => a.SignalName == item.SignalName); + 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; @@ -172,6 +384,11 @@ namespace CapMachine.Wpf.Services /// 终端电阻使能。 /// 是否合并接收。 /// 合并接收缓冲帧数。 + /// + /// 该方法仅更新服务层保存的“配置快照”,不会立即触发驱动重连。 + /// - 生效时机:下一次调用 打开设备并初始化通道。 + /// - 当前实现仅使用通道0配置。 + /// public void UpdateConfig(uint deviceIndex, uint arbBaudRate, uint dataBaudRate, bool resEnable, bool mergeReceive = false, int mergeReceiveBufferFrames = 100) { _deviceIndex = deviceIndex; @@ -185,6 +402,14 @@ namespace CapMachine.Wpf.Services /// /// 打开 CAN/CANFD(按 Mode)。 /// + /// + /// - 该方法是幂等的:若 为 true,则直接返回; + /// - 成功打开后会初始化通道并启动接收线程(见 )。 + /// + /// 注意: + /// - 本方法不会自动加载 DBC,也不会自动注入 CmdData; + /// - 建议调用顺序:UpdateConfig -> StartCanDrive -> StartDbc -> LoadCmdDataToDrive。 + /// public void StartCanDrive() { if (OpenState) @@ -201,6 +426,12 @@ namespace CapMachine.Wpf.Services /// 使能/停止循环接收。 /// /// true=启动接收线程,false=停止接收线程。 + /// + /// 说明: + /// - 接收线程属于 Driver 内部资源; + /// - 若开启合并接收,接收线程会按通道0配置决定采用 merge 或 per-channel 接收。 + /// + /// 设备未连接。 public void SetReceiveEnabled(bool enable) { if (!OpenState) @@ -229,6 +460,12 @@ namespace CapMachine.Wpf.Services /// /// 关闭设备(会同时关闭共享的 LIN 通道)。 /// + /// + /// Close 语义强调“彻底停止后台活动”: + /// - 停止调度表:取消软件调度任务,并清理硬件 auto_send 列表(避免设备侧继续发); + /// - 停止事件驱动发送:将 置为 false; + /// - 停止接收: 内部会停止接收线程并释放句柄。 + /// public void CloseDevice() { try @@ -248,6 +485,15 @@ namespace CapMachine.Wpf.Services } } + /// + /// 设置调度表配置(CAN)。 + /// + /// 调度表配置项。 + /// + /// 该方法仅保存调度表条目快照,不会立即启动发送。 + /// - 真正启动由 触发。 + /// - MsgName 为空的条目会被过滤。 + /// public void SetScheduleConfigs(IEnumerable configs) { var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List(); @@ -259,6 +505,13 @@ namespace CapMachine.Wpf.Services } } + /// + /// 设置调度表配置(CANFD)。 + /// + /// 调度表配置项。 + /// + /// 语义同 CAN 版本 ,差异仅在于配置来源 DTO 类型。 + /// public void SetScheduleConfigs(IEnumerable configs) { var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List(); @@ -270,6 +523,20 @@ namespace CapMachine.Wpf.Services } } + /// + /// 启动调度表(硬件定时发送列表 auto_send)。 + /// + /// + /// 本方法选择“硬件调度表”实现: + /// - 将调度表条目转换为设备 auto_send 列表并 apply,使设备按周期自动发送; + /// - 运行时若 CmdData 对应信号值变化,Driver 会覆盖更新对应 index 的帧数据。 + /// + /// 前置条件: + /// - 设备已连接( true); + /// - 调度表不为空; + /// - DBC 已加载且 CmdData 可编码(否则对应 MsgName 会被 Driver 跳过并记录 warn)。 + /// + /// 设备未连接或调度表为空。 public void StartSchedule() { if (!OpenState) @@ -280,6 +547,9 @@ namespace CapMachine.Wpf.Services List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items; lock (_scheduleLock) { + // 取快照: + // - 避免启动过程中与 UI 线程并发修改调度表集合; + // - 后续 Driver.StartAutoSendSchedule 会在内部再次排序并建立 MsgName->index 映射。 items = _scheduleItems.ToList(); } @@ -288,10 +558,40 @@ namespace CapMachine.Wpf.Services throw new InvalidOperationException("调度表为空,无法启动调度表。"); } + // 周立功:调度表=硬件定时发送列表(auto_send) + // - 周期精度高,由设备侧时钟保障 + // - PC 仍可同时调用发送接口发送其它数据 _scheduleUseConfigItems = true; - StartSoftwareScheduler(items); + + // 启动前先停止已有任务(软件调度 + 硬件 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) @@ -306,11 +606,26 @@ namespace CapMachine.Wpf.Services 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; @@ -328,18 +643,54 @@ namespace CapMachine.Wpf.Services 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(); @@ -353,77 +704,190 @@ namespace CapMachine.Wpf.Services // 统一:软件调度开启后,等同“循环发送开启” IsCycleSend = true; - _scheduleTask = Task.Run(async () => + var frequency = Stopwatch.Frequency; + long ToTicks(int ms) { - var token = cts.Token; + var safeMs = Math.Max(1, ms); + // 使用 long 避免溢出;周期最小为 1 tick。 + var ticks = (long)(safeMs * (double)frequency / 1000d); + return Math.Max(1, ticks); + } - // next due time for each msg - var now = DateTime.UtcNow; - var due = new Dictionary(StringComparer.Ordinal); - var cycle = new Dictionary(StringComparer.Ordinal); - var order = new Dictionary(StringComparer.Ordinal); - foreach (var it in items) + // 只在启动时做一次去重/排序;循环内不分配。 + 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)) { - if (!due.ContainsKey(it.MsgName)) + 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)) { - due[it.MsgName] = now; - cycle[it.MsgName] = Math.Max(1, it.CycleMs); - order[it.MsgName] = it.OrderSend; + 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; } } - while (!token.IsCancellationRequested) + var waitTicks = minDue - nowTicks; + if (waitTicks > 0) { - if (!OpenState) + // 混合等待: + // - 剩余时间较长:WaitOne(<=200ms) 降低 CPU 占用 + // - 临近到期:1ms 级等待 + 短自旋提升边界精度 + var waitMs = (int)(waitTicks * 1000 / frequency); + if (waitMs > 5) { - await Task.Delay(50, token).ConfigureAwait(false); + var ms = Math.Min(waitMs - 1, 200); + if (token.WaitHandle.WaitOne(ms)) + { + break; + } continue; } - var utcNow = DateTime.UtcNow; - var minDue = due.Values.Min(); - var delay = minDue - utcNow; - if (delay > TimeSpan.Zero) + if (waitMs > 1) { - var ms = (int)Math.Min(delay.TotalMilliseconds, 200); - await Task.Delay(ms, token).ConfigureAwait(false); + if (token.WaitHandle.WaitOne(1)) + { + break; + } continue; } - // due messages - var ready = due.Where(kv => kv.Value <= utcNow).Select(kv => kv.Key).ToList(); - if (ready.Count == 0) + spin.SpinOnce(); + continue; + } + + // 到期发送: + // - 同一条消息“错过多个周期”时,只发送一次(避免 burst),然后尽快恢复节拍。 + nowTicks = sw.ElapsedTicks; + for (var i = 0; i < items.Length; i++) + { + var it = items[i]; + if (it.NextDueTicks > nowTicks) { - await Task.Delay(1, token).ConfigureAwait(false); continue; } - // 顺序/并行:这里只决定同一 tick 内的发送顺序(并行模式仍按字典序依次发) - ready.Sort(StringComparer.Ordinal); - foreach (var msg in ready) + try { - if (token.IsCancellationRequested) break; - 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) { - Driver.SendOneMsgByCmdData(msg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD); + it.LastWarnTicks = t; + _log.Warn($"软件循环发送失败:{it.MsgName},{ex.Message}"); } - catch (Exception ex) + } + finally + { + // 漂移控制:正常情况下按“计划时间”累加;若落后过多,则跳到 now+period(只补发一次)。 + var next = it.NextDueTicks + it.PeriodTicks; + if (next <= nowTicks) { - _log.Warn($"调度表发送失败:{msg},{ex.Message}"); - } - finally - { - due[msg] = DateTime.UtcNow.AddMilliseconds(cycle[msg]); + next = nowTicks + it.PeriodTicks; } + it.NextDueTicks = next; } } - }, cts.Token); + } } /// /// 加载 DBC。 /// /// DBC 路径。 + /// DBC 解析得到的信号集合。 + /// + /// - DBC 加载成功后会更新 ; + /// - 后续发送(调度表/事件驱动/手动发送)都依赖 DBC 编码能力。 + /// + /// 设备未连接。 public ObservableCollection StartDbc(string path) { if (!OpenState) @@ -431,7 +895,14 @@ namespace CapMachine.Wpf.Services 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; } @@ -440,13 +911,28 @@ namespace CapMachine.Wpf.Services /// 加载要发送的数据(订阅数据变化事件)。 /// /// 指令数据集合。 + /// + /// 该方法会: + /// - 更新本服务的 集合; + /// - 从集合中选取“转速”项缓存到 (用于兼容旧的手动发送接口); + /// - 调用 注入 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)) @@ -455,6 +941,9 @@ namespace CapMachine.Wpf.Services } } + // 将 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); } @@ -462,6 +951,12 @@ namespace CapMachine.Wpf.Services /// 手动发送(目前对齐原服务:仅发送转速)。 /// /// 转速。 + /// + /// 行为说明: + /// - 优先将 speedData 写入 的 SignalCmdValue; + /// - 若当前未启用事件驱动发送( 为 false),则主动触发一次编码发送, + /// 以维持旧版本“点击发送即下发”的体验。 + /// public void SendMsgToCanDrive(double speedData) { if (!OpenState) @@ -471,6 +966,9 @@ namespace CapMachine.Wpf.Services if (SpeedCanCmdData != null) { + // 先更新写入值: + // - 若当前启用了事件驱动发送(IsCycleSend && SchEnable),该赋值本身会触发 Driver 的增量发送/覆盖更新; + // - 若未启用事件驱动发送,下方会主动补发一次,保证“点一下就发”的旧行为。 SpeedCanCmdData.SignalCmdValue = speedData; } else @@ -481,6 +979,9 @@ namespace CapMachine.Wpf.Services // 若未启用事件驱动发送,则这里主动发送一次(与旧行为兼容) if (!IsCycleSend || !SchEnable) { + // 历史行为:选取 CmdData 的第一条 MsgName 进行发送。 + // - 由于本方法本质上是“转速”单信号写入,严格来说应按 SpeedCanCmdData.MsgName 发送; + // - 但为避免改动业务逻辑,这里保持旧逻辑,仅通过注释明确其局限。 var firstMsg = CmdData.FirstOrDefault()?.MsgName; if (!string.IsNullOrWhiteSpace(firstMsg)) { diff --git a/CapMachine.Wpf/Services/ZlgLinDriveService.cs b/CapMachine.Wpf/Services/ZlgLinDriveService.cs index 1773bcb..7a33a3a 100644 --- a/CapMachine.Wpf/Services/ZlgLinDriveService.cs +++ b/CapMachine.Wpf/Services/ZlgLinDriveService.cs @@ -1,24 +1,143 @@ 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 模型)。 /// @@ -34,9 +153,682 @@ namespace CapMachine.Wpf.Services 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 解析状态(ZLG LIN 暂未接入 LDF,固定为 false)。 + /// LDF 解析状态。 + /// 说明: + /// - 当 成功解析并建立运行时索引后为 true; + /// - 若未解析/解析失败/关闭设备则为 false。 + /// - LIN 收发编解码依赖该状态(未解析时接收帧会被忽略,发送会在找不到帧定义时抛出异常)。 /// public bool LdfParserState { @@ -45,10 +837,18 @@ namespace CapMachine.Wpf.Services } /// - /// LDF 消息集合(UI 绑定),ZLG LIN 暂未接入 LDF,默认空。 + /// LDF 消息集合(UI 绑定)。 + /// 说明: + /// - 该集合由 解析并构建,用于 UI 展示“帧-信号”全集与实时值; + /// - 未解析 LDF 时保持为空。 /// public ObservableCollection ListLinLdfModel { get; private set; } = new ObservableCollection(); + /// + /// 要发送的 LIN 指令集合(来源于配置程序+读写设置)。 + /// + public List CmdData { get; } = new List(); + /// /// 是否启用调度发送(与 UI 的调度表使能语义对齐)。 /// @@ -77,12 +877,208 @@ namespace CapMachine.Wpf.Services _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 => { - // 未来接入 LDF 后在这里做 Sync/Decode;当前仅保留事件链路,不做假解析。 + 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 配置信息(目前仅缓存)。 /// @@ -122,6 +1118,7 @@ namespace CapMachine.Wpf.Services // 统一由 CAN 服务侧启动接收线程(设备级 merge 接收可以同时收 CAN/LIN) if (!_zlgCanDriveService.Driver.IsReceiving) { + // mergeReceive=true:走合并接收,减少线程与句柄数量(与 CAN 侧默认策略一致)。 _zlgCanDriveService.Driver.StartReceiveLoop(mergeReceive: true, bufferFrames: 200); } @@ -148,7 +1145,10 @@ namespace CapMachine.Wpf.Services } /// - /// 加载 LDF(ZLG LIN 暂未接入 LDF 解析 DLL,因此此处明确失败,不返回模拟数据)。 + /// 加载并解析 LDF 文件,建立“帧/信号”的运行时索引,并生成 UI 可绑定的 集合。 + /// 说明: + /// - 这里使用轻量解析器解析常见 LDF 结构(Frames/Signals/Nodes),目的是满足当前项目的 LIN 编解码与 UI 展示需求。 + /// - 若 LDF 格式差异较大,解析可能失败并抛出异常;调用方(通常为 ViewModel)应捕获并提示。 /// /// LDF 路径。 public ObservableCollection StartLdf(string path) @@ -165,18 +1165,15 @@ namespace CapMachine.Wpf.Services try { + // 读取文本:项目约定 UTF-8;若现场文件是 ANSI/GBK,可能需要外部统一转码。 var text = File.ReadAllText(path, Encoding.UTF8); // 去除单行注释,简化解析 text = Regex.Replace(text, @"//.*?$", string.Empty, RegexOptions.Multiline); - var models = ParseLdfFramesAndSignals(text); - - ListLinLdfModel.Clear(); - foreach (var item in models) - { - ListLinLdfModel.Add(item); - } + // 解析并构建索引(在 _ldfLock 下更新全量索引,避免并发读到半更新状态)。 + var (frames, signals, masterNodeName) = ParseLdf(text); + BuildRuntimeIndex(frames, signals, masterNodeName); LdfParserState = true; return ListLinLdfModel; diff --git a/CapMachine.Wpf/ViewModels/DialogLINSchConfigViewModel.cs b/CapMachine.Wpf/ViewModels/DialogLINSchConfigViewModel.cs index 3557829..b8cb0f4 100644 --- a/CapMachine.Wpf/ViewModels/DialogLINSchConfigViewModel.cs +++ b/CapMachine.Wpf/ViewModels/DialogLINSchConfigViewModel.cs @@ -15,12 +15,11 @@ namespace CapMachine.Wpf.ViewModels { public class DialogLINSchConfigViewModel : DialogViewModel { - public DialogLINSchConfigViewModel(IFreeSql freeSql, IMapper mapper, LinDriveService linDriveService) + public DialogLINSchConfigViewModel(IFreeSql freeSql, IMapper mapper) { Title = "调度表 LIN 配置"; FreeSql = freeSql; Mapper = mapper; - LinDriveService = linDriveService; //默认只能用1号调度器 SchTabIndexCbxItems = new ObservableCollection() @@ -54,7 +53,6 @@ namespace CapMachine.Wpf.ViewModels public IFreeSql FreeSql { get; } public IMapper Mapper { get; } - public LinDriveService LinDriveService { get; } private string name; /// @@ -415,17 +413,45 @@ namespace CapMachine.Wpf.ViewModels /// private void LoadSch() { - var ListLINScheduleConfig = LinDriveService.ToomossLinDrive.GetLINScheduleConfigs(); - ListLINScheduleConfigDto.Clear(); - //先清空 + if (ListMsg == null || ListMsg.Count == 0) + { + MessageBox.Show("当前没有可配置的消息帧(ListMsg 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + // 覆盖当前配置程序的调度表(系统层面的调度表,来源于 UI 选择的消息帧集合) FreeSql.Delete() .Where(a => a.CanLinConfigProId == SelectCanLinConfigProId) .ExecuteAffrows(); - foreach (var item in ListLINScheduleConfig) + var schTabName = "Default"; + var schTabIndex = 0; + var defaultCycle = 100; + + var idx = 0; + foreach (var msg in ListMsg.Where(a => !string.IsNullOrWhiteSpace(a)).Distinct(StringComparer.Ordinal)) { - ListLINScheduleConfigDto.Add(Mapper.Map(item)); + var dto = new LINScheduleConfigDto + { + CanLinConfigProId = SelectCanLinConfigProId, + SchTabName = schTabName, + SchTabIndex = schTabIndex, + MsgName = msg, + MsgNameIndex = idx++, + Cycle = defaultCycle, + IsActive = true, + IsMsgActived = true, + }; + + FreeSql.InsertOrUpdate() + .SetSource(Mapper.Map(dto)) + .ExecuteAffrows(); } + + ListLINScheduleConfigDto = new ObservableCollection( + Mapper.Map>( + FreeSql.Select().Where(a => a.CanLinConfigProId == SelectCanLinConfigProId).ToList())); + // 重新构建树 BuildTree(); } diff --git a/CapMachine.Wpf/ViewModels/DialogZlgCANFDSchConfigViewModel.cs b/CapMachine.Wpf/ViewModels/DialogZlgCANFDSchConfigViewModel.cs new file mode 100644 index 0000000..ec2f99c --- /dev/null +++ b/CapMachine.Wpf/ViewModels/DialogZlgCANFDSchConfigViewModel.cs @@ -0,0 +1,358 @@ +using AutoMapper; +using CapMachine.Core; +using CapMachine.Model.CANLIN; +using CapMachine.Wpf.Dtos; +using Prism.Commands; +using Prism.Services.Dialogs; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows; + +namespace CapMachine.Wpf.ViewModels +{ + /// + /// ZLG CAN FD 调度表配置弹窗 ViewModel。 + /// 说明: + /// - 调度表项本身由“读写设置”弹窗中的“加入定时调度表”能力生成,这里主要负责: + /// 1) 设置发送方式(并行/顺序) + /// 2) 配置周期(Cycle) + /// 3) 删除与保存落库 + /// - 保存时会使用 InsertOrUpdate 做 Upsert,并在保存完成后从数据库重载,作为权威数据再回传给调用方。 + /// + public class DialogZlgCANFDSchConfigViewModel : DialogViewModel + { + /// + /// 构造函数。 + /// + /// FreeSql。 + /// AutoMapper。 + public DialogZlgCANFDSchConfigViewModel(IFreeSql freeSql, IMapper mapper) + { + Title = "调度表 CANFD 配置"; + FreeSql = freeSql; + Mapper = mapper; + + SendOrderCbxItems = new ObservableCollection() + { + new CbxItems(){ + Key="0", + Text="并行", + }, + new CbxItems(){ + Key="1", + Text="顺序", + }, + }; + + SchTabIndexCbxItems = new ObservableCollection() + { + new CbxItems(){ + Key="0", + Text="0", + }, + }; + } + + /// + /// FreeSql。 + /// + public IFreeSql FreeSql { get; } + + /// + /// AutoMapper。 + /// + public IMapper Mapper { get; } + + private ObservableCollection _listCANFdScheduleConfigDto = new ObservableCollection(); + + /// + /// CAN FD 调度表数据集合。 + /// + public ObservableCollection ListCANFdScheduleConfigDto + { + get { return _listCANFdScheduleConfigDto; } + set { _listCANFdScheduleConfigDto = value; RaisePropertyChanged(); } + } + + /// + /// 消息名列表。 + /// + public List? ListMsg { get; set; } + + private ObservableCollection _msgCbxItems; + + /// + /// 消息名称下拉框数据。 + /// + public ObservableCollection MsgCbxItems + { + get { return _msgCbxItems; } + set { _msgCbxItems = value; RaisePropertyChanged(); } + } + + /// + /// 选中的程序 Id。 + /// + public long SelectCanLinConfigProId { get; set; } + + private ObservableCollection _sendOrderCbxItems; + + /// + /// 发送方式集合。 + /// + public ObservableCollection SendOrderCbxItems + { + get { return _sendOrderCbxItems; } + set { _sendOrderCbxItems = value; RaisePropertyChanged(); } + } + + private string? _curSendOrder; + + /// + /// 当前发送方式。 + /// + public string? CurSendOrder + { + get { return _curSendOrder; } + set { _curSendOrder = value; RaisePropertyChanged(); } + } + + private ObservableCollection _schTabIndexCbxItems; + + /// + /// 调度器序号集合。 + /// + public ObservableCollection SchTabIndexCbxItems + { + get { return _schTabIndexCbxItems; } + set { _schTabIndexCbxItems = value; RaisePropertyChanged(); } + } + + private CANFdScheduleConfigDto _curSelectedItem; + + /// + /// 当前选中项。 + /// + public CANFdScheduleConfigDto CurSelectedItem + { + get { return _curSelectedItem; } + set { _curSelectedItem = value; RaisePropertyChanged(); } + } + + private DelegateCommand _gridSelectionChangedCmd; + + /// + /// DataGrid 选中行变化。 + /// + public DelegateCommand GridSelectionChangedCmd + { + get + { + if (_gridSelectionChangedCmd == null) + { + _gridSelectionChangedCmd = new DelegateCommand(GridSelectionChangedCmdMethod); + } + return _gridSelectionChangedCmd; + } + } + + /// + /// 选中行变化处理。 + /// + /// 选中项。 + private void GridSelectionChangedCmdMethod(object par) + { + var selected = par as CANFdScheduleConfigDto; + if (selected != null) + { + CurSelectedItem = selected; + } + } + + private DelegateCommand _opCmd; + + /// + /// 操作命令(删除)。 + /// + public DelegateCommand OpCmd + { + get + { + if (_opCmd == null) + { + _opCmd = new DelegateCommand(OpCmdCall); + } + return _opCmd; + } + } + + /// + /// 操作命令处理。 + /// + /// 参数。 + private void OpCmdCall(string par) + { + switch (par) + { + case "Add": + MessageBox.Show("调度表项由【读写设置】中的写入配置通过“加入定时调度表”生成,这里不支持新增。", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + break; + case "Delete": + if (CurSelectedItem != null) + { + FreeSql.Delete(CurSelectedItem.Id).ExecuteAffrows(); + ListCANFdScheduleConfigDto.Remove(CurSelectedItem); + CurSelectedItem = null; + } + else + { + MessageBox.Show("请选中后再进行【删除】操作?", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + } + break; + default: + break; + } + } + + private DelegateCommand _saveCmd; + + /// + /// 保存命令。 + /// + public DelegateCommand SaveCmd + { + get + { + if (_saveCmd == null) + { + _saveCmd = new DelegateCommand(SaveCmdMethod); + } + return _saveCmd; + } + } + + /// + /// 保存命令处理。 + /// + private void SaveCmdMethod() + { + // 校验:确保每个调度项具备最小可用字段,避免落库无效配置。 + foreach (var item in ListCANFdScheduleConfigDto) + { + // UI 上的“发送方式”是全局下拉框,这里同步写回每个子项,确保落库数据一致。 + item.OrderSend = CurSendOrder == "0" ? 0 : 1; + + if (string.IsNullOrEmpty(item.MsgName)) + { + MessageBox.Show("消息名为空:请在【读写设置】-> 写入配置中选择报文后点击“加入定时调度表”,再回到此处设置周期。", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (item.Cycle <= 0) + { + MessageBox.Show("请确认周期是否正确", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (item.OrderSend >= 2) + { + MessageBox.Show("请确认发送方式是否正确", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + } + + foreach (var item in ListCANFdScheduleConfigDto) + { + // Upsert:逐项落库。 + FreeSql.InsertOrUpdate() + .SetSource(Mapper.Map(item)) + .ExecuteAffrows(); + } + + // 保存完成后重载: + // - 避免 UI 集合与数据库实际数据不一致; + // - 同时让新增项获取真实 Id。 + ListCANFdScheduleConfigDto = new ObservableCollection( + Mapper.Map>( + FreeSql.Select() + .Where(a => a.CanLinConfigProId == SelectCanLinConfigProId) + .ToList())); + + DialogParameters pars = new DialogParameters + { + { "ReturnValue", ListCANFdScheduleConfigDto } + }; + + RaiseRequestClose(new DialogResult(ButtonResult.OK, pars)); + } + + private DelegateCommand _cancelCmd; + + /// + /// 取消命令。 + /// + public DelegateCommand CancelCmd + { + get + { + if (_cancelCmd == null) + { + _cancelCmd = new DelegateCommand(CancelCmdMethod); + } + return _cancelCmd; + } + } + + /// + /// 取消命令处理。 + /// + private void CancelCmdMethod() + { + RaiseRequestClose(new DialogResult(ButtonResult.Cancel)); + } + + /// + /// 弹窗打开时参数接收并初始化 UI。 + /// 说明: + /// - ListMsg:用于构造消息名下拉框(候选); + /// - ListCANFdScheduleConfigDto:用于回显当前调度表; + /// - SelectCanLinConfigProId:用于 Save 后按配置程序 id 重载。 + /// + /// 参数。 + public override void OnDialogOpened(IDialogParameters parameters) + { + if (parameters.ContainsKey("ListMsg")) + { + ListMsg = parameters.GetValue>("ListMsg"); + if (ListMsg != null) + { + // 转换为 CbxItems 集合(Key/Text 都使用消息名)。 + MsgCbxItems = new ObservableCollection( + ListMsg.Select(value => new CbxItems + { + Key = value, + Text = value + })); + } + } + + ListCANFdScheduleConfigDto = parameters.GetValue>("ListCANFdScheduleConfigDto"); + if (ListCANFdScheduleConfigDto == null) ListCANFdScheduleConfigDto = new ObservableCollection(); + + if (ListCANFdScheduleConfigDto.Count > 0) + { + // 回显:默认取第一条的 OrderSend。 + CurSendOrder = ListCANFdScheduleConfigDto.FirstOrDefault()!.OrderSend.ToString(); + } + else + { + // 默认:顺序发送。 + CurSendOrder = "1"; + } + + SelectCanLinConfigProId = parameters.GetValue("SelectCanLinConfigProId"); + } + } +} diff --git a/CapMachine.Wpf/ViewModels/DialogZlgCANSchConfigViewModel.cs b/CapMachine.Wpf/ViewModels/DialogZlgCANSchConfigViewModel.cs new file mode 100644 index 0000000..264f97d --- /dev/null +++ b/CapMachine.Wpf/ViewModels/DialogZlgCANSchConfigViewModel.cs @@ -0,0 +1,402 @@ +using AutoMapper; +using CapMachine.Core; +using CapMachine.Model; +using CapMachine.Model.CANLIN; +using CapMachine.Wpf.Dtos; +using CapMachine.Wpf.Services; +using ImTools; +using Prism.Commands; +using Prism.Services.Dialogs; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace CapMachine.Wpf.ViewModels +{ + /// + /// ZLG CAN 调度表配置 + /// + public class DialogZlgCANSchConfigViewModel : DialogViewModel + { + /// + /// 构造函数 + /// + public DialogZlgCANSchConfigViewModel(IFreeSql freeSql, IMapper mapper) + { + Title = "调度表 CAN 配置"; + FreeSql = freeSql; + Mapper = mapper; + + + SendOrderCbxItems = new ObservableCollection() + { + new CbxItems(){ + Key="0", + Text="并行", + }, + new CbxItems(){ + Key="1", + Text="顺序", + }, + }; + + + //默认只能用1号调度器 + SchTabIndexCbxItems = new ObservableCollection() + { + new CbxItems(){ + Key="0", + Text="0", + }, + //new CbxItems(){ + // Key="1", + // Text="1", + //}, + //new CbxItems(){ + // Key="2", + // Text="2", + //}, + //new CbxItems(){ + // Key="3", + // Text="3", + //}, + //new CbxItems(){ + // Key="4", + // Text="4", + //}, + }; + + } + + /// + /// FreeSql(用于调度表项的删除/保存落库)。 + /// + public IFreeSql FreeSql { get; } + + /// + /// AutoMapper(DTO 与实体映射)。 + /// + public IMapper Mapper { get; } + + private string name; + /// + /// 名称 + /// + public string Name + { + get { return name; } + set { name = value; RaisePropertyChanged(); } + } + + private ObservableCollection _ListCANScheduleConfigDto = new ObservableCollection(); + /// + /// CAN 调度表数据集合 + /// + public ObservableCollection ListCANScheduleConfigDto + { + get { return _ListCANScheduleConfigDto; } + set { _ListCANScheduleConfigDto = value; RaisePropertyChanged(); } + } + + /// + /// 消息/帧报文信息集合 + /// + public List? ListMsg { get; set; } + + private ObservableCollection _MsgCbxItems; + /// + /// 消息名称 集合信息 + /// + public ObservableCollection MsgCbxItems + { + get { return _MsgCbxItems; } + set { _MsgCbxItems = value; RaisePropertyChanged(); } + } + + /// + /// 选中的程序的Id + /// + public long SelectCanLinConfigProId { get; set; } + + private ObservableCollection _SendOrderCbxItems; + /// + /// 发送方式 集合信息 + /// + public ObservableCollection SendOrderCbxItems + { + get { return _SendOrderCbxItems; } + set { _SendOrderCbxItems = value; RaisePropertyChanged(); } + } + + + private string? _CurSendOrder; + /// + /// 当前发送方式 + /// + public string? CurSendOrder + { + get { return _CurSendOrder; } + set { _CurSendOrder = value; RaisePropertyChanged(); } + } + + + private ObservableCollection _SchTabIndexCbxItems; + /// + /// 调度器序号 集合信息 + /// + public ObservableCollection SchTabIndexCbxItems + { + get { return _SchTabIndexCbxItems; } + set { _SchTabIndexCbxItems = value; RaisePropertyChanged(); } + } + + private CANScheduleConfigDto _CurSelectedItem; + /// + /// 当前选中的调度表项(用于删除等操作)。 + /// + public CANScheduleConfigDto CurSelectedItem + { + get { return _CurSelectedItem; } + set { _CurSelectedItem = value; RaisePropertyChanged(); } + } + + + private DelegateCommand _GridSelectionChangedCmd; + /// + /// DataGrid 选中行变化。 + /// + public DelegateCommand GridSelectionChangedCmd + { + set + { + _GridSelectionChangedCmd = value; + } + get + { + if (_GridSelectionChangedCmd == null) + { + _GridSelectionChangedCmd = new DelegateCommand((par) => GridSelectionChangedCmdMethod(par)); + } + return _GridSelectionChangedCmd; + } + } + private void GridSelectionChangedCmdMethod(object par) + { + // 先判断是否是正确的集合数据,防止 DataGrid 的 ItemsSource 刷新导致的误触发。 + var Selecteddata = par as CANScheduleConfigDto; + + if (Selecteddata != null) + { + CurSelectedItem = Selecteddata; + } + } + + private DelegateCommand _OpCmd; + /// + /// 操作命令(删除)。 + /// 说明:新增调度表项由“读写设置”弹窗内的“加入定时调度表”生成,此处仅允许删除与保存。 + /// + public DelegateCommand OpCmd + { + set + { + _OpCmd = value; + } + get + { + if (_OpCmd == null) + { + _OpCmd = new DelegateCommand((Par) => OpCmdCall(Par)); + } + return _OpCmd; + } + } + + private void OpCmdCall(string Par) + { + switch (Par) + { + case "Add": + MessageBox.Show("调度表项由【读写设置】中的写入配置通过“加入定时调度表”生成,这里不支持新增。", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + break; + case "Delete": + if (CurSelectedItem != null) + { + // 直接落库删除(无论 UI 是否还有引用,都以数据库为准)。 + FreeSql.Delete(CurSelectedItem.Id).ExecuteAffrows(); + ListCANScheduleConfigDto.Remove(CurSelectedItem); + + CurSelectedItem = null; + } + else + { + MessageBox.Show("请选中后再进行【删除】操作?", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + } + break; + default: + break; + } + } + + private DelegateCommand saveCmd; + /// + /// 保存命令 + /// + public DelegateCommand SaveCmd + { + set + { + saveCmd = value; + } + get + { + if (saveCmd == null) + { + saveCmd = new DelegateCommand(() => SaveCmdMethod()); + } + return saveCmd; + } + } + + /// + /// 保存命令方法。 + /// 说明: + /// - 先做必要校验(MsgName/Cycle/发送方式等),避免写入无效配置; + /// - 使用 InsertOrUpdate 保证“新增/修改”统一处理; + /// - 保存完成后重新从数据库加载,作为最终权威数据,并通过 DialogResult 回传给调用方。 + /// + private void SaveCmdMethod() + { + // 校验:确保每个调度项具备最小可用字段。 + foreach (var item in ListCANScheduleConfigDto) + { + // UI 上的“发送方式”是全局下拉框,这里同步写回到每个子项,确保落库数据一致。 + item.OrderSend = CurSendOrder == "0" ? 0 : 1; + + if (string.IsNullOrEmpty(item.MsgName)) + { + MessageBox.Show("消息名为空:请在【读写设置】-> 写入配置中选择报文后点击“加入定时调度表”,再回到此处设置周期。", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (item.Cycle <= 0) + { + MessageBox.Show("请确认周期是否正确", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (item.OrderSend >= 2) + { + MessageBox.Show("请确认发送方式是否正确", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + } + + + //发送的控制帧都放到同一个调度表中,不需要检查了 + ////检查重复设置问题 + //bool isRepeat = ListCANScheduleConfigDto.GroupBy(i => i.MsgName).Any(g => g.Count() > 1); + //if (isRepeat) + //{ + // MessageBox.Show("请确认是否重复设置", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + // return; + //} + + // Upsert:逐项落库。 + foreach (var item in ListCANScheduleConfigDto) + { + FreeSql.InsertOrUpdate() + .SetSource(Mapper.Map(item)). + ExecuteAffrows(); + } + + // 保存完成后重新加载: + // - 防止 UI 缓存与数据库实际数据不一致; + // - 同时也让新增项拿到真实 Id。 + ListCANScheduleConfigDto = new ObservableCollection(Mapper.Map>(FreeSql.Select().Where(a => a.CanLinConfigProId == SelectCanLinConfigProId).ToList())); + + DialogParameters pars = new DialogParameters + { + { "ReturnValue", ListCANScheduleConfigDto } + }; + + RaiseRequestClose(new DialogResult(ButtonResult.OK, pars)); + } + + private DelegateCommand cancelCmd; + /// + /// 取消命令。 + /// + public DelegateCommand CancelCmd + { + set + { + cancelCmd = value; + } + get + { + if (cancelCmd == null) + { + cancelCmd = new DelegateCommand(() => CancelCmdMethod()); + } + return cancelCmd; + } + } + + + /// + /// 取消命令方法。 + /// + private void CancelCmdMethod() + { + RaiseRequestClose(new DialogResult(ButtonResult.Cancel)); + } + + /// + /// 弹窗打开时接收参数并初始化 UI。 + /// 说明: + /// - ListMsg 用于构造消息名下拉框; + /// - ListCANScheduleConfigDto 用于回显现有调度表; + /// - SelectCanLinConfigProId 用于 Save 时按配置程序 id 过滤重载。 + /// + /// + public override void OnDialogOpened(IDialogParameters parameters) + { + if (parameters.ContainsKey("ListMsg")) + { + ListMsg = parameters.GetValue>("ListMsg"); + if (ListMsg != null) + { + // 转换为CbxItems集合,都是文本内容 + MsgCbxItems = new ObservableCollection( + ListMsg.Select(value => new CbxItems + { + Key = value, + Text = value + })); + } + } + + ListCANScheduleConfigDto = parameters.GetValue>("ListCANScheduleConfigDto"); + //防止返回的数据为空,就无法增加了 + if (ListCANScheduleConfigDto == null) ListCANScheduleConfigDto = new ObservableCollection(); + //Name = parameters.GetValue("Name"); + if (ListCANScheduleConfigDto.Count > 0) + { + // 回显:默认取第一条的 OrderSend 作为当前下拉框选择。 + CurSendOrder = ListCANScheduleConfigDto.FirstOrDefault()!.OrderSend.ToString(); + } + else + { + // 默认:顺序发送。 + CurSendOrder = "1"; + } + + SelectCanLinConfigProId = parameters.GetValue("SelectCanLinConfigProId"); + } + + + } +} diff --git a/CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs b/CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs index 31f8ef5..1e0b5da 100644 --- a/CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs +++ b/CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs @@ -27,6 +27,44 @@ namespace CapMachine.Wpf.ViewModels private long _canLinConfigProId; + private bool _enableHardwareCycleSchedule = true; + private bool _useCanFdSchedule; + + /// + /// 是否启用“加入定时调度表(ZLG auto_send)”能力。 + /// 说明:该能力仅适用于 ZLG CAN/CANFD 硬件 auto_send;LIN 当前使用软件调度,不允许写入 CANScheduleConfig。 + /// + public bool EnableHardwareCycleSchedule + { + get { return _enableHardwareCycleSchedule; } + private set + { + _enableHardwareCycleSchedule = value; + RaisePropertyChanged(); + RaisePropertyChanged(nameof(CanAddCycleTimeSch)); + } + } + + /// + /// 是否允许执行“加入定时调度表”操作。 + /// 说明: + /// - 需要 打开(调用方允许硬件调度表能力); + /// - 且弹窗处于可编辑态()。 + /// + public bool CanAddCycleTimeSch + { + get { return EnableHardwareCycleSchedule && IsEditable; } + } + + /// + /// 是否使用 CANFD 调度表(决定“加入定时调度表”写入 还是 )。 + /// + public bool UseCanFdSchedule + { + get { return _useCanFdSchedule; } + private set { _useCanFdSchedule = value; RaisePropertyChanged(); } + } + /// /// 构造函数。 /// @@ -153,6 +191,105 @@ namespace CapMachine.Wpf.ViewModels /// public CanLinRWConfigDto? SelectedWriteConfig { get; set; } + private DelegateCommand? _addCycleTimeSch; + + /// + /// 将当前选中的写入配置对应的报文加入“硬件定时调度表”(ZLG auto_send)。 + /// + /// + /// 说明: + /// - 调度表按“报文/帧(MsgFrameName)”维度配置;同一报文可包含多个写入信号,但调度表只需要一条。 + /// - 该命令会直接落库到 ,随后由主界面刷新配置程序时加载显示。 + /// + public DelegateCommand AddCycleTimeSch => _addCycleTimeSch ??= new DelegateCommand(AddCycleTimeSchMethod); + + /// + /// 加入定时调度表命令处理。 + /// + private void AddCycleTimeSchMethod() + { + if (!EnableHardwareCycleSchedule) + { + MessageBox.Show("当前模式不支持加入硬件定时调度表(LIN 使用软件调度表)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (!IsEditable) + { + MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (_canLinConfigProId <= 0) + { + MessageBox.Show("配置程序ID无效,无法加入调度表", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectedWriteConfig == null) + { + MessageBox.Show("请先选中写入配置中的一行", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (string.IsNullOrWhiteSpace(SelectedWriteConfig.MsgFrameName)) + { + MessageBox.Show("写入配置的【消息名称】为空,无法加入调度表", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + var msgName = SelectedWriteConfig.MsgFrameName.Trim(); + + try + { + var exists = UseCanFdSchedule + ? _freeSql.Select() + .Where(a => a.CanLinConfigProId == _canLinConfigProId) + .Where(a => a.MsgName == msgName) + .Any() + : _freeSql.Select() + .Where(a => a.CanLinConfigProId == _canLinConfigProId) + .Where(a => a.MsgName == msgName) + .Any(); + + if (exists) + { + MessageBox.Show($"该报文已在定时调度表中:{msgName}", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + if (UseCanFdSchedule) + { + _freeSql.Insert(new CANFdScheduleConfig + { + CanLinConfigProId = _canLinConfigProId, + MsgName = msgName, + Cycle = 100, + OrderSend = 1, + SchTabIndex = 0, + }).ExecuteAffrows(); + } + else + { + _freeSql.Insert(new CANScheduleConfig + { + CanLinConfigProId = _canLinConfigProId, + MsgName = msgName, + Cycle = 100, + OrderSend = 1, + SchTabIndex = 0, + }).ExecuteAffrows(); + } + + MessageBox.Show($"已加入定时调度表:{msgName}(默认周期 100ms,可在调度表中修改)", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + catch (Exception ex) + { + _logService.Error($"加入定时调度表失败:{msgName},{ex}"); + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + /// /// 当前选中的读取配置行。 /// @@ -402,8 +539,18 @@ namespace CapMachine.Wpf.ViewModels { _canLinConfigProId = parameters.GetValue("CanLinConfigProId"); + EnableHardwareCycleSchedule = parameters.ContainsKey("EnableHardwareCycleSchedule") + ? parameters.GetValue("EnableHardwareCycleSchedule") + : true; + + // 兼容参数命名:调用方传入的 key 为 "IsCanFdSchedule"。 + UseCanFdSchedule = parameters.ContainsKey("IsCanFdSchedule") + ? parameters.GetValue("IsCanFdSchedule") + : false; + IsEditable = parameters.ContainsKey("IsEditable") ? parameters.GetValue("IsEditable") : true; RaisePropertyChanged(nameof(IsEditable)); + RaisePropertyChanged(nameof(CanAddCycleTimeSch)); if (parameters.ContainsKey("WriteConfigs")) { @@ -557,6 +704,9 @@ namespace CapMachine.Wpf.ViewModels var desiredAll = desiredWrite.Concat(desiredRead).ToList(); + // 以 (RWInfo + SignalName) 作为“业务主键”对齐数据库: + // - desiredKeySet:本次保存期望存在的 key 集合 + // - existingByKey:当前数据库已存在的 key -> 实体 var desiredKeySet = new HashSet(desiredAll.Select(a => BuildKey(a.Rw, a.SignalName)), StringComparer.Ordinal); var existingByKey = existing.ToDictionary(a => BuildKey(a.RWInfo, a.SignalName ?? string.Empty), a => a, StringComparer.Ordinal); @@ -566,6 +716,7 @@ namespace CapMachine.Wpf.ViewModels var key = BuildKey(old.RWInfo, old.SignalName ?? string.Empty); if (!desiredKeySet.Contains(key)) { + // 保持与 UI 一致:用户在弹窗中移除的项,需要同步删除数据库记录。 _freeSql.Delete(old.Id).ExecuteAffrows(); } } diff --git a/CapMachine.Wpf/ViewModels/ZlgCanDriveConfigViewModel.cs b/CapMachine.Wpf/ViewModels/ZlgCanDriveConfigViewModel.cs index e975951..8ba3087 100644 --- a/CapMachine.Wpf/ViewModels/ZlgCanDriveConfigViewModel.cs +++ b/CapMachine.Wpf/ViewModels/ZlgCanDriveConfigViewModel.cs @@ -25,11 +25,42 @@ namespace CapMachine.Wpf.ViewModels /// /// ZLG CAN/CANFD 合并配置 ViewModel(模式切换:单选)。 /// + /// + /// 该 ViewModel 负责 ZLG CAN/CANFD 的 UI 配置与用户交互编排,典型职责包括: + /// - 打开/关闭设备、切换模式(CAN/CANFD),并与 同步状态; + /// - 加载 DBC 并维护可绑定的信号集合(用于读写设置与实时显示); + /// - 管理“读写设置”弹窗的数据准备与回写(写入/读取规则、信号候选集合); + /// - 管理调度表/循环发送相关 UI 状态(软件调度 vs 硬件 auto_send 的选择由 Service/Driver 实现)。 + /// + /// 线程说明: + /// - 本类绝大多数方法由 UI 线程调用; + /// - Driver 接收线程产生的数据更新事件若需要影响 UI,应通过属性通知或 Dispatcher 进行线程切换(具体由 Service/Driver 侧处理)。 + /// public class ZlgCanDriveConfigViewModel : NavigationViewModel { /// /// 构造函数。 /// + /// Prism 弹窗服务,用于打开读写设置等对话框。 + /// 数据访问组件,用于读取/保存配置程序与读写配置等持久化数据。 + /// 事件聚合器,用于跨模块消息通知(如状态变化/日志等)。 + /// 区域导航服务,用于页面/视图切换。 + /// 系统运行服务(与全局运行态/权限等相关)。 + /// 通信互斥控制服务,用于保证 CAN/LIN 等通道操作的互斥与安全。 + /// 日志服务,用于输出 UI 层操作过程与异常。 + /// 逻辑规则服务,为“写入规则/触发规则”下拉框提供数据源。 + /// 配置服务,用于读取/应用系统配置参数。 + /// ZLG CAN/CANFD 服务层,负责驱动生命周期、DBC、发送/调度表编排。 + /// ZLG LIN 服务层,用于互斥判断或组合配置场景。 + /// 对象映射器,用于 DTO/实体之间转换。 + /// + /// 构造时会初始化: + /// - 波特率下拉数据源; + /// - 写入规则下拉数据源; + /// - CAN 配置程序列表(用于 UI 选择)。 + /// + /// 注意:构造函数仅做“数据源初始化与依赖注入”,不会直接打开设备。 + /// public ZlgCanDriveConfigViewModel(IDialogService dialogService, IFreeSql freeSql, IEventAggregator eventAggregator, IRegionManager regionManager, SysRunService sysRunService, ComActionService comActionService, ILogService logService, LogicRuleService logicRuleService, @@ -98,6 +129,16 @@ namespace CapMachine.Wpf.ViewModels InitLoadCanConfigPro(); } + /// + /// 初始化“写入规则”下拉框数据源。 + /// + /// + /// 数据源来自 。 + /// - Key:规则 Id + /// - Text:规则名称 + /// + /// 该集合通常被“读写设置”弹窗中的 ComboBox 绑定使用。 + /// private void InitWriteRuleCbx() { WriteRuleCbxItems = new ObservableCollection(); @@ -111,6 +152,20 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 打开“读写设置”弹窗。 + /// + /// + /// 弹窗数据准备流程: + /// - 基于当前页面的 / 克隆出可编辑副本; + /// - 根据 构建信号候选集合(MsgName/SignalName/描述等); + /// - 通过 打开 DialogZlgCanLinRwConfigView; + /// - 用户点击 OK 后回调中调用 ReloadCurrentConfigPro 刷新当前配置并提示。 + /// + /// 前置条件: + /// - 必须先选择配置程序( 不为 null)。 + /// - 推荐调用方在进入本方法前确保已连接设备且 DBC 已加载,否则候选信号集合可能为空。 + /// private void OpenRwDialog() { try @@ -159,6 +214,7 @@ namespace CapMachine.Wpf.ViewModels { "Title", "读写设置" }, { "CanLinConfigProId", SelectCanLinConfigPro.Id }, { "IsEditable", IsRwEditable }, + { "IsCanFdSchedule", SelectedMode == ZlgCanMode.CanFd }, { "WriteConfigs", writeClones }, { "ReadConfigs", readClones }, { "SignalCandidates", candidates }, @@ -180,6 +236,16 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 克隆一个读写配置 DTO。 + /// + /// 源对象。 + /// 新实例(字段逐个复制)。 + /// + /// 目的: + /// - 弹窗编辑时不直接修改页面当前集合,避免用户取消时污染原始配置; + /// - 用户确认(OK)后再由弹窗 ViewModel 负责持久化,页面侧随后通过 Reload 刷新。 + /// private static CanLinRWConfigDto CloneRwDto(CanLinRWConfigDto src) { return new CanLinRWConfigDto @@ -255,6 +321,9 @@ namespace CapMachine.Wpf.ViewModels /// public IMapper Mapper { get; } + /// + /// 的 backing 字段。 + /// private ObservableCollection _arbBaudRateCbxItems = new ObservableCollection(); /// /// 仲裁波特率下拉项(bps)。 @@ -265,6 +334,9 @@ namespace CapMachine.Wpf.ViewModels set { _arbBaudRateCbxItems = value; RaisePropertyChanged(); } } + /// + /// 的 backing 字段。 + /// private ObservableCollection _dataBaudRateCbxItems = new ObservableCollection(); /// /// 数据波特率下拉项(bps)。 @@ -275,9 +347,23 @@ namespace CapMachine.Wpf.ViewModels set { _dataBaudRateCbxItems = value; RaisePropertyChanged(); } } + /// + /// 配置程序列表的本地缓存(数据库读取结果)。 + /// + /// + /// 与 的区别: + /// - 本字段用于保存从数据库一次性读取的实体列表; + /// - UI 绑定通常使用 (ObservableCollection)。 + /// private List _canLinConfigPros = new List(); private ObservableCollection _writeRuleCbxItems = new ObservableCollection(); + /// + /// 写入规则下拉项(逻辑规则列表)。 + /// + /// + /// 该集合由 初始化,通常绑定到读写设置弹窗或列表编辑处的 ComboBox。 + /// public ObservableCollection WriteRuleCbxItems { get { return _writeRuleCbxItems; } @@ -285,6 +371,19 @@ namespace CapMachine.Wpf.ViewModels } private ObservableCollection _listWriteCanLinRWConfigDto = new ObservableCollection(); + /// + /// 写入配置集合(绑定到写入 DataGrid)。 + /// + /// + /// 每条记录对应一个“写入信号”配置项,包含: + /// - MsgFrameName/SignalName:定位 DBC 信号 + /// - LogicRuleId:写入规则 + /// - DefautValue:默认写入值 + /// + /// 注意: + /// - 配置程序激活( true)时应禁止编辑。 + /// - 读写互斥/重复校验主要在读写设置弹窗 ViewModel 中实现。 + /// public ObservableCollection ListWriteCanLinRWConfigDto { get { return _listWriteCanLinRWConfigDto; } @@ -292,13 +391,36 @@ namespace CapMachine.Wpf.ViewModels } private ObservableCollection _listReadCanLinRWConfigDto = new ObservableCollection(); + /// + /// 读取配置集合(绑定到读取 DataGrid)。 + /// + /// + /// 每条记录对应一个“读取信号”配置项,包含: + /// - MsgFrameName/SignalName:定位 DBC 信号 + /// - DefautValue:默认值/初始值(视业务语义而定) + /// + /// 读取配置通常用于“从接收帧解码后更新 UI/变量”的目标信号集合。 + /// public ObservableCollection ListReadCanLinRWConfigDto { get { return _listReadCanLinRWConfigDto; } set { _listReadCanLinRWConfigDto = value; RaisePropertyChanged(); } } + /// + /// 当前在写入 DataGrid 中选中的行。 + /// + /// + /// 用于 Delete 等操作定位目标记录。该属性不直接通知 UI(由 SelectionChanged 命令更新)。 + /// private CanLinRWConfigDto? SelectedWriteCanLinRWConfigDto { get; set; } + + /// + /// 当前在读取 DataGrid 中选中的行。 + /// + /// + /// 用于 Delete 等操作定位目标记录。该属性不直接通知 UI(由 SelectionChanged 命令更新)。 + /// private CanLinRWConfigDto? SelectedReadCanLinRWConfigDto { get; set; } private string? _opTip; @@ -352,6 +474,9 @@ namespace CapMachine.Wpf.ViewModels set { _isCANConfigDatagridActive = value; RaisePropertyChanged(); } } + /// + /// 的 backing 字段。 + /// private ZlgCanMode _selectedMode; /// /// 模式选择:CAN/CANFD(单选)。 @@ -431,7 +556,16 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 写入 DataGrid 选中项变化命令。 + /// + /// + /// 由 XAML 绑定到 SelectionChanged,用于把当前选中行同步到 。 + /// private DelegateCommand? _writeGridSelectionChangedCmd; + /// + /// 写入 DataGrid 选中项变化命令(供 XAML 绑定)。 + /// public DelegateCommand WriteGridSelectionChangedCmd { get @@ -444,6 +578,10 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 写入 DataGrid SelectionChanged 命令处理。 + /// + /// 选中项(期望为 )。 private void WriteGridSelectionChangedCmdMethod(object par) { if (par is CanLinRWConfigDto dto) @@ -452,7 +590,16 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 读取 DataGrid 选中项变化命令。 + /// + /// + /// 由 XAML 绑定到 SelectionChanged,用于把当前选中行同步到 。 + /// private DelegateCommand? _readGridSelectionChangedCmd; + /// + /// 读取 DataGrid 选中项变化命令(供 XAML 绑定)。 + /// public DelegateCommand ReadGridSelectionChangedCmd { get @@ -465,6 +612,10 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 读取 DataGrid SelectionChanged 命令处理。 + /// + /// 选中项(期望为 )。 private void ReadGridSelectionChangedCmdMethod(object par) { if (par is CanLinRWConfigDto dto) @@ -473,7 +624,18 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 写入配置操作命令(保存/删除)。 + /// + /// + /// 约定: + /// - "Edit":把 DataGrid 中当前列表逐条更新到数据库; + /// - "Delete":删除当前选中项。 + /// private DelegateCommand? _writeCmd; + /// + /// 写入配置操作命令(供 XAML 绑定)。 + /// public DelegateCommand WriteCmd { get @@ -486,6 +648,15 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 写入配置操作命令处理。 + /// + /// 操作类型("Edit"/"Delete")。 + /// + /// 前置条件: + /// - 必须已选择配置程序( 不为 null); + /// - 若配置已激活( true),禁止修改写入配置。 + /// private void WriteCmdMethod(string par) { if (SelectCanLinConfigPro == null) @@ -528,7 +699,18 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 读取配置操作命令(保存/删除)。 + /// + /// + /// 约定: + /// - "Edit":把 DataGrid 中当前列表逐条更新到数据库; + /// - "Delete":删除当前选中项。 + /// private DelegateCommand? _readCmd; + /// + /// 读取配置操作命令(供 XAML 绑定)。 + /// public DelegateCommand ReadCmd { get @@ -541,6 +723,15 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 读取配置操作命令处理。 + /// + /// 操作类型("Edit"/"Delete")。 + /// + /// 前置条件: + /// - 必须已选择配置程序( 不为 null); + /// - 若配置已激活( true),禁止修改读取配置。 + /// private void ReadCmdMethod(string par) { if (SelectCanLinConfigPro == null) @@ -667,6 +858,9 @@ namespace CapMachine.Wpf.ViewModels get { return SelectedMode == ZlgCanMode.Can ? "CAN-DBC文件路径:" : "CANFD-DBC文件路径:"; } } + /// + /// 的 backing 字段。 + /// private ObservableCollection? _listCanLinConfigPro; /// /// 配置程序集合。 @@ -716,6 +910,9 @@ namespace CapMachine.Wpf.ViewModels private CanDbcModel? _selectedCanDbcModel; + /// + /// 的 backing 字段。 + /// private CANConfigExdDto? _selectedCANConfigExdDto; /// /// CAN 配置 DTO。 @@ -726,6 +923,9 @@ namespace CapMachine.Wpf.ViewModels set { _selectedCANConfigExdDto = value; RaisePropertyChanged(); } } + /// + /// 的 backing 字段。 + /// private CANFdConfigExdDto? _selectedCANFdConfigExdDto; /// /// CANFD 配置 DTO。 @@ -736,6 +936,18 @@ namespace CapMachine.Wpf.ViewModels set { _selectedCANFdConfigExdDto = value; RaisePropertyChanged(); } } + /// + /// 从数据库加载 CAN/CANFD 配置程序列表。 + /// + /// + /// 加载内容包含: + /// - 主表 + /// - 扩展配置(CAN/CANFD) + /// - 读写配置项(包含逻辑规则) + /// - 调度表配置项(CAN/CANFD) + /// + /// 该方法会刷新本地缓存 与 UI 绑定集合 。 + /// private void InitLoadCanConfigPro() { _canLinConfigPros = FreeSql.Select() @@ -753,6 +965,18 @@ namespace CapMachine.Wpf.ViewModels /// /// 同步选中的配置。 /// + /// + /// 当 UI 选中不同配置程序时,本方法会同步以下内容到 ViewModel: + /// - 根据配置程序的 CANLINInfo 自动切换 (避免右侧参数/DBC 显示与配置类型不一致); + /// - 映射 CAN/CANFD 两套扩展 DTO(若不存在则为 null/默认); + /// - 从配置内容构建读写配置 DTO 列表; + /// - 根据模式加载对应调度表集合; + /// - 构建并注入 CmdData 到 (事件驱动发送依赖); + /// - 最后重置并尝试加载当前配置的 DBC 信号集合(未连接时也会尝试解析)。 + /// + /// 注意: + /// - 已连接或已激活时,外层命令会禁止切换配置程序;该方法通常只在允许切换时被调用。 + /// private void SyncSelectedConfig() { if (SelectCanLinConfigPro == null) @@ -828,6 +1052,16 @@ namespace CapMachine.Wpf.ViewModels /// /// 切换配置程序时重置并尝试加载当前配置对应的 DBC 信号集合。 /// + /// + /// 背景:当“未连接设备”时,如果切换配置程序但不清空信号集合,UI 会残留上一次配置的信号列表。 + /// + /// 行为: + /// - 先清空 ; + /// - 未连接时: + /// - 若当前配置未设置 DBC 路径,则信号集合置空; + /// - 若有 DBC 路径,则调用 解析并加载信号集合; + /// - 已连接时:理论上禁止切换配置程序;这里做保底清空,避免 UI 残留。 + /// private void ResetAndTryLoadDbcSignalsForSelectedConfig() { SelectedCanDbcModel = null; @@ -844,6 +1078,10 @@ namespace CapMachine.Wpf.ViewModels try { ListCanDbcModel = ZlgCanDriveService.StartDbc(CurrentDbcPath); + if (SelectCanLinConfigPro != null) + { + ZlgCanDriveService.InitCanConfig(SelectCanLinConfigPro); + } MatchSeletedAndCanDbcModel(); } catch (Exception ex) @@ -856,10 +1094,14 @@ namespace CapMachine.Wpf.ViewModels return; } - // 已连接时:禁止切换配置程序,理论上不会执行到这里;保底清空,避免 UI 残留。 - ListCanDbcModel = new ObservableCollection(); + // 已连接时:禁止切换配置程序。 + // 注意:此处不应清空信号集合,否则在“保存读写设置/刷新配置”这类操作中会导致信号表瞬间变空。 + return; } + /// + /// 的 backing 字段。 + /// private ObservableCollection _listCANScheduleConfigDto = new ObservableCollection(); /// /// CAN 调度表集合。 @@ -870,6 +1112,9 @@ namespace CapMachine.Wpf.ViewModels set { _listCANScheduleConfigDto = value; RaisePropertyChanged(); } } + /// + /// 的 backing 字段。 + /// private ObservableCollection _listCANFdScheduleConfigDto = new ObservableCollection(); /// /// CANFD 调度表集合。 @@ -981,12 +1226,24 @@ namespace CapMachine.Wpf.ViewModels /// /// 构建并加载命令数据到驱动。 /// + /// + /// 数据流: + /// - 从当前配置程序的写入项(RW=Write)构建 列表; + /// - 每条写入项会携带 MsgName/SignalName/默认值/逻辑规则; + /// - 最终调用 注入驱动。 + /// + /// 作用: + /// - 事件驱动发送:信号值变化可触发增量发送/覆盖 auto_send; + /// - 调度表/循环发送:编码发送依赖当前 CmdData 中的信号值。 + /// private void BuildAndLoadCmdDataToDrive() { try { if (SelectCanLinConfigPro?.CanLinConfigContents == null) { + // 防御性清空: + // - 当前未选择配置程序或无配置内容时,服务层应收到空 CmdData,避免沿用旧配置继续触发事件驱动发送。 ZlgCanDriveService.LoadCmdDataToDrive(new List()); return; } @@ -998,6 +1255,10 @@ namespace CapMachine.Wpf.ViewModels var cmdList = new List(); foreach (var item in writeItems) { + // 构建 CanCmdData: + // - MsgName/SignalName 决定 DBC 编码定位; + // - 默认值 DefautValue 作为 SignalCmdValue 初始值,确保首次启动循环发送时有可编码数据; + // - LogicRule 透传到发送链路(驱动侧在事件触发时可能按规则决定是否发送/如何发送)。 cmdList.Add(new CanCmdData() { ConfigName = item.Name, @@ -1008,6 +1269,7 @@ namespace CapMachine.Wpf.ViewModels }); } + // 统一入口:将 CmdData 注入 Service/Driver,建立事件驱动增量发送能力。 ZlgCanDriveService.LoadCmdDataToDrive(cmdList); } catch (Exception ex) @@ -1085,6 +1347,9 @@ namespace CapMachine.Wpf.ViewModels } set { + // 说明: + // - 该属性是“配置程序的持久化字段”(DTO/DB)的映射; + // - 它本身不一定立即下发到驱动(驱动下发由 SchEnableCmdCall 或 CycleSend 开启前的同步逻辑完成)。 if (SelectedMode == ZlgCanMode.Can) { if (SelectedCANConfigExdDto == null) return; @@ -1145,6 +1410,14 @@ namespace CapMachine.Wpf.ViewModels /// /// 匹配选中的配置和 DBC 模型。 /// + /// + /// 用途:对信号集合做“是否已被配置为写入/读取”的标记,便于 UI 颜色或状态提示。 + /// - RW.Write 记为 1 + /// - RW.Read 记为 2 + /// - 未配置记为 0 + /// + /// 该标记依赖 。 + /// private void MatchSeletedAndCanDbcModel() { if (ListCanDbcModel == null || ListCanDbcModel.Count == 0) return; @@ -1175,10 +1448,17 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 配置程序 DataGrid 选中项变化命令的 backing 字段。 + /// private DelegateCommand? _canConfigProGridSelectionChangedCmd; /// /// 配置程序选中变化。 /// + /// + /// - 打开/激活状态下禁止切换配置程序; + /// - 允许切换时会更新 并调用 。 + /// public DelegateCommand CanConfigProGridSelectionChangedCmd { get @@ -1191,6 +1471,17 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 配置程序 DataGrid SelectionChanged 命令处理。 + /// + /// + /// 可能为: + /// - (直接传入选中项); + /// - (WPF SelectionChanged 参数)。 + /// + /// + /// WPF 的 SelectionChanged 可能被重复触发/不同类型参数混用,因此这里做了多种分支兼容。 + /// private void CanConfigProGridSelectionChangedCmdMethod(object par) { if (par == null) return; @@ -1225,6 +1516,9 @@ namespace CapMachine.Wpf.ViewModels SyncSelectedConfig(); } + /// + /// 配置程序切换前拦截命令的 backing 字段。 + /// private DelegateCommand? _canConfigProGridPreviewMouseLeftButtonDownCmd; /// /// 配置程序切换前拦截(对齐图莫斯 PreviewMouseLeftButtonDown)。 @@ -1242,6 +1536,13 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 配置程序切换前拦截命令处理。 + /// + /// 鼠标事件参数(可能为 )。 + /// + /// 若拦截命中,会将 MouseButtonEventArgs.Handled 置为 true,阻止 DataGrid 继续改变选中项。 + /// private void CanConfigProGridPreviewMouseLeftButtonDownCmdMethod(object par) { try @@ -1266,10 +1567,17 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 选择 DBC 文件命令的 backing 字段。 + /// private DelegateCommand? _loadDbcCmd; /// /// 选择 DBC 文件。 /// + /// + /// 该命令只负责让用户选择文件并写回 , + /// 不会自动解析;解析由 CAN 操作命令中的 "Parse" 或连接流程中的自动解析触发。 + /// public DelegateCommand LoadDbcCmd { get @@ -1282,6 +1590,14 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 选择 DBC 文件命令处理。 + /// + /// + /// 前置条件:必须已选择配置程序()。 + /// + /// 选择成功后会更新 ;用户取消则保持原值。 + /// private void LoadDbcCmdMethod() { try @@ -1324,10 +1640,17 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 调度使能下发命令的 backing 字段。 + /// private DelegateCommand? _schEnableCmd; /// /// 调度使能写入驱动。 /// + /// + /// 该命令用于把 UI 的 立即同步到 。 + /// - SchEnable 影响事件驱动发送与调度表/auto_send 相关行为。 + /// public DelegateCommand SchEnableCmd { get @@ -1340,16 +1663,37 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 调度使能同步到驱动。 + /// + /// 未使用(与 XAML CommandParameter 兼容)。 private void SchEnableCmdCall(object par) { // 与旧 Toomoss 行为对齐:UI 勾选后立即下发到驱动 + // 说明: + // - SchEnable 控制“事件驱动发送/覆盖更新 auto_send”的总开关; + // - 仅修改 CurrentSchEnable(DTO)不会影响驱动,必须执行该命令(或在 CycleSend 开启前同步)。 ZlgCanDriveService.SchEnable = CurrentSchEnable; } + /// + /// 配置程序操作命令的 backing 字段。 + /// private DelegateCommand? _canLinConfigProCmd; /// /// 配置程序操作(新建/修改/删除)。 /// + /// + /// 约定: + /// - "Add":新建配置程序(按当前 创建 CAN 或 CANFD 扩展表记录); + /// - "Edit":修改配置程序名称; + /// - "Delete":删除配置程序及其内容(包含读写配置、调度表、扩展表); + /// - "Active":激活/取消激活当前配置程序(激活后禁止切换配置程序)。 + /// + /// 激活语义: + /// - 激活要求设备已连接且 DBC 已解析成功; + /// - 激活后会把当前配置下发到服务层(CmdData + 调度表),用于循环发送/硬件调度表等功能。 + /// public DelegateCommand CanLinConfigProCmd { get @@ -1362,6 +1706,16 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 配置程序操作命令处理。 + /// + /// 操作类型(Add/Edit/Delete/Active)。 + /// + /// 约束说明: + /// - 打开 CAN()或已激活()时, + /// 禁止新建/修改/删除与切换配置程序,避免“驱动正在运行但底层配置被替换”导致状态错乱。 + /// - Delete 会同时清理 CAN/CANFD 两套扩展配置(如果都存在),避免孤儿记录残留。 + /// private void CanLinConfigProCmdMethod(string par) { if (string.IsNullOrWhiteSpace(par)) return; @@ -1683,10 +2037,20 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 调度表配置弹窗命令的 backing 字段。 + /// private DelegateCommand? _scheduleConfigCmd; /// /// 调度表配置(弹窗)。 /// + /// + /// 打开调度表弹窗时,会从 中提取 MsgName 列表作为候选消息帧。 + /// - CAN 模式:打开 DialogCANSchConfigView + /// - CANFD 模式:打开 DialogCANFdSchConfigView + /// + /// 弹窗确认后会把返回的调度表 DTO 写回当前配置程序实体,并调用 ReloadCurrentConfigPro 刷新。 + /// public DelegateCommand ScheduleConfigCmd { get @@ -1699,6 +2063,17 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 调度表配置弹窗命令处理。 + /// + /// + /// 前置条件: + /// - 必须选中配置程序; + /// - 必须存在可发送消息(CmdData 非空且 MsgName 有效),否则无法配置调度表。 + /// + /// 注意: + /// - 调度表本身并不直接发送;真正启动发送由 CAN 操作命令中的 "CycleSend" 触发。 + /// private void ScheduleConfigCmdMethod() { try @@ -1709,8 +2084,11 @@ namespace CapMachine.Wpf.ViewModels return; } + // 调度表弹窗需要 MsgName 候选列表: + // - 来源于当前已下发的 CmdData(写入项聚合后的消息名); + // - 这样能保证“可配置的调度表项”与“实际会被编码发送的消息帧”一致。 var msgList = ZlgCanDriveService.CmdData.GroupBy(a => a.MsgName).Select(a => a.Key).Where(a => !string.IsNullOrWhiteSpace(a)).ToList(); - if (msgList == null || msgList.Count == 0) + if ((msgList == null || msgList.Count == 0) && (ListCANScheduleConfigDto == null || ListCANScheduleConfigDto.Count == 0)) { MessageBox.Show("未发现写入指令数据(CmdData 为空),无法配置调度表", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); return; @@ -1718,7 +2096,10 @@ namespace CapMachine.Wpf.ViewModels if (SelectedMode == ZlgCanMode.Can) { - DialogService.ShowDialog("DialogCANSchConfigView", new DialogParameters() + // CAN 调度表弹窗: + // - 入参包含当前调度表 DTO(用于回显) + // - 确认后返回新的调度表 DTO 集合并回写到配置程序实体 + DialogService.ShowDialog(nameof(DialogZlgCANSchConfigView), new DialogParameters() { { "ListMsg", msgList }, { "ListCANScheduleConfigDto", ListCANScheduleConfigDto }, @@ -1727,13 +2108,16 @@ namespace CapMachine.Wpf.ViewModels { if (r.Result != ButtonResult.OK) return; ListCANScheduleConfigDto = r.Parameters.GetValue>("ReturnValue") ?? new ObservableCollection(); + + // DTO -> 实体:用于持久化(保存到数据库),并在后续激活/循环发送前由 SyncSelectedConfig 再同步到 Service。 SelectCanLinConfigPro.CanScheduleConfigs = Mapper.Map>(ListCANScheduleConfigDto.ToList()); ReloadCurrentConfigPro(); }); } else { - DialogService.ShowDialog("DialogCANFdSchConfigView", new DialogParameters() + // CANFD 调度表弹窗:语义同 CAN。 + DialogService.ShowDialog(nameof(DialogZlgCANFDSchConfigView), new DialogParameters() { { "ListMsg", msgList }, { "ListCANFdScheduleConfigDto", ListCANFdScheduleConfigDto }, @@ -1754,10 +2138,20 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 读写配置操作命令的 backing 字段。 + /// private DelegateCommand? _writeReadConfigCmd; /// /// 写入/读取/删除 信号配置项。 /// + /// + /// 常见取值: + /// - "OpenDialog":打开“读写设置”弹窗(需要已连接且信号集合非空); + /// - "Write":将当前选中信号加入写入配置; + /// - "Read":将当前选中信号加入读取配置; + /// - "Delete":删除当前选中信号的配置项。 + /// public DelegateCommand WriteReadConfigCmd { get @@ -1770,6 +2164,22 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 写入/读取/删除信号配置项命令处理。 + /// + /// 操作类型。 + /// + /// 数据来源: + /// - 操作对象为右侧信号列表中当前选中的 。 + /// + /// 重要约束: + /// - 配置程序激活后禁止变更读写配置(避免发送/解析过程中配置被修改); + /// - "OpenDialog" 会强校验:设备已连接 + 信号集合非空,防止弹窗候选集合为空。 + /// + /// 注意: + /// - 读写互斥与更严格的冲突校验主要在读写设置弹窗 ViewModel 中实现; + /// - 此处只做“同一 RWInfo 下禁止重复”与基础防御校验。 + /// private void WriteReadConfigCmdMethod(string par) { if (string.IsNullOrWhiteSpace(par)) return; @@ -1866,6 +2276,14 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 重新从数据库加载配置程序,并保持当前选中项。 + /// + /// + /// 用途: + /// - 在新增/删除/保存读写配置、保存调度表等操作后,刷新 UI 绑定对象,确保界面与数据库一致; + /// - 刷新后会重新执行 ,从而重建 CmdData、读写列表与调度表列表。 + /// private void ReloadCurrentConfigPro() { var id = SelectCanLinConfigPro?.Id; @@ -1877,13 +2295,29 @@ namespace CapMachine.Wpf.ViewModels SyncSelectedConfig(); } + if (SelectCanLinConfigPro != null) + { + ZlgCanDriveService.InitCanConfig(SelectCanLinConfigPro); + } MatchSeletedAndCanDbcModel(); } + /// + /// CAN 操作命令的 backing 字段。 + /// private DelegateCommand? _canOpCmd; /// /// CAN 操作命令。 /// + /// + /// 常见取值: + /// - "Open":按当前模式与参数打开设备并启动接收线程 + /// - "Close":关闭设备(同时停止循环发送/循环接收,并退出激活态) + /// - "Parse":解析当前 DBC(需要已连接且 DBC 路径有效) + /// - "CycleSend":循环发送开关(第一次开启,第二次关闭) + /// - "CycleRecive":循环接收开关 + /// - "Save":保存当前配置(包括 CAN/CANFD 扩展配置与模式切换) + /// public DelegateCommand CanOpCmd { get @@ -1896,6 +2330,19 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// CAN 操作命令处理。 + /// + /// 操作类型。 + /// + /// 循环发送两种实现: + /// - 若 为 true:使用调度表(硬件 auto_send);会校验每个 MsgName 在调度表中都有配置。 + /// - 若 为 false:使用软件精确定时循环发送(周期取 )。 + /// + /// 互斥约束: + /// - 开启 CAN 前会检查 状态,防止 CAN/LIN 同时占用。 + /// - Close 时会通过 Service 层确保停止发送/接收与硬件 auto_send 清理。 + /// private void CanOpCmdMethod(string par) { if (string.IsNullOrWhiteSpace(par)) @@ -1967,6 +2414,10 @@ namespace CapMachine.Wpf.ViewModels if (!string.IsNullOrWhiteSpace(CurrentDbcPath)) { ListCanDbcModel = ZlgCanDriveService.StartDbc(CurrentDbcPath); + if (SelectCanLinConfigPro != null) + { + ZlgCanDriveService.InitCanConfig(SelectCanLinConfigPro); + } MatchSeletedAndCanDbcModel(); } @@ -2000,6 +2451,10 @@ namespace CapMachine.Wpf.ViewModels } ListCanDbcModel = ZlgCanDriveService.StartDbc(CurrentDbcPath); + if (SelectCanLinConfigPro != null) + { + ZlgCanDriveService.InitCanConfig(SelectCanLinConfigPro); + } MatchSeletedAndCanDbcModel(); LastError = null; OpTip = "DBC 已解析"; @@ -2025,11 +2480,18 @@ namespace CapMachine.Wpf.ViewModels BuildAndLoadCmdDataToDrive(); // 同步 SchEnable(防止 UI 勾选但未触发 Command 导致服务端状态不一致) + // 说明: + // - SchEnable=true 时,Service 会走“硬件调度表(auto_send)”路径; + // - SchEnable=false 时,Service 会走“软件精确定时循环发送”路径; + // - 因此 CycleSend 开启前必须保证 Service.SchEnable 与 UI/DTO 一致。 ZlgCanDriveService.SchEnable = CurrentSchEnable; if (ZlgCanDriveService.SchEnable) { // 使用调度表:校验每个 MsgName 都配置了周期 + // 注意: + // - 这里校验的是“当前会被发送的消息帧集合”(CmdData 聚合)是否都在调度表中配置; + // - 缺失配置会导致硬件 auto_send 无法周期性发送对应帧,因此直接阻止开启。 var groupMsg = ZlgCanDriveService.CmdData .Where(a => !string.IsNullOrWhiteSpace(a.MsgName)) .GroupBy(a => a.MsgName) @@ -2081,11 +2543,15 @@ namespace CapMachine.Wpf.ViewModels ZlgCanDriveService.SetScheduleConfigs(ListCANFdScheduleConfigDto.ToList()); } + // 启动硬件调度表: + // - Service 层会 StopSchedule 清理旧任务/旧 auto_send,然后下发 auto_send 并 apply; + // - 运行中 CmdData 信号变化会由 Driver 覆盖更新对应 index 的帧数据(需要 IsCycleSend=true 且 SchEnable=true)。 ZlgCanDriveService.StartSchedule(); } else { // 不使用调度表:按当前配置周期做精确循环发送 + // 说明:该路径完全在 PC 侧按周期触发发送,精度受系统调度影响,但不依赖硬件 auto_send。 var cycle = Math.Max(1, CurrentCycle); ZlgCanDriveService.StartPrecisionCycleSend(cycle); } @@ -2094,6 +2560,9 @@ namespace CapMachine.Wpf.ViewModels } else { + // 关闭循环发送: + // - 先关闭事件驱动发送标志(避免 CmdData 变化继续触发增量发送/覆盖更新); + // - 再调用 StopSchedule:停止软件任务 + clear_auto_send(防止硬件继续发)。 ZlgCanDriveService.IsCycleSend = false; ZlgCanDriveService.StopSchedule(); OpTip = "循环发送:已关闭"; diff --git a/CapMachine.Wpf/ViewModels/ZlgLinDriveConfigViewModel.cs b/CapMachine.Wpf/ViewModels/ZlgLinDriveConfigViewModel.cs index 0b3e211..7163f8f 100644 --- a/CapMachine.Wpf/ViewModels/ZlgLinDriveConfigViewModel.cs +++ b/CapMachine.Wpf/ViewModels/ZlgLinDriveConfigViewModel.cs @@ -50,16 +50,60 @@ namespace CapMachine.Wpf.ViewModels InitLoadLinConfigPro(); } + /// + /// Prism 弹窗服务。 + /// public IDialogService DialogService { get; } + + /// + /// FreeSql(用于 LIN 配置程序/读写项/调度表 的查询与持久化)。 + /// public IFreeSql FreeSql { get; } + + /// + /// 事件聚合器。 + /// public IEventAggregator EventAggregator { get; } + + /// + /// 区域管理器。 + /// public IRegionManager RegionManager { get; } + + /// + /// 系统运行状态服务。 + /// public SysRunService SysRunService { get; } + + /// + /// 全局配置服务(用于记录当前系统选择的通信类型等状态)。 + /// public ConfigService ConfigService { get; } + + /// + /// ZLG LIN 服务(LIN 生命周期、LDF 解析、软件调度、收发指示灯等)。 + /// public ZlgLinDriveService ZlgLinDriveService { get; } + + /// + /// ZLG CAN 服务。 + /// 说明:LIN 与 CAN 共用同一硬件/设备句柄,打开 LIN 前需要确保 CAN 已关闭。 + /// public ZlgCanDriveService ZlgCanDriveService { get; } + + /// + /// 通信驱动互斥控制服务(同一时刻只能有一种驱动操作压缩机)。 + /// public ComActionService ComActionService { get; } + + /// + /// 逻辑规则服务。 + /// public LogicRuleService LogicRuleService { get; } + + /// + /// AutoMapper。 + /// public IMapper Mapper { get; } private List linConfigPros = new List(); @@ -80,6 +124,9 @@ namespace CapMachine.Wpf.ViewModels public CanLinConfigPro? SelectCanLinConfigPro { get; set; } private ObservableCollection _listWriteCanLinRWConfigDto = new ObservableCollection(); + /// + /// 写入配置 DTO 集合(UI 展示/编辑用)。 + /// public ObservableCollection ListWriteCanLinRWConfigDto { get { return _listWriteCanLinRWConfigDto; } @@ -87,15 +134,57 @@ namespace CapMachine.Wpf.ViewModels } private ObservableCollection _listReadCanLinRWConfigDto = new ObservableCollection(); + /// + /// 读取配置 DTO 集合(UI 展示/编辑用)。 + /// public ObservableCollection ListReadCanLinRWConfigDto { get { return _listReadCanLinRWConfigDto; } set { _listReadCanLinRWConfigDto = value; RaisePropertyChanged(); } } + private ObservableCollection _listLINScheduleConfigDto = new ObservableCollection(); + /// + /// LIN 调度表集合(系统层)。 + /// + public ObservableCollection ListLINScheduleConfigDto + { + get { return _listLINScheduleConfigDto; } + set { _listLINScheduleConfigDto = value; RaisePropertyChanged(); } + } + + private bool _isLinConfigProActive; + /// + /// 当前配置程序是否已激活(对齐 CAN Active 语义)。激活后禁止切换配置程序。 + /// + public bool IsLinConfigProActive + { + get { return _isLinConfigProActive; } + set + { + _isLinConfigProActive = value; + RaisePropertyChanged(); + RaisePropertyChanged(nameof(IsRwEditable)); + } + } + + private bool _isLINConfigDatagridActive = true; + /// + /// 配置程序 DataGrid 是否可操作(与 IsLinConfigProActive 取反)。 + /// + public bool IsLINConfigDatagridActive + { + get { return _isLINConfigDatagridActive; } + set { _isLINConfigDatagridActive = value; RaisePropertyChanged(); } + } + + /// + /// 读写设置是否允许编辑。 + /// 说明:与 语义绑定:激活后代表“配置已下发并进入运行态”,因此禁止编辑配置/切换配置程序。 + /// public bool IsRwEditable { - get { return !ZlgLinDriveService.OpenState; } + get { return !IsLinConfigProActive; } } private LINConfigExdDto? _SelectedLINConfigExdDto; @@ -105,7 +194,16 @@ namespace CapMachine.Wpf.ViewModels public LINConfigExdDto? SelectedLINConfigExdDto { get { return _SelectedLINConfigExdDto; } - set { _SelectedLINConfigExdDto = value; RaisePropertyChanged(); RaisePropertyChanged(nameof(CurrentLdfPath)); RaisePropertyChanged(nameof(CurrentSchEnable)); RaisePropertyChanged(nameof(LinBaudRate)); } + set + { + _SelectedLINConfigExdDto = value; + RaisePropertyChanged(); + + // SelectedLINConfigExdDto 变更时,需要同步刷新其映射的“派生属性”(都是 UI 绑定入口)。 + RaisePropertyChanged(nameof(CurrentLdfPath)); + RaisePropertyChanged(nameof(CurrentSchEnable)); + RaisePropertyChanged(nameof(LinBaudRate)); + } } /// @@ -122,6 +220,36 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 循环周期(ms)。 + /// + public int CurrentCycle + { + get { return SelectedLINConfigExdDto?.Cycle ?? 0; } + set + { + if (SelectedLINConfigExdDto == null) return; + SelectedLINConfigExdDto.Cycle = value; + RaisePropertyChanged(); + } + } + + /// + /// 连接按钮文字。 + /// + public string ConnectButtonText + { + get { return "连接LIN"; } + } + + /// + /// 关闭按钮文字。 + /// + public string CloseButtonText + { + get { return "关闭LIN"; } + } + /// /// LIN 波特率。 /// @@ -152,6 +280,9 @@ namespace CapMachine.Wpf.ViewModels private void InitLoadLinConfigPro() { + // 从数据库加载 LIN 配置程序: + // - 同时 include 扩展配置、读写项(含逻辑规则)、调度表 + // - 供主界面 DataGrid 与右侧配置区域绑定 linConfigPros = FreeSql.Select() .Where(a => a.CANLINInfo == CANLIN.LIN) .Include(a => a.LINConfigExd) @@ -165,6 +296,11 @@ namespace CapMachine.Wpf.ViewModels private void SyncSelectedConfig() { if (SelectCanLinConfigPro == null) return; + + // 实体 -> DTO: + // - DTO 用于 UI 编辑(双向绑定); + // - 保存时再由 VM 负责落库; + // - 激活时则以 DTO/当前列表为准,下发到 Service(避免直接引用数据库实体导致并发/脏写)。 SelectedLINConfigExdDto = Mapper.Map(SelectCanLinConfigPro.LINConfigExd); var writeData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Write).ToList() ?? new List(); @@ -173,7 +309,12 @@ namespace CapMachine.Wpf.ViewModels var readData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Read).ToList() ?? new List(); ListReadCanLinRWConfigDto = new ObservableCollection(Mapper.Map>(readData)); + var schData = SelectCanLinConfigPro.LinScheduleConfigs?.ToList() ?? new List(); + ListLINScheduleConfigDto = new ObservableCollection(Mapper.Map>(schData)); + + // 触发依赖属性刷新:IsRwEditable 与 CurrentCycle 取决于当前选择的配置与激活状态。 RaisePropertyChanged(nameof(IsRwEditable)); + RaisePropertyChanged(nameof(CurrentCycle)); } private DelegateCommand? _LinConfigProGridSelectionChangedCmd; @@ -197,6 +338,12 @@ namespace CapMachine.Wpf.ViewModels if (par == null) return; if (par is SelectionChangedEventArgs) return; + if (IsLinConfigProActive) + { + MessageBox.Show("当前配置程序已激活,请先取消激活后再切换", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (par is CanLinConfigPro) { SelectCanLinConfigPro = par as CanLinConfigPro; @@ -213,6 +360,257 @@ namespace CapMachine.Wpf.ViewModels SyncSelectedConfig(); } + private DelegateCommand? _linConfigProCmd; + /// + /// 配置程序操作(新建/修改/删除/激活)。 + /// + public DelegateCommand LinConfigProCmd + { + get + { + if (_linConfigProCmd == null) + { + _linConfigProCmd = new DelegateCommand(LinConfigProCmdMethod); + } + return _linConfigProCmd; + } + } + + private void LinConfigProCmdMethod(string par) + { + if (string.IsNullOrWhiteSpace(par)) return; + + try + { + switch (par) + { + case "Add": + if (IsLinConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再新建/修改配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (ZlgLinDriveService.OpenState) + { + MessageBox.Show("请先关闭 LIN 后再新建配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + DialogService.ShowDialog("DialogCanLinConfigCreateView", new DialogParameters() { { "Name", "" } }, r => + { + if (r.Result != ButtonResult.OK) return; + + var name = r.Parameters.GetValue("Name")?.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + MessageBox.Show("名称不能为空", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + var exists = FreeSql.Select() + .Where(a => a.CANLINInfo == CANLIN.LIN) + .Where(a => a.ConfigName == name) + .Any(); + if (exists) + { + MessageBox.Show("名称已存在,请更换名称", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + long newProId = 0; + FreeSql.Transaction(() => + { + var exdList = FreeSql.Insert(new LINConfigExd() + { + BaudRate = 19200, + Cycle = 100, + LdfPath = string.Empty, + SchEnable = false, + }).ExecuteInserted(); + var exd = exdList?.FirstOrDefault(); + if (exd == null) + { + throw new InvalidOperationException("创建 LIN 扩展配置失败"); + } + + var proList = FreeSql.Insert(new CanLinConfigPro() + { + ConfigName = name, + CANLINInfo = CANLIN.LIN, + LINConfigExdId = exd.Id, + }).ExecuteInserted(); + var pro = proList?.FirstOrDefault(); + if (pro == null) + { + throw new InvalidOperationException("创建 LIN 配置程序失败"); + } + + newProId = pro.Id; + }); + + InitLoadLinConfigPro(); + SelectCanLinConfigPro = linConfigPros.Find(a => a.Id == newProId); + SyncSelectedConfig(); + }); + break; + + case "Edit": + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (IsLinConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再修改配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (ZlgLinDriveService.OpenState) + { + MessageBox.Show("请先关闭 LIN 后再修改配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + DialogService.ShowDialog("DialogCanLinConfigCreateView", new DialogParameters() { { "Name", SelectCanLinConfigPro.ConfigName } }, r => + { + if (r.Result != ButtonResult.OK) return; + + var name = r.Parameters.GetValue("Name")?.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + MessageBox.Show("名称不能为空", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + var exists = FreeSql.Select() + .Where(a => a.CANLINInfo == CANLIN.LIN) + .Where(a => a.ConfigName == name) + .Where(a => a.Id != SelectCanLinConfigPro.Id) + .Any(); + if (exists) + { + MessageBox.Show("名称已存在,请更换名称", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + FreeSql.Update() + .Set(a => a.ConfigName, name) + .Where(a => a.Id == SelectCanLinConfigPro.Id) + .ExecuteAffrows(); + + ReloadCurrentConfigPro(); + }); + break; + + case "Delete": + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (IsLinConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再删除配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (ZlgLinDriveService.OpenState) + { + MessageBox.Show("请先关闭 LIN 后再删除配置程序", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + if (MessageBox.Show($"确定删除配置程序:{SelectCanLinConfigPro.ConfigName}?", "确认", MessageBoxButton.OKCancel, MessageBoxImage.Warning) != MessageBoxResult.OK) + { + return; + } + + var repo = FreeSql.GetRepository(); + repo.DbContextOptions.EnableCascadeSave = true; + var delList = repo.Select + .Include(a => a.LINConfigExd) + .IncludeMany(a => a.CanLinConfigContents) + .IncludeMany(a => a.LinScheduleConfigs) + .Where(a => a.Id == SelectCanLinConfigPro.Id) + .ToList(); + repo.Delete(delList); + foreach (var item in delList) + { + if (item.LINConfigExdId > 0) + { + FreeSql.Delete(item.LINConfigExdId).ExecuteAffrows(); + } + } + + SelectCanLinConfigPro = null; + SelectedLINConfigExdDto = null; + InitLoadLinConfigPro(); + ListLINScheduleConfigDto = new ObservableCollection(); + break; + + case "Active": + if (IsLinConfigProActive) + { + // 取消激活: + // - 停止循环/调度发送 + // - 关闭事件驱动发送开关 + // - 清空 CmdData(避免残留订阅导致误发送) + IsLinConfigProActive = false; + IsLINConfigDatagridActive = true; + + ZlgLinDriveService.StopSchedule(); + ZlgLinDriveService.IsCycleSend = false; + ZlgLinDriveService.LoadCmdDataToDrive(new List()); + + return; + } + + if (!ZlgLinDriveService.OpenState) + { + MessageBox.Show("请确保 LIN 已连接后再激活", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (SelectCanLinConfigPro == null || SelectedLINConfigExdDto == null) + { + MessageBox.Show("选中后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (string.IsNullOrWhiteSpace(CurrentLdfPath)) + { + MessageBox.Show("请先选择并解析 LDF 文件后再激活", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + // 激活前强制按“当前配置的 LDF 路径”重新解析,避免 LdfParserState 来自其它配置导致错配。 + try + { + ZlgLinDriveService.StartLdf(CurrentLdfPath); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + IsLinConfigProActive = true; + IsLINConfigDatagridActive = false; + + // 激活后: + // - 将当前配置程序引用交给 Service(用于后续业务关联/显示) + // - 构建并下发 CmdData + // - 下发调度表配置快照(仅缓存,真正启动由 CycleSend/SchEnable 控制) + ZlgLinDriveService.SelectedCanLinConfigPro = SelectCanLinConfigPro; + BuildAndLoadCmdDataToDrive(); + ZlgLinDriveService.SetScheduleConfigs(ListLINScheduleConfigDto?.ToList() ?? new List()); + break; + } + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + private DelegateCommand? _LoadLdfCmd; /// /// 选择 LDF 文件。 @@ -299,13 +697,31 @@ namespace CapMachine.Wpf.ViewModels ConfigService.CanLinRunStateModel.CurSysSelectedCanLin = CanLinEnum.Lin; } + // 打开后同步调度使能状态(避免 UI 未触发 Click 时服务端状态不一致) + ZlgLinDriveService.SchEnable = CurrentSchEnable; + RaisePropertyChanged(nameof(IsRwEditable)); break; case "Close": + try + { + // 停止循环发送/事件驱动发送,避免关闭后仍持续发送 + ZlgLinDriveService.StopSchedule(); + ZlgLinDriveService.IsCycleSend = false; + ZlgLinDriveService.LoadCmdDataToDrive(new List()); + } + catch + { + // ignore + } ZlgLinDriveService.CloseDevice(); ConfigService.CanLinRunStateModel.CurSysSelectedCanLin = CanLinEnum.No; + // 关闭设备时自动退出激活,恢复可切换状态 + IsLinConfigProActive = false; + IsLINConfigDatagridActive = true; + RaisePropertyChanged(nameof(IsRwEditable)); break; @@ -316,6 +732,8 @@ namespace CapMachine.Wpf.ViewModels return; } + // 仅保存 LINConfigExd(LDF 路径/周期/波特率/SchEnable 等); + // 读写项/调度表由各自弹窗或其它操作链路持久化。 FreeSql.Update() .Set(a => a.LdfPath, SelectedLINConfigExdDto.LdfPath) .Set(a => a.Cycle, SelectedLINConfigExdDto.Cycle) @@ -328,7 +746,9 @@ namespace CapMachine.Wpf.ViewModels break; case "Parse": - // 明确提示:当前 ZLG LIN 暂不支持 LDF + // 解析 LDF: + // - 解析成功后会建立运行时索引(帧/信号位定义),用于后续编码发送与接收解码; + // - 并同步构建 CmdData(用于后续调度/事件驱动)。 if (SelectCanLinConfigPro == null || SelectedLINConfigExdDto == null) { MessageBox.Show("选中LIN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); @@ -338,16 +758,138 @@ namespace CapMachine.Wpf.ViewModels try { ZlgLinDriveService.StartLdf(SelectedLINConfigExdDto.LdfPath ?? string.Empty); + + // 解析完成后同步 CmdData(用于后续调度/事件驱动) + BuildAndLoadCmdDataToDrive(); } catch (Exception ex) { MessageBox.Show(ex.Message, "提示", MessageBoxButton.OK, MessageBoxImage.Hand); } break; + + case "CycleSend": + if (!ZlgLinDriveService.OpenState) + { + MessageBox.Show("请先打开 LIN 后再进行循环发送", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (!ZlgLinDriveService.LdfParserState) + { + MessageBox.Show("请先解析 LDF 后再进行循环发送", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + // 第一次点击开启,第二次点击关闭 + if (!ZlgLinDriveService.IsCycleSend) + { + // 开启前:确保最新 CmdData 已下发 + BuildAndLoadCmdDataToDrive(); + + // 同步 SchEnable + ZlgLinDriveService.SchEnable = CurrentSchEnable; + + if (ZlgLinDriveService.SchEnable) + { + var groupMsg = ZlgLinDriveService.CmdData + .Where(a => !string.IsNullOrWhiteSpace(a.MsgName)) + .GroupBy(a => a.MsgName) + .Select(g => g.Key) + .ToList(); + + if (groupMsg.Count == 0) + { + MessageBox.Show("未发现可发送的消息帧(CmdData.MsgName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + foreach (var msg in groupMsg) + { + if (!ListLINScheduleConfigDto.Any(a => string.Equals(a.MsgName, msg, StringComparison.Ordinal))) + { + MessageBox.Show($"你使能了调度表,但是调度表中没有设置【{msg}】信息,请设置后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + } + + if (ListLINScheduleConfigDto == null || ListLINScheduleConfigDto.Count == 0) + { + MessageBox.Show("调度表配置为空数据,请检查!将无法发送数据", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + // LIN 调度表:软件调度(每帧独立周期) + ZlgLinDriveService.SetScheduleConfigs(ListLINScheduleConfigDto.ToList()); + ZlgLinDriveService.StartSchedule(); + } + else + { + // 未启用调度表:按统一周期对 CmdData 中出现的帧做软件精确定时循环发送。 + var cycle = Math.Max(1, CurrentCycle); + ZlgLinDriveService.StartPrecisionCycleSend(cycle); + } + + ZlgLinDriveService.IsCycleSend = true; + } + else + { + ZlgLinDriveService.IsCycleSend = false; + ZlgLinDriveService.StopSchedule(); + } + break; + + case "CycleRecive": + ZlgLinDriveService.SetReceiveEnabled(!ZlgLinDriveService.IsCycleRevice); + break; + } + } + + /// + /// 从当前配置程序的写入配置构建并下发 LinCmdData。 + /// + private void BuildAndLoadCmdDataToDrive() + { + try + { + if (SelectCanLinConfigPro?.CanLinConfigContents == null) + { + ZlgLinDriveService.LoadCmdDataToDrive(new List()); + return; + } + + var writeItems = SelectCanLinConfigPro.CanLinConfigContents + .Where(a => a.RWInfo == RW.Write) + .ToList(); + + var cmdList = new List(); + foreach (var item in writeItems) + { + cmdList.Add(new LinCmdData() + { + ConfigName = item.Name, + MsgName = item.MsgFrameName, + SignalName = item.SignalName, + SignalCmdValue = double.TryParse(item.DefautValue, out double result) ? result : 0, + LogicRuleDto = Mapper.Map(item.LogicRule), + }); + } + + ZlgLinDriveService.LoadCmdDataToDrive(cmdList); + } + catch (Exception ex) + { + MessageBox.Show($"构建/下发 LIN CmdData 失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } private DelegateCommand? _openRwDialogCmd; + /// + /// 打开“读写设置”弹窗。 + /// 说明: + /// - 弹窗内允许编辑写入/读取项,并可从 LDF 解析得到的信号池中选择; + /// - 弹窗确认后通过 重新从数据库加载并刷新 UI。 + /// public DelegateCommand OpenRwDialogCmd { get @@ -391,6 +933,7 @@ namespace CapMachine.Wpf.ViewModels var candidates = new ObservableCollection(); if (ZlgLinDriveService.ListLinLdfModel != null) { + // 信号候选池:由 LDF 解析结果生成(帧名+信号名)。 foreach (var sig in ZlgLinDriveService.ListLinLdfModel) { candidates.Add(new DialogZlgCanLinRwConfigViewModel.SignalCandidate @@ -408,6 +951,7 @@ namespace CapMachine.Wpf.ViewModels { "Title", "读写设置" }, { "CanLinConfigProId", SelectCanLinConfigPro.Id }, { "IsEditable", IsRwEditable }, + { "EnableHardwareCycleSchedule", false }, { "WriteConfigs", writeClones }, { "ReadConfigs", readClones }, { "SignalCandidates", candidates }, @@ -417,6 +961,7 @@ namespace CapMachine.Wpf.ViewModels { if (r.Result == ButtonResult.OK) { + // 弹窗内部会持久化保存;这里仅负责重新加载并刷新当前 UI 绑定。 ReloadCurrentConfigPro(); } }); @@ -439,6 +984,12 @@ namespace CapMachine.Wpf.ViewModels } } + /// + /// 克隆读写配置 DTO。 + /// 说明:打开弹窗前先克隆一份,避免用户在弹窗中编辑时直接影响主界面集合(取消时也不会污染当前 UI 状态)。 + /// + /// 源 DTO。 + /// 克隆后的 DTO。 private static CanLinRWConfigDto CloneRwDto(CanLinRWConfigDto src) { return new CanLinRWConfigDto @@ -472,7 +1023,138 @@ namespace CapMachine.Wpf.ViewModels private void SchEnableCmdCall(object par) { - ZlgLinDriveService.SchEnable = CurrentSchEnable; + bool enable; + if (par is bool b) + { + enable = b; + } + else + { + enable = CurrentSchEnable; + } + + // 立即同步到 Service: + // - CurrentSchEnable 只是 DTO 字段,改变它并不等于驱动侧状态已更新; + // - 这里的命令用于“用户点击开关时”立刻让 Service/Driver 生效。 + ZlgLinDriveService.SchEnable = enable; + } + + private DelegateCommand? _scheduleConfigCmd; + /// + /// 调度表配置(弹窗)。 + /// + public DelegateCommand ScheduleConfigCmd + { + get + { + if (_scheduleConfigCmd == null) + { + _scheduleConfigCmd = new DelegateCommand(ScheduleConfigCmdMethod); + } + return _scheduleConfigCmd; + } + } + + private void ScheduleConfigCmdMethod() + { + try + { + if (SelectCanLinConfigPro == null) + { + MessageBox.Show("选中LIN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + if (IsLinConfigProActive) + { + MessageBox.Show("当前配置已激活,请先取消激活后再配置调度表", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + // 确保 CmdData 最新(用于候选消息帧) + BuildAndLoadCmdDataToDrive(); + + // 调度表弹窗的 MsgName 候选列表:来自当前 CmdData 聚合。 + // 这样能保证“可配置的调度表项”与“实际会被编码发送的帧”一致。 + var msgList = ZlgLinDriveService.CmdData + .GroupBy(a => a.MsgName) + .Select(g => g.Key) + .Where(a => !string.IsNullOrWhiteSpace(a)) + .ToList(); + + if ((msgList == null || msgList.Count == 0) && (ListLINScheduleConfigDto == null || ListLINScheduleConfigDto.Count == 0)) + { + MessageBox.Show("未发现写入指令数据(CmdData 为空),无法配置调度表", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); + return; + } + + DialogService.ShowDialog(nameof(DialogLINSchConfigView), new DialogParameters() + { + { "ListMsg", msgList }, + { "ListLINScheduleConfigDto", ListLINScheduleConfigDto }, + { "SelectCanLinConfigProId", SelectCanLinConfigPro.Id }, + }, r => + { + if (r.Result != ButtonResult.OK) return; + ListLINScheduleConfigDto = r.Parameters.GetValue>("ReturnValue") ?? new ObservableCollection(); + + // DTO -> 实体:用于持久化到数据库;后续激活/循环发送前会再次由 SyncSelectedConfig/BuildAndLoadCmdDataToDrive 同步到 Service。 + SelectCanLinConfigPro.LinScheduleConfigs = Mapper.Map>(ListLINScheduleConfigDto.ToList()); + ReloadCurrentConfigPro(); + }); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + /// + /// 根据 UI 当前 SchEnable/Cycle 状态启动或停止发送。 + /// + private void ApplyScheduleByUiState(bool schEnable) + { + if (!ZlgLinDriveService.OpenState) + { + return; + } + + if (!schEnable) + { + ZlgLinDriveService.StopSchedule(); + ZlgLinDriveService.IsCycleSend = false; + return; + } + + // 开启前:确保 LDF 已解析(否则无法编码) + if (!ZlgLinDriveService.LdfParserState) + { + if (SelectedLINConfigExdDto == null) + { + throw new InvalidOperationException("未选择 LIN 配置,无法启用调度。"); + } + + ZlgLinDriveService.StartLdf(SelectedLINConfigExdDto.LdfPath ?? string.Empty); + } + + // 下发 CmdData + BuildAndLoadCmdDataToDrive(); + + // 打开事件驱动发送(用于“值变化增量发送”) + ZlgLinDriveService.IsCycleSend = true; + + // 启动软件精确定时循环发送(作为当前 ZLG LIN 的“调度表”能力) + var cycle = SelectedLINConfigExdDto?.Cycle ?? 0; + if (cycle <= 0) + { + cycle = 100; + } + ZlgLinDriveService.StartPrecisionCycleSend(cycle); + } + + private void ApplyScheduleByUiState() + { + ApplyScheduleByUiState(CurrentSchEnable); } } } diff --git a/CapMachine.Wpf/Views/DialogZlgCANFDSchConfigView.xaml b/CapMachine.Wpf/Views/DialogZlgCANFDSchConfigView.xaml new file mode 100644 index 0000000..d8f9a19 --- /dev/null +++ b/CapMachine.Wpf/Views/DialogZlgCANFDSchConfigView.xaml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -155,7 +261,12 @@ - + + + + + + @@ -164,46 +275,467 @@ VerticalAlignment="Center" FontSize="18" FontWeight="Bold" - Text="参数" /> + Text="参数操作" /> - - - + + + + - - - - - - - + Margin="10,0" + VerticalAlignment="Center" + FontSize="18" + FontWeight="Bold" + Text="状态" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +