* 扩展 ControllerClient 兼容层的执行参数和运行时编排 * 新增 /status 页面与 /api/status/snapshot 状态快照接口 * 补充 FANUC 协议、客户端和状态接口的最小验证测试 * 更新 README、兼容要求和真机 Socket 通信实现计划
189 lines
5.1 KiB
C#
189 lines
5.1 KiB
C#
using System.Net.Sockets;
|
||
|
||
namespace Flyshot.Runtime.Fanuc.Protocol;
|
||
|
||
/// <summary>
|
||
/// FANUC TCP 10010 状态通道客户端,持续接收状态帧并缓存最新快照。
|
||
/// </summary>
|
||
public sealed class FanucStateClient : IDisposable
|
||
{
|
||
private readonly object _stateLock = new();
|
||
private TcpClient? _tcpClient;
|
||
private NetworkStream? _stream;
|
||
private CancellationTokenSource? _receiveCts;
|
||
private Task? _receiveTask;
|
||
private FanucStateFrame? _latestFrame;
|
||
private bool _disposed;
|
||
|
||
/// <summary>
|
||
/// 获取当前是否已建立连接。
|
||
/// </summary>
|
||
public bool IsConnected => _tcpClient?.Connected ?? false;
|
||
|
||
/// <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 (_tcpClient is not null)
|
||
{
|
||
throw new InvalidOperationException("状态通道已经连接,请先 Disconnect。");
|
||
}
|
||
|
||
_tcpClient = new TcpClient { NoDelay = true };
|
||
await _tcpClient.ConnectAsync(ip, port, cancellationToken).ConfigureAwait(false);
|
||
_stream = _tcpClient.GetStream();
|
||
|
||
_receiveCts = new CancellationTokenSource();
|
||
_receiveTask = Task.Run(() => ReceiveLoopAsync(_receiveCts.Token), _receiveCts.Token);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 断开状态通道并停止后台接收循环。
|
||
/// </summary>
|
||
public void Disconnect()
|
||
{
|
||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||
|
||
_receiveCts?.Cancel();
|
||
|
||
try
|
||
{
|
||
_receiveTask?.Wait(TimeSpan.FromSeconds(2));
|
||
}
|
||
catch (AggregateException)
|
||
{
|
||
// 后台循环可能因取消而抛出 OperationCanceledException,忽略即可。
|
||
}
|
||
|
||
_receiveTask?.Dispose();
|
||
_receiveTask = null;
|
||
_receiveCts?.Dispose();
|
||
_receiveCts = null;
|
||
|
||
_stream?.Dispose();
|
||
_stream = null;
|
||
_tcpClient?.Dispose();
|
||
_tcpClient = null;
|
||
|
||
lock (_stateLock)
|
||
{
|
||
_latestFrame = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取最近一次解析的状态帧;若尚未收到任何帧则返回 null。
|
||
/// </summary>
|
||
/// <returns>最新状态帧或 null。</returns>
|
||
public FanucStateFrame? GetLatestFrame()
|
||
{
|
||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||
|
||
lock (_stateLock)
|
||
{
|
||
return _latestFrame;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放客户端资源。
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
if (_disposed)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_disposed = true;
|
||
_receiveCts?.Cancel();
|
||
|
||
try
|
||
{
|
||
_receiveTask?.Wait(TimeSpan.FromSeconds(2));
|
||
}
|
||
catch (AggregateException)
|
||
{
|
||
// 忽略取消异常。
|
||
}
|
||
|
||
_receiveTask?.Dispose();
|
||
_receiveCts?.Dispose();
|
||
_stream?.Dispose();
|
||
_tcpClient?.Dispose();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 后台循环:持续从流中读取固定长度状态帧并更新缓存。
|
||
/// </summary>
|
||
private async Task ReceiveLoopAsync(CancellationToken cancellationToken)
|
||
{
|
||
if (_stream is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var buffer = new byte[FanucStateProtocol.StateFrameLength];
|
||
|
||
try
|
||
{
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
await ReadExactAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||
|
||
var frame = FanucStateProtocol.ParseFrame(buffer);
|
||
lock (_stateLock)
|
||
{
|
||
_latestFrame = frame;
|
||
}
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// 正常取消,无需处理。
|
||
}
|
||
catch (IOException)
|
||
{
|
||
// 连接断开,退出循环。
|
||
}
|
||
catch (InvalidDataException)
|
||
{
|
||
// 解析到异常帧,退出循环由上层重连。
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从流中精确读取固定长度字节。
|
||
/// </summary>
|
||
private async Task ReadExactAsync(byte[] buffer, CancellationToken cancellationToken)
|
||
{
|
||
if (_stream is null)
|
||
{
|
||
throw new InvalidOperationException("状态通道未连接。");
|
||
}
|
||
|
||
var totalRead = 0;
|
||
while (totalRead < buffer.Length)
|
||
{
|
||
var read = await _stream.ReadAsync(buffer.AsMemory(totalRead), cancellationToken).ConfigureAwait(false);
|
||
if (read == 0)
|
||
{
|
||
throw new IOException("状态通道已断开,读取到 EOF。");
|
||
}
|
||
|
||
totalRead += read;
|
||
}
|
||
}
|
||
}
|