CAN修复
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -173,7 +173,8 @@ namespace MonitorModule.ViewModels
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_scope?.Dispose();
|
||||
// _scope 来自 GlobalInfo.ScopeDic,拥有者是 AutomatedTestingViewModel,
|
||||
// 借用方不能 Dispose,否则关闭查询页会销毁整个工位并让 CAN 引用计数提前归零
|
||||
}
|
||||
|
||||
#region 筛选条件加载
|
||||
|
||||
+604
-89
@@ -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,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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放CAN驱动
|
||||
/// 释放CAN驱动。
|
||||
/// <para>释放后清除初始化缓存与回调注册状态,允许下一次 Init 重新初始化。</para>
|
||||
/// </summary>
|
||||
[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 一拖多引用计数
|
||||
|
||||
/// <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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注销 CANFD 报文回调。
|
||||
/// <para>指定委托时只移除该订阅者;不指定时移除全部订阅者。所有订阅者移除后才反注册原生回调。</para>
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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}");
|
||||
}
|
||||
}
|
||||
var re = TsMasterApi.tsapp_unregister_event_canfd(ref obj, listener);
|
||||
Debug.Assert(re == 0);
|
||||
return re;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -80,27 +318,44 @@ 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -109,20 +364,36 @@ namespace TSMasterCAN
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败(可用 <see cref="GetErrorDescription"/> 查询错误说明)</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
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
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -196,7 +600,15 @@ namespace TSMasterCAN
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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,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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -269,21 +698,41 @@ 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)
|
||||
{
|
||||
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
|
||||
/// <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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
/// <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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有循环发送的报文,停止全部周期发送任务。
|
||||
/// <para>注意:TSMaster 的循环发送表是全局的,此调用会同时清掉其他工位的循环发送。</para>
|
||||
/// </summary>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -441,15 +948,23 @@ namespace TSMasterCAN
|
||||
/// <returns>错误描述文本,未知错误代码时返回提示信息</returns>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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<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)
|
||||
{
|
||||
if (IsStopped) 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);
|
||||
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<byte>.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<byte>.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<byte>.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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止监测:从广播器注销并清理实时数据。
|
||||
/// 停止监测:注销原生报文回调、从广播器注销并清理实时数据。
|
||||
/// <para>必须先反注册回调再释放实例,否则原生线程仍会回调到已停止的工位。</para>
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
IsStopped = true;
|
||||
if (IsRegistered)
|
||||
{
|
||||
CAN.UnRegisterListener(MonitorEvent);
|
||||
CANSignalBroadcaster.UnregisterService(this);
|
||||
IsRegistered = false;
|
||||
}
|
||||
|
||||
@@ -150,20 +150,28 @@ namespace UIShare.GlobalVariable
|
||||
string methodName = BuildMethodName((uint)cfg.MessageID, signalName);
|
||||
|
||||
// 广播信号值到对应作用域
|
||||
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Publish(new HardwareReportArgs
|
||||
// 逐信号隔离:单个信号广播/报警失败不应中断整个广播循环
|
||||
try
|
||||
{
|
||||
Scope = scopeName,
|
||||
HardwareFingerprint = fingerprint,
|
||||
MethodName = methodName,
|
||||
Value = kvp.Value,
|
||||
Time = DateTime.Now
|
||||
});
|
||||
_eventAggregator.GetEvent<HardwareDataReportedEvent>().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<AlarmEvent>().Publish((scopeName, canFingerprint, alarmStatus));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_eventAggregator.GetEvent<AlarmEvent>().Publish((scopeName, canFingerprint, alarmStatus));
|
||||
Logger.LoggerHelper.Error($"CANSignalBroadcaster 广播信号异常(作用域 {scopeName} {methodName},已隔离):{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
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<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;
|
||||
|
||||
// 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user