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