This commit is contained in:
2026-03-02 11:20:08 +08:00
parent 74338fdb3a
commit 5be18ded2e
21 changed files with 5984 additions and 224 deletions

View File

@@ -207,6 +207,8 @@ namespace CapMachine.Wpf
containerRegistry.RegisterDialog<DialogCANFdSchConfigView, DialogCANFdSchConfigViewModel>();
containerRegistry.RegisterDialog<DialogCANSchConfigView, DialogCANSchConfigViewModel>();
containerRegistry.RegisterDialog<DialogZlgCANSchConfigView, DialogZlgCANSchConfigViewModel>();
containerRegistry.RegisterDialog<DialogZlgCANFDSchConfigView, DialogZlgCANFDSchConfigViewModel>();
containerRegistry.RegisterDialog<DialogLINSchConfigView, DialogLINSchConfigViewModel>();
containerRegistry.RegisterDialog<SplashScreenView, SplashScreenViewModel>();

View File

@@ -22,7 +22,25 @@ namespace CapMachine.Wpf.CanDrive
/// 但不是所有的SignalName都会配置一个Name只是需要时才会配置名称
/// 但是CanDbcModel集合会包括所有的SignalName名称的
/// </summary>
public string? Name { get; set; }
private string? _name;
/// <summary>
/// 配置的中文名称:速度,转速限制,使能等常用的信息数据
/// 但不是所有的SignalName都会配置一个Name只是需要时才会配置名称
/// 但是CanDbcModel集合会包括所有的SignalName名称的
/// </summary>
public string? Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
RaisePropertyChanged();
}
}
}
/// <summary>
/// 消息名称

View File

@@ -22,15 +22,35 @@ using System.Windows.Interop;
namespace CapMachine.Wpf.CanDrive
{
/// <summary>
/// Toomoss CANFD
/// 图莫斯(ToomossCAN FD 驱动封装。
/// </summary>
/// <remarks>
/// 职责边界:
/// - 负责图莫斯 USB2XXXCANFDSDK 的设备扫描/打开/初始化/关闭;
/// - 负责 DBC 文件解析,并提供“信号值 <-> CANFD 帧”的互转(通过 SDK 的 DBCParserByFD 接口);
/// - 提供三类发送能力:
/// - 单次发送(<see cref="SendCanMsg"/>
/// - 软件侧精确周期发送(<see cref="StartPrecisionCycleSendMsg"/>,以及并行版本 <see cref="StartParallelPrecisionCycleSendMsg"/>
/// - 硬件侧调度表发送(<see cref="StartSchedule"/> / <see cref="StopSchedule"/>,以及 <see cref="UpdateSchDataByCmdDataChanged"/> 增量更新);
/// - 提供接收能力:后台轮询读取报文并同步到 DBC 信号实时值(<see cref="StartCycleReviceCanMsg"/>)。
///
/// 线程与资源:
/// - 发送/接收/调度表更新均可能在后台线程运行;
/// - 多处使用 <see cref="Marshal.AllocHGlobal"/> 分配非托管内存,必须在 finally 中释放,避免长期运行内存上涨。
///
/// 重要约束:
/// - <see cref="DBCHandle"/> 为 0 表示 DBC 未解析成功,任何 DBC_* 调用都应视为不可用;
/// - <see cref="WriteCANIndex"/> / <see cref="ReadCANIndex"/> 表示 CANFD 通道索引0=CAN1,1=CAN2
/// </remarks>
public class ToomossCanFD : BindableBase
{
private readonly IContainerProvider ContainerProvider;
/// <summary>
/// 实例化函数
/// 构造函数
/// </summary>
/// <param name="containerProvider">DI 容器。</param>
/// <param name="logService">日志服务。</param>
public ToomossCanFD(IContainerProvider containerProvider, ILogService logService)
{
ContainerProvider = containerProvider;
@@ -49,8 +69,19 @@ namespace CapMachine.Wpf.CanDrive
public ILogService LoggerService { get; set; }
/// <summary>
/// 开始CAN的驱动
/// 启动 CANFD 驱动(设备准备流程)。
/// </summary>
/// <remarks>
/// 执行顺序与 SDK 依赖一致:
/// 1) 校验 DLL 是否存在;
/// 2) 扫描设备并选取句柄;
/// 3) 打开设备;
/// 4) 读取设备信息;
/// 5) 获取 CANFD 初始化配置(仲裁/数据波特率);
/// 6) 初始化 CANFD。
///
/// 注意:该方法不解析 DBCDBC 解析由 <see cref="StartDbc"/> 显式触发。
/// </remarks>
public void StartCanDrive()
{
try
@@ -70,8 +101,10 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 开始Dbc文件写入
/// 解析并加载 DBC 文件。
/// </summary>
/// <param name="DbcPath">DBC 文件路径。</param>
/// <returns>解析得到的信号列表(用于 UI 展示与实时值刷新)。</returns>
public ObservableCollection<CanDbcModel> StartDbc(string DbcPath)
{
DBC_Parser(DbcPath);
@@ -79,36 +112,44 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// Dbc消息集合
/// DBC 解析得到的信号集合
/// </summary>
/// <remarks>
/// 接收线程会持续更新 <see cref="CanDbcModel.SignalRtValue"/>。
/// </remarks>
public ObservableCollection<CanDbcModel> ListCanFdDbcModel { get; set; } = new ObservableCollection<CanDbcModel>();
#region
/// <summary>
/// 仲裁波特率
/// 仲裁波特率Arbitration Bit Rate
/// </summary>
public uint ArbBaudRate { get; set; } = 500000;
/// <summary>
/// 数据波特率
/// 数据波特率Data Bit Rate
/// </summary>
public uint DataBaudRate { get; set; } = 2000000;
/// <summary>
/// CAN FD标准 ISO是否启用
/// 是否启用 ISO CRCCAN FD ISO 标准)。
/// </summary>
public bool ISOEnable { get; set; } = true;
/// <summary>
/// 终端电阻 是否启用
/// 终端电阻是否启用
/// </summary>
public bool ResEnable { get; set; } = true;
/// <summary>
/// 更新配置
/// 更新 CANFD 参数配置(仅更新内存变量,实际生效需重新 InitCAN
/// </summary>
/// <param name="arbBaudRate">仲裁波特率。</param>
/// <param name="dataBaudRate">数据波特率。</param>
/// <param name="iSOEnable">ISO CRC 使能。</param>
/// <param name="resEnable">终端电阻使能。</param>
/// <param name="sendCycle">软件循环发送周期ms。</param>
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();
/// <summary>
/// DBC Handle
/// DBC 解析成功后得到的句柄。
/// </summary>
/// <remarks>
/// 为 0 表示 DBC 未解析/解析失败。
/// </remarks>
public UInt64 DBCHandle { get; set; }
/// <summary>
@@ -154,19 +198,19 @@ namespace CapMachine.Wpf.CanDrive
public Int32 DevHandle { get; set; } = 0;
/// <summary>
/// Write CAN Index
/// 发送通道索引0=CAN1,1=CAN2
/// </summary>
public Byte WriteCANIndex { get; set; } = 0;
/// <summary>
/// Read CAN Index
/// 接收通道索引0=CAN1,1=CAN2
/// </summary>
public Byte ReadCANIndex { get; set; } = 0;
private bool _OpenState;
/// <summary>
/// 打开设备的状态
/// 设备打开状态
/// </summary>
public bool OpenState
{
@@ -176,7 +220,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _DbcParserState;
/// <summary>
/// DBC解析状态
/// DBC 解析状态
/// </summary>
public bool DbcParserState
{
@@ -405,8 +449,12 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 发送CAN数据
/// 单次发送:根据 CmdData 中的信号值构建 CANFD 报文并发送。
/// </summary>
/// <param name="CmdData">待发送的信号指令集合(按 MsgName 分组后组成帧)。</param>
/// <remarks>
/// 注意:该方法会分配临时非托管内存 msgPt并在结束前释放。
/// </remarks>
public void SendCanMsg(List<CanCmdData> 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; }
/// <summary>
/// 精确周期发送CAN数据
/// 软件侧精确周期发送 CANFD 数据
/// </summary>
/// <remarks>
/// - 使用 <see cref="Stopwatch"/> tick 作为时间基准,减少 DateTime 抖动;
/// - 使用 Task.Delay + SpinWait 组合等待;
/// - 使用 <see cref="CycleSendCts"/> 支持外部取消。
/// </remarks>
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
}
/// <summary>
/// 修改停止发送的方法
/// 停止软件循环发送。
/// </summary>
/// <remarks>
/// - 置 <see cref="IsCycleSend"/> 为 false循环自然退出
/// - 触发 <see cref="CycleSendCts"/> 取消,用于尽快唤醒 Delay 并退出。
/// </remarks>
public void StopCycleSendMsg()
{
IsCycleSend = false;
@@ -871,8 +930,11 @@ namespace CapMachine.Wpf.CanDrive
private bool _SchEnable;
/// <summary>
/// 调度表使能
/// 调度表使能开关。
/// </summary>
/// <remarks>
/// 该开关用于控制“硬件调度表发送”及“CmdData 变化时是否增量更新调度表”。
/// </remarks>
public bool SchEnable
{
get { return _SchEnable; }
@@ -977,8 +1039,16 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 开始调度表执行
/// 启动硬件调度表CANFD
/// </summary>
/// <remarks>
/// 流程与 CAN 类似:
/// 1) 基于 <see cref="CmdData"/> 分组构建帧数组;
/// 2) 使用 DBC 将信号值同步到帧;
/// 3) 将调度周期写入帧 <see cref="USB2CANFD.CANFD_MSG.TimeStamp"/>
/// 4) 调用 CANFD_SetSchedule 下发;
/// 5) 调用 CANFD_StartSchedule 启动调度器 0。
/// </remarks>
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
/// <summary>
/// 停止调度表
/// 停止硬件调度表
/// </summary>
/// <remarks>
/// 停止后设备侧将不再按周期自动发送。
/// </remarks>
public void StopSchedule()
{
ret = USB2CANFD.CANFD_StopSchedule(DevHandle, WriteCANIndex);//启动第一个调度表,表里面的CAN帧并行发送
@@ -1121,10 +1194,12 @@ namespace CapMachine.Wpf.CanDrive
private int CycleUpdateIndex = 0;
/// <summary>
/// 加载要发送的数据
/// 一般是激活后才注册事件
/// 绑定/刷新驱动侧要发送的 CmdData并自动维护“值变化事件”的订阅。
/// </summary>
/// <param name="cmdData"></param>
/// <param name="cmdData">新的指令集合。</param>
/// <remarks>
/// 注意:必须先对旧集合取消订阅,否则会造成重复触发和内存泄漏。
/// </remarks>
public void LoadCmdDataToDrive(List<CanCmdData> cmdData)
{
// Unsubscribe from events on the old CmdData items
@@ -1145,25 +1220,27 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 指令数据发生变化执行方法
/// CmdData 中任意信号值变化事件处理。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <param name="sender">事件源。</param>
/// <param name="e">变化的 MsgName约定为报文名</param>
private void CmdData_CanCmdDataChangedHandler(object? sender, string e)
{
UpdateSchDataByCmdDataChanged();
}
/// <summary>
/// 指令数据发生变化执行更新调度表锁
/// 调度表更新互斥锁。
/// </summary>
private readonly object SchUpdateLock = new object();
/// <summary>
/// 指令数据发生变化执行方法
/// 当 CmdData 中信号值变化时,增量刷新调度表的帧数据。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// 触发条件:<see cref="IsCycleSend"/> 与 <see cref="SchEnable"/> 同时为 true。
/// 当前实现为“全量覆盖更新”CANFD_UpdateSchedule
/// </remarks>
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;
/// <summary>
/// 是否循环接收数据
/// 是否循环接收数据
/// </summary>
public bool IsCycleRevice
{
@@ -1258,7 +1335,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsCycleSend;
/// <summary>
/// 是否循环发送数据
/// 是否循环发送数据
/// </summary>
public bool IsCycleSend
{
@@ -1267,12 +1344,12 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 循环发送数据
/// 软件循环发送周期(毫秒)。
/// </summary>
public ushort SendCycle { get; set; } = 200;
/// <summary>
/// 循环接受数据
/// 接收轮询周期(毫秒)。
/// </summary>
public ushort ReviceCycle { get; set; } = 200;
@@ -1292,7 +1369,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsSendOk;
/// <summary>
/// 发送报文是否OK
/// 最近一次发送是否成功。
/// </summary>
public bool IsSendOk
{
@@ -1310,7 +1387,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsReviceOk;
/// <summary>
/// 接收报文是否OK
/// 最近一次接收是否成功。
/// </summary>
public bool IsReviceOk
{
@@ -1327,13 +1404,17 @@ namespace CapMachine.Wpf.CanDrive
/// <summary>
/// 要发送的数据
/// 当前激活的指令集合(由 Service/UI 下发)。
/// </summary>
public List<CanCmdData> CmdData { get; set; } = new List<CanCmdData>();
/// <summary>
/// 循环获取CAN消息
/// 启动后台循环接收 CANFD 报文,并同步到 DBC 信号实时值。
/// </summary>
/// <remarks>
/// 注意:当前实现每轮接收都会分配 msgPtRead 并释放,长时间运行会造成 GC 压力和潜在碎片;
/// 若后续需要优化,可参考 CAN 版本的“接收缓冲池复用 + CloseDevice 互斥释放”。
/// </remarks>
public void StartCycleReviceCanMsg()
{
CycleReviceTask = Task.Run(async () =>
@@ -1477,8 +1558,14 @@ namespace CapMachine.Wpf.CanDrive
/// <summary>
/// 关闭设备
/// 关闭设备并释放资源。
/// </summary>
/// <remarks>
/// 释放顺序要点:
/// - 关闭设备并下置状态位;
/// - 若启用调度表则先停止;
/// - 等待接收任务退出(短等待,避免 UI 卡死)。
/// </remarks>
public void CloseDevice()
{
//关闭设备

View File

@@ -13,15 +13,38 @@ using System.Text;
namespace CapMachine.Wpf.CanDrive
{
/// <summary>
/// Toomoss CAN
/// 图莫斯(ToomossCAN 驱动封装。
/// </summary>
/// <remarks>
/// 职责边界:
/// - 负责图莫斯 USB2XXX SDK 的设备扫描/打开/初始化/关闭;
/// - 负责 DBC 文件解析,并提供“信号值 <-> CAN 帧”的互转(通过 SDK 的 DBCParser 接口);
/// - 提供三类发送能力:
/// - 单次发送(<see cref="SendCanMsg"/>
/// - 软件侧精确周期发送(<see cref="StartPrecisionCycleSendMsg"/>
/// - 硬件侧调度表发送(<see cref="StartSchedule"/> / <see cref="StopSchedule"/>,以及 <see cref="UpdateSchDataByCmdDataChanged"/> 增量更新);
/// - 提供接收能力:后台轮询读取报文并同步到 DBC 信号值(<see cref="StartCycleReviceCanMsg"/>)。
///
/// 线程与资源:
/// - 发送/接收/调度表更新均可能在后台线程运行;
/// - 本类会分配非托管内存IntPtr 缓冲区)。必须通过 <see cref="CloseDevice"/> 释放,且释放过程需要与接收线程互斥。
///
/// 重要约束:
/// - <see cref="DBCHandle"/> 为 0 表示 DBC 未解析成功,任何 DBC_* 调用都应视为不可用;
/// - <see cref="WriteCANIndex"/> / <see cref="ReadCANIndex"/> 表示 CAN 通道索引0=CAN1,1=CAN2当前实现约定读写通道一致。
/// </remarks>
public class ToomossCan : BindableBase
{
/// <summary>
/// Prism 容器,用于解析运行时依赖(如 <see cref="HighSpeedDataService"/>)。
/// </summary>
private readonly IContainerProvider ContainerProvider;
/// <summary>
/// 实例化函数
/// 构造函数
/// </summary>
/// <param name="containerProvider">DI 容器。</param>
/// <param name="logService">日志服务(外部注入,避免驱动类自己创建日志对象)。</param>
public ToomossCan(IContainerProvider containerProvider, ILogService logService)
{
ContainerProvider = containerProvider;
@@ -35,8 +58,19 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 开始CAN驱动
/// 启动 CAN 驱动(设备准备流程)。
/// </summary>
/// <remarks>
/// 执行顺序与 SDK 依赖一致:
/// 1) 校验 DLL 是否存在;
/// 2) 扫描设备并选取句柄;
/// 3) 打开设备;
/// 4) 读取设备信息;
/// 5) 获取 CAN 初始化配置;
/// 6) 初始化 CAN。
///
/// 注意:该方法不解析 DBCDBC 解析由 <see cref="StartDbc"/> 显式触发。
/// </remarks>
public void StartCanDrive()
{
IsExistsDllFile();
@@ -50,18 +84,20 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// HighSpeedDataService 实例
/// 高速数据服务:用于把接收到的原始报文写入高速记录/显示通道。
/// </summary>
public HighSpeedDataService HighSpeedDataService { get; set; }
/// <summary>
/// Logger 实例
/// 日志服务。
/// </summary>
public ILogService LoggerService { get; set; }
/// <summary>
/// 开始Dbc文件写入
/// 解析并加载 DBC 文件。
/// </summary>
/// <param name="DbcPath">DBC 文件路径。</param>
/// <returns>解析得到的信号列表(用于 UI 展示与实时值刷新)。</returns>
public ObservableCollection<CanDbcModel> StartDbc(string DbcPath)
{
DBC_Parser(DbcPath);
@@ -77,38 +113,47 @@ namespace CapMachine.Wpf.CanDrive
//}
/// <summary>
/// Dbc消息集合
/// 包括读取的实时值和数据
/// DBC 解析得到的信号集合
/// </summary>
/// <remarks>
/// - 该集合的每个元素代表一个信号Signal包含所属报文名、信号名、单位/描述、实时值等。
/// - 接收线程会持续更新 <see cref="CanDbcModel.SignalRtValue"/>。
/// </remarks>
public ObservableCollection<CanDbcModel> ListCanDbcModel { get; set; } = new ObservableCollection<CanDbcModel>();
#region
/// <summary>
/// 设备固件信息
/// 设备固件信息(由 SDK 填充)。
/// </summary>
public USB_DEVICE.DEVICE_INFO DevInfo = new USB_DEVICE.DEVICE_INFO();
/// <summary>
/// CAN Config
/// CAN 初始化配置(由 <see cref="GetCANConfig"/> 读取或计算后用于 <see cref="InitCAN"/>)。
/// </summary>
USB2CAN.CAN_INIT_CONFIG CANConfig = new USB2CAN.CAN_INIT_CONFIG();
/// <summary>
/// DBC加载后获取的Handle
/// DBC Handle
/// DBC 解析成功后得到的句柄。
/// </summary>
/// <remarks>
/// - 为 0 表示 DBC 未解析/解析失败;
/// - 发送与接收解析均依赖该句柄。
/// </remarks>
public UInt64 DBCHandle { get; set; }
/// <summary>
/// 扫描设备Handle集合
/// 扫描到的设备句柄数组SDK 输出)。
/// </summary>
public Int32[] DevHandles { get; set; } = new Int32[20];
/// <summary>
/// 扫描设备Handle
/// 当前打开的设备句柄。
/// </summary>
/// <remarks>
/// 该值由 <see cref="ScanDevice"/> 选取并在 <see cref="OpenDevice"/> 中打开。
/// </remarks>
public Int32 DevHandle { get; set; } = 0;
/// <summary>
@@ -149,7 +194,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _OpenState;
/// <summary>
/// 打开设备的状态
/// 设备打开状态
/// </summary>
public bool OpenState
{
@@ -159,7 +204,7 @@ namespace CapMachine.Wpf.CanDrive
private bool _DbcParserState;
/// <summary>
/// DBC解析状态
/// DBC 解析状态
/// </summary>
public bool DbcParserState
{
@@ -174,6 +219,12 @@ namespace CapMachine.Wpf.CanDrive
public Int32 ret { get; set; }
/// <summary>
/// USB2XXX SDK 的 DLL 文件名。
/// </summary>
/// <remarks>
/// 注意:此处仅校验 DLL 是否存在,实际的 DllImport 绑定仍由运行时加载机制决定。
/// </remarks>
public string dllFilePath { get; set; } = "USB2XXX.dll";
@@ -185,10 +236,12 @@ namespace CapMachine.Wpf.CanDrive
#endregion
/// <summary>
/// ******************【1】*********************
/// 是否存在Dll文件
/// 【1】检查 USB2XXX SDK 的 DLL 是否存在。
/// </summary>
/// <returns></returns>
/// <returns>存在返回 true否则返回 false。</returns>
/// <remarks>
/// 这是最前置的防御性校验,避免后续调用直接触发 DllNotFoundException。
/// </remarks>
public bool IsExistsDllFile()
{
if (!File.Exists(dllFilePath))
@@ -202,10 +255,13 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// ******************【2】*********************
/// 扫描查找设备,并将每个设备的唯一设备号存放到数组中,后面的函数需要用到
/// 【2】扫描设备。
/// </summary>
/// <returns></returns>
/// <returns>扫描到设备返回 true否则返回 false。</returns>
/// <remarks>
/// SDK 会把设备句柄写入 <see cref="DevHandles"/>,并返回设备数量。
/// 当前实现默认选取第一个设备句柄作为 <see cref="DevHandle"/>。
/// </remarks>
public bool ScanDevice()
{
DevNum = USB_DEVICE.USB_ScanDevice(DevHandles);
@@ -330,9 +386,14 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// ******************【7】*********************
/// DBC解析
/// 【7】解析 DBC 文件并构建信号列表。
/// </summary>
/// <param name="Path">DBC 文件路径。</param>
/// <remarks>
/// 解析结果:
/// - <see cref="DBCHandle"/>:后续信号赋值/取值/同步帧均依赖它;
/// - <see cref="ListCanDbcModel"/>:用于 UI 列表展示与实时值更新。
/// </remarks>
public void DBC_Parser(string Path)
{
//解析DBC文件
@@ -390,9 +451,18 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 发送CAN数据
/// 发送一次
/// 单次发送:根据 CmdData 中的信号值构建 CAN 报文并发送。
/// </summary>
/// <param name="CmdData">待发送的信号指令集合(按 MsgName 分组后组成帧)。</param>
/// <remarks>
/// 数据流:
/// 1) 按 MsgName 分组(每组代表同一个报文/帧);
/// 2) 对每个分组:逐信号调用 DBC_SetSignalValue 写入 DBC 缓存;
/// 3) 调用 DBC_SyncValueToCANMsg 生成帧;
/// 4) 调用 CAN_SendMsg 一次性批量发送。
///
/// 注意:此方法会分配临时非托管内存 msgPt并在结束前释放。
/// </remarks>
public void SendCanMsg(List<CanCmdData> 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;
/// <summary>
/// 是否循环接收数据
/// 是否循环接收数据
/// </summary>
/// <remarks>
/// 由 UI/Service 控制开关,接收任务 <see cref="StartCycleReviceCanMsg"/> 以该标记作为退出条件。
/// </remarks>
public bool IsCycleRevice
{
get { return _IsCycleRevice; }
@@ -454,8 +528,13 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsCycleSend;
/// <summary>
/// 是否循环发送数据
/// 是否循环发送数据
/// </summary>
/// <remarks>
/// 由 UI/Service 控制开关:
/// - 软件精确周期发送:<see cref="StartPrecisionCycleSendMsg"/> / <see cref="StopCycleSendMsg"/>
/// - 调度表发送:<see cref="StartSchedule"/> / <see cref="StopSchedule"/>。
/// </remarks>
public bool IsCycleSend
{
get { return _IsCycleSend; }
@@ -464,28 +543,41 @@ namespace CapMachine.Wpf.CanDrive
/// <summary>
/// 循环发送数据
/// 循环发送周期(毫秒)。
/// </summary>
/// <remarks>
/// 该周期用于软件侧循环发送/接收 Task.Delay硬件调度表发送周期由调度表项的 Cycle 决定。
/// </remarks>
public ushort SendCycle { get; set; } = 100;
/// <summary>
/// 循环接受数据
/// 循环接收轮询周期(毫秒)。
/// </summary>
public ushort ReviceCycle { get; set; } = 500;
/// <summary>
/// CycleRevice 扫描Task
/// 循环接收任务。
/// </summary>
/// <remarks>
/// 这里是 static意味着同进程内多个实例会共享该任务引用这在多设备场景需要特别注意
/// </remarks>
private static Task CycleReviceTask { get; set; }
/// <summary>
/// CycleSend 扫描Task
/// 循环发送任务。
/// </summary>
/// <remarks>
/// 这里是 static意味着同进程内多个实例会共享该任务引用这在多设备场景需要特别注意
/// </remarks>
private static Task CycleSendTask { get; set; }
/// <summary>
/// 定时扫描更新数据 扫描Task
/// (旧方案)周期性刷新调度表任务。
/// </summary>
/// <remarks>
/// 当前已引入“CmdData 变化事件 -> 增量更新调度表”的机制(见 <see cref="UpdateSchDataByCmdDataChanged"/>
/// 该 Task 属于保留兼容路径。
/// </remarks>
private static Task CycleUpdateCmdTask { get; set; }
StringBuilder ValueSb = new StringBuilder(16);
@@ -519,8 +611,12 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsSendOk;
/// <summary>
/// 发送报文是否OK
/// 最近一次发送是否成功。
/// </summary>
/// <remarks>
/// - 软件循环发送路径:由 CAN_SendMsg 返回值决定;
/// - 调度表更新路径:由 CAN_UpdateSchedule 返回值决定。
/// </remarks>
public bool IsSendOk
{
get { return _IsSendOk; }
@@ -537,8 +633,14 @@ namespace CapMachine.Wpf.CanDrive
private bool _IsReviceOk;
/// <summary>
/// 接收报文是否OK
/// 最近一次接收是否成功。
/// </summary>
/// <remarks>
/// 该状态反映最近一次轮询读取 CAN_GetMsgWithSize 的结果:
/// - CanNum &gt; 0成功收到数据
/// - CanNum == 0本轮无数据
/// - CanNum &lt; 0调用失败。
/// </remarks>
public bool IsReviceOk
{
get { return _IsReviceOk; }
@@ -553,13 +655,20 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 要发送的数据
/// 当前激活的指令集合(由 Service/UI 下发)。
/// </summary>
/// <remarks>
/// - 每个元素代表一个信号的写入指令;
/// - 发送时按 MsgName 分组,同一 MsgName 下的多个信号会被打包到同一帧。
/// </remarks>
public List<CanCmdData> CmdData { get; set; } = new List<CanCmdData>();
/// <summary>
/// 循环发送数据
/// (普通)软件循环发送:以 <see cref="SendCycle"/> 为周期构建帧并发送。
/// </summary>
/// <remarks>
/// 该方法属于早期实现,精度与线程抖动相关;若追求更高精度建议使用 <see cref="StartPrecisionCycleSendMsg"/>。
/// </remarks>
public void StartCycleSendMsg()
{
CycleSendTask = Task.Run(async () =>
@@ -865,8 +974,16 @@ namespace CapMachine.Wpf.CanDrive
private static readonly Random _random = new Random();
/// <summary>
/// 精确周期发送CAN数据
/// 软件侧精确周期发送 CAN 数据
/// </summary>
/// <remarks>
/// 关键点:
/// - 使用 <see cref="Stopwatch"/> tick 作为时间基准,减少 DateTime 带来的抖动;
/// - 使用 Task.Delay + SpinWait 组合:大等待让出 CPU小等待自旋微调
/// - 使用 <see cref="CycleSendCts"/> 支持外部取消。
///
/// 注意:调用方需要先将 <see cref="IsCycleSend"/> 置为 true停止时请调用 <see cref="StopCycleSendMsg"/>。
/// </remarks>
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
/// <summary>
/// 修改停止发送的方法
/// 停止软件循环发送。
/// </summary>
/// <remarks>
/// - 置 <see cref="IsCycleSend"/> 为 false循环自然退出
/// - 触发 <see cref="CycleSendCts"/> 取消,用于尽快唤醒 Delay 并退出。
/// </remarks>
public void StopCycleSendMsg()
{
IsCycleSend = false;
@@ -1046,8 +1168,11 @@ namespace CapMachine.Wpf.CanDrive
private bool _SchEnable;
/// <summary>
/// 调度表使能
/// 调度表使能开关。
/// </summary>
/// <remarks>
/// 该开关用于控制“硬件调度表发送”及“CmdData 变化时是否增量更新调度表”。
/// </remarks>
public bool SchEnable
{
get { return _SchEnable; }
@@ -1060,13 +1185,21 @@ namespace CapMachine.Wpf.CanDrive
/// <summary>
/// 调度表的发送的报文数据
/// 当前已下发到设备调度表的帧缓存。
/// </summary>
/// <remarks>
/// - 在 <see cref="StartSchedule"/> 中构建,并通过 CAN_SetSchedule 下发;
/// - 在 <see cref="UpdateSchDataByCmdDataChanged"/> 中重建并通过 CAN_UpdateSchedule 覆盖更新。
/// </remarks>
private USB2CAN.CAN_MSG[] SchCanMsg { get; set; }
/// <summary>
/// 依据消息的分组
/// CmdData 按 MsgName 分组后的视图。
/// </summary>
/// <remarks>
/// 该分组的遍历顺序决定了 <see cref="SchCanMsg"/> 中每个帧的 Index
/// 因而要求:调度表启动后,更新时必须按相同顺序遍历(当前实现保持一致)。
/// </remarks>
private IEnumerable<IGrouping<string, CanCmdData>> GroupMsg { get; set; }
/// <summary>
@@ -1111,7 +1244,7 @@ namespace CapMachine.Wpf.CanDrive
/// </summary>
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
}
/// <summary>
/// 开始调度表执行
/// 启动硬件调度表
/// </summary>
/// <remarks>
/// 流程:
/// 1) 基于当前 <see cref="CmdData"/> 分组构建帧数组 <see cref="SchCanMsg"/>
/// 2) 使用 DBC 将信号值同步到帧;
/// 3) 根据 <see cref="ListCANScheduleConfig"/> 把周期写入帧的 TimeStamp 字段;
/// 4) 调用 CAN_SetSchedule 下发;
/// 5) 调用 CAN_StartSchedule 启动(当前约定只启动调度器 0
///
/// 注意:本实现把所有报文都放入调度表 0因此 UI 上的 SchTabIndex 实际未被使用。
/// </remarks>
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
/// <summary>
/// 停止调度表
/// 停止硬件调度表
/// </summary>
/// <remarks>
/// 停止后设备侧将不再按周期自动发送。
/// </remarks>
public void StopSchedule()
{
ret = USB2CAN.CAN_StopSchedule(DevHandle, WriteCANIndex);//启动第一个调度表,表里面的CAN帧并行发送
@@ -1389,10 +1535,15 @@ namespace CapMachine.Wpf.CanDrive
/// <summary>
/// 加载要发送的数据
/// 一般是激活后才注册事件
/// 绑定/刷新驱动侧要发送的 CmdData并自动维护“值变化事件”的订阅。
/// </summary>
/// <param name="cmdData"></param>
/// <param name="cmdData">新的指令集合。</param>
/// <remarks>
/// 目的当某个信号值变化时CanCmdData.SignalCmdValue setter 触发),
/// 可以通过 <see cref="CmdData_CanCmdDataChangedHandler"/> 触发调度表增量刷新。
///
/// 注意:必须先对旧集合取消订阅,否则会造成重复触发和内存泄漏。
/// </remarks>
public void LoadCmdDataToDrive(List<CanCmdData> cmdData)
{
// Unsubscribe from events on the old CmdData items
@@ -1413,25 +1564,37 @@ namespace CapMachine.Wpf.CanDrive
}
/// <summary>
/// 指令数据发生变化执行方法
/// CmdData 中任意信号值变化事件处理。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <param name="sender">事件源。</param>
/// <param name="e">变化的 MsgName约定为报文名</param>
/// <remarks>
/// 当前实现采取“全量覆盖更新调度表”的方式UpdateSchedule并未按 MsgName 做局部更新。
/// </remarks>
private void CmdData_CanCmdDataChangedHandler(object? sender, string e)
{
UpdateSchDataByCmdDataChanged();
}
/// <summary>
/// 指令数据发生变化执行更新调度表锁
/// 调度表更新互斥锁。
/// </summary>
/// <remarks>
/// - CmdData 变化事件可能高频触发;
/// - 同时 UpdateSchedule 调用耗时且涉及非托管内存与 DBC 操作,必须串行化。
/// </remarks>
private readonly object SchUpdateLock = new object();
/// <summary>
/// 指令数据发生变化执行方法
/// 当 CmdData 中信号值变化时,增量刷新调度表的帧数据。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// 触发条件:<see cref="IsCycleSend"/> 与 <see cref="SchEnable"/> 同时为 true。
///
/// 当前实现策略:
/// - 为保证简单与一致性:每次变化都重建所有帧并全量调用 CAN_UpdateSchedule 覆盖更新;
/// - 若后续性能成为瓶颈,可根据变化的 MsgName 实现“按帧更新MsgIndex + MsgNum=1”。
/// </remarks>
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
/// <summary>
/// 循环获取CAN消息
/// 启动后台循环接收 CAN 报文,并同步到 DBC 信号实时值。
/// </summary>
/// <remarks>
/// 关键点:
/// - 使用 CAN_GetMsgWithSize 从设备内部 FIFO 拉取报文;
/// - 使用 <see cref="RecvMsgBufferPtr"/> 作为重用缓冲,避免每轮申请/释放非托管内存导致碎片与性能问题;
/// - 使用 <see cref="RecvBufferSync"/> 与 <see cref="CloseDevice"/> 互斥,避免指针并发释放。
/// </remarks>
public void StartCycleReviceCanMsg()
{
// 防止重复启动,若已有任务在运行则直接返回
@@ -1694,8 +1863,16 @@ namespace CapMachine.Wpf.CanDrive
/// <summary>
/// 关闭设备
/// 关闭设备并释放资源。
/// </summary>
/// <remarks>
/// 释放顺序要点:
/// - 先关闭设备并下置状态位;
/// - 若启用调度表则先停止;
/// - 停止定时器/周期发送;
/// - 等待接收任务退出(短等待,避免 UI 卡死);
/// - 在互斥锁内释放接收缓冲指针,避免接收线程仍在使用。
/// </remarks>
public void CloseDevice()
{
//关闭设备

View File

@@ -2,10 +2,22 @@
namespace CapMachine.Wpf.CanDrive.ZlgCan
{
/// <summary>
/// ZLG 原生驱动zlgcan.dllP/Invoke 映射层。
/// 说明:
/// - 本类仅负责将厂商 SDK 的函数/常量/结构体映射到 C#,不包含任何业务逻辑;上层 Driver/Service 会对这些 API 进行封装并提供线程安全与资源管理。
/// - 互操作注意事项:
/// 1) <c>zlgcan.dll</c> 必须随程序部署且位数与进程一致x86/x64否则会在加载/调用时失败。
/// 2) 结构体需严格按 SDK 定义设置 <see cref="StructLayoutAttribute"/> 与 Pack字段顺序与大小必须一致。
/// 3) DllImport 的 CallingConvention 与 SetLastError 需与 SDK 匹配,否则可能出现栈不平衡或错误码不可用。
/// - 约定:当前工程中通常由 <c>ZlgCanFd200uDriver</c> 负责检查 DLL 存在与架构匹配,并在打开设备前完成预检查。
/// </summary>
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
{

File diff suppressed because it is too large Load Diff

View File

@@ -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<LINScheduleConfig, LINScheduleConfigDto>().ReverseMap();
}
}
}

View File

@@ -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
/// <summary>
/// ZLG CAN/CANFD 驱动服务(共享设备句柄)。
/// </summary>
/// <remarks>
/// 该类属于“服务层/编排层”,主要职责:
/// - 管理 <see cref="ZlgCanFd200uDriver"/> 的生命周期(打开、接收线程、关闭释放);
/// - 管理 DBC 加载与信号模型集合(<see cref="ListCanDbcModel"/>
/// - 管理写入指令集合(<see cref="CmdData"/>并将其注入驱动形成“DBC 编码发送”的闭环;
/// - 提供两种循环发送实现:
/// 1) 硬件定时发送(调度表):通过周立功 <c>auto_send</c> 列表 + <c>apply_auto_send</c>(精度高)
/// 2) 软件精确定时循环发送:通过后台 Task 在 PC 侧按周期触发 <see cref="ZlgCanFd200uDriver.SendOneMsgByCmdData"/>(用于不依赖硬件调度或调试场景)
///
/// 线程模型:
/// - 接收线程由 <see cref="ZlgCanFd200uDriver"/> 内部创建;
/// - 软件调度发送由本类创建后台 Task见 <see cref="StartSoftwareScheduler"/>
/// - UI 绑定的属性变化通过 <see cref="BindableBase"/> 的 <c>RaisePropertyChanged</c> 通知。
/// </remarks>
public sealed class ZlgCanDriveService : BindableBase
{
/// <summary>
/// 软件调度条目(仅在软件调度线程内使用)。
/// </summary>
/// <remarks>
/// 该结构体承担“纯运行态状态”的职责:
/// - 启动时由 <see cref="StartSoftwareScheduler"/> 构建并排序;
/// - 运行中由 <see cref="RunSoftwareScheduler"/> 在单线程内更新 <see cref="NextDueTicks"/> 等字段;
/// - 不对外暴露,也不需要线程安全的读写(因为不会跨线程访问)。
/// </remarks>
private sealed class SoftwareScheduleItem
{
/// <summary>
/// 构造。
/// </summary>
/// <param name="msgName">DBC 报文名称。</param>
/// <param name="periodTicks">周期Stopwatch ticks。</param>
/// <param name="orderSend">发送顺序(用于排序)。</param>
public SoftwareScheduleItem(string msgName, long periodTicks, int orderSend)
{
MsgName = msgName;
PeriodTicks = periodTicks;
OrderSend = orderSend;
}
/// <summary>
/// DBC 报文名称MsgName
/// </summary>
public string MsgName { get; }
/// <summary>
/// 周期(以 <see cref="Stopwatch"/> ticks 表示)。
/// </summary>
public long PeriodTicks { get; }
/// <summary>
/// 发送顺序(用于同 tick 内的确定性排序)。
/// </summary>
public int OrderSend { get; }
/// <summary>
/// 下次计划发送的到期时间sw.ElapsedTicks
/// </summary>
public long NextDueTicks;
/// <summary>
/// 上一次记录 warn 的时间(用于日志节流)。
/// </summary>
public long LastWarnTicks;
}
/// <summary>
/// 日志服务。
/// </summary>
private readonly ILogService _log;
/// <summary>
@@ -28,37 +96,69 @@ namespace CapMachine.Wpf.Services
/// <summary>
/// 当前选中的配置程序(沿用原有 FreeSql 模型)。
/// </summary>
/// <remarks>
/// 该属性用于与 UI“配置程序选择”联动。
/// - 本类不负责持久化;
/// - 只在 <see cref="InitCanConfig"/> 中用于把配置中的显示名称映射到 DBC 信号集合。
/// </remarks>
public CanLinConfigPro? SelectedCanLinConfigPro { get; set; }
/// <summary>
/// Dbc 消息集合(用于 UI 绑定)。
/// </summary>
/// <remarks>
/// 该集合由 <see cref="StartDbc"/> / <see cref="ZlgCanFd200uDriver.StartDbc"/> 构建并替换,通常用于界面显示:
/// - MsgName/SignalName/实时值等;
/// - 也会被 <see cref="InitCanConfig"/> 用于把“配置程序中的显示名称”映射到信号上。
///
/// 注意:
/// - DBC 解码更新发生在 Driver 的接收线程中,若 UI 对集合元素的属性变化敏感,请确保 UI 侧的线程切换策略。
/// </remarks>
public ObservableCollection<CanDbcModel> ListCanDbcModel { get; private set; } = new ObservableCollection<CanDbcModel>();
/// <summary>
/// <see cref="DbcParserState"/> 的 backing 字段。
/// </summary>
private bool _dbcParserState;
/// <summary>
/// DBC 解析状态。
/// </summary>
/// <remarks>
/// 该状态仅表示“DBC 已加载且有信号集合”,常用于 UI 控件的 enable/disable。
/// </remarks>
public bool DbcParserState
{
get { return _dbcParserState; }
private set { _dbcParserState = value; RaisePropertyChanged(); }
}
/// <summary>
/// <see cref="OpenState"/> 的 backing 字段。
/// </summary>
private bool _openState;
/// <summary>
/// 设备打开状态。
/// </summary>
/// <remarks>
/// 该属性与 <see cref="ZlgCanFd200uDriver.OpenState"/> 保持一致,由构造函数中订阅 Driver.PropertyChanged 同步刷新。
/// </remarks>
public bool OpenState
{
get { return _openState; }
private set { _openState = value; RaisePropertyChanged(); }
}
/// <summary>
/// <see cref="Mode"/> 的 backing 字段。
/// </summary>
private ZlgCanMode _mode;
/// <summary>
/// CAN 模式单选CAN 或 CANFD
/// </summary>
/// <remarks>
/// - 该属性影响发送/调度表下发时使用的 frameTypeCAN=8字节CANFD=64字节
/// - 切换 Mode 后若要立即生效,通常应重新调用 <see cref="LoadCmdDataToDrive"/>(使 Driver 侧 frameType 更新)。
/// </remarks>
public ZlgCanMode Mode
{
get { return _mode; }
@@ -68,6 +168,11 @@ namespace CapMachine.Wpf.Services
/// <summary>
/// 发送使能(与 UI 的调度表使能语义对齐)。
/// </summary>
/// <remarks>
/// 该属性透传到 <see cref="ZlgCanFd200uDriver.SchEnable"/>
/// - true允许“事件驱动发送”CmdData 变化时发送/覆盖 auto_send
/// - false仅允许手动发送或软件调度任务主动调用发送接口
/// </remarks>
public bool SchEnable
{
get { return Driver.SchEnable; }
@@ -77,6 +182,11 @@ namespace CapMachine.Wpf.Services
/// <summary>
/// 是否启用事件驱动发送。
/// </summary>
/// <remarks>
/// 该属性透传到 <see cref="ZlgCanFd200uDriver.IsCycleSend"/>。
/// - true 且 <see cref="SchEnable"/> 为 true 时CmdData 变化会触发增量发送/覆盖更新;
/// - falseCmdData 变化不会触发任何发送(由外部显式调用发送方法)。
/// </remarks>
public bool IsCycleSend
{
get { return Driver.IsCycleSend; }
@@ -86,6 +196,9 @@ namespace CapMachine.Wpf.Services
/// <summary>
/// 是否正在循环接收(对齐 ToomossIsCycleRevice
/// </summary>
/// <remarks>
/// 该属性为只读透传:接收线程由 <see cref="ZlgCanFd200uDriver.StartReceiveLoop"/> 创建。
/// </remarks>
public bool IsCycleRevice
{
get { return Driver.IsReceiving; }
@@ -94,6 +207,9 @@ namespace CapMachine.Wpf.Services
/// <summary>
/// 最近是否发送成功(用于 UI 指示)。
/// </summary>
/// <remarks>
/// 该标志由 Driver 的发送调用或 Tx 回显事件触发,短时间保持 true 后自动回落。
/// </remarks>
public bool IsSendOk
{
get { return Driver.IsSendOk; }
@@ -102,6 +218,9 @@ namespace CapMachine.Wpf.Services
/// <summary>
/// 最近是否接收成功(用于 UI 指示)。
/// </summary>
/// <remarks>
/// 该标志由 Driver 接收线程收到 Rx 帧触发,短时间保持 true 后自动回落。
/// </remarks>
public bool IsReviceOk
{
get { return Driver.IsReviceOk; }
@@ -110,23 +229,92 @@ namespace CapMachine.Wpf.Services
/// <summary>
/// 要发送的 CAN 指令数据。
/// </summary>
/// <remarks>
/// 该集合通常来源于“读写设置/配置程序”生成的信号写入项。
/// - 发送时并不是逐条直接写信号,而是按 MsgName 聚合后通过 DBC 编码为帧(由 Driver 完成)。
/// - 本类在 <see cref="LoadCmdDataToDrive"/> 中将集合注入 Driver并建立信号变化事件订阅。
/// </remarks>
public List<CanCmdData> CmdData { get; } = new List<CanCmdData>();
/// <summary>
/// 调度表(软件调度任务)互斥锁。
/// </summary>
/// <remarks>
/// 保护以下共享状态的并发访问:
/// - <see cref="_scheduleItems"/> 调度表条目快照
/// - <see cref="_scheduleCts"/> / <see cref="_scheduleTask"/> 生命周期切换
///
/// 说明:
/// - 硬件 auto_send 的生命周期主要由 Driver 内部管理;
/// - 本类在 <see cref="StopSchedule"/> 中同时清理软件任务与硬件 auto_send确保“停止=设备不再发”。
/// </remarks>
private readonly object _scheduleLock = new object();
/// <summary>
/// 当前调度表条目快照。
/// </summary>
/// <remarks>
/// 元组含义:
/// - MsgNameDBC Message Name
/// - CycleMs周期ms
/// - OrderSend/SchTabIndex用于排序保证硬件列表 index 的确定性
/// </remarks>
private List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> _scheduleItems = new List<(string, int, int, int)>();
/// <summary>
/// 软件调度任务取消令牌。
/// </summary>
private CancellationTokenSource? _scheduleCts;
/// <summary>
/// 软件调度后台任务。
/// </summary>
private Task? _scheduleTask;
/// <summary>
/// 标记“当前循环发送使用配置的调度表项”。
/// </summary>
/// <remarks>
/// 该字段当前仅用于区分启动来源(配置调度表 or 精确定时循环),未来可用于 UI 展示或行为分支。
/// </remarks>
private bool _scheduleUseConfigItems;
/// <summary>
/// 要发送的速度指令数据。
/// </summary>
/// <remarks>
/// 该字段用于兼容旧接口 <see cref="SendMsgToCanDrive"/> 的“手动发送转速”行为:
/// - 通过 <see cref="LoadCmdDataToDrive"/> 从 <see cref="CmdData"/> 中按 ConfigName=转速 选取;
/// - 若未配置该项,则手动发送会记录 warn 并忽略。
/// </remarks>
private CanCmdData? SpeedCanCmdData { get; set; }
/// <summary>
/// 当前打开的设备索引(默认 0
/// </summary>
/// <remarks>
/// 由 <see cref="UpdateConfig"/> 更新,在 <see cref="StartCanDrive"/> 时作为打开参数。
/// </remarks>
private uint _deviceIndex = 0;
/// <summary>
/// 通道0配置快照。
/// </summary>
/// <remarks>
/// 目前服务层仅初始化通道0并将合并接收等设备级开关按通道0的配置写入设备。
/// </remarks>
private ZlgCanFdChannelOptions _channel0 = new ZlgCanFdChannelOptions();
/// <summary>
/// 构造。
/// </summary>
/// <param name="logService">日志服务。</param>
/// <remarks>
/// 构造时会:
/// - 创建 <see cref="Driver"/>
/// - 订阅 Driver 的 <see cref="System.ComponentModel.INotifyPropertyChanged.PropertyChanged"/>,并同步更新本服务的派生属性:
/// <see cref="OpenState"/> / <see cref="IsCycleRevice"/> / <see cref="IsSendOk"/> / <see cref="IsReviceOk"/>。
/// </remarks>
public ZlgCanDriveService(ILogService logService)
{
_log = logService;
@@ -148,14 +336,38 @@ namespace CapMachine.Wpf.Services
/// 初始化 CAN 配置信息,并将配置中的名称映射到 DBC 信号集合(用于 UI 显示)。
/// </summary>
/// <param name="selectedCanLinConfigPro">选中的配置。</param>
/// <remarks>
/// 该方法不与设备连接状态强绑定:
/// - 只要 <see cref="ListCanDbcModel"/> 已有信号集合,就可以进行“显示名称映射”。
///
/// 映射规则:
/// - 以 SignalName 为键,在 DBC 模型集合中找到对应信号,并把配置项的 Name 写回 <see cref="CanDbcModel.Name"/>。
/// </remarks>
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
/// <param name="resEnable">终端电阻使能。</param>
/// <param name="mergeReceive">是否合并接收。</param>
/// <param name="mergeReceiveBufferFrames">合并接收缓冲帧数。</param>
/// <remarks>
/// 该方法仅更新服务层保存的“配置快照”,不会立即触发驱动重连。
/// - 生效时机:下一次调用 <see cref="StartCanDrive"/> 打开设备并初始化通道。
/// - 当前实现仅使用通道0配置。
/// </remarks>
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
/// <summary>
/// 打开 CAN/CANFD按 Mode
/// </summary>
/// <remarks>
/// - 该方法是幂等的:若 <see cref="OpenState"/> 为 true则直接返回
/// - 成功打开后会初始化通道并启动接收线程(见 <see cref="ZlgCanFd200uDriver.StartReceiveLoop"/>)。
///
/// 注意:
/// - 本方法不会自动加载 DBC也不会自动注入 CmdData
/// - 建议调用顺序UpdateConfig -&gt; StartCanDrive -&gt; StartDbc -&gt; LoadCmdDataToDrive。
/// </remarks>
public void StartCanDrive()
{
if (OpenState)
@@ -201,6 +426,12 @@ namespace CapMachine.Wpf.Services
/// 使能/停止循环接收。
/// </summary>
/// <param name="enable">true=启动接收线程false=停止接收线程。</param>
/// <remarks>
/// 说明:
/// - 接收线程属于 Driver 内部资源;
/// - 若开启合并接收接收线程会按通道0配置决定采用 merge 或 per-channel 接收。
/// </remarks>
/// <exception cref="InvalidOperationException">设备未连接。</exception>
public void SetReceiveEnabled(bool enable)
{
if (!OpenState)
@@ -229,6 +460,12 @@ namespace CapMachine.Wpf.Services
/// <summary>
/// 关闭设备(会同时关闭共享的 LIN 通道)。
/// </summary>
/// <remarks>
/// Close 语义强调“彻底停止后台活动”:
/// - 停止调度表:取消软件调度任务,并清理硬件 auto_send 列表(避免设备侧继续发);
/// - 停止事件驱动发送:将 <see cref="IsCycleSend"/> 置为 false
/// - 停止接收:<see cref="ZlgCanFd200uDriver.Close"/> 内部会停止接收线程并释放句柄。
/// </remarks>
public void CloseDevice()
{
try
@@ -248,6 +485,15 @@ namespace CapMachine.Wpf.Services
}
}
/// <summary>
/// 设置调度表配置CAN
/// </summary>
/// <param name="configs">调度表配置项。</param>
/// <remarks>
/// 该方法仅保存调度表条目快照,不会立即启动发送。
/// - 真正启动由 <see cref="StartSchedule"/> 触发。
/// - MsgName 为空的条目会被过滤。
/// </remarks>
public void SetScheduleConfigs(IEnumerable<CANScheduleConfigDto> configs)
{
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<CANScheduleConfigDto>();
@@ -259,6 +505,13 @@ namespace CapMachine.Wpf.Services
}
}
/// <summary>
/// 设置调度表配置CANFD
/// </summary>
/// <param name="configs">调度表配置项。</param>
/// <remarks>
/// 语义同 CAN 版本 <see cref="SetScheduleConfigs(IEnumerable{CANScheduleConfigDto})"/>,差异仅在于配置来源 DTO 类型。
/// </remarks>
public void SetScheduleConfigs(IEnumerable<CANFdScheduleConfigDto> configs)
{
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<CANFdScheduleConfigDto>();
@@ -270,6 +523,20 @@ namespace CapMachine.Wpf.Services
}
}
/// <summary>
/// 启动调度表(硬件定时发送列表 auto_send
/// </summary>
/// <remarks>
/// 本方法选择“硬件调度表”实现:
/// - 将调度表条目转换为设备 auto_send 列表并 apply使设备按周期自动发送
/// - 运行时若 CmdData 对应信号值变化Driver 会覆盖更新对应 index 的帧数据。
///
/// 前置条件:
/// - 设备已连接(<see cref="OpenState"/> true
/// - 调度表不为空;
/// - DBC 已加载且 CmdData 可编码(否则对应 MsgName 会被 Driver 跳过并记录 warn
/// </remarks>
/// <exception cref="InvalidOperationException">设备未连接或调度表为空。</exception>
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-&gt;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);
}
/// <summary>
/// 启动软件精确定时循环发送。
/// </summary>
/// <param name="cycleMs">周期ms。</param>
/// <remarks>
/// 与 <see cref="StartSchedule"/> 的区别:
/// - 本方法完全在 PC 侧调度,周期精度受系统调度与负载影响;
/// - 适用于不依赖硬件 auto_send 的场景,或作为调试/兼容方案。
///
/// 发送对象:
/// - 按当前 <see cref="CmdData"/> 中出现的 MsgName 去重后,按统一周期循环发送。
/// </remarks>
/// <exception cref="InvalidOperationException">设备未连接或 CmdData 为空。</exception>
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 固定为 1SchTabIndex 固定为 0
var items = msgNames.Select(n => (n, ms, 1, 0)).ToList();
_scheduleUseConfigItems = false;
StartSoftwareScheduler(items);
}
/// <summary>
/// 停止调度表/循环发送。
/// </summary>
/// <remarks>
/// 该方法会同时停止两类发送资源:
/// - 软件调度:取消 Task 并等待短时间退出;
/// - 硬件调度:调用 Driver.StopAutoSendSchedule 清空设备 auto_send 列表,防止设备继续发送。
///
/// 该方法不会自动将 <see cref="IsCycleSend"/> 置为 false
/// - 保留给上层决定(例如仅暂停调度,但仍允许事件驱动发送)。
/// </remarks>
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停止流程不因硬件清理失败而中断。
}
}
/// <summary>
/// 启动软件调度后台任务。
/// </summary>
/// <param name="items">调度表条目集合(由调用方准备好快照)。</param>
/// <remarks>
/// 该任务使用“下一次到期时间 due”字典进行调度
/// - 优先 sleep 到最早 due减少 CPU 占用;
/// - tick 内对 ready 的消息按字典序依次发送;
/// - 每次发送后更新下一次 due。
///
/// 注意:
/// - 这是软件调度,不保证绝对周期精度;
/// - 发送异常会记录 warn但不会终止调度循环。
/// </remarks>
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<string, DateTime>(StringComparer.Ordinal);
var cycle = new Dictionary<string, int>(StringComparer.Ordinal);
var order = new Dictionary<string, int>(StringComparer.Ordinal);
// 只在启动时做一次去重/排序;循环内不分配。
var unique = new Dictionary<string, (int CycleMs, int OrderSend)>(StringComparer.Ordinal);
foreach (var it in items)
{
if (!due.ContainsKey(it.MsgName))
if (string.IsNullOrWhiteSpace(it.MsgName)) continue;
var name = it.MsgName.Trim();
if (!unique.ContainsKey(name))
{
due[it.MsgName] = now;
cycle[it.MsgName] = Math.Max(1, it.CycleMs);
order[it.MsgName] = it.OrderSend;
unique[name] = (Math.Max(1, it.CycleMs), it.OrderSend);
}
}
var list = new List<SoftwareScheduleItem>(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);
}
}
/// <summary>
/// 软件调度循环(后台线程)。
/// </summary>
/// <param name="sw">单调计时器(启动时创建,贯穿整个调度生命周期)。</param>
/// <param name="items">调度条目数组(启动时构建;运行中会更新每项的到期时间)。</param>
/// <param name="token">取消令牌(由 <see cref="StopSchedule"/> 触发取消)。</param>
/// <param name="frequency"><see cref="Stopwatch.Frequency"/>(用于 ticks/ms 换算)。</param>
/// <remarks>
/// 设计目标:
/// - 尽量降低 CPU 占用:优先等待到最早 due 附近;
/// - 尽量降低抖动:临近到期使用短等待 + 自旋;
/// - 避免“积压补发”:如果错过多个周期,只发送一次并将 due 跳到 now+period。
///
/// 线程安全说明:
/// - 该方法仅由 <see cref="StartSoftwareScheduler"/> 创建的 LongRunning 任务调用;
/// - <paramref name="items"/> 在该线程内独占读写(不会跨线程访问),因此不需要额外锁。
/// </remarks>
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)
{
await Task.Delay(50, token).ConfigureAwait(false);
// 设备未打开时不发送:
// - 避免在 Close/重连窗口持续抛异常;
// - 这里仍然响应取消,保证 StopSchedule 不会“卡住等待”。
if (token.WaitHandle.WaitOne(50))
{
break;
}
continue;
}
var utcNow = DateTime.UtcNow;
var minDue = due.Values.Min();
var delay = minDue - utcNow;
if (delay > TimeSpan.Zero)
var nowTicks = sw.ElapsedTicks;
long minDue = long.MaxValue;
for (var i = 0; i < items.Length; i++)
{
var ms = (int)Math.Min(delay.TotalMilliseconds, 200);
await Task.Delay(ms, token).ConfigureAwait(false);
var d = items[i].NextDueTicks;
if (d < minDue)
{
minDue = d;
}
}
var waitTicks = minDue - nowTicks;
if (waitTicks > 0)
{
// 混合等待:
// - 剩余时间较长WaitOne(<=200ms) 降低 CPU 占用
// - 临近到期1ms 级等待 + 短自旋提升边界精度
var waitMs = (int)(waitTicks * 1000 / frequency);
if (waitMs > 5)
{
var ms = Math.Min(waitMs - 1, 200);
if (token.WaitHandle.WaitOne(ms))
{
break;
}
continue;
}
// due messages
var ready = due.Where(kv => kv.Value <= utcNow).Select(kv => kv.Key).ToList();
if (ready.Count == 0)
if (waitMs > 1)
{
await Task.Delay(1, token).ConfigureAwait(false);
if (token.WaitHandle.WaitOne(1))
{
break;
}
continue;
}
// 顺序/并行:这里只决定同一 tick 内的发送顺序(并行模式仍按字典序依次发)
ready.Sort(StringComparer.Ordinal);
foreach (var msg in ready)
spin.SpinOnce();
continue;
}
// 到期发送:
// - 同一条消息“错过多个周期”时,只发送一次(避免 burst然后尽快恢复节拍。
nowTicks = sw.ElapsedTicks;
for (var i = 0; i < items.Length; i++)
{
if (token.IsCancellationRequested) break;
var it = items[i];
if (it.NextDueTicks > nowTicks)
{
continue;
}
try
{
Driver.SendOneMsgByCmdData(msg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
// 软件调度最终仍复用 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)
{
_log.Warn($"调度表发送失败:{msg}{ex.Message}");
var t = sw.ElapsedTicks;
if (t - it.LastWarnTicks >= warnIntervalTicks)
{
it.LastWarnTicks = t;
_log.Warn($"软件循环发送失败:{it.MsgName}{ex.Message}");
}
}
finally
{
due[msg] = DateTime.UtcNow.AddMilliseconds(cycle[msg]);
// 漂移控制:正常情况下按“计划时间”累加;若落后过多,则跳到 now+period只补发一次
var next = it.NextDueTicks + it.PeriodTicks;
if (next <= nowTicks)
{
next = nowTicks + it.PeriodTicks;
}
it.NextDueTicks = next;
}
}
}
}, cts.Token);
}
/// <summary>
/// 加载 DBC。
/// </summary>
/// <param name="path">DBC 路径。</param>
/// <returns>DBC 解析得到的信号集合。</returns>
/// <remarks>
/// - DBC 加载成功后会更新 <see cref="DbcParserState"/> 与 <see cref="ListCanDbcModel"/>
/// - 后续发送(调度表/事件驱动/手动发送)都依赖 DBC 编码能力。
/// </remarks>
/// <exception cref="InvalidOperationException">设备未连接。</exception>
public ObservableCollection<CanDbcModel> 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
/// 加载要发送的数据(订阅数据变化事件)。
/// </summary>
/// <param name="cmdData">指令数据集合。</param>
/// <remarks>
/// 该方法会:
/// - 更新本服务的 <see cref="CmdData"/> 集合;
/// - 从集合中选取“转速”项缓存到 <see cref="SpeedCanCmdData"/>(用于兼容旧的手动发送接口);
/// - 调用 <see cref="ZlgCanFd200uDriver.LoadCmdDataToDrive"/> 注入 Driver并建立事件驱动链路。
///
/// 注意:
/// - frameType 由 <see cref="Mode"/> 决定;若运行时切换 Mode建议重新调用本方法。
/// </remarks>
public void LoadCmdDataToDrive(List<CanCmdData> cmdData)
{
var list = cmdData ?? new List<CanCmdData>();
// 以“清空 + 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
/// 手动发送(目前对齐原服务:仅发送转速)。
/// </summary>
/// <param name="speedData">转速。</param>
/// <remarks>
/// 行为说明:
/// - 优先将 speedData 写入 <see cref="SpeedCanCmdData"/> 的 SignalCmdValue
/// - 若当前未启用事件驱动发送(<see cref="IsCycleSend"/> 或 <see cref="SchEnable"/> 为 false则主动触发一次编码发送
/// 以维持旧版本“点击发送即下发”的体验。
/// </remarks>
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))
{

File diff suppressed because it is too large Load Diff

View File

@@ -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<CbxItems>()
@@ -54,7 +53,6 @@ namespace CapMachine.Wpf.ViewModels
public IFreeSql FreeSql { get; }
public IMapper Mapper { get; }
public LinDriveService LinDriveService { get; }
private string name;
/// <summary>
@@ -415,17 +413,45 @@ namespace CapMachine.Wpf.ViewModels
/// </summary>
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<LINScheduleConfig>()
.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<LINScheduleConfigDto>(item));
var dto = new LINScheduleConfigDto
{
CanLinConfigProId = SelectCanLinConfigProId,
SchTabName = schTabName,
SchTabIndex = schTabIndex,
MsgName = msg,
MsgNameIndex = idx++,
Cycle = defaultCycle,
IsActive = true,
IsMsgActived = true,
};
FreeSql.InsertOrUpdate<LINScheduleConfig>()
.SetSource(Mapper.Map<LINScheduleConfig>(dto))
.ExecuteAffrows();
}
ListLINScheduleConfigDto = new ObservableCollection<LINScheduleConfigDto>(
Mapper.Map<List<LINScheduleConfigDto>>(
FreeSql.Select<LINScheduleConfig>().Where(a => a.CanLinConfigProId == SelectCanLinConfigProId).ToList()));
// 重新构建树
BuildTree();
}

View File

@@ -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
{
/// <summary>
/// ZLG CAN FD 调度表配置弹窗 ViewModel。
/// 说明:
/// - 调度表项本身由“读写设置”弹窗中的“加入定时调度表”能力生成,这里主要负责:
/// 1) 设置发送方式(并行/顺序)
/// 2) 配置周期Cycle
/// 3) 删除与保存落库
/// - 保存时会使用 InsertOrUpdate 做 Upsert并在保存完成后从数据库重载作为权威数据再回传给调用方。
/// </summary>
public class DialogZlgCANFDSchConfigViewModel : DialogViewModel
{
/// <summary>
/// 构造函数。
/// </summary>
/// <param name="freeSql">FreeSql。</param>
/// <param name="mapper">AutoMapper。</param>
public DialogZlgCANFDSchConfigViewModel(IFreeSql freeSql, IMapper mapper)
{
Title = "调度表 CANFD 配置";
FreeSql = freeSql;
Mapper = mapper;
SendOrderCbxItems = new ObservableCollection<CbxItems>()
{
new CbxItems(){
Key="0",
Text="并行",
},
new CbxItems(){
Key="1",
Text="顺序",
},
};
SchTabIndexCbxItems = new ObservableCollection<CbxItems>()
{
new CbxItems(){
Key="0",
Text="0",
},
};
}
/// <summary>
/// FreeSql。
/// </summary>
public IFreeSql FreeSql { get; }
/// <summary>
/// AutoMapper。
/// </summary>
public IMapper Mapper { get; }
private ObservableCollection<CANFdScheduleConfigDto> _listCANFdScheduleConfigDto = new ObservableCollection<CANFdScheduleConfigDto>();
/// <summary>
/// CAN FD 调度表数据集合。
/// </summary>
public ObservableCollection<CANFdScheduleConfigDto> ListCANFdScheduleConfigDto
{
get { return _listCANFdScheduleConfigDto; }
set { _listCANFdScheduleConfigDto = value; RaisePropertyChanged(); }
}
/// <summary>
/// 消息名列表。
/// </summary>
public List<string>? ListMsg { get; set; }
private ObservableCollection<CbxItems> _msgCbxItems;
/// <summary>
/// 消息名称下拉框数据。
/// </summary>
public ObservableCollection<CbxItems> MsgCbxItems
{
get { return _msgCbxItems; }
set { _msgCbxItems = value; RaisePropertyChanged(); }
}
/// <summary>
/// 选中的程序 Id。
/// </summary>
public long SelectCanLinConfigProId { get; set; }
private ObservableCollection<CbxItems> _sendOrderCbxItems;
/// <summary>
/// 发送方式集合。
/// </summary>
public ObservableCollection<CbxItems> SendOrderCbxItems
{
get { return _sendOrderCbxItems; }
set { _sendOrderCbxItems = value; RaisePropertyChanged(); }
}
private string? _curSendOrder;
/// <summary>
/// 当前发送方式。
/// </summary>
public string? CurSendOrder
{
get { return _curSendOrder; }
set { _curSendOrder = value; RaisePropertyChanged(); }
}
private ObservableCollection<CbxItems> _schTabIndexCbxItems;
/// <summary>
/// 调度器序号集合。
/// </summary>
public ObservableCollection<CbxItems> SchTabIndexCbxItems
{
get { return _schTabIndexCbxItems; }
set { _schTabIndexCbxItems = value; RaisePropertyChanged(); }
}
private CANFdScheduleConfigDto _curSelectedItem;
/// <summary>
/// 当前选中项。
/// </summary>
public CANFdScheduleConfigDto CurSelectedItem
{
get { return _curSelectedItem; }
set { _curSelectedItem = value; RaisePropertyChanged(); }
}
private DelegateCommand<object> _gridSelectionChangedCmd;
/// <summary>
/// DataGrid 选中行变化。
/// </summary>
public DelegateCommand<object> GridSelectionChangedCmd
{
get
{
if (_gridSelectionChangedCmd == null)
{
_gridSelectionChangedCmd = new DelegateCommand<object>(GridSelectionChangedCmdMethod);
}
return _gridSelectionChangedCmd;
}
}
/// <summary>
/// 选中行变化处理。
/// </summary>
/// <param name="par">选中项。</param>
private void GridSelectionChangedCmdMethod(object par)
{
var selected = par as CANFdScheduleConfigDto;
if (selected != null)
{
CurSelectedItem = selected;
}
}
private DelegateCommand<string> _opCmd;
/// <summary>
/// 操作命令(删除)。
/// </summary>
public DelegateCommand<string> OpCmd
{
get
{
if (_opCmd == null)
{
_opCmd = new DelegateCommand<string>(OpCmdCall);
}
return _opCmd;
}
}
/// <summary>
/// 操作命令处理。
/// </summary>
/// <param name="par">参数。</param>
private void OpCmdCall(string par)
{
switch (par)
{
case "Add":
MessageBox.Show("调度表项由【读写设置】中的写入配置通过“加入定时调度表”生成,这里不支持新增。", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
break;
case "Delete":
if (CurSelectedItem != null)
{
FreeSql.Delete<CANFdScheduleConfig>(CurSelectedItem.Id).ExecuteAffrows();
ListCANFdScheduleConfigDto.Remove(CurSelectedItem);
CurSelectedItem = null;
}
else
{
MessageBox.Show("请选中后再进行【删除】操作?", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
}
break;
default:
break;
}
}
private DelegateCommand _saveCmd;
/// <summary>
/// 保存命令。
/// </summary>
public DelegateCommand SaveCmd
{
get
{
if (_saveCmd == null)
{
_saveCmd = new DelegateCommand(SaveCmdMethod);
}
return _saveCmd;
}
}
/// <summary>
/// 保存命令处理。
/// </summary>
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<CANFdScheduleConfig>()
.SetSource(Mapper.Map<CANFdScheduleConfig>(item))
.ExecuteAffrows();
}
// 保存完成后重载:
// - 避免 UI 集合与数据库实际数据不一致;
// - 同时让新增项获取真实 Id。
ListCANFdScheduleConfigDto = new ObservableCollection<CANFdScheduleConfigDto>(
Mapper.Map<List<CANFdScheduleConfigDto>>(
FreeSql.Select<CANFdScheduleConfig>()
.Where(a => a.CanLinConfigProId == SelectCanLinConfigProId)
.ToList()));
DialogParameters pars = new DialogParameters
{
{ "ReturnValue", ListCANFdScheduleConfigDto }
};
RaiseRequestClose(new DialogResult(ButtonResult.OK, pars));
}
private DelegateCommand _cancelCmd;
/// <summary>
/// 取消命令。
/// </summary>
public DelegateCommand CancelCmd
{
get
{
if (_cancelCmd == null)
{
_cancelCmd = new DelegateCommand(CancelCmdMethod);
}
return _cancelCmd;
}
}
/// <summary>
/// 取消命令处理。
/// </summary>
private void CancelCmdMethod()
{
RaiseRequestClose(new DialogResult(ButtonResult.Cancel));
}
/// <summary>
/// 弹窗打开时参数接收并初始化 UI。
/// 说明:
/// - ListMsg用于构造消息名下拉框候选
/// - ListCANFdScheduleConfigDto用于回显当前调度表
/// - SelectCanLinConfigProId用于 Save 后按配置程序 id 重载。
/// </summary>
/// <param name="parameters">参数。</param>
public override void OnDialogOpened(IDialogParameters parameters)
{
if (parameters.ContainsKey("ListMsg"))
{
ListMsg = parameters.GetValue<List<string>>("ListMsg");
if (ListMsg != null)
{
// 转换为 CbxItems 集合Key/Text 都使用消息名)。
MsgCbxItems = new ObservableCollection<CbxItems>(
ListMsg.Select(value => new CbxItems
{
Key = value,
Text = value
}));
}
}
ListCANFdScheduleConfigDto = parameters.GetValue<ObservableCollection<CANFdScheduleConfigDto>>("ListCANFdScheduleConfigDto");
if (ListCANFdScheduleConfigDto == null) ListCANFdScheduleConfigDto = new ObservableCollection<CANFdScheduleConfigDto>();
if (ListCANFdScheduleConfigDto.Count > 0)
{
// 回显:默认取第一条的 OrderSend。
CurSendOrder = ListCANFdScheduleConfigDto.FirstOrDefault()!.OrderSend.ToString();
}
else
{
// 默认:顺序发送。
CurSendOrder = "1";
}
SelectCanLinConfigProId = parameters.GetValue<long>("SelectCanLinConfigProId");
}
}
}

View File

@@ -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
{
/// <summary>
/// ZLG CAN 调度表配置
/// </summary>
public class DialogZlgCANSchConfigViewModel : DialogViewModel
{
/// <summary>
/// 构造函数
/// </summary>
public DialogZlgCANSchConfigViewModel(IFreeSql freeSql, IMapper mapper)
{
Title = "调度表 CAN 配置";
FreeSql = freeSql;
Mapper = mapper;
SendOrderCbxItems = new ObservableCollection<CbxItems>()
{
new CbxItems(){
Key="0",
Text="并行",
},
new CbxItems(){
Key="1",
Text="顺序",
},
};
//默认只能用1号调度器
SchTabIndexCbxItems = new ObservableCollection<CbxItems>()
{
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",
//},
};
}
/// <summary>
/// FreeSql用于调度表项的删除/保存落库)。
/// </summary>
public IFreeSql FreeSql { get; }
/// <summary>
/// AutoMapperDTO 与实体映射)。
/// </summary>
public IMapper Mapper { get; }
private string name;
/// <summary>
/// 名称
/// </summary>
public string Name
{
get { return name; }
set { name = value; RaisePropertyChanged(); }
}
private ObservableCollection<CANScheduleConfigDto> _ListCANScheduleConfigDto = new ObservableCollection<CANScheduleConfigDto>();
/// <summary>
/// CAN 调度表数据集合
/// </summary>
public ObservableCollection<CANScheduleConfigDto> ListCANScheduleConfigDto
{
get { return _ListCANScheduleConfigDto; }
set { _ListCANScheduleConfigDto = value; RaisePropertyChanged(); }
}
/// <summary>
/// 消息/帧报文信息集合
/// </summary>
public List<string>? ListMsg { get; set; }
private ObservableCollection<CbxItems> _MsgCbxItems;
/// <summary>
/// 消息名称 集合信息
/// </summary>
public ObservableCollection<CbxItems> MsgCbxItems
{
get { return _MsgCbxItems; }
set { _MsgCbxItems = value; RaisePropertyChanged(); }
}
/// <summary>
/// 选中的程序的Id
/// </summary>
public long SelectCanLinConfigProId { get; set; }
private ObservableCollection<CbxItems> _SendOrderCbxItems;
/// <summary>
/// 发送方式 集合信息
/// </summary>
public ObservableCollection<CbxItems> SendOrderCbxItems
{
get { return _SendOrderCbxItems; }
set { _SendOrderCbxItems = value; RaisePropertyChanged(); }
}
private string? _CurSendOrder;
/// <summary>
/// 当前发送方式
/// </summary>
public string? CurSendOrder
{
get { return _CurSendOrder; }
set { _CurSendOrder = value; RaisePropertyChanged(); }
}
private ObservableCollection<CbxItems> _SchTabIndexCbxItems;
/// <summary>
/// 调度器序号 集合信息
/// </summary>
public ObservableCollection<CbxItems> SchTabIndexCbxItems
{
get { return _SchTabIndexCbxItems; }
set { _SchTabIndexCbxItems = value; RaisePropertyChanged(); }
}
private CANScheduleConfigDto _CurSelectedItem;
/// <summary>
/// 当前选中的调度表项(用于删除等操作)。
/// </summary>
public CANScheduleConfigDto CurSelectedItem
{
get { return _CurSelectedItem; }
set { _CurSelectedItem = value; RaisePropertyChanged(); }
}
private DelegateCommand<object> _GridSelectionChangedCmd;
/// <summary>
/// DataGrid 选中行变化。
/// </summary>
public DelegateCommand<object> GridSelectionChangedCmd
{
set
{
_GridSelectionChangedCmd = value;
}
get
{
if (_GridSelectionChangedCmd == null)
{
_GridSelectionChangedCmd = new DelegateCommand<object>((par) => GridSelectionChangedCmdMethod(par));
}
return _GridSelectionChangedCmd;
}
}
private void GridSelectionChangedCmdMethod(object par)
{
// 先判断是否是正确的集合数据,防止 DataGrid 的 ItemsSource 刷新导致的误触发。
var Selecteddata = par as CANScheduleConfigDto;
if (Selecteddata != null)
{
CurSelectedItem = Selecteddata;
}
}
private DelegateCommand<string> _OpCmd;
/// <summary>
/// 操作命令(删除)。
/// 说明:新增调度表项由“读写设置”弹窗内的“加入定时调度表”生成,此处仅允许删除与保存。
/// </summary>
public DelegateCommand<string> OpCmd
{
set
{
_OpCmd = value;
}
get
{
if (_OpCmd == null)
{
_OpCmd = new DelegateCommand<string>((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<CANScheduleConfig>(CurSelectedItem.Id).ExecuteAffrows();
ListCANScheduleConfigDto.Remove(CurSelectedItem);
CurSelectedItem = null;
}
else
{
MessageBox.Show("请选中后再进行【删除】操作?", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
}
break;
default:
break;
}
}
private DelegateCommand saveCmd;
/// <summary>
/// 保存命令
/// </summary>
public DelegateCommand SaveCmd
{
set
{
saveCmd = value;
}
get
{
if (saveCmd == null)
{
saveCmd = new DelegateCommand(() => SaveCmdMethod());
}
return saveCmd;
}
}
/// <summary>
/// 保存命令方法。
/// 说明:
/// - 先做必要校验MsgName/Cycle/发送方式等),避免写入无效配置;
/// - 使用 InsertOrUpdate 保证“新增/修改”统一处理;
/// - 保存完成后重新从数据库加载,作为最终权威数据,并通过 DialogResult 回传给调用方。
/// </summary>
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<CANScheduleConfig>()
.SetSource(Mapper.Map<CANScheduleConfig>(item)).
ExecuteAffrows();
}
// 保存完成后重新加载:
// - 防止 UI 缓存与数据库实际数据不一致;
// - 同时也让新增项拿到真实 Id。
ListCANScheduleConfigDto = new ObservableCollection<CANScheduleConfigDto>(Mapper.Map<List<CANScheduleConfigDto>>(FreeSql.Select<CANScheduleConfig>().Where(a => a.CanLinConfigProId == SelectCanLinConfigProId).ToList()));
DialogParameters pars = new DialogParameters
{
{ "ReturnValue", ListCANScheduleConfigDto }
};
RaiseRequestClose(new DialogResult(ButtonResult.OK, pars));
}
private DelegateCommand cancelCmd;
/// <summary>
/// 取消命令。
/// </summary>
public DelegateCommand CancelCmd
{
set
{
cancelCmd = value;
}
get
{
if (cancelCmd == null)
{
cancelCmd = new DelegateCommand(() => CancelCmdMethod());
}
return cancelCmd;
}
}
/// <summary>
/// 取消命令方法。
/// </summary>
private void CancelCmdMethod()
{
RaiseRequestClose(new DialogResult(ButtonResult.Cancel));
}
/// <summary>
/// 弹窗打开时接收参数并初始化 UI。
/// 说明:
/// - ListMsg 用于构造消息名下拉框;
/// - ListCANScheduleConfigDto 用于回显现有调度表;
/// - SelectCanLinConfigProId 用于 Save 时按配置程序 id 过滤重载。
/// </summary>
/// <param name="parameters"></param>
public override void OnDialogOpened(IDialogParameters parameters)
{
if (parameters.ContainsKey("ListMsg"))
{
ListMsg = parameters.GetValue<List<string>>("ListMsg");
if (ListMsg != null)
{
// 转换为CbxItems集合都是文本内容
MsgCbxItems = new ObservableCollection<CbxItems>(
ListMsg.Select(value => new CbxItems
{
Key = value,
Text = value
}));
}
}
ListCANScheduleConfigDto = parameters.GetValue<ObservableCollection<CANScheduleConfigDto>>("ListCANScheduleConfigDto");
//防止返回的数据为空,就无法增加了
if (ListCANScheduleConfigDto == null) ListCANScheduleConfigDto = new ObservableCollection<CANScheduleConfigDto>();
//Name = parameters.GetValue<string>("Name");
if (ListCANScheduleConfigDto.Count > 0)
{
// 回显:默认取第一条的 OrderSend 作为当前下拉框选择。
CurSendOrder = ListCANScheduleConfigDto.FirstOrDefault()!.OrderSend.ToString();
}
else
{
// 默认:顺序发送。
CurSendOrder = "1";
}
SelectCanLinConfigProId = parameters.GetValue<long>("SelectCanLinConfigProId");
}
}
}

View File

@@ -27,6 +27,44 @@ namespace CapMachine.Wpf.ViewModels
private long _canLinConfigProId;
private bool _enableHardwareCycleSchedule = true;
private bool _useCanFdSchedule;
/// <summary>
/// 是否启用“加入定时调度表ZLG auto_send”能力。
/// 说明:该能力仅适用于 ZLG CAN/CANFD 硬件 auto_sendLIN 当前使用软件调度,不允许写入 CANScheduleConfig。
/// </summary>
public bool EnableHardwareCycleSchedule
{
get { return _enableHardwareCycleSchedule; }
private set
{
_enableHardwareCycleSchedule = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(CanAddCycleTimeSch));
}
}
/// <summary>
/// 是否允许执行“加入定时调度表”操作。
/// 说明:
/// - 需要 <see cref="EnableHardwareCycleSchedule"/> 打开(调用方允许硬件调度表能力);
/// - 且弹窗处于可编辑态(<see cref="IsEditable"/>)。
/// </summary>
public bool CanAddCycleTimeSch
{
get { return EnableHardwareCycleSchedule && IsEditable; }
}
/// <summary>
/// 是否使用 CANFD 调度表(决定“加入定时调度表”写入 <see cref="CANFdScheduleConfig"/> 还是 <see cref="CANScheduleConfig"/>)。
/// </summary>
public bool UseCanFdSchedule
{
get { return _useCanFdSchedule; }
private set { _useCanFdSchedule = value; RaisePropertyChanged(); }
}
/// <summary>
/// 构造函数。
/// </summary>
@@ -153,6 +191,105 @@ namespace CapMachine.Wpf.ViewModels
/// </summary>
public CanLinRWConfigDto? SelectedWriteConfig { get; set; }
private DelegateCommand? _addCycleTimeSch;
/// <summary>
/// 将当前选中的写入配置对应的报文加入“硬件定时调度表”ZLG auto_send
/// </summary>
/// <remarks>
/// 说明:
/// - 调度表按“报文/帧MsgFrameName”维度配置同一报文可包含多个写入信号但调度表只需要一条。
/// - 该命令会直接落库到 <see cref="CANScheduleConfig"/>,随后由主界面刷新配置程序时加载显示。
/// </remarks>
public DelegateCommand AddCycleTimeSch => _addCycleTimeSch ??= new DelegateCommand(AddCycleTimeSchMethod);
/// <summary>
/// 加入定时调度表命令处理。
/// </summary>
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<CANFdScheduleConfig>()
.Where(a => a.CanLinConfigProId == _canLinConfigProId)
.Where(a => a.MsgName == msgName)
.Any()
: _freeSql.Select<CANScheduleConfig>()
.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<CANFdScheduleConfig>(new CANFdScheduleConfig
{
CanLinConfigProId = _canLinConfigProId,
MsgName = msgName,
Cycle = 100,
OrderSend = 1,
SchTabIndex = 0,
}).ExecuteAffrows();
}
else
{
_freeSql.Insert<CANScheduleConfig>(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);
}
}
/// <summary>
/// 当前选中的读取配置行。
/// </summary>
@@ -402,8 +539,18 @@ namespace CapMachine.Wpf.ViewModels
{
_canLinConfigProId = parameters.GetValue<long>("CanLinConfigProId");
EnableHardwareCycleSchedule = parameters.ContainsKey("EnableHardwareCycleSchedule")
? parameters.GetValue<bool>("EnableHardwareCycleSchedule")
: true;
// 兼容参数命名:调用方传入的 key 为 "IsCanFdSchedule"。
UseCanFdSchedule = parameters.ContainsKey("IsCanFdSchedule")
? parameters.GetValue<bool>("IsCanFdSchedule")
: false;
IsEditable = parameters.ContainsKey("IsEditable") ? parameters.GetValue<bool>("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<string>(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<CanLinRWConfig>(old.Id).ExecuteAffrows();
}
}

View File

@@ -25,11 +25,42 @@ namespace CapMachine.Wpf.ViewModels
/// <summary>
/// ZLG CAN/CANFD 合并配置 ViewModel模式切换单选
/// </summary>
/// <remarks>
/// 该 ViewModel 负责 ZLG CAN/CANFD 的 UI 配置与用户交互编排,典型职责包括:
/// - 打开/关闭设备、切换模式CAN/CANFD并与 <see cref="ZlgCanDriveService"/> 同步状态;
/// - 加载 DBC 并维护可绑定的信号集合(用于读写设置与实时显示);
/// - 管理“读写设置”弹窗的数据准备与回写(写入/读取规则、信号候选集合);
/// - 管理调度表/循环发送相关 UI 状态(软件调度 vs 硬件 auto_send 的选择由 Service/Driver 实现)。
///
/// 线程说明:
/// - 本类绝大多数方法由 UI 线程调用;
/// - Driver 接收线程产生的数据更新事件若需要影响 UI应通过属性通知或 Dispatcher 进行线程切换(具体由 Service/Driver 侧处理)。
/// </remarks>
public class ZlgCanDriveConfigViewModel : NavigationViewModel
{
/// <summary>
/// 构造函数。
/// </summary>
/// <param name="dialogService">Prism 弹窗服务,用于打开读写设置等对话框。</param>
/// <param name="freeSql">数据访问组件,用于读取/保存配置程序与读写配置等持久化数据。</param>
/// <param name="eventAggregator">事件聚合器,用于跨模块消息通知(如状态变化/日志等)。</param>
/// <param name="regionManager">区域导航服务,用于页面/视图切换。</param>
/// <param name="sysRunService">系统运行服务(与全局运行态/权限等相关)。</param>
/// <param name="comActionService">通信互斥控制服务,用于保证 CAN/LIN 等通道操作的互斥与安全。</param>
/// <param name="logService">日志服务,用于输出 UI 层操作过程与异常。</param>
/// <param name="logicRuleService">逻辑规则服务,为“写入规则/触发规则”下拉框提供数据源。</param>
/// <param name="configService">配置服务,用于读取/应用系统配置参数。</param>
/// <param name="zlgCanDriveService">ZLG CAN/CANFD 服务层负责驱动生命周期、DBC、发送/调度表编排。</param>
/// <param name="zlgLinDriveService">ZLG LIN 服务层,用于互斥判断或组合配置场景。</param>
/// <param name="mapper">对象映射器,用于 DTO/实体之间转换。</param>
/// <remarks>
/// 构造时会初始化:
/// - 波特率下拉数据源;
/// - 写入规则下拉数据源;
/// - CAN 配置程序列表(用于 UI 选择)。
///
/// 注意:构造函数仅做“数据源初始化与依赖注入”,不会直接打开设备。
/// </remarks>
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();
}
/// <summary>
/// 初始化“写入规则”下拉框数据源。
/// </summary>
/// <remarks>
/// 数据源来自 <see cref="LogicRuleService.LogicRuleDtos"/>。
/// - Key规则 Id
/// - Text规则名称
///
/// 该集合通常被“读写设置”弹窗中的 ComboBox 绑定使用。
/// </remarks>
private void InitWriteRuleCbx()
{
WriteRuleCbxItems = new ObservableCollection<CbxItems>();
@@ -111,6 +152,20 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 打开“读写设置”弹窗。
/// </summary>
/// <remarks>
/// 弹窗数据准备流程:
/// - 基于当前页面的 <see cref="ListWriteCanLinRWConfigDto"/> / <see cref="ListReadCanLinRWConfigDto"/> 克隆出可编辑副本;
/// - 根据 <see cref="ListCanDbcModel"/> 构建信号候选集合MsgName/SignalName/描述等);
/// - 通过 <see cref="DialogService"/> 打开 <c>DialogZlgCanLinRwConfigView</c>
/// - 用户点击 OK 后回调中调用 <c>ReloadCurrentConfigPro</c> 刷新当前配置并提示。
///
/// 前置条件:
/// - 必须先选择配置程序(<see cref="SelectCanLinConfigPro"/> 不为 null
/// - 推荐调用方在进入本方法前确保已连接设备且 DBC 已加载,否则候选信号集合可能为空。
/// </remarks>
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
}
}
/// <summary>
/// 克隆一个读写配置 DTO。
/// </summary>
/// <param name="src">源对象。</param>
/// <returns>新实例(字段逐个复制)。</returns>
/// <remarks>
/// 目的:
/// - 弹窗编辑时不直接修改页面当前集合,避免用户取消时污染原始配置;
/// - 用户确认OK后再由弹窗 ViewModel 负责持久化,页面侧随后通过 Reload 刷新。
/// </remarks>
private static CanLinRWConfigDto CloneRwDto(CanLinRWConfigDto src)
{
return new CanLinRWConfigDto
@@ -255,6 +321,9 @@ namespace CapMachine.Wpf.ViewModels
/// </summary>
public IMapper Mapper { get; }
/// <summary>
/// <see cref="ArbBaudRateCbxItems"/> 的 backing 字段。
/// </summary>
private ObservableCollection<CbxItems> _arbBaudRateCbxItems = new ObservableCollection<CbxItems>();
/// <summary>
/// 仲裁波特率下拉项bps
@@ -265,6 +334,9 @@ namespace CapMachine.Wpf.ViewModels
set { _arbBaudRateCbxItems = value; RaisePropertyChanged(); }
}
/// <summary>
/// <see cref="DataBaudRateCbxItems"/> 的 backing 字段。
/// </summary>
private ObservableCollection<CbxItems> _dataBaudRateCbxItems = new ObservableCollection<CbxItems>();
/// <summary>
/// 数据波特率下拉项bps
@@ -275,9 +347,23 @@ namespace CapMachine.Wpf.ViewModels
set { _dataBaudRateCbxItems = value; RaisePropertyChanged(); }
}
/// <summary>
/// 配置程序列表的本地缓存(数据库读取结果)。
/// </summary>
/// <remarks>
/// 与 <see cref="ListCanLinConfigPro"/> 的区别:
/// - 本字段用于保存从数据库一次性读取的实体列表;
/// - UI 绑定通常使用 <see cref="ListCanLinConfigPro"/>ObservableCollection
/// </remarks>
private List<CanLinConfigPro> _canLinConfigPros = new List<CanLinConfigPro>();
private ObservableCollection<CbxItems> _writeRuleCbxItems = new ObservableCollection<CbxItems>();
/// <summary>
/// 写入规则下拉项(逻辑规则列表)。
/// </summary>
/// <remarks>
/// 该集合由 <see cref="InitWriteRuleCbx"/> 初始化,通常绑定到读写设置弹窗或列表编辑处的 ComboBox。
/// </remarks>
public ObservableCollection<CbxItems> WriteRuleCbxItems
{
get { return _writeRuleCbxItems; }
@@ -285,6 +371,19 @@ namespace CapMachine.Wpf.ViewModels
}
private ObservableCollection<CanLinRWConfigDto> _listWriteCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>();
/// <summary>
/// 写入配置集合(绑定到写入 DataGrid
/// </summary>
/// <remarks>
/// 每条记录对应一个“写入信号”配置项,包含:
/// - MsgFrameName/SignalName定位 DBC 信号
/// - LogicRuleId写入规则
/// - DefautValue默认写入值
///
/// 注意:
/// - 配置程序激活(<see cref="IsCanConfigProActive"/> true时应禁止编辑。
/// - 读写互斥/重复校验主要在读写设置弹窗 ViewModel 中实现。
/// </remarks>
public ObservableCollection<CanLinRWConfigDto> ListWriteCanLinRWConfigDto
{
get { return _listWriteCanLinRWConfigDto; }
@@ -292,13 +391,36 @@ namespace CapMachine.Wpf.ViewModels
}
private ObservableCollection<CanLinRWConfigDto> _listReadCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>();
/// <summary>
/// 读取配置集合(绑定到读取 DataGrid
/// </summary>
/// <remarks>
/// 每条记录对应一个“读取信号”配置项,包含:
/// - MsgFrameName/SignalName定位 DBC 信号
/// - DefautValue默认值/初始值(视业务语义而定)
///
/// 读取配置通常用于“从接收帧解码后更新 UI/变量”的目标信号集合。
/// </remarks>
public ObservableCollection<CanLinRWConfigDto> ListReadCanLinRWConfigDto
{
get { return _listReadCanLinRWConfigDto; }
set { _listReadCanLinRWConfigDto = value; RaisePropertyChanged(); }
}
/// <summary>
/// 当前在写入 DataGrid 中选中的行。
/// </summary>
/// <remarks>
/// 用于 Delete 等操作定位目标记录。该属性不直接通知 UI由 SelectionChanged 命令更新)。
/// </remarks>
private CanLinRWConfigDto? SelectedWriteCanLinRWConfigDto { get; set; }
/// <summary>
/// 当前在读取 DataGrid 中选中的行。
/// </summary>
/// <remarks>
/// 用于 Delete 等操作定位目标记录。该属性不直接通知 UI由 SelectionChanged 命令更新)。
/// </remarks>
private CanLinRWConfigDto? SelectedReadCanLinRWConfigDto { get; set; }
private string? _opTip;
@@ -352,6 +474,9 @@ namespace CapMachine.Wpf.ViewModels
set { _isCANConfigDatagridActive = value; RaisePropertyChanged(); }
}
/// <summary>
/// <see cref="SelectedMode"/> 的 backing 字段。
/// </summary>
private ZlgCanMode _selectedMode;
/// <summary>
/// 模式选择CAN/CANFD单选
@@ -431,7 +556,16 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 写入 DataGrid 选中项变化命令。
/// </summary>
/// <remarks>
/// 由 XAML 绑定到 SelectionChanged用于把当前选中行同步到 <see cref="SelectedWriteCanLinRWConfigDto"/>。
/// </remarks>
private DelegateCommand<object>? _writeGridSelectionChangedCmd;
/// <summary>
/// 写入 DataGrid 选中项变化命令(供 XAML 绑定)。
/// </summary>
public DelegateCommand<object> WriteGridSelectionChangedCmd
{
get
@@ -444,6 +578,10 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 写入 DataGrid SelectionChanged 命令处理。
/// </summary>
/// <param name="par">选中项(期望为 <see cref="CanLinRWConfigDto"/>)。</param>
private void WriteGridSelectionChangedCmdMethod(object par)
{
if (par is CanLinRWConfigDto dto)
@@ -452,7 +590,16 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 读取 DataGrid 选中项变化命令。
/// </summary>
/// <remarks>
/// 由 XAML 绑定到 SelectionChanged用于把当前选中行同步到 <see cref="SelectedReadCanLinRWConfigDto"/>。
/// </remarks>
private DelegateCommand<object>? _readGridSelectionChangedCmd;
/// <summary>
/// 读取 DataGrid 选中项变化命令(供 XAML 绑定)。
/// </summary>
public DelegateCommand<object> ReadGridSelectionChangedCmd
{
get
@@ -465,6 +612,10 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 读取 DataGrid SelectionChanged 命令处理。
/// </summary>
/// <param name="par">选中项(期望为 <see cref="CanLinRWConfigDto"/>)。</param>
private void ReadGridSelectionChangedCmdMethod(object par)
{
if (par is CanLinRWConfigDto dto)
@@ -473,7 +624,18 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 写入配置操作命令(保存/删除)。
/// </summary>
/// <remarks>
/// <paramref name="par"/> 约定:
/// - "Edit":把 DataGrid 中当前列表逐条更新到数据库;
/// - "Delete":删除当前选中项。
/// </remarks>
private DelegateCommand<string>? _writeCmd;
/// <summary>
/// 写入配置操作命令(供 XAML 绑定)。
/// </summary>
public DelegateCommand<string> WriteCmd
{
get
@@ -486,6 +648,15 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 写入配置操作命令处理。
/// </summary>
/// <param name="par">操作类型("Edit"/"Delete")。</param>
/// <remarks>
/// 前置条件:
/// - 必须已选择配置程序(<see cref="SelectCanLinConfigPro"/> 不为 null
/// - 若配置已激活(<see cref="IsCanConfigProActive"/> true禁止修改写入配置。
/// </remarks>
private void WriteCmdMethod(string par)
{
if (SelectCanLinConfigPro == null)
@@ -528,7 +699,18 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 读取配置操作命令(保存/删除)。
/// </summary>
/// <remarks>
/// <paramref name="par"/> 约定:
/// - "Edit":把 DataGrid 中当前列表逐条更新到数据库;
/// - "Delete":删除当前选中项。
/// </remarks>
private DelegateCommand<string>? _readCmd;
/// <summary>
/// 读取配置操作命令(供 XAML 绑定)。
/// </summary>
public DelegateCommand<string> ReadCmd
{
get
@@ -541,6 +723,15 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 读取配置操作命令处理。
/// </summary>
/// <param name="par">操作类型("Edit"/"Delete")。</param>
/// <remarks>
/// 前置条件:
/// - 必须已选择配置程序(<see cref="SelectCanLinConfigPro"/> 不为 null
/// - 若配置已激活(<see cref="IsCanConfigProActive"/> true禁止修改读取配置。
/// </remarks>
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文件路径:"; }
}
/// <summary>
/// <see cref="ListCanLinConfigPro"/> 的 backing 字段。
/// </summary>
private ObservableCollection<CanLinConfigPro>? _listCanLinConfigPro;
/// <summary>
/// 配置程序集合。
@@ -716,6 +910,9 @@ namespace CapMachine.Wpf.ViewModels
private CanDbcModel? _selectedCanDbcModel;
/// <summary>
/// <see cref="SelectedCANConfigExdDto"/> 的 backing 字段。
/// </summary>
private CANConfigExdDto? _selectedCANConfigExdDto;
/// <summary>
/// CAN 配置 DTO。
@@ -726,6 +923,9 @@ namespace CapMachine.Wpf.ViewModels
set { _selectedCANConfigExdDto = value; RaisePropertyChanged(); }
}
/// <summary>
/// <see cref="SelectedCANFdConfigExdDto"/> 的 backing 字段。
/// </summary>
private CANFdConfigExdDto? _selectedCANFdConfigExdDto;
/// <summary>
/// CANFD 配置 DTO。
@@ -736,6 +936,18 @@ namespace CapMachine.Wpf.ViewModels
set { _selectedCANFdConfigExdDto = value; RaisePropertyChanged(); }
}
/// <summary>
/// 从数据库加载 CAN/CANFD 配置程序列表。
/// </summary>
/// <remarks>
/// 加载内容包含:
/// - 主表 <see cref="CanLinConfigPro"/>
/// - 扩展配置CAN/CANFD
/// - 读写配置项(包含逻辑规则)
/// - 调度表配置项CAN/CANFD
///
/// 该方法会刷新本地缓存 <see cref="_canLinConfigPros"/> 与 UI 绑定集合 <see cref="ListCanLinConfigPro"/>。
/// </remarks>
private void InitLoadCanConfigPro()
{
_canLinConfigPros = FreeSql.Select<CanLinConfigPro>()
@@ -753,6 +965,18 @@ namespace CapMachine.Wpf.ViewModels
/// <summary>
/// 同步选中的配置。
/// </summary>
/// <remarks>
/// 当 UI 选中不同配置程序时,本方法会同步以下内容到 ViewModel
/// - 根据配置程序的 CANLINInfo 自动切换 <see cref="SelectedMode"/>(避免右侧参数/DBC 显示与配置类型不一致);
/// - 映射 CAN/CANFD 两套扩展 DTO若不存在则为 null/默认);
/// - 从配置内容构建读写配置 DTO 列表;
/// - 根据模式加载对应调度表集合;
/// - 构建并注入 CmdData 到 <see cref="ZlgCanDriveService"/>(事件驱动发送依赖);
/// - 最后重置并尝试加载当前配置的 DBC 信号集合(未连接时也会尝试解析)。
///
/// 注意:
/// - 已连接或已激活时,外层命令会禁止切换配置程序;该方法通常只在允许切换时被调用。
/// </remarks>
private void SyncSelectedConfig()
{
if (SelectCanLinConfigPro == null)
@@ -828,6 +1052,16 @@ namespace CapMachine.Wpf.ViewModels
/// <summary>
/// 切换配置程序时重置并尝试加载当前配置对应的 DBC 信号集合。
/// </summary>
/// <remarks>
/// 背景当“未连接设备”时如果切换配置程序但不清空信号集合UI 会残留上一次配置的信号列表。
///
/// 行为:
/// - 先清空 <see cref="SelectedCanDbcModel"/>
/// - 未连接时:
/// - 若当前配置未设置 DBC 路径,则信号集合置空;
/// - 若有 DBC 路径,则调用 <see cref="ZlgCanDriveService.StartDbc"/> 解析并加载信号集合;
/// - 已连接时:理论上禁止切换配置程序;这里做保底清空,避免 UI 残留。
/// </remarks>
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<CanDbcModel>();
// 已连接时:禁止切换配置程序。
// 注意:此处不应清空信号集合,否则在“保存读写设置/刷新配置”这类操作中会导致信号表瞬间变空。
return;
}
/// <summary>
/// <see cref="ListCANScheduleConfigDto"/> 的 backing 字段。
/// </summary>
private ObservableCollection<CANScheduleConfigDto> _listCANScheduleConfigDto = new ObservableCollection<CANScheduleConfigDto>();
/// <summary>
/// CAN 调度表集合。
@@ -870,6 +1112,9 @@ namespace CapMachine.Wpf.ViewModels
set { _listCANScheduleConfigDto = value; RaisePropertyChanged(); }
}
/// <summary>
/// <see cref="ListCANFdScheduleConfigDto"/> 的 backing 字段。
/// </summary>
private ObservableCollection<CANFdScheduleConfigDto> _listCANFdScheduleConfigDto = new ObservableCollection<CANFdScheduleConfigDto>();
/// <summary>
/// CANFD 调度表集合。
@@ -981,12 +1226,24 @@ namespace CapMachine.Wpf.ViewModels
/// <summary>
/// 构建并加载命令数据到驱动。
/// </summary>
/// <remarks>
/// 数据流:
/// - 从当前配置程序的写入项RW=Write构建 <see cref="CanCmdData"/> 列表;
/// - 每条写入项会携带 MsgName/SignalName/默认值/逻辑规则;
/// - 最终调用 <see cref="ZlgCanDriveService.LoadCmdDataToDrive"/> 注入驱动。
///
/// 作用:
/// - 事件驱动发送:信号值变化可触发增量发送/覆盖 auto_send
/// - 调度表/循环发送:编码发送依赖当前 CmdData 中的信号值。
/// </remarks>
private void BuildAndLoadCmdDataToDrive()
{
try
{
if (SelectCanLinConfigPro?.CanLinConfigContents == null)
{
// 防御性清空:
// - 当前未选择配置程序或无配置内容时,服务层应收到空 CmdData避免沿用旧配置继续触发事件驱动发送。
ZlgCanDriveService.LoadCmdDataToDrive(new List<CanCmdData>());
return;
}
@@ -998,6 +1255,10 @@ namespace CapMachine.Wpf.ViewModels
var cmdList = new List<CanCmdData>();
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
/// <summary>
/// 匹配选中的配置和 DBC 模型。
/// </summary>
/// <remarks>
/// 用途:对信号集合做“是否已被配置为写入/读取”的标记,便于 UI 颜色或状态提示。
/// - RW.Write 记为 1
/// - RW.Read 记为 2
/// - 未配置记为 0
///
/// 该标记依赖 <see cref="SelectCanLinConfigPro.CanLinConfigContents"/> 与 <see cref="ListCanDbcModel"/>。
/// </remarks>
private void MatchSeletedAndCanDbcModel()
{
if (ListCanDbcModel == null || ListCanDbcModel.Count == 0) return;
@@ -1175,10 +1448,17 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 配置程序 DataGrid 选中项变化命令的 backing 字段。
/// </summary>
private DelegateCommand<object>? _canConfigProGridSelectionChangedCmd;
/// <summary>
/// 配置程序选中变化。
/// </summary>
/// <remarks>
/// - 打开/激活状态下禁止切换配置程序;
/// - 允许切换时会更新 <see cref="SelectCanLinConfigPro"/> 并调用 <see cref="SyncSelectedConfig"/>。
/// </remarks>
public DelegateCommand<object> CanConfigProGridSelectionChangedCmd
{
get
@@ -1191,6 +1471,17 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 配置程序 DataGrid SelectionChanged 命令处理。
/// </summary>
/// <param name="par">
/// 可能为:
/// - <see cref="CanLinConfigPro"/>(直接传入选中项);
/// - <see cref="SelectionChangedEventArgs"/>WPF SelectionChanged 参数)。
/// </param>
/// <remarks>
/// WPF 的 SelectionChanged 可能被重复触发/不同类型参数混用,因此这里做了多种分支兼容。
/// </remarks>
private void CanConfigProGridSelectionChangedCmdMethod(object par)
{
if (par == null) return;
@@ -1225,6 +1516,9 @@ namespace CapMachine.Wpf.ViewModels
SyncSelectedConfig();
}
/// <summary>
/// 配置程序切换前拦截命令的 backing 字段。
/// </summary>
private DelegateCommand<object>? _canConfigProGridPreviewMouseLeftButtonDownCmd;
/// <summary>
/// 配置程序切换前拦截(对齐图莫斯 PreviewMouseLeftButtonDown
@@ -1242,6 +1536,13 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 配置程序切换前拦截命令处理。
/// </summary>
/// <param name="par">鼠标事件参数(可能为 <see cref="System.Windows.Input.MouseButtonEventArgs"/>)。</param>
/// <remarks>
/// 若拦截命中,会将 MouseButtonEventArgs.Handled 置为 true阻止 DataGrid 继续改变选中项。
/// </remarks>
private void CanConfigProGridPreviewMouseLeftButtonDownCmdMethod(object par)
{
try
@@ -1266,10 +1567,17 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 选择 DBC 文件命令的 backing 字段。
/// </summary>
private DelegateCommand? _loadDbcCmd;
/// <summary>
/// 选择 DBC 文件。
/// </summary>
/// <remarks>
/// 该命令只负责让用户选择文件并写回 <see cref="CurrentDbcPath"/>
/// 不会自动解析;解析由 CAN 操作命令中的 "Parse" 或连接流程中的自动解析触发。
/// </remarks>
public DelegateCommand LoadDbcCmd
{
get
@@ -1282,6 +1590,14 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 选择 DBC 文件命令处理。
/// </summary>
/// <remarks>
/// 前置条件:必须已选择配置程序(<see cref="SelectCanLinConfigPro"/>)。
///
/// 选择成功后会更新 <see cref="CurrentDbcPath"/>;用户取消则保持原值。
/// </remarks>
private void LoadDbcCmdMethod()
{
try
@@ -1324,10 +1640,17 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 调度使能下发命令的 backing 字段。
/// </summary>
private DelegateCommand<object>? _schEnableCmd;
/// <summary>
/// 调度使能写入驱动。
/// </summary>
/// <remarks>
/// 该命令用于把 UI 的 <see cref="CurrentSchEnable"/> 立即同步到 <see cref="ZlgCanDriveService.SchEnable"/>。
/// - SchEnable 影响事件驱动发送与调度表/auto_send 相关行为。
/// </remarks>
public DelegateCommand<object> SchEnableCmd
{
get
@@ -1340,16 +1663,37 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 调度使能同步到驱动。
/// </summary>
/// <param name="par">未使用(与 XAML CommandParameter 兼容)。</param>
private void SchEnableCmdCall(object par)
{
// 与旧 Toomoss 行为对齐UI 勾选后立即下发到驱动
// 说明:
// - SchEnable 控制“事件驱动发送/覆盖更新 auto_send”的总开关
// - 仅修改 CurrentSchEnableDTO不会影响驱动必须执行该命令或在 CycleSend 开启前同步)。
ZlgCanDriveService.SchEnable = CurrentSchEnable;
}
/// <summary>
/// 配置程序操作命令的 backing 字段。
/// </summary>
private DelegateCommand<string>? _canLinConfigProCmd;
/// <summary>
/// 配置程序操作(新建/修改/删除)。
/// </summary>
/// <remarks>
/// <paramref name="par"/> 约定:
/// - "Add":新建配置程序(按当前 <see cref="SelectedMode"/> 创建 CAN 或 CANFD 扩展表记录);
/// - "Edit":修改配置程序名称;
/// - "Delete":删除配置程序及其内容(包含读写配置、调度表、扩展表);
/// - "Active":激活/取消激活当前配置程序(激活后禁止切换配置程序)。
///
/// 激活语义:
/// - 激活要求设备已连接且 DBC 已解析成功;
/// - 激活后会把当前配置下发到服务层CmdData + 调度表),用于循环发送/硬件调度表等功能。
/// </remarks>
public DelegateCommand<string> CanLinConfigProCmd
{
get
@@ -1362,6 +1706,16 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 配置程序操作命令处理。
/// </summary>
/// <param name="par">操作类型Add/Edit/Delete/Active。</param>
/// <remarks>
/// 约束说明:
/// - 打开 CAN<see cref="ZlgCanDriveService.OpenState"/>)或已激活(<see cref="IsCanConfigProActive"/>)时,
/// 禁止新建/修改/删除与切换配置程序,避免“驱动正在运行但底层配置被替换”导致状态错乱。
/// - Delete 会同时清理 CAN/CANFD 两套扩展配置(如果都存在),避免孤儿记录残留。
/// </remarks>
private void CanLinConfigProCmdMethod(string par)
{
if (string.IsNullOrWhiteSpace(par)) return;
@@ -1683,10 +2037,20 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 调度表配置弹窗命令的 backing 字段。
/// </summary>
private DelegateCommand? _scheduleConfigCmd;
/// <summary>
/// 调度表配置(弹窗)。
/// </summary>
/// <remarks>
/// 打开调度表弹窗时,会从 <see cref="ZlgCanDriveService.CmdData"/> 中提取 MsgName 列表作为候选消息帧。
/// - CAN 模式:打开 DialogCANSchConfigView
/// - CANFD 模式:打开 DialogCANFdSchConfigView
///
/// 弹窗确认后会把返回的调度表 DTO 写回当前配置程序实体,并调用 <c>ReloadCurrentConfigPro</c> 刷新。
/// </remarks>
public DelegateCommand ScheduleConfigCmd
{
get
@@ -1699,6 +2063,17 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 调度表配置弹窗命令处理。
/// </summary>
/// <remarks>
/// 前置条件:
/// - 必须选中配置程序;
/// - 必须存在可发送消息CmdData 非空且 MsgName 有效),否则无法配置调度表。
///
/// 注意:
/// - 调度表本身并不直接发送;真正启动发送由 CAN 操作命令中的 "CycleSend" 触发。
/// </remarks>
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<ObservableCollection<CANScheduleConfigDto>>("ReturnValue") ?? new ObservableCollection<CANScheduleConfigDto>();
// DTO -> 实体:用于持久化(保存到数据库),并在后续激活/循环发送前由 SyncSelectedConfig 再同步到 Service。
SelectCanLinConfigPro.CanScheduleConfigs = Mapper.Map<List<CANScheduleConfig>>(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
}
}
/// <summary>
/// 读写配置操作命令的 backing 字段。
/// </summary>
private DelegateCommand<string>? _writeReadConfigCmd;
/// <summary>
/// 写入/读取/删除 信号配置项。
/// </summary>
/// <remarks>
/// <paramref name="par"/> 常见取值:
/// - "OpenDialog":打开“读写设置”弹窗(需要已连接且信号集合非空);
/// - "Write":将当前选中信号加入写入配置;
/// - "Read":将当前选中信号加入读取配置;
/// - "Delete":删除当前选中信号的配置项。
/// </remarks>
public DelegateCommand<string> WriteReadConfigCmd
{
get
@@ -1770,6 +2164,22 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 写入/读取/删除信号配置项命令处理。
/// </summary>
/// <param name="par">操作类型。</param>
/// <remarks>
/// 数据来源:
/// - 操作对象为右侧信号列表中当前选中的 <see cref="SelectedCanDbcModel"/>。
///
/// 重要约束:
/// - 配置程序激活后禁止变更读写配置(避免发送/解析过程中配置被修改);
/// - "OpenDialog" 会强校验:设备已连接 + 信号集合非空,防止弹窗候选集合为空。
///
/// 注意:
/// - 读写互斥与更严格的冲突校验主要在读写设置弹窗 ViewModel 中实现;
/// - 此处只做“同一 RWInfo 下禁止重复”与基础防御校验。
/// </remarks>
private void WriteReadConfigCmdMethod(string par)
{
if (string.IsNullOrWhiteSpace(par)) return;
@@ -1866,6 +2276,14 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 重新从数据库加载配置程序,并保持当前选中项。
/// </summary>
/// <remarks>
/// 用途:
/// - 在新增/删除/保存读写配置、保存调度表等操作后,刷新 UI 绑定对象,确保界面与数据库一致;
/// - 刷新后会重新执行 <see cref="SyncSelectedConfig"/>,从而重建 CmdData、读写列表与调度表列表。
/// </remarks>
private void ReloadCurrentConfigPro()
{
var id = SelectCanLinConfigPro?.Id;
@@ -1877,13 +2295,29 @@ namespace CapMachine.Wpf.ViewModels
SyncSelectedConfig();
}
if (SelectCanLinConfigPro != null)
{
ZlgCanDriveService.InitCanConfig(SelectCanLinConfigPro);
}
MatchSeletedAndCanDbcModel();
}
/// <summary>
/// CAN 操作命令的 backing 字段。
/// </summary>
private DelegateCommand<string>? _canOpCmd;
/// <summary>
/// CAN 操作命令。
/// </summary>
/// <remarks>
/// <paramref name="par"/> 常见取值:
/// - "Open":按当前模式与参数打开设备并启动接收线程
/// - "Close":关闭设备(同时停止循环发送/循环接收,并退出激活态)
/// - "Parse":解析当前 DBC需要已连接且 DBC 路径有效)
/// - "CycleSend":循环发送开关(第一次开启,第二次关闭)
/// - "CycleRecive":循环接收开关
/// - "Save":保存当前配置(包括 CAN/CANFD 扩展配置与模式切换)
/// </remarks>
public DelegateCommand<string> CanOpCmd
{
get
@@ -1896,6 +2330,19 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// CAN 操作命令处理。
/// </summary>
/// <param name="par">操作类型。</param>
/// <remarks>
/// 循环发送两种实现:
/// - 若 <see cref="CurrentSchEnable"/> 为 true使用调度表硬件 auto_send会校验每个 MsgName 在调度表中都有配置。
/// - 若 <see cref="CurrentSchEnable"/> 为 false使用软件精确定时循环发送周期取 <see cref="CurrentCycle"/>)。
///
/// 互斥约束:
/// - 开启 CAN 前会检查 <see cref="ComActionService"/> 与 <see cref="ZlgLinDriveService"/> 状态,防止 CAN/LIN 同时占用。
/// - Close 时会通过 Service 层确保停止发送/接收与硬件 auto_send 清理。
/// </remarks>
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 = "循环发送:已关闭";

View File

@@ -50,16 +50,60 @@ namespace CapMachine.Wpf.ViewModels
InitLoadLinConfigPro();
}
/// <summary>
/// Prism 弹窗服务。
/// </summary>
public IDialogService DialogService { get; }
/// <summary>
/// FreeSql用于 LIN 配置程序/读写项/调度表 的查询与持久化)。
/// </summary>
public IFreeSql FreeSql { get; }
/// <summary>
/// 事件聚合器。
/// </summary>
public IEventAggregator EventAggregator { get; }
/// <summary>
/// 区域管理器。
/// </summary>
public IRegionManager RegionManager { get; }
/// <summary>
/// 系统运行状态服务。
/// </summary>
public SysRunService SysRunService { get; }
/// <summary>
/// 全局配置服务(用于记录当前系统选择的通信类型等状态)。
/// </summary>
public ConfigService ConfigService { get; }
/// <summary>
/// ZLG LIN 服务LIN 生命周期、LDF 解析、软件调度、收发指示灯等)。
/// </summary>
public ZlgLinDriveService ZlgLinDriveService { get; }
/// <summary>
/// ZLG CAN 服务。
/// 说明LIN 与 CAN 共用同一硬件/设备句柄,打开 LIN 前需要确保 CAN 已关闭。
/// </summary>
public ZlgCanDriveService ZlgCanDriveService { get; }
/// <summary>
/// 通信驱动互斥控制服务(同一时刻只能有一种驱动操作压缩机)。
/// </summary>
public ComActionService ComActionService { get; }
/// <summary>
/// 逻辑规则服务。
/// </summary>
public LogicRuleService LogicRuleService { get; }
/// <summary>
/// AutoMapper。
/// </summary>
public IMapper Mapper { get; }
private List<CanLinConfigPro> linConfigPros = new List<CanLinConfigPro>();
@@ -80,6 +124,9 @@ namespace CapMachine.Wpf.ViewModels
public CanLinConfigPro? SelectCanLinConfigPro { get; set; }
private ObservableCollection<CanLinRWConfigDto> _listWriteCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>();
/// <summary>
/// 写入配置 DTO 集合UI 展示/编辑用)。
/// </summary>
public ObservableCollection<CanLinRWConfigDto> ListWriteCanLinRWConfigDto
{
get { return _listWriteCanLinRWConfigDto; }
@@ -87,15 +134,57 @@ namespace CapMachine.Wpf.ViewModels
}
private ObservableCollection<CanLinRWConfigDto> _listReadCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>();
/// <summary>
/// 读取配置 DTO 集合UI 展示/编辑用)。
/// </summary>
public ObservableCollection<CanLinRWConfigDto> ListReadCanLinRWConfigDto
{
get { return _listReadCanLinRWConfigDto; }
set { _listReadCanLinRWConfigDto = value; RaisePropertyChanged(); }
}
private ObservableCollection<LINScheduleConfigDto> _listLINScheduleConfigDto = new ObservableCollection<LINScheduleConfigDto>();
/// <summary>
/// LIN 调度表集合(系统层)。
/// </summary>
public ObservableCollection<LINScheduleConfigDto> ListLINScheduleConfigDto
{
get { return _listLINScheduleConfigDto; }
set { _listLINScheduleConfigDto = value; RaisePropertyChanged(); }
}
private bool _isLinConfigProActive;
/// <summary>
/// 当前配置程序是否已激活(对齐 CAN Active 语义)。激活后禁止切换配置程序。
/// </summary>
public bool IsLinConfigProActive
{
get { return _isLinConfigProActive; }
set
{
_isLinConfigProActive = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(IsRwEditable));
}
}
private bool _isLINConfigDatagridActive = true;
/// <summary>
/// 配置程序 DataGrid 是否可操作(与 IsLinConfigProActive 取反)。
/// </summary>
public bool IsLINConfigDatagridActive
{
get { return _isLINConfigDatagridActive; }
set { _isLINConfigDatagridActive = value; RaisePropertyChanged(); }
}
/// <summary>
/// 读写设置是否允许编辑。
/// 说明:与 <see cref="IsLinConfigProActive"/> 语义绑定:激活后代表“配置已下发并进入运行态”,因此禁止编辑配置/切换配置程序。
/// </summary>
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));
}
}
/// <summary>
@@ -122,6 +220,36 @@ namespace CapMachine.Wpf.ViewModels
}
}
/// <summary>
/// 循环周期ms
/// </summary>
public int CurrentCycle
{
get { return SelectedLINConfigExdDto?.Cycle ?? 0; }
set
{
if (SelectedLINConfigExdDto == null) return;
SelectedLINConfigExdDto.Cycle = value;
RaisePropertyChanged();
}
}
/// <summary>
/// 连接按钮文字。
/// </summary>
public string ConnectButtonText
{
get { return "连接LIN"; }
}
/// <summary>
/// 关闭按钮文字。
/// </summary>
public string CloseButtonText
{
get { return "关闭LIN"; }
}
/// <summary>
/// LIN 波特率。
/// </summary>
@@ -152,6 +280,9 @@ namespace CapMachine.Wpf.ViewModels
private void InitLoadLinConfigPro()
{
// 从数据库加载 LIN 配置程序:
// - 同时 include 扩展配置、读写项(含逻辑规则)、调度表
// - 供主界面 DataGrid 与右侧配置区域绑定
linConfigPros = FreeSql.Select<CanLinConfigPro>()
.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<LINConfigExdDto>(SelectCanLinConfigPro.LINConfigExd);
var writeData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Write).ToList() ?? new List<CanLinRWConfig>();
@@ -173,7 +309,12 @@ namespace CapMachine.Wpf.ViewModels
var readData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Read).ToList() ?? new List<CanLinRWConfig>();
ListReadCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>(Mapper.Map<List<CanLinRWConfigDto>>(readData));
var schData = SelectCanLinConfigPro.LinScheduleConfigs?.ToList() ?? new List<LINScheduleConfig>();
ListLINScheduleConfigDto = new ObservableCollection<LINScheduleConfigDto>(Mapper.Map<List<LINScheduleConfigDto>>(schData));
// 触发依赖属性刷新IsRwEditable 与 CurrentCycle 取决于当前选择的配置与激活状态。
RaisePropertyChanged(nameof(IsRwEditable));
RaisePropertyChanged(nameof(CurrentCycle));
}
private DelegateCommand<object>? _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<string>? _linConfigProCmd;
/// <summary>
/// 配置程序操作(新建/修改/删除/激活)。
/// </summary>
public DelegateCommand<string> LinConfigProCmd
{
get
{
if (_linConfigProCmd == null)
{
_linConfigProCmd = new DelegateCommand<string>(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<string>("Name")?.Trim();
if (string.IsNullOrWhiteSpace(name))
{
MessageBox.Show("名称不能为空", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
var exists = FreeSql.Select<CanLinConfigPro>()
.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<LINConfigExd>(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<CanLinConfigPro>(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<string>("Name")?.Trim();
if (string.IsNullOrWhiteSpace(name))
{
MessageBox.Show("名称不能为空", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
var exists = FreeSql.Select<CanLinConfigPro>()
.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<CanLinConfigPro>()
.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<CanLinConfigPro>();
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<LINConfigExd>(item.LINConfigExdId).ExecuteAffrows();
}
}
SelectCanLinConfigPro = null;
SelectedLINConfigExdDto = null;
InitLoadLinConfigPro();
ListLINScheduleConfigDto = new ObservableCollection<LINScheduleConfigDto>();
break;
case "Active":
if (IsLinConfigProActive)
{
// 取消激活:
// - 停止循环/调度发送
// - 关闭事件驱动发送开关
// - 清空 CmdData避免残留订阅导致误发送
IsLinConfigProActive = false;
IsLINConfigDatagridActive = true;
ZlgLinDriveService.StopSchedule();
ZlgLinDriveService.IsCycleSend = false;
ZlgLinDriveService.LoadCmdDataToDrive(new List<LinCmdData>());
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<LINScheduleConfigDto>());
break;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private DelegateCommand? _LoadLdfCmd;
/// <summary>
/// 选择 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<LinCmdData>());
}
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;
}
// 仅保存 LINConfigExdLDF 路径/周期/波特率/SchEnable 等);
// 读写项/调度表由各自弹窗或其它操作链路持久化。
FreeSql.Update<LINConfigExd>()
.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;
}
}
/// <summary>
/// 从当前配置程序的写入配置构建并下发 LinCmdData。
/// </summary>
private void BuildAndLoadCmdDataToDrive()
{
try
{
if (SelectCanLinConfigPro?.CanLinConfigContents == null)
{
ZlgLinDriveService.LoadCmdDataToDrive(new List<LinCmdData>());
return;
}
var writeItems = SelectCanLinConfigPro.CanLinConfigContents
.Where(a => a.RWInfo == RW.Write)
.ToList();
var cmdList = new List<LinCmdData>();
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<LogicRuleDto>(item.LogicRule),
});
}
ZlgLinDriveService.LoadCmdDataToDrive(cmdList);
}
catch (Exception ex)
{
MessageBox.Show($"构建/下发 LIN CmdData 失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private DelegateCommand? _openRwDialogCmd;
/// <summary>
/// 打开“读写设置”弹窗。
/// 说明:
/// - 弹窗内允许编辑写入/读取项,并可从 LDF 解析得到的信号池中选择;
/// - 弹窗确认后通过 <see cref="ReloadCurrentConfigPro"/> 重新从数据库加载并刷新 UI。
/// </summary>
public DelegateCommand OpenRwDialogCmd
{
get
@@ -391,6 +933,7 @@ namespace CapMachine.Wpf.ViewModels
var candidates = new ObservableCollection<DialogZlgCanLinRwConfigViewModel.SignalCandidate>();
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
}
}
/// <summary>
/// 克隆读写配置 DTO。
/// 说明:打开弹窗前先克隆一份,避免用户在弹窗中编辑时直接影响主界面集合(取消时也不会污染当前 UI 状态)。
/// </summary>
/// <param name="src">源 DTO。</param>
/// <returns>克隆后的 DTO。</returns>
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;
/// <summary>
/// 调度表配置(弹窗)。
/// </summary>
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<ObservableCollection<LINScheduleConfigDto>>("ReturnValue") ?? new ObservableCollection<LINScheduleConfigDto>();
// DTO -> 实体:用于持久化到数据库;后续激活/循环发送前会再次由 SyncSelectedConfig/BuildAndLoadCmdDataToDrive 同步到 Service。
SelectCanLinConfigPro.LinScheduleConfigs = Mapper.Map<List<LINScheduleConfig>>(ListLINScheduleConfigDto.ToList());
ReloadCurrentConfigPro();
});
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 根据 UI 当前 SchEnable/Cycle 状态启动或停止发送。
/// </summary>
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);
}
}
}

View File

@@ -0,0 +1,143 @@
<UserControl
x:Class="CapMachine.Wpf.Views.DialogZlgCANFDSchConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:CapMachine.Wpf.Views"
xmlns:localEx="clr-namespace:CapMachine.Wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
Width="1000"
Height="800"
mc:Ignorable="d">
<UserControl.Resources>
<localEx:BindingProxy x:Key="Proxy" Data="{Binding}" />
<Style x:Key="myHeaderStyle" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="FontSize" Value="24" />
<Setter Property="Width" Value="218" />
</Style>
<Style x:Key="myHeaderStyle1" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="FontSize" Value="16" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Button
Margin="10,0"
Command="{Binding OpCmd}"
CommandParameter="Delete"
FontWeight="Bold"
Foreground="#404040"
Style="{StaticResource MaterialDesignFlatAccentBgButton}">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="0,0,5,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="14"
Foreground="#404040"
Text="&#xe9d9;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="删除" />
</StackPanel>
</Button>
</StackPanel>
<StackPanel
Margin="30,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<TextBlock
Margin="10,0,5,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="22"
Text="&#xe9ff;" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
Text="报文发送方式:"
TextAlignment="Center" />
<ComboBox
Width="120"
DisplayMemberPath="Text"
FontSize="20"
ItemsSource="{Binding SendOrderCbxItems}"
SelectedValue="{Binding CurSendOrder, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Key" />
</StackPanel>
<Grid Grid.Row="1">
<DataGrid
x:Name="MainDatagrid"
Margin="5"
AutoGenerateColumns="False"
CanUserAddRows="False"
HeadersVisibility="Column"
ItemsSource="{Binding ListCANFdScheduleConfigDto}"
SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn
Width="260"
Binding="{Binding MsgName}"
Header="消息名"
IsReadOnly="True" />
<DataGridTextColumn
Width="100"
Binding="{Binding Cycle}"
Header="周期" />
<DataGridTemplateColumn Width="160" Header="调度器序号">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
DisplayMemberPath="Text"
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.SchTabIndexCbxItems}"
SelectedValue="{Binding SchTabIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Key" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<prism:InvokeCommandAction Command="{Binding GridSelectionChangedCmd}" CommandParameter="{Binding ElementName=MainDatagrid, Path=SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
</Grid>
<StackPanel
Grid.Row="2"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="10,10"
Command="{Binding SaveCmd}"
Content="确定"
Foreground="White" />
<Button
Margin="10,0"
Command="{Binding CancelCmd}"
Content="取消"
Foreground="White" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,18 @@
using System.Windows.Controls;
namespace CapMachine.Wpf.Views
{
/// <summary>
/// DialogZlgCANFDSchConfigView.xaml 的交互逻辑
/// </summary>
public partial class DialogZlgCANFDSchConfigView : UserControl
{
/// <summary>
/// 构造函数。
/// </summary>
public DialogZlgCANFDSchConfigView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,180 @@
<UserControl
x:Class="CapMachine.Wpf.Views.DialogZlgCANSchConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:CapMachine.Wpf.Views"
xmlns:localEx="clr-namespace:CapMachine.Wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
Width="1000"
Height="800"
mc:Ignorable="d">
<UserControl.Resources>
<localEx:BindingProxy x:Key="Proxy" Data="{Binding}" />
<Style x:Key="myHeaderStyle" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="FontSize" Value="24" />
<Setter Property="Width" Value="218" />
</Style>
<Style x:Key="myHeaderStyle1" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="FontSize" Value="16" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<!--<Button
Margin="10,0"
Command="{Binding CopyCmd}"
FontWeight="Bold"
Foreground="#404040"
Style="{StaticResource MaterialDesignFlatAccentBgButton}">
<StackPanel Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="14"
Foreground="#404040"
Text="&#xe9c6;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="复制" />
</StackPanel>
</Button>-->
<Button
Margin="10,0"
Command="{Binding OpCmd}"
CommandParameter="Delete"
FontWeight="Bold"
Foreground="#404040"
Style="{StaticResource MaterialDesignFlatAccentBgButton}">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="0,0,5,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="14"
Foreground="#404040"
Text="&#xe9d9;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="删除" />
</StackPanel>
</Button>
</StackPanel>
<StackPanel
Margin="30,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<TextBlock
Margin="10,0,5,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="22"
Text="&#xe9ff;" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
Text="报文发送方式:"
TextAlignment="Center" />
<ComboBox
Width="120"
DisplayMemberPath="Text"
FontSize="20"
ItemsSource="{Binding SendOrderCbxItems}"
SelectedValue="{Binding CurSendOrder, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Key" />
</StackPanel>
<Grid Grid.Row="1">
<DataGrid
x:Name="MainDatagrid"
Margin="5"
AutoGenerateColumns="False"
CanUserAddRows="False"
HeadersVisibility="Column"
ItemsSource="{Binding ListCANScheduleConfigDto}"
SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Columns>
<!--<DataGridTextColumn
Binding="{Binding Index}"
Header="序号"
IsReadOnly="True" />-->
<DataGridTextColumn
Width="260"
Binding="{Binding MsgName}"
Header="消息名"
IsReadOnly="True" />
<DataGridTextColumn
Width="100"
Binding="{Binding Cycle}"
Header="周期" />
<!--<DataGridTemplateColumn Width="200" Header="发送方式">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
DisplayMemberPath="Text"
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.SendOrderCbxItems}"
SelectedValue="{Binding OrderSend, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Key" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>-->
<DataGridTemplateColumn Width="160" Header="调度器序号">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
DisplayMemberPath="Text"
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.SchTabIndexCbxItems}"
SelectedValue="{Binding SchTabIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Key" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!--<DataGridCheckBoxColumn Binding="{Binding IsEnable}" Header="是否启用" />-->
</DataGrid.Columns>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<prism:InvokeCommandAction Command="{Binding GridSelectionChangedCmd}" CommandParameter="{Binding ElementName=MainDatagrid, Path=SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
</Grid>
<StackPanel
Grid.Row="2"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="10,10"
Command="{Binding SaveCmd}"
Content="确定"
Foreground="White" />
<Button
Margin="10,0"
Command="{Binding CancelCmd}"
Content="取消"
Foreground="White" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace CapMachine.Wpf.Views
{
/// <summary>
/// DialogZlgCANSchConfigView.xaml 的交互逻辑
/// </summary>
public partial class DialogZlgCANSchConfigView : UserControl
{
public DialogZlgCANSchConfigView()
{
InitializeComponent();
}
}
}

View File

@@ -54,6 +54,43 @@
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.Resources>
<Style
x:Key="TextBoxStyle"
BasedOn="{StaticResource MaterialDesignTextBoxBase}"
TargetType="TextBox">
<Setter Property="FontSize" Value="18" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="10,0" />
<Setter Property="Width" Value="80" />
</Style>
<Style x:Key="TextBlockStyle" TargetType="TextBlock">
<Setter Property="Width" Value="110" />
<Setter Property="FontSize" Value="18" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="5,0" />
</Style>
</Grid.Resources>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock
Margin="10,0,2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="20"
Text="&#xe796;" />
<TextBlock
Width="90"
Style="{StaticResource TextBlockStyle}"
Text="定时调度表:" />
<Button
Margin="10,0"
Command="{Binding AddCycleTimeSch}"
Content="加入定时调度表"
Foreground="White"
IsEnabled="{Binding CanAddCycleTimeSch}" />
</StackPanel>
<StackPanel
Grid.Row="0"
@@ -261,6 +298,11 @@
Grid.Row="2"
Margin="5"
ItemsSource="{Binding SignalTree}">
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:DialogZlgCanLinRwConfigViewModel+SignalFrameNode}" ItemsSource="{Binding Signals}">
<TextBlock FontWeight="Bold" Text="{Binding FrameName}" />

View File

@@ -17,6 +17,29 @@
<localEx:BindingProxy x:Key="Proxy" Data="{Binding}" />
</UserControl.Resources>
<Grid>
<Grid.Resources>
<Style
x:Key="TextBoxStyle"
BasedOn="{StaticResource MaterialDesignTextBoxBase}"
TargetType="TextBox">
<Setter Property="FontSize" Value="18" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="10,0" />
<Setter Property="Width" Value="120" />
</Style>
<Style x:Key="TextBlockStyle" TargetType="TextBlock">
<Setter Property="Width" Value="110" />
<Setter Property="FontSize" Value="18" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="5,0" />
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="6*" />
<ColumnDefinition Width="6*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition />
@@ -26,7 +49,8 @@
Margin="3"
Background="{DynamicResource MaterialDesignLightBackground}"
Foreground="{DynamicResource PrimaryHueLightForegroundBrush}"
UniformCornerRadius="5">
UniformCornerRadius="5"
Grid.ColumnSpan="2">
<StackPanel Orientation="Horizontal">
<TextBlock
@@ -98,7 +122,7 @@
</StackPanel>
</materialDesign:Card>
<Grid Grid.Row="1">
<Grid Grid.Row="1" Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400" />
<ColumnDefinition />
@@ -112,6 +136,7 @@
UniformCornerRadius="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition />
</Grid.RowDefinitions>
@@ -123,12 +148,93 @@
FontWeight="Bold"
Text="配置程序" />
<StackPanel Grid.Row="1" Orientation="Horizontal">
<StackPanel.Resources>
<Style
x:Key="btnStyle"
BasedOn="{StaticResource MaterialDesignFlatSecondaryLightBgButton}"
TargetType="Button">
<Setter Property="Margin" Value="5,2" />
<Setter Property="Foreground" Value="White" />
</Style>
</StackPanel.Resources>
<Button
Command="{Binding LinConfigProCmd}"
CommandParameter="Add"
Style="{StaticResource btnStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe8c0;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="新建" />
</StackPanel>
</Button>
<Button
Command="{Binding LinConfigProCmd}"
CommandParameter="Edit"
Style="{StaticResource btnStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe73a;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="修改" />
</StackPanel>
</Button>
<Button
Command="{Binding LinConfigProCmd}"
CommandParameter="Delete"
Style="{StaticResource btnStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe748;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="删除" />
</StackPanel>
</Button>
<Button
Command="{Binding LinConfigProCmd}"
CommandParameter="Active"
Style="{StaticResource btnStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe8fa;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="激活" />
</StackPanel>
</Button>
</StackPanel>
<DataGrid
x:Name="LinConfigDatagrid"
Grid.Row="1"
Grid.Row="2"
AutoGenerateColumns="False"
CanUserAddRows="False"
HeadersVisibility="Column"
IsEnabled="{Binding IsLINConfigDatagridActive}"
ItemsSource="{Binding ListCanLinConfigPro}"
SelectedItem="{Binding SelectCanLinConfigPro, Mode=TwoWay}">
<i:Interaction.Triggers>
@@ -155,7 +261,12 @@
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="80" />
<RowDefinition Height="60" />
<RowDefinition Height="40" />
<RowDefinition Height="200" />
<RowDefinition Height="40" />
<RowDefinition Height="220" />
<RowDefinition Height="40" />
<RowDefinition />
</Grid.RowDefinitions>
@@ -164,46 +275,467 @@
VerticalAlignment="Center"
FontSize="18"
FontWeight="Bold"
Text="参数" />
Text="参数操作" />
<StackPanel Grid.Row="1" Margin="10" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="16" Text="波特率:" />
<TextBox
Width="120"
Margin="5,0,20,0"
VerticalContentAlignment="Center"
Text="{Binding LinBaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel
Grid.Row="1"
Margin="0,0,15,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,0,20,0"
Command="{Binding OpenRwDialogCmd}"
Foreground="White"
IsEnabled="{Binding IsRwEditable}">
<TextBlock FontSize="14" Text="读写设置" />
Margin="5,0"
Command="{Binding LinOpCmd}"
CommandParameter="Open"
Foreground="White">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe743;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="{Binding ConnectButtonText}" />
</StackPanel>
</Button>
<Button
Margin="5,0"
Command="{Binding LinOpCmd}"
CommandParameter="Close"
Foreground="White">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe7fb;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="{Binding CloseButtonText}" />
</StackPanel>
</Button>
<Button
Margin="5,0"
Command="{Binding LinOpCmd}"
CommandParameter="Parse"
Foreground="White">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe93f;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="解析LDF" />
</StackPanel>
</Button>
<Button
Margin="5,0"
Command="{Binding LinOpCmd}"
CommandParameter="Save"
Foreground="White">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe936;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="保存配置" />
</StackPanel>
</Button>
<CheckBox
VerticalAlignment="Center"
Content="调度使能"
IsChecked="{Binding CurrentSchEnable, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<prism:InvokeCommandAction
Command="{Binding SchEnableCmd}"
CommandParameter="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=CheckBox}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</CheckBox>
</StackPanel>
<TextBlock
Grid.Row="2"
Margin="10,0"
VerticalAlignment="Center"
FontSize="18"
FontWeight="Bold"
Text="状态" />
<Grid Grid.Row="3">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="10,0,2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="20"
Text="&#xe977;" />
<TextBlock
Width="80"
Style="{StaticResource TextBlockStyle}"
Text="报文状态:" />
<Border
Width="50"
Margin="0,12"
BorderThickness="1"
CornerRadius="5">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Gray" />
<Style.Triggers>
<DataTrigger Binding="{Binding ZlgLinDriveService.IsSendOk}" Value="true">
<Setter Property="Background" Value="LimeGreen" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"
Text="发送" />
</Border>
<Border
Width="50"
Margin="2,12"
BorderThickness="1"
CornerRadius="5">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Gray" />
<Style.Triggers>
<DataTrigger Binding="{Binding ZlgLinDriveService.IsReviceOk}" Value="true">
<Setter Property="Background" Value="LimeGreen" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"
Text="接收" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock
Margin="10,0,2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="20"
Text="&#xe796;" />
<TextBlock
Width="90"
Style="{StaticResource TextBlockStyle}"
Text="启用调度表:" />
<ToggleButton
Width="40"
Margin="2,2"
Command="{Binding SchEnableCmd}"
IsChecked="{Binding CurrentSchEnable, Mode=TwoWay}"
Style="{StaticResource MaterialDesignSwitchToggleButton}"
ToolTip="启用调度表" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock
Margin="10,0,2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe8fa;" />
<TextBlock Style="{StaticResource TextBlockStyle}" Text="LIN连接状态" />
<Border
Width="50"
Margin="5,10"
Padding="5"
CornerRadius="3">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding ZlgLinDriveService.OpenState}" Value="True">
<Setter Property="Background" Value="LimeGreen" />
</DataTrigger>
<DataTrigger Binding="{Binding ZlgLinDriveService.OpenState}" Value="False">
<Setter Property="Background" Value="Gray" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"
Text="连接" />
</Border>
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.Column="0"
Orientation="Horizontal">
<Button
Margin="15,0,5,0"
Command="{Binding LinOpCmd}"
CommandParameter="CycleSend">
<Button.Style>
<Style BasedOn="{StaticResource MaterialDesignFlatDarkBgButton}" TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ZlgLinDriveService.IsCycleSend}" Value="true">
<Setter Property="Background" Value="LimeGreen" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding ZlgLinDriveService.IsCycleSend}" Value="false">
<Setter Property="Background" Value="Gray" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe796;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="循环发送" />
</StackPanel>
</Button>
<Button
Margin="5,0"
Command="{Binding LinOpCmd}"
CommandParameter="CycleRecive">
<Button.Style>
<Style BasedOn="{StaticResource MaterialDesignFlatDarkBgButton}" TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ZlgLinDriveService.IsCycleRevice}" Value="true">
<Setter Property="Background" Value="LimeGreen" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding ZlgLinDriveService.IsCycleRevice}" Value="false">
<Setter Property="Background" Value="Gray" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe73a;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="循环接收" />
</StackPanel>
</Button>
</StackPanel>
</Grid>
<TextBlock
Grid.Row="4"
Margin="10,0"
VerticalAlignment="Center"
FontSize="18"
FontWeight="Bold"
Text="参数信息" />
<Grid Grid.Row="5">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="10,0,10,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe9f8;" />
<TextBlock Style="{StaticResource TextBlockStyle}" Text="波特率" />
<TextBox Style="{StaticResource TextBoxStyle}" Text="{Binding LinBaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock
Margin="10,0,10,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe9f8;" />
<TextBlock Style="{StaticResource TextBlockStyle}" Text="循环周期(ms)" />
<TextBox Style="{StaticResource TextBoxStyle}" Text="{Binding CurrentCycle, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
<TextBlock
Grid.Row="6"
Margin="10,0"
VerticalAlignment="Center"
FontSize="18"
FontWeight="Bold"
Text="写入读取操作" />
<Grid Grid.Row="7">
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel
Margin="0,0,15,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="5,0"
Command="{Binding OpenRwDialogCmd}"
Foreground="White"
IsEnabled="{Binding IsRwEditable}">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe796;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="读写设置" />
</StackPanel>
</Button>
<Button
Margin="15,0,0,0"
Command="{Binding ScheduleConfigCmd}"
Foreground="White">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2,0"
VerticalAlignment="Center"
FontFamily="/Assets/Fonts/#iconfont"
FontSize="18"
Text="&#xe796;" />
<TextBlock
VerticalAlignment="Center"
FontSize="14"
Text="调度表" />
</StackPanel>
</Button>
</StackPanel>
<TextBlock
Grid.Row="1"
Margin="10"
VerticalAlignment="Center"
FontSize="16"
TextWrapping="Wrap"
Text="提示:当前 ZLG LIN 仅提供硬件 LIN 收发能力LDF 解析需要后续接入 ZLG 的 LDF DLL不会返回模拟数据" />
Text="提示:点击【读写设置】在弹窗中统一管理写入/读取配置(含规则与默认值)。"
TextWrapping="Wrap" />
</Grid>
</Grid>
</materialDesign:Card>
</Grid>
<materialDesign:Card
Grid.Row="1"
Grid.Column="1"
Margin="3"
Background="{DynamicResource MaterialDesignLightBackground}"
Foreground="{DynamicResource PrimaryHueLightForegroundBrush}"
UniformCornerRadius="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
Margin="10,0"
VerticalAlignment="Center"
FontSize="18"
FontWeight="Bold"
Text="信号" />
<DataGrid
Grid.Row="1"
AutoGenerateColumns="False"
CanUserAddRows="False"
HeadersVisibility="Column"
IsReadOnly="True"
ItemsSource="{Binding ZlgLinDriveService.ListLinLdfModel}">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSeletedInfo}" Value="1">
<Setter Property="Background" Value="LightGreen" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSeletedInfo}" Value="2">
<Setter Property="Background" Value="SkyBlue" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Width="200" Binding="{Binding MsgName}">
<DataGridTextColumn.Header>
<TextBlock FontWeight="Bold" Text="帧名称" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Width="140" Binding="{Binding Name}">
<DataGridTextColumn.Header>
<TextBlock FontWeight="Bold" Text="中文名称" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Width="*" Binding="{Binding SignalName}">
<DataGridTextColumn.Header>
<TextBlock FontWeight="Bold" Text="信号名称" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Width="90" Binding="{Binding SignalUnit}">
<DataGridTextColumn.Header>
<TextBlock FontWeight="Bold" Text="单位" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Width="150" Binding="{Binding SignalRtValue}">
<DataGridTextColumn.Header>
<TextBlock FontWeight="Bold" Text="实时值" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</materialDesign:Card>
</Grid>
</UserControl>