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

@@ -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 () =>
@@ -1403,7 +1484,7 @@ namespace CapMachine.Wpf.CanDrive
//}
}
//释放数据缓冲区,必须释放,否则程序运行一段时间后会报内存不足
// 释放数据缓冲区,必须释放,否则程序运行一段时间后会报内存不足
Marshal.FreeHGlobal(msgPtRead);
Thread.Sleep(10);
@@ -1477,8 +1558,14 @@ namespace CapMachine.Wpf.CanDrive
/// <summary>
/// 关闭设备
/// 关闭设备并释放资源。
/// </summary>
/// <remarks>
/// 释放顺序要点:
/// - 关闭设备并下置状态位;
/// - 若启用调度表则先停止;
/// - 等待接收任务退出(短等待,避免 UI 卡死)。
/// </remarks>
public void CloseDevice()
{
//关闭设备