CanOpenSlaveDevice.cs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  1. using CanTest;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. namespace CCDCount.DLL.CanBus
  9. {
  10. /// <summary>
  11. /// CANopen从站设备 - 基于Nameless.eds文件实现
  12. /// 支持4个RPDO和4个TPDO,符合CiA DS301标准
  13. /// </summary>
  14. public class CanOpenSlaveDevice : IDisposable
  15. {
  16. #region 私有字段
  17. private CanOpenManager m_canManager;
  18. private byte m_nodeId = 1; // 默认节点ID
  19. private NmtState m_currentState = NmtState.Initializing;
  20. // PDO配置 - 根据EDS文件定义
  21. private PdoConfig[] m_rpdoConfigs = new PdoConfig[4]; // 接收PDO
  22. private PdoConfig[] m_tpdoConfigs = new PdoConfig[4]; // 发送PDO
  23. // PDO映射对象
  24. private PdoMapping[,] m_rpdoMappings = new PdoMapping[4, 4]; // RPDO映射表
  25. private PdoMapping[,] m_tpdoMappings = new PdoMapping[4, 4]; // TPDO映射表
  26. // 应用数据缓冲区 (用于PDO数据交换)
  27. private Dictionary<ushort, object> m_objectDictionary = new Dictionary<ushort, object>();
  28. // 心跳参数
  29. private ushort m_producerHeartbeatTime = 0; // 心跳时间(ms),0表示禁用
  30. private Timer m_heartbeatTimer;
  31. // SYNC参数
  32. private bool m_syncEnabled = false;
  33. private byte m_syncCounter = 0;
  34. // 线程控制
  35. private CancellationTokenSource m_cancellationTokenSource;
  36. private Task m_receiveTask;
  37. private bool m_isRunning = false;
  38. #endregion
  39. #region 事件定义
  40. /// <summary>
  41. /// RPDO接收事件
  42. /// </summary>
  43. public event Action<byte, byte, byte[]> OnRpdoReceived;
  44. /// <summary>
  45. /// NMT状态改变事件
  46. /// </summary>
  47. public event Action<NmtState, NmtState> OnNmtStateChanged;
  48. /// <summary>
  49. /// SDO读取请求事件
  50. /// </summary>
  51. public event Action<byte, ushort, byte> OnSdoReadRequest;
  52. /// <summary>
  53. /// SDO写入请求事件
  54. /// </summary>
  55. public event Action<byte, ushort, byte, uint> OnSdoWriteRequest;
  56. #endregion
  57. #region 构造函数
  58. /// <summary>
  59. /// 构造函数
  60. /// </summary>
  61. /// <param name="nodeId">节点ID(1-127)</param>
  62. /// <param name="deviceType">设备类型</param>
  63. /// <param name="deviceIndex">设备索引</param>
  64. /// <param name="canIndex">CAN通道索引</param>
  65. public CanOpenSlaveDevice(byte nodeId = 1, UInt32 deviceType = 4, UInt32 deviceIndex = 0, UInt32 canIndex = 0)
  66. {
  67. if (nodeId < 1 || nodeId > 127)
  68. throw new ArgumentException("节点ID必须在1-127之间");
  69. m_nodeId = nodeId;
  70. m_canManager = new CanOpenManager(deviceType, deviceIndex, canIndex);
  71. InitializePdoConfigs();
  72. InitializeObjectDictionary();
  73. }
  74. #endregion
  75. #region 初始化方法
  76. /// <summary>
  77. /// 初始化PDO配置 - 根据EDS文件
  78. /// </summary>
  79. private void InitializePdoConfigs()
  80. {
  81. // 初始化RPDO配置 (1400-1403)
  82. for (int i = 0; i < 4; i++)
  83. {
  84. m_rpdoConfigs[i] = new PdoConfig
  85. {
  86. CobId = GetRpdoCobId((byte)(i + 1), m_nodeId),
  87. TransmissionType = 0xFF, // 事件驱动
  88. Enabled = true
  89. };
  90. // 初始化映射表为空
  91. for (int j = 0; j < 4; j++)
  92. {
  93. m_rpdoMappings[i, j] = new PdoMapping
  94. {
  95. Index = 0,
  96. SubIndex = 0,
  97. BitLength = 0
  98. };
  99. }
  100. }
  101. // 初始化TPDO配置 (1800-1803)
  102. for (int i = 0; i < 4; i++)
  103. {
  104. m_tpdoConfigs[i] = new PdoConfig
  105. {
  106. CobId = GetTpdoCobId((byte)(i + 1), m_nodeId),
  107. TransmissionType = 0xFF, // 事件驱动
  108. Enabled = true
  109. };
  110. // 初始化映射表为空
  111. for (int j = 0; j < 4; j++)
  112. {
  113. m_tpdoMappings[i, j] = new PdoMapping
  114. {
  115. Index = 0,
  116. SubIndex = 0,
  117. BitLength = 0
  118. };
  119. }
  120. }
  121. }
  122. /// <summary>
  123. /// 初始化对象字典 - 根据EDS文件定义
  124. /// </summary>
  125. private void InitializeObjectDictionary()
  126. {
  127. // 1000h - Device type
  128. m_objectDictionary[(ushort)0x1000] = (uint)0x00000000; // 默认值
  129. // 1001h - Error register
  130. m_objectDictionary[(ushort)0x1001] = (byte)0x00;
  131. // 1017h - Producer Heartbeat Time (心跳时间)
  132. // 根据CANopen标准,此对象用于配置心跳发送间隔
  133. m_objectDictionary[(ushort)0x1017] = (ushort)1000; // 默认1000ms
  134. // 1018h - Identity Object
  135. m_objectDictionary[(ushort)0x1018] = new IdentityObject
  136. {
  137. VendorId = 0x0000000D, // HanLin
  138. ProductCode = 0x00000001, // CCDCountCanOpenSlave
  139. RevisionNumber = 0x00010001
  140. };
  141. // 1800-1803h - TPDO communication parameters
  142. for (int i = 0; i < 4; i++)
  143. {
  144. m_objectDictionary[(ushort)(0x1800 + i)] = new TpdoCommParam
  145. {
  146. CobId = GetTpdoCobId((byte)(i + 1), m_nodeId),
  147. TransmissionType = 0xFF
  148. };
  149. }
  150. // 1A00-1A03h - TPDO mapping parameters
  151. for (int i = 0; i < 4; i++)
  152. {
  153. m_objectDictionary[(ushort)(0x1A00 + i)] = new PdoMappingParam
  154. {
  155. NumberOfEntries = 4,
  156. Mappings = new uint[4]
  157. };
  158. }
  159. // 1400-1403h - RPDO communication parameters
  160. for (int i = 0; i < 4; i++)
  161. {
  162. m_objectDictionary[(ushort)(0x1400 + i)] = new RpdoCommParam
  163. {
  164. CobId = GetRpdoCobId((byte)(i + 1), m_nodeId),
  165. TransmissionType = 0xFF
  166. };
  167. }
  168. // 1600-1603h - RPDO mapping parameters
  169. for (int i = 0; i < 4; i++)
  170. {
  171. m_objectDictionary[(ushort)(0x1600 + i)] = new PdoMappingParam
  172. {
  173. NumberOfEntries = 4,
  174. Mappings = new uint[4]
  175. };
  176. }
  177. }
  178. #endregion
  179. #region 启动和停止
  180. /// <summary>
  181. /// 启动从站设备
  182. /// </summary>
  183. /// <param name="baudRate">波特率</param>
  184. /// <returns>是否成功</returns>
  185. public bool Start(CanBaudRate baudRate = CanBaudRate.BaudRate_1M)
  186. {
  187. if (m_isRunning)
  188. {
  189. Console.WriteLine("从站设备已在运行");
  190. return true;
  191. }
  192. // 初始化CAN
  193. if (!m_canManager.Initialize(baudRate))
  194. {
  195. Console.WriteLine("CAN初始化失败");
  196. return false;
  197. }
  198. m_isRunning = true;
  199. m_currentState = NmtState.Initializing;
  200. // 默认配置心跳为1000ms
  201. ConfigureHeartbeat(1000);
  202. // 发送无错误紧急报文,表明设备正常
  203. SendNoErrorEmcy();
  204. // 等待主站发送NMT命令来控制状态转换
  205. Console.WriteLine($"NMT: 节点{m_nodeId}已初始化,等待主站命令...");
  206. // 启动接收线程
  207. m_cancellationTokenSource = new CancellationTokenSource();
  208. m_receiveTask = Task.Run(() => ReceiveLoop(m_cancellationTokenSource.Token));
  209. Console.WriteLine($"CANopen从站启动成功 - 节点ID: {m_nodeId}");
  210. return true;
  211. }
  212. /// <summary>
  213. /// 停止从站设备
  214. /// </summary>
  215. public void Stop()
  216. {
  217. if (!m_isRunning)
  218. return;
  219. m_isRunning = false;
  220. // 停止接收线程
  221. m_cancellationTokenSource?.Cancel();
  222. m_receiveTask?.Wait(1000);
  223. m_receiveTask?.Dispose();
  224. // 停止心跳定时器
  225. m_heartbeatTimer?.Dispose();
  226. // 关闭CAN
  227. m_canManager.Close();
  228. m_currentState = NmtState.Stopped;
  229. Console.WriteLine("CANopen从站已停止");
  230. }
  231. #endregion
  232. #region 接收处理循环
  233. /// <summary>
  234. /// CAN帧接收循环
  235. /// </summary>
  236. private void ReceiveLoop(CancellationToken cancellationToken)
  237. {
  238. while (!cancellationToken.IsCancellationRequested)
  239. {
  240. try
  241. {
  242. var frames = m_canManager.ReceiveCanFrames(50);
  243. foreach (var frame in frames)
  244. {
  245. ProcessCanFrame(frame);
  246. }
  247. }
  248. catch (Exception ex)
  249. {
  250. Console.WriteLine($"接收错误: {ex.Message}");
  251. }
  252. Thread.Sleep(1); // 避免CPU占用过高
  253. }
  254. }
  255. /// <summary>
  256. /// 处理接收到的CAN帧
  257. /// </summary>
  258. private void ProcessCanFrame(CanOpenFrame frame)
  259. {
  260. // 记录所有接收到的帧(用于调试)
  261. Console.WriteLine($"[接收详细] COB-ID: 0x{frame.CobId:X3}, Len: {frame.DataLength}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  262. // 检查是否是NMT命令 (COB-ID = 0x000)
  263. if (frame.CobId == (uint)0x000 && frame.DataLength >= 2)
  264. {
  265. Console.WriteLine($"[NMT命令] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  266. ProcessNmtCommand(frame.Data);
  267. return;
  268. }
  269. // 检查是否是SYNC (COB-ID = 0x080)
  270. if (frame.CobId == (uint)0x080)
  271. {
  272. Console.WriteLine($"[SYNC] COB-ID: 0x{frame.CobId:X3}");
  273. ProcessSync(frame.Data);
  274. return;
  275. }
  276. // 检查是否是SDO请求 (COB-ID = 0x600 + nodeId)
  277. uint sdoRequestCobId = (uint)0x600 + m_nodeId;
  278. if (frame.CobId == sdoRequestCobId)
  279. {
  280. Console.WriteLine($"[SDO请求] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  281. ProcessSdoRequest(frame.Data);
  282. return;
  283. }
  284. // 检查是否是RPDO
  285. for (int i = 0; i < 4; i++)
  286. {
  287. if (frame.CobId == m_rpdoConfigs[i].CobId && m_rpdoConfigs[i].Enabled)
  288. {
  289. Console.WriteLine($"[RPDO{i+1}] COB-ID: 0x{frame.CobId:X3}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  290. ProcessRpdo((byte)(i + 1), frame.Data, frame.DataLength);
  291. return;
  292. }
  293. }
  294. // 其他未知帧
  295. Console.WriteLine($"[未知帧] COB-ID: 0x{frame.CobId:X3}, Len: {frame.DataLength}, Data: {BitConverter.ToString(frame.Data, 0, frame.DataLength).Replace("-", " ")}");
  296. }
  297. #endregion
  298. #region NMT处理
  299. /// <summary>
  300. /// 处理NMT命令
  301. /// </summary>
  302. private void ProcessNmtCommand(byte[] data)
  303. {
  304. byte commandSpecifier = data[0];
  305. byte nodeId = data[1];
  306. // 检查是否是广播命令或针对本节点的命令
  307. if (nodeId != 0 && nodeId != m_nodeId)
  308. return;
  309. NmtState oldState = m_currentState;
  310. Console.WriteLine($"[NMT详细] 收到命令: 0x{commandSpecifier:X2}, 目标节点: {nodeId}, 当前状态: {m_currentState}");
  311. switch (commandSpecifier)
  312. {
  313. case 0x01: // 启动节点
  314. Console.WriteLine($"[NMT详细] 执行启动节点命令");
  315. m_currentState = NmtState.Operational;
  316. Console.WriteLine($"NMT: 节点{m_nodeId}进入运行状态");
  317. break;
  318. case 0x02: // 停止节点
  319. Console.WriteLine($"[NMT详细] 执行停止节点命令");
  320. m_currentState = NmtState.Stopped;
  321. Console.WriteLine($"NMT: 节点{m_nodeId}进入停止状态");
  322. break;
  323. case 0x80: // 进入预操作状态
  324. Console.WriteLine($"[NMT详细] 执行进入预操作状态命令");
  325. m_currentState = NmtState.PreOperational;
  326. Console.WriteLine($"NMT: 节点{m_nodeId}进入预操作状态");
  327. break;
  328. case 0x81: // 重置节点
  329. Console.WriteLine($"[NMT详细] 执行重置节点命令 - 开始初始化流程");
  330. // 执行节点重置逻辑
  331. m_currentState = NmtState.Initializing;
  332. // 打印当前对象字典状态(用于调试)
  333. Console.WriteLine($"[NMT调试] 当前对象字典关键参数:");
  334. if (m_objectDictionary.ContainsKey((ushort)0x1000))
  335. Console.WriteLine($" - 设备类型(0x1000): {m_objectDictionary[(ushort)0x1000]}");
  336. if (m_objectDictionary.ContainsKey((ushort)0x1001))
  337. Console.WriteLine($" - 错误寄存器(0x1001): {m_objectDictionary[(ushort)0x1001]}");
  338. if (m_objectDictionary.ContainsKey((ushort)0x1017))
  339. Console.WriteLine($" - 心跳时间(0x1017): {m_objectDictionary[(ushort)0x1017]}ms");
  340. if (m_objectDictionary.ContainsKey((ushort)0x1018))
  341. {
  342. var identity = m_objectDictionary[(ushort)0x1018] as IdentityObject;
  343. if (identity != null)
  344. Console.WriteLine($" - 厂商ID(0x1018.1): 0x{identity.VendorId:X4}");
  345. }
  346. // 短暂延迟后自动进入预操作状态(模拟初始化过程)
  347. Task.Delay(50).ContinueWith(_ =>
  348. {
  349. // 立即发送一次心跳,确认状态转换
  350. SendHeartbeat();
  351. m_currentState = NmtState.PreOperational;
  352. Console.WriteLine($"NMT: 节点{m_nodeId}初始化完成,进入预操作状态");
  353. OnNmtStateChanged?.Invoke(oldState, m_currentState);
  354. });
  355. return; // 提前返回,等待异步完成
  356. case 0x82: // 重置通信
  357. Console.WriteLine($"[NMT详细] 执行重置通信命令");
  358. m_currentState = NmtState.PreOperational;
  359. Console.WriteLine($"NMT: 节点{m_nodeId}通信重置");
  360. // TODO: 执行通信重置逻辑
  361. break;
  362. default:
  363. Console.WriteLine($"[NMT详细] 未知命令: 0x{commandSpecifier:X2}");
  364. break;
  365. }
  366. // 触发状态改变事件
  367. if (oldState != m_currentState)
  368. {
  369. Console.WriteLine($"[NMT详细] 状态改变: {oldState} → {m_currentState}");
  370. OnNmtStateChanged?.Invoke(oldState, m_currentState);
  371. }
  372. else
  373. {
  374. Console.WriteLine($"[NMT详细] 状态未改变: {oldState}");
  375. }
  376. }
  377. #endregion
  378. #region SYNC处理
  379. /// <summary>
  380. /// 处理SYNC帧
  381. /// </summary>
  382. private void ProcessSync(byte[] data)
  383. {
  384. if (data.Length > 0)
  385. {
  386. m_syncCounter = data[0];
  387. }
  388. // 如果TPDO配置为同步触发,则发送
  389. for (int i = 0; i < 4; i++)
  390. {
  391. if (m_tpdoConfigs[i].Enabled &&
  392. m_tpdoConfigs[i].TransmissionType >= 1 &&
  393. m_tpdoConfigs[i].TransmissionType <= 240)
  394. {
  395. SendTpdo((byte)(i + 1));
  396. }
  397. }
  398. }
  399. #endregion
  400. #region SDO处理
  401. /// <summary>
  402. /// 处理SDO请求
  403. /// </summary>
  404. private void ProcessSdoRequest(byte[] data)
  405. {
  406. if (data.Length < 8)
  407. return;
  408. byte cs = (byte)(data[0] >> 5); // 命令 specifier
  409. ushort index = (ushort)(data[1] | (data[2] << 8));
  410. byte subIndex = data[3];
  411. switch (cs)
  412. {
  413. case 2: // SDO上传请求 (读)
  414. HandleSdoUpload(index, subIndex);
  415. break;
  416. case 1: // SDO下载请求 (写)
  417. HandleSdoDownload(index, subIndex, data);
  418. break;
  419. default:
  420. Console.WriteLine($"未支持的SDO命令: CS={cs}");
  421. break;
  422. }
  423. }
  424. /// <summary>
  425. /// 处理SDO上传 (读对象字典)
  426. /// </summary>
  427. private void HandleSdoUpload(ushort index, byte subIndex)
  428. {
  429. byte[] response = new byte[8];
  430. Console.WriteLine($"[SDO详细] 收到上传请求: 索引0x{index:X4}, 子索引{subIndex}");
  431. // 触发自定义事件
  432. OnSdoReadRequest?.Invoke(m_nodeId, index, subIndex);
  433. // 根据索引返回数据
  434. if (m_objectDictionary.TryGetValue(index, out object value))
  435. {
  436. Console.WriteLine($"[SDO详细] 找到对象 0x{index:X4}");
  437. if (subIndex == 0)
  438. {
  439. // 返回子索引数量
  440. response[0] = 0x4F; // 快速上传响应
  441. response[1] = (byte)(index & 0xFF);
  442. response[2] = (byte)(index >> 8);
  443. response[3] = subIndex;
  444. if (value is IdentityObject identity)
  445. {
  446. response[4] = 4; // 4个子条目
  447. Console.WriteLine($"[SDO详细] 返回Identity对象,子条目数: 4");
  448. }
  449. else if (value is PdoMappingParam mappingParam)
  450. {
  451. response[4] = mappingParam.NumberOfEntries;
  452. Console.WriteLine($"[SDO详细] 返回PDO映射参数,子条目数: {mappingParam.NumberOfEntries}");
  453. }
  454. else
  455. {
  456. response[4] = 0;
  457. Console.WriteLine($"[SDO详细] 返回简单对象,子条目数: 0");
  458. }
  459. }
  460. else
  461. {
  462. // 返回具体子索引的值
  463. response[0] = 0x4B; // 4字节快速上传
  464. response[1] = (byte)(index & 0xFF);
  465. response[2] = (byte)(index >> 8);
  466. response[3] = subIndex;
  467. uint dataValue = 0;
  468. if (index == (ushort)0x1000 && value is uint deviceType)
  469. {
  470. dataValue = deviceType;
  471. Console.WriteLine($"[SDO详细] 返回设备类型: 0x{dataValue:X8}");
  472. }
  473. else if (index == (ushort)0x1001 && value is byte errorReg)
  474. {
  475. response[0] = 0x4F; // 1字节
  476. dataValue = errorReg;
  477. Console.WriteLine($"[SDO详细] 返回错误寄存器: 0x{dataValue:X2}");
  478. }
  479. else if (index == (ushort)0x1017 && value is ushort heartbeatTime)
  480. {
  481. response[0] = 0x4B; // 2字节
  482. dataValue = heartbeatTime;
  483. Console.WriteLine($"[SDO详细] 返回心跳时间: {heartbeatTime}ms");
  484. }
  485. else if (index == (ushort)0x1018 && value is IdentityObject identity)
  486. {
  487. response[0] = 0x4B;
  488. switch (subIndex)
  489. {
  490. case 1: dataValue = identity.VendorId; break;
  491. case 2: dataValue = identity.ProductCode; break;
  492. case 3: dataValue = identity.RevisionNumber; break;
  493. default: dataValue = 0; break;
  494. }
  495. Console.WriteLine($"[SDO详细] 返回Identity子索引{subIndex}: 0x{dataValue:X8}");
  496. }
  497. else if ((index >= (ushort)0x1800 && index <= (ushort)0x1803) && value is TpdoCommParam tpdoParam)
  498. {
  499. switch (subIndex)
  500. {
  501. case 1: dataValue = tpdoParam.CobId; break;
  502. case 2: dataValue = tpdoParam.TransmissionType; break;
  503. default: dataValue = 0; break;
  504. }
  505. Console.WriteLine($"[SDO详细] 返回TPDO{index - 0x1800 + 1}通信参数子索引{subIndex}: 0x{dataValue:X8}");
  506. }
  507. else if ((index >= (ushort)0x1400 && index <= (ushort)0x1403) && value is RpdoCommParam rpdoParam)
  508. {
  509. switch (subIndex)
  510. {
  511. case 1: dataValue = rpdoParam.CobId; break;
  512. case 2: dataValue = rpdoParam.TransmissionType; break;
  513. default: dataValue = 0; break;
  514. }
  515. Console.WriteLine($"[SDO详细] 返回RPDO{index - 0x1400 + 1}通信参数子索引{subIndex}: 0x{dataValue:X8}");
  516. }
  517. response[4] = (byte)(dataValue & 0xFF);
  518. response[5] = (byte)((dataValue >> 8) & 0xFF);
  519. response[6] = (byte)((dataValue >> 16) & 0xFF);
  520. response[7] = (byte)((dataValue >> 24) & 0xFF);
  521. }
  522. // 发送SDO响应
  523. uint responseCobId = (uint)0x580 + m_nodeId;
  524. Console.WriteLine($"[SDO响应] COB-ID: 0x{responseCobId:X3}, Index: 0x{index:X4}, SubIndex: {subIndex}, Data: {BitConverter.ToString(response).Replace("-", " ")}");
  525. m_canManager.SendCanFrame(responseCobId, response);
  526. }
  527. else
  528. {
  529. // 对象不存在,发送错误响应
  530. Console.WriteLine($"[SDO详细] 对象 0x{index:X4} 不存在,返回错误");
  531. response[0] = 0x80; // 错误响应
  532. response[1] = (byte)(index & 0xFF);
  533. response[2] = (byte)(index >> 8);
  534. response[3] = subIndex;
  535. response[4] = 0x06; // 错误代码: 对象不存在
  536. response[5] = 0x02;
  537. response[6] = 0x00;
  538. response[7] = 0x00;
  539. uint responseCobId = (uint)0x580 + m_nodeId;
  540. Console.WriteLine($"[SDO错误响应] COB-ID: 0x{responseCobId:X3}, Index: 0x{index:X4}, SubIndex: {subIndex}");
  541. m_canManager.SendCanFrame(responseCobId, response);
  542. }
  543. }
  544. /// <summary>
  545. /// 处理SDO下载 (写对象字典)
  546. /// </summary>
  547. private void HandleSdoDownload(ushort index, byte subIndex, byte[] data)
  548. {
  549. uint value = (uint)(data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24));
  550. // 触发自定义事件
  551. OnSdoWriteRequest?.Invoke(m_nodeId, index, subIndex, value);
  552. // 更新对象字典
  553. if (m_objectDictionary.ContainsKey(index))
  554. {
  555. // 特殊处理:心跳时间对象 (0x1017)
  556. if (index == (ushort)0x1017 && subIndex == 0)
  557. {
  558. ushort heartbeatTime = (ushort)value;
  559. m_objectDictionary[index] = heartbeatTime;
  560. ConfigureHeartbeat(heartbeatTime); // 重新配置心跳
  561. Console.WriteLine($"[SDO写入] 心跳时间已更新为: {heartbeatTime}ms");
  562. }
  563. else if (index >= (ushort)0x1800 && index <= (ushort)0x1803 && m_objectDictionary[index] is TpdoCommParam tpdoParam)
  564. {
  565. int pdoIndex = index - (ushort)0x1800;
  566. if (subIndex == 1)
  567. {
  568. tpdoParam.CobId = value;
  569. m_tpdoConfigs[pdoIndex].CobId = value;
  570. }
  571. else if (subIndex == 2)
  572. {
  573. tpdoParam.TransmissionType = (byte)value;
  574. m_tpdoConfigs[pdoIndex].TransmissionType = (byte)value;
  575. }
  576. }
  577. else if (index >= (ushort)0x1400 && index <= (ushort)0x1403 && m_objectDictionary[index] is RpdoCommParam rpdoParam)
  578. {
  579. int pdoIndex = index - (ushort)0x1400;
  580. if (subIndex == 1)
  581. {
  582. rpdoParam.CobId = value;
  583. m_rpdoConfigs[pdoIndex].CobId = value;
  584. }
  585. else if (subIndex == 2)
  586. {
  587. rpdoParam.TransmissionType = (byte)value;
  588. m_rpdoConfigs[pdoIndex].TransmissionType = (byte)value;
  589. }
  590. }
  591. }
  592. // 发送SDO确认响应
  593. byte[] response = new byte[8];
  594. response[0] = 0x60; // SDO下载确认
  595. response[1] = (byte)(index & 0xFF);
  596. response[2] = (byte)(index >> 8);
  597. response[3] = subIndex;
  598. uint responseCobId = (uint)0x580 + m_nodeId;
  599. m_canManager.SendCanFrame(responseCobId, response);
  600. }
  601. #endregion
  602. #region RPDO处理
  603. /// <summary>
  604. /// 处理接收到的RPDO
  605. /// </summary>
  606. private void ProcessRpdo(byte pdoNumber, byte[] data, byte length)
  607. {
  608. Console.WriteLine($"收到RPDO{pdoNumber}: 长度{length}字节");
  609. // 复制有效数据
  610. byte[] validData = new byte[length];
  611. Array.Copy(data, validData, length);
  612. // 触发事件
  613. OnRpdoReceived?.Invoke(m_nodeId, pdoNumber, validData);
  614. }
  615. #endregion
  616. #region TPDO发送
  617. /// <summary>
  618. /// 发送TPDO
  619. /// </summary>
  620. public void SendTpdo(byte pdoNumber)
  621. {
  622. if (pdoNumber < 1 || pdoNumber > 4)
  623. throw new ArgumentException("PDO编号必须在1-4之间");
  624. if (!m_tpdoConfigs[pdoNumber - 1].Enabled)
  625. {
  626. Console.WriteLine($"TPDO{pdoNumber}未启用");
  627. return;
  628. }
  629. // TODO: 根据映射表构建PDO数据
  630. // 这里使用示例数据
  631. byte[] pdoData = new byte[8];
  632. // 示例: 填充一些测试数据
  633. pdoData[0] = (byte)(1);
  634. pdoData[1] = (byte)(2);
  635. pdoData[2] = (byte)(3);
  636. pdoData[3] = (byte)(4);
  637. pdoData[4] = (byte)(4);
  638. pdoData[5] = (byte)(5);
  639. pdoData[6] = (byte)(6);
  640. pdoData[7] = (byte)(7);
  641. uint cobId = m_tpdoConfigs[pdoNumber - 1].CobId;
  642. m_canManager.SendCanFrame(cobId, pdoData);
  643. Console.WriteLine($"发送TPDO{pdoNumber}: COB-ID 0x{cobId:X3}");
  644. }
  645. /// <summary>
  646. /// 发送所有启用的TPDO
  647. /// </summary>
  648. public void SendAllTpdos()
  649. {
  650. for (byte i = 1; i <= 4; i++)
  651. {
  652. if (m_tpdoConfigs[i - 1].Enabled)
  653. {
  654. SendTpdo(i);
  655. }
  656. }
  657. }
  658. #endregion
  659. #region 心跳机制
  660. /// <summary>
  661. /// 配置心跳
  662. /// </summary>
  663. /// <param name="heartbeatTimeMs">心跳时间(毫秒),0表示禁用</param>
  664. public void ConfigureHeartbeat(ushort heartbeatTimeMs)
  665. {
  666. m_producerHeartbeatTime = heartbeatTimeMs;
  667. // 同步更新对象字典中的 1017h 对象
  668. if (m_objectDictionary.ContainsKey((ushort)0x1017))
  669. {
  670. m_objectDictionary[(ushort)0x1017] = heartbeatTimeMs;
  671. }
  672. // 停止旧的定时器
  673. m_heartbeatTimer?.Dispose();
  674. if (heartbeatTimeMs > 0)
  675. {
  676. // 创建新的定时器
  677. m_heartbeatTimer = new Timer(state =>
  678. {
  679. // 只在PreOperational和Operational状态下发送心跳
  680. // Initializing和Stopped状态下不发送心跳
  681. if (m_currentState == NmtState.Operational ||
  682. m_currentState == NmtState.PreOperational)
  683. {
  684. SendHeartbeat();
  685. }
  686. else
  687. {
  688. // 在其他状态下跳过心跳发送
  689. //Console.WriteLine($"[心跳跳过] 当前状态: {m_currentState}, 不发送心跳");
  690. }
  691. }, null, heartbeatTimeMs, heartbeatTimeMs);
  692. Console.WriteLine($"心跳已配置: {heartbeatTimeMs}ms");
  693. }
  694. else
  695. {
  696. Console.WriteLine("心跳已禁用");
  697. }
  698. }
  699. /// <summary>
  700. /// 发送心跳
  701. /// </summary>
  702. private void SendHeartbeat()
  703. {
  704. byte[] heartbeatData = new byte[1];
  705. // 根据当前状态设置心跳字节
  706. switch (m_currentState)
  707. {
  708. case NmtState.Initializing:
  709. heartbeatData[0] = 0x00;
  710. break;
  711. case NmtState.PreOperational:
  712. heartbeatData[0] = 0x7F;
  713. break;
  714. case NmtState.Operational:
  715. heartbeatData[0] = 0x05;
  716. break;
  717. case NmtState.Stopped:
  718. heartbeatData[0] = 0x04;
  719. break;
  720. default:
  721. heartbeatData[0] = 0x00;
  722. break;
  723. }
  724. uint heartbeatCobId = (uint)0x700 + (uint)m_nodeId;
  725. //Console.WriteLine($"[心跳发送] COB-ID: 0x{heartbeatCobId:X3}, Data: {heartbeatData[0]:X2} (状态: {m_currentState})");
  726. m_canManager.SendCanFrame(heartbeatCobId, heartbeatData);
  727. }
  728. /// <summary>
  729. /// 发送无错误紧急报文 (EMCY)
  730. /// 用于在启动时表明设备无错误状态
  731. /// </summary>
  732. private void SendNoErrorEmcy()
  733. {
  734. byte[] emcyData = new byte[8];
  735. emcyData[0] = 0x00; // 错误代码低字节 - 无错误
  736. emcyData[1] = 0x00; // 错误代码高字节
  737. emcyData[2] = 0x00; // 错误寄存器
  738. uint emcyCobId = (uint)0x080 + (uint)m_nodeId;
  739. Console.WriteLine($"[EMCY] 发送无错误报文 - COB-ID: 0x{emcyCobId:X3}");
  740. m_canManager.SendCanFrame(emcyCobId, emcyData);
  741. }
  742. #endregion
  743. #region 辅助方法
  744. /// <summary>
  745. /// 获取RPDO的COB-ID
  746. /// </summary>
  747. private uint GetRpdoCobId(byte pdoNumber, byte nodeId)
  748. {
  749. switch (pdoNumber)
  750. {
  751. case 1: return (uint)0x200 + nodeId;
  752. case 2: return (uint)0x300 + nodeId;
  753. case 3: return (uint)0x400 + nodeId;
  754. case 4: return (uint)0x500 + nodeId;
  755. default: throw new ArgumentException("无效的PDO编号");
  756. }
  757. }
  758. /// <summary>
  759. /// 获取TPDO的COB-ID
  760. /// </summary>
  761. private uint GetTpdoCobId(byte pdoNumber, byte nodeId)
  762. {
  763. switch (pdoNumber)
  764. {
  765. case 1: return (uint)0x180 + nodeId;
  766. case 2: return (uint)0x280 + nodeId;
  767. case 3: return (uint)0x380 + nodeId;
  768. case 4: return (uint)0x480 + nodeId;
  769. default: throw new ArgumentException("无效的PDO编号");
  770. }
  771. }
  772. #endregion
  773. #region 资源管理
  774. /// <summary>
  775. /// 释放资源
  776. /// </summary>
  777. public void Dispose()
  778. {
  779. Stop();
  780. m_canManager?.Dispose();
  781. m_cancellationTokenSource?.Dispose();
  782. m_heartbeatTimer?.Dispose();
  783. }
  784. #endregion
  785. #region 公共属性
  786. /// <summary>
  787. /// 获取当前节点ID
  788. /// </summary>
  789. public byte NodeId => m_nodeId;
  790. /// <summary>
  791. /// 获取当前NMT状态
  792. /// </summary>
  793. public NmtState CurrentState => m_currentState;
  794. /// <summary>
  795. /// 获取是否正在运行
  796. /// </summary>
  797. public bool IsRunning => m_isRunning;
  798. #endregion
  799. }
  800. #region 数据结构定义
  801. /// <summary>
  802. /// NMT状态枚举
  803. /// </summary>
  804. public enum NmtState
  805. {
  806. Initializing = 0x00,
  807. PreOperational = 0x7F,
  808. Operational = 0x05,
  809. Stopped = 0x04
  810. }
  811. /// <summary>
  812. /// PDO配置
  813. /// </summary>
  814. public class PdoConfig
  815. {
  816. public uint CobId { get; set; }
  817. public byte TransmissionType { get; set; } // 0xFF=事件驱动, 1-240=同步周期
  818. public bool Enabled { get; set; }
  819. }
  820. /// <summary>
  821. /// PDO映射
  822. /// </summary>
  823. public class PdoMapping
  824. {
  825. public ushort Index { get; set; }
  826. public byte SubIndex { get; set; }
  827. public byte BitLength { get; set; }
  828. }
  829. /// <summary>
  830. /// PDO映射参数
  831. /// </summary>
  832. public class PdoMappingParam
  833. {
  834. public byte NumberOfEntries { get; set; }
  835. public uint[] Mappings { get; set; }
  836. }
  837. /// <summary>
  838. /// 身份对象 (1018h)
  839. /// </summary>
  840. public class IdentityObject
  841. {
  842. public uint VendorId { get; set; }
  843. public uint ProductCode { get; set; }
  844. public uint RevisionNumber { get; set; }
  845. }
  846. /// <summary>
  847. /// TPDO通信参数 (1800-1803h)
  848. /// </summary>
  849. public class TpdoCommParam
  850. {
  851. public uint CobId { get; set; }
  852. public byte TransmissionType { get; set; }
  853. }
  854. /// <summary>
  855. /// RPDO通信参数 (1400-1403h)
  856. /// </summary>
  857. public class RpdoCommParam
  858. {
  859. public uint CobId { get; set; }
  860. public byte TransmissionType { get; set; }
  861. }
  862. #endregion
  863. }