From 9cb139b2089eff07132ce96b8c4f1fddf9e6d754 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9Chsc=E2=80=9D?= <“huangsucan@kaiyili-lab.com”>
Date: Fri, 23 Oct 2026 18:03:07 +0800
Subject: [PATCH] =?UTF-8?q?CAN=E4=BF=AE=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ACP/App.xaml.cs | 4 +
CANModule/ViewModels/CANViewModel.cs | 67 +-
MonitorModule/ViewModels/MonitorViewModel.cs | 5 +-
MonitorModule/ViewModels/RecordViewModel.cs | 3 +-
TSMasterCAN/CAN.cs | 693 +++++++++++++++---
TSMasterCAN/DBCParse.cs | 13 +
.../ViewModels/TSMasterXCPViewModel.cs | 129 ++++
TSMasterXCPModule/Views/TSMasterXCPView.xaml | 92 +++
.../GlobalVariable/CANMonitoringService.cs | 126 ++--
.../GlobalVariable/CANSignalBroadcaster.cs | 30 +-
UIShare/GlobalVariable/DeviceManager.cs | 88 ++-
XCPCore/XCPCore/A2lDatabase.cs | 379 ++++++++++
12 files changed, 1462 insertions(+), 167 deletions(-)
create mode 100644 XCPCore/XCPCore/A2lDatabase.cs
diff --git a/ACP/App.xaml.cs b/ACP/App.xaml.cs
index c9b45b0..5035818 100644
--- a/ACP/App.xaml.cs
+++ b/ACP/App.xaml.cs
@@ -49,11 +49,15 @@ namespace ACP
private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
LoggerHelper.Error(e.Exception.Message, e.Exception.StackTrace);
+ // 必须标记已处理,否则异常在日志后继续传播,进程照样闪退
+ e.Handled = true;
}
private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
LoggerHelper.Error(e.Exception.Message, e.Exception.StackTrace);
+ // 必须标记已观察,否则终结器线程会重新抛出该异常并导致进程闪退
+ e.SetObserved();
}
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
diff --git a/CANModule/ViewModels/CANViewModel.cs b/CANModule/ViewModels/CANViewModel.cs
index 67585f0..847943b 100644
--- a/CANModule/ViewModels/CANViewModel.cs
+++ b/CANModule/ViewModels/CANViewModel.cs
@@ -315,9 +315,10 @@ namespace CANModule.ViewModels
}
catch (Exception ex)
{
- _dialogService.ShowDialog("MessageDialog", new DialogParameters
+ _dialogService.ShowDialog("MessageBox", new DialogParameters
{
- { "message", $"报文转换失败:{ex.Message}" }
+ { "Message", $"报文转换失败:{ex.Message}" },
+ { "ShowOk", true },
});
return;
}
@@ -343,16 +344,43 @@ namespace CANModule.ViewModels
private void SetAndSendMessage()
{
- if (int.TryParse(CycleSendInterval, out int sendPeriod) && double.TryParse(SignalValue, out double SValue) && byte.TryParse(MessageSendChannel, out byte c))
+ if (!int.TryParse(CycleSendInterval, out int sendPeriod) ||
+ !double.TryParse(SignalValue, out double SValue) ||
+ !byte.TryParse(MessageSendChannel, out byte c))
{
- channel = c;
- var message = SelectedMessage.Split(']')[1];
- var re = CAN.SetSignalValue(channel, message, SelectedSignal, SValue, IsDirectlySend, sendPeriod);
+ ShowMessage("发送参数无效:请检查发送周期、信号值和发送通道。");
+ return;
}
- else
- {
+ channel = c;
+ var parts = SelectedMessage.Split(']');
+ var message = parts.Length > 1 ? parts[1] : SelectedMessage;
+ try
+ {
+ var re = CAN.SetSignalValue(channel, message, SelectedSignal, SValue, IsDirectlySend, sendPeriod);
+ if (re != 0)
+ {
+ // -1 表示托管缓存里没有这条报文:DBC 没加载到该通道,或加载通道与发送通道不一致
+ ShowMessage(re == -1
+ ? $"通道 {channel} 的 DBC 缓存中没有报文 [{message}],请先加载 DBC 并确认加载通道与发送通道一致。"
+ : $"发送报文失败,返回代码 {re}:{CAN.GetErrorDescription(re)}");
+ }
}
+ catch (Exception ex)
+ {
+ // 93(message not found in database) 等原生错误在这里提示,避免冒泡到全局异常处理器
+ ShowMessage(ex.Message);
+ }
+ }
+
+ /// 统一的提示弹窗
+ private void ShowMessage(string message)
+ {
+ _dialogService.ShowDialog("MessageBox", new DialogParameters
+ {
+ { "Message", message },
+ { "ShowOk", true },
+ });
}
private void ReleaseTSMaster()
@@ -363,6 +391,29 @@ namespace CANModule.ViewModels
private void InitTSMaster()
{
var re = CAN.Init("ACP");
+ if (re != 0)
+ {
+ ShowMessage($"TSMaster 初始化失败,返回代码 {re}:{CAN.GetErrorDescription(re)}");
+ return;
+ }
+
+ // 驱动被释放后原生 DBC 数据库为空、托管缓存也已被清空,
+ // 这里按保存的自动加载列表把 DBC 重新下发,否则发送会得到 93
+ if (CAN.NeedReloadDbc) ReloadAutoDbc();
+ }
+
+ /// 重新下发配置中保存的 DBC(驱动重启后调用)
+ private void ReloadAutoDbc()
+ {
+ foreach (var item in DBCAutoLoadList)
+ {
+ if (item.DBCChannel < 0 || item.DBCChannel >= 4) continue;
+ if (string.IsNullOrWhiteSpace(item.DBCFilePath) || !File.Exists(item.DBCFilePath)) continue;
+
+ var re = CAN.LoadDBC(item.DBCFilePath, new[] { item.DBCChannel }, out _);
+ if (re != 0)
+ ShowMessage($"DBC 重新加载失败(通道 {item.DBCChannel}):{item.DBCFilePath},返回代码 {re}");
+ }
}
private void UpLoadDBC()
{
diff --git a/MonitorModule/ViewModels/MonitorViewModel.cs b/MonitorModule/ViewModels/MonitorViewModel.cs
index 410b664..ba4d40d 100644
--- a/MonitorModule/ViewModels/MonitorViewModel.cs
+++ b/MonitorModule/ViewModels/MonitorViewModel.cs
@@ -186,8 +186,9 @@ namespace MonitorModule.ViewModels
if (!string.IsNullOrEmpty(TestStatus))
_canSignalBroadcaster?.UnregisterScope(TestStatus);
- // 3. 释放容器作用域
- _scope?.Dispose();
+ // 注意:_scope 是从 GlobalInfo.ScopeDic 借用的工位作用域,拥有者是 AutomatedTestingViewModel,
+ // 这里不能 Dispose,否则关闭监控页就会销毁整个工位(DeviceManager/SystemConfig 全部失效,
+ // 并让 CAN 卡的作用域引用计数提前归零)。只清理本页面自己的资源即可。
Channels.Clear();
AvailableMethods.Clear();
}
diff --git a/MonitorModule/ViewModels/RecordViewModel.cs b/MonitorModule/ViewModels/RecordViewModel.cs
index 358b7cc..a74608b 100644
--- a/MonitorModule/ViewModels/RecordViewModel.cs
+++ b/MonitorModule/ViewModels/RecordViewModel.cs
@@ -173,7 +173,8 @@ namespace MonitorModule.ViewModels
public void Dispose()
{
- _scope?.Dispose();
+ // _scope 来自 GlobalInfo.ScopeDic,拥有者是 AutomatedTestingViewModel,
+ // 借用方不能 Dispose,否则关闭查询页会销毁整个工位并让 CAN 引用计数提前归零
}
#region 筛选条件加载
diff --git a/TSMasterCAN/CAN.cs b/TSMasterCAN/CAN.cs
index 0b31c35..74cc49e 100644
--- a/TSMasterCAN/CAN.cs
+++ b/TSMasterCAN/CAN.cs
@@ -22,8 +22,28 @@ namespace TSMasterCAN
public static event Action DisConnectEvent;
public static bool ConnectFlag { get; set; } = false;
+ /// 初始化互斥锁:TSMaster 原生库每个进程只允许初始化一次
+ private static readonly object _initLock = new object();
+ /// 初始化结果缓存(0 表示已成功初始化,无需再次初始化)
+ private static int _initResult = int.MinValue;
+
///
- /// 初始化CAN驱动
+ /// 驱动是否刚被释放、需要重新下发 DBC。
+ ///
+ /// Release()/UnLoadDBC 成功后置 true,任一次 DBC 加载成功后置 false;
+ /// 上层(如 CANModule 点「初始化驱动」后)据此决定是否重载 DBC,
+ /// 避免拿残留的托管缓存去匹配已经清空的原生数据库(返回 93)。
+ ///
+ ///
+ public static bool NeedReloadDbc { get; private set; }
+
+ ///
+ /// 初始化CAN驱动。
+ ///
+ /// TSMaster 原生库每个进程只允许初始化一次:多工位(一拖三)各自调用时,
+ /// 重复 initialize_lib_tsmaster 会重建原生引擎,正在运行的循环发送表与回调队列随之失效,
+ /// 下一次发报文即触发原生访问冲突闪退。因此这里做进程级一次性保护,后续调用直接复用首次结果。
+ ///
///
///
///
@@ -31,46 +51,264 @@ namespace TSMasterCAN
[Browsable(false)]
public static int Init(string ProjectName, string? filePath = null)
{
- if (string.IsNullOrEmpty(filePath))
+ try
{
- return TsMasterApi.initialize_lib_tsmaster(ProjectName);
+ lock (_initLock)
+ {
+ // 已成功初始化则直接复用:重复 initialize 会重建原生引擎,
+ // 正在循环发送的报文表随即失效。初始化失败则允许后续重试。
+ if (_initResult == 0) return 0;
+
+ _initResult = string.IsNullOrEmpty(filePath)
+ ? TsMasterApi.initialize_lib_tsmaster(ProjectName)
+ : TsMasterApi.initialize_lib_tsmaster_with_project(ProjectName, Path.GetFullPath(filePath));
+
+ return _initResult;
+ }
}
- else
+ catch (Exception ex)
{
- return TsMasterApi.initialize_lib_tsmaster_with_project(ProjectName, Path.GetFullPath(filePath));
+ // 原生 API 异常统一转为返回码,不得向外传播导致进程闪退
+ Debug.WriteLine($"CAN.Init 异常:{ex.Message}");
+ return -1;
}
}
///
- /// 释放CAN驱动
+ /// 释放CAN驱动。
+ /// 释放后清除初始化缓存与回调注册状态,允许下一次 Init 重新初始化。
///
[Browsable(false)]
public static void Release()
{
- TsMasterApi.finalize_lib_tsmaster();
+ try
+ {
+ lock (_nativeSendLock)
+ {
+ lock (_initLock)
+ {
+ TsMasterApi.finalize_lib_tsmaster();
+ _initResult = int.MinValue;
+ // 驱动释放后 TSMaster 内部 DBC 数据库全部失效:
+ // 本地加载记录与托管报文缓存都要清空,并标记需要重新下发 DBC,
+ // 否则后续发送会拿残留缓存去匹配空数据库,返回 93(message not found in database)
+ _loadedDbcPool.Clear();
+ DBCParse.ClearMsgDatabase();
+ NeedReloadDbc = true;
+ ResetListenerRegistration();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.Release 异常:{ex.Message}");
+ }
}
public static int obj = 0;
- public static TCANFDQueueEvent_Win32 listener;
+
+ #region 一拖多引用计数
+
+ /// 作用域名缺失时的兜底键
+ private const string DefaultScopeKey = "__default__";
+
+ ///
+ /// CAN 卡的作用域引用表(作用域名 → 引用计数)。
+ ///
+ /// CAN 卡是一拖多共享硬件:多个工位共用同一个 TSMaster 实例,
+ /// 必须等最后一个工位销毁后才能停止循环报文、断开总线并释放原生驱动。
+ ///
+ ///
+ private static readonly ConcurrentDictionary _scopeRefs =
+ new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+
+ /// 当前仍持有 CAN 卡引用的作用域数量。
+ public static int ScopeRefCount => _scopeRefs.Count;
+
+ private static string NormalizeScopeKey(string? scopeName)
+ => string.IsNullOrWhiteSpace(scopeName) ? DefaultScopeKey : scopeName!;
+
+ ///
+ /// 登记一个作用域对 CAN 卡的引用,需与 成对调用。
+ ///
+ /// 作用域名(通常为 SystemConfig.Title)
+ public static void AcquireScope(string? scopeName)
+ {
+ var key = NormalizeScopeKey(scopeName);
+ _scopeRefs.AddOrUpdate(key, 1, (_, count) => count + 1);
+ Debug.WriteLine($"CAN.AcquireScope:{key},当前引用数 {_scopeRefs.Count}");
+ }
+
+ ///
+ /// 注销一个作用域对 CAN 卡的引用;最后一个引用移除时停止循环报文、断开总线并释放驱动。
+ ///
+ /// 作用域名(通常为 SystemConfig.Title)
+ /// 剩余引用数,0 表示已停止 CAN 卡
+ public static int ReleaseScope(string? scopeName)
+ {
+ var key = NormalizeScopeKey(scopeName);
+
+ while (true)
+ {
+ if (!_scopeRefs.TryGetValue(key, out var count)) return _scopeRefs.Count;
+
+ if (count > 1)
+ {
+ if (_scopeRefs.TryUpdate(key, count - 1, count)) return _scopeRefs.Count;
+ continue;
+ }
+
+ if (_scopeRefs.TryRemove(new KeyValuePair(key, count)))
+ {
+ Debug.WriteLine($"CAN.ReleaseScope:{key},剩余引用数 {_scopeRefs.Count}");
+
+ if (!_scopeRefs.IsEmpty) return _scopeRefs.Count;
+
+ // 最后一个作用域已销毁:先停循环报文,再断开总线,最后释放原生驱动
+ DeleteCyclicMsgs();
+ DisConnect();
+ Release();
+ return 0;
+ }
+ }
+ }
+
+ #endregion
+
+ /// 订阅者增删锁(仅保护列表本身,分发时不持锁,避免与原生回调线程死锁)
+ private static readonly object _listenerLock = new object();
+
+ ///
+ /// 发送类原生 API 串行锁:多工位共用同一个 TSMaster 实例,
+ /// 并发进入 tsapp_add_cyclic_msg_canfd / tsapp_transmit_canfd_async / tsapp_delete_cyclic_msgs 会破坏原生状态。
+ ///
+ private static readonly object _nativeSendLock = new object();
+
+ /// 各工位注册的回调(在托管侧保根,原生库只持有唯一的 _dispatcher 函数指针)
+ private static readonly List _listeners = new List();
+
+ ///
+ /// 唯一注册到 TSMaster 的原生回调委托。
+ ///
+ /// 必须由静态字段长期持有:原生库只保存函数指针,托管委托一旦被 GC 回收,
+ /// 原生线程回调到已释放的 thunk 就会直接访问冲突闪退(.NET 无法捕获)。
+ /// 多工位场景下原先每个工位各自覆盖 listener 静态字段,前一工位的委托随即失去根引用,
+ /// 这正是开两个工位发报文必闪退的原因。
+ ///
+ ///
+ private static TCANFDQueueEvent_Win32? _dispatcher;
+
+ /// 原生回调是否已注册(保证全进程只注册一次,避免同一个 obj 重复注册破坏 TSMaster 内部回调表)
+ private static bool _dispatcherRegistered = false;
+
+ ///
+ /// 注册 CANFD 报文回调(多工位安全)。
+ /// 首个订阅者触发原生注册,其余订阅者仅加入托管侧分发列表,从而避免重复注册与委托被回收。
+ ///
[Browsable(false)]
public static int RegisterListener(TCANFDQueueEvent_Win32 listenEvent)
{
- listener = listenEvent;
- var re = TsMasterApi.tsapp_register_event_canfd(ref obj, listener);
- Debug.Assert(re == 0);
- return re;
+ if (listenEvent == null) return -1;
+ try
+ {
+ lock (_listenerLock)
+ {
+ if (!_listeners.Contains(listenEvent)) _listeners.Add(listenEvent);
+
+ if (_dispatcherRegistered) return 0;
+
+ _dispatcher = DispatchCanFdEvent;
+ var re = TsMasterApi.tsapp_register_event_canfd(ref obj, _dispatcher);
+ Debug.Assert(re == 0);
+ if (re == 0) _dispatcherRegistered = true;
+ return re;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.RegisterListener 异常:{ex.Message}");
+ return -1;
+ }
}
+ ///
+ /// 注销 CANFD 报文回调。
+ /// 指定委托时只移除该订阅者;不指定时移除全部订阅者。所有订阅者移除后才反注册原生回调。
+ ///
[Browsable(false)]
- public static int UnRegisterListener(TCANFDQueueEvent_Win32 listenEvent = null)
+ public static int UnRegisterListener(TCANFDQueueEvent_Win32? listenEvent = null)
{
- if (listenEvent is not null)
+ try
{
- listener = listenEvent;
+ lock (_listenerLock)
+ {
+ if (listenEvent is not null)
+ {
+ _listeners.Remove(listenEvent);
+ }
+ else
+ {
+ _listeners.Clear();
+ }
+
+ if (_listeners.Count > 0 || !_dispatcherRegistered) return 0;
+
+ var dispatcher = _dispatcher;
+ if (dispatcher is null) return 0;
+
+ var re = TsMasterApi.tsapp_unregister_event_canfd(ref obj, dispatcher);
+ Debug.Assert(re == 0);
+ if (re == 0) ResetListenerRegistrationCore();
+ return re;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.UnRegisterListener 异常:{ex.Message}");
+ return -1;
+ }
+ }
+
+ /// 清除回调注册状态(调用方需自行持有 )
+ private static void ResetListenerRegistrationCore()
+ {
+ _dispatcherRegistered = false;
+ _dispatcher = null;
+ }
+
+ /// 清除回调注册状态与全部订阅者
+ private static void ResetListenerRegistration()
+ {
+ lock (_listenerLock)
+ {
+ _listeners.Clear();
+ ResetListenerRegistrationCore();
+ }
+ }
+
+ ///
+ /// 原生 CANFD 回调统一入口:向所有已注册工位分发同一帧数据。
+ /// 单个订阅者异常必须就地隔离,异常一旦跨出原生边界即进程闪退。
+ ///
+ private static void DispatchCanFdEvent(ref int AObj, ref TLIBCANFD AData)
+ {
+ TCANFDQueueEvent_Win32[] snapshot;
+ lock (_listenerLock)
+ {
+ snapshot = _listeners.ToArray();
+ }
+
+ foreach (var listener in snapshot)
+ {
+ try
+ {
+ listener(ref AObj, ref AData);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN 回调订阅者异常(已隔离):{ex.Message}");
+ }
}
- var re = TsMasterApi.tsapp_unregister_event_canfd(ref obj, listener);
- Debug.Assert(re == 0);
- return re;
}
///
@@ -80,27 +318,44 @@ namespace TSMasterCAN
/// 返回代码,0 表示成功,非 0 表示失败(可用 查询错误说明)
public static int Connect()
{
+ try
+ {
+ lock (_nativeSendLock)
+ {
+ // 多工位共用同一张 CAN 卡:任一工位已连接即复用,避免重复 tsapp_connect
+ if (ConnectFlag) return 0;
- var re = TsMasterApi.tsapp_connect();
- if (re == 0)
- {
- re = TsMasterApi.tscom_can_rbs_start();
- if (re == 0)
- {
- ConnectFlag = true;
- Task.Run(() => ConnectEvent?.Invoke());
- }
- else
- {
- Debug.WriteLine($"CAN_RBS启动失败,错误代码:{re}");
+ var re = TsMasterApi.tsapp_connect();
+ if (re == 0)
+ {
+ re = TsMasterApi.tscom_can_rbs_start();
+ if (re == 0)
+ {
+ ConnectFlag = true;
+ Task.Run(() =>
+ {
+ try { ConnectEvent?.Invoke(); }
+ catch (Exception ex) { Debug.WriteLine($"CAN.ConnectEvent 订阅者异常:{ex.Message}"); }
+ });
+ }
+ else
+ {
+ Debug.WriteLine($"CAN_RBS启动失败,错误代码:{re}");
+ }
+ }
+ else
+ {
+ Debug.WriteLine($"CAN连接失败,错误代码:{re}");
+ return re;
+ }
+ return re;
}
}
- else
+ catch (Exception ex)
{
- Debug.WriteLine($"CAN连接失败,错误代码:{re}");
- return re;
+ Debug.WriteLine($"CAN.Connect 异常:{ex.Message}");
+ return -1;
}
- return re;
}
///
@@ -109,20 +364,36 @@ namespace TSMasterCAN
/// 返回代码,0 表示成功,非 0 表示失败(可用 查询错误说明)
public static int DisConnect()
{
- var re = TsMasterApi.tsapp_disconnect();
- //Debug.Assert(re == 0);
- if (re == 0)
+ try
{
- ConnectFlag = false;
- Task.Run(() => DisConnectEvent?.Invoke());
+ lock (_nativeSendLock)
+ {
+ // 已断开则直接返回:多工位场景下避免后续工位重复断开总线
+ if (!ConnectFlag) return 0;
+
+ var re = TsMasterApi.tsapp_disconnect();
+ if (re == 0)
+ {
+ ConnectFlag = false;
+ Task.Run(() =>
+ {
+ try { DisConnectEvent?.Invoke(); }
+ catch (Exception ex) { Debug.WriteLine($"CAN.DisConnectEvent 订阅者异常:{ex.Message}"); }
+ });
+ }
+ else
+ {
+ Debug.WriteLine($"断开CAN连接失败,错误代码:{re}");
+ }
+
+ return re;
+ }
}
- else
+ catch (Exception ex)
{
- Debug.WriteLine($"断开CAN连接失败,错误代码:{re}");
+ Debug.WriteLine($"CAN.DisConnect 异常:{ex.Message}");
+ return -1;
}
-
-
- return re;
}
///
@@ -137,15 +408,102 @@ namespace TSMasterCAN
{
databaseID = 0;
if (!Path.Exists(filePath)) return -1;
- var re = TsMasterApi.tsdb_load_can_db(Path.GetFullPath(filePath), string.Join(",", channel), ref databaseID);
- foreach (var item in channel)
+ int re;
+ try
{
- DBCParse.parse(databaseID, item);
- DBCParse.rbs_parse(databaseID, item);
+ re = TsMasterApi.tsdb_load_can_db(Path.GetFullPath(filePath), string.Join(",", channel), ref databaseID);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.LoadDBC 异常:{ex.Message}");
+ return -1;
}
+ if (re != 0)
+ {
+ // 加载失败时不再解析:databaseID 无效,解析只会把错误的报文混进托管缓存,
+ // 之后发送这些报文会得到 93(message not found in database)
+ Debug.WriteLine($"CAN.LoadDBC 失败:{filePath},返回代码 {re}");
+ return re;
+ }
+
+ foreach (var item in channel)
+ {
+ try
+ {
+ if (item < 0 || item >= DBCParse.MsgDatabase.Count) continue;
+ DBCParse.parse(databaseID, item);
+ DBCParse.rbs_parse(databaseID, item);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.LoadDBC 解析失败(通道 {item}):{ex.Message}");
+ }
+ }
+
+ // 原生库已重新装载报文,托管缓存与原生库重新对齐
+ NeedReloadDbc = false;
return re;
}
+
+ ///
+ /// 已加载过的 DBC 记录(键 = 通道集合 + 绝对路径,值 = TSMaster 数据库 ID)。
+ ///
+ /// 一拖多场景下多个工位共用同一张 CAN 卡:DBC 只需下发并解析一次,
+ /// 重复加载会让 DBCParse 把同一批报文重复追加进 MsgDatabase。
+ ///
+ ///
+ private static readonly ConcurrentDictionary _loadedDbcPool =
+ new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+
+ private static string BuildDbcKey(string filePath, int[] channels)
+ => $"{string.Join(",", channels.OrderBy(c => c))}|{Path.GetFullPath(filePath)}";
+
+ ///
+ /// 查询指定通道上的 DBC 是否已由其他工位加载过。
+ ///
+ /// DBC 文件路径
+ /// 通道号数组
+ [Browsable(false)]
+ public static bool IsDbcLoaded(string filePath, int[] channel)
+ {
+ if (!Path.Exists(filePath)) return false;
+ return _loadedDbcPool.ContainsKey(BuildDbcKey(filePath, channel));
+ }
+
+ ///
+ /// 加载DBC(同一通道 + 同一文件只加载一次)。
+ ///
+ /// 供多工位自动加载使用:其他工位已加载过同一 DBC 时直接返回 0,
+ /// 不重复调用 tsdb_load_can_db,也不重复追加 DBCParse.MsgDatabase。
+ ///
+ ///
+ /// DBC 文件路径
+ /// 通道号数组
+ /// 首次加载返回的数据库 ID;已加载过时返回首次的 ID
+ /// 返回代码,0 表示成功(含已加载跳过)
+ [Browsable(false)]
+ public static int LoadDBCOnce(string filePath, int[] channel, out uint databaseID)
+ {
+ databaseID = 0;
+ if (!Path.Exists(filePath)) return -1;
+
+ var key = BuildDbcKey(filePath, channel);
+ if (_loadedDbcPool.TryGetValue(key, out var loadedId))
+ {
+ databaseID = loadedId;
+ return 0;
+ }
+
+ var re = LoadDBC(filePath, channel, out databaseID);
+ if (re == 0) _loadedDbcPool[key] = databaseID;
+ return re;
+ }
+
+ /// 清空 DBC 加载记录(卸载 DBC 或释放驱动时调用)。
+ [Browsable(false)]
+ public static void ClearDbcLoadRecords() => _loadedDbcPool.Clear();
+
///
/// 加载DBC至单一通道
///
@@ -156,9 +514,28 @@ namespace TSMasterCAN
{
uint databaseID = 0;
if (!Path.Exists(filePath)) return -1;
- var re = TsMasterApi.tsdb_load_can_db(Path.GetFullPath(filePath), string.Join(",", channel), ref databaseID);
- DBCParse.parse(databaseID, channel);
- DBCParse.rbs_parse(databaseID, channel);
+ int re;
+ try
+ {
+ re = TsMasterApi.tsdb_load_can_db(Path.GetFullPath(filePath), string.Join(",", channel), ref databaseID);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.LoadDBCToSingleChannel 异常:{ex.Message}");
+ return -1;
+ }
+ try
+ {
+ if (channel >= 0 && channel < DBCParse.MsgDatabase.Count)
+ {
+ DBCParse.parse(databaseID, channel);
+ DBCParse.rbs_parse(databaseID, channel);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.LoadDBCToSingleChannel 解析失败(通道 {channel}):{ex.Message}");
+ }
return re;
}
@@ -170,13 +547,32 @@ namespace TSMasterCAN
[Browsable(false)]
public static int UnLoadDBC(uint? databaseID = null)
{
- if (databaseID == null)
+ try
{
- return TsMasterApi.tsdb_unload_can_dbs();
+ int re;
+ if (databaseID == null)
+ {
+ re = TsMasterApi.tsdb_unload_can_dbs();
+ }
+ else
+ {
+ re = TsMasterApi.tsdb_unload_can_db(databaseID.Value);
+ }
+
+ // TSMaster 内部数据库已卸载:本地加载记录与托管报文缓存必须同步失效,
+ // 否则再发送时会用残留的报文名去匹配空数据库,返回 93(message not found in database)
+ if (re == 0)
+ {
+ _loadedDbcPool.Clear();
+ DBCParse.ClearMsgDatabase();
+ NeedReloadDbc = true;
+ }
+ return re;
}
- else
+ catch (Exception ex)
{
- return TsMasterApi.tsdb_unload_can_db(databaseID.Value);
+ Debug.WriteLine($"CAN.UnLoadDBC 异常:{ex.Message}");
+ return -1;
}
}
@@ -187,7 +583,15 @@ namespace TSMasterCAN
/// 返回代码,0 表示成功,非 0 表示失败
public static int StartLogging(string filePath)
{
- return TsMasterApi.tsapp_start_logging(Path.GetFullPath(filePath));
+ try
+ {
+ return TsMasterApi.tsapp_start_logging(Path.GetFullPath(filePath));
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.StartLogging 异常:{ex.Message}");
+ return -1;
+ }
}
///
@@ -196,7 +600,15 @@ namespace TSMasterCAN
/// 返回代码,0 表示成功,非 0 表示失败
public static int StopLogging()
{
- return TsMasterApi.tsapp_stop_logging();
+ try
+ {
+ return TsMasterApi.tsapp_stop_logging();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.StopLogging 异常:{ex.Message}");
+ return -1;
+ }
}
///
@@ -220,8 +632,16 @@ namespace TSMasterCAN
public static double GetSignalValue(byte channel, string AMsgName, string ASgnName)
{
double value = double.NaN;
- var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
- var re = TsMasterApi.tsdb_get_signal_value_canfd(ref find.ACANFD, AMsgName, ASgnName, ref value);
+ var find = DBCParse.MsgDatabase[channel].FirstOrDefault(s => s.msg_name == AMsgName);
+ if (find == null) return value;
+ try
+ {
+ TsMasterApi.tsdb_get_signal_value_canfd(ref find.ACANFD, AMsgName, ASgnName, ref value);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.GetSignalValue 异常:{ex.Message}");
+ }
return value;
}
@@ -237,8 +657,17 @@ namespace TSMasterCAN
[Browsable(false)]
public static int GetSignalValue(byte channel, string AMsgName, string ASgnName, ref double value)
{
- var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
- return TsMasterApi.tsdb_get_signal_value_canfd(ref find.ACANFD, AMsgName, ASgnName, ref value);
+ var find = DBCParse.MsgDatabase[channel].FirstOrDefault(s => s.msg_name == AMsgName);
+ if (find == null) return -1;
+ try
+ {
+ return TsMasterApi.tsdb_get_signal_value_canfd(ref find.ACANFD, AMsgName, ASgnName, ref value);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.GetSignalValue 异常:{ex.Message}");
+ return -1;
+ }
}
///
@@ -269,21 +698,41 @@ namespace TSMasterCAN
/// 信号编码写入报文失败时抛出
public static int SetSignalValue(byte channel, string AMsgName, string ASgnName, double AValue, bool isSend = false, float sendPeriod = 0)
{
- var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
+ // MsgDatabase 只预分配 12 个通道,越界会让发送入口直接抛 IndexOutOfRange
+ if (channel >= DBCParse.MsgDatabase.Count) return -1;
+
+ var find = DBCParse.MsgDatabase[channel].FirstOrDefault(s => s.msg_name == AMsgName);
+ if (find == null) return -1;
find.ACANFD.FIdxChn = channel;
var re = TsMasterApi.tsdb_set_signal_value_canfd(ref find.ACANFD, AMsgName, ASgnName, AValue);
- Debug.Assert(re == 0);
- if (re != 0) throw new Exception($"设置报文失败!返回代码{re}");
- if (re != 0) return re;
+ if (re != 0)
+ {
+ // 93 = message not found in database:原生库在该通道上找不到这个报文名。
+ // 常见成因:驱动被释放 / DBC 被卸载后托管缓存没同步,或 DBC 实际加载在别的通道。
+ throw new Exception($"设置报文失败!返回代码{re}({GetErrorDescription(re)})");
+ }
if (isSend)
{
- if (sendPeriod == 0)
+ try
{
- return TsMasterApi.tsapp_transmit_canfd_async(ref find.ACANFD);
+ // 多工位共用同一个 TSMaster 实例,发送类原生 API 必须串行化,
+ // 并发调用会破坏原生内部循环发送表,表现为发报文瞬间闪退。
+ lock (_nativeSendLock)
+ {
+ if (sendPeriod == 0)
+ {
+ return TsMasterApi.tsapp_transmit_canfd_async(ref find.ACANFD);
+ }
+ else
+ {
+ return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
+ }
+ }
}
- else
+ catch (Exception ex)
{
- return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
+ Debug.WriteLine($"CAN.SetSignalValue 发送异常:{ex.Message}");
+ return -1;
}
}
return 0;
@@ -299,15 +748,27 @@ namespace TSMasterCAN
/// 返回代码,0 表示成功,非 0 表示失败
public static int SetSignalValue(APP_CHANNEL channel, string AMsgName, float sendPeriod = 0)
{
- var find = DBCParse.MsgDatabase[(byte)channel].First(s => s.msg_name == AMsgName);
+ var find = DBCParse.MsgDatabase[(byte)channel].FirstOrDefault(s => s.msg_name == AMsgName);
+ if (find == null) return -1;
find.ACANFD.FIdxChn = (byte)channel;
- if (sendPeriod == 0)
+ try
{
- return TsMasterApi.tsapp_transmit_canfd_async(ref find.ACANFD);
+ lock (_nativeSendLock)
+ {
+ if (sendPeriod == 0)
+ {
+ return TsMasterApi.tsapp_transmit_canfd_async(ref find.ACANFD);
+ }
+ else
+ {
+ return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
+ }
+ }
}
- else
+ catch (Exception ex)
{
- return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
+ Debug.WriteLine($"CAN.SetSignalValue 发送异常:{ex.Message}");
+ return -1;
}
}
@@ -318,7 +779,18 @@ namespace TSMasterCAN
[Browsable(false)]
public static int SetMsg(TLIBCANFD msg)
{
- return TsMasterApi.tsapp_transmit_canfd_async(ref msg);
+ try
+ {
+ lock (_nativeSendLock)
+ {
+ return TsMasterApi.tsapp_transmit_canfd_async(ref msg);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.SetMsg 发送异常:{ex.Message}");
+ return -1;
+ }
}
///
@@ -391,16 +863,27 @@ namespace TSMasterCAN
int length = Math.Min(ADataArray.Length, 64);
Array.Copy(ADataArray, 0, send.FData, 0, length);
int re;
- if (sendPeriod == 0)
+ try
{
- re = TsMasterApi.tsapp_transmit_canfd_async(ref send);
+ lock (_nativeSendLock)
+ {
+ if (sendPeriod == 0)
+ {
+ re = TsMasterApi.tsapp_transmit_canfd_async(ref send);
+ }
+ else
+ {
+ re = TsMasterApi.tsapp_add_cyclic_msg_canfd(ref send, sendPeriod);
+ }
+ }
}
- else
+ catch (Exception ex)
{
- re = TsMasterApi.tsapp_add_cyclic_msg_canfd(ref send, sendPeriod);
+ Debug.WriteLine($"CAN.SendMsg 发送异常(通道 {(byte)AIdxChn} ID 0x{AID:X}):{ex.Message}");
+ return -1;
}
- if (isUpdateDBCDatabase)
+ if (isUpdateDBCDatabase && (byte)AIdxChn < DBCParse.MsgDatabase.Count)
{
var find = DBCParse.MsgDatabase[(byte)AIdxChn].FirstOrDefault(s => s.ACANFD.FIdentifier == send.FIdentifier);
if (find != null)
@@ -420,18 +903,42 @@ namespace TSMasterCAN
/// 返回代码,0 表示成功,非 0 表示失败
public static int AddCyclicMsg(byte channel, string AMsgName, float sendPeriod)
{
- var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
+ var find = DBCParse.MsgDatabase[channel].FirstOrDefault(s => s.msg_name == AMsgName);
+ if (find == null) return -1;
find.ACANFD.FIdxChn = channel;
- return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
+ try
+ {
+ lock (_nativeSendLock)
+ {
+ return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.AddCyclicMsg 异常(通道 {channel} {AMsgName}):{ex.Message}");
+ return -1;
+ }
}
///
/// 清除所有循环发送的报文,停止全部周期发送任务。
+ /// 注意:TSMaster 的循环发送表是全局的,此调用会同时清掉其他工位的循环发送。
///
/// 返回代码,0 表示成功,非 0 表示失败
public static int DeleteCyclicMsgs()
{
- return TsMasterApi.tsapp_delete_cyclic_msgs();
+ try
+ {
+ lock (_nativeSendLock)
+ {
+ return TsMasterApi.tsapp_delete_cyclic_msgs();
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.DeleteCyclicMsgs 异常:{ex.Message}");
+ return -1;
+ }
}
///
@@ -441,15 +948,23 @@ namespace TSMasterCAN
/// 错误描述文本,未知错误代码时返回提示信息
public static string GetErrorDescription(int errorCode)
{
- IntPtr ADesc = IntPtr.Zero;
- TsMasterApi.tsapp_get_error_description(errorCode, ref ADesc);
+ try
+ {
+ IntPtr ADesc = IntPtr.Zero;
+ TsMasterApi.tsapp_get_error_description(errorCode, ref ADesc);
- if (ADesc == IntPtr.Zero) return $"未知错误代码: {errorCode}";
+ if (ADesc == IntPtr.Zero) return $"未知错误代码: {errorCode}";
- // 假设返回的是 ANSI 字符串(如 C 的 char*)
- string? description = Marshal.PtrToStringAnsi(ADesc);
+ // 假设返回的是 ANSI 字符串(如 C 的 char*)
+ string? description = Marshal.PtrToStringAnsi(ADesc);
- return description ?? $"未知错误代码: {errorCode}";
+ return description ?? $"未知错误代码: {errorCode}";
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"CAN.GetErrorDescription 异常:{ex.Message}");
+ return $"未知错误代码: {errorCode}";
+ }
}
}
diff --git a/TSMasterCAN/DBCParse.cs b/TSMasterCAN/DBCParse.cs
index 420b93c..5f81bb3 100644
--- a/TSMasterCAN/DBCParse.cs
+++ b/TSMasterCAN/DBCParse.cs
@@ -125,6 +125,19 @@ namespace TSMasterCAN
public static List> MsgDatabase { get; set; } =
[.. Enumerable.Range(0, 12).Select(_ => new List<_Msg_>())];
public static can_network can_Network;
+
+ ///
+ /// 清空托管侧报文缓存。
+ ///
+ /// TSMaster 原生数据库被卸载(tsdb_unload_can_dbs)或驱动被 finalize 后,
+ /// 这里的缓存必须同步清空,否则再次发送时会拿缓存里的报文去匹配空数据库,
+ /// tsdb_set_signal_value_canfd 会返回 93(message not found in database)。
+ ///
+ ///
+ public static void ClearMsgDatabase()
+ {
+ foreach (var list in MsgDatabase) list.Clear();
+ }
public static string tsdb_get_can_db_info(uint ADatabaseId, int AType, int AIndex, int ASubIndex)
{
nint tmpInt = new nint();
diff --git a/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs b/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
index 70e9a5e..97bc6cb 100644
--- a/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
+++ b/TSMasterXCPModule/ViewModels/TSMasterXCPViewModel.cs
@@ -1,9 +1,11 @@
using Prism.Commands;
using System;
+using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Win32;
using TSMasterCAN;
using UIShare.ViewModelBase;
using XCPCore;
@@ -50,6 +52,52 @@ namespace TSMasterXCPModule.ViewModels
/// TSMaster 总线是否已连接(tsapp_connect + rbs_start 成功)
public bool IsBusConnected { get => _isBusConnected; set => SetProperty(ref _isBusConnected, value); }
+ // ---------- A2L 描述文件 ----------
+
+ private string _a2lFilePath = string.Empty;
+ /// A2L 文件路径
+ public string A2lFilePath { get => _a2lFilePath; set => SetProperty(ref _a2lFilePath, value); }
+
+ private string _a2lSummaryText = "未加载 A2L 文件";
+ /// A2L 解析结果摘要
+ public string A2lSummaryText { get => _a2lSummaryText; set => SetProperty(ref _a2lSummaryText, value); }
+
+ /// 解析出的测量量列表(选中后自动填入 DAQ 地址与长度)
+ public ObservableCollection A2lMeasurements { get; } = new ObservableCollection();
+
+ /// 解析出的标定量列表(选中后自动填入内存读写地址与长度)
+ public ObservableCollection A2lCharacteristics { get; } = new ObservableCollection();
+
+ private A2lMeasurement? _selectedMeasurement;
+ /// 当前选中的测量量:自动带入 DAQ 的 ODT 地址与长度
+ public A2lMeasurement? SelectedMeasurement
+ {
+ get => _selectedMeasurement;
+ set
+ {
+ if (!SetProperty(ref _selectedMeasurement, value) || value == null) return;
+
+ OdtAddressText = value.AddressText;
+ if (value.SizeBytes > 0) OdtLength = value.SizeBytes.ToString();
+ Log($"A2L 测量量 {value.Name}:地址 {value.AddressText},长度 {value.SizeText}(已填入 DAQ 配置)");
+ }
+ }
+
+ private A2lCharacteristic? _selectedCharacteristic;
+ /// 当前选中的标定量:自动带入内存读写的地址与长度
+ public A2lCharacteristic? SelectedCharacteristic
+ {
+ get => _selectedCharacteristic;
+ set
+ {
+ if (!SetProperty(ref _selectedCharacteristic, value) || value == null) return;
+
+ MemAddressText = value.AddressText;
+ if (value.SizeBytes > 0 && value.SizeBytes <= 200) MemReadLength = value.SizeBytes.ToString();
+ Log($"A2L 标定量 {value.Name}({value.CharacteristicType}):地址 {value.AddressText},长度 {value.SizeText}(已填入内存读写)");
+ }
+ }
+
// ---------- XCP 连接配置 ----------
private string _canChannel = "0";
@@ -136,6 +184,11 @@ namespace TSMasterXCPModule.ViewModels
#region 命令
+ /// 浏览并加载 A2L 描述文件
+ public DelegateCommand BrowseA2lCommand { get; }
+ /// 按当前路径重新解析 A2L 描述文件
+ public DelegateCommand ReloadA2lCommand { get; }
+
/// 初始化 TSMaster 驱动(可选加载工程)
public DelegateCommand InitDriverCommand { get; }
/// 释放 TSMaster 驱动
@@ -170,6 +223,8 @@ namespace TSMasterXCPModule.ViewModels
/// 创建视图模型
public TSMasterXCPViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
+ BrowseA2lCommand = new DelegateCommand(BrowseA2l);
+ ReloadA2lCommand = new DelegateCommand(ReloadA2l);
InitDriverCommand = new DelegateCommand(() => RunGuarded(InitDriverAsync, "初始化驱动"));
ReleaseDriverCommand = new DelegateCommand(() => RunGuarded(ReleaseDriverAsync, "释放驱动"));
ConnectBusCommand = new DelegateCommand(() => RunGuarded(ConnectBusAsync, "连接总线"));
@@ -192,6 +247,80 @@ namespace TSMasterXCPModule.ViewModels
Log("TSMaster XCP 测试界面就绪。使用顺序:初始化驱动 → 连接总线 → XCP 连接 → 内存读写 / DAQ。");
}
+ #region 命令处理 —— A2L 描述文件
+
+ /// 浏览选择 A2L 文件并解析
+ private void BrowseA2l()
+ {
+ var dialog = new OpenFileDialog
+ {
+ Title = "选择 A2L 描述文件",
+ Filter = "A2L 描述文件 (*.a2l)|*.a2l|所有文件 (*.*)|*.*",
+ CheckFileExists = true,
+ Multiselect = false
+ };
+
+ var currentDir = Path.GetDirectoryName(A2lFilePath);
+ if (!string.IsNullOrEmpty(currentDir) && Directory.Exists(currentDir))
+ dialog.InitialDirectory = currentDir;
+
+ if (dialog.ShowDialog() != true) return;
+ LoadA2l(dialog.FileName);
+ }
+
+ /// 按当前路径重新解析 A2L
+ private void ReloadA2l()
+ {
+ var path = A2lFilePath?.Trim() ?? string.Empty;
+ if (path.Length == 0)
+ {
+ Log("[提示] 请先选择 A2L 文件(点击「浏览...」)。");
+ return;
+ }
+ LoadA2l(path);
+ }
+
+ /// 解析并刷新测量量 / 标定量列表
+ private void LoadA2l(string filePath)
+ {
+ if (!File.Exists(filePath))
+ {
+ A2lSummaryText = "A2L 文件不存在";
+ Log($"[错误] A2L 文件不存在: {filePath}");
+ return;
+ }
+
+ try
+ {
+ var db = A2lParser.LoadFile(filePath);
+ A2lFilePath = filePath;
+
+ A2lMeasurements.Clear();
+ foreach (var item in db.Measurements) A2lMeasurements.Add(item);
+
+ A2lCharacteristics.Clear();
+ foreach (var item in db.Characteristics) A2lCharacteristics.Add(item);
+
+ SelectedMeasurement = null;
+ SelectedCharacteristic = null;
+
+ A2lSummaryText = $"{db.Summary} · {Path.GetFileName(filePath)}";
+ Log($"已加载 A2L:{filePath}({db.Summary})");
+
+ if (db.Measurements.Count == 0 && db.Characteristics.Count == 0)
+ Log("[警告] 未解析出可用条目,请确认该文件是 A2L(ASAM MCD-2 MC)描述文件。");
+ }
+ catch (Exception ex)
+ {
+ A2lMeasurements.Clear();
+ A2lCharacteristics.Clear();
+ A2lSummaryText = "A2L 解析失败";
+ Log($"[错误] 解析 A2L 失败:{ex.Message}");
+ }
+ }
+
+ #endregion
+
#region 命令处理 —— TSMaster 总线
private async Task InitDriverAsync()
diff --git a/TSMasterXCPModule/Views/TSMasterXCPView.xaml b/TSMasterXCPModule/Views/TSMasterXCPView.xaml
index 5140dc8..9db3394 100644
--- a/TSMasterXCPModule/Views/TSMasterXCPView.xaml
+++ b/TSMasterXCPModule/Views/TSMasterXCPView.xaml
@@ -122,6 +122,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UIShare/GlobalVariable/CANMonitoringService.cs b/UIShare/GlobalVariable/CANMonitoringService.cs
index 1bbae7c..18712dd 100644
--- a/UIShare/GlobalVariable/CANMonitoringService.cs
+++ b/UIShare/GlobalVariable/CANMonitoringService.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks;
using TSMasterCAN;
using TSMaster;
+using Logger;
namespace UIShare.GlobalVariable
{
public class CANMonitoringService
@@ -23,79 +24,106 @@ namespace UIShare.GlobalVariable
}
public int Init(CancellationToken ct = default)
{
- int re = 0;
- if (IsInitialized == false)
+ // 多工位共用同一个 TSMaster 实例:首次工位已完成 initialize_lib_tsmaster,
+ // 后续工位的 CAN.Init 会直接复用首次结果(返回 0),因此这里不能再用
+ // “初始化未成功就跳过”的判断,否则第二个工位永远注册不到报文回调。
+ var initRe = CAN.Init(_systemConfig.TSMasterName);
+ if (initRe != 0)
{
- re = CAN.Init(_systemConfig.TSMasterName);
- }
- if (IsInitialized == false)
- {
- re = CAN.RegisterListener(MonitorEvent);
- if (re == 0) IsInitialized = true;
+ LoggerHelper.Warn($"CAN 驱动初始化返回 {initRe}({CAN.GetErrorDescription(initRe)}),继续注册报文回调。");
}
+
+ // RegisterListener 幂等:重复调用只补充订阅者;驱动被 Release 后再 Init 也能重新挂回原生回调
+ var re = CAN.RegisterListener(MonitorEvent);
+ if (re == 0) IsInitialized = true;
return re;
}
public ConcurrentDictionary RealTimeMessages { get; set; } = new();
public ConcurrentDictionary RealTimeSignals { get; set; } = new();
+ ///
+ /// TSMaster 原生库收报文回调(原生线程直接调用)。
+ /// 回调内抛出的异常一旦跨出原生边界会直接导致进程闪退,
+ /// 因此整个方法体必须包裹异常隔离,且不做任何可能阻塞的调用。
+ ///
public void MonitorEvent(ref int AObj, ref TLIBCANFD AData)
{
- if (IsStopped) return;
- byte[] oldbytes = null;
-
- //记录至实时报文数据库
- var cpAData = AData;
- cpAData.FData = ArrayPool.Shared.Rent(64);
- Array.Copy(AData.FData, 0, cpAData.FData, 0, cpAData.FData.Length);
- var MessageName = $"{AData.FIdxChn}/{AData.FIdentifier}";
- if (RealTimeMessages.TryGetValue(MessageName, out var v))
+ try
{
- oldbytes = v.FData;
- }
- RealTimeMessages[MessageName] = cpAData;
+ if (IsStopped) return;
- //记录至DBC数据库
- var dbcfind = DBCParse.MsgDatabase[AData.FIdxChn].FirstOrDefault(s => s.msg_id == cpAData.FIdentifier);
- if (dbcfind != null)
- {
- Array.Copy(cpAData.FData, 0, dbcfind.ACANFD.FData, 0, cpAData.FData.Length);
- dbcfind.ACANFD.FTimeUS = cpAData.FTimeUS;
- dbcfind.ACANFD.FIsFD = cpAData.FIsFD;
- dbcfind.ACANFD.FIsBRS = cpAData.FIsBRS;
- dbcfind.ACANFD.FIsESI = cpAData.FIsESI;
- dbcfind.ACANFD.FIsError = cpAData.FIsError;
- dbcfind.ACANFD.FIsRemote = cpAData.FIsRemote;
- dbcfind.ACANFD.FIsExt = cpAData.FIsExt;
- }
+ // 通道越界防护:多通道/异常帧的 FIdxChn 可能超出 DBC 数据库(12 通道)范围
+ if (AData.FIdxChn >= DBCParse.MsgDatabase.Count) return;
+ if (AData.FData == null) return;
- if (oldbytes is not null) ArrayPool.Shared.Return(oldbytes);
+ byte[] oldbytes = null;
- var id = AData.FIdentifier;
- var ch = AData.FIdxChn;
-
- var canSignalList = _systemConfig.ConfigurationList.Where(s => s.MessageID == id && s.Channel == ch).ToArray();
-
- if (canSignalList.Length > 0)
- {
- var find = DBCParse.MsgDatabase[ch].First(s => s.msg_id == id);
- foreach (var item in canSignalList)
+ //记录至实时报文数据库
+ var cpAData = AData;
+ cpAData.FData = ArrayPool.Shared.Rent(64);
+ Array.Copy(AData.FData, 0, cpAData.FData, 0, Math.Min(AData.FData.Length, cpAData.FData.Length));
+ var MessageName = $"{AData.FIdxChn}/{AData.FIdentifier}";
+ if (RealTimeMessages.TryGetValue(MessageName, out var v))
{
- double Signalre = double.NaN;
- var re = CAN.GetSignalValue(ref cpAData, item.MessageName, item.SignalName, ref Signalre);
- //Debug.Assert(re == 0);
- var SinalName = $"{AData.FIdxChn}/{item.MessageName}/{item.SignalName}";
- RealTimeSignals[SinalName] = Signalre;
+ oldbytes = v.FData;
}
+ RealTimeMessages[MessageName] = cpAData;
+
+ //记录至DBC数据库
+ var dbcfind = DBCParse.MsgDatabase[AData.FIdxChn].FirstOrDefault(s => s.msg_id == cpAData.FIdentifier);
+ if (dbcfind != null)
+ {
+ Array.Copy(cpAData.FData, 0, dbcfind.ACANFD.FData, 0, Math.Min(cpAData.FData.Length, dbcfind.ACANFD.FData.Length));
+ dbcfind.ACANFD.FTimeUS = cpAData.FTimeUS;
+ dbcfind.ACANFD.FIsFD = cpAData.FIsFD;
+ dbcfind.ACANFD.FIsBRS = cpAData.FIsBRS;
+ dbcfind.ACANFD.FIsESI = cpAData.FIsESI;
+ dbcfind.ACANFD.FIsError = cpAData.FIsError;
+ dbcfind.ACANFD.FIsRemote = cpAData.FIsRemote;
+ dbcfind.ACANFD.FIsExt = cpAData.FIsExt;
+ }
+
+ if (oldbytes is not null) ArrayPool.Shared.Return(oldbytes);
+
+ var id = AData.FIdentifier;
+ var ch = AData.FIdxChn;
+
+ var configList = _systemConfig.ConfigurationList;
+ if (configList == null) return;
+
+ var canSignalList = configList.Where(s => s.MessageID == id && s.Channel == ch).ToArray();
+
+ if (canSignalList.Length > 0)
+ {
+ // DBC 中查不到该报文时静默跳过(配置与 DBC 不一致不应导致进程崩溃)
+ var find = DBCParse.MsgDatabase[ch].FirstOrDefault(s => s.msg_id == id);
+ if (find == null) return;
+ foreach (var item in canSignalList)
+ {
+ double Signalre = double.NaN;
+ var re = CAN.GetSignalValue(ref cpAData, item.MessageName, item.SignalName, ref Signalre);
+ //Debug.Assert(re == 0);
+ var SinalName = $"{AData.FIdxChn}/{item.MessageName}/{item.SignalName}";
+ RealTimeSignals[SinalName] = Signalre;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // 原生回调内绝不能向外抛异常:跨原生边界即进程闪退。
+ LoggerHelper.Error($"CANMonitoringService.MonitorEvent 回调异常(已隔离):{ex.Message}");
}
}
///
- /// 停止监测:从广播器注销并清理实时数据。
+ /// 停止监测:注销原生报文回调、从广播器注销并清理实时数据。
+ /// 必须先反注册回调再释放实例,否则原生线程仍会回调到已停止的工位。
///
public void Stop()
{
IsStopped = true;
if (IsRegistered)
{
+ CAN.UnRegisterListener(MonitorEvent);
CANSignalBroadcaster.UnregisterService(this);
IsRegistered = false;
}
diff --git a/UIShare/GlobalVariable/CANSignalBroadcaster.cs b/UIShare/GlobalVariable/CANSignalBroadcaster.cs
index adca0c3..6bef2c0 100644
--- a/UIShare/GlobalVariable/CANSignalBroadcaster.cs
+++ b/UIShare/GlobalVariable/CANSignalBroadcaster.cs
@@ -150,20 +150,28 @@ namespace UIShare.GlobalVariable
string methodName = BuildMethodName((uint)cfg.MessageID, signalName);
// 广播信号值到对应作用域
- _eventAggregator.GetEvent().Publish(new HardwareReportArgs
+ // 逐信号隔离:单个信号广播/报警失败不应中断整个广播循环
+ try
{
- Scope = scopeName,
- HardwareFingerprint = fingerprint,
- MethodName = methodName,
- Value = kvp.Value,
- Time = DateTime.Now
- });
+ _eventAggregator.GetEvent().Publish(new HardwareReportArgs
+ {
+ Scope = scopeName,
+ HardwareFingerprint = fingerprint,
+ MethodName = methodName,
+ Value = kvp.Value,
+ Time = DateTime.Now
+ });
- // 报警检查(使用该作用域自己的配置)
- string alarmStatus = ValueLimitAlarmHelper.CheckAlarm(fingerprint, methodName, kvp.Value, scopeConfig);
- if (!string.IsNullOrEmpty(alarmStatus) && alarmStatus != "未报警")
+ // 报警检查(使用该作用域自己的配置)
+ string alarmStatus = ValueLimitAlarmHelper.CheckAlarm(fingerprint, methodName, kvp.Value, scopeConfig);
+ if (!string.IsNullOrEmpty(alarmStatus) && alarmStatus != "未报警")
+ {
+ _eventAggregator.GetEvent().Publish((scopeName, canFingerprint, alarmStatus));
+ }
+ }
+ catch (Exception ex)
{
- _eventAggregator.GetEvent().Publish((scopeName, canFingerprint, alarmStatus));
+ Logger.LoggerHelper.Error($"CANSignalBroadcaster 广播信号异常(作用域 {scopeName} {methodName},已隔离):{ex.Message}");
}
}
}
diff --git a/UIShare/GlobalVariable/DeviceManager.cs b/UIShare/GlobalVariable/DeviceManager.cs
index 6d6ee3a..8cc39a4 100644
--- a/UIShare/GlobalVariable/DeviceManager.cs
+++ b/UIShare/GlobalVariable/DeviceManager.cs
@@ -48,10 +48,39 @@ namespace UIShare.GlobalVariable
_CANMonitoringService=new CANMonitoringService(_systemConfig);
// 用 SystemConfig.Title 作为作用域唯一标识,无需反查 ConfigDic
_scopeName = _systemConfig.Title;
+ AcquireCanScope();
InitDevices();
InitCAN();
}
+ /// 本工位(作用域)是否已登记 CAN 卡引用,保证 Acquire/Release 成对且幂等
+ private bool _canScopeAcquired;
+
+ ///
+ /// 登记本工位对 CAN 卡的引用。
+ /// CAN 卡是一拖多共享硬件,与 HardwarePool 中的设备一样需要按作用域引用计数。
+ ///
+ private void AcquireCanScope()
+ {
+ if (_canScopeAcquired) return;
+ CAN.AcquireScope(_scopeName);
+ _canScopeAcquired = true;
+ }
+
+ ///
+ /// 注销本工位对 CAN 卡的引用。
+ ///
+ /// 只有最后一个工位释放时才真正停止循环报文、断开总线并释放 TSMaster 驱动;
+ /// 仍有其他工位在用时只做引用减一,避免误伤其他工位。
+ ///
+ ///
+ private void ReleaseCanScope()
+ {
+ if (!_canScopeAcquired) return;
+ _canScopeAcquired = false;
+ CAN.ReleaseScope(_scopeName);
+ }
+
public void InitCAN()
{
var re1 = _CANMonitoringService.Init();
@@ -112,6 +141,13 @@ namespace UIShare.GlobalVariable
{
if (config == null || !config.IsEnabled) continue;
+ // CAN 卡由 ConnectCanAsync 独立管理(静态 TSMaster 驱动 + 作用域引用计数),
+ // 不参与 HardwarePool 的设备实例池,这里直接跳过避免误报"未知设备类型"
+ if (string.Equals(config.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
if (string.IsNullOrWhiteSpace(config.DeviceType) ||
!_deviceTypeMap.TryGetValue(config.DeviceType, out var deviceType))
{
@@ -314,11 +350,18 @@ namespace UIShare.GlobalVariable
// 先停止健康监控,避免重连定时器与关闭操作冲突
StopHealthMonitor();
+ // CAN 卡按作用域引用计数释放:仅最后一个工位关闭时才停循环报文、断总线并释放驱动
+ ReleaseCanScope();
+
List tasks = new List();
lock (_lockObj)
{
- if (DeviceMap.Count == 0 ) return;
+ if (DeviceMap.Count == 0)
+ {
+ LoggerHelper.Info("所有设备已执行关闭操作。");
+ return;
+ }
foreach (var kvp in DeviceMap)
{
@@ -331,7 +374,6 @@ namespace UIShare.GlobalVariable
}
}
- CAN.DisConnect();
await Task.WhenAll(tasks);
LoggerHelper.Info("所有设备已执行关闭操作。");
}
@@ -453,9 +495,16 @@ namespace UIShare.GlobalVariable
try
{
+ // 重新连接时恢复本工位对 CAN 卡的引用(之前可能被主动关闭过)
+ AcquireCanScope();
+ // 驱动若曾被最后一个工位释放,这里重新初始化并把报文回调挂回原生库
+ InitCAN();
+
bool ok = await Task.Run(() =>
{
// 自动加载 DBC 文件
+ // 一拖多共享 CAN 卡:同一通道 + 同一文件只加载一次,
+ // 其他工位已经加载过时直接跳过,避免重复下发并重复追加 DBCParse 报文库
if (_systemConfig?.DBCAutoLoadList != null)
{
foreach (var item in _systemConfig.DBCAutoLoadList)
@@ -474,11 +523,22 @@ namespace UIShare.GlobalVariable
LoggerHelper.Warn($"CAN 通道 {item.DBCChannel} 自动加载 DBC 失败:文件不存在 [{item.DBCFilePath}]");
continue;
}
- bool loadOk = CAN.LoadDBC(item.DBCFilePath,new int[] { item.DBCChannel },out _ )==0;
+
+ var dbcChannels = new int[] { item.DBCChannel };
+ bool alreadyLoaded = CAN.IsDbcLoaded(item.DBCFilePath, dbcChannels);
+ bool loadOk = CAN.LoadDBCOnce(item.DBCFilePath, dbcChannels, out _) == 0;
if (loadOk)
{
- LoggerHelper.Info($"CAN 通道 {item.DBCChannel} 已自动加载 DBC:{item.DBCFilePath}");
- // 发布 DBC 加载完成事件,通知监控系统刷新信号列表
+ if (alreadyLoaded)
+ {
+ LoggerHelper.Info($"CAN 通道 {item.DBCChannel} DBC 已由其他工位加载,跳过重复加载:{item.DBCFilePath}");
+ }
+ else
+ {
+ LoggerHelper.Info($"CAN 通道 {item.DBCChannel} 已自动加载 DBC:{item.DBCFilePath}");
+ }
+
+ // 即使跳过加载,也要通知本工位监控系统刷新信号列表
_eventAggregator.GetEvent().Publish(new DBCLoadedArgs
{
Channel = (uint)item.DBCChannel,
@@ -519,11 +579,22 @@ namespace UIShare.GlobalVariable
}
///
- /// 关闭 CAN 卡
+ /// 关闭 CAN 卡:按作用域引用计数释放。
+ ///
+ /// CAN 卡为一拖多共享硬件,这里只注销本工位的引用;
+ /// 仅当最后一个工位释放时, 才停止循环报文、断开总线并释放驱动。
+ ///
///
private void CloseCan()
{
- CAN.DisConnect();
+ try
+ {
+ ReleaseCanScope();
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.Error($"关闭 CAN 卡异常(已隔离):{ex.Message}");
+ }
}
private static IReadOnlyDictionary BuildDeviceTypeMap()
@@ -612,6 +683,9 @@ namespace UIShare.GlobalVariable
// 停止 CAN 信号监测服务
_CANMonitoringService?.Stop();
+ // CAN 卡按作用域引用计数释放:最后一个工位销毁时才停循环报文、断总线并释放驱动
+ ReleaseCanScope();
+
if (string.IsNullOrEmpty(_scopeName)) return;
// 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用
diff --git a/XCPCore/XCPCore/A2lDatabase.cs b/XCPCore/XCPCore/A2lDatabase.cs
new file mode 100644
index 0000000..789c324
--- /dev/null
+++ b/XCPCore/XCPCore/A2lDatabase.cs
@@ -0,0 +1,379 @@
+using System.Globalization;
+using System.Text;
+
+namespace XCPCore
+{
+ ///
+ /// A2L 中的测量量(MEASUREMENT)条目
+ ///
+ public sealed class A2lMeasurement
+ {
+ /// 测量量名称
+ public string Name { get; init; } = string.Empty;
+ /// 测量量描述(A2L 中的 LongIdentifier)
+ public string Description { get; init; } = string.Empty;
+ /// A2L 数据类型(UBYTE / UWORD / FLOAT32_IEEE 等)
+ public string DataType { get; init; } = string.Empty;
+ /// ECU 内存地址
+ public uint Address { get; init; }
+ /// 占用字节数(由数据类型推导)
+ public byte SizeBytes { get; init; }
+ /// 地址的 hex 文本
+ public string AddressText => $"0x{Address:X8}";
+ /// 长度的显示文本
+ public string SizeText => SizeBytes > 0 ? $"{SizeBytes}B" : "?";
+ /// 列表 ToolTip 使用的完整描述
+ public string Detail => $"{Name} {AddressText} {DataType} {SizeText}\n{Description}".TrimEnd();
+ }
+
+ ///
+ /// A2L 中的标定量(CHARACTERISTIC)条目
+ ///
+ public sealed class A2lCharacteristic
+ {
+ /// 标定量名称
+ public string Name { get; init; } = string.Empty;
+ /// 标定量描述(A2L 中的 LongIdentifier)
+ public string Description { get; init; } = string.Empty;
+ /// 标定量结构类型(VALUE / CURVE / MAP / VAL_BLK / ASCII 等)
+ public string CharacteristicType { get; init; } = string.Empty;
+ /// A2L 数据类型(由 RECORD_LAYOUT 的 FNC_VALUES 推导,未知时为空)
+ public string DataType { get; init; } = string.Empty;
+ /// ECU 内存地址
+ public uint Address { get; init; }
+ /// 单元素字节数(未知时为 0)
+ public byte SizeBytes { get; init; }
+ /// 地址的 hex 文本
+ public string AddressText => $"0x{Address:X8}";
+ /// 长度的显示文本
+ public string SizeText => SizeBytes > 0 ? $"{SizeBytes}B" : "?";
+ /// 列表 ToolTip 使用的完整描述
+ public string Detail => $"{Name} {AddressText} {CharacteristicType} {DataType} {SizeText}\n{Description}".TrimEnd();
+ }
+
+ ///
+ /// A2L(ASAM MCD-2 MC)描述文件解析结果
+ ///
+ public sealed class A2lDatabase
+ {
+ /// 来源文件全路径
+ public string FilePath { get; set; } = string.Empty;
+ /// PROJECT 名称(A2L 中可能缺省)
+ public string ProjectName { get; set; } = string.Empty;
+ /// 解析出的测量量列表
+ public List Measurements { get; } = new List();
+ /// 解析出的标定量列表
+ public List Characteristics { get; } = new List();
+ /// 解析结果摘要
+ public string Summary => $"MEASUREMENT {Measurements.Count} 项 · CHARACTERISTIC {Characteristics.Count} 项";
+ }
+
+ ///
+ /// 轻量 A2L 解析器:只提取 XCP 采集/标定所需的测量量、标定量及其地址与长度。
+ ///
+ /// 支持块:MEASUREMENT、CHARACTERISTIC、RECORD_LAYOUT、PROJECT;其余块整体跳过。
+ /// 采用词法切分而非正则,能正确处理块内嵌套的 IF_DATA / AXIS_DESCR 等子块与 /* */ 注释。
+ ///
+ ///
+ public static class A2lParser
+ {
+ private const char StringMark = '\u0001';
+
+ ///
+ /// 读取并解析 A2L 文件
+ ///
+ /// A2L 文件路径
+ /// 解析结果(无法识别的块被忽略,不抛异常)
+ public static A2lDatabase LoadFile(string filePath)
+ {
+ var db = Parse(ReadAllTextSmart(filePath));
+ db.FilePath = filePath;
+ return db;
+ }
+
+ ///
+ /// 解析 A2L 文本内容
+ ///
+ /// A2L 文件内容
+ /// 解析结果(无法识别的块被忽略,不抛异常)
+ public static A2lDatabase Parse(string text)
+ {
+ var db = new A2lDatabase();
+ if (string.IsNullOrWhiteSpace(text)) return db;
+
+ var recordLayouts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var tokens = Tokenize(text);
+
+ for (int i = 0; i + 1 < tokens.Count; i++)
+ {
+ if (!IsKeyword(tokens[i], "/begin")) continue;
+
+ string block = tokens[i + 1].ToUpperInvariant();
+ int contentStart = i + 2;
+ int end = FindBlockEnd(tokens, contentStart, block);
+ if (end <= contentStart) { i = end; continue; }
+
+ switch (block)
+ {
+ case "PROJECT":
+ case "MODULE":
+ // 容器块:MEASUREMENT/CHARACTERISTIC 都嵌套在 MODULE 内,必须继续扫描而不能整块跳过
+ // /begin PROJECT
+ if (block == "PROJECT" && contentStart < end)
+ {
+ db.ProjectName = tokens[contentStart];
+ }
+ continue;
+ case "RECORD_LAYOUT":
+ ParseRecordLayout(tokens, contentStart, end, recordLayouts);
+ break;
+ case "MEASUREMENT":
+ ParseMeasurement(tokens, contentStart, end, db);
+ break;
+ case "CHARACTERISTIC":
+ ParseCharacteristic(tokens, contentStart, end, recordLayouts, db);
+ break;
+ }
+
+ i = end;
+ }
+
+ return db;
+ }
+
+ /// 解析 MEASUREMENT 块:/begin MEASUREMENT Name Desc DataType Conv Resolution Accuracy Lower Upper ... ECU_ADDRESS x
+ private static void ParseMeasurement(List t, int start, int end, A2lDatabase db)
+ {
+ // Name + LongIdentifier + DataType + Conversion + Resolution + Accuracy + Lower + Upper = 8 个必备字段
+ if (end - start < 8) return;
+
+ string name = t[start];
+ if (name.Length == 0 || name[0] == '/') return;
+
+ uint address = 0;
+ for (int i = start + 8; i + 1 < end; i++)
+ {
+ if (IsKeyword(t[i], "ECU_ADDRESS"))
+ {
+ address = ParseHexUInt32(t[i + 1]);
+ break;
+ }
+ }
+ if (address == 0) return; // 没有地址的测量量无法用于 DAQ
+
+ string dataType = t[start + 2].ToUpperInvariant();
+ db.Measurements.Add(new A2lMeasurement
+ {
+ Name = name,
+ Description = Unquote(t[start + 1]),
+ DataType = dataType,
+ Address = address,
+ SizeBytes = GetDataTypeSize(dataType)
+ });
+ }
+
+ /// 解析 CHARACTERISTIC 块:/begin CHARACTERISTIC Name Desc Type Address Deposit MaxDiff Conversion Lower Upper ...
+ private static void ParseCharacteristic(List t, int start, int end,
+ Dictionary recordLayouts, A2lDatabase db)
+ {
+ // Name + LongIdentifier + Type + Address + Deposit + MaxDiff + Conversion + Lower + Upper = 9 个必备字段
+ if (end - start < 9) return;
+
+ string name = t[start];
+ if (name.Length == 0 || name[0] == '/') return;
+
+ uint address = ParseHexUInt32(t[start + 3]);
+ if (address == 0) return;
+
+ string type = t[start + 2].ToUpperInvariant();
+ string deposit = t[start + 4];
+ string dataType = recordLayouts.TryGetValue(deposit, out var layoutType) ? layoutType : string.Empty;
+
+ db.Characteristics.Add(new A2lCharacteristic
+ {
+ Name = name,
+ Description = Unquote(t[start + 1]),
+ CharacteristicType = type,
+ DataType = dataType,
+ Address = address,
+ SizeBytes = GetCharacteristicSize(type, dataType)
+ });
+ }
+
+ /// 解析 RECORD_LAYOUT 块,记录 FNC_VALUES 的元素数据类型,供 CHARACTERISTIC 推导长度
+ private static void ParseRecordLayout(List t, int start, int end,
+ Dictionary recordLayouts)
+ {
+ if (start >= end) return;
+
+ string layoutName = t[start];
+ for (int i = start + 1; i + 3 < end; i++)
+ {
+ if (IsKeyword(t[i], "FNC_VALUES"))
+ {
+ // FNC_VALUES
+ recordLayouts[layoutName] = t[i + 2].ToUpperInvariant();
+ return;
+ }
+ }
+ }
+
+ /// 标定量长度:VALUE 取元素大小;CURVE/MAP/VAL_BLK 的维度信息在 AXIS_DESCR 中,这里按单元素估算
+ private static byte GetCharacteristicSize(string characteristicType, string dataType)
+ {
+ if (characteristicType == "ASCII") return 1;
+ return GetDataTypeSize(dataType);
+ }
+
+ /// A2L 数据类型 → 字节数(未知返回 0)
+ private static byte GetDataTypeSize(string dataType)
+ {
+ switch (dataType)
+ {
+ case "UBYTE":
+ case "SBYTE":
+ case "BYTE":
+ return 1;
+ case "UWORD":
+ case "SWORD":
+ case "WORD":
+ return 2;
+ case "ULONG":
+ case "SLONG":
+ case "LONG":
+ return 4;
+ case "A_UINT64":
+ case "A_INT64":
+ return 8;
+ case "FLOAT16_IEEE":
+ case "FLOAT16":
+ return 2;
+ case "FLOAT32_IEEE":
+ case "FLOAT32":
+ return 4;
+ case "FLOAT64_IEEE":
+ case "FLOAT64":
+ return 8;
+ default:
+ return 0;
+ }
+ }
+
+ /// 找到与 /begin 匹配的 /end 位置(支持同名块嵌套)
+ private static int FindBlockEnd(List tokens, int start, string block)
+ {
+ int depth = 1;
+ for (int i = start; i + 1 < tokens.Count; i++)
+ {
+ if (!IsKeyword(tokens[i], "/begin") && !IsKeyword(tokens[i], "/end")) continue;
+
+ if (!string.Equals(tokens[i + 1], block, StringComparison.OrdinalIgnoreCase)) continue;
+
+ if (IsKeyword(tokens[i], "/begin"))
+ {
+ depth++;
+ }
+ else if (--depth == 0)
+ {
+ return i;
+ }
+ }
+ return tokens.Count;
+ }
+
+ /// 词法切分:跳过 /* */ 与 // 注释,字符串字面量整体保留(首字符加标记区分)
+ private static List Tokenize(string text)
+ {
+ var tokens = new List(text.Length / 8);
+ int i = 0, n = text.Length;
+
+ while (i < n)
+ {
+ char c = text[i];
+
+ if (char.IsWhiteSpace(c)) { i++; continue; }
+
+ if (c == '/' && i + 1 < n && text[i + 1] == '*')
+ {
+ int commentEnd = text.IndexOf("*/", i + 2, StringComparison.Ordinal);
+ i = commentEnd < 0 ? n : commentEnd + 2;
+ continue;
+ }
+
+ if (c == '/' && i + 1 < n && text[i + 1] == '/')
+ {
+ int lineEnd = text.IndexOf('\n', i + 2);
+ i = lineEnd < 0 ? n : lineEnd + 1;
+ continue;
+ }
+
+ if (c == '"')
+ {
+ var sb = new StringBuilder();
+ i++;
+ while (i < n)
+ {
+ if (text[i] == '\\' && i + 1 < n) { sb.Append(text[i + 1]); i += 2; continue; }
+ if (text[i] == '"') { i++; break; }
+ sb.Append(text[i]);
+ i++;
+ }
+ tokens.Add(StringMark + sb.ToString());
+ continue;
+ }
+
+ int tokenStart = i;
+ while (i < n && !char.IsWhiteSpace(text[i]) && text[i] != '"'
+ && !(text[i] == '/' && i + 1 < n && (text[i + 1] == '*' || text[i + 1] == '/')))
+ {
+ i++;
+ }
+
+ if (i > tokenStart) tokens.Add(text.Substring(tokenStart, i - tokenStart));
+ }
+
+ return tokens;
+ }
+
+ private static bool IsKeyword(string token, string keyword)
+ => string.Equals(token, keyword, StringComparison.OrdinalIgnoreCase);
+
+ private static bool IsString(string token) => token.Length > 0 && token[0] == StringMark;
+
+ private static string Unquote(string token)
+ => IsString(token) ? token[1..].Trim() : string.Empty;
+
+ /// A2L 中的地址一律按 hex 解析(允许 0x 前缀)
+ private static uint ParseHexUInt32(string token)
+ {
+ if (string.IsNullOrEmpty(token)) return 0;
+
+ var text = token.Trim();
+ if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) text = text[2..];
+
+ return uint.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value) ? value : 0;
+ }
+
+ /// 按 BOM / UTF-8 严格校验读取文本,非 UTF-8(GBK 等)退化为 Latin1,保证 ASCII 关键字可解析
+ private static string ReadAllTextSmart(string filePath)
+ {
+ var bytes = File.ReadAllBytes(filePath);
+
+ if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
+ return Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3);
+ if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE)
+ return Encoding.Unicode.GetString(bytes, 2, bytes.Length - 2);
+ if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF)
+ return Encoding.BigEndianUnicode.GetString(bytes, 2, bytes.Length - 2);
+
+ try
+ {
+ return new UTF8Encoding(false, true).GetString(bytes);
+ }
+ catch (DecoderFallbackException)
+ {
+ return Encoding.Latin1.GetString(bytes);
+ }
+ }
+ }
+}