Files
CapMachine/CapMachine.Wpf/Services/ZlgLinDriveService.cs
2026-03-02 11:20:08 +08:00

1315 lines
50 KiB
C#
Raw Permalink 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.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
{
/// <summary>
/// ZLG LIN 驱动服务(共享设备句柄)。
/// 说明:
/// - 该服务不直接持有/管理底层原生句柄,而是复用 <see cref="ZlgCanDriveService"/> 中的 <c>Driver</c>(设备级句柄与接收线程均由 Driver 统一管理)。
/// - LIN 的“调度表/循环发送”在当前实现中使用软件精确定时(<see cref="Stopwatch"/> + <see cref="SpinWait"/>)完成;
/// 与 ZLG CAN/CANFD 的硬件 <c>auto_send</c> 不同LIN 侧没有调用硬件调度表能力。
/// - LDF 解析:此处实现了一个尽量容错的轻量解析器,用于建立“帧/信号 -> 位定义”的运行时索引,供 LIN 收发编解码使用。
/// 线程与绑定:
/// - LIN 接收回调通常来自 Driver 的接收线程;更新 WPF 绑定对象时会切换到 UI 线程(见 <see cref="UpdateSignalRtValue"/>)。
/// - CmdData / LDF 索引 / 调度表快照均通过独立锁对象保护,避免 UI 线程与后台线程并发修改引发的竞态。
/// </summary>
public sealed class ZlgLinDriveService : BindableBase
{
/// <summary>
/// LIN 信号定义(运行时索引条目)。
/// 说明:目前仅使用 StartBit/BitLength 做 bit 级读写Factor/Offset/IsSigned 预留用于后续扩展(物理量换算)。
/// </summary>
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; }
}
/// <summary>
/// LIN 帧定义(运行时索引条目)。
/// 说明:
/// - 同时缓存裸 FrameId0-63与受保护 PID含奇偶校验位
/// - Signals 字典仅包含在 LDF 的 Frames 里声明且在 Signals 区能找到 bit 定义的信号。
/// </summary>
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<string, LinSignalDef> Signals { get; } = new Dictionary<string, LinSignalDef>(StringComparer.Ordinal);
}
/// <summary>
/// 软件调度项(以 ticks 为最小单位进行精确定时)。
/// 说明:
/// - NextDueTicks/LastWarnTicks 会在调度线程中更新,因此不对外暴露,仅在调度线程内使用。
/// - PeriodTicks 是基于 <see cref="Stopwatch.Frequency"/> 换算而来,与系统时间无关,抗时间跳变。
/// </summary>
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;
/// <summary>
/// LDF 索引锁:保护帧/信号定义索引与 UI 绑定模型映射_framesByName/_framesByPid/_modelIndex 等)。
/// </summary>
private readonly object _ldfLock = new object();
/// <summary>
/// CmdData 锁:保护 <see cref="CmdData"/> 的替换与事件订阅关系,避免并发枚举/修改。
/// </summary>
private readonly object _cmdLock = new object();
/// <summary>
/// 调度锁:保护调度配置快照与调度线程的启动/停止_scheduleCts/_scheduleTask/_scheduleRunning
/// </summary>
private readonly object _scheduleLock = new object();
/// <summary>
/// 帧名 -> 帧定义索引(用于发送:按 MsgName 找到 PID/信号位定义)。
/// </summary>
private Dictionary<string, LinFrameDef> _framesByName = new Dictionary<string, LinFrameDef>(StringComparer.Ordinal);
/// <summary>
/// PID/ID -> 帧定义索引(用于接收:按 pid/6bit id 回查帧定义并解码信号)。
/// </summary>
private Dictionary<byte, LinFrameDef> _framesByPid = new Dictionary<byte, LinFrameDef>();
/// <summary>
/// “帧名+信号名” -> UI 模型索引,用于接收线程快速定位绑定对象并更新实时值。
/// </summary>
private Dictionary<string, LinLdfModel> _modelIndex = new Dictionary<string, LinLdfModel>(StringComparer.Ordinal);
/// <summary>
/// 未知 PID 的日志节流表:避免接收线程在 LDF 缺失时刷屏。
/// </summary>
private readonly Dictionary<byte, long> _unknownPidLastLogTicks = new Dictionary<byte, long>();
/// <summary>
/// 调度取消源(置空表示未运行)。
/// </summary>
private CancellationTokenSource? _scheduleCts;
/// <summary>
/// 调度后台任务LongRunning
/// </summary>
private Task? _scheduleTask;
/// <summary>
/// 调度运行标记(与 _scheduleCts 配合;主要用于可读性与状态表达)。
/// </summary>
private bool _scheduleRunning;
/// <summary>
/// LIN 调度表 DTO 缓存(由 ViewModel/上层写入;启动调度时会取快照)。
/// </summary>
private List<LINScheduleConfigDto> _linScheduleConfigs = new List<LINScheduleConfigDto>();
/// <summary>
/// 当前选中的配置程序(沿用原有 FreeSql 模型)。
/// </summary>
public CanLinConfigPro? SelectedCanLinConfigPro { get; set; }
private bool _openState;
/// <summary>
/// LIN 打开状态。
/// </summary>
public bool OpenState
{
get { return _openState; }
private set { _openState = value; RaisePropertyChanged(); }
}
/// <summary>
/// 解析 LDF 文本,提取 Frames/Signals/NodesMaster信息。
/// 说明:这是一个轻量解析器:
/// - 目标是构建“帧-信号位定义”的索引,并支持 UI 展示;
/// - 不追求覆盖所有 LDF 语法,仅尽量兼容常见格式。
/// </summary>
/// <param name="ldfText">LDF 原始文本(建议已剔除注释)。</param>
/// <returns>Frames 列表、Signals 位定义字典、Master 节点名(可能为空)。</returns>
private static (List<(string FrameName, int FrameId, int DataLen, string? Publisher, List<string> Signals)>, Dictionary<string, (int StartBit, int BitLen)>, 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*(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*;", RegexOptions.Compiled);
if (m.Success)
{
return m.Groups["name"].Value;
}
return null;
}
private static Dictionary<string, (int StartBit, int BitLen)> ParseSignalsBlock(string? signalsBlock)
{
var dict = new Dictionary<string, (int StartBit, int BitLen)>(StringComparer.Ordinal);
if (string.IsNullOrWhiteSpace(signalsBlock))
{
return dict;
}
// 兼容常见格式SigName: 0, 8;
var sigRegex = new Regex(@"(?im)^\s*(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?<start>\d+)\s*,\s*(?<len>\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<string> Signals)> ParseFramesBlock(string? framesBlock)
{
var list = new List<(string FrameName, int FrameId, int DataLen, string? Publisher, List<string> Signals)>();
if (string.IsNullOrWhiteSpace(framesBlock))
{
return list;
}
// 常见格式FrameName : 0x10, Publisher, 8 { SigA, SigB };
var frameRegex = new Regex(@"(?s)(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?<id>0x[0-9A-Fa-f]+|\d+)\s*,\s*(?<pub>[A-Za-z_][A-Za-z0-9_]*)\s*,\s*(?<len>\d+)\s*\{(?<body>.*?)\}\s*;?", RegexOptions.Compiled);
var sigRegex = new Regex(@"(?m)^[\s\t]*(?<sig>[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<string>();
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)(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?<body>.*?)\}\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<string>();
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);
}
/// <summary>
/// 基于解析结果构建运行时索引:
/// - _framesByName/_framesByPid用于发送与接收定位帧定义
/// - ListLinLdfModel/_modelIndex用于 UI 展示与实时值更新。
/// </summary>
/// <param name="frames">帧定义集合。</param>
/// <param name="signals">信号位定义字典。</param>
/// <param name="masterNodeName">Master 节点名(可能为空)。</param>
private void BuildRuntimeIndex(
List<(string FrameName, int FrameId, int DataLen, string? Publisher, List<string> Signals)> frames,
Dictionary<string, (int StartBit, int BitLen)> signals,
string? masterNodeName)
{
lock (_ldfLock)
{
_framesByName = new Dictionary<string, LinFrameDef>(StringComparer.Ordinal);
_framesByPid = new Dictionary<byte, LinFrameDef>();
_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 为“裸 ID0-63仅 6bit
// 因此两种 key 都建立索引,确保接收解码稳定。
_framesByPid[frame.Pid] = frame;
_framesByPid[(byte)(frame.FrameId & 0x3F)] = frame;
}
ListLinLdfModel.Clear();
_modelIndex = new Dictionary<string, LinLdfModel>(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;
}
}
}
}
/// <summary>
/// 处理一帧 LIN 接收数据:按 pid 匹配帧定义,按信号位定义解码并更新 UI 实时值。
/// 说明:
/// - 该方法可能在 Driver 的接收线程中被调用;
/// - 不做阻塞操作(如 IO/长时间锁持有),避免拖慢接收线程。
/// </summary>
/// <param name="frame">原始 LIN 接收帧。</param>
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;
}
/// <summary>
/// 发送一帧(按当前 CmdData 聚合编码)。
/// </summary>
/// <param name="msgName">帧名。</param>
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<LinCmdData> 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}";
}
/// <summary>
/// 启动软件精确定时循环发送(按统一周期发送当前 CmdData 中出现的帧)。
/// </summary>
/// <param name="cycleMs">周期ms。</param>
public void StartPrecisionCycleSend(int cycleMs)
{
if (!OpenState)
{
throw new InvalidOperationException("设备未连接,无法启动循环发送。");
}
var ms = Math.Max(1, cycleMs);
List<string> 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);
}
}
/// <summary>
/// 停止循环发送。
/// </summary>
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;
/// <summary>
/// LDF 解析状态。
/// 说明:
/// - 当 <see cref="StartLdf"/> 成功解析并建立运行时索引后为 true
/// - 若未解析/解析失败/关闭设备则为 false。
/// - LIN 收发编解码依赖该状态(未解析时接收帧会被忽略,发送会在找不到帧定义时抛出异常)。
/// </summary>
public bool LdfParserState
{
get { return _ldfParserState; }
private set { _ldfParserState = value; RaisePropertyChanged(); }
}
/// <summary>
/// LDF 消息集合UI 绑定)。
/// 说明:
/// - 该集合由 <see cref="StartLdf"/> 解析并构建,用于 UI 展示“帧-信号”全集与实时值;
/// - 未解析 LDF 时保持为空。
/// </summary>
public ObservableCollection<LinLdfModel> ListLinLdfModel { get; private set; } = new ObservableCollection<LinLdfModel>();
/// <summary>
/// 要发送的 LIN 指令集合(来源于配置程序+读写设置)。
/// </summary>
public List<LinCmdData> CmdData { get; } = new List<LinCmdData>();
/// <summary>
/// 是否启用调度发送(与 UI 的调度表使能语义对齐)。
/// </summary>
public bool SchEnable
{
get { return _zlgCanDriveService.Driver.SchEnable; }
set { _zlgCanDriveService.Driver.SchEnable = value; RaisePropertyChanged(); }
}
/// <summary>
/// 是否启用事件驱动发送。
/// </summary>
public bool IsCycleSend
{
get { return _zlgCanDriveService.Driver.IsCycleSend; }
set { _zlgCanDriveService.Driver.IsCycleSend = value; RaisePropertyChanged(); }
}
/// <summary>
/// 构造。
/// </summary>
/// <param name="zlgCanDriveService">共享 CAN 服务。</param>
/// <param name="logService">日志服务。</param>
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}");
}
};
}
/// <summary>
/// 是否正在循环接收(透传底层 Driver 接收线程状态)。
/// </summary>
public bool IsCycleRevice
{
get { return _zlgCanDriveService.IsCycleRevice; }
}
/// <summary>
/// 发送状态指示(短时间保持 true 后回落,透传 Driver
/// </summary>
public bool IsSendOk
{
get { return _zlgCanDriveService.IsSendOk; }
}
/// <summary>
/// 接收状态指示(短时间保持 true 后回落,透传 Driver
/// </summary>
public bool IsReviceOk
{
get { return _zlgCanDriveService.IsReviceOk; }
}
/// <summary>
/// 开关接收线程(共享设备接收线程)。
/// </summary>
/// <param name="enable">是否启用。</param>
public void SetReceiveEnabled(bool enable)
{
_zlgCanDriveService.SetReceiveEnabled(enable);
RaisePropertyChanged(nameof(IsCycleRevice));
}
/// <summary>
/// 下发调度表配置(系统层调度表,支持每帧不同周期)。
/// </summary>
/// <param name="configs">调度表配置集合。</param>
public void SetScheduleConfigs(List<LINScheduleConfigDto> configs)
{
lock (_scheduleLock)
{
// 仅缓存配置快照:真正启动调度时会再取一次快照,避免 UI 边编辑边启动导致的并发问题。
_linScheduleConfigs = configs ?? new List<LINScheduleConfigDto>();
}
}
/// <summary>
/// 启动软件调度(按调度表中每帧的 Cycle 分别调度发送)。
/// 说明ZLG LIN 当前未接入硬件调度表/auto_send因此用软件精确定时实现。
/// </summary>
public void StartSchedule()
{
if (!OpenState)
{
throw new InvalidOperationException("设备未连接,无法启动调度发送。");
}
List<LINScheduleConfigDto> snapshot;
lock (_scheduleLock)
{
snapshot = _linScheduleConfigs?.ToList() ?? new List<LINScheduleConfigDto>();
}
// 仅使用“被激活的调度表”中的“被选中消息”
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);
}
}
/// <summary>
/// 设置并订阅要发送的指令集合(事件驱动)。
/// </summary>
/// <param name="cmdData">指令集合。</param>
public void LoadCmdDataToDrive(IEnumerable<LinCmdData> 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)
{
// 订阅变化事件:用于“值变化增量发送”(由 <see cref="IsCycleSend"/> + <see cref="SchEnable"/> 共同控制)。
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}");
}
}
/// <summary>
/// 初始化 LIN 配置信息(目前仅缓存)。
/// </summary>
/// <param name="selectedLinConfigPro">选中的配置。</param>
public void InitLinConfig(CanLinConfigPro selectedLinConfigPro)
{
SelectedCanLinConfigPro = selectedLinConfigPro;
}
/// <summary>
/// 打开 LIN共享设备句柄
/// </summary>
/// <param name="deviceIndex">设备索引。</param>
/// <param name="baudRate">波特率。</param>
/// <param name="isMaster">是否主节点。</param>
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;
}
}
/// <summary>
/// 关闭 LIN共享设备句柄下当前实现以 CloseDevice 为准:关闭将同时关闭 CAN/LIN
/// </summary>
public void CloseDevice()
{
// ZLG 的通道句柄都在 Driver 内部;当前 Close 会关闭所有通道,保持与旧系统“同一时刻只有一种驱动工作”的原则一致。
_zlgCanDriveService.CloseDevice();
OpenState = false;
LdfParserState = false;
}
/// <summary>
/// 加载并解析 LDF 文件,建立“帧/信号”的运行时索引,并生成 UI 可绑定的 <see cref="LinLdfModel"/> 集合。
/// 说明:
/// - 这里使用轻量解析器解析常见 LDF 结构Frames/Signals/Nodes目的是满足当前项目的 LIN 编解码与 UI 展示需求。
/// - 若 LDF 格式差异较大,解析可能失败并抛出异常;调用方(通常为 ViewModel应捕获并提示。
/// </summary>
/// <param name="path">LDF 路径。</param>
public ObservableCollection<LinLdfModel> 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<LinLdfModel> ParseLdfFramesAndSignals(string ldfText)
{
// 说明:此解析器只用于生成“帧-信号全集池”,不做位宽/缩放等语义解析。
// 目标:尽可能容错地从 Frames 区域提取 FrameName 与其包含的 SignalName 列表。
var framesBlock = TryExtractNamedBlock(ldfText, "Frames");
if (string.IsNullOrWhiteSpace(framesBlock))
{
return new List<LinLdfModel>();
}
var result = new List<LinLdfModel>();
var exists = new HashSet<string>(StringComparer.Ordinal);
// Frame 定义一般形式FrameName : ... { ... }
// 这里以非贪婪匹配提取每个 Frame 的 body
var frameRegex = new Regex(@"(?s)(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?<body>.*?)\}\s*;?", RegexOptions.Compiled);
var sigRegex = new Regex(@"(?m)^\s*(?<sig>[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;
}
}
}
}