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
{
///
/// ZLG CAN/CANFD 驱动服务(共享设备句柄)。
///
///
/// 该类属于“服务层/编排层”,主要职责:
/// - 管理 的生命周期(打开、接收线程、关闭释放);
/// - 管理 DBC 加载与信号模型集合();
/// - 管理写入指令集合()并将其注入驱动形成“DBC 编码发送”的闭环;
/// - 提供两种循环发送实现:
/// 1) 硬件定时发送(调度表):通过周立功 auto_send 列表 + apply_auto_send(精度高)
/// 2) 软件精确定时循环发送:通过后台 Task 在 PC 侧按周期触发 (用于不依赖硬件调度或调试场景)
///
/// 线程模型:
/// - 接收线程由 内部创建;
/// - 软件调度发送由本类创建后台 Task(见 );
/// - UI 绑定的属性变化通过 的 RaisePropertyChanged 通知。
///
public sealed class ZlgCanDriveService : BindableBase
{
///
/// 软件调度条目(仅在软件调度线程内使用)。
///
///
/// 该结构体承担“纯运行态状态”的职责:
/// - 启动时由 构建并排序;
/// - 运行中由 在单线程内更新 等字段;
/// - 不对外暴露,也不需要线程安全的读写(因为不会跨线程访问)。
///
private sealed class SoftwareScheduleItem
{
///
/// 构造。
///
/// DBC 报文名称。
/// 周期(Stopwatch ticks)。
/// 发送顺序(用于排序)。
public SoftwareScheduleItem(string msgName, long periodTicks, int orderSend)
{
MsgName = msgName;
PeriodTicks = periodTicks;
OrderSend = orderSend;
}
///
/// DBC 报文名称(MsgName)。
///
public string MsgName { get; }
///
/// 周期(以 ticks 表示)。
///
public long PeriodTicks { get; }
///
/// 发送顺序(用于同 tick 内的确定性排序)。
///
public int OrderSend { get; }
///
/// 下次计划发送的到期时间(sw.ElapsedTicks)。
///
public long NextDueTicks;
///
/// 上一次记录 warn 的时间(用于日志节流)。
///
public long LastWarnTicks;
}
///
/// 日志服务。
///
private readonly ILogService _log;
///
/// 共享的 ZLG 驱动实例(CAN/CANFD/LIN)。
///
public ZlgCanFd200uDriver Driver { get; }
///
/// 当前选中的配置程序(沿用原有 FreeSql 模型)。
///
///
/// 该属性用于与 UI“配置程序选择”联动。
/// - 本类不负责持久化;
/// - 只在 中用于把配置中的显示名称映射到 DBC 信号集合。
///
public CanLinConfigPro? SelectedCanLinConfigPro { get; set; }
///
/// Dbc 消息集合(用于 UI 绑定)。
///
///
/// 该集合由 / 构建并替换,通常用于界面显示:
/// - MsgName/SignalName/实时值等;
/// - 也会被 用于把“配置程序中的显示名称”映射到信号上。
///
/// 注意:
/// - DBC 解码更新发生在 Driver 的接收线程中,若 UI 对集合元素的属性变化敏感,请确保 UI 侧的线程切换策略。
///
public ObservableCollection ListCanDbcModel { get; private set; } = new ObservableCollection();
///
/// 的 backing 字段。
///
private bool _dbcParserState;
///
/// DBC 解析状态。
///
///
/// 该状态仅表示“DBC 已加载且有信号集合”,常用于 UI 控件的 enable/disable。
///
public bool DbcParserState
{
get { return _dbcParserState; }
private set { _dbcParserState = value; RaisePropertyChanged(); }
}
///
/// 的 backing 字段。
///
private bool _openState;
///
/// 设备打开状态。
///
///
/// 该属性与 保持一致,由构造函数中订阅 Driver.PropertyChanged 同步刷新。
///
public bool OpenState
{
get { return _openState; }
private set { _openState = value; RaisePropertyChanged(); }
}
///
/// 的 backing 字段。
///
private ZlgCanMode _mode;
///
/// CAN 模式(单选:CAN 或 CANFD)。
///
///
/// - 该属性影响发送/调度表下发时使用的 frameType(CAN=8字节,CANFD=64字节);
/// - 切换 Mode 后若要立即生效,通常应重新调用 (使 Driver 侧 frameType 更新)。
///
public ZlgCanMode Mode
{
get { return _mode; }
set { _mode = value; RaisePropertyChanged(); }
}
///
/// 发送使能(与 UI 的调度表使能语义对齐)。
///
///
/// 该属性透传到 :
/// - true:允许“事件驱动发送”(CmdData 变化时发送/覆盖 auto_send)
/// - false:仅允许手动发送或软件调度任务主动调用发送接口
///
public bool SchEnable
{
get { return Driver.SchEnable; }
set { Driver.SchEnable = value; RaisePropertyChanged(); }
}
///
/// 是否启用事件驱动发送。
///
///
/// 该属性透传到 。
/// - true 且 为 true 时:CmdData 变化会触发增量发送/覆盖更新;
/// - false:CmdData 变化不会触发任何发送(由外部显式调用发送方法)。
///
public bool IsCycleSend
{
get { return Driver.IsCycleSend; }
set { Driver.IsCycleSend = value; RaisePropertyChanged(); }
}
///
/// 是否正在循环接收(对齐 Toomoss:IsCycleRevice)。
///
///
/// 该属性为只读透传:接收线程由 创建。
///
public bool IsCycleRevice
{
get { return Driver.IsReceiving; }
}
///
/// 最近是否发送成功(用于 UI 指示)。
///
///
/// 该标志由 Driver 的发送调用或 Tx 回显事件触发,短时间保持 true 后自动回落。
///
public bool IsSendOk
{
get { return Driver.IsSendOk; }
}
///
/// 最近是否接收成功(用于 UI 指示)。
///
///
/// 该标志由 Driver 接收线程收到 Rx 帧触发,短时间保持 true 后自动回落。
///
public bool IsReviceOk
{
get { return Driver.IsReviceOk; }
}
///
/// 要发送的 CAN 指令数据。
///
///
/// 该集合通常来源于“读写设置/配置程序”生成的信号写入项。
/// - 发送时并不是逐条直接写信号,而是按 MsgName 聚合后通过 DBC 编码为帧(由 Driver 完成)。
/// - 本类在 中将集合注入 Driver,并建立信号变化事件订阅。
///
public List CmdData { get; } = new List();
///
/// 调度表(软件调度任务)互斥锁。
///
///
/// 保护以下共享状态的并发访问:
/// - 调度表条目快照
/// - / 生命周期切换
///
/// 说明:
/// - 硬件 auto_send 的生命周期主要由 Driver 内部管理;
/// - 本类在 中同时清理软件任务与硬件 auto_send,确保“停止=设备不再发”。
///
private readonly object _scheduleLock = new object();
///
/// 当前调度表条目快照。
///
///
/// 元组含义:
/// - MsgName:DBC Message Name
/// - CycleMs:周期(ms)
/// - OrderSend/SchTabIndex:用于排序,保证硬件列表 index 的确定性
///
private List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> _scheduleItems = new List<(string, int, int, int)>();
///
/// 软件调度任务取消令牌。
///
private CancellationTokenSource? _scheduleCts;
///
/// 软件调度后台任务。
///
private Task? _scheduleTask;
///
/// 标记“当前循环发送使用配置的调度表项”。
///
///
/// 该字段当前仅用于区分启动来源(配置调度表 or 精确定时循环),未来可用于 UI 展示或行为分支。
///
private bool _scheduleUseConfigItems;
///
/// 要发送的速度指令数据。
///
///
/// 该字段用于兼容旧接口 的“手动发送转速”行为:
/// - 通过 从 中按 ConfigName=转速 选取;
/// - 若未配置该项,则手动发送会记录 warn 并忽略。
///
private CanCmdData? SpeedCanCmdData { get; set; }
///
/// 当前打开的设备索引(默认 0)。
///
///
/// 由 更新,在 时作为打开参数。
///
private uint _deviceIndex = 0;
///
/// 通道0配置快照。
///
///
/// 目前服务层仅初始化通道0,并将合并接收等设备级开关按通道0的配置写入设备。
///
private ZlgCanFdChannelOptions _channel0 = new ZlgCanFdChannelOptions();
///
/// 构造。
///
/// 日志服务。
///
/// 构造时会:
/// - 创建 ;
/// - 订阅 Driver 的 ,并同步更新本服务的派生属性:
/// / / / 。
///
public ZlgCanDriveService(ILogService logService)
{
_log = logService;
Driver = new ZlgCanFd200uDriver(logService);
Driver.PropertyChanged += (_, __) =>
{
OpenState = Driver.OpenState;
RaisePropertyChanged(nameof(IsCycleRevice));
RaisePropertyChanged(nameof(IsSendOk));
RaisePropertyChanged(nameof(IsReviceOk));
};
OpenState = Driver.OpenState;
Mode = ZlgCanMode.Can;
}
///
/// 初始化 CAN 配置信息,并将配置中的名称映射到 DBC 信号集合(用于 UI 显示)。
///
/// 选中的配置。
///
/// 该方法不与设备连接状态强绑定:
/// - 只要 已有信号集合,就可以进行“显示名称映射”。
///
/// 映射规则:
/// - 以 SignalName 为键,在 DBC 模型集合中找到对应信号,并把配置项的 Name 写回 。
///
public void InitCanConfig(CanLinConfigPro selectedCanLinConfigPro)
{
SelectedCanLinConfigPro = selectedCanLinConfigPro;
if (SelectedCanLinConfigPro?.CanLinConfigContents == null) return;
// 先清空旧映射,避免配置项删除/切换后中文名称残留。
if (ListCanDbcModel != null && ListCanDbcModel.Count > 0)
{
foreach (var m in ListCanDbcModel)
{
m.Name = null;
}
}
foreach (var item in SelectedCanLinConfigPro.CanLinConfigContents)
{
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;
}
}
}
///
/// 更新配置(从 DTO/DB 同步到驱动)。
///
/// 设备索引。
/// 仲裁波特率(bps)。
/// 数据波特率(bps)。
/// 终端电阻使能。
/// 是否合并接收。
/// 合并接收缓冲帧数。
///
/// 该方法仅更新服务层保存的“配置快照”,不会立即触发驱动重连。
/// - 生效时机:下一次调用 打开设备并初始化通道。
/// - 当前实现仅使用通道0配置。
///
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;
}
///
/// 打开 CAN/CANFD(按 Mode)。
///
///
/// - 该方法是幂等的:若 为 true,则直接返回;
/// - 成功打开后会初始化通道并启动接收线程(见 )。
///
/// 注意:
/// - 本方法不会自动加载 DBC,也不会自动注入 CmdData;
/// - 建议调用顺序:UpdateConfig -> StartCanDrive -> StartDbc -> LoadCmdDataToDrive。
///
public void StartCanDrive()
{
if (OpenState)
{
return;
}
Driver.OpenAndInitCan(_deviceIndex, _channel0);
Driver.StartReceiveLoop(_channel0.EnableMergeReceive, _channel0.MergeReceiveBufferFrames);
OpenState = Driver.OpenState;
}
///
/// 使能/停止循环接收。
///
/// true=启动接收线程,false=停止接收线程。
///
/// 说明:
/// - 接收线程属于 Driver 内部资源;
/// - 若开启合并接收,接收线程会按通道0配置决定采用 merge 或 per-channel 接收。
///
/// 设备未连接。
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));
}
///
/// 关闭设备(会同时关闭共享的 LIN 通道)。
///
///
/// Close 语义强调“彻底停止后台活动”:
/// - 停止调度表:取消软件调度任务,并清理硬件 auto_send 列表(避免设备侧继续发);
/// - 停止事件驱动发送:将 置为 false;
/// - 停止接收: 内部会停止接收线程并释放句柄。
///
public void CloseDevice()
{
try
{
// Close 语义:关闭时必须停止循环发送与循环接收。
// - 循环发送:停止软件调度,并关闭事件驱动发送标志。
// - 循环接收:Driver.Close 内部会 StopReceiveLoop。
StopSchedule();
IsCycleSend = false;
Driver.Close();
}
finally
{
OpenState = Driver.OpenState;
DbcParserState = false;
}
}
///
/// 设置调度表配置(CAN)。
///
/// 调度表配置项。
///
/// 该方法仅保存调度表条目快照,不会立即启动发送。
/// - 真正启动由 触发。
/// - MsgName 为空的条目会被过滤。
///
public void SetScheduleConfigs(IEnumerable configs)
{
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List();
lock (_scheduleLock)
{
_scheduleItems = list
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
.ToList();
}
}
///
/// 设置调度表配置(CANFD)。
///
/// 调度表配置项。
///
/// 语义同 CAN 版本 ,差异仅在于配置来源 DTO 类型。
///
public void SetScheduleConfigs(IEnumerable configs)
{
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List();
lock (_scheduleLock)
{
_scheduleItems = list
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
.ToList();
}
}
///
/// 启动调度表(硬件定时发送列表 auto_send)。
///
///
/// 本方法选择“硬件调度表”实现:
/// - 将调度表条目转换为设备 auto_send 列表并 apply,使设备按周期自动发送;
/// - 运行时若 CmdData 对应信号值变化,Driver 会覆盖更新对应 index 的帧数据。
///
/// 前置条件:
/// - 设备已连接( true);
/// - 调度表不为空;
/// - DBC 已加载且 CmdData 可编码(否则对应 MsgName 会被 Driver 跳过并记录 warn)。
///
/// 设备未连接或调度表为空。
public void StartSchedule()
{
if (!OpenState)
{
throw new InvalidOperationException("设备未连接,无法启动调度表。");
}
List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items;
lock (_scheduleLock)
{
// 取快照:
// - 避免启动过程中与 UI 线程并发修改调度表集合;
// - 后续 Driver.StartAutoSendSchedule 会在内部再次排序并建立 MsgName->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);
}
///
/// 启动软件精确定时循环发送。
///
/// 周期(ms)。
///
/// 与 的区别:
/// - 本方法完全在 PC 侧调度,周期精度受系统调度与负载影响;
/// - 适用于不依赖硬件 auto_send 的场景,或作为调试/兼容方案。
///
/// 发送对象:
/// - 按当前 中出现的 MsgName 去重后,按统一周期循环发送。
///
/// 设备未连接或 CmdData 为空。
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 固定为 1,SchTabIndex 固定为 0
var items = msgNames.Select(n => (n, ms, 1, 0)).ToList();
_scheduleUseConfigItems = false;
StartSoftwareScheduler(items);
}
///
/// 停止调度表/循环发送。
///
///
/// 该方法会同时停止两类发送资源:
/// - 软件调度:取消 Task 并等待短时间退出;
/// - 硬件调度:调用 Driver.StopAutoSendSchedule 清空设备 auto_send 列表,防止设备继续发送。
///
/// 该方法不会自动将 置为 false:
/// - 保留给上层决定(例如仅暂停调度,但仍允许事件驱动发送)。
///
public void StopSchedule()
{
CancellationTokenSource? cts;
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:停止流程不因硬件清理失败而中断。
}
}
///
/// 启动软件调度后台任务。
///
/// 调度表条目集合(由调用方准备好快照)。
///
/// 该任务使用“下一次到期时间 due”字典进行调度:
/// - 优先 sleep 到最早 due,减少 CPU 占用;
/// - tick 内对 ready 的消息按字典序依次发送;
/// - 每次发送后更新下一次 due。
///
/// 注意:
/// - 这是软件调度,不保证绝对周期精度;
/// - 发送异常会记录 warn,但不会终止调度循环。
///
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(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(unique.Count);
foreach (var kv in unique)
{
list.Add(new SoftwareScheduleItem(kv.Key, ToTicks(kv.Value.CycleMs), kv.Value.OrderSend));
}
list.Sort((a, b) =>
{
var byOrder = a.OrderSend.CompareTo(b.OrderSend);
return byOrder != 0 ? byOrder : string.CompareOrdinal(a.MsgName, b.MsgName);
});
var scheduleItems = list.ToArray();
// 使用单调时钟:避免系统时间跳变导致周期紊乱。
var sw = Stopwatch.StartNew();
for (var i = 0; i < scheduleItems.Length; i++)
{
// 首次启动:立即发送一轮(NextDueTicks=0)。
scheduleItems[i].NextDueTicks = 0;
scheduleItems[i].LastWarnTicks = long.MinValue;
}
lock (_scheduleLock)
{
_scheduleTask = Task.Factory.StartNew(() =>
{
// LongRunning:为调度线程提供独立线程,减少线程池饥饿导致的周期抖动。
RunSoftwareScheduler(sw, scheduleItems, cts.Token, frequency);
}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
}
///
/// 软件调度循环(后台线程)。
///
/// 单调计时器(启动时创建,贯穿整个调度生命周期)。
/// 调度条目数组(启动时构建;运行中会更新每项的到期时间)。
/// 取消令牌(由 触发取消)。
/// (用于 ticks/ms 换算)。
///
/// 设计目标:
/// - 尽量降低 CPU 占用:优先等待到最早 due 附近;
/// - 尽量降低抖动:临近到期使用短等待 + 自旋;
/// - 避免“积压补发”:如果错过多个周期,只发送一次并将 due 跳到 now+period。
///
/// 线程安全说明:
/// - 该方法仅由 创建的 LongRunning 任务调用;
/// - 在该线程内独占读写(不会跨线程访问),因此不需要额外锁。
///
private void RunSoftwareScheduler(Stopwatch sw, SoftwareScheduleItem[] items, CancellationToken token, long frequency)
{
// 日志节流:同一条消息每 10 秒最多记录一次发送异常。
var warnIntervalTicks = frequency * 10;
var spin = new SpinWait();
while (!token.IsCancellationRequested)
{
if (!OpenState)
{
// 设备未打开时不发送:
// - 避免在 Close/重连窗口持续抛异常;
// - 这里仍然响应取消,保证 StopSchedule 不会“卡住等待”。
if (token.WaitHandle.WaitOne(50))
{
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;
}
}
}
}
///
/// 加载 DBC。
///
/// DBC 路径。
/// DBC 解析得到的信号集合。
///
/// - DBC 加载成功后会更新 与 ;
/// - 后续发送(调度表/事件驱动/手动发送)都依赖 DBC 编码能力。
///
/// 设备未连接。
public ObservableCollection 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;
}
///
/// 加载要发送的数据(订阅数据变化事件)。
///
/// 指令数据集合。
///
/// 该方法会:
/// - 更新本服务的 集合;
/// - 从集合中选取“转速”项缓存到 (用于兼容旧的手动发送接口);
/// - 调用 注入 Driver,并建立事件驱动链路。
///
/// 注意:
/// - frameType 由 决定;若运行时切换 Mode,建议重新调用本方法。
///
public void LoadCmdDataToDrive(List cmdData)
{
var list = cmdData ?? new List();
// 以“清空 + AddRange”的方式更新:
// - 保持 CmdData 引用不变,避免上层若持有该 List 引用时出现替换不可见;
// - 也便于在 Driver 层按引用订阅 CanCmdDataChanged 事件(其内部会自行做快照)。
CmdData.Clear();
CmdData.AddRange(list);
// 兼容旧接口:从 CmdData 中缓存“转速”项。
// - SendMsgToCanDrive 仅维护这一个信号;
// - 若业务后续扩展为多信号手动发送,应改为按 MsgName/SignalName 精确定位。
foreach (var item in CmdData)
{
if (string.Equals(item.ConfigName, "转速", StringComparison.Ordinal))
{
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);
}
///
/// 手动发送(目前对齐原服务:仅发送转速)。
///
/// 转速。
///
/// 行为说明:
/// - 优先将 speedData 写入 的 SignalCmdValue;
/// - 若当前未启用事件驱动发送( 或 为 false),则主动触发一次编码发送,
/// 以维持旧版本“点击发送即下发”的体验。
///
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);
}
}
}
}
///
/// ZLG CAN 工作模式。
///
public enum ZlgCanMode
{
///
/// CAN 经典帧。
///
Can = 0,
///
/// CAN FD。
///
CanFd = 1
}
}