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 } }