| | |
| | | public int SimulatedLatencyMs { get; set; } = 0; |
| | | |
| | | /// <summary> |
| | | /// 总线级应答器:开启后,任何节点发出的帧会被原样回显给发送者自身 |
| | | /// (模拟单个从设备应答,无需第二个节点在线)。来源标记为 RESPONDER。 |
| | | /// </summary> |
| | | public bool EchoResponderToSender { get; set; } = false; |
| | | |
| | | /// <summary> |
| | | /// 已注册的节点 |
| | | /// </summary> |
| | | private readonly ConcurrentDictionary<string, CanNode> _nodes = new(); |
| | |
| | | TotalFramesTransmitted++; |
| | | LastActivityTime = DateTime.Now; |
| | | |
| | | // 路由到所有其他节点 |
| | | // 路由到所有其他节点;若接收节点启用自动响应,由其回复 |
| | | int routed = 0; |
| | | foreach (var kvp in _nodes) |
| | | { |
| | |
| | | kvp.Value.ReceiveFrame(frame); |
| | | routed++; |
| | | TotalFramesReceived++; |
| | | |
| | | // 接收方自动响应:模拟从设备收到命令后答复(面板“自动响应”语义) |
| | | if (kvp.Value.AutoResponseEnabled) |
| | | SendAutoResponse(kvp.Value, frame); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | // 检查源节点是否启用自动响应模式 |
| | | if (_nodes.TryGetValue(frame.SourceNodeId, out var sourceNode) && sourceNode.AutoResponseEnabled) |
| | | // 总线级应答器:把该帧原样回显给发送者自身(绕过"不回源",直接投递) |
| | | if (EchoResponderToSender && _nodes.TryGetValue(frame.SourceNodeId, out var senderNode)) |
| | | { |
| | | try |
| | | { |
| | | // 模拟响应延迟 |
| | | if (sourceNode.AutoResponseDelayMs > 0) |
| | | { |
| | | Thread.Sleep(sourceNode.AutoResponseDelayMs); |
| | | } |
| | | |
| | | // 创建响应帧(复制原始帧数据,来源为源节点) |
| | | var responseFrame = new CanFrame |
| | | var echo = new CanFrame |
| | | { |
| | | FrameId = frame.FrameId, |
| | | Format = frame.Format, |
| | | Type = frame.Type, |
| | | Data = (byte[])frame.Data.Clone(), |
| | | SourceNodeId = sourceNode.NodeId, |
| | | SourceNodeId = "RESPONDER", |
| | | Timestamp = DateTime.Now |
| | | }; |
| | | senderNode.ReceiveFrame(echo); |
| | | TotalFramesReceived++; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[CanBus] 应答器回显给 {frame.SourceNodeId} 失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | Log.Debug($"[CanBus] 帧 {frame.FrameIdHex} 从 {frame.SourceNodeId} 路由到 {routed} 个节点"); |
| | | FrameTransmitted?.Invoke(this, new CanBusFrameEventArgs(frame, frame.SourceNodeId)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 接收节点自动响应:把收到帧原样回给其它节点(含发帧方),并记入自身邮箱与总线日志 |
| | | /// </summary> |
| | | private void SendAutoResponse(CanNode responder, CanFrame incoming) |
| | | { |
| | | try |
| | | { |
| | | // 模拟响应延迟 |
| | | if (responder.AutoResponseDelayMs > 0) |
| | | { |
| | | Thread.Sleep(responder.AutoResponseDelayMs); |
| | | } |
| | | |
| | | var responseFrame = new CanFrame |
| | | { |
| | | FrameId = incoming.FrameId, |
| | | Format = incoming.Format, |
| | | Type = incoming.Type, |
| | | Data = (byte[])incoming.Data.Clone(), |
| | | SourceNodeId = responder.NodeId, |
| | | Timestamp = DateTime.Now |
| | | }; |
| | | |
| | | // 将响应帧路由到所有其他节点(不包括响应源自身) |
| | | // 响应帧路由到其它节点(含最初的发帧方) |
| | | foreach (var kvp in _nodes) |
| | | { |
| | | if (kvp.Key == sourceNode.NodeId) continue; |
| | | if (kvp.Key == responder.NodeId) continue; |
| | | try |
| | | { |
| | | kvp.Value.ReceiveFrame(responseFrame); |
| | |
| | | } |
| | | } |
| | | |
| | | // 将响应帧送入源节点的接收邮箱(让源节点能看到自己的响应) |
| | | sourceNode.ReceiveFrame(responseFrame); |
| | | // 响应帧送入应答节点自身接收邮箱(调试页可见自己发出的响应) |
| | | responder.ReceiveFrame(responseFrame); |
| | | TotalFramesReceived++; |
| | | |
| | | // 记录响应帧到总线日志 |
| | | _busLog.Enqueue(responseFrame); |
| | | TotalFramesTransmitted++; |
| | | |
| | | Log.Debug($"[CanBus] 节点 {sourceNode.NodeId} 自动响应帧 {responseFrame.FrameIdHex}"); |
| | | Log.Debug($"[CanBus] 接收节点 {responder.NodeId} 自动响应帧 {responseFrame.FrameIdHex}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[CanBus] 源节点自动响应失败: {ex.Message}"); |
| | | Log.Error($"[CanBus] 节点 {responder.NodeId} 自动响应失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | Log.Debug($"[CanBus] 帧 {frame.FrameIdHex} 从 {frame.SourceNodeId} 路由到 {routed} 个节点"); |
| | | FrameTransmitted?.Invoke(this, new CanBusFrameEventArgs(frame, frame.SourceNodeId)); |
| | | } |
| | | |
| | | /// <summary> |