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。");
}
}
}