Files
CapMachine/CapMachine.Wpf/Services/ZlgCanDriveService.cs

1009 lines
42 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CapMachine.Model.CANLIN;
using CapMachine.Wpf.CanDrive;
using CapMachine.Wpf.CanDrive.ZlgCan;
using CapMachine.Wpf.Dtos;
using ImTools;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CapMachine.Wpf.Services
{
/// <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>
/// 共享的 ZLG 驱动实例CAN/CANFD/LIN
/// </summary>
public ZlgCanFd200uDriver Driver { get; }
/// <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; }
set { _mode = value; RaisePropertyChanged(); }
}
/// <summary>
/// 发送使能(与 UI 的调度表使能语义对齐)。
/// </summary>
/// <remarks>
/// 该属性透传到 <see cref="ZlgCanFd200uDriver.SchEnable"/>
/// - true允许“事件驱动发送”CmdData 变化时发送/覆盖 auto_send
/// - false仅允许手动发送或软件调度任务主动调用发送接口
/// </remarks>
public bool SchEnable
{
get { return Driver.SchEnable; }
set { Driver.SchEnable = value; RaisePropertyChanged(); }
}
/// <summary>
/// 是否启用事件驱动发送。
/// </summary>
/// <remarks>
/// 该属性透传到 <see cref="ZlgCanFd200uDriver.IsCycleSend"/>。
/// - true 且 <see cref="SchEnable"/> 为 true 时CmdData 变化会触发增量发送/覆盖更新;
/// - falseCmdData 变化不会触发任何发送(由外部显式调用发送方法)。
/// </remarks>
public bool IsCycleSend
{
get { return Driver.IsCycleSend; }
set { Driver.IsCycleSend = value; RaisePropertyChanged(); }
}
/// <summary>
/// 是否正在循环接收(对齐 ToomossIsCycleRevice
/// </summary>
/// <remarks>
/// 该属性为只读透传:接收线程由 <see cref="ZlgCanFd200uDriver.StartReceiveLoop"/> 创建。
/// </remarks>
public bool IsCycleRevice
{
get { return Driver.IsReceiving; }
}
/// <summary>
/// 最近是否发送成功(用于 UI 指示)。
/// </summary>
/// <remarks>
/// 该标志由 Driver 的发送调用或 Tx 回显事件触发,短时间保持 true 后自动回落。
/// </remarks>
public bool IsSendOk
{
get { return Driver.IsSendOk; }
}
/// <summary>
/// 最近是否接收成功(用于 UI 指示)。
/// </summary>
/// <remarks>
/// 该标志由 Driver 接收线程收到 Rx 帧触发,短时间保持 true 后自动回落。
/// </remarks>
public bool IsReviceOk
{
get { return Driver.IsReviceOk; }
}
/// <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;
Driver = new ZlgCanFd200uDriver(logService);
Driver.PropertyChanged += (_, __) =>
{
OpenState = Driver.OpenState;
RaisePropertyChanged(nameof(IsCycleRevice));
RaisePropertyChanged(nameof(IsSendOk));
RaisePropertyChanged(nameof(IsReviceOk));
};
OpenState = Driver.OpenState;
Mode = ZlgCanMode.Can;
}
/// <summary>
/// 初始化 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)
{
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;
}
}
}
/// <summary>
/// 更新配置(从 DTO/DB 同步到驱动)。
/// </summary>
/// <param name="deviceIndex">设备索引。</param>
/// <param name="arbBaudRate">仲裁波特率bps。</param>
/// <param name="dataBaudRate">数据波特率bps。</param>
/// <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;
_channel0.ArbitrationBaudRate = arbBaudRate;
_channel0.DataBaudRate = dataBaudRate;
_channel0.EnableInternalResistance = resEnable;
_channel0.EnableMergeReceive = mergeReceive;
_channel0.MergeReceiveBufferFrames = mergeReceiveBufferFrames;
}
/// <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)
{
return;
}
Driver.OpenAndInitCan(_deviceIndex, _channel0);
Driver.StartReceiveLoop(_channel0.EnableMergeReceive, _channel0.MergeReceiveBufferFrames);
OpenState = Driver.OpenState;
}
/// <summary>
/// 使能/停止循环接收。
/// </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)
{
throw new InvalidOperationException("设备未连接,无法切换循环接收。");
}
if (enable)
{
if (!Driver.IsReceiving)
{
Driver.StartReceiveLoop(_channel0.EnableMergeReceive, _channel0.MergeReceiveBufferFrames);
}
}
else
{
if (Driver.IsReceiving)
{
Driver.StopReceiveLoop();
}
}
RaisePropertyChanged(nameof(IsCycleRevice));
}
/// <summary>
/// 关闭设备(会同时关闭共享的 LIN 通道)。
/// </summary>
/// <remarks>
/// Close 语义强调“彻底停止后台活动”:
/// - 停止调度表:取消软件调度任务,并清理硬件 auto_send 列表(避免设备侧继续发);
/// - 停止事件驱动发送:将 <see cref="IsCycleSend"/> 置为 false
/// - 停止接收:<see cref="ZlgCanFd200uDriver.Close"/> 内部会停止接收线程并释放句柄。
/// </remarks>
public void CloseDevice()
{
try
{
// Close 语义:关闭时必须停止循环发送与循环接收。
// - 循环发送:停止软件调度,并关闭事件驱动发送标志。
// - 循环接收Driver.Close 内部会 StopReceiveLoop。
StopSchedule();
IsCycleSend = false;
Driver.Close();
}
finally
{
OpenState = Driver.OpenState;
DbcParserState = false;
}
}
/// <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>();
lock (_scheduleLock)
{
_scheduleItems = list
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
.ToList();
}
}
/// <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>();
lock (_scheduleLock)
{
_scheduleItems = list
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
.ToList();
}
}
/// <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)
{
throw new InvalidOperationException("设备未连接,无法启动调度表。");
}
List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items;
lock (_scheduleLock)
{
// 取快照:
// - 避免启动过程中与 UI 线程并发修改调度表集合;
// - 后续 Driver.StartAutoSendSchedule 会在内部再次排序并建立 MsgName-&gt;index 映射。
items = _scheduleItems.ToList();
}
if (items.Count == 0)
{
throw new InvalidOperationException("调度表为空,无法启动调度表。");
}
// 周立功:调度表=硬件定时发送列表auto_send
// - 周期精度高,由设备侧时钟保障
// - PC 仍可同时调用发送接口发送其它数据
_scheduleUseConfigItems = true;
// 启动前先停止已有任务(软件调度 + 硬件 auto_send
StopSchedule();
// 统一:启用调度表后,等同“循环发送开启”
// 说明:这里的 IsCycleSend 指的是“允许事件驱动发送/覆盖更新”。
// - 硬件 auto_send 本身不依赖 IsCycleSend
// - 但运行中信号值变化要想覆盖更新到硬件列表,需要 IsCycleSend=true 且 SchEnable=true。
IsCycleSend = true;
// 使用硬件定时发送列表:按 scheduleItems 下发 auto_send 并 apply
Driver.StartAutoSendSchedule(
items.Select(a => (a.MsgName, a.CycleMs, a.OrderSend, a.SchTabIndex)),
channelIndex: 0,
frameType: Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
}
/// <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)
{
throw new InvalidOperationException("设备未连接,无法启动循环发送。");
}
var ms = Math.Max(1, cycleMs);
var msgNames = CmdData.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).Select(a => a.MsgName!).Distinct(StringComparer.Ordinal).ToList();
if (msgNames.Count == 0)
{
throw new InvalidOperationException("CmdData 为空,无法启动循环发送。");
}
// 软件调度:把 CmdData 中出现的 MsgName 去重后按统一周期循环发送。
// 注意:这与“配置调度表”的区别是:
// - 配置调度表:每条消息有自己的 Cycle/Order/SchTabIndex
// - 软件精确循环所有消息同周期OrderSend 固定为 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;
Task? task;
lock (_scheduleLock)
{
cts = _scheduleCts;
task = _scheduleTask;
_scheduleCts = null;
_scheduleTask = null;
}
try
{
cts?.Cancel();
if (task != null)
{
// 等待软件调度线程退出:
// - 这里不做无限等待,避免 UI 卡死;
// - 超时/异常会被吞掉(与旧代码风格保持一致)。
task.Wait(TimeSpan.FromSeconds(2));
}
}
catch
{
// 说明:
// - task.Wait 可能抛 AggregateException/TaskCanceledException
// - StopSchedule 属于“停止/清理”语义,不应因等待异常影响后续硬件清理。
}
finally
{
cts?.Dispose();
}
// 无论当前是否处于硬件定时发送都做一次清理clear_auto_send保证关闭/停止时设备侧不再发。
try
{
if (OpenState)
{
// 防御性清理:
// - 若此前是硬件调度表,必须 clear_auto_send否则设备会继续按周期发送
// - 若此前是软件调度,该调用也不会破坏状态(最多是清空一个空表)。
Driver.StopAutoSendSchedule(channelIndex: 0);
}
}
catch
{
// ignored停止流程不因硬件清理失败而中断。
}
}
/// <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();
var cts = new CancellationTokenSource();
lock (_scheduleLock)
{
_scheduleCts = cts;
}
// 统一:软件调度开启后,等同“循环发送开启”
IsCycleSend = true;
var frequency = Stopwatch.Frequency;
long ToTicks(int ms)
{
var safeMs = Math.Max(1, ms);
// 使用 long 避免溢出;周期最小为 1 tick。
var ticks = (long)(safeMs * (double)frequency / 1000d);
return Math.Max(1, ticks);
}
// 只在启动时做一次去重/排序;循环内不分配。
var unique = new Dictionary<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))
{
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))
{
break;
}
continue;
}
var nowTicks = sw.ElapsedTicks;
long minDue = long.MaxValue;
for (var i = 0; i < items.Length; i++)
{
var d = items[i].NextDueTicks;
if (d < minDue)
{
minDue = d;
}
}
var waitTicks = minDue - nowTicks;
if (waitTicks > 0)
{
// 混合等待:
// - 剩余时间较长WaitOne(<=200ms) 降低 CPU 占用
// - 临近到期1ms 级等待 + 短自旋提升边界精度
var waitMs = (int)(waitTicks * 1000 / frequency);
if (waitMs > 5)
{
var ms = Math.Min(waitMs - 1, 200);
if (token.WaitHandle.WaitOne(ms))
{
break;
}
continue;
}
if (waitMs > 1)
{
if (token.WaitHandle.WaitOne(1))
{
break;
}
continue;
}
spin.SpinOnce();
continue;
}
// 到期发送:
// - 同一条消息“错过多个周期”时,只发送一次(避免 burst然后尽快恢复节拍。
nowTicks = sw.ElapsedTicks;
for (var i = 0; i < items.Length; i++)
{
var it = items[i];
if (it.NextDueTicks > nowTicks)
{
continue;
}
try
{
// 软件调度最终仍复用 Driver 的“按 MsgName 编码并发送一帧”的能力。
// 这意味着:
// - 发送数据来源仍是 CmdData 当前值;
// - 若 DBC 未加载/MsgName 无法编码Driver 内部会快速返回或抛异常。
Driver.SendOneMsgByCmdData(it.MsgName, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
}
catch (Exception ex)
{
var t = sw.ElapsedTicks;
if (t - it.LastWarnTicks >= warnIntervalTicks)
{
it.LastWarnTicks = t;
_log.Warn($"软件循环发送失败:{it.MsgName}{ex.Message}");
}
}
finally
{
// 漂移控制:正常情况下按“计划时间”累加;若落后过多,则跳到 now+period只补发一次
var next = it.NextDueTicks + it.PeriodTicks;
if (next <= nowTicks)
{
next = nowTicks + it.PeriodTicks;
}
it.NextDueTicks = next;
}
}
}
}
/// <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)
{
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;
}
/// <summary>
/// 加载要发送的数据(订阅数据变化事件)。
/// </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))
{
SpeedCanCmdData = item;
}
}
// 将 CmdData 注入 Driver
// - Driver 会订阅每个 CanCmdData 的变化事件,用于“事件驱动发送/覆盖更新 auto_send”
// - frameType 必须与 Mode 对齐CAN 与 CANFD 的长度/编码规则不同。
Driver.LoadCmdDataToDrive(CmdData, channelIndex: 0, frameType: Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
}
/// <summary>
/// 手动发送(目前对齐原服务:仅发送转速)。
/// </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)
{
return;
}
if (SpeedCanCmdData != null)
{
// 先更新写入值:
// - 若当前启用了事件驱动发送IsCycleSend && SchEnable该赋值本身会触发 Driver 的增量发送/覆盖更新;
// - 若未启用事件驱动发送,下方会主动补发一次,保证“点一下就发”的旧行为。
SpeedCanCmdData.SignalCmdValue = speedData;
}
else
{
_log.Warn("未配置转速指令项ConfigName=转速),忽略手动发送。");
}
// 若未启用事件驱动发送,则这里主动发送一次(与旧行为兼容)
if (!IsCycleSend || !SchEnable)
{
// 历史行为:选取 CmdData 的第一条 MsgName 进行发送。
// - 由于本方法本质上是“转速”单信号写入,严格来说应按 SpeedCanCmdData.MsgName 发送;
// - 但为避免改动业务逻辑,这里保持旧逻辑,仅通过注释明确其局限。
var firstMsg = CmdData.FirstOrDefault()?.MsgName;
if (!string.IsNullOrWhiteSpace(firstMsg))
{
Driver.SendOneMsgByCmdData(firstMsg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
}
}
}
}
/// <summary>
/// ZLG CAN 工作模式。
/// </summary>
public enum ZlgCanMode
{
/// <summary>
/// CAN 经典帧。
/// </summary>
Can = 0,
/// <summary>
/// CAN FD。
/// </summary>
CanFd = 1
}
}