| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365 |
- using CanTest;
- using System;
- using System.Collections.Concurrent;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- namespace CCDCount.DLL.CanBus
- {
- /// <summary>
- /// CANopen主站管理器
- /// 负责管理多个从站节点,实现NMT控制、SDO读写、PDO通信等功能
- /// </summary>
- public class CanOpenMaster : IDisposable
- {
- #region 私有字段
-
- private CanOpenManager m_canManager;
- private Dictionary<byte, CanOpenNode> m_nodes;
- private Dictionary<byte, EdsDeviceInfo> m_edsDevices; // EDS设备信息缓存
- private Thread m_receiveThread;
- private volatile bool m_isRunning = false;
- private CancellationTokenSource m_cancellationTokenSource;
-
- // SYNC相关
- private Timer m_syncTimer;
- private byte m_syncCounter = 0;
- private bool m_syncEnabled = false;
-
- // 事件回调
- public event Action<byte, byte[]> OnTpdoReceived; // TPDO接收事件(节点ID, 数据)
- public event Action<byte, byte> OnHeartbeatReceived; // 心跳接收事件(节点ID, 状态)
- public event Action<byte, ushort> OnEmergencyReceived; // 紧急消息事件(节点ID, 错误代码)
- public event Action<string> OnLogMessage; // 日志消息事件
-
- #endregion
-
- #region 属性
-
- /// <summary>
- /// 已注册的节点列表
- /// </summary>
- public List<byte> NodeIds => m_nodes.Keys.ToList();
-
- /// <summary>
- /// 主站是否正在运行
- /// </summary>
- public bool IsRunning => m_isRunning;
-
- #endregion
-
- #region 构造函数和初始化
-
- /// <summary>
- /// 构造函数
- /// </summary>
- /// <param name="deviceType">设备类型(默认4=USBCAN2)</param>
- /// <param name="deviceIndex">设备索引</param>
- /// <param name="canIndex">CAN通道索引</param>
- public CanOpenMaster(UInt32 deviceType = 4, UInt32 deviceIndex = 0, UInt32 canIndex = 0)
- {
- m_canManager = new CanOpenManager(deviceType, deviceIndex, canIndex);
- m_nodes = new Dictionary<byte, CanOpenNode>();
- m_edsDevices = new Dictionary<byte, EdsDeviceInfo>();
- m_cancellationTokenSource = new CancellationTokenSource();
-
- Log("CANopen主站对象创建成功");
- }
-
- /// <summary>
- /// 初始化主站
- /// </summary>
- /// <param name="baudRate">波特率</param>
- /// <returns>是否成功</returns>
- public bool Initialize(CanBaudRate baudRate = CanBaudRate.BaudRate_500K)
- {
- try
- {
- Log($"正在初始化CANopen主站,波特率:{baudRate}...");
-
- if (!m_canManager.Initialize(baudRate))
- {
- Log("CAN设备初始化失败");
- return false;
- }
-
- // 启动接收线程
- m_isRunning = true;
- m_receiveThread = new Thread(ReceiveLoop);
- m_receiveThread.IsBackground = true;
- m_receiveThread.Start();
-
- Log("CANopen主站初始化成功");
- return true;
- }
- catch (Exception ex)
- {
- Log($"初始化失败: {ex.Message}");
- return false;
- }
- }
-
- #endregion
-
- #region EDS文件支持(可选)
-
- /// <summary>
- /// 为节点加载EDS文件(可选功能)
- /// </summary>
- /// <param name="nodeId">节点ID</param>
- /// <param name="edsFilePath">EDS文件路径</param>
- /// <returns>是否成功</returns>
- public bool LoadEdsFile(byte nodeId, string edsFilePath)
- {
- if (!System.IO.File.Exists(edsFilePath))
- {
- Log($"EDS文件不存在: {edsFilePath}");
- return false;
- }
-
- try
- {
- var edsInfo = ParseEdsFile(edsFilePath);
- if (edsInfo != null)
- {
- m_edsDevices[nodeId] = edsInfo;
- Log($"节点{nodeId} EDS文件加载成功: {edsInfo.DeviceName}");
- return true;
- }
- else
- {
- Log($"解析EDS文件失败: {edsFilePath}");
- return false;
- }
- }
- catch (Exception ex)
- {
- Log($"加载EDS文件异常: {ex.Message}");
- return false;
- }
- }
-
- /// <summary>
- /// 获取节点的EDS信息
- /// </summary>
- public EdsDeviceInfo GetEdsInfo(byte nodeId)
- {
- m_edsDevices.TryGetValue(nodeId, out var info);
- return info;
- }
-
- /// <summary>
- /// 根据EDS信息自动配置PDO映射
- /// </summary>
- public bool AutoConfigurePdoFromEds(byte nodeId)
- {
- var edsInfo = GetEdsInfo(nodeId);
- if (edsInfo == null)
- {
- Log($"节点{nodeId}没有EDS信息,无法自动配置PDO");
- return false;
- }
-
- try
- {
- // 配置TPDO传输类型
- foreach (var tpdo in edsInfo.TpdoMappings)
- {
- ConfigureTpdoTransmissionType(nodeId, tpdo.PdoNumber, tpdo.TransmissionType);
- Log($"配置TPDO{tpdo.PdoNumber}: 传输类型={tpdo.TransmissionType}");
- }
-
- // 配置RPDO
- foreach (var rpdo in edsInfo.RpdoMappings)
- {
- Log($"RPDO{rpdo.PdoNumber}已配置: {rpdo.MappedObjects.Count}个对象");
- }
-
- return true;
- }
- catch (Exception ex)
- {
- Log($"自动配置PDO失败: {ex.Message}");
- return false;
- }
- }
-
- /// <summary>
- /// 解析EDS文件(简化版)
- /// </summary>
- private EdsDeviceInfo ParseEdsFile(string filePath)
- {
- var info = new EdsDeviceInfo();
- info.DeviceName = System.IO.Path.GetFileNameWithoutExtension(filePath);
-
- // TODO: 实现完整的EDS解析逻辑
- // 这里提供一个简化版本,实际项目中可以使用现有的EdsFileGenerator反向解析
-
- var lines = System.IO.File.ReadAllLines(filePath);
- bool inDeviceInfo = false;
-
- foreach (var line in lines)
- {
- var trimmed = line.Trim();
-
- // 解析设备信息
- if (trimmed == "[DeviceInfo]")
- {
- inDeviceInfo = true;
- continue;
- }
- else if (trimmed.StartsWith("[") && inDeviceInfo)
- {
- inDeviceInfo = false;
- }
-
- if (inDeviceInfo)
- {
- if (trimmed.StartsWith("VendorName="))
- info.VendorName = trimmed.Substring(11);
- else if (trimmed.StartsWith("ProductName="))
- info.ProductName = trimmed.Substring(12);
- else if (trimmed.StartsWith("VendorNumber="))
- {
- if (uint.TryParse(trimmed.Substring(13), out uint vendorId))
- info.VendorId = vendorId;
- }
- else if (trimmed.StartsWith("ProductNumber="))
- {
- if (uint.TryParse(trimmed.Substring(14), out uint productCode))
- info.ProductCode = productCode;
- }
- }
-
- // 解析PDO映射
- if (trimmed.StartsWith("[1A00sub0]"))
- {
- // TPDO1映射对象数量
- }
- // ... 更多解析逻辑
- }
-
- return info;
- }
-
- #endregion
-
- #region 节点管理
-
- /// <summary>
- /// 添加从站节点
- /// </summary>
- /// <param name="nodeId">节点ID(1-127)</param>
- /// <param name="nodeName">节点名称(可选)</param>
- /// <param name="edsFilePath">EDS文件路径(可选)</param>
- /// <returns>是否成功</returns>
- public bool AddNode(byte nodeId, string nodeName = null, string edsFilePath = null)
- {
- if (nodeId == 0 || nodeId > 127)
- {
- Log($"无效的节点ID: {nodeId}");
- return false;
- }
-
- if (m_nodes.ContainsKey(nodeId))
- {
- Log($"节点{nodeId}已存在");
- return false;
- }
-
- var node = new CanOpenNode(nodeId, nodeName ?? $"Node_{nodeId}");
- m_nodes[nodeId] = node;
-
- // 如果提供了EDS文件,加载它
- if (!string.IsNullOrEmpty(edsFilePath))
- {
- LoadEdsFile(nodeId, edsFilePath);
- }
-
- Log($"添加节点: ID={nodeId}, Name={node.NodeName}" +
- (edsFilePath != null ? $", EDS={System.IO.Path.GetFileName(edsFilePath)}" : ""));
- return true;
- }
-
- /// <summary>
- /// 移除从站节点
- /// </summary>
- public bool RemoveNode(byte nodeId)
- {
- if (!m_nodes.ContainsKey(nodeId))
- {
- Log($"节点{nodeId}不存在");
- return false;
- }
-
- m_nodes.Remove(nodeId);
- Log($"移除节点: {nodeId}");
- return true;
- }
-
- /// <summary>
- /// 获取节点对象
- /// </summary>
- public CanOpenNode GetNode(byte nodeId)
- {
- if (m_nodes.TryGetValue(nodeId, out var node))
- {
- return node;
- }
- return null;
- }
-
- /// <summary>
- /// 批量添加节点
- /// </summary>
- public void AddNodes(byte[] nodeIds)
- {
- foreach (var nodeId in nodeIds)
- {
- AddNode(nodeId);
- }
- }
-
- #endregion
-
- #region NMT控制
-
- /// <summary>
- /// 启动所有节点
- /// </summary>
- public void StartAllNodes()
- {
- Log("启动所有节点...");
- foreach (var nodeId in m_nodes.Keys)
- {
- NmtStartNode(nodeId);
- Thread.Sleep(10); // 避免总线拥塞
- }
- }
-
- /// <summary>
- /// 停止所有节点
- /// </summary>
- public void StopAllNodes()
- {
- Log("停止所有节点...");
- foreach (var nodeId in m_nodes.Keys)
- {
- NmtStopNode(nodeId);
- Thread.Sleep(10);
- }
- }
-
- /// <summary>
- /// NMT启动指定节点
- /// </summary>
- public void NmtStartNode(byte nodeId)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- m_canManager.NmtStartNode(nodeId);
- UpdateNodeState(nodeId, NodeState.Operational);
- Log($"NMT启动节点: {nodeId}");
- }
-
- /// <summary>
- /// NMT停止指定节点
- /// </summary>
- public void NmtStopNode(byte nodeId)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- m_canManager.NmtStopNode(nodeId);
- UpdateNodeState(nodeId, NodeState.Stopped);
- Log($"NMT停止节点: {nodeId}");
- }
-
- /// <summary>
- /// NMT进入预操作状态
- /// </summary>
- public void NmtEnterPreOperational(byte nodeId)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- m_canManager.NmtEnterPreOperational(nodeId);
- UpdateNodeState(nodeId, NodeState.PreOperational);
- Log($"NMT进入预操作状态: {nodeId}");
- }
-
- /// <summary>
- /// NMT重置节点
- /// </summary>
- public void NmtResetNode(byte nodeId)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- m_canManager.NmtResetNode(nodeId);
- UpdateNodeState(nodeId, NodeState.Initializing);
- Log($"NMT重置节点: {nodeId}");
- }
-
- /// <summary>
- /// NMT重置通信
- /// </summary>
- public void NmtResetCommunication(byte nodeId)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- m_canManager.NmtResetCommunication(nodeId);
- Log($"NMT重置通信: {nodeId}");
- }
-
- #endregion
-
- #region SDO通信
-
- /// <summary>
- /// SDO读取(快速,不等待响应)
- /// </summary>
- public void SdoReadQuick(byte nodeId, ushort index, byte subIndex)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- m_canManager.SdoReadQuick(nodeId, index, subIndex);
- }
-
- /// <summary>
- /// SDO写入(快速,不等待响应)
- /// </summary>
- public void SdoWriteQuick(byte nodeId, ushort index, byte subIndex, uint data)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- m_canManager.SdoWrite(nodeId, index, subIndex, data);
- }
-
- /// <summary>
- /// SDO读取并等待响应
- /// </summary>
- public byte[] SdoReadAndWait(byte nodeId, ushort index, byte subIndex, int timeoutMs = 1000)
- {
- if (!ValidateNodeId(nodeId)) return null;
-
- var response = m_canManager.SdoReadAndWait(nodeId, index, subIndex, timeoutMs);
-
- if (response != null)
- {
- var node = GetNode(nodeId);
- if (node != null)
- {
- node.LastSdoResponseTime = DateTime.Now;
- node.SdoSuccessCount++;
- }
- }
- else
- {
- var node = GetNode(nodeId);
- if (node != null)
- {
- node.SdoTimeoutCount++;
- }
- }
-
- return response;
- }
-
- /// <summary>
- /// SDO写入并等待确认
- /// </summary>
- public bool SdoWriteAndWait(byte nodeId, ushort index, byte subIndex, uint data, int timeoutMs = 1000)
- {
- if (!ValidateNodeId(nodeId)) return false;
-
- var success = m_canManager.SdoWriteAndWait(nodeId, index, subIndex, data, timeoutMs);
-
- var node = GetNode(nodeId);
- if (node != null)
- {
- node.LastSdoResponseTime = DateTime.Now;
- if (success)
- node.SdoSuccessCount++;
- else
- node.SdoTimeoutCount++;
- }
-
- return success;
- }
-
- /// <summary>
- /// 读取设备类型
- /// </summary>
- public uint ReadDeviceType(byte nodeId)
- {
- var response = SdoReadAndWait(nodeId, 0x1000, 0x00);
- if (response != null && response.Length >= 4)
- {
- return BitConverter.ToUInt32(response, 4);
- }
- return 0;
- }
-
- /// <summary>
- /// 读取厂商ID
- /// </summary>
- public uint ReadVendorId(byte nodeId)
- {
- var response = SdoReadAndWait(nodeId, 0x1018, 0x01);
- if (response != null && response.Length >= 4)
- {
- return BitConverter.ToUInt32(response, 4);
- }
- return 0;
- }
-
- /// <summary>
- /// 读取产品代码
- /// </summary>
- public uint ReadProductCode(byte nodeId)
- {
- var response = SdoReadAndWait(nodeId, 0x1018, 0x02);
- if (response != null && response.Length >= 4)
- {
- return BitConverter.ToUInt32(response, 4);
- }
- return 0;
- }
-
- /// <summary>
- /// 读取序列号
- /// </summary>
- public uint ReadSerialNumber(byte nodeId)
- {
- var response = SdoReadAndWait(nodeId, 0x1018, 0x04);
- if (response != null && response.Length >= 4)
- {
- return BitConverter.ToUInt32(response, 4);
- }
- return 0;
- }
-
- #endregion
-
- #region PDO通信
-
- /// <summary>
- /// 发送SYNC帧
- /// </summary>
- public void SendSync(byte counter = 0)
- {
- m_canManager.SendSync(counter);
- }
-
- /// <summary>
- /// 启用周期性SYNC
- /// </summary>
- /// <param name="intervalMs">SYNC周期(毫秒)</param>
- public void EnableSync(int intervalMs = 10)
- {
- if (m_syncTimer != null)
- {
- m_syncTimer.Dispose();
- }
-
- m_syncEnabled = true;
- m_syncCounter = 0;
-
- m_syncTimer = new Timer(state =>
- {
- if (m_syncEnabled)
- {
- m_syncCounter++;
- SendSync(m_syncCounter);
- }
- }, null, 0, intervalMs);
-
- Log($"启用周期性SYNC,周期:{intervalMs}ms");
- }
-
- /// <summary>
- /// 禁用SYNC
- /// </summary>
- public void DisableSync()
- {
- m_syncEnabled = false;
- m_syncTimer?.Dispose();
- m_syncTimer = null;
- Log("禁用SYNC");
- }
-
- /// <summary>
- /// 配置TPDO传输类型
- /// </summary>
- public void ConfigureTpdoTransmissionType(byte nodeId, byte pdoNumber, byte transmissionType)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- m_canManager.ConfigureTpdoTransmissionType(nodeId, pdoNumber, transmissionType);
- }
-
- /// <summary>
- /// 触发节点发送TPDO
- /// </summary>
- public void TriggerTpdo(byte nodeId, byte pdoNumber)
- {
- if (!ValidateNodeId(nodeId)) return;
-
- // 通过SDO设置TPDO传输类型为"事件驱动"
- ushort index = (ushort)(0x1800 + pdoNumber - 1);
- SdoWriteAndWait(nodeId, index, 0x02, 0xFF); // 0xFF表示事件驱动
-
- Log($"触发节点{nodeId}发送TPDO{pdoNumber}");
- }
-
- /// <summary>
- /// 主站向从站发送RPDO数据(写入PDO)
- /// </summary>
- /// <param name="nodeId">目标节点ID</param>
- /// <param name="pdoNumber">RPDO编号(1-4)</param>
- /// <param name="data">要发送的数据</param>
- /// <returns>是否成功</returns>
- public bool SendRpdo(byte nodeId, byte pdoNumber, byte[] data)
- {
- if (!ValidateNodeId(nodeId)) return false;
-
- if (pdoNumber < 1 || pdoNumber > 4)
- {
- Log($"无效的RPDO编号: {pdoNumber}");
- return false;
- }
-
- if (data == null || data.Length > 8)
- {
- Log("RPDO数据无效(必须1-8字节)");
- return false;
- }
-
- // 计算RPDO的COB-ID
- uint cobId = GetRpdoCobId(pdoNumber, nodeId);
-
- // 发送CAN帧
- bool success = m_canManager.SendCanFrame(cobId, data);
-
- if (success)
- {
- var node = GetNode(nodeId);
- if (node != null)
- {
- node.LastTpdoTime[pdoNumber] = DateTime.Now;
- }
- Log($"发送RPDO{pdoNumber}到节点{nodeId}: COB-ID=0x{cobId:X3}, 数据长度={data.Length}");
- }
- else
- {
- Log($"发送RPDO{pdoNumber}失败");
- }
-
- return success;
- }
-
- /// <summary>
- /// 发送RPDO并等待从站确认(通过心跳或SDO响应)
- /// </summary>
- public bool SendRpdoAndWait(byte nodeId, byte pdoNumber, byte[] data, int timeoutMs = 100)
- {
- if (!SendRpdo(nodeId, pdoNumber, data))
- return false;
-
- // 等待一段时间,让从站处理数据
- Thread.Sleep(timeoutMs);
-
- // 检查从站是否在线(有心跳)
- var node = GetNode(nodeId);
- if (node != null)
- {
- return node.IsOnline(timeoutMs * 2);
- }
-
- return true;
- }
-
- /// <summary>
- /// 配置RPDO映射(通过SDO修改从站的对象字典)
- /// </summary>
- /// <param name="nodeId">节点ID</param>
- /// <param name="pdoNumber">RPDO编号(1-4)</param>
- /// <param name="mappedObjects">映射对象列表</param>
- /// <returns>是否成功</returns>
- public bool ConfigureRpdoMapping(byte nodeId, byte pdoNumber, List<MappedObject> mappedObjects)
- {
- if (!ValidateNodeId(nodeId)) return false;
-
- if (pdoNumber < 1 || pdoNumber > 4)
- {
- Log($"无效的RPDO编号: {pdoNumber}");
- return false;
- }
-
- try
- {
- ushort mappingIndex = (ushort)(0x1600 + pdoNumber - 1); // RPDO映射索引
-
- // 1. 先禁用RPDO(设置COB-ID最高位为1)
- ushort commIndex = (ushort)(0x1400 + pdoNumber - 1);
- uint currentCobId = BitConverter.ToUInt32(SdoReadAndWait(nodeId, commIndex, 0x01), 4);
- uint disabledCobId = currentCobId | 0x80000000;
- SdoWriteAndWait(nodeId, commIndex, 0x01, disabledCobId);
-
- // 2. 设置映射对象数量
- SdoWriteAndWait(nodeId, mappingIndex, 0x00, (uint)mappedObjects.Count);
-
- // 3. 配置每个映射对象
- for (int i = 0; i < mappedObjects.Count; i++)
- {
- var obj = mappedObjects[i];
- byte subIndex = (byte)(i + 1);
-
- // 打包映射条目: 0xIIIISSLL
- uint mappingEntry = ((uint)obj.Index << 16) |
- ((uint)obj.SubIndex << 8) |
- (uint)obj.BitLength;
-
- SdoWriteAndWait(nodeId, mappingIndex, subIndex, mappingEntry);
- Log($"配置RPDO{pdoNumber}映射[{subIndex}]: 0x{obj.Index:X4}:{obj.SubIndex:X2} ({obj.BitLength}位)");
- }
-
- // 4. 重新启用RPDO(清除COB-ID最高位)
- uint enabledCobId = currentCobId & 0x7FFFFFFF;
- SdoWriteAndWait(nodeId, commIndex, 0x01, enabledCobId);
-
- Log($"RPDO{pdoNumber}映射配置完成: {mappedObjects.Count}个对象");
- return true;
- }
- catch (Exception ex)
- {
- Log($"配置RPDO映射失败: {ex.Message}");
- return false;
- }
- }
-
- /// <summary>
- /// 配置TPDO映射(通过SDO修改从站的对象字典)
- /// </summary>
- public bool ConfigureTpdoMapping(byte nodeId, byte pdoNumber, List<MappedObject> mappedObjects)
- {
- if (!ValidateNodeId(nodeId)) return false;
-
- if (pdoNumber < 1 || pdoNumber > 4)
- {
- Log($"无效的TPDO编号: {pdoNumber}");
- return false;
- }
-
- try
- {
- ushort mappingIndex = (ushort)(0x1A00 + pdoNumber - 1); // TPDO映射索引
-
- // 1. 先禁用TPDO
- ushort commIndex = (ushort)(0x1800 + pdoNumber - 1);
- uint currentCobId = BitConverter.ToUInt32(SdoReadAndWait(nodeId, commIndex, 0x01), 4);
- uint disabledCobId = currentCobId | 0x80000000;
- SdoWriteAndWait(nodeId, commIndex, 0x01, disabledCobId);
-
- // 2. 设置映射对象数量
- SdoWriteAndWait(nodeId, mappingIndex, 0x00, (uint)mappedObjects.Count);
-
- // 3. 配置每个映射对象
- for (int i = 0; i < mappedObjects.Count; i++)
- {
- var obj = mappedObjects[i];
- byte subIndex = (byte)(i + 1);
-
- uint mappingEntry = ((uint)obj.Index << 16) |
- ((uint)obj.SubIndex << 8) |
- (uint)obj.BitLength;
-
- SdoWriteAndWait(nodeId, mappingIndex, subIndex, mappingEntry);
- }
-
- // 4. 重新启用TPDO
- uint enabledCobId = currentCobId & 0x7FFFFFFF;
- SdoWriteAndWait(nodeId, commIndex, 0x01, enabledCobId);
-
- Log($"TPDO{pdoNumber}映射配置完成: {mappedObjects.Count}个对象");
- return true;
- }
- catch (Exception ex)
- {
- Log($"配置TPDO映射失败: {ex.Message}");
- return false;
- }
- }
-
- /// <summary>
- /// 获取RPDO的COB-ID
- /// </summary>
- private uint GetRpdoCobId(byte pdoNumber, byte nodeId)
- {
- switch (pdoNumber)
- {
- case 1: return (uint)(0x200 + nodeId); // RPDO1
- case 2: return (uint)(0x300 + nodeId); // RPDO2
- case 3: return (uint)(0x400 + nodeId); // RPDO3
- case 4: return (uint)(0x500 + nodeId); // RPDO4
- default: throw new ArgumentException("无效的RPDO编号");
- }
- }
-
- #endregion
-
- #region 接收处理线程
-
- private void ReceiveLoop()
- {
- Log("接收线程启动");
-
- while (m_isRunning && !m_cancellationTokenSource.Token.IsCancellationRequested)
- {
- try
- {
- var frames = m_canManager.ReceiveCanFrames(100);
-
- foreach (var frame in frames)
- {
- ProcessCanOpenFrame(frame);
- }
- }
- catch (Exception ex)
- {
- Log($"接收线程异常: {ex.Message}");
- }
- }
-
- Log("接收线程退出");
- }
-
- private void ProcessCanOpenFrame(CanOpenFrame frame)
- {
- try
- {
- // 解析COB-ID功能码
- byte functionCode = (byte)((frame.CobId >> 7) & 0x0F);
- byte nodeId = (byte)(frame.CobId & 0x7F);
-
- switch (functionCode)
- {
- case 0x00: // NMT
- ProcessNmtFrame(frame);
- break;
-
- case 0x01: // SYNC
- // SYNC通常由主站发送,这里忽略
- break;
-
- case 0x02: // EMCY (紧急消息)
- ProcessEmergencyFrame(nodeId, frame.Data);
- break;
-
- case 0x03: // TIME (时间戳对象)
- // 可选功能,暂不处理
- break;
-
- case 0x05: // EMCY (扩展)
- ProcessEmergencyFrame(nodeId, frame.Data);
- break;
-
- case 0x07: // Heartbeat / NMT Error Control
- ProcessHeartbeatFrame(nodeId, frame.Data);
- break;
-
- case 0x08: // SDO Response
- ProcessSdoResponse(nodeId, frame.Data);
- break;
-
- case 0x09: // SDO Request (主站通常不接收)
- break;
-
- default:
- // PDO (0x0B-0x0F for TPDO, 0x13-0x17 for RPDO)
- if (functionCode >= 0x0B && functionCode <= 0x0F)
- {
- ProcessTpdoFrame(nodeId, functionCode, frame.Data);
- }
- break;
- }
- }
- catch (Exception ex)
- {
- Log($"处理帧异常: {ex.Message}");
- }
- }
-
- private void ProcessNmtFrame(CanOpenFrame frame)
- {
- // NMT通常由主站发送,这里可以记录从站的NMT响应(如果有)
- }
-
- private void ProcessEmergencyFrame(byte nodeId, byte[] data)
- {
- if (data.Length >= 2)
- {
- ushort errorCode = m_canManager.ParseEmergencyMessage(data);
- OnEmergencyReceived?.Invoke(nodeId, errorCode);
-
- var node = GetNode(nodeId);
- if (node != null)
- {
- node.CurrentErrorRegister = data.Length > 2 ? data[2] : (byte)0;
- node.LastEmergencyTime = DateTime.Now;
- node.EmergencyCount++;
- }
-
- Log($"收到紧急消息 - 节点{nodeId}: 错误代码0x{errorCode:X4}");
- }
- }
-
- private void ProcessHeartbeatFrame(byte nodeId, byte[] data)
- {
- if (data.Length >= 1)
- {
- byte status = m_canManager.ParseHeartbeat(nodeId, data);
- OnHeartbeatReceived?.Invoke(nodeId, status);
-
- var node = GetNode(nodeId);
- if (node != null)
- {
- node.CurrentState = (NodeState)(status & 0x7F);
- node.LastHeartbeatTime = DateTime.Now;
- node.HeartbeatCount++;
- }
- }
- }
-
- private void ProcessSdoResponse(byte nodeId, byte[] data)
- {
- // SDO响应已在CanOpenManager中处理
- // 这里可以做额外的日志记录
- }
-
- private void ProcessTpdoFrame(byte nodeId, byte functionCode, byte[] data)
- {
- byte pdoNumber = (byte)(functionCode - 0x0A); // TPDO1=1, TPDO2=2, ...
-
- OnTpdoReceived?.Invoke(nodeId, data);
-
- var node = GetNode(nodeId);
- if (node != null)
- {
- node.LastTpdoTime[pdoNumber] = DateTime.Now;
- node.TpdoCount[pdoNumber]++;
-
- // 存储最新TPDO数据
- node.LastTpdoData[pdoNumber] = data;
- }
- }
-
- #endregion
-
- #region 辅助方法
-
- private bool ValidateNodeId(byte nodeId)
- {
- if (nodeId == 0 || nodeId > 127)
- {
- Log($"无效的节点ID: {nodeId}");
- return false;
- }
-
- if (!m_nodes.ContainsKey(nodeId))
- {
- Log($"节点{nodeId}未注册");
- return false;
- }
-
- return true;
- }
-
- private void UpdateNodeState(byte nodeId, NodeState state)
- {
- var node = GetNode(nodeId);
- if (node != null)
- {
- node.CurrentState = state;
- node.LastStateChangeTime = DateTime.Now;
- }
- }
-
- private void Log(string message)
- {
- string logMsg = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
- Console.WriteLine(logMsg);
- OnLogMessage?.Invoke(logMsg);
- }
-
- #endregion
-
- #region 资源管理
-
- /// <summary>
- /// 关闭主站
- /// </summary>
- public void Close()
- {
- Log("正在关闭CANopen主站...");
-
- // 停止接收线程
- m_isRunning = false;
- m_cancellationTokenSource?.Cancel();
-
- if (m_receiveThread != null && m_receiveThread.IsAlive)
- {
- m_receiveThread.Join(1000);
- }
-
- // 禁用SYNC
- DisableSync();
-
- // 关闭CAN设备
- m_canManager?.Close();
-
- Log("CANopen主站已关闭");
- }
-
- /// <summary>
- /// 释放资源
- /// </summary>
- public void Dispose()
- {
- Close();
- m_cancellationTokenSource?.Dispose();
- m_syncTimer?.Dispose();
- }
-
- #endregion
- }
-
- /// <summary>
- /// CANopen从站节点信息
- /// </summary>
- public class CanOpenNode
- {
- #region 属性
-
- /// <summary>
- /// 节点ID
- /// </summary>
- public byte NodeId { get; private set; }
-
- /// <summary>
- /// 节点名称
- /// </summary>
- public string NodeName { get; private set; }
-
- /// <summary>
- /// 当前状态
- /// </summary>
- public NodeState CurrentState { get; set; }
-
- /// <summary>
- /// 最后心跳时间
- /// </summary>
- public DateTime LastHeartbeatTime { get; set; }
-
- /// <summary>
- /// 心跳计数
- /// </summary>
- public uint HeartbeatCount { get; set; }
-
- /// <summary>
- /// 最后TPDO时间
- /// </summary>
- public Dictionary<byte, DateTime> LastTpdoTime { get; private set; }
-
- /// <summary>
- /// TPDO计数
- /// </summary>
- public Dictionary<byte, uint> TpdoCount { get; private set; }
-
- /// <summary>
- /// 最后TPDO数据
- /// </summary>
- public Dictionary<byte, byte[]> LastTpdoData { get; private set; }
-
- /// <summary>
- /// 最后SDO响应时间
- /// </summary>
- public DateTime LastSdoResponseTime { get; set; }
-
- /// <summary>
- /// SDO成功计数
- /// </summary>
- public uint SdoSuccessCount { get; set; }
-
- /// <summary>
- /// SDO超时计数
- /// </summary>
- public uint SdoTimeoutCount { get; set; }
-
- /// <summary>
- /// 最后紧急消息时间
- /// </summary>
- public DateTime LastEmergencyTime { get; set; }
-
- /// <summary>
- /// 紧急消息计数
- /// </summary>
- public uint EmergencyCount { get; set; }
-
- /// <summary>
- /// 当前错误寄存器
- /// </summary>
- public byte CurrentErrorRegister { get; set; }
-
- /// <summary>
- /// 最后状态改变时间
- /// </summary>
- public DateTime LastStateChangeTime { get; set; }
-
- #endregion
-
- #region 构造函数
-
- public CanOpenNode(byte nodeId, string nodeName)
- {
- NodeId = nodeId;
- NodeName = nodeName;
- CurrentState = NodeState.Unknown;
-
- LastTpdoTime = new Dictionary<byte, DateTime>();
- TpdoCount = new Dictionary<byte, uint>();
- LastTpdoData = new Dictionary<byte, byte[]>();
-
- // 初始化4个TPDO
- for (byte i = 1; i <= 4; i++)
- {
- LastTpdoTime[i] = DateTime.MinValue;
- TpdoCount[i] = 0;
- LastTpdoData[i] = null;
- }
- }
-
- #endregion
-
- #region 方法
-
- /// <summary>
- /// 检查节点是否在线(基于心跳超时判断)
- /// </summary>
- /// <param name="timeoutMs">超时时间(毫秒)</param>
- public bool IsOnline(int timeoutMs = 1000)
- {
- if (LastHeartbeatTime == DateTime.MinValue)
- return false;
-
- return (DateTime.Now - LastHeartbeatTime).TotalMilliseconds < timeoutMs;
- }
-
- /// <summary>
- /// 获取节点状态字符串
- /// </summary>
- public string GetStateString()
- {
- switch (CurrentState)
- {
- case NodeState.Initializing: return "Initializing";
- case NodeState.Disconnected: return "Disconnected";
- case NodeState.Connecting: return "Connecting";
- case NodeState.PreOperational: return "Pre-Operational";
- case NodeState.Operational: return "Operational";
- case NodeState.Stopped: return "Stopped";
- default: return "Unknown";
- }
- }
-
- /// <summary>
- /// 获取节点统计信息
- /// </summary>
- public override string ToString()
- {
- return $"Node {NodeId} ({NodeName}): State={GetStateString()}, " +
- $"Heartbeats={HeartbeatCount}, " +
- $"SDO(Success={SdoSuccessCount}, Timeout={SdoTimeoutCount}), " +
- $"Emergencies={EmergencyCount}";
- }
-
- #endregion
- }
-
- /// <summary>
- /// 节点状态枚举
- /// </summary>
- public enum NodeState
- {
- Unknown = 0x00,
- Initializing = 0x00,
- Disconnected = 0x01,
- Connecting = 0x02,
- PreOperational = 0x7F,
- Operational = 0x05,
- Stopped = 0x04
- }
-
- /// <summary>
- /// EDS设备信息(简化版)
- /// </summary>
- public class EdsDeviceInfo
- {
- /// <summary>
- /// 设备名称
- /// </summary>
- public string DeviceName { get; set; }
-
- /// <summary>
- /// 厂商名称
- /// </summary>
- public string VendorName { get; set; }
-
- /// <summary>
- /// 产品名称
- /// </summary>
- public string ProductName { get; set; }
-
- /// <summary>
- /// 厂商ID
- /// </summary>
- public uint VendorId { get; set; }
-
- /// <summary>
- /// 产品代码
- /// </summary>
- public uint ProductCode { get; set; }
-
- /// <summary>
- /// 设备类型
- /// </summary>
- public uint DeviceType { get; set; }
-
- /// <summary>
- /// TPDO映射配置
- /// </summary>
- public List<PdoMappingConfig> TpdoMappings { get; set; } = new List<PdoMappingConfig>();
-
- /// <summary>
- /// RPDO映射配置
- /// </summary>
- public List<PdoMappingConfig> RpdoMappings { get; set; } = new List<PdoMappingConfig>();
-
- /// <summary>
- /// 对象字典条目
- /// </summary>
- public Dictionary<ushort, ObjectDictionaryEntry> ObjectDictionary { get; set; }
- = new Dictionary<ushort, ObjectDictionaryEntry>();
-
- public override string ToString()
- {
- return $"{DeviceName} (Vendor={VendorName}, Product={ProductName})";
- }
- }
-
- /// <summary>
- /// PDO映射配置
- /// </summary>
- public class PdoMappingConfig
- {
- /// <summary>
- /// PDO编号(1-4)
- /// </summary>
- public byte PdoNumber { get; set; }
-
- /// <summary>
- /// COB-ID
- /// </summary>
- public uint CobId { get; set; }
-
- /// <summary>
- /// 传输类型
- /// 0=禁止, 1=同步周期, 254=远程请求, 255=事件驱动
- /// </summary>
- public byte TransmissionType { get; set; }
-
- /// <summary>
- /// 映射的对象列表
- /// </summary>
- public List<MappedObject> MappedObjects { get; set; } = new List<MappedObject>();
-
- public override string ToString()
- {
- return $"PDO{PdoNumber}: COB-ID=0x{CobId:X3}, Type={TransmissionType}, Objects={MappedObjects.Count}";
- }
- }
-
- /// <summary>
- /// 映射对象
- /// </summary>
- public class MappedObject
- {
- /// <summary>
- /// 对象索引
- /// </summary>
- public ushort Index { get; set; }
-
- /// <summary>
- /// 子索引
- /// </summary>
- public byte SubIndex { get; set; }
-
- /// <summary>
- /// 数据长度(位)
- /// </summary>
- public byte BitLength { get; set; }
-
- public override string ToString()
- {
- return $"0x{Index:X4}:{SubIndex:X2} ({BitLength} bits)";
- }
- }
-
- /// <summary>
- /// 对象字典条目
- /// </summary>
- public class ObjectDictionaryEntry
- {
- /// <summary>
- /// 索引
- /// </summary>
- public ushort Index { get; set; }
-
- /// <summary>
- /// 对象名称
- /// </summary>
- public string Name { get; set; }
-
- /// <summary>
- /// 对象类型(VAR/RECORD/ARRAY)
- /// </summary>
- public string ObjectType { get; set; }
-
- /// <summary>
- /// 数据类型
- /// </summary>
- public string DataType { get; set; }
-
- /// <summary>
- /// 访问权限(ro/rw/wo)
- /// </summary>
- public string AccessType { get; set; }
-
- /// <summary>
- /// 默认值
- /// </summary>
- public string DefaultValue { get; set; }
-
- /// <summary>
- /// 子对象(如果是RECORD或ARRAY)
- /// </summary>
- public Dictionary<byte, ObjectDictionaryEntry> SubObjects { get; set; }
- = new Dictionary<byte, ObjectDictionaryEntry>();
-
- public override string ToString()
- {
- return $"0x{Index:X4} {Name} ({ObjectType}, {DataType}, {AccessType})";
- }
- }
- }
|