CanOpenSlaveDevice.cs 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  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. // 此处数据长度不可以超过PLC中PDO映射的长度
  631. // 这里使用示例数据
  632. byte[] pdoData = new byte[4];
  633. // 示例: 填充一些测试数据
  634. pdoData[0] = (byte)(1);
  635. pdoData[1] = (byte)(3);
  636. uint cobId = m_tpdoConfigs[pdoNumber - 1].CobId;
  637. m_canManager.SendCanFrame(cobId, pdoData);
  638. Console.WriteLine($"发送TPDO{pdoNumber}: COB-ID 0x{cobId:X3}");
  639. }
  640. /// <summary>
  641. /// 发送所有启用的TPDO
  642. /// </summary>
  643. public void SendAllTpdos()
  644. {
  645. for (byte i = 1; i <= 4; i++)
  646. {
  647. if (m_tpdoConfigs[i - 1].Enabled)
  648. {
  649. SendTpdo(i);
  650. }
  651. }
  652. }
  653. #endregion
  654. #region 心跳机制
  655. /// <summary>
  656. /// 配置心跳
  657. /// </summary>
  658. /// <param name="heartbeatTimeMs">心跳时间(毫秒),0表示禁用</param>
  659. public void ConfigureHeartbeat(ushort heartbeatTimeMs)
  660. {
  661. m_producerHeartbeatTime = heartbeatTimeMs;
  662. // 同步更新对象字典中的 1017h 对象
  663. if (m_objectDictionary.ContainsKey((ushort)0x1017))
  664. {
  665. m_objectDictionary[(ushort)0x1017] = heartbeatTimeMs;
  666. }
  667. // 停止旧的定时器
  668. m_heartbeatTimer?.Dispose();
  669. if (heartbeatTimeMs > 0)
  670. {
  671. // 创建新的定时器
  672. m_heartbeatTimer = new Timer(state =>
  673. {
  674. // 只在PreOperational和Operational状态下发送心跳
  675. // Initializing和Stopped状态下不发送心跳
  676. if (m_currentState == NmtState.Operational ||
  677. m_currentState == NmtState.PreOperational)
  678. {
  679. SendHeartbeat();
  680. }
  681. else
  682. {
  683. // 在其他状态下跳过心跳发送
  684. //Console.WriteLine($"[心跳跳过] 当前状态: {m_currentState}, 不发送心跳");
  685. }
  686. }, null, heartbeatTimeMs, heartbeatTimeMs);
  687. Console.WriteLine($"心跳已配置: {heartbeatTimeMs}ms");
  688. }
  689. else
  690. {
  691. Console.WriteLine("心跳已禁用");
  692. }
  693. }
  694. /// <summary>
  695. /// 发送心跳
  696. /// </summary>
  697. private void SendHeartbeat()
  698. {
  699. byte[] heartbeatData = new byte[1];
  700. // 根据当前状态设置心跳字节
  701. switch (m_currentState)
  702. {
  703. case NmtState.Initializing:
  704. heartbeatData[0] = 0x00;
  705. break;
  706. case NmtState.PreOperational:
  707. heartbeatData[0] = 0x7F;
  708. break;
  709. case NmtState.Operational:
  710. heartbeatData[0] = 0x05;
  711. break;
  712. case NmtState.Stopped:
  713. heartbeatData[0] = 0x04;
  714. break;
  715. default:
  716. heartbeatData[0] = 0x00;
  717. break;
  718. }
  719. uint heartbeatCobId = (uint)0x700 + (uint)m_nodeId;
  720. //Console.WriteLine($"[心跳发送] COB-ID: 0x{heartbeatCobId:X3}, Data: {heartbeatData[0]:X2} (状态: {m_currentState})");
  721. m_canManager.SendCanFrame(heartbeatCobId, heartbeatData);
  722. }
  723. /// <summary>
  724. /// 发送无错误紧急报文 (EMCY)
  725. /// 用于在启动时表明设备无错误状态
  726. /// </summary>
  727. private void SendNoErrorEmcy()
  728. {
  729. byte[] emcyData = new byte[8];
  730. emcyData[0] = 0x00; // 错误代码低字节 - 无错误
  731. emcyData[1] = 0x00; // 错误代码高字节
  732. emcyData[2] = 0x00; // 错误寄存器
  733. uint emcyCobId = (uint)0x080 + (uint)m_nodeId;
  734. Console.WriteLine($"[EMCY] 发送无错误报文 - COB-ID: 0x{emcyCobId:X3}");
  735. m_canManager.SendCanFrame(emcyCobId, emcyData);
  736. }
  737. #endregion
  738. #region 辅助方法
  739. /// <summary>
  740. /// 获取RPDO的COB-ID
  741. /// </summary>
  742. private uint GetRpdoCobId(byte pdoNumber, byte nodeId)
  743. {
  744. switch (pdoNumber)
  745. {
  746. case 1: return (uint)0x200 + nodeId;
  747. case 2: return (uint)0x300 + nodeId;
  748. case 3: return (uint)0x400 + nodeId;
  749. case 4: return (uint)0x500 + nodeId;
  750. default: throw new ArgumentException("无效的PDO编号");
  751. }
  752. }
  753. /// <summary>
  754. /// 获取TPDO的COB-ID
  755. /// </summary>
  756. private uint GetTpdoCobId(byte pdoNumber, byte nodeId)
  757. {
  758. switch (pdoNumber)
  759. {
  760. case 1: return (uint)0x180 + nodeId;
  761. case 2: return (uint)0x280 + nodeId;
  762. case 3: return (uint)0x380 + nodeId;
  763. case 4: return (uint)0x480 + nodeId;
  764. default: throw new ArgumentException("无效的PDO编号");
  765. }
  766. }
  767. #endregion
  768. #region 资源管理
  769. /// <summary>
  770. /// 释放资源
  771. /// </summary>
  772. public void Dispose()
  773. {
  774. Stop();
  775. m_canManager?.Dispose();
  776. m_cancellationTokenSource?.Dispose();
  777. m_heartbeatTimer?.Dispose();
  778. }
  779. #endregion
  780. #region 公共属性
  781. /// <summary>
  782. /// 获取当前节点ID
  783. /// </summary>
  784. public byte NodeId => m_nodeId;
  785. /// <summary>
  786. /// 获取当前NMT状态
  787. /// </summary>
  788. public NmtState CurrentState => m_currentState;
  789. /// <summary>
  790. /// 获取是否正在运行
  791. /// </summary>
  792. public bool IsRunning => m_isRunning;
  793. #endregion
  794. }
  795. #region 数据结构定义
  796. /// <summary>
  797. /// NMT状态枚举
  798. /// </summary>
  799. public enum NmtState
  800. {
  801. Initializing = 0x00,
  802. PreOperational = 0x7F,
  803. Operational = 0x05,
  804. Stopped = 0x04
  805. }
  806. /// <summary>
  807. /// PDO配置
  808. /// </summary>
  809. public class PdoConfig
  810. {
  811. public uint CobId { get; set; }
  812. public byte TransmissionType { get; set; } // 0xFF=事件驱动, 1-240=同步周期
  813. public bool Enabled { get; set; }
  814. }
  815. /// <summary>
  816. /// PDO映射
  817. /// </summary>
  818. public class PdoMapping
  819. {
  820. public ushort Index { get; set; }
  821. public byte SubIndex { get; set; }
  822. public byte BitLength { get; set; }
  823. }
  824. /// <summary>
  825. /// PDO映射参数
  826. /// </summary>
  827. public class PdoMappingParam
  828. {
  829. public byte NumberOfEntries { get; set; }
  830. public uint[] Mappings { get; set; }
  831. }
  832. /// <summary>
  833. /// 身份对象 (1018h)
  834. /// </summary>
  835. public class IdentityObject
  836. {
  837. public uint VendorId { get; set; }
  838. public uint ProductCode { get; set; }
  839. public uint RevisionNumber { get; set; }
  840. }
  841. /// <summary>
  842. /// TPDO通信参数 (1800-1803h)
  843. /// </summary>
  844. public class TpdoCommParam
  845. {
  846. public uint CobId { get; set; }
  847. public byte TransmissionType { get; set; }
  848. }
  849. /// <summary>
  850. /// RPDO通信参数 (1400-1403h)
  851. /// </summary>
  852. public class RpdoCommParam
  853. {
  854. public uint CobId { get; set; }
  855. public byte TransmissionType { get; set; }
  856. }
  857. #endregion
  858. }