using CapMachine.Model.CANLIN; using CapMachine.Wpf.LinDrive; using CapMachine.Wpf.Dtos; using Prism.Mvvm; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; namespace CapMachine.Wpf.Services { /// /// ZLG LIN 驱动服务(共享设备句柄)。 /// 说明: /// - 该服务不直接持有/管理底层原生句柄,而是复用 中的 Driver(设备级句柄与接收线程均由 Driver 统一管理)。 /// - LIN 的“调度表/循环发送”在当前实现中使用软件精确定时( + )完成; /// 与 ZLG CAN/CANFD 的硬件 auto_send 不同,LIN 侧没有调用硬件调度表能力。 /// - LDF 解析:此处实现了一个尽量容错的轻量解析器,用于建立“帧/信号 -> 位定义”的运行时索引,供 LIN 收发编解码使用。 /// 线程与绑定: /// - LIN 接收回调通常来自 Driver 的接收线程;更新 WPF 绑定对象时会切换到 UI 线程(见 )。 /// - CmdData / LDF 索引 / 调度表快照均通过独立锁对象保护,避免 UI 线程与后台线程并发修改引发的竞态。 /// public sealed class ZlgLinDriveService : BindableBase { /// /// LIN 信号定义(运行时索引条目)。 /// 说明:目前仅使用 StartBit/BitLength 做 bit 级读写;Factor/Offset/IsSigned 预留用于后续扩展(物理量换算)。 /// private sealed class LinSignalDef { public string SignalName { get; set; } = string.Empty; public int StartBit { get; set; } public int BitLength { get; set; } public bool IsSigned { get; set; } public double Factor { get; set; } = 1; public double Offset { get; set; } } /// /// LIN 帧定义(运行时索引条目)。 /// 说明: /// - 同时缓存裸 FrameId(0-63)与受保护 PID(含奇偶校验位)。 /// - Signals 字典仅包含在 LDF 的 Frames 里声明且在 Signals 区能找到 bit 定义的信号。 /// private sealed class LinFrameDef { public string FrameName { get; set; } = string.Empty; public byte FrameId { get; set; } public byte Pid { get; set; } public int DataLen { get; set; } = 8; public string? Publisher { get; set; } public bool IsMasterFrame { get; set; } public Dictionary Signals { get; } = new Dictionary(StringComparer.Ordinal); } /// /// 软件调度项(以 ticks 为最小单位进行精确定时)。 /// 说明: /// - NextDueTicks/LastWarnTicks 会在调度线程中更新,因此不对外暴露,仅在调度线程内使用。 /// - PeriodTicks 是基于 换算而来,与系统时间无关,抗时间跳变。 /// private sealed class SoftwareScheduleItem { public SoftwareScheduleItem(string msgName, long periodTicks) { MsgName = msgName; PeriodTicks = periodTicks; } public string MsgName { get; } public long PeriodTicks { get; } public long NextDueTicks; public long LastWarnTicks; } private readonly ILogService _log; private readonly ZlgCanDriveService _zlgCanDriveService; /// /// LDF 索引锁:保护帧/信号定义索引与 UI 绑定模型映射(_framesByName/_framesByPid/_modelIndex 等)。 /// private readonly object _ldfLock = new object(); /// /// CmdData 锁:保护 的替换与事件订阅关系,避免并发枚举/修改。 /// private readonly object _cmdLock = new object(); /// /// 调度锁:保护调度配置快照与调度线程的启动/停止(_scheduleCts/_scheduleTask/_scheduleRunning)。 /// private readonly object _scheduleLock = new object(); /// /// 帧名 -> 帧定义索引(用于发送:按 MsgName 找到 PID/信号位定义)。 /// private Dictionary _framesByName = new Dictionary(StringComparer.Ordinal); /// /// PID/ID -> 帧定义索引(用于接收:按 pid/6bit id 回查帧定义并解码信号)。 /// private Dictionary _framesByPid = new Dictionary(); /// /// “帧名+信号名” -> UI 模型索引,用于接收线程快速定位绑定对象并更新实时值。 /// private Dictionary _modelIndex = new Dictionary(StringComparer.Ordinal); /// /// 未知 PID 的日志节流表:避免接收线程在 LDF 缺失时刷屏。 /// private readonly Dictionary _unknownPidLastLogTicks = new Dictionary(); /// /// 调度取消源(置空表示未运行)。 /// private CancellationTokenSource? _scheduleCts; /// /// 调度后台任务(LongRunning)。 /// private Task? _scheduleTask; /// /// 调度运行标记(与 _scheduleCts 配合;主要用于可读性与状态表达)。 /// private bool _scheduleRunning; /// /// LIN 调度表 DTO 缓存(由 ViewModel/上层写入;启动调度时会取快照)。 /// private List _linScheduleConfigs = new List(); /// /// 当前选中的配置程序(沿用原有 FreeSql 模型)。 /// public CanLinConfigPro? SelectedCanLinConfigPro { get; set; } private bool _openState; /// /// LIN 打开状态。 /// public bool OpenState { get { return _openState; } private set { _openState = value; RaisePropertyChanged(); } } /// /// 解析 LDF 文本,提取 Frames/Signals/Nodes(Master)信息。 /// 说明:这是一个轻量解析器: /// - 目标是构建“帧-信号位定义”的索引,并支持 UI 展示; /// - 不追求覆盖所有 LDF 语法,仅尽量兼容常见格式。 /// /// LDF 原始文本(建议已剔除注释)。 /// Frames 列表、Signals 位定义字典、Master 节点名(可能为空)。 private static (List<(string FrameName, int FrameId, int DataLen, string? Publisher, List Signals)>, Dictionary, string? MasterNodeName) ParseLdf(string ldfText) { var master = TryExtractMasterNodeName(ldfText); var framesBlock = TryExtractNamedBlock(ldfText, "Frames"); var signalsBlock = TryExtractNamedBlock(ldfText, "Signals"); var signals = ParseSignalsBlock(signalsBlock); var frames = ParseFramesBlock(framesBlock); return (frames, signals, master); } private static string? TryExtractMasterNodeName(string ldfText) { var nodesBlock = TryExtractNamedBlock(ldfText, "Nodes"); if (string.IsNullOrWhiteSpace(nodesBlock)) return null; // 常见格式:Master: MasterName; var m = Regex.Match(nodesBlock, @"(?im)^\s*Master\s*:\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*;", RegexOptions.Compiled); if (m.Success) { return m.Groups["name"].Value; } return null; } private static Dictionary ParseSignalsBlock(string? signalsBlock) { var dict = new Dictionary(StringComparer.Ordinal); if (string.IsNullOrWhiteSpace(signalsBlock)) { return dict; } // 兼容常见格式:SigName: 0, 8; var sigRegex = new Regex(@"(?im)^\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?\d+)\s*,\s*(?\d+)", RegexOptions.Compiled); foreach (Match m in sigRegex.Matches(signalsBlock)) { var name = m.Groups["name"].Value; if (string.IsNullOrWhiteSpace(name)) { continue; } if (!int.TryParse(m.Groups["start"].Value, out var startBit)) { continue; } if (!int.TryParse(m.Groups["len"].Value, out var bitLen)) { bitLen = 1; } if (bitLen <= 0) { bitLen = 1; } dict[name] = (startBit, bitLen); } return dict; } private static List<(string FrameName, int FrameId, int DataLen, string? Publisher, List Signals)> ParseFramesBlock(string? framesBlock) { var list = new List<(string FrameName, int FrameId, int DataLen, string? Publisher, List Signals)>(); if (string.IsNullOrWhiteSpace(framesBlock)) { return list; } // 常见格式:FrameName : 0x10, Publisher, 8 { SigA, SigB }; var frameRegex = new Regex(@"(?s)(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?0x[0-9A-Fa-f]+|\d+)\s*,\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*,\s*(?\d+)\s*\{(?.*?)\}\s*;?", RegexOptions.Compiled); var sigRegex = new Regex(@"(?m)^[\s\t]*(?[A-Za-z_][A-Za-z0-9_]*)\s*[,;]", RegexOptions.Compiled); foreach (Match fm in frameRegex.Matches(framesBlock)) { var frameName = fm.Groups["name"].Value; var idText = fm.Groups["id"].Value; var pub = fm.Groups["pub"].Value; var lenText = fm.Groups["len"].Value; var body = fm.Groups["body"].Value; if (string.IsNullOrWhiteSpace(frameName) || string.IsNullOrWhiteSpace(idText) || string.IsNullOrWhiteSpace(lenText)) { continue; } if (!TryParseIntAuto(idText, out var frameId)) { continue; } if (!int.TryParse(lenText, out var dataLen)) { dataLen = 8; } dataLen = Math.Max(1, Math.Min(8, dataLen)); var sigs = new List(); foreach (Match sm in sigRegex.Matches(body)) { var sigName = sm.Groups["sig"].Value; if (string.IsNullOrWhiteSpace(sigName)) { continue; } if (IsReservedKeyword(sigName)) { continue; } sigs.Add(sigName); } list.Add((frameName, frameId, dataLen, string.IsNullOrWhiteSpace(pub) ? null : pub, sigs)); } // 兜底:若正则未匹配到帧(格式差异),退回到只提取 frameName 与 signals if (list.Count == 0) { var fallbackFrameRegex = new Regex(@"(?s)(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?.*?)\}\s*;?", RegexOptions.Compiled); foreach (Match fm in fallbackFrameRegex.Matches(framesBlock)) { var frameName = fm.Groups["name"].Value; var body = fm.Groups["body"].Value; if (string.IsNullOrWhiteSpace(frameName) || string.IsNullOrWhiteSpace(body)) { continue; } var sigs = new List(); foreach (Match sm in sigRegex.Matches(body)) { var sigName = sm.Groups["sig"].Value; if (string.IsNullOrWhiteSpace(sigName)) { continue; } if (IsReservedKeyword(sigName)) { continue; } sigs.Add(sigName); } list.Add((frameName, 0, 8, null, sigs)); } } return list; } private static bool TryParseIntAuto(string s, out int value) { s = s.Trim(); if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { return int.TryParse(s.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out value); } return int.TryParse(s, out value); } /// /// 基于解析结果构建运行时索引: /// - _framesByName/_framesByPid:用于发送与接收定位帧定义; /// - ListLinLdfModel/_modelIndex:用于 UI 展示与实时值更新。 /// /// 帧定义集合。 /// 信号位定义字典。 /// Master 节点名(可能为空)。 private void BuildRuntimeIndex( List<(string FrameName, int FrameId, int DataLen, string? Publisher, List Signals)> frames, Dictionary signals, string? masterNodeName) { lock (_ldfLock) { _framesByName = new Dictionary(StringComparer.Ordinal); _framesByPid = new Dictionary(); _unknownPidLastLogTicks.Clear(); foreach (var f in frames) { var pid = BuildProtectedId((byte)f.FrameId); var isMasterFrame = !string.IsNullOrWhiteSpace(masterNodeName) && string.Equals(f.Publisher, masterNodeName, StringComparison.Ordinal); var frame = new LinFrameDef { FrameName = f.FrameName, FrameId = (byte)f.FrameId, Pid = pid, DataLen = f.DataLen, Publisher = f.Publisher, IsMasterFrame = isMasterFrame, }; foreach (var sigName in f.Signals) { if (!signals.TryGetValue(sigName, out var def)) { continue; } frame.Signals[sigName] = new LinSignalDef { SignalName = sigName, StartBit = def.StartBit, BitLength = def.BitLen, IsSigned = false, Factor = 1, Offset = 0, }; } _framesByName[frame.FrameName] = frame; // 兼容: // - 有些设备/固件回传 pid 为“受保护 PID(含奇偶校验位)” // - 有些回传 pid 为“裸 ID(0-63,仅 6bit)” // 因此两种 key 都建立索引,确保接收解码稳定。 _framesByPid[frame.Pid] = frame; _framesByPid[(byte)(frame.FrameId & 0x3F)] = frame; } ListLinLdfModel.Clear(); _modelIndex = new Dictionary(StringComparer.Ordinal); foreach (var frame in _framesByName.Values.OrderBy(a => a.FrameName, StringComparer.Ordinal)) { foreach (var sig in frame.Signals.Values.OrderBy(a => a.SignalName, StringComparer.Ordinal)) { var model = new LinLdfModel { MsgName = frame.FrameName, SignalName = sig.SignalName, Publisher = frame.Publisher, IsMasterFrame = frame.IsMasterFrame ? "是" : "否", SignalDesc = null, SignalUnit = null, Name = null, IsSeletedInfo = 0, }; ListLinLdfModel.Add(model); _modelIndex[BuildMsgSigKey(frame.FrameName, sig.SignalName)] = model; } } } } /// /// 处理一帧 LIN 接收数据:按 pid 匹配帧定义,按信号位定义解码并更新 UI 实时值。 /// 说明: /// - 该方法可能在 Driver 的接收线程中被调用; /// - 不做阻塞操作(如 IO/长时间锁持有),避免拖慢接收线程。 /// /// 原始 LIN 接收帧。 private void HandleLinFrame(CanDrive.ZlgCan.ZlgLinRxFrame frame) { if (!LdfParserState) { return; } LinFrameDef? def; lock (_ldfLock) { if (!_framesByPid.TryGetValue(frame.Pid, out def)) { // pid 可能是 6bit ID 或 8bit PID,做回退匹配 var id6 = (byte)(frame.Pid & 0x3F); if (!_framesByPid.TryGetValue(id6, out def)) { var pidProtected = BuildProtectedId(id6); _framesByPid.TryGetValue(pidProtected, out def); } if (def == null) { // 节流日志:每个 pid 至少间隔 5s 输出一次,避免刷屏 var now = Environment.TickCount64; if (!_unknownPidLastLogTicks.TryGetValue(frame.Pid, out var last) || now - last >= 5000) { _unknownPidLastLogTicks[frame.Pid] = now; _log.Debug($"ZLG LIN 收到未知 PID=0x{frame.Pid:X2}(id6=0x{id6:X2}),LDF 中未找到对应帧,已忽略。DataLen={frame.Data?.Length ?? 0}。"); } return; } } } if (def == null) { return; } var bytes = frame.Data; foreach (var sig in def.Signals.Values) { // LIN 信号位序按 Intel/Little-Endian 方式读取(与现有 CmdData 写入一致)。 var raw = ReadBitsIntel(bytes, sig.StartBit, sig.BitLength, sig.IsSigned); UpdateSignalRtValue(def.FrameName, sig.SignalName, raw.ToString()); } } private void UpdateSignalRtValue(string msgName, string signalName, string value) { var key = BuildMsgSigKey(msgName, signalName); LinLdfModel? model; lock (_ldfLock) { _modelIndex.TryGetValue(key, out model); } if (model == null) { return; } // WPF:从接收线程更新 UI 绑定对象时,尽量切到 UI 线程。 var app = Application.Current; if (app != null && !app.Dispatcher.CheckAccess()) { app.Dispatcher.BeginInvoke(new Action(() => model.SignalRtValue = value)); return; } model.SignalRtValue = value; } /// /// 发送一帧(按当前 CmdData 聚合编码)。 /// /// 帧名。 public void SendOneFrameByMsgName(string msgName) { if (!OpenState) { throw new InvalidOperationException("设备未连接,无法发送。"); } LinFrameDef? def; lock (_ldfLock) { if (!_framesByName.TryGetValue(msgName, out def)) { throw new InvalidOperationException($"未找到帧定义:{msgName}。请先解析 LDF。"); } } if (def == null) { return; } var data = new byte[8]; List cmds; lock (_cmdLock) { cmds = CmdData.Where(a => string.Equals(a.MsgName, msgName, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(a.SignalName)).ToList(); } foreach (var cmd in cmds) { if (cmd.SignalName == null) { continue; } if (!def.Signals.TryGetValue(cmd.SignalName, out var sigDef)) { continue; } var raw = ConvertPhysicalToRaw(cmd.SignalCmdValue, sigDef); WriteBitsIntel(data, sigDef.StartBit, sigDef.BitLength, raw); } // LIN 发送:复用 CAN Service 中的 Driver 发送接口。 // 说明: // - channelIndex 当前固定为 0(项目约定)。 // - dataLen 取 LDF 帧定义中的长度,避免发送多余填充字节。 _zlgCanDriveService.Driver.TransmitLin(0, def.Pid, data.AsSpan(0, def.DataLen), 0); } private static ulong ConvertPhysicalToRaw(double physical, LinSignalDef sigDef) { var v = (physical - sigDef.Offset) / (Math.Abs(sigDef.Factor) < double.Epsilon ? 1 : sigDef.Factor); if (sigDef.IsSigned) { var max = (1L << (sigDef.BitLength - 1)) - 1; var min = -(1L << (sigDef.BitLength - 1)); var val = (long)Math.Round(v); if (val > max) val = max; if (val < min) val = min; unchecked { return (ulong)val; } } var umax = sigDef.BitLength >= 64 ? ulong.MaxValue : ((1UL << sigDef.BitLength) - 1UL); var uval = (ulong)Math.Max(0, Math.Round(v)); if (uval > umax) uval = umax; return uval; } private static void WriteBitsIntel(byte[] data, int startBit, int bitLen, ulong value) { if (data == null || data.Length == 0) return; if (bitLen <= 0) return; for (int i = 0; i < bitLen; i++) { var bitPos = startBit + i; var byteIndex = bitPos / 8; var bitIndex = bitPos % 8; if (byteIndex < 0 || byteIndex >= data.Length) break; var mask = (byte)(1 << bitIndex); if (((value >> i) & 0x1) == 1) { data[byteIndex] |= mask; } else { data[byteIndex] &= (byte)~mask; } } } private static long ReadBitsIntel(byte[] data, int startBit, int bitLen, bool isSigned) { if (data == null || data.Length == 0) return 0; if (bitLen <= 0) return 0; ulong raw = 0; for (int i = 0; i < bitLen; i++) { var bitPos = startBit + i; var byteIndex = bitPos / 8; var bitIndex = bitPos % 8; if (byteIndex < 0 || byteIndex >= data.Length) break; var bit = (data[byteIndex] >> bitIndex) & 0x1; raw |= ((ulong)bit << i); } if (!isSigned) { return (long)raw; } if (bitLen >= 64) { unchecked { return (long)raw; } } var signBit = 1UL << (bitLen - 1); if ((raw & signBit) == 0) { return (long)raw; } var mask = (1UL << bitLen) - 1; var twos = (~raw + 1) & mask; return -(long)twos; } private static byte BuildProtectedId(byte frameId) { var id = (byte)(frameId & 0x3F); var id0 = (id >> 0) & 1; var id1 = (id >> 1) & 1; var id2 = (id >> 2) & 1; var id3 = (id >> 3) & 1; var id4 = (id >> 4) & 1; var id5 = (id >> 5) & 1; var p0 = (id0 ^ id1 ^ id2 ^ id4) & 1; var p1 = (~(id1 ^ id3 ^ id4 ^ id5)) & 1; return (byte)(id | (p0 << 6) | (p1 << 7)); } private static string BuildMsgSigKey(string msgName, string signalName) { return $"{msgName}\0{signalName}"; } /// /// 启动软件精确定时循环发送(按统一周期发送当前 CmdData 中出现的帧)。 /// /// 周期(ms)。 public void StartPrecisionCycleSend(int cycleMs) { if (!OpenState) { throw new InvalidOperationException("设备未连接,无法启动循环发送。"); } var ms = Math.Max(1, cycleMs); List msgNames; lock (_cmdLock) { msgNames = CmdData.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).Select(a => a.MsgName!).Distinct(StringComparer.Ordinal).ToList(); } if (msgNames.Count == 0) { throw new InvalidOperationException("CmdData 为空,无法启动循环发送。"); } var sw = Stopwatch.StartNew(); var freq = Stopwatch.Frequency; var periodTicks = (long)(ms * (double)freq / 1000); if (periodTicks <= 0) periodTicks = 1; var items = msgNames.Select(n => new SoftwareScheduleItem(n, periodTicks)).ToArray(); var now = sw.ElapsedTicks; for (int i = 0; i < items.Length; i++) { items[i].NextDueTicks = now + items[i].PeriodTicks; } lock (_scheduleLock) { StopSchedule(); _scheduleRunning = true; _scheduleCts = new CancellationTokenSource(); var token = _scheduleCts.Token; _scheduleTask = Task.Factory.StartNew( () => RunSoftwareScheduler(sw, items, token, freq), token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } } /// /// 停止循环发送。 /// public void StopSchedule() { lock (_scheduleLock) { _scheduleRunning = false; try { _scheduleCts?.Cancel(); } catch { // ignore } _scheduleCts = null; _scheduleTask = null; } } private void RunSoftwareScheduler(Stopwatch sw, SoftwareScheduleItem[] items, CancellationToken token, long frequency) { // 说明: // - 采用“下一次到期 ticks”驱动调度,避免 Thread.Sleep 累积误差。 // - waitMs>5 时先 WaitOne 较长时间以降低 CPU;临近触发时用 1ms 等待+SpinWait 降低抖动。 // - 对单帧发送失败做节流告警,避免循环中刷屏。 var warnIntervalTicks = frequency * 10; var spin = new SpinWait(); while (!token.IsCancellationRequested) { if (!OpenState) { 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) { 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; } nowTicks = sw.ElapsedTicks; for (var i = 0; i < items.Length; i++) { var it = items[i]; if (it.NextDueTicks > nowTicks) { continue; } try { SendOneFrameByMsgName(it.MsgName); } catch (Exception ex) { var t = sw.ElapsedTicks; if (t - it.LastWarnTicks >= warnIntervalTicks) { it.LastWarnTicks = t; _log.Warn($"LIN 软件循环发送失败:{it.MsgName},{ex.Message}"); } } finally { // 以“上一次计划时间 + 周期”推进 next,保证周期稳定; // 若调度发生严重滞后(next 已过期),则直接以当前时间重新对齐。 var next = it.NextDueTicks + it.PeriodTicks; if (next <= nowTicks) { next = nowTicks + it.PeriodTicks; } it.NextDueTicks = next; } } } } private bool _ldfParserState; /// /// LDF 解析状态。 /// 说明: /// - 当 成功解析并建立运行时索引后为 true; /// - 若未解析/解析失败/关闭设备则为 false。 /// - LIN 收发编解码依赖该状态(未解析时接收帧会被忽略,发送会在找不到帧定义时抛出异常)。 /// public bool LdfParserState { get { return _ldfParserState; } private set { _ldfParserState = value; RaisePropertyChanged(); } } /// /// LDF 消息集合(UI 绑定)。 /// 说明: /// - 该集合由 解析并构建,用于 UI 展示“帧-信号”全集与实时值; /// - 未解析 LDF 时保持为空。 /// public ObservableCollection ListLinLdfModel { get; private set; } = new ObservableCollection(); /// /// 要发送的 LIN 指令集合(来源于配置程序+读写设置)。 /// public List CmdData { get; } = new List(); /// /// 是否启用调度发送(与 UI 的调度表使能语义对齐)。 /// public bool SchEnable { get { return _zlgCanDriveService.Driver.SchEnable; } set { _zlgCanDriveService.Driver.SchEnable = value; RaisePropertyChanged(); } } /// /// 是否启用事件驱动发送。 /// public bool IsCycleSend { get { return _zlgCanDriveService.Driver.IsCycleSend; } set { _zlgCanDriveService.Driver.IsCycleSend = value; RaisePropertyChanged(); } } /// /// 构造。 /// /// 共享 CAN 服务。 /// 日志服务。 public ZlgLinDriveService(ZlgCanDriveService zlgCanDriveService, ILogService logService) { _zlgCanDriveService = zlgCanDriveService; _log = logService; // 透传 CAN Service/Driver 的状态指示灯属性(用于 UI 绑定)。 _zlgCanDriveService.PropertyChanged += (_, __) => { RaisePropertyChanged(nameof(IsCycleRevice)); RaisePropertyChanged(nameof(IsSendOk)); RaisePropertyChanged(nameof(IsReviceOk)); }; // 订阅 LIN 帧接收事件:回调线程由 Driver 的接收线程决定(通常为后台线程)。 _zlgCanDriveService.Driver.LinFrameReceived += frame => { try { HandleLinFrame(frame); } catch (Exception ex) { _log.Warn($"ZLG LIN 接收处理异常:{ex.Message}"); } }; } /// /// 是否正在循环接收(透传底层 Driver 接收线程状态)。 /// public bool IsCycleRevice { get { return _zlgCanDriveService.IsCycleRevice; } } /// /// 发送状态指示(短时间保持 true 后回落,透传 Driver)。 /// public bool IsSendOk { get { return _zlgCanDriveService.IsSendOk; } } /// /// 接收状态指示(短时间保持 true 后回落,透传 Driver)。 /// public bool IsReviceOk { get { return _zlgCanDriveService.IsReviceOk; } } /// /// 开关接收线程(共享设备接收线程)。 /// /// 是否启用。 public void SetReceiveEnabled(bool enable) { _zlgCanDriveService.SetReceiveEnabled(enable); RaisePropertyChanged(nameof(IsCycleRevice)); } /// /// 下发调度表配置(系统层调度表,支持每帧不同周期)。 /// /// 调度表配置集合。 public void SetScheduleConfigs(List configs) { lock (_scheduleLock) { // 仅缓存配置快照:真正启动调度时会再取一次快照,避免 UI 边编辑边启动导致的并发问题。 _linScheduleConfigs = configs ?? new List(); } } /// /// 启动软件调度(按调度表中每帧的 Cycle 分别调度发送)。 /// 说明:ZLG LIN 当前未接入硬件调度表/auto_send,因此用软件精确定时实现。 /// public void StartSchedule() { if (!OpenState) { throw new InvalidOperationException("设备未连接,无法启动调度发送。"); } List snapshot; lock (_scheduleLock) { snapshot = _linScheduleConfigs?.ToList() ?? new List(); } // 仅使用“被激活的调度表”中的“被选中消息” var msgItems = snapshot .Where(a => a != null) .Where(a => a.IsActive) .Where(a => a.IsMsgActived) .Where(a => !string.IsNullOrWhiteSpace(a.MsgName)) .GroupBy(a => a.MsgName!, StringComparer.Ordinal) .Select(g => { // 同一 MsgName 若出现多个周期,以最小周期为准(更安全) var cycle = g.Select(x => x.Cycle).Where(x => x > 0).DefaultIfEmpty(100).Min(); return (MsgName: g.Key, Cycle: cycle); }) .ToList(); if (msgItems.Count == 0) { throw new InvalidOperationException("调度表为空或未选择任何消息帧,无法启动调度。"); } var sw = Stopwatch.StartNew(); var freq = Stopwatch.Frequency; var items = msgItems .Select(x => { var ms = Math.Max(1, x.Cycle); var ticks = (long)(ms * (double)freq / 1000); if (ticks <= 0) ticks = 1; return new SoftwareScheduleItem(x.MsgName, ticks); }) .ToArray(); var now = sw.ElapsedTicks; for (int i = 0; i < items.Length; i++) { items[i].NextDueTicks = now + items[i].PeriodTicks; } lock (_scheduleLock) { StopSchedule(); _scheduleRunning = true; _scheduleCts = new CancellationTokenSource(); var token = _scheduleCts.Token; _scheduleTask = Task.Factory.StartNew( () => RunSoftwareScheduler(sw, items, token, freq), token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } } /// /// 设置并订阅要发送的指令集合(事件驱动)。 /// /// 指令集合。 public void LoadCmdDataToDrive(IEnumerable cmdData) { lock (_cmdLock) { if (CmdData.Count > 0) { foreach (var cmd in CmdData) { // 先退订旧对象事件,避免 UI/服务重复绑定导致“同一变化触发多次发送”。 cmd.LinCmdDataChangedHandler -= CmdData_LinCmdDataChangedHandler; } } CmdData.Clear(); if (cmdData != null) { CmdData.AddRange(cmdData); } foreach (var cmd in CmdData) { // 订阅变化事件:用于“值变化增量发送”(由 + 共同控制)。 cmd.LinCmdDataChangedHandler += CmdData_LinCmdDataChangedHandler; } } } private void CmdData_LinCmdDataChangedHandler(object? sender, string msgName) { if (!OpenState) { return; } if (!IsCycleSend || !SchEnable) { // 语义: // - IsCycleSend:是否允许“事件驱动发送”; // - SchEnable:是否启用发送(与 UI 的调度表使能开关对齐)。 return; } if (string.IsNullOrWhiteSpace(msgName)) { return; } try { // 事件触发只发送当前 MsgName 对应帧,避免每次改动都全量发送。 SendOneFrameByMsgName(msgName.Trim()); } catch (Exception ex) { _log.Warn($"ZLG LIN 事件驱动发送失败:{msgName},{ex.Message}"); } } /// /// 初始化 LIN 配置信息(目前仅缓存)。 /// /// 选中的配置。 public void InitLinConfig(CanLinConfigPro selectedLinConfigPro) { SelectedCanLinConfigPro = selectedLinConfigPro; } /// /// 打开 LIN(共享设备句柄)。 /// /// 设备索引。 /// 波特率。 /// 是否主节点。 public void StartLinDrive(uint deviceIndex, uint baudRate, bool isMaster) { if (OpenState) { return; } try { // 先确保设备打开(不影响 CAN 后续 Init) _zlgCanDriveService.Driver.OpenDevice(deviceIndex); // 初始化 LIN 通道 _zlgCanDriveService.Driver.OpenAndInitLin(0, new CanDrive.ZlgCan.ZlgLinChannelOptions { BaudRate = baudRate, IsMaster = isMaster, MaxLength = 8, ChecksumMode = 3 }); // 统一由 CAN 服务侧启动接收线程(设备级 merge 接收可以同时收 CAN/LIN) if (!_zlgCanDriveService.Driver.IsReceiving) { // mergeReceive=true:走合并接收,减少线程与句柄数量(与 CAN 侧默认策略一致)。 _zlgCanDriveService.Driver.StartReceiveLoop(mergeReceive: true, bufferFrames: 200); } OpenState = true; LdfParserState = false; } catch (Exception ex) { _log.Error($"ZLG LIN 打开失败:{ex.Message}"); OpenState = false; throw; } } /// /// 关闭 LIN(共享设备句柄下,当前实现以 CloseDevice 为准:关闭将同时关闭 CAN/LIN)。 /// public void CloseDevice() { // ZLG 的通道句柄都在 Driver 内部;当前 Close 会关闭所有通道,保持与旧系统“同一时刻只有一种驱动工作”的原则一致。 _zlgCanDriveService.CloseDevice(); OpenState = false; LdfParserState = false; } /// /// 加载并解析 LDF 文件,建立“帧/信号”的运行时索引,并生成 UI 可绑定的 集合。 /// 说明: /// - 这里使用轻量解析器解析常见 LDF 结构(Frames/Signals/Nodes),目的是满足当前项目的 LIN 编解码与 UI 展示需求。 /// - 若 LDF 格式差异较大,解析可能失败并抛出异常;调用方(通常为 ViewModel)应捕获并提示。 /// /// LDF 路径。 public ObservableCollection StartLdf(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("LDF 路径为空", nameof(path)); } if (!File.Exists(path)) { throw new FileNotFoundException($"LDF 文件不存在:{path}", path); } try { // 读取文本:项目约定 UTF-8;若现场文件是 ANSI/GBK,可能需要外部统一转码。 var text = File.ReadAllText(path, Encoding.UTF8); // 去除单行注释,简化解析 text = Regex.Replace(text, @"//.*?$", string.Empty, RegexOptions.Multiline); // 解析并构建索引(在 _ldfLock 下更新全量索引,避免并发读到半更新状态)。 var (frames, signals, masterNodeName) = ParseLdf(text); BuildRuntimeIndex(frames, signals, masterNodeName); LdfParserState = true; return ListLinLdfModel; } catch (Exception ex) { _log.Error($"ZLG LIN 解析 LDF 失败:{ex.Message}"); LdfParserState = false; throw; } } private static List ParseLdfFramesAndSignals(string ldfText) { // 说明:此解析器只用于生成“帧-信号全集池”,不做位宽/缩放等语义解析。 // 目标:尽可能容错地从 Frames 区域提取 FrameName 与其包含的 SignalName 列表。 var framesBlock = TryExtractNamedBlock(ldfText, "Frames"); if (string.IsNullOrWhiteSpace(framesBlock)) { return new List(); } var result = new List(); var exists = new HashSet(StringComparer.Ordinal); // Frame 定义一般形式:FrameName : ... { ... } // 这里以非贪婪匹配提取每个 Frame 的 body var frameRegex = new Regex(@"(?s)(?[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?.*?)\}\s*;?", RegexOptions.Compiled); var sigRegex = new Regex(@"(?m)^\s*(?[A-Za-z_][A-Za-z0-9_]*)\s*[,;]", RegexOptions.Compiled); foreach (Match fm in frameRegex.Matches(framesBlock)) { var frameName = fm.Groups["name"].Value; var body = fm.Groups["body"].Value; if (string.IsNullOrWhiteSpace(frameName) || string.IsNullOrWhiteSpace(body)) { continue; } foreach (Match sm in sigRegex.Matches(body)) { var sigName = sm.Groups["sig"].Value; if (string.IsNullOrWhiteSpace(sigName)) { continue; } // 排除明显的关键字(避免误采集) if (IsReservedKeyword(sigName)) { continue; } var key = $"{frameName}:{sigName}"; if (!exists.Add(key)) { continue; } result.Add(new LinLdfModel { MsgName = frameName, SignalName = sigName, Name = null, SignalDesc = null, SignalUnit = null, IsSeletedInfo = 0, }); } } return result; } private static string? TryExtractNamedBlock(string text, string blockName) { // 提取形如:blockName { ... } 的块内容(不包含外层大括号)。 // 基于括号深度扫描,避免正则在嵌套结构上失效。 var idx = CultureInvariantIndexOf(text, blockName); if (idx < 0) return null; var braceIdx = text.IndexOf('{', idx); if (braceIdx < 0) return null; int depth = 0; for (int i = braceIdx; i < text.Length; i++) { var ch = text[i]; if (ch == '{') depth++; else if (ch == '}') { depth--; if (depth == 0) { return text.Substring(braceIdx + 1, i - braceIdx - 1); } } } return null; } private static int CultureInvariantIndexOf(string text, string value) { return text.IndexOf(value, StringComparison.OrdinalIgnoreCase); } private static bool IsReservedKeyword(string token) { // LDF 常见关键字/区块名(用于降低误匹配概率) switch (token) { case "Frames": case "Signals": case "Signal": case "Nodes": case "Master": case "Slaves": case "Diagnostic": case "Diagnostics": case "Checksum": case "Event_triggered_frames": case "Sporadic_frames": case "Schedule_tables": case "Node_attributes": case "Node_composition": case "LIN_protocol": case "LIN_protocol_version": case "LIN_speed": case "Protocol_version": return true; default: return false; } } } }