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

@@ -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);
foreach (var it in items)
// 只在启动时做一次去重/排序;循环内不分配。
var unique = new Dictionary<string, (int CycleMs, int OrderSend)>(StringComparer.Ordinal);
foreach (var it in items)
{
if (string.IsNullOrWhiteSpace(it.MsgName)) continue;
var name = it.MsgName.Trim();
if (!unique.ContainsKey(name))
{
if (!due.ContainsKey(it.MsgName))
unique[name] = (Math.Max(1, it.CycleMs), it.OrderSend);
}
}
var list = new List<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)
{
// 设备未打开时不发送:
// - 避免在 Close/重连窗口持续抛异常;
// - 这里仍然响应取消,保证 StopSchedule 不会“卡住等待”。
if (token.WaitHandle.WaitOne(50))
{
due[it.MsgName] = now;
cycle[it.MsgName] = Math.Max(1, it.CycleMs);
order[it.MsgName] = it.OrderSend;
break;
}
continue;
}
var nowTicks = sw.ElapsedTicks;
long minDue = long.MaxValue;
for (var i = 0; i < items.Length; i++)
{
var d = items[i].NextDueTicks;
if (d < minDue)
{
minDue = d;
}
}
while (!token.IsCancellationRequested)
var waitTicks = minDue - nowTicks;
if (waitTicks > 0)
{
if (!OpenState)
// 混合等待:
// - 剩余时间较长WaitOne(<=200ms) 降低 CPU 占用
// - 临近到期1ms 级等待 + 短自旋提升边界精度
var waitMs = (int)(waitTicks * 1000 / frequency);
if (waitMs > 5)
{
await Task.Delay(50, token).ConfigureAwait(false);
var ms = Math.Min(waitMs - 1, 200);
if (token.WaitHandle.WaitOne(ms))
{
break;
}
continue;
}
var utcNow = DateTime.UtcNow;
var minDue = due.Values.Min();
var delay = minDue - utcNow;
if (delay > TimeSpan.Zero)
if (waitMs > 1)
{
var ms = (int)Math.Min(delay.TotalMilliseconds, 200);
await Task.Delay(ms, token).ConfigureAwait(false);
if (token.WaitHandle.WaitOne(1))
{
break;
}
continue;
}
// due messages
var ready = due.Where(kv => kv.Value <= utcNow).Select(kv => kv.Key).ToList();
if (ready.Count == 0)
spin.SpinOnce();
continue;
}
// 到期发送:
// - 同一条消息“错过多个周期”时,只发送一次(避免 burst然后尽快恢复节拍。
nowTicks = sw.ElapsedTicks;
for (var i = 0; i < items.Length; i++)
{
var it = items[i];
if (it.NextDueTicks > nowTicks)
{
await Task.Delay(1, token).ConfigureAwait(false);
continue;
}
// 顺序/并行:这里只决定同一 tick 内的发送顺序(并行模式仍按字典序依次发)
ready.Sort(StringComparer.Ordinal);
foreach (var msg in ready)
try
{
if (token.IsCancellationRequested) break;
try
// 软件调度最终仍复用 Driver 的“按 MsgName 编码并发送一帧”的能力。
// 这意味着:
// - 发送数据来源仍是 CmdData 当前值;
// - 若 DBC 未加载/MsgName 无法编码Driver 内部会快速返回或抛异常。
Driver.SendOneMsgByCmdData(it.MsgName, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
}
catch (Exception ex)
{
var t = sw.ElapsedTicks;
if (t - it.LastWarnTicks >= warnIntervalTicks)
{
Driver.SendOneMsgByCmdData(msg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
it.LastWarnTicks = t;
_log.Warn($"软件循环发送失败:{it.MsgName}{ex.Message}");
}
catch (Exception ex)
}
finally
{
// 漂移控制:正常情况下按“计划时间”累加;若落后过多,则跳到 now+period只补发一次
var next = it.NextDueTicks + it.PeriodTicks;
if (next <= nowTicks)
{
_log.Warn($"调度表发送失败:{msg}{ex.Message}");
}
finally
{
due[msg] = DateTime.UtcNow.AddMilliseconds(cycle[msg]);
next = nowTicks + it.PeriodTicks;
}
it.NextDueTicks = next;
}
}
}, cts.Token);
}
}
/// <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))
{