1315 lines
50 KiB
C#
1315 lines
50 KiB
C#
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 帧定义(运行时索引条目)。
|
||
/// 说明:
|
||
/// - 同时缓存裸 FrameId(0-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/Nodes(Master)信息。
|
||
/// 说明:这是一个轻量解析器:
|
||
/// - 目标是构建“帧-信号位定义”的索引,并支持 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 为“裸 ID(0-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;
|
||
}
|
||
}
|
||
}
|
||
}
|