- 改为高优先级 J519 接收线程与复用缓冲区发送链路 - 增加稠密执行前的 J519 就绪重试与状态诊断 - 修正程序状态响应字段顺序与 EnableRobot 默认参数 - 为飞拍轨迹补充平滑起停时间轴与首尾整形验证 - 补充真实运行配置、报警窗口与边界对比测试 - 同步更新限值文档、分析脚本与 .NET 8 SDK 固定配置
633 lines
21 KiB
C#
633 lines
21 KiB
C#
using System.Net.Sockets;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace Flyshot.Runtime.Fanuc.Protocol;
|
||
|
||
/// <summary>
|
||
/// 表示 FANUC TCP 10010 状态通道客户端的连接阶段。
|
||
/// </summary>
|
||
public enum FanucStateConnectionState
|
||
{
|
||
/// <summary>
|
||
/// 状态通道未连接。
|
||
/// </summary>
|
||
Disconnected,
|
||
|
||
/// <summary>
|
||
/// 状态通道正在建立连接。
|
||
/// </summary>
|
||
Connecting,
|
||
|
||
/// <summary>
|
||
/// 状态通道已连接并由后台循环接收状态帧。
|
||
/// </summary>
|
||
Connected,
|
||
|
||
/// <summary>
|
||
/// 状态通道在限定时间内没有收到完整状态帧。
|
||
/// </summary>
|
||
TimedOut,
|
||
|
||
/// <summary>
|
||
/// 状态通道正在按退避策略重新连接。
|
||
/// </summary>
|
||
Reconnecting,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 定义 FANUC TCP 10010 状态通道的超时和重连参数。
|
||
/// </summary>
|
||
public sealed class FanucStateClientOptions
|
||
{
|
||
/// <summary>
|
||
/// 获取或设置接收一帧完整 90B 状态帧允许的最长时间。
|
||
/// </summary>
|
||
public TimeSpan FrameTimeout { get; init; } = TimeSpan.FromMilliseconds(250);
|
||
|
||
/// <summary>
|
||
/// 获取或设置初始重连等待时间。
|
||
/// </summary>
|
||
public TimeSpan ReconnectInitialDelay { get; init; } = TimeSpan.FromMilliseconds(100);
|
||
|
||
/// <summary>
|
||
/// 获取或设置重连等待时间的上限。
|
||
/// </summary>
|
||
public TimeSpan ReconnectMaxDelay { get; init; } = TimeSpan.FromSeconds(5);
|
||
|
||
/// <summary>
|
||
/// 获取或设置单次 TCP 建连允许的最长时间。
|
||
/// </summary>
|
||
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(5);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 表示 FANUC TCP 10010 状态通道客户端的当前诊断状态。
|
||
/// </summary>
|
||
public sealed class FanucStateClientStatus
|
||
{
|
||
/// <summary>
|
||
/// 初始化状态通道诊断状态。
|
||
/// </summary>
|
||
public FanucStateClientStatus(
|
||
FanucStateConnectionState state,
|
||
bool isFrameStale,
|
||
DateTimeOffset? lastFrameAt,
|
||
long reconnectAttemptCount,
|
||
string? lastErrorMessage)
|
||
{
|
||
State = state;
|
||
IsFrameStale = isFrameStale;
|
||
LastFrameAt = lastFrameAt;
|
||
ReconnectAttemptCount = reconnectAttemptCount;
|
||
LastErrorMessage = lastErrorMessage;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取状态通道当前连接阶段。
|
||
/// </summary>
|
||
public FanucStateConnectionState State { get; }
|
||
|
||
/// <summary>
|
||
/// 获取最近缓存状态帧是否已经超过状态帧超时窗口。
|
||
/// </summary>
|
||
public bool IsFrameStale { get; }
|
||
|
||
/// <summary>
|
||
/// 获取最近一次成功解析状态帧的 UTC 时间。
|
||
/// </summary>
|
||
public DateTimeOffset? LastFrameAt { get; }
|
||
|
||
/// <summary>
|
||
/// 获取后台循环发起重连的累计次数。
|
||
/// </summary>
|
||
public long ReconnectAttemptCount { get; }
|
||
|
||
/// <summary>
|
||
/// 获取最近一次状态通道异常的诊断文本。
|
||
/// </summary>
|
||
public string? LastErrorMessage { get; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// FANUC TCP 10010 状态通道客户端,持续接收状态帧并缓存最新快照。
|
||
/// </summary>
|
||
public sealed class FanucStateClient : IDisposable
|
||
{
|
||
private readonly object _stateLock = new();
|
||
private readonly FanucStateClientOptions _options;
|
||
private readonly ILogger<FanucStateClient>? _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;
|
||
|
||
/// <summary>
|
||
/// 使用默认状态通道参数初始化客户端。
|
||
/// </summary>
|
||
public FanucStateClient()
|
||
: this(new FanucStateClientOptions(), null)
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用指定状态通道参数初始化客户端。
|
||
/// </summary>
|
||
/// <param name="options">超时和重连参数。</param>
|
||
public FanucStateClient(FanucStateClientOptions options)
|
||
: this(options, null)
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用指定状态通道参数和日志记录器初始化客户端。
|
||
/// </summary>
|
||
/// <param name="options">超时和重连参数。</param>
|
||
/// <param name="logger">日志记录器;允许 null。</param>
|
||
public FanucStateClient(FanucStateClientOptions options, ILogger<FanucStateClient>? logger)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(options);
|
||
ValidateOptions(options);
|
||
_options = options;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前是否已建立连接。
|
||
/// </summary>
|
||
public bool IsConnected => GetStatus().State == FanucStateConnectionState.Connected;
|
||
|
||
/// <summary>
|
||
/// 建立到 FANUC 控制柜 TCP 10010 状态通道的连接并启动后台接收循环。
|
||
/// </summary>
|
||
/// <param name="ip">控制柜 IP 地址。</param>
|
||
/// <param name="port">状态通道端口,默认 10010。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 断开状态通道并停止后台接收循环。
|
||
/// </summary>
|
||
public void Disconnect()
|
||
{
|
||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||
|
||
_logger?.LogInformation("StateClient Disconnect");
|
||
Shutdown(clearLatestFrame: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取最近一次解析的状态帧;若尚未收到任何帧则返回 null。
|
||
/// </summary>
|
||
/// <returns>最新状态帧或 null。</returns>
|
||
public FanucStateFrame? GetLatestFrame()
|
||
{
|
||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||
|
||
lock (_stateLock)
|
||
{
|
||
return _latestFrame;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取状态通道当前诊断状态。
|
||
/// </summary>
|
||
/// <returns>状态通道诊断快照。</returns>
|
||
public FanucStateClientStatus GetStatus()
|
||
{
|
||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||
|
||
lock (_stateLock)
|
||
{
|
||
return new FanucStateClientStatus(
|
||
_connectionState,
|
||
IsFrameStaleLocked(DateTimeOffset.UtcNow),
|
||
_lastFrameAt,
|
||
_reconnectAttemptCount,
|
||
_lastErrorMessage);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放客户端资源。
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
if (_disposed)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_disposed = true;
|
||
Shutdown(clearLatestFrame: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 后台循环:持续接收状态帧;断线、超时或坏帧后进入退避重连。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从当前连接中持续读取状态帧,直到连接异常或被取消。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从流中精确读取固定长度字节,超过帧超时窗口则抛出超时异常。
|
||
/// </summary>
|
||
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 状态帧。");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开 TCP 状态通道并更新连接状态。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按退避策略循环尝试重新连接,并返回下一次异常后的退避时间。
|
||
/// </summary>
|
||
private async Task<TimeSpan> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭当前 TCP 连接,不清除最新状态帧,供重连路径保留诊断数据。
|
||
/// </summary>
|
||
private void CloseCurrentConnection()
|
||
{
|
||
NetworkStream? stream;
|
||
TcpClient? tcpClient;
|
||
lock (_stateLock)
|
||
{
|
||
stream = _stream;
|
||
tcpClient = _tcpClient;
|
||
_stream = null;
|
||
_tcpClient = null;
|
||
}
|
||
|
||
stream?.Dispose();
|
||
tcpClient?.Dispose();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 记录接收异常并更新状态通道连接阶段。
|
||
/// </summary>
|
||
private void MarkReceiveFailure(FanucStateConnectionState state, string message)
|
||
{
|
||
lock (_stateLock)
|
||
{
|
||
_connectionState = state;
|
||
_lastErrorMessage = message;
|
||
}
|
||
|
||
_logger?.LogWarning("StateClient 接收失败: state={State}, message={Message}", state, message);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭后台循环和 socket 资源。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断缓存帧是否已经不能代表当前控制柜状态。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算下一轮重连等待时间。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 校验状态通道参数,避免后台循环使用无效时间窗口。
|
||
/// </summary>
|
||
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), "最大重连等待时间不能小于初始重连等待时间。");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 校验时间参数必须为正值。
|
||
/// </summary>
|
||
private static void ValidatePositive(TimeSpan value, string parameterName)
|
||
{
|
||
if (value <= TimeSpan.Zero)
|
||
{
|
||
throw new ArgumentOutOfRangeException(parameterName, "时间参数必须大于 0。");
|
||
}
|
||
}
|
||
}
|