CAN修复

This commit is contained in:
“hsc”
2026-10-26 10:46:44 +08:00
parent e877dc9a90
commit 9cb139b208
12 changed files with 1462 additions and 167 deletions
+4
View File
@@ -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)
+59 -8
View File
@@ -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,17 +344,44 @@ 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))
{
ShowMessage("发送参数无效:请检查发送周期、信号值和发送通道。");
return;
}
channel = c;
var message = SelectedMessage.Split(']')[1];
var re = CAN.SetSignalValue(channel, message, SelectedSignal, SValue, IsDirectlySend, sendPeriod);
}
else
{
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);
}
}
/// <summary>统一的提示弹窗</summary>
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();
}
/// <summary>重新下发配置中保存的 DBC(驱动重启后调用)</summary>
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()
{
+3 -2
View File
@@ -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();
}
+2 -1
View File
@@ -173,7 +173,8 @@ namespace MonitorModule.ViewModels
public void Dispose()
{
_scope?.Dispose();
// _scope 来自 GlobalInfo.ScopeDic,拥有者是 AutomatedTestingViewModel,
// 借用方不能 Dispose,否则关闭查询页会销毁整个工位并让 CAN 引用计数提前归零
}
#region 筛选条件加载
+545 -30
View File
@@ -22,8 +22,28 @@ namespace TSMasterCAN
public static event Action DisConnectEvent;
public static bool ConnectFlag { get; set; } = false;
/// <summary>初始化互斥锁:TSMaster 原生库每个进程只允许初始化一次</summary>
private static readonly object _initLock = new object();
/// <summary>初始化结果缓存(0 表示已成功初始化,无需再次初始化)</summary>
private static int _initResult = int.MinValue;
/// <summary>
/// 初始化CAN驱动
/// 驱动是否刚被释放、需要重新下发 DBC。
/// <para>
/// Release()/UnLoadDBC 成功后置 true,任一次 DBC 加载成功后置 false;
/// 上层(如 CANModule 点「初始化驱动」后)据此决定是否重载 DBC,
/// 避免拿残留的托管缓存去匹配已经清空的原生数据库(返回 93)。
/// </para>
/// </summary>
public static bool NeedReloadDbc { get; private set; }
/// <summary>
/// 初始化CAN驱动。
/// <para>
/// TSMaster 原生库每个进程只允许初始化一次:多工位(一拖三)各自调用时,
/// 重复 initialize_lib_tsmaster 会重建原生引擎,正在运行的循环发送表与回调队列随之失效,
/// 下一次发报文即触发原生访问冲突闪退。因此这里做进程级一次性保护,后续调用直接复用首次结果。
/// </para>
/// </summary>
/// <param name="ProjectName"></param>
/// <param name="filePath"></param>
@@ -31,47 +51,265 @@ 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;
}
}
/// <summary>
/// 释放CAN驱动
/// 释放CAN驱动。
/// <para>释放后清除初始化缓存与回调注册状态,允许下一次 Init 重新初始化。</para>
/// </summary>
[Browsable(false)]
public static void Release()
{
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 一拖多引用计数
/// <summary>作用域名缺失时的兜底键</summary>
private const string DefaultScopeKey = "__default__";
/// <summary>
/// CAN 卡的作用域引用表(作用域名 → 引用计数)。
/// <para>
/// CAN 卡是一拖多共享硬件:多个工位共用同一个 TSMaster 实例,
/// 必须等最后一个工位销毁后才能停止循环报文、断开总线并释放原生驱动。
/// </para>
/// </summary>
private static readonly ConcurrentDictionary<string, int> _scopeRefs =
new ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
/// <summary>当前仍持有 CAN 卡引用的作用域数量。</summary>
public static int ScopeRefCount => _scopeRefs.Count;
private static string NormalizeScopeKey(string? scopeName)
=> string.IsNullOrWhiteSpace(scopeName) ? DefaultScopeKey : scopeName!;
/// <summary>
/// 登记一个作用域对 CAN 卡的引用,需与 <see cref="ReleaseScope"/> 成对调用。
/// </summary>
/// <param name="scopeName">作用域名(通常为 SystemConfig.Title)</param>
public static void AcquireScope(string? scopeName)
{
var key = NormalizeScopeKey(scopeName);
_scopeRefs.AddOrUpdate(key, 1, (_, count) => count + 1);
Debug.WriteLine($"CAN.AcquireScope:{key},当前引用数 {_scopeRefs.Count}");
}
/// <summary>
/// 注销一个作用域对 CAN 卡的引用;最后一个引用移除时停止循环报文、断开总线并释放驱动。
/// </summary>
/// <param name="scopeName">作用域名(通常为 SystemConfig.Title)</param>
/// <returns>剩余引用数,0 表示已停止 CAN 卡</returns>
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<string, int>(key, count)))
{
Debug.WriteLine($"CAN.ReleaseScope:{key},剩余引用数 {_scopeRefs.Count}");
if (!_scopeRefs.IsEmpty) return _scopeRefs.Count;
// 最后一个作用域已销毁:先停循环报文,再断开总线,最后释放原生驱动
DeleteCyclicMsgs();
DisConnect();
Release();
return 0;
}
}
}
#endregion
/// <summary>订阅者增删锁(仅保护列表本身,分发时不持锁,避免与原生回调线程死锁)</summary>
private static readonly object _listenerLock = new object();
/// <summary>
/// 发送类原生 API 串行锁:多工位共用同一个 TSMaster 实例,
/// 并发进入 tsapp_add_cyclic_msg_canfd / tsapp_transmit_canfd_async / tsapp_delete_cyclic_msgs 会破坏原生状态。
/// </summary>
private static readonly object _nativeSendLock = new object();
/// <summary>各工位注册的回调(在托管侧保根,原生库只持有唯一的 _dispatcher 函数指针)</summary>
private static readonly List<TCANFDQueueEvent_Win32> _listeners = new List<TCANFDQueueEvent_Win32>();
/// <summary>
/// 唯一注册到 TSMaster 的原生回调委托。
/// <para>
/// 必须由静态字段长期持有:原生库只保存函数指针,托管委托一旦被 GC 回收,
/// 原生线程回调到已释放的 thunk 就会直接访问冲突闪退(.NET 无法捕获)。
/// 多工位场景下原先每个工位各自覆盖 listener 静态字段,前一工位的委托随即失去根引用,
/// 这正是开两个工位发报文必闪退的原因。
/// </para>
/// </summary>
private static TCANFDQueueEvent_Win32? _dispatcher;
/// <summary>原生回调是否已注册(保证全进程只注册一次,避免同一个 obj 重复注册破坏 TSMaster 内部回调表)</summary>
private static bool _dispatcherRegistered = false;
/// <summary>
/// 注册 CANFD 报文回调(多工位安全)。
/// <para>首个订阅者触发原生注册,其余订阅者仅加入托管侧分发列表,从而避免重复注册与委托被回收。</para>
/// </summary>
[Browsable(false)]
public static int RegisterListener(TCANFDQueueEvent_Win32 listenEvent)
{
listener = listenEvent;
var re = TsMasterApi.tsapp_register_event_canfd(ref obj, listener);
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;
}
}
/// <summary>
/// 注销 CANFD 报文回调。
/// <para>指定委托时只移除该订阅者;不指定时移除全部订阅者。所有订阅者移除后才反注册原生回调。</para>
/// </summary>
[Browsable(false)]
public static int UnRegisterListener(TCANFDQueueEvent_Win32 listenEvent = null)
public static int UnRegisterListener(TCANFDQueueEvent_Win32? listenEvent = null)
{
try
{
lock (_listenerLock)
{
if (listenEvent is not null)
{
listener = listenEvent;
_listeners.Remove(listenEvent);
}
var re = TsMasterApi.tsapp_unregister_event_canfd(ref obj, listener);
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;
}
}
/// <summary>清除回调注册状态(调用方需自行持有 <see cref="_listenerLock"/>)</summary>
private static void ResetListenerRegistrationCore()
{
_dispatcherRegistered = false;
_dispatcher = null;
}
/// <summary>清除回调注册状态与全部订阅者</summary>
private static void ResetListenerRegistration()
{
lock (_listenerLock)
{
_listeners.Clear();
ResetListenerRegistrationCore();
}
}
/// <summary>
/// 原生 CANFD 回调统一入口:向所有已注册工位分发同一帧数据。
/// <para>单个订阅者异常必须就地隔离,异常一旦跨出原生边界即进程闪退。</para>
/// </summary>
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}");
}
}
}
/// <summary>
/// 连接 TSMaster CAN 总线:建立总线连接并启动 RBS 仿真系统,
@@ -80,6 +318,12 @@ namespace TSMasterCAN
/// <returns>返回代码,0 表示成功,非 0 表示失败(可用 <see cref="GetErrorDescription"/> 查询错误说明)</returns>
public static int Connect()
{
try
{
lock (_nativeSendLock)
{
// 多工位共用同一张 CAN 卡:任一工位已连接即复用,避免重复 tsapp_connect
if (ConnectFlag) return 0;
var re = TsMasterApi.tsapp_connect();
if (re == 0)
@@ -88,7 +332,11 @@ namespace TSMasterCAN
if (re == 0)
{
ConnectFlag = true;
Task.Run(() => ConnectEvent?.Invoke());
Task.Run(() =>
{
try { ConnectEvent?.Invoke(); }
catch (Exception ex) { Debug.WriteLine($"CAN.ConnectEvent 订阅者异常:{ex.Message}"); }
});
}
else
{
@@ -102,6 +350,13 @@ namespace TSMasterCAN
}
return re;
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.Connect 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 断开 TSMaster CAN 总线连接,成功后清除连接标志并触发 <see cref="DisConnectEvent"/> 断开事件。
@@ -109,21 +364,37 @@ namespace TSMasterCAN
/// <returns>返回代码,0 表示成功,非 0 表示失败(可用 <see cref="GetErrorDescription"/> 查询错误说明)</returns>
public static int DisConnect()
{
try
{
lock (_nativeSendLock)
{
// 已断开则直接返回:多工位场景下避免后续工位重复断开总线
if (!ConnectFlag) return 0;
var re = TsMasterApi.tsapp_disconnect();
//Debug.Assert(re == 0);
if (re == 0)
{
ConnectFlag = false;
Task.Run(() => DisConnectEvent?.Invoke());
Task.Run(() =>
{
try { DisConnectEvent?.Invoke(); }
catch (Exception ex) { Debug.WriteLine($"CAN.DisConnectEvent 订阅者异常:{ex.Message}"); }
});
}
else
{
Debug.WriteLine($"断开CAN连接失败,错误代码:{re}");
}
return re;
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.DisConnect 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 加载DBC
@@ -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);
int re;
try
{
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;
}
/// <summary>
/// 已加载过的 DBC 记录(键 = 通道集合 + 绝对路径,值 = TSMaster 数据库 ID)。
/// <para>
/// 一拖多场景下多个工位共用同一张 CAN 卡:DBC 只需下发并解析一次,
/// 重复加载会让 DBCParse 把同一批报文重复追加进 MsgDatabase。
/// </para>
/// </summary>
private static readonly ConcurrentDictionary<string, uint> _loadedDbcPool =
new ConcurrentDictionary<string, uint>(StringComparer.OrdinalIgnoreCase);
private static string BuildDbcKey(string filePath, int[] channels)
=> $"{string.Join(",", channels.OrderBy(c => c))}|{Path.GetFullPath(filePath)}";
/// <summary>
/// 查询指定通道上的 DBC 是否已由其他工位加载过。
/// </summary>
/// <param name="filePath">DBC 文件路径</param>
/// <param name="channel">通道号数组</param>
[Browsable(false)]
public static bool IsDbcLoaded(string filePath, int[] channel)
{
if (!Path.Exists(filePath)) return false;
return _loadedDbcPool.ContainsKey(BuildDbcKey(filePath, channel));
}
/// <summary>
/// 加载DBC(同一通道 + 同一文件只加载一次)。
/// <para>
/// 供多工位自动加载使用:其他工位已加载过同一 DBC 时直接返回 0,
/// 不重复调用 tsdb_load_can_db,也不重复追加 DBCParse.MsgDatabase。
/// </para>
/// </summary>
/// <param name="filePath">DBC 文件路径</param>
/// <param name="channel">通道号数组</param>
/// <param name="databaseID">首次加载返回的数据库 ID;已加载过时返回首次的 ID</param>
/// <returns>返回代码,0 表示成功(含已加载跳过)</returns>
[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;
}
/// <summary>清空 DBC 加载记录(卸载 DBC 或释放驱动时调用)。</summary>
[Browsable(false)]
public static void ClearDbcLoadRecords() => _loadedDbcPool.Clear();
/// <summary>
/// 加载DBC至单一通道
/// </summary>
@@ -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);
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)
{
try
{
int re;
if (databaseID == null)
{
return TsMasterApi.tsdb_unload_can_dbs();
re = TsMasterApi.tsdb_unload_can_dbs();
}
else
{
return TsMasterApi.tsdb_unload_can_db(databaseID.Value);
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;
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.UnLoadDBC 异常:{ex.Message}");
return -1;
}
}
@@ -186,18 +582,34 @@ namespace TSMasterCAN
/// <param name="filePath">日志文件保存路径(自动转换为绝对路径)</param>
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
public static int StartLogging(string filePath)
{
try
{
return TsMasterApi.tsapp_start_logging(Path.GetFullPath(filePath));
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.StartLogging 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 结束总线日志记录。
/// </summary>
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
public static int StopLogging()
{
try
{
return TsMasterApi.tsapp_stop_logging();
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.StopLogging 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 弹出通道映射窗口
@@ -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,9 +657,18 @@ 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);
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;
}
}
/// <summary>
/// 获取信号值
@@ -269,13 +698,26 @@ namespace TSMasterCAN
/// <exception cref="Exception">信号编码写入报文失败时抛出</exception>
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)
{
try
{
// 多工位共用同一个 TSMaster 实例,发送类原生 API 必须串行化,
// 并发调用会破坏原生内部循环发送表,表现为发报文瞬间闪退。
lock (_nativeSendLock)
{
if (sendPeriod == 0)
{
@@ -286,6 +728,13 @@ namespace TSMasterCAN
return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.SetSignalValue 发送异常:{ex.Message}");
return -1;
}
}
return 0;
}
@@ -299,8 +748,13 @@ namespace TSMasterCAN
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
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;
try
{
lock (_nativeSendLock)
{
if (sendPeriod == 0)
{
return TsMasterApi.tsapp_transmit_canfd_async(ref find.ACANFD);
@@ -310,6 +764,13 @@ namespace TSMasterCAN
return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.SetSignalValue 发送异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 发送自定义报文
@@ -317,9 +778,20 @@ namespace TSMasterCAN
/// <returns></returns>
[Browsable(false)]
public static int SetMsg(TLIBCANFD msg)
{
try
{
lock (_nativeSendLock)
{
return TsMasterApi.tsapp_transmit_canfd_async(ref msg);
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.SetMsg 发送异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 按帧 ID 更新 DBC 数据库中指定报文的数据区(仅更新内存中的数据,不发送)。
@@ -391,6 +863,10 @@ namespace TSMasterCAN
int length = Math.Min(ADataArray.Length, 64);
Array.Copy(ADataArray, 0, send.FData, 0, length);
int re;
try
{
lock (_nativeSendLock)
{
if (sendPeriod == 0)
{
re = TsMasterApi.tsapp_transmit_canfd_async(ref send);
@@ -399,8 +875,15 @@ namespace TSMasterCAN
{
re = TsMasterApi.tsapp_add_cyclic_msg_canfd(ref send, sendPeriod);
}
}
}
catch (Exception ex)
{
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,19 +903,43 @@ namespace TSMasterCAN
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
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;
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;
}
}
/// <summary>
/// 清除所有循环发送的报文,停止全部周期发送任务。
/// <para>注意:TSMaster 的循环发送表是全局的,此调用会同时清掉其他工位的循环发送。</para>
/// </summary>
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
public static int DeleteCyclicMsgs()
{
try
{
lock (_nativeSendLock)
{
return TsMasterApi.tsapp_delete_cyclic_msgs();
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.DeleteCyclicMsgs 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 根据错误代码获取对应的错误文字描述,用于诊断 TSMaster API 返回码。
@@ -440,6 +947,8 @@ namespace TSMasterCAN
/// <param name="errorCode">TSMaster API 返回的错误代码</param>
/// <returns>错误描述文本,未知错误代码时返回提示信息</returns>
public static string GetErrorDescription(int errorCode)
{
try
{
IntPtr ADesc = IntPtr.Zero;
TsMasterApi.tsapp_get_error_description(errorCode, ref ADesc);
@@ -451,6 +960,12 @@ namespace TSMasterCAN
return description ?? $"未知错误代码: {errorCode}";
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.GetErrorDescription 异常:{ex.Message}");
return $"未知错误代码: {errorCode}";
}
}
}
}
+13
View File
@@ -125,6 +125,19 @@ namespace TSMasterCAN
public static List<List<_Msg_>> MsgDatabase { get; set; } =
[.. Enumerable.Range(0, 12).Select(_ => new List<_Msg_>())];
public static can_network can_Network;
/// <summary>
/// 清空托管侧报文缓存。
/// <para>
/// TSMaster 原生数据库被卸载(tsdb_unload_can_dbs)或驱动被 finalize 后,
/// 这里的缓存必须同步清空,否则再次发送时会拿缓存里的报文去匹配空数据库,
/// tsdb_set_signal_value_canfd 会返回 93(message not found in database)。
/// </para>
/// </summary>
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();
@@ -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
/// <summary>TSMaster 总线是否已连接(tsapp_connect + rbs_start 成功)</summary>
public bool IsBusConnected { get => _isBusConnected; set => SetProperty(ref _isBusConnected, value); }
// ---------- A2L 描述文件 ----------
private string _a2lFilePath = string.Empty;
/// <summary>A2L 文件路径</summary>
public string A2lFilePath { get => _a2lFilePath; set => SetProperty(ref _a2lFilePath, value); }
private string _a2lSummaryText = "未加载 A2L 文件";
/// <summary>A2L 解析结果摘要</summary>
public string A2lSummaryText { get => _a2lSummaryText; set => SetProperty(ref _a2lSummaryText, value); }
/// <summary>解析出的测量量列表(选中后自动填入 DAQ 地址与长度)</summary>
public ObservableCollection<A2lMeasurement> A2lMeasurements { get; } = new ObservableCollection<A2lMeasurement>();
/// <summary>解析出的标定量列表(选中后自动填入内存读写地址与长度)</summary>
public ObservableCollection<A2lCharacteristic> A2lCharacteristics { get; } = new ObservableCollection<A2lCharacteristic>();
private A2lMeasurement? _selectedMeasurement;
/// <summary>当前选中的测量量:自动带入 DAQ 的 ODT 地址与长度</summary>
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;
/// <summary>当前选中的标定量:自动带入内存读写的地址与长度</summary>
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 命令
/// <summary>浏览并加载 A2L 描述文件</summary>
public DelegateCommand BrowseA2lCommand { get; }
/// <summary>按当前路径重新解析 A2L 描述文件</summary>
public DelegateCommand ReloadA2lCommand { get; }
/// <summary>初始化 TSMaster 驱动(可选加载工程)</summary>
public DelegateCommand InitDriverCommand { get; }
/// <summary>释放 TSMaster 驱动</summary>
@@ -170,6 +223,8 @@ namespace TSMasterXCPModule.ViewModels
/// <summary>创建视图模型</summary>
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 描述文件
/// <summary>浏览选择 A2L 文件并解析</summary>
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);
}
/// <summary>按当前路径重新解析 A2L</summary>
private void ReloadA2l()
{
var path = A2lFilePath?.Trim() ?? string.Empty;
if (path.Length == 0)
{
Log("[提示] 请先选择 A2L 文件(点击「浏览...」)。");
return;
}
LoadA2l(path);
}
/// <summary>解析并刷新测量量 / 标定量列表</summary>
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()
@@ -122,6 +122,98 @@
</StackPanel>
</GroupBox>
<!-- ============ A2L 描述文件 ============ -->
<GroupBox Header="A2L 描述文件(测量量 / 标定量)" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="A2L文件" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="460"
ToolTip="ECU 描述文件(.a2l)。加载后可从下方列表选择测量量/标定量,自动带入地址与长度"
Text="{Binding A2lFilePath, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="浏览..." Command="{Binding BrowseA2lCommand}"
Style="{StaticResource CmdBtn}"
Background="#1565C0" Foreground="White"
ToolTip="选择 A2L 文件并解析" />
<Button Content="重新解析" Command="{Binding ReloadA2lCommand}"
Style="{StaticResource CmdBtn}"
ToolTip="按当前路径重新解析(A2L 被工具重新生成后使用)" />
<TextBlock Text="{Binding A2lSummaryText}"
VerticalAlignment="Center" Margin="12,0,0,0"
FontSize="12" Foreground="#616161"
TextTrimming="CharacterEllipsis" MaxWidth="360" />
</StackPanel>
<Grid Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
<TextBlock DockPanel.Dock="Top" Text="测量量 MEASUREMENT → DAQ 地址/长度"
FontSize="12" FontWeight="SemiBold"
Foreground="#1565C0" Margin="2,0,0,4" />
<ListBox Height="180"
ItemsSource="{Binding A2lMeasurements}"
SelectedItem="{Binding SelectedMeasurement}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ToolTip="{Binding Detail}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" FontSize="12"
TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="1" Text="{Binding AddressText}"
FontFamily="Consolas" FontSize="11"
Foreground="#616161" Margin="8,0,0,0" />
<TextBlock Grid.Column="2" Text="{Binding SizeText}"
FontSize="11" Foreground="#9E9E9E"
Margin="8,0,0,0" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<DockPanel Grid.Column="2">
<TextBlock DockPanel.Dock="Top" Text="标定量 CHARACTERISTIC → 内存读写"
FontSize="12" FontWeight="SemiBold"
Foreground="#1565C0" Margin="2,0,0,4" />
<ListBox Height="180"
ItemsSource="{Binding A2lCharacteristics}"
SelectedItem="{Binding SelectedCharacteristic}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ToolTip="{Binding Detail}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" FontSize="12"
TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="1" Text="{Binding CharacteristicType}"
FontSize="11" Foreground="#9E9E9E"
Margin="8,0,0,0" />
<TextBlock Grid.Column="2" Text="{Binding AddressText}"
FontFamily="Consolas" FontSize="11"
Foreground="#616161" Margin="8,0,0,0" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Grid>
</StackPanel>
</GroupBox>
<!-- ============ XCP 连接配置 ============ -->
<GroupBox Header="XCP 连接配置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
+40 -12
View File
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks;
using TSMasterCAN;
using TSMaster;
using Logger;
namespace UIShare.GlobalVariable
{
public class CANMonitoringService
@@ -23,29 +24,43 @@ 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);
LoggerHelper.Warn($"CAN 驱动初始化返回 {initRe}({CAN.GetErrorDescription(initRe)}),继续注册报文回调。");
}
if (IsInitialized == false)
{
re = CAN.RegisterListener(MonitorEvent);
// RegisterListener 幂等:重复调用只补充订阅者;驱动被 Release 后再 Init 也能重新挂回原生回调
var re = CAN.RegisterListener(MonitorEvent);
if (re == 0) IsInitialized = true;
}
return re;
}
public ConcurrentDictionary<string, TLIBCANFD> RealTimeMessages { get; set; } = new();
public ConcurrentDictionary<string, double> RealTimeSignals { get; set; } = new();
/// <summary>
/// TSMaster 原生库收报文回调(原生线程直接调用)。
/// 回调内抛出的异常一旦跨出原生边界会直接导致进程闪退,
/// 因此整个方法体必须包裹异常隔离,且不做任何可能阻塞的调用。
/// </summary>
public void MonitorEvent(ref int AObj, ref TLIBCANFD AData)
{
try
{
if (IsStopped) return;
// 通道越界防护:多通道/异常帧的 FIdxChn 可能超出 DBC 数据库(12 通道)范围
if (AData.FIdxChn >= DBCParse.MsgDatabase.Count) return;
if (AData.FData == null) return;
byte[] oldbytes = null;
//记录至实时报文数据库
var cpAData = AData;
cpAData.FData = ArrayPool<byte>.Shared.Rent(64);
Array.Copy(AData.FData, 0, cpAData.FData, 0, cpAData.FData.Length);
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))
{
@@ -57,7 +72,7 @@ namespace UIShare.GlobalVariable
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);
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;
@@ -72,11 +87,16 @@ namespace UIShare.GlobalVariable
var id = AData.FIdentifier;
var ch = AData.FIdxChn;
var canSignalList = _systemConfig.ConfigurationList.Where(s => s.MessageID == id && s.Channel == ch).ToArray();
var configList = _systemConfig.ConfigurationList;
if (configList == null) return;
var canSignalList = configList.Where(s => s.MessageID == id && s.Channel == ch).ToArray();
if (canSignalList.Length > 0)
{
var find = DBCParse.MsgDatabase[ch].First(s => s.msg_id == id);
// 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;
@@ -87,15 +107,23 @@ namespace UIShare.GlobalVariable
}
}
}
catch (Exception ex)
{
// 原生回调内绝不能向外抛异常:跨原生边界即进程闪退。
LoggerHelper.Error($"CANMonitoringService.MonitorEvent 回调异常(已隔离):{ex.Message}");
}
}
/// <summary>
/// 停止监测:从广播器注销并清理实时数据。
/// 停止监测:注销原生报文回调、从广播器注销并清理实时数据。
/// <para>必须先反注册回调再释放实例,否则原生线程仍会回调到已停止的工位。</para>
/// </summary>
public void Stop()
{
IsStopped = true;
if (IsRegistered)
{
CAN.UnRegisterListener(MonitorEvent);
CANSignalBroadcaster.UnregisterService(this);
IsRegistered = false;
}
@@ -150,6 +150,9 @@ namespace UIShare.GlobalVariable
string methodName = BuildMethodName((uint)cfg.MessageID, signalName);
// 广播信号值到对应作用域
// 逐信号隔离:单个信号广播/报警失败不应中断整个广播循环
try
{
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Publish(new HardwareReportArgs
{
Scope = scopeName,
@@ -166,6 +169,11 @@ namespace UIShare.GlobalVariable
_eventAggregator.GetEvent<AlarmEvent>().Publish((scopeName, canFingerprint, alarmStatus));
}
}
catch (Exception ex)
{
Logger.LoggerHelper.Error($"CANSignalBroadcaster 广播信号异常(作用域 {scopeName} {methodName},已隔离):{ex.Message}");
}
}
}
}
+80 -6
View File
@@ -48,10 +48,39 @@ namespace UIShare.GlobalVariable
_CANMonitoringService=new CANMonitoringService(_systemConfig);
// 用 SystemConfig.Title 作为作用域唯一标识,无需反查 ConfigDic
_scopeName = _systemConfig.Title;
AcquireCanScope();
InitDevices();
InitCAN();
}
/// <summary>本工位(作用域)是否已登记 CAN 卡引用,保证 Acquire/Release 成对且幂等</summary>
private bool _canScopeAcquired;
/// <summary>
/// 登记本工位对 CAN 卡的引用。
/// <para>CAN 卡是一拖多共享硬件,与 HardwarePool 中的设备一样需要按作用域引用计数。</para>
/// </summary>
private void AcquireCanScope()
{
if (_canScopeAcquired) return;
CAN.AcquireScope(_scopeName);
_canScopeAcquired = true;
}
/// <summary>
/// 注销本工位对 CAN 卡的引用。
/// <para>
/// 只有最后一个工位释放时才真正停止循环报文、断开总线并释放 TSMaster 驱动;
/// 仍有其他工位在用时只做引用减一,避免误伤其他工位。
/// </para>
/// </summary>
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<Task> tasks = new List<Task>();
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)
{
if (alreadyLoaded)
{
LoggerHelper.Info($"CAN 通道 {item.DBCChannel} DBC 已由其他工位加载,跳过重复加载:{item.DBCFilePath}");
}
else
{
LoggerHelper.Info($"CAN 通道 {item.DBCChannel} 已自动加载 DBC:{item.DBCFilePath}");
// 发布 DBC 加载完成事件,通知监控系统刷新信号列表
}
// 即使跳过加载,也要通知本工位监控系统刷新信号列表
_eventAggregator.GetEvent<DBCLoadedEvent>().Publish(new DBCLoadedArgs
{
Channel = (uint)item.DBCChannel,
@@ -519,11 +579,22 @@ namespace UIShare.GlobalVariable
}
/// <summary>
/// 关闭 CAN 卡
/// 关闭 CAN 卡:按作用域引用计数释放。
/// <para>
/// CAN 卡为一拖多共享硬件,这里只注销本工位的引用;
/// 仅当最后一个工位释放时,<see cref="CAN.ReleaseScope"/> 才停止循环报文、断开总线并释放驱动。
/// </para>
/// </summary>
private void CloseCan()
{
CAN.DisConnect();
try
{
ReleaseCanScope();
}
catch (Exception ex)
{
LoggerHelper.Error($"关闭 CAN 卡异常(已隔离):{ex.Message}");
}
}
private static IReadOnlyDictionary<string, Type> BuildDeviceTypeMap()
@@ -612,6 +683,9 @@ namespace UIShare.GlobalVariable
// 停止 CAN 信号监测服务
_CANMonitoringService?.Stop();
// CAN 卡按作用域引用计数释放:最后一个工位销毁时才停循环报文、断总线并释放驱动
ReleaseCanScope();
if (string.IsNullOrEmpty(_scopeName)) return;
// 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用
+379
View File
@@ -0,0 +1,379 @@
using System.Globalization;
using System.Text;
namespace XCPCore
{
/// <summary>
/// A2L 中的测量量(MEASUREMENT)条目
/// </summary>
public sealed class A2lMeasurement
{
/// <summary>测量量名称</summary>
public string Name { get; init; } = string.Empty;
/// <summary>测量量描述(A2L 中的 LongIdentifier)</summary>
public string Description { get; init; } = string.Empty;
/// <summary>A2L 数据类型(UBYTE / UWORD / FLOAT32_IEEE 等)</summary>
public string DataType { get; init; } = string.Empty;
/// <summary>ECU 内存地址</summary>
public uint Address { get; init; }
/// <summary>占用字节数(由数据类型推导)</summary>
public byte SizeBytes { get; init; }
/// <summary>地址的 hex 文本</summary>
public string AddressText => $"0x{Address:X8}";
/// <summary>长度的显示文本</summary>
public string SizeText => SizeBytes > 0 ? $"{SizeBytes}B" : "?";
/// <summary>列表 ToolTip 使用的完整描述</summary>
public string Detail => $"{Name} {AddressText} {DataType} {SizeText}\n{Description}".TrimEnd();
}
/// <summary>
/// A2L 中的标定量(CHARACTERISTIC)条目
/// </summary>
public sealed class A2lCharacteristic
{
/// <summary>标定量名称</summary>
public string Name { get; init; } = string.Empty;
/// <summary>标定量描述(A2L 中的 LongIdentifier)</summary>
public string Description { get; init; } = string.Empty;
/// <summary>标定量结构类型(VALUE / CURVE / MAP / VAL_BLK / ASCII 等)</summary>
public string CharacteristicType { get; init; } = string.Empty;
/// <summary>A2L 数据类型(由 RECORD_LAYOUT 的 FNC_VALUES 推导,未知时为空)</summary>
public string DataType { get; init; } = string.Empty;
/// <summary>ECU 内存地址</summary>
public uint Address { get; init; }
/// <summary>单元素字节数(未知时为 0)</summary>
public byte SizeBytes { get; init; }
/// <summary>地址的 hex 文本</summary>
public string AddressText => $"0x{Address:X8}";
/// <summary>长度的显示文本</summary>
public string SizeText => SizeBytes > 0 ? $"{SizeBytes}B" : "?";
/// <summary>列表 ToolTip 使用的完整描述</summary>
public string Detail => $"{Name} {AddressText} {CharacteristicType} {DataType} {SizeText}\n{Description}".TrimEnd();
}
/// <summary>
/// A2L(ASAM MCD-2 MC)描述文件解析结果
/// </summary>
public sealed class A2lDatabase
{
/// <summary>来源文件全路径</summary>
public string FilePath { get; set; } = string.Empty;
/// <summary>PROJECT 名称(A2L 中可能缺省)</summary>
public string ProjectName { get; set; } = string.Empty;
/// <summary>解析出的测量量列表</summary>
public List<A2lMeasurement> Measurements { get; } = new List<A2lMeasurement>();
/// <summary>解析出的标定量列表</summary>
public List<A2lCharacteristic> Characteristics { get; } = new List<A2lCharacteristic>();
/// <summary>解析结果摘要</summary>
public string Summary => $"MEASUREMENT {Measurements.Count} 项 · CHARACTERISTIC {Characteristics.Count} 项";
}
/// <summary>
/// 轻量 A2L 解析器:只提取 XCP 采集/标定所需的测量量、标定量及其地址与长度。
/// <para>
/// 支持块:MEASUREMENT、CHARACTERISTIC、RECORD_LAYOUT、PROJECT;其余块整体跳过。
/// 采用词法切分而非正则,能正确处理块内嵌套的 IF_DATA / AXIS_DESCR 等子块与 /* */ 注释。
/// </para>
/// </summary>
public static class A2lParser
{
private const char StringMark = '\u0001';
/// <summary>
/// 读取并解析 A2L 文件
/// </summary>
/// <param name="filePath">A2L 文件路径</param>
/// <returns>解析结果(无法识别的块被忽略,不抛异常)</returns>
public static A2lDatabase LoadFile(string filePath)
{
var db = Parse(ReadAllTextSmart(filePath));
db.FilePath = filePath;
return db;
}
/// <summary>
/// 解析 A2L 文本内容
/// </summary>
/// <param name="text">A2L 文件内容</param>
/// <returns>解析结果(无法识别的块被忽略,不抛异常)</returns>
public static A2lDatabase Parse(string text)
{
var db = new A2lDatabase();
if (string.IsNullOrWhiteSpace(text)) return db;
var recordLayouts = new Dictionary<string, string>(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 <Name> <LongIdentifier>
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;
}
/// <summary>解析 MEASUREMENT 块:/begin MEASUREMENT Name Desc DataType Conv Resolution Accuracy Lower Upper ... ECU_ADDRESS x</summary>
private static void ParseMeasurement(List<string> 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)
});
}
/// <summary>解析 CHARACTERISTIC 块:/begin CHARACTERISTIC Name Desc Type Address Deposit MaxDiff Conversion Lower Upper ...</summary>
private static void ParseCharacteristic(List<string> t, int start, int end,
Dictionary<string, string> 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)
});
}
/// <summary>解析 RECORD_LAYOUT 块,记录 FNC_VALUES 的元素数据类型,供 CHARACTERISTIC 推导长度</summary>
private static void ParseRecordLayout(List<string> t, int start, int end,
Dictionary<string, string> 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 <position> <datatype> <indexMode> <addressType>
recordLayouts[layoutName] = t[i + 2].ToUpperInvariant();
return;
}
}
}
/// <summary>标定量长度:VALUE 取元素大小;CURVE/MAP/VAL_BLK 的维度信息在 AXIS_DESCR 中,这里按单元素估算</summary>
private static byte GetCharacteristicSize(string characteristicType, string dataType)
{
if (characteristicType == "ASCII") return 1;
return GetDataTypeSize(dataType);
}
/// <summary>A2L 数据类型 → 字节数(未知返回 0)</summary>
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;
}
}
/// <summary>找到与 /begin 匹配的 /end 位置(支持同名块嵌套)</summary>
private static int FindBlockEnd(List<string> 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;
}
/// <summary>词法切分:跳过 /* */ 与 // 注释,字符串字面量整体保留(首字符加标记区分)</summary>
private static List<string> Tokenize(string text)
{
var tokens = new List<string>(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;
/// <summary>A2L 中的地址一律按 hex 解析(允许 0x 前缀)</summary>
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;
}
/// <summary>按 BOM / UTF-8 严格校验读取文本,非 UTF-8(GBK 等)退化为 Latin1,保证 ASCII 关键字可解析</summary>
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);
}
}
}
}