From 559890c482f5973f06aafdfec9550fee5ba3c7f2 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 09 十一月 2026 17:02:15 +0800
Subject: [PATCH] feat: CAN 模拟器 TCP 化 + 多设备并发调试 + 后台生命周期管理
---
Aging.Infrastructure/Drivers/NodeChannelTransport.cs | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 219 insertions(+), 0 deletions(-)
diff --git a/Aging.Infrastructure/Drivers/NodeChannelTransport.cs b/Aging.Infrastructure/Drivers/NodeChannelTransport.cs
new file mode 100644
index 0000000..fc0ff09
--- /dev/null
+++ b/Aging.Infrastructure/Drivers/NodeChannelTransport.cs
@@ -0,0 +1,219 @@
+using System;
+using System.Configuration;
+using System.IO;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using CanSimulator;
+using log4net;
+
+namespace Aging.Infrastructure.Drivers
+{
+ /// <summary>
+ /// Local 浼犺緭鍚庣锛歍CP 鐩磋繛 CanSimulatorServer锛屾彙鎵� HELLO 鎸傝浇 (bus,nodeIndex)锛�
+ /// 鍛戒护涓茬粡 TransceiveAsync 涓�闂竴绛旓紝鎺ユ敹甯х敱鏈嶅姟绔� EV:RECV 涓诲姩鎺ㄩ�佽Е鍙� FramePushed銆�
+ /// </summary>
+ internal sealed class NodeChannelTransport : ICanTransport
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(NodeChannelTransport));
+
+ private readonly string _host;
+ private readonly int _port;
+ private readonly string _clientTag;
+
+ private TcpClient _socket;
+ private Stream _stream;
+ private CancellationTokenSource _cts;
+ private Task _readLoop;
+
+ // 涓�闂竴绛旓細鍚屼竴鏃跺埢浠呬竴涓湪閫旇姹�
+ private readonly SemaphoreSlim _requestLock = new(1, 1);
+ private TaskCompletionSource<string> _pending;
+
+ private volatile bool _connected;
+ private bool _disposed;
+ private string _busName;
+ private int _nodeIndex = -1;
+
+ public bool IsConnected => _connected;
+ public event Action<string> FramePushed;
+ public event Action TransportBroken;
+
+ public NodeChannelTransport(string hostOverride, int? portOverride, string clientTag)
+ {
+ _host = hostOverride ?? ReadCfg("CanSimulator.Host", "127.0.0.1");
+ _port = portOverride ?? ReadIntCfg("CanSimulator.Port", 9000);
+ _clientTag = clientTag ?? $"{Environment.MachineName}/{System.Diagnostics.Process.GetCurrentProcess().Id}";
+ }
+
+ private static string ReadCfg(string key, string def)
+ {
+ try { var v = ConfigurationManager.AppSettings[key]; return string.IsNullOrWhiteSpace(v) ? def : v; }
+ catch { return def; }
+ }
+ private static int ReadIntCfg(string key, int def)
+ => int.TryParse(ReadCfg(key, null), out int v) ? v : def;
+
+ public async Task ConnectAsync(string busName, int nodeIndex, CancellationToken ct)
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(NodeChannelTransport));
+ _busName = busName;
+ _nodeIndex = nodeIndex;
+
+ try
+ {
+ _socket = new TcpClient();
+ await _socket.ConnectAsync(_host, _port).ConfigureAwait(false);
+ _socket.NoDelay = true;
+ _stream = _socket.GetStream();
+ _cts = new CancellationTokenSource();
+
+ // HELLO 鎻℃墜锛氳寰幆鍚姩鍓嶅悓姝ョ洿璇讳竴琛屽搷搴旓紙姝ゆ椂鏃犲苟鍙戞帹閫侊級
+ string ack = await HandshakeAsync(
+ $"HELLO:{busName}:{nodeIndex}:{_clientTag}", ct).ConfigureAwait(false);
+
+ if (ack == "ERR:NODE_BUSY")
+ throw new InvalidOperationException(
+ $"妯℃嫙鍣ㄦ�荤嚎 {busName} 涓婅妭鐐圭紪鍙� {nodeIndex} 宸茶鍗犵敤锛岃淇敼璁惧绔偣鐨勮妭鐐圭紪鍙�");
+ if (!ack.StartsWith("OK:HELLO", StringComparison.Ordinal))
+ throw new InvalidOperationException($"妯℃嫙鍣ㄦ彙鎵嬪け璐�: {ack}");
+
+ _connected = true;
+ _readLoop = Task.Run(() => ReadLoopAsync(_cts.Token));
+ Log.Info($"[CAN-TCP] 宸叉寕杞� {busName}:N{nodeIndex} @ {_host}:{_port}");
+ }
+ catch (SocketException ex)
+ {
+ SafeCleanupSocket();
+ throw new InvalidOperationException(
+ $"鏃犳硶杩炴帴 CAN 妯℃嫙鍣ㄦ湇鍔� {_host}:{_port}锛坽ex.Message}锛夈�傝鍏堝惎鍔� CanSimulatorServer.exe", ex);
+ }
+ }
+
+ public async Task DisconnectAsync()
+ {
+ if (!_connected) return;
+ try { await RequestLineAsync("DISCONNECT", CancellationToken.None).ConfigureAwait(false); }
+ catch { /* 鏂紑灏藉姏鑰屼负 */ }
+ Teardown();
+ }
+
+ public async Task<string> TransceiveAsync(string line, CancellationToken ct)
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(NodeChannelTransport));
+ if (!_connected) throw new InvalidOperationException("CAN 閫氶亾鏈繛鎺�");
+ try
+ {
+ return await RequestLineAsync(line, ct).ConfigureAwait(false);
+ }
+ catch (IOException ex)
+ {
+ OnBroken();
+ throw new InvalidOperationException("CAN 妯℃嫙鍣ㄨ繛鎺ュ凡涓柇: " + ex.Message, ex);
+ }
+ }
+
+ /// <summary>鎻℃墜涓撶敤锛氬啓 HELLO 鍚庣洿鎺ュ悓姝ヨ鍙栦竴琛屽搷搴旓紙璇诲惊鐜皻鏈惎鍔紝鏃犲苟鍙戯級銆�</summary>
+ private async Task<string> HandshakeAsync(string helloLine, CancellationToken ct)
+ {
+ byte[] frame = SimulatorFraming.Encode(helloLine);
+ await _stream.WriteAsync(frame, 0, frame.Length, ct).ConfigureAwait(false);
+ await _stream.FlushAsync(ct).ConfigureAwait(false);
+ string resp = await SimulatorFraming.ReadFrameAsync(_stream, ct).ConfigureAwait(false);
+ return resp ?? "<EOF>";
+ }
+
+ /// <summary>鍙戦�佷竴琛屽苟绛夊緟鍏跺搷搴旇锛汦V:RECV 鎺ㄩ�佸湪姝よ繃绋嬩腑琚垎娴佸嚭鍘伙紝涓嶈涓哄搷搴斻��</summary>
+ private async Task<string> RequestLineAsync(string line, CancellationToken ct)
+ {
+ await _requestLock.WaitAsync(ct).ConfigureAwait(false);
+ try
+ {
+ var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
+ _pending = tcs;
+
+ byte[] frame = SimulatorFraming.Encode(line);
+ await _stream.WriteAsync(frame, 0, frame.Length, ct).ConfigureAwait(false);
+ await _stream.FlushAsync(ct).ConfigureAwait(false);
+
+ // deadline锛氶粯璁� 5s锛岃秴鏃惰涓虹粨鏋滄湭鐭ワ紙涓婂眰绂佹鍋囧畾鏈墽琛岋級
+ var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5), ct);
+ var done = await Task.WhenAny(tcs.Task, timeoutTask).ConfigureAwait(false);
+ if (done != tcs.Task)
+ {
+ _pending = null;
+ throw new TimeoutException($"CAN 妯℃嫙鍣ㄥ懡浠よ秴鏃�: {line}");
+ }
+ _pending = null;
+ ct.ThrowIfCancellationRequested();
+ return await tcs.Task.ConfigureAwait(false);
+ }
+ finally
+ {
+ _requestLock.Release();
+ }
+ }
+
+ private async Task ReadLoopAsync(CancellationToken ct)
+ {
+ try
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ string resp = await SimulatorFraming.ReadFrameAsync(_stream, ct).ConfigureAwait(false);
+ if (resp == null) break; // EOF
+
+ if (resp.StartsWith("EV:", StringComparison.Ordinal))
+ {
+ FramePushed?.Invoke(resp);
+ continue;
+ }
+
+ // 鏅�氬搷搴旓細浜ょ粰鍦ㄩ�旇姹�
+ _pending?.TrySetResult(resp);
+ }
+ }
+ catch (IOException) { }
+ catch (InvalidDataException) { }
+ catch (OperationCanceledException) { }
+ catch (Exception ex) { Log.Warn($"[CAN-TCP] 璇诲惊鐜紓甯�: {ex.Message}"); }
+ finally
+ {
+ if (!_disposed) OnBroken();
+ }
+ }
+
+ private void OnBroken()
+ {
+ if (!_connected && _pending == null) return;
+ _connected = false;
+ _pending?.TrySetException(new IOException("CAN 妯℃嫙鍣ㄩ�氶亾宸叉柇寮�"));
+ TransportBroken?.Invoke();
+ }
+
+ private void Teardown()
+ {
+ _connected = false;
+ try { _cts?.Cancel(); } catch { }
+ SafeCleanupSocket();
+ }
+
+ private void SafeCleanupSocket()
+ {
+ try { _stream?.Dispose(); } catch { }
+ try { _socket?.Close(); } catch { }
+ _stream = null;
+ _socket = null;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ Teardown();
+ try { _readLoop?.Wait(200); } catch { }
+ _requestLock.Dispose();
+ _cts?.Dispose();
+ }
+ }
+}
--
Gitblit v1.9.1