Files
ACP/TSMasterCAN/CAN.cs
T
2026-10-26 10:46:44 +08:00

972 lines
39 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Common.Attributes;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using TSMaster;
namespace TSMasterCAN
{
/// <summary>
/// TSMaster CAN/CANFD 总线控制类,基于 TSMaster API 实现总线连接管理、
/// DBC 信号读写、报文发送(单次/循环)、日志记录等功能,暴露给命令树供测试步骤调用。
/// </summary>
[ACPCommand]
public class CAN
{
public static event Action ConnectEvent;
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>
/// 驱动是否刚被释放、需要重新下发 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>
/// <returns></returns>
[Browsable(false)]
public static int Init(string ProjectName, string? filePath = null)
{
try
{
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;
}
}
catch (Exception ex)
{
// 原生 API 异常统一转为返回码,不得向外传播导致进程闪退
Debug.WriteLine($"CAN.Init 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 释放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;
#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)
{
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)
{
try
{
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}");
}
}
}
/// <summary>
/// 连接 TSMaster CAN 总线:建立总线连接并启动 RBS 仿真系统,
/// 成功后置位连接标志并触发 <see cref="ConnectEvent"/> 连接事件。
/// </summary>
/// <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(() =>
{
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;
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.Connect 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 断开 TSMaster CAN 总线连接,成功后清除连接标志并触发 <see cref="DisConnectEvent"/> 断开事件。
/// </summary>
/// <returns>返回代码,0 表示成功,非 0 表示失败(可用 <see cref="GetErrorDescription"/> 查询错误说明)</returns>
public static int DisConnect()
{
try
{
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;
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.DisConnect 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 加载DBC
/// </summary>
/// <param name="filePath"></param>
/// <param name="channel"></param>
/// <param name="databaseID"></param>
/// <returns></returns>
[Browsable(false)]
public static int LoadDBC(string filePath, int[] channel, out uint databaseID)
{
databaseID = 0;
if (!Path.Exists(filePath)) return -1;
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>
/// <param name="filePath"></param>
/// <param name="channel"></param>
/// <returns></returns>
public static int LoadDBCToSingleChannel(string filePath, int channel)
{
uint databaseID = 0;
if (!Path.Exists(filePath)) return -1;
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;
}
/// <summary>
/// 卸载DBC
/// </summary>
/// <param name="databaseID"></param>
/// <returns></returns>
[Browsable(false)]
public static int UnLoadDBC(uint? databaseID = null)
{
try
{
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;
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.UnLoadDBC 异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 开始记录总线日志,将总线上的报文数据记录到指定文件。
/// </summary>
/// <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>
/// 弹出通道映射窗口
/// </summary>
/// <param name="isWait"></param>
/// <returns></returns>
[Browsable(false)]
public static int ShowChannelMappingWindow(bool isWait = false)
{
return TsMasterApi.tsapp_show_tsmaster_window("Hardware", isWait);
}
/// <summary>
/// 获取 DBC 中定义信号的当前物理值(从总线接收报文中解析)。
/// </summary>
/// <param name="channel">通道号(0 起始)</param>
/// <param name="AMsgName">DBC 中的报文名称</param>
/// <param name="ASgnName">DBC 中的信号名称</param>
/// <returns>信号物理值,获取失败时返回 <see cref="double.NaN"/></returns>
public static double GetSignalValue(byte channel, string AMsgName, string ASgnName)
{
double value = double.NaN;
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;
}
/// <summary>
/// 获取信号值
/// </summary>
/// <param name="channel"></param>
/// <param name="AMsgName"></param>
/// <param name="ASgnName"></param>
/// <param name="value"></param>
/// <returns></returns>
[Browsable(false)]
public static int GetSignalValue(byte channel, string AMsgName, string ASgnName, ref double 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>
/// 获取信号值
/// </summary>
/// <param name="ACANFD"></param>
/// <param name="AMsgName"></param>
/// <param name="ASgnName"></param>
/// <param name="value"></param>
/// <returns></returns>
[Browsable(false)]
public static int GetSignalValue(ref TLIBCANFD ACANFD, string AMsgName, string ASgnName, ref double value)
{
return TsMasterApi.tsdb_get_signal_value_canfd(ref ACANFD, AMsgName, ASgnName, ref value);
}
/// <summary>
/// 设置 DBC 中定义信号的值(按物理值编码进报文数据),可选择立即发送或周期发送。
/// </summary>
/// <param name="channel">通道号(0 起始)</param>
/// <param name="AMsgName">DBC 中的报文名称</param>
/// <param name="ASgnName">DBC 中的信号名称</param>
/// <param name="AValue">要设置的信号物理值</param>
/// <param name="isSend">是否发送:false 仅更新数据不发送,true 则发送</param>
/// <param name="sendPeriod">发送周期(毫秒):0 = 只发送一次;&gt;0 = 按该周期循环发送</param>
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
/// <exception cref="Exception">信号编码写入报文失败时抛出</exception>
public static int SetSignalValue(byte channel, string AMsgName, string ASgnName, double AValue, bool isSend = false, float sendPeriod = 0)
{
// 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);
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)
{
return TsMasterApi.tsapp_transmit_canfd_async(ref find.ACANFD);
}
else
{
return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.SetSignalValue 发送异常:{ex.Message}");
return -1;
}
}
return 0;
}
/// <summary>
/// 按报文名称发送 DBC 中定义的报文(使用该报文在 DBC 数据库中的当前数据)。
/// </summary>
/// <param name="channel">通道号</param>
/// <param name="AMsgName">DBC 中的报文名称</param>
/// <param name="sendPeriod">发送周期(毫秒):0 = 只发送一次;&gt;0 = 按该周期循环发送</param>
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
public static int SetSignalValue(APP_CHANNEL channel, string AMsgName, float sendPeriod = 0)
{
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);
}
else
{
return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.SetSignalValue 发送异常:{ex.Message}");
return -1;
}
}
/// <summary>
/// 发送自定义报文
/// </summary>
/// <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 数据库中指定报文的数据区(仅更新内存中的数据,不发送)。
/// </summary>
/// <param name="channel">通道号</param>
/// <param name="ID">报文帧 ID(DBC 中定义的 FIdentifier)</param>
/// <param name="bytes">要写入的报文数据数组,超出报文数据长度的部分会被截断</param>
public static void SetMsg(APP_CHANNEL channel, int ID, byte[] bytes)
{
var find = DBCParse.MsgDatabase[(byte)channel].FirstOrDefault(s => s.ACANFD.FIdentifier == ID);
if (find != null)
{
Array.Copy(bytes, 0, find.ACANFD.FData, 0, Math.Min(bytes.Length, find.ACANFD.FData.Length));
}
}
/// <summary>
/// 按帧 ID 构造并发送 CANFD 报文(不依赖 DBC 数据库与报文名),可选择单次发送或周期发送。
/// </summary>
/// <param name="channel">通道号</param>
/// <param name="ID">报文帧 ID</param>
/// <param name="bytes">报文数据数组(最多 64 字节,超出部分截断)</param>
/// <param name="sendPeriod">发送周期(毫秒):0 = 只发送一次;&gt;0 = 按该周期循环发送</param>
/// <param name="isExt">是否扩展帧(29 位 ID),默认标准帧</param>
/// <returns>返回代码,0 表示成功,非 0 表示失败(可用 <see cref="GetErrorDescription"/> 查询错误说明)</returns>
public static int SendMsgById(APP_CHANNEL channel, int ID, byte[] bytes, float sendPeriod = 0, bool isExt = false)
{
var data = new byte[64];
Array.Copy(bytes, data, Math.Min(bytes.Length, 64));
return SendMsg(channel, ID, true, isExt, false, (byte)Math.Min(bytes.Length, 64),
data, true, false, false, sendPeriod);
}
/// <summary>
/// 构造并发送自定义 CAN/CANFD 报文,可选择单次发送或周期发送,并可同步更新 DBC 数据库中同 ID 报文的数据。
/// </summary>
/// <param name="AIdxChn">通道号</param>
/// <param name="AID">报文帧 ID</param>
/// <param name="AIsTx">是否为发送帧</param>
/// <param name="AIsExt">是否为扩展帧(29 位 ID)</param>
/// <param name="AIsRemote">是否为远程帧</param>
/// <param name="ADLC">数据长度代码(数据字节数,最大 64)</param>
/// <param name="ADataArray">报文数据数组,超过 64 字节的部分会被截断</param>
/// <param name="AIsFD">是否为 CANFD 帧,默认 true</param>
/// <param name="AIsBRS">是否切换数据段波特率(仅 CANFD 有效),默认 false</param>
/// <param name="isUpdateDBCDatabase">是否同步更新 DBC 数据库中同帧 ID 报文的数据,默认 false</param>
/// <param name="sendPeriod">发送周期(毫秒):0 = 只发送一次;&gt;0 = 按该周期循环发送</param>
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
public static int SendMsg(APP_CHANNEL AIdxChn, int AID, bool AIsTx, bool AIsExt,
bool AIsRemote, byte ADLC, byte[] ADataArray, bool AIsFD = true, bool AIsBRS = false,
bool isUpdateDBCDatabase = false, float sendPeriod = 0)
{
var send = new TLIBCANFD
{
FIdxChn = (byte)AIdxChn,
FProperties = 0,
FIdentifier = AID,
FDLC = ADLC,
FTimeUS = 0uL,
FData = new byte[64],
FFDProperties = 0,
FIsTx = AIsTx,
FIsError = false,
FIsExt = AIsExt,
FIsRemote = AIsRemote,
FIsFD = AIsFD,
FIsBRS = AIsBRS
};
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);
}
else
{
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 && (byte)AIdxChn < DBCParse.MsgDatabase.Count)
{
var find = DBCParse.MsgDatabase[(byte)AIdxChn].FirstOrDefault(s => s.ACANFD.FIdentifier == send.FIdentifier);
if (find != null)
{
Array.Copy(send.FData, 0, find.ACANFD.FData, 0, Math.Min(ADataArray.Length, find.ACANFD.FData.Length));
}
}
return re;
}
/// <summary>
/// 将 DBC 中定义的报文加入循环发送列表,按指定周期持续发送。
/// </summary>
/// <param name="channel">通道号(0 起始)</param>
/// <param name="AMsgName">DBC 中的报文名称</param>
/// <param name="sendPeriod">发送周期(毫秒)</param>
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
public static int AddCyclicMsg(byte channel, string AMsgName, float sendPeriod)
{
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 返回码。
/// </summary>
/// <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);
if (ADesc == IntPtr.Zero) return $"未知错误代码: {errorCode}";
// 假设返回的是 ANSI 字符串(如 C 的 char*)
string? description = Marshal.PtrToStringAnsi(ADesc);
return description ?? $"未知错误代码: {errorCode}";
}
catch (Exception ex)
{
Debug.WriteLine($"CAN.GetErrorDescription 异常:{ex.Message}");
return $"未知错误代码: {errorCode}";
}
}
}
}