using System.Net.Sockets; using Microsoft.Extensions.Logging; namespace Flyshot.Runtime.Fanuc.Protocol; /// /// 表示 FANUC TCP 10010 状态通道客户端的连接阶段。 /// public enum FanucStateConnectionState { /// /// 状态通道未连接。 /// Disconnected, /// /// 状态通道正在建立连接。 /// Connecting, /// /// 状态通道已连接并由后台循环接收状态帧。 /// Connected, /// /// 状态通道在限定时间内没有收到完整状态帧。 /// TimedOut, /// /// 状态通道正在按退避策略重新连接。 /// Reconnecting, } /// /// 定义 FANUC TCP 10010 状态通道的超时和重连参数。 /// public sealed class FanucStateClientOptions { /// /// 获取或设置接收一帧完整 90B 状态帧允许的最长时间。 /// public TimeSpan FrameTimeout { get; init; } = TimeSpan.FromMilliseconds(250); /// /// 获取或设置初始重连等待时间。 /// public TimeSpan ReconnectInitialDelay { get; init; } = TimeSpan.FromMilliseconds(100); /// /// 获取或设置重连等待时间的上限。 /// public TimeSpan ReconnectMaxDelay { get; init; } = TimeSpan.FromSeconds(5); /// /// 获取或设置单次 TCP 建连允许的最长时间。 /// public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(5); } /// /// 表示 FANUC TCP 10010 状态通道客户端的当前诊断状态。 /// public sealed class FanucStateClientStatus { /// /// 初始化状态通道诊断状态。 /// public FanucStateClientStatus( FanucStateConnectionState state, bool isFrameStale, DateTimeOffset? lastFrameAt, long reconnectAttemptCount, string? lastErrorMessage) { State = state; IsFrameStale = isFrameStale; LastFrameAt = lastFrameAt; ReconnectAttemptCount = reconnectAttemptCount; LastErrorMessage = lastErrorMessage; } /// /// 获取状态通道当前连接阶段。 /// public FanucStateConnectionState State { get; } /// /// 获取最近缓存状态帧是否已经超过状态帧超时窗口。 /// public bool IsFrameStale { get; } /// /// 获取最近一次成功解析状态帧的 UTC 时间。 /// public DateTimeOffset? LastFrameAt { get; } /// /// 获取后台循环发起重连的累计次数。 /// public long ReconnectAttemptCount { get; } /// /// 获取最近一次状态通道异常的诊断文本。 /// public string? LastErrorMessage { get; } } /// /// FANUC TCP 10010 状态通道客户端,持续接收状态帧并缓存最新快照。 /// public sealed class FanucStateClient : IDisposable { private readonly object _stateLock = new(); private readonly FanucStateClientOptions _options; private readonly ILogger? _logger; private TcpClient? _tcpClient; private NetworkStream? _stream; private CancellationTokenSource? _receiveCts; private Task? _receiveTask; private FanucStateFrame? _latestFrame; private FanucStateConnectionState _connectionState = FanucStateConnectionState.Disconnected; private DateTimeOffset? _lastConnectedAt; private DateTimeOffset? _lastFrameAt; private long _reconnectAttemptCount; private string? _lastErrorMessage; private bool _disposed; /// /// 使用默认状态通道参数初始化客户端。 /// public FanucStateClient() : this(new FanucStateClientOptions(), null) { } /// /// 使用指定状态通道参数初始化客户端。 /// /// 超时和重连参数。 public FanucStateClient(FanucStateClientOptions options) : this(options, null) { } /// /// 使用指定状态通道参数和日志记录器初始化客户端。 /// /// 超时和重连参数。 /// 日志记录器;允许 null。 public FanucStateClient(FanucStateClientOptions options, ILogger? logger) { ArgumentNullException.ThrowIfNull(options); ValidateOptions(options); _options = options; _logger = logger; } /// /// 获取当前是否已建立连接。 /// public bool IsConnected => GetStatus().State == FanucStateConnectionState.Connected; /// /// 建立到 FANUC 控制柜 TCP 10010 状态通道的连接并启动后台接收循环。 /// /// 控制柜 IP 地址。 /// 状态通道端口,默认 10010。 /// 取消令牌。 public async Task ConnectAsync(string ip, int port = 10010, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, this); if (string.IsNullOrWhiteSpace(ip)) { throw new ArgumentException("IP 不能为空。", nameof(ip)); } if (_receiveTask is not null) { throw new InvalidOperationException("状态通道已经连接,请先 Disconnect。"); } _logger?.LogInformation("StateClient ConnectAsync: {Ip}:{Port}", ip, port); _receiveCts = new CancellationTokenSource(); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _receiveCts.Token); lock (_stateLock) { _connectionState = FanucStateConnectionState.Connecting; _latestFrame = null; _lastConnectedAt = null; _lastFrameAt = null; _reconnectAttemptCount = 0; _lastErrorMessage = null; } try { await OpenConnectionAsync(ip, port, linkedCts.Token).ConfigureAwait(false); } catch (Exception exception) { _logger?.LogError(exception, "StateClient 连接失败: {Ip}:{Port}", ip, port); CloseCurrentConnection(); lock (_stateLock) { _connectionState = FanucStateConnectionState.Disconnected; } _receiveCts.Dispose(); _receiveCts = null; throw; } _receiveTask = Task.Run( () => ReceiveAndReconnectLoopAsync(ip, port, _receiveCts.Token), _receiveCts.Token); _logger?.LogInformation("StateClient 已连接并启动接收循环: {Ip}:{Port}", ip, port); } /// /// 断开状态通道并停止后台接收循环。 /// public void Disconnect() { ObjectDisposedException.ThrowIf(_disposed, this); _logger?.LogInformation("StateClient Disconnect"); Shutdown(clearLatestFrame: true); } /// /// 获取最近一次解析的状态帧;若尚未收到任何帧则返回 null。 /// /// 最新状态帧或 null。 public FanucStateFrame? GetLatestFrame() { ObjectDisposedException.ThrowIf(_disposed, this); lock (_stateLock) { return _latestFrame; } } /// /// 获取状态通道当前诊断状态。 /// /// 状态通道诊断快照。 public FanucStateClientStatus GetStatus() { ObjectDisposedException.ThrowIf(_disposed, this); lock (_stateLock) { return new FanucStateClientStatus( _connectionState, IsFrameStaleLocked(DateTimeOffset.UtcNow), _lastFrameAt, _reconnectAttemptCount, _lastErrorMessage); } } /// /// 释放客户端资源。 /// public void Dispose() { if (_disposed) { return; } _disposed = true; Shutdown(clearLatestFrame: true); } /// /// 后台循环:持续接收状态帧;断线、超时或坏帧后进入退避重连。 /// private async Task ReceiveAndReconnectLoopAsync(string ip, int port, CancellationToken cancellationToken) { var reconnectDelay = _options.ReconnectInitialDelay; _logger?.LogInformation("StateClient 接收循环启动: {Ip}:{Port}", ip, port); while (!cancellationToken.IsCancellationRequested) { try { await ReceiveCurrentConnectionAsync(cancellationToken).ConfigureAwait(false); reconnectDelay = _options.ReconnectInitialDelay; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { _logger?.LogInformation("StateClient 接收循环正常取消"); return; } catch (TimeoutException ex) { _logger?.LogWarning(ex, "StateClient 接收超时"); MarkReceiveFailure(FanucStateConnectionState.TimedOut, ex.Message); } catch (Exception ex) when (ex is IOException or InvalidDataException or SocketException or ObjectDisposedException) { _logger?.LogWarning(ex, "StateClient 连接异常,准备重连"); MarkReceiveFailure(FanucStateConnectionState.Reconnecting, ex.Message); } CloseCurrentConnection(); if (cancellationToken.IsCancellationRequested) { return; } reconnectDelay = await ReconnectWithBackoffAsync(ip, port, reconnectDelay, cancellationToken).ConfigureAwait(false); } } /// /// 从当前连接中持续读取状态帧,直到连接异常或被取消。 /// private async Task ReceiveCurrentConnectionAsync(CancellationToken cancellationToken) { NetworkStream stream; lock (_stateLock) { stream = _stream ?? throw new IOException("状态通道未连接。"); } var buffer = new byte[FanucStateProtocol.StateFrameLength]; long frameCount = 0; FanucStateFrame? lastLoggedFrame = null; while (!cancellationToken.IsCancellationRequested) { await ReadExactAsync(stream, buffer, cancellationToken).ConfigureAwait(false); var frame = FanucStateProtocol.ParseFrame(buffer); lock (_stateLock) { _latestFrame = frame; _lastFrameAt = DateTimeOffset.UtcNow; _connectionState = FanucStateConnectionState.Connected; _lastErrorMessage = null; } frameCount++; // 仅在状态变化或首次接收时记录 Info,避免高频日志。 if (lastLoggedFrame is null || lastLoggedFrame.CartesianPose[0] != frame.CartesianPose[0] || !lastLoggedFrame.RawTailWords.SequenceEqual(frame.RawTailWords)) { _logger?.LogInformation( "StateClient 收到状态帧: pose=[{X:F1}, {Y:F1}, {Z:F1}], tail=[{Tail}]", frame.CartesianPose[0], frame.CartesianPose[1], frame.CartesianPose[2], string.Join(", ", frame.RawTailWords)); lastLoggedFrame = frame; } else if (frameCount % 1000 == 0) { _logger?.LogDebug("StateClient 已接收 {Count} 个状态帧", frameCount); } } } /// /// 从流中精确读取固定长度字节,超过帧超时窗口则抛出超时异常。 /// private async Task ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken cancellationToken) { using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(_options.FrameTimeout); var totalRead = 0; try { while (totalRead < buffer.Length) { var read = await stream.ReadAsync( buffer.AsMemory(totalRead, buffer.Length - totalRead), timeoutCts.Token).ConfigureAwait(false); if (read == 0) { throw new IOException("状态通道已断开,读取到 EOF。"); } totalRead += read; } } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException("状态通道接收超时,未在限定时间内收到完整 90B 状态帧。"); } } /// /// 打开 TCP 状态通道并更新连接状态。 /// private async Task OpenConnectionAsync(string ip, int port, CancellationToken cancellationToken) { var tcpClient = new TcpClient { NoDelay = true }; try { _logger?.LogInformation("StateClient 正在连接 {Ip}:{Port}...", ip, port); using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(_options.ConnectTimeout); await tcpClient.ConnectAsync(ip, port, timeoutCts.Token).ConfigureAwait(false); lock (_stateLock) { _tcpClient = tcpClient; _stream = tcpClient.GetStream(); _lastConnectedAt = DateTimeOffset.UtcNow; _connectionState = FanucStateConnectionState.Connected; } _logger?.LogInformation("StateClient 已连接到 {Ip}:{Port}", ip, port); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { _logger?.LogWarning("StateClient 连接 {Ip}:{Port} 超时", ip, port); tcpClient.Dispose(); throw new TimeoutException("状态通道建连超时。"); } catch (Exception ex) { _logger?.LogWarning(ex, "StateClient 连接 {Ip}:{Port} 失败", ip, port); tcpClient.Dispose(); throw; } } /// /// 按退避策略循环尝试重新连接,并返回下一次异常后的退避时间。 /// private async Task ReconnectWithBackoffAsync( string ip, int port, TimeSpan reconnectDelay, CancellationToken cancellationToken) { var nextDelay = reconnectDelay; while (!cancellationToken.IsCancellationRequested) { lock (_stateLock) { _connectionState = FanucStateConnectionState.Reconnecting; } _logger?.LogInformation( "StateClient 将在 {Delay}ms 后尝试重连 {Ip}:{Port}...", nextDelay.TotalMilliseconds, ip, port); await Task.Delay(nextDelay, cancellationToken).ConfigureAwait(false); lock (_stateLock) { _reconnectAttemptCount++; } try { await OpenConnectionAsync(ip, port, cancellationToken).ConfigureAwait(false); _logger?.LogInformation( "StateClient 重连成功: {Ip}:{Port}, 累计重连次数={Count}", ip, port, _reconnectAttemptCount); return _options.ReconnectInitialDelay; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) when (ex is SocketException or IOException or TimeoutException) { CloseCurrentConnection(); lock (_stateLock) { _connectionState = FanucStateConnectionState.Reconnecting; _lastErrorMessage = ex.Message; } _logger?.LogWarning( ex, "StateClient 重连失败: {Ip}:{Port}, 下次等待={NextDelay}ms", ip, port, nextDelay.TotalMilliseconds * 2); nextDelay = IncreaseReconnectDelay(nextDelay); } } return nextDelay; } /// /// 关闭当前 TCP 连接,不清除最新状态帧,供重连路径保留诊断数据。 /// private void CloseCurrentConnection() { NetworkStream? stream; TcpClient? tcpClient; lock (_stateLock) { stream = _stream; tcpClient = _tcpClient; _stream = null; _tcpClient = null; } stream?.Dispose(); tcpClient?.Dispose(); } /// /// 记录接收异常并更新状态通道连接阶段。 /// private void MarkReceiveFailure(FanucStateConnectionState state, string message) { lock (_stateLock) { _connectionState = state; _lastErrorMessage = message; } _logger?.LogWarning("StateClient 接收失败: state={State}, message={Message}", state, message); } /// /// 关闭后台循环和 socket 资源。 /// private void Shutdown(bool clearLatestFrame) { _receiveCts?.Cancel(); CloseCurrentConnection(); try { _receiveTask?.Wait(TimeSpan.FromSeconds(2)); } catch (AggregateException) { // 后台循环可能因取消而抛出 OperationCanceledException,忽略即可。 } _receiveTask = null; _receiveCts?.Dispose(); _receiveCts = null; lock (_stateLock) { _connectionState = FanucStateConnectionState.Disconnected; _lastConnectedAt = null; _lastErrorMessage = null; _reconnectAttemptCount = 0; if (clearLatestFrame) { _latestFrame = null; _lastFrameAt = null; } } } /// /// 判断缓存帧是否已经不能代表当前控制柜状态。 /// private bool IsFrameStaleLocked(DateTimeOffset now) { if (_latestFrame is null) { return _connectionState is FanucStateConnectionState.TimedOut or FanucStateConnectionState.Reconnecting || _reconnectAttemptCount > 0 || (_lastConnectedAt.HasValue && now - _lastConnectedAt.Value > _options.FrameTimeout); } return _lastFrameAt.HasValue && now - _lastFrameAt.Value > _options.FrameTimeout; } /// /// 计算下一轮重连等待时间。 /// private TimeSpan IncreaseReconnectDelay(TimeSpan currentDelay) { var doubledMilliseconds = Math.Max(currentDelay.TotalMilliseconds * 2.0, _options.ReconnectInitialDelay.TotalMilliseconds); var cappedMilliseconds = Math.Min(doubledMilliseconds, _options.ReconnectMaxDelay.TotalMilliseconds); return TimeSpan.FromMilliseconds(cappedMilliseconds); } /// /// 校验状态通道参数,避免后台循环使用无效时间窗口。 /// private static void ValidateOptions(FanucStateClientOptions options) { ValidatePositive(options.FrameTimeout, nameof(options.FrameTimeout)); ValidatePositive(options.ReconnectInitialDelay, nameof(options.ReconnectInitialDelay)); ValidatePositive(options.ReconnectMaxDelay, nameof(options.ReconnectMaxDelay)); ValidatePositive(options.ConnectTimeout, nameof(options.ConnectTimeout)); if (options.ReconnectMaxDelay < options.ReconnectInitialDelay) { throw new ArgumentOutOfRangeException(nameof(options), "最大重连等待时间不能小于初始重连等待时间。"); } } /// /// 校验时间参数必须为正值。 /// private static void ValidatePositive(TimeSpan value, string parameterName) { if (value <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(parameterName, "时间参数必须大于 0。"); } } }