CanOpenMaster.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  1. using CanTest;
  2. using System;
  3. using System.Collections.Concurrent;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. namespace CCDCount.DLL.CanBus
  10. {
  11. /// <summary>
  12. /// CANopen主站管理器
  13. /// 负责管理多个从站节点,实现NMT控制、SDO读写、PDO通信等功能
  14. /// </summary>
  15. public class CanOpenMaster : IDisposable
  16. {
  17. #region 私有字段
  18. private CanOpenManager m_canManager;
  19. private Dictionary<byte, CanOpenNode> m_nodes;
  20. private Dictionary<byte, EdsDeviceInfo> m_edsDevices; // EDS设备信息缓存
  21. private Thread m_receiveThread;
  22. private volatile bool m_isRunning = false;
  23. private CancellationTokenSource m_cancellationTokenSource;
  24. // SYNC相关
  25. private Timer m_syncTimer;
  26. private byte m_syncCounter = 0;
  27. private bool m_syncEnabled = false;
  28. // 事件回调
  29. public event Action<byte, byte[]> OnTpdoReceived; // TPDO接收事件(节点ID, 数据)
  30. public event Action<byte, byte> OnHeartbeatReceived; // 心跳接收事件(节点ID, 状态)
  31. public event Action<byte, ushort> OnEmergencyReceived; // 紧急消息事件(节点ID, 错误代码)
  32. public event Action<string> OnLogMessage; // 日志消息事件
  33. #endregion
  34. #region 属性
  35. /// <summary>
  36. /// 已注册的节点列表
  37. /// </summary>
  38. public List<byte> NodeIds => m_nodes.Keys.ToList();
  39. /// <summary>
  40. /// 主站是否正在运行
  41. /// </summary>
  42. public bool IsRunning => m_isRunning;
  43. #endregion
  44. #region 构造函数和初始化
  45. /// <summary>
  46. /// 构造函数
  47. /// </summary>
  48. /// <param name="deviceType">设备类型(默认4=USBCAN2)</param>
  49. /// <param name="deviceIndex">设备索引</param>
  50. /// <param name="canIndex">CAN通道索引</param>
  51. public CanOpenMaster(UInt32 deviceType = 4, UInt32 deviceIndex = 0, UInt32 canIndex = 0)
  52. {
  53. m_canManager = new CanOpenManager(deviceType, deviceIndex, canIndex);
  54. m_nodes = new Dictionary<byte, CanOpenNode>();
  55. m_edsDevices = new Dictionary<byte, EdsDeviceInfo>();
  56. m_cancellationTokenSource = new CancellationTokenSource();
  57. Log("CANopen主站对象创建成功");
  58. }
  59. /// <summary>
  60. /// 初始化主站
  61. /// </summary>
  62. /// <param name="baudRate">波特率</param>
  63. /// <returns>是否成功</returns>
  64. public bool Initialize(CanBaudRate baudRate = CanBaudRate.BaudRate_500K)
  65. {
  66. try
  67. {
  68. Log($"正在初始化CANopen主站,波特率:{baudRate}...");
  69. if (!m_canManager.Initialize(baudRate))
  70. {
  71. Log("CAN设备初始化失败");
  72. return false;
  73. }
  74. // 启动接收线程
  75. m_isRunning = true;
  76. m_receiveThread = new Thread(ReceiveLoop);
  77. m_receiveThread.IsBackground = true;
  78. m_receiveThread.Start();
  79. Log("CANopen主站初始化成功");
  80. return true;
  81. }
  82. catch (Exception ex)
  83. {
  84. Log($"初始化失败: {ex.Message}");
  85. return false;
  86. }
  87. }
  88. #endregion
  89. #region EDS文件支持(可选)
  90. /// <summary>
  91. /// 为节点加载EDS文件(可选功能)
  92. /// </summary>
  93. /// <param name="nodeId">节点ID</param>
  94. /// <param name="edsFilePath">EDS文件路径</param>
  95. /// <returns>是否成功</returns>
  96. public bool LoadEdsFile(byte nodeId, string edsFilePath)
  97. {
  98. if (!System.IO.File.Exists(edsFilePath))
  99. {
  100. Log($"EDS文件不存在: {edsFilePath}");
  101. return false;
  102. }
  103. try
  104. {
  105. var edsInfo = ParseEdsFile(edsFilePath);
  106. if (edsInfo != null)
  107. {
  108. m_edsDevices[nodeId] = edsInfo;
  109. Log($"节点{nodeId} EDS文件加载成功: {edsInfo.DeviceName}");
  110. return true;
  111. }
  112. else
  113. {
  114. Log($"解析EDS文件失败: {edsFilePath}");
  115. return false;
  116. }
  117. }
  118. catch (Exception ex)
  119. {
  120. Log($"加载EDS文件异常: {ex.Message}");
  121. return false;
  122. }
  123. }
  124. /// <summary>
  125. /// 获取节点的EDS信息
  126. /// </summary>
  127. public EdsDeviceInfo GetEdsInfo(byte nodeId)
  128. {
  129. m_edsDevices.TryGetValue(nodeId, out var info);
  130. return info;
  131. }
  132. /// <summary>
  133. /// 根据EDS信息自动配置PDO映射
  134. /// </summary>
  135. public bool AutoConfigurePdoFromEds(byte nodeId)
  136. {
  137. var edsInfo = GetEdsInfo(nodeId);
  138. if (edsInfo == null)
  139. {
  140. Log($"节点{nodeId}没有EDS信息,无法自动配置PDO");
  141. return false;
  142. }
  143. try
  144. {
  145. // 配置TPDO传输类型
  146. foreach (var tpdo in edsInfo.TpdoMappings)
  147. {
  148. ConfigureTpdoTransmissionType(nodeId, tpdo.PdoNumber, tpdo.TransmissionType);
  149. Log($"配置TPDO{tpdo.PdoNumber}: 传输类型={tpdo.TransmissionType}");
  150. }
  151. // 配置RPDO
  152. foreach (var rpdo in edsInfo.RpdoMappings)
  153. {
  154. Log($"RPDO{rpdo.PdoNumber}已配置: {rpdo.MappedObjects.Count}个对象");
  155. }
  156. return true;
  157. }
  158. catch (Exception ex)
  159. {
  160. Log($"自动配置PDO失败: {ex.Message}");
  161. return false;
  162. }
  163. }
  164. /// <summary>
  165. /// 解析EDS文件(简化版)
  166. /// </summary>
  167. private EdsDeviceInfo ParseEdsFile(string filePath)
  168. {
  169. var info = new EdsDeviceInfo();
  170. info.DeviceName = System.IO.Path.GetFileNameWithoutExtension(filePath);
  171. // TODO: 实现完整的EDS解析逻辑
  172. // 这里提供一个简化版本,实际项目中可以使用现有的EdsFileGenerator反向解析
  173. var lines = System.IO.File.ReadAllLines(filePath);
  174. bool inDeviceInfo = false;
  175. foreach (var line in lines)
  176. {
  177. var trimmed = line.Trim();
  178. // 解析设备信息
  179. if (trimmed == "[DeviceInfo]")
  180. {
  181. inDeviceInfo = true;
  182. continue;
  183. }
  184. else if (trimmed.StartsWith("[") && inDeviceInfo)
  185. {
  186. inDeviceInfo = false;
  187. }
  188. if (inDeviceInfo)
  189. {
  190. if (trimmed.StartsWith("VendorName="))
  191. info.VendorName = trimmed.Substring(11);
  192. else if (trimmed.StartsWith("ProductName="))
  193. info.ProductName = trimmed.Substring(12);
  194. else if (trimmed.StartsWith("VendorNumber="))
  195. {
  196. if (uint.TryParse(trimmed.Substring(13), out uint vendorId))
  197. info.VendorId = vendorId;
  198. }
  199. else if (trimmed.StartsWith("ProductNumber="))
  200. {
  201. if (uint.TryParse(trimmed.Substring(14), out uint productCode))
  202. info.ProductCode = productCode;
  203. }
  204. }
  205. // 解析PDO映射
  206. if (trimmed.StartsWith("[1A00sub0]"))
  207. {
  208. // TPDO1映射对象数量
  209. }
  210. // ... 更多解析逻辑
  211. }
  212. return info;
  213. }
  214. #endregion
  215. #region 节点管理
  216. /// <summary>
  217. /// 添加从站节点
  218. /// </summary>
  219. /// <param name="nodeId">节点ID(1-127)</param>
  220. /// <param name="nodeName">节点名称(可选)</param>
  221. /// <param name="edsFilePath">EDS文件路径(可选)</param>
  222. /// <returns>是否成功</returns>
  223. public bool AddNode(byte nodeId, string nodeName = null, string edsFilePath = null)
  224. {
  225. if (nodeId == 0 || nodeId > 127)
  226. {
  227. Log($"无效的节点ID: {nodeId}");
  228. return false;
  229. }
  230. if (m_nodes.ContainsKey(nodeId))
  231. {
  232. Log($"节点{nodeId}已存在");
  233. return false;
  234. }
  235. var node = new CanOpenNode(nodeId, nodeName ?? $"Node_{nodeId}");
  236. m_nodes[nodeId] = node;
  237. // 如果提供了EDS文件,加载它
  238. if (!string.IsNullOrEmpty(edsFilePath))
  239. {
  240. LoadEdsFile(nodeId, edsFilePath);
  241. }
  242. Log($"添加节点: ID={nodeId}, Name={node.NodeName}" +
  243. (edsFilePath != null ? $", EDS={System.IO.Path.GetFileName(edsFilePath)}" : ""));
  244. return true;
  245. }
  246. /// <summary>
  247. /// 移除从站节点
  248. /// </summary>
  249. public bool RemoveNode(byte nodeId)
  250. {
  251. if (!m_nodes.ContainsKey(nodeId))
  252. {
  253. Log($"节点{nodeId}不存在");
  254. return false;
  255. }
  256. m_nodes.Remove(nodeId);
  257. Log($"移除节点: {nodeId}");
  258. return true;
  259. }
  260. /// <summary>
  261. /// 获取节点对象
  262. /// </summary>
  263. public CanOpenNode GetNode(byte nodeId)
  264. {
  265. if (m_nodes.TryGetValue(nodeId, out var node))
  266. {
  267. return node;
  268. }
  269. return null;
  270. }
  271. /// <summary>
  272. /// 批量添加节点
  273. /// </summary>
  274. public void AddNodes(byte[] nodeIds)
  275. {
  276. foreach (var nodeId in nodeIds)
  277. {
  278. AddNode(nodeId);
  279. }
  280. }
  281. #endregion
  282. #region NMT控制
  283. /// <summary>
  284. /// 启动所有节点
  285. /// </summary>
  286. public void StartAllNodes()
  287. {
  288. Log("启动所有节点...");
  289. foreach (var nodeId in m_nodes.Keys)
  290. {
  291. NmtStartNode(nodeId);
  292. Thread.Sleep(10); // 避免总线拥塞
  293. }
  294. }
  295. /// <summary>
  296. /// 停止所有节点
  297. /// </summary>
  298. public void StopAllNodes()
  299. {
  300. Log("停止所有节点...");
  301. foreach (var nodeId in m_nodes.Keys)
  302. {
  303. NmtStopNode(nodeId);
  304. Thread.Sleep(10);
  305. }
  306. }
  307. /// <summary>
  308. /// NMT启动指定节点
  309. /// </summary>
  310. public void NmtStartNode(byte nodeId)
  311. {
  312. if (!ValidateNodeId(nodeId)) return;
  313. m_canManager.NmtStartNode(nodeId);
  314. UpdateNodeState(nodeId, NodeState.Operational);
  315. Log($"NMT启动节点: {nodeId}");
  316. }
  317. /// <summary>
  318. /// NMT停止指定节点
  319. /// </summary>
  320. public void NmtStopNode(byte nodeId)
  321. {
  322. if (!ValidateNodeId(nodeId)) return;
  323. m_canManager.NmtStopNode(nodeId);
  324. UpdateNodeState(nodeId, NodeState.Stopped);
  325. Log($"NMT停止节点: {nodeId}");
  326. }
  327. /// <summary>
  328. /// NMT进入预操作状态
  329. /// </summary>
  330. public void NmtEnterPreOperational(byte nodeId)
  331. {
  332. if (!ValidateNodeId(nodeId)) return;
  333. m_canManager.NmtEnterPreOperational(nodeId);
  334. UpdateNodeState(nodeId, NodeState.PreOperational);
  335. Log($"NMT进入预操作状态: {nodeId}");
  336. }
  337. /// <summary>
  338. /// NMT重置节点
  339. /// </summary>
  340. public void NmtResetNode(byte nodeId)
  341. {
  342. if (!ValidateNodeId(nodeId)) return;
  343. m_canManager.NmtResetNode(nodeId);
  344. UpdateNodeState(nodeId, NodeState.Initializing);
  345. Log($"NMT重置节点: {nodeId}");
  346. }
  347. /// <summary>
  348. /// NMT重置通信
  349. /// </summary>
  350. public void NmtResetCommunication(byte nodeId)
  351. {
  352. if (!ValidateNodeId(nodeId)) return;
  353. m_canManager.NmtResetCommunication(nodeId);
  354. Log($"NMT重置通信: {nodeId}");
  355. }
  356. #endregion
  357. #region SDO通信
  358. /// <summary>
  359. /// SDO读取(快速,不等待响应)
  360. /// </summary>
  361. public void SdoReadQuick(byte nodeId, ushort index, byte subIndex)
  362. {
  363. if (!ValidateNodeId(nodeId)) return;
  364. m_canManager.SdoReadQuick(nodeId, index, subIndex);
  365. }
  366. /// <summary>
  367. /// SDO写入(快速,不等待响应)
  368. /// </summary>
  369. public void SdoWriteQuick(byte nodeId, ushort index, byte subIndex, uint data)
  370. {
  371. if (!ValidateNodeId(nodeId)) return;
  372. m_canManager.SdoWrite(nodeId, index, subIndex, data);
  373. }
  374. /// <summary>
  375. /// SDO读取并等待响应
  376. /// </summary>
  377. public byte[] SdoReadAndWait(byte nodeId, ushort index, byte subIndex, int timeoutMs = 1000)
  378. {
  379. if (!ValidateNodeId(nodeId)) return null;
  380. var response = m_canManager.SdoReadAndWait(nodeId, index, subIndex, timeoutMs);
  381. if (response != null)
  382. {
  383. var node = GetNode(nodeId);
  384. if (node != null)
  385. {
  386. node.LastSdoResponseTime = DateTime.Now;
  387. node.SdoSuccessCount++;
  388. }
  389. }
  390. else
  391. {
  392. var node = GetNode(nodeId);
  393. if (node != null)
  394. {
  395. node.SdoTimeoutCount++;
  396. }
  397. }
  398. return response;
  399. }
  400. /// <summary>
  401. /// SDO写入并等待确认
  402. /// </summary>
  403. public bool SdoWriteAndWait(byte nodeId, ushort index, byte subIndex, uint data, int timeoutMs = 1000)
  404. {
  405. if (!ValidateNodeId(nodeId)) return false;
  406. var success = m_canManager.SdoWriteAndWait(nodeId, index, subIndex, data, timeoutMs);
  407. var node = GetNode(nodeId);
  408. if (node != null)
  409. {
  410. node.LastSdoResponseTime = DateTime.Now;
  411. if (success)
  412. node.SdoSuccessCount++;
  413. else
  414. node.SdoTimeoutCount++;
  415. }
  416. return success;
  417. }
  418. /// <summary>
  419. /// 读取设备类型
  420. /// </summary>
  421. public uint ReadDeviceType(byte nodeId)
  422. {
  423. var response = SdoReadAndWait(nodeId, 0x1000, 0x00);
  424. if (response != null && response.Length >= 4)
  425. {
  426. return BitConverter.ToUInt32(response, 4);
  427. }
  428. return 0;
  429. }
  430. /// <summary>
  431. /// 读取厂商ID
  432. /// </summary>
  433. public uint ReadVendorId(byte nodeId)
  434. {
  435. var response = SdoReadAndWait(nodeId, 0x1018, 0x01);
  436. if (response != null && response.Length >= 4)
  437. {
  438. return BitConverter.ToUInt32(response, 4);
  439. }
  440. return 0;
  441. }
  442. /// <summary>
  443. /// 读取产品代码
  444. /// </summary>
  445. public uint ReadProductCode(byte nodeId)
  446. {
  447. var response = SdoReadAndWait(nodeId, 0x1018, 0x02);
  448. if (response != null && response.Length >= 4)
  449. {
  450. return BitConverter.ToUInt32(response, 4);
  451. }
  452. return 0;
  453. }
  454. /// <summary>
  455. /// 读取序列号
  456. /// </summary>
  457. public uint ReadSerialNumber(byte nodeId)
  458. {
  459. var response = SdoReadAndWait(nodeId, 0x1018, 0x04);
  460. if (response != null && response.Length >= 4)
  461. {
  462. return BitConverter.ToUInt32(response, 4);
  463. }
  464. return 0;
  465. }
  466. #endregion
  467. #region PDO通信
  468. /// <summary>
  469. /// 发送SYNC帧
  470. /// </summary>
  471. public void SendSync(byte counter = 0)
  472. {
  473. m_canManager.SendSync(counter);
  474. }
  475. /// <summary>
  476. /// 启用周期性SYNC
  477. /// </summary>
  478. /// <param name="intervalMs">SYNC周期(毫秒)</param>
  479. public void EnableSync(int intervalMs = 10)
  480. {
  481. if (m_syncTimer != null)
  482. {
  483. m_syncTimer.Dispose();
  484. }
  485. m_syncEnabled = true;
  486. m_syncCounter = 0;
  487. m_syncTimer = new Timer(state =>
  488. {
  489. if (m_syncEnabled)
  490. {
  491. m_syncCounter++;
  492. SendSync(m_syncCounter);
  493. }
  494. }, null, 0, intervalMs);
  495. Log($"启用周期性SYNC,周期:{intervalMs}ms");
  496. }
  497. /// <summary>
  498. /// 禁用SYNC
  499. /// </summary>
  500. public void DisableSync()
  501. {
  502. m_syncEnabled = false;
  503. m_syncTimer?.Dispose();
  504. m_syncTimer = null;
  505. Log("禁用SYNC");
  506. }
  507. /// <summary>
  508. /// 配置TPDO传输类型
  509. /// </summary>
  510. public void ConfigureTpdoTransmissionType(byte nodeId, byte pdoNumber, byte transmissionType)
  511. {
  512. if (!ValidateNodeId(nodeId)) return;
  513. m_canManager.ConfigureTpdoTransmissionType(nodeId, pdoNumber, transmissionType);
  514. }
  515. /// <summary>
  516. /// 触发节点发送TPDO
  517. /// </summary>
  518. public void TriggerTpdo(byte nodeId, byte pdoNumber)
  519. {
  520. if (!ValidateNodeId(nodeId)) return;
  521. // 通过SDO设置TPDO传输类型为"事件驱动"
  522. ushort index = (ushort)(0x1800 + pdoNumber - 1);
  523. SdoWriteAndWait(nodeId, index, 0x02, 0xFF); // 0xFF表示事件驱动
  524. Log($"触发节点{nodeId}发送TPDO{pdoNumber}");
  525. }
  526. /// <summary>
  527. /// 主站向从站发送RPDO数据(写入PDO)
  528. /// </summary>
  529. /// <param name="nodeId">目标节点ID</param>
  530. /// <param name="pdoNumber">RPDO编号(1-4)</param>
  531. /// <param name="data">要发送的数据</param>
  532. /// <returns>是否成功</returns>
  533. public bool SendRpdo(byte nodeId, byte pdoNumber, byte[] data)
  534. {
  535. if (!ValidateNodeId(nodeId)) return false;
  536. if (pdoNumber < 1 || pdoNumber > 4)
  537. {
  538. Log($"无效的RPDO编号: {pdoNumber}");
  539. return false;
  540. }
  541. if (data == null || data.Length > 8)
  542. {
  543. Log("RPDO数据无效(必须1-8字节)");
  544. return false;
  545. }
  546. // 计算RPDO的COB-ID
  547. uint cobId = GetRpdoCobId(pdoNumber, nodeId);
  548. // 发送CAN帧
  549. bool success = m_canManager.SendCanFrame(cobId, data);
  550. if (success)
  551. {
  552. var node = GetNode(nodeId);
  553. if (node != null)
  554. {
  555. node.LastTpdoTime[pdoNumber] = DateTime.Now;
  556. }
  557. Log($"发送RPDO{pdoNumber}到节点{nodeId}: COB-ID=0x{cobId:X3}, 数据长度={data.Length}");
  558. }
  559. else
  560. {
  561. Log($"发送RPDO{pdoNumber}失败");
  562. }
  563. return success;
  564. }
  565. /// <summary>
  566. /// 发送RPDO并等待从站确认(通过心跳或SDO响应)
  567. /// </summary>
  568. public bool SendRpdoAndWait(byte nodeId, byte pdoNumber, byte[] data, int timeoutMs = 100)
  569. {
  570. if (!SendRpdo(nodeId, pdoNumber, data))
  571. return false;
  572. // 等待一段时间,让从站处理数据
  573. Thread.Sleep(timeoutMs);
  574. // 检查从站是否在线(有心跳)
  575. var node = GetNode(nodeId);
  576. if (node != null)
  577. {
  578. return node.IsOnline(timeoutMs * 2);
  579. }
  580. return true;
  581. }
  582. /// <summary>
  583. /// 配置RPDO映射(通过SDO修改从站的对象字典)
  584. /// </summary>
  585. /// <param name="nodeId">节点ID</param>
  586. /// <param name="pdoNumber">RPDO编号(1-4)</param>
  587. /// <param name="mappedObjects">映射对象列表</param>
  588. /// <returns>是否成功</returns>
  589. public bool ConfigureRpdoMapping(byte nodeId, byte pdoNumber, List<MappedObject> mappedObjects)
  590. {
  591. if (!ValidateNodeId(nodeId)) return false;
  592. if (pdoNumber < 1 || pdoNumber > 4)
  593. {
  594. Log($"无效的RPDO编号: {pdoNumber}");
  595. return false;
  596. }
  597. try
  598. {
  599. ushort mappingIndex = (ushort)(0x1600 + pdoNumber - 1); // RPDO映射索引
  600. // 1. 先禁用RPDO(设置COB-ID最高位为1)
  601. ushort commIndex = (ushort)(0x1400 + pdoNumber - 1);
  602. uint currentCobId = BitConverter.ToUInt32(SdoReadAndWait(nodeId, commIndex, 0x01), 4);
  603. uint disabledCobId = currentCobId | 0x80000000;
  604. SdoWriteAndWait(nodeId, commIndex, 0x01, disabledCobId);
  605. // 2. 设置映射对象数量
  606. SdoWriteAndWait(nodeId, mappingIndex, 0x00, (uint)mappedObjects.Count);
  607. // 3. 配置每个映射对象
  608. for (int i = 0; i < mappedObjects.Count; i++)
  609. {
  610. var obj = mappedObjects[i];
  611. byte subIndex = (byte)(i + 1);
  612. // 打包映射条目: 0xIIIISSLL
  613. uint mappingEntry = ((uint)obj.Index << 16) |
  614. ((uint)obj.SubIndex << 8) |
  615. (uint)obj.BitLength;
  616. SdoWriteAndWait(nodeId, mappingIndex, subIndex, mappingEntry);
  617. Log($"配置RPDO{pdoNumber}映射[{subIndex}]: 0x{obj.Index:X4}:{obj.SubIndex:X2} ({obj.BitLength}位)");
  618. }
  619. // 4. 重新启用RPDO(清除COB-ID最高位)
  620. uint enabledCobId = currentCobId & 0x7FFFFFFF;
  621. SdoWriteAndWait(nodeId, commIndex, 0x01, enabledCobId);
  622. Log($"RPDO{pdoNumber}映射配置完成: {mappedObjects.Count}个对象");
  623. return true;
  624. }
  625. catch (Exception ex)
  626. {
  627. Log($"配置RPDO映射失败: {ex.Message}");
  628. return false;
  629. }
  630. }
  631. /// <summary>
  632. /// 配置TPDO映射(通过SDO修改从站的对象字典)
  633. /// </summary>
  634. public bool ConfigureTpdoMapping(byte nodeId, byte pdoNumber, List<MappedObject> mappedObjects)
  635. {
  636. if (!ValidateNodeId(nodeId)) return false;
  637. if (pdoNumber < 1 || pdoNumber > 4)
  638. {
  639. Log($"无效的TPDO编号: {pdoNumber}");
  640. return false;
  641. }
  642. try
  643. {
  644. ushort mappingIndex = (ushort)(0x1A00 + pdoNumber - 1); // TPDO映射索引
  645. // 1. 先禁用TPDO
  646. ushort commIndex = (ushort)(0x1800 + pdoNumber - 1);
  647. uint currentCobId = BitConverter.ToUInt32(SdoReadAndWait(nodeId, commIndex, 0x01), 4);
  648. uint disabledCobId = currentCobId | 0x80000000;
  649. SdoWriteAndWait(nodeId, commIndex, 0x01, disabledCobId);
  650. // 2. 设置映射对象数量
  651. SdoWriteAndWait(nodeId, mappingIndex, 0x00, (uint)mappedObjects.Count);
  652. // 3. 配置每个映射对象
  653. for (int i = 0; i < mappedObjects.Count; i++)
  654. {
  655. var obj = mappedObjects[i];
  656. byte subIndex = (byte)(i + 1);
  657. uint mappingEntry = ((uint)obj.Index << 16) |
  658. ((uint)obj.SubIndex << 8) |
  659. (uint)obj.BitLength;
  660. SdoWriteAndWait(nodeId, mappingIndex, subIndex, mappingEntry);
  661. }
  662. // 4. 重新启用TPDO
  663. uint enabledCobId = currentCobId & 0x7FFFFFFF;
  664. SdoWriteAndWait(nodeId, commIndex, 0x01, enabledCobId);
  665. Log($"TPDO{pdoNumber}映射配置完成: {mappedObjects.Count}个对象");
  666. return true;
  667. }
  668. catch (Exception ex)
  669. {
  670. Log($"配置TPDO映射失败: {ex.Message}");
  671. return false;
  672. }
  673. }
  674. /// <summary>
  675. /// 获取RPDO的COB-ID
  676. /// </summary>
  677. private uint GetRpdoCobId(byte pdoNumber, byte nodeId)
  678. {
  679. switch (pdoNumber)
  680. {
  681. case 1: return (uint)(0x200 + nodeId); // RPDO1
  682. case 2: return (uint)(0x300 + nodeId); // RPDO2
  683. case 3: return (uint)(0x400 + nodeId); // RPDO3
  684. case 4: return (uint)(0x500 + nodeId); // RPDO4
  685. default: throw new ArgumentException("无效的RPDO编号");
  686. }
  687. }
  688. #endregion
  689. #region 接收处理线程
  690. private void ReceiveLoop()
  691. {
  692. Log("接收线程启动");
  693. while (m_isRunning && !m_cancellationTokenSource.Token.IsCancellationRequested)
  694. {
  695. try
  696. {
  697. var frames = m_canManager.ReceiveCanFrames(100);
  698. foreach (var frame in frames)
  699. {
  700. ProcessCanOpenFrame(frame);
  701. }
  702. }
  703. catch (Exception ex)
  704. {
  705. Log($"接收线程异常: {ex.Message}");
  706. }
  707. }
  708. Log("接收线程退出");
  709. }
  710. private void ProcessCanOpenFrame(CanOpenFrame frame)
  711. {
  712. try
  713. {
  714. // 解析COB-ID功能码
  715. byte functionCode = (byte)((frame.CobId >> 7) & 0x0F);
  716. byte nodeId = (byte)(frame.CobId & 0x7F);
  717. switch (functionCode)
  718. {
  719. case 0x00: // NMT
  720. ProcessNmtFrame(frame);
  721. break;
  722. case 0x01: // SYNC
  723. // SYNC通常由主站发送,这里忽略
  724. break;
  725. case 0x02: // EMCY (紧急消息)
  726. ProcessEmergencyFrame(nodeId, frame.Data);
  727. break;
  728. case 0x03: // TIME (时间戳对象)
  729. // 可选功能,暂不处理
  730. break;
  731. case 0x05: // EMCY (扩展)
  732. ProcessEmergencyFrame(nodeId, frame.Data);
  733. break;
  734. case 0x07: // Heartbeat / NMT Error Control
  735. ProcessHeartbeatFrame(nodeId, frame.Data);
  736. break;
  737. case 0x08: // SDO Response
  738. ProcessSdoResponse(nodeId, frame.Data);
  739. break;
  740. case 0x09: // SDO Request (主站通常不接收)
  741. break;
  742. default:
  743. // PDO (0x0B-0x0F for TPDO, 0x13-0x17 for RPDO)
  744. if (functionCode >= 0x0B && functionCode <= 0x0F)
  745. {
  746. ProcessTpdoFrame(nodeId, functionCode, frame.Data);
  747. }
  748. break;
  749. }
  750. }
  751. catch (Exception ex)
  752. {
  753. Log($"处理帧异常: {ex.Message}");
  754. }
  755. }
  756. private void ProcessNmtFrame(CanOpenFrame frame)
  757. {
  758. // NMT通常由主站发送,这里可以记录从站的NMT响应(如果有)
  759. }
  760. private void ProcessEmergencyFrame(byte nodeId, byte[] data)
  761. {
  762. if (data.Length >= 2)
  763. {
  764. ushort errorCode = m_canManager.ParseEmergencyMessage(data);
  765. OnEmergencyReceived?.Invoke(nodeId, errorCode);
  766. var node = GetNode(nodeId);
  767. if (node != null)
  768. {
  769. node.CurrentErrorRegister = data.Length > 2 ? data[2] : (byte)0;
  770. node.LastEmergencyTime = DateTime.Now;
  771. node.EmergencyCount++;
  772. }
  773. Log($"收到紧急消息 - 节点{nodeId}: 错误代码0x{errorCode:X4}");
  774. }
  775. }
  776. private void ProcessHeartbeatFrame(byte nodeId, byte[] data)
  777. {
  778. if (data.Length >= 1)
  779. {
  780. byte status = m_canManager.ParseHeartbeat(nodeId, data);
  781. OnHeartbeatReceived?.Invoke(nodeId, status);
  782. var node = GetNode(nodeId);
  783. if (node != null)
  784. {
  785. node.CurrentState = (NodeState)(status & 0x7F);
  786. node.LastHeartbeatTime = DateTime.Now;
  787. node.HeartbeatCount++;
  788. }
  789. }
  790. }
  791. private void ProcessSdoResponse(byte nodeId, byte[] data)
  792. {
  793. // SDO响应已在CanOpenManager中处理
  794. // 这里可以做额外的日志记录
  795. }
  796. private void ProcessTpdoFrame(byte nodeId, byte functionCode, byte[] data)
  797. {
  798. byte pdoNumber = (byte)(functionCode - 0x0A); // TPDO1=1, TPDO2=2, ...
  799. OnTpdoReceived?.Invoke(nodeId, data);
  800. var node = GetNode(nodeId);
  801. if (node != null)
  802. {
  803. node.LastTpdoTime[pdoNumber] = DateTime.Now;
  804. node.TpdoCount[pdoNumber]++;
  805. // 存储最新TPDO数据
  806. node.LastTpdoData[pdoNumber] = data;
  807. }
  808. }
  809. #endregion
  810. #region 辅助方法
  811. private bool ValidateNodeId(byte nodeId)
  812. {
  813. if (nodeId == 0 || nodeId > 127)
  814. {
  815. Log($"无效的节点ID: {nodeId}");
  816. return false;
  817. }
  818. if (!m_nodes.ContainsKey(nodeId))
  819. {
  820. Log($"节点{nodeId}未注册");
  821. return false;
  822. }
  823. return true;
  824. }
  825. private void UpdateNodeState(byte nodeId, NodeState state)
  826. {
  827. var node = GetNode(nodeId);
  828. if (node != null)
  829. {
  830. node.CurrentState = state;
  831. node.LastStateChangeTime = DateTime.Now;
  832. }
  833. }
  834. private void Log(string message)
  835. {
  836. string logMsg = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
  837. Console.WriteLine(logMsg);
  838. OnLogMessage?.Invoke(logMsg);
  839. }
  840. #endregion
  841. #region 资源管理
  842. /// <summary>
  843. /// 关闭主站
  844. /// </summary>
  845. public void Close()
  846. {
  847. Log("正在关闭CANopen主站...");
  848. // 停止接收线程
  849. m_isRunning = false;
  850. m_cancellationTokenSource?.Cancel();
  851. if (m_receiveThread != null && m_receiveThread.IsAlive)
  852. {
  853. m_receiveThread.Join(1000);
  854. }
  855. // 禁用SYNC
  856. DisableSync();
  857. // 关闭CAN设备
  858. m_canManager?.Close();
  859. Log("CANopen主站已关闭");
  860. }
  861. /// <summary>
  862. /// 释放资源
  863. /// </summary>
  864. public void Dispose()
  865. {
  866. Close();
  867. m_cancellationTokenSource?.Dispose();
  868. m_syncTimer?.Dispose();
  869. }
  870. #endregion
  871. }
  872. /// <summary>
  873. /// CANopen从站节点信息
  874. /// </summary>
  875. public class CanOpenNode
  876. {
  877. #region 属性
  878. /// <summary>
  879. /// 节点ID
  880. /// </summary>
  881. public byte NodeId { get; private set; }
  882. /// <summary>
  883. /// 节点名称
  884. /// </summary>
  885. public string NodeName { get; private set; }
  886. /// <summary>
  887. /// 当前状态
  888. /// </summary>
  889. public NodeState CurrentState { get; set; }
  890. /// <summary>
  891. /// 最后心跳时间
  892. /// </summary>
  893. public DateTime LastHeartbeatTime { get; set; }
  894. /// <summary>
  895. /// 心跳计数
  896. /// </summary>
  897. public uint HeartbeatCount { get; set; }
  898. /// <summary>
  899. /// 最后TPDO时间
  900. /// </summary>
  901. public Dictionary<byte, DateTime> LastTpdoTime { get; private set; }
  902. /// <summary>
  903. /// TPDO计数
  904. /// </summary>
  905. public Dictionary<byte, uint> TpdoCount { get; private set; }
  906. /// <summary>
  907. /// 最后TPDO数据
  908. /// </summary>
  909. public Dictionary<byte, byte[]> LastTpdoData { get; private set; }
  910. /// <summary>
  911. /// 最后SDO响应时间
  912. /// </summary>
  913. public DateTime LastSdoResponseTime { get; set; }
  914. /// <summary>
  915. /// SDO成功计数
  916. /// </summary>
  917. public uint SdoSuccessCount { get; set; }
  918. /// <summary>
  919. /// SDO超时计数
  920. /// </summary>
  921. public uint SdoTimeoutCount { get; set; }
  922. /// <summary>
  923. /// 最后紧急消息时间
  924. /// </summary>
  925. public DateTime LastEmergencyTime { get; set; }
  926. /// <summary>
  927. /// 紧急消息计数
  928. /// </summary>
  929. public uint EmergencyCount { get; set; }
  930. /// <summary>
  931. /// 当前错误寄存器
  932. /// </summary>
  933. public byte CurrentErrorRegister { get; set; }
  934. /// <summary>
  935. /// 最后状态改变时间
  936. /// </summary>
  937. public DateTime LastStateChangeTime { get; set; }
  938. #endregion
  939. #region 构造函数
  940. public CanOpenNode(byte nodeId, string nodeName)
  941. {
  942. NodeId = nodeId;
  943. NodeName = nodeName;
  944. CurrentState = NodeState.Unknown;
  945. LastTpdoTime = new Dictionary<byte, DateTime>();
  946. TpdoCount = new Dictionary<byte, uint>();
  947. LastTpdoData = new Dictionary<byte, byte[]>();
  948. // 初始化4个TPDO
  949. for (byte i = 1; i <= 4; i++)
  950. {
  951. LastTpdoTime[i] = DateTime.MinValue;
  952. TpdoCount[i] = 0;
  953. LastTpdoData[i] = null;
  954. }
  955. }
  956. #endregion
  957. #region 方法
  958. /// <summary>
  959. /// 检查节点是否在线(基于心跳超时判断)
  960. /// </summary>
  961. /// <param name="timeoutMs">超时时间(毫秒)</param>
  962. public bool IsOnline(int timeoutMs = 1000)
  963. {
  964. if (LastHeartbeatTime == DateTime.MinValue)
  965. return false;
  966. return (DateTime.Now - LastHeartbeatTime).TotalMilliseconds < timeoutMs;
  967. }
  968. /// <summary>
  969. /// 获取节点状态字符串
  970. /// </summary>
  971. public string GetStateString()
  972. {
  973. switch (CurrentState)
  974. {
  975. case NodeState.Initializing: return "Initializing";
  976. case NodeState.Disconnected: return "Disconnected";
  977. case NodeState.Connecting: return "Connecting";
  978. case NodeState.PreOperational: return "Pre-Operational";
  979. case NodeState.Operational: return "Operational";
  980. case NodeState.Stopped: return "Stopped";
  981. default: return "Unknown";
  982. }
  983. }
  984. /// <summary>
  985. /// 获取节点统计信息
  986. /// </summary>
  987. public override string ToString()
  988. {
  989. return $"Node {NodeId} ({NodeName}): State={GetStateString()}, " +
  990. $"Heartbeats={HeartbeatCount}, " +
  991. $"SDO(Success={SdoSuccessCount}, Timeout={SdoTimeoutCount}), " +
  992. $"Emergencies={EmergencyCount}";
  993. }
  994. #endregion
  995. }
  996. /// <summary>
  997. /// 节点状态枚举
  998. /// </summary>
  999. public enum NodeState
  1000. {
  1001. Unknown = 0x00,
  1002. Initializing = 0x00,
  1003. Disconnected = 0x01,
  1004. Connecting = 0x02,
  1005. PreOperational = 0x7F,
  1006. Operational = 0x05,
  1007. Stopped = 0x04
  1008. }
  1009. /// <summary>
  1010. /// EDS设备信息(简化版)
  1011. /// </summary>
  1012. public class EdsDeviceInfo
  1013. {
  1014. /// <summary>
  1015. /// 设备名称
  1016. /// </summary>
  1017. public string DeviceName { get; set; }
  1018. /// <summary>
  1019. /// 厂商名称
  1020. /// </summary>
  1021. public string VendorName { get; set; }
  1022. /// <summary>
  1023. /// 产品名称
  1024. /// </summary>
  1025. public string ProductName { get; set; }
  1026. /// <summary>
  1027. /// 厂商ID
  1028. /// </summary>
  1029. public uint VendorId { get; set; }
  1030. /// <summary>
  1031. /// 产品代码
  1032. /// </summary>
  1033. public uint ProductCode { get; set; }
  1034. /// <summary>
  1035. /// 设备类型
  1036. /// </summary>
  1037. public uint DeviceType { get; set; }
  1038. /// <summary>
  1039. /// TPDO映射配置
  1040. /// </summary>
  1041. public List<PdoMappingConfig> TpdoMappings { get; set; } = new List<PdoMappingConfig>();
  1042. /// <summary>
  1043. /// RPDO映射配置
  1044. /// </summary>
  1045. public List<PdoMappingConfig> RpdoMappings { get; set; } = new List<PdoMappingConfig>();
  1046. /// <summary>
  1047. /// 对象字典条目
  1048. /// </summary>
  1049. public Dictionary<ushort, ObjectDictionaryEntry> ObjectDictionary { get; set; }
  1050. = new Dictionary<ushort, ObjectDictionaryEntry>();
  1051. public override string ToString()
  1052. {
  1053. return $"{DeviceName} (Vendor={VendorName}, Product={ProductName})";
  1054. }
  1055. }
  1056. /// <summary>
  1057. /// PDO映射配置
  1058. /// </summary>
  1059. public class PdoMappingConfig
  1060. {
  1061. /// <summary>
  1062. /// PDO编号(1-4)
  1063. /// </summary>
  1064. public byte PdoNumber { get; set; }
  1065. /// <summary>
  1066. /// COB-ID
  1067. /// </summary>
  1068. public uint CobId { get; set; }
  1069. /// <summary>
  1070. /// 传输类型
  1071. /// 0=禁止, 1=同步周期, 254=远程请求, 255=事件驱动
  1072. /// </summary>
  1073. public byte TransmissionType { get; set; }
  1074. /// <summary>
  1075. /// 映射的对象列表
  1076. /// </summary>
  1077. public List<MappedObject> MappedObjects { get; set; } = new List<MappedObject>();
  1078. public override string ToString()
  1079. {
  1080. return $"PDO{PdoNumber}: COB-ID=0x{CobId:X3}, Type={TransmissionType}, Objects={MappedObjects.Count}";
  1081. }
  1082. }
  1083. /// <summary>
  1084. /// 映射对象
  1085. /// </summary>
  1086. public class MappedObject
  1087. {
  1088. /// <summary>
  1089. /// 对象索引
  1090. /// </summary>
  1091. public ushort Index { get; set; }
  1092. /// <summary>
  1093. /// 子索引
  1094. /// </summary>
  1095. public byte SubIndex { get; set; }
  1096. /// <summary>
  1097. /// 数据长度(位)
  1098. /// </summary>
  1099. public byte BitLength { get; set; }
  1100. public override string ToString()
  1101. {
  1102. return $"0x{Index:X4}:{SubIndex:X2} ({BitLength} bits)";
  1103. }
  1104. }
  1105. /// <summary>
  1106. /// 对象字典条目
  1107. /// </summary>
  1108. public class ObjectDictionaryEntry
  1109. {
  1110. /// <summary>
  1111. /// 索引
  1112. /// </summary>
  1113. public ushort Index { get; set; }
  1114. /// <summary>
  1115. /// 对象名称
  1116. /// </summary>
  1117. public string Name { get; set; }
  1118. /// <summary>
  1119. /// 对象类型(VAR/RECORD/ARRAY)
  1120. /// </summary>
  1121. public string ObjectType { get; set; }
  1122. /// <summary>
  1123. /// 数据类型
  1124. /// </summary>
  1125. public string DataType { get; set; }
  1126. /// <summary>
  1127. /// 访问权限(ro/rw/wo)
  1128. /// </summary>
  1129. public string AccessType { get; set; }
  1130. /// <summary>
  1131. /// 默认值
  1132. /// </summary>
  1133. public string DefaultValue { get; set; }
  1134. /// <summary>
  1135. /// 子对象(如果是RECORD或ARRAY)
  1136. /// </summary>
  1137. public Dictionary<byte, ObjectDictionaryEntry> SubObjects { get; set; }
  1138. = new Dictionary<byte, ObjectDictionaryEntry>();
  1139. public override string ToString()
  1140. {
  1141. return $"0x{Index:X4} {Name} ({ObjectType}, {DataType}, {AccessType})";
  1142. }
  1143. }
  1144. }