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 @@ + + + + + + +