using System.Net.Sockets;
namespace Flyshot.Runtime.Fanuc.Protocol;
///
/// FANUC TCP 10012 命令通道客户端,提供 Req/Res 同步命令下发能力。
///
public sealed class FanucCommandClient : IDisposable
{
private readonly SemaphoreSlim _sendLock = new(1, 1);
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private bool _disposed;
///
/// 获取当前是否已建立连接。
///
public bool IsConnected => _tcpClient?.Connected ?? false;
///
/// 建立到 FANUC 控制柜 TCP 10012 命令通道的连接。
///
/// 控制柜 IP 地址。
/// 命令通道端口,默认 10012。
/// 取消令牌。
public async Task ConnectAsync(string ip, int port = 10012, 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();
}
///
/// 断开命令通道并释放资源。
///
public void Disconnect()
{
ObjectDisposedException.ThrowIf(_disposed, this);
_stream?.Dispose();
_stream = null;
_tcpClient?.Dispose();
_tcpClient = null;
}
///
/// 发送通用命令并等待响应。
///
/// 命令消息号。
/// 命令业务体。
/// 取消令牌。
/// 原始响应帧。
public async Task SendCommandAsync(uint messageId, ReadOnlyMemory body, CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_stream is null)
{
throw new InvalidOperationException("命令通道未连接。");
}
await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var frame = FanucCommandProtocol.PackFrame(messageId, body.Span);
await _stream.WriteAsync(frame, cancellationToken).ConfigureAwait(false);
return await ReadResponseFrameAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_sendLock.Release();
}
}
///
/// 发送携带程序名的命令并等待响应。
///
/// 命令消息号。
/// 程序名。
/// 取消令牌。
/// 结果响应。
public async Task SendProgramCommandAsync(uint messageId, string programName, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackProgramCommand(messageId, programName);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseResultResponse(response));
}
///
/// 停止指定程序。
///
/// 程序名。
/// 取消令牌。
/// 结果响应。
public Task StopProgramAsync(string programName, CancellationToken cancellationToken = default)
{
return SendProgramCommandAsync(FanucCommandMessageIds.StopProgram, programName, cancellationToken);
}
///
/// 复位控制器。
///
/// 取消令牌。
/// 结果响应。
public async Task ResetRobotAsync(CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackEmptyCommand(FanucCommandMessageIds.ResetRobot);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseResultResponse(response));
}
///
/// 查询指定程序状态。
///
/// 程序名。
/// 取消令牌。
/// 程序状态响应。
public async Task GetProgramStatusAsync(string programName, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackProgramCommand(FanucCommandMessageIds.GetProgramStatus, programName);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseProgramStatusResponse(response));
}
///
/// 启动指定程序。
///
/// 程序名。
/// 取消令牌。
/// 结果响应。
public Task StartProgramAsync(string programName, CancellationToken cancellationToken = default)
{
return SendProgramCommandAsync(FanucCommandMessageIds.StartProgram, programName, cancellationToken);
}
///
/// 读取控制器速度倍率。
///
/// 取消令牌。
/// 速度倍率响应。
public async Task GetSpeedRatioAsync(CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackGetSpeedRatioCommand();
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseSpeedRatioResponse(response));
}
///
/// 设置控制器速度倍率。
///
/// 目标速度倍率。
/// 取消令牌。
/// 结果响应。
public async Task SetSpeedRatioAsync(double ratio, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackSetSpeedRatioCommand(ratio);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseResultResponse(response));
}
///
/// 读取控制器 TCP 位姿。
///
/// TCP ID。
/// 取消令牌。
/// TCP 位姿响应。
public async Task GetTcpAsync(uint tcpId = 1, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackGetTcpCommand(tcpId);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseTcpResponse(response));
}
///
/// 设置控制器 TCP 位姿。
///
/// TCP ID。
/// 7 维 TCP 位姿。
/// 取消令牌。
/// 结果响应。
public async Task SetTcpAsync(uint tcpId, IReadOnlyList pose, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackSetTcpCommand(tcpId, pose);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseResultResponse(response));
}
///
/// 读取控制器 IO。
///
/// IO 索引。
/// IO 类型字符串。
/// 取消令牌。
/// IO 读取响应。
public async Task GetIoAsync(int port, string ioType, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackGetIoCommand(FanucIoTypes.FromName(ioType), port);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseIoResponse(response));
}
///
/// 设置控制器 IO。
///
/// IO 索引。
/// 目标 IO 值。
/// IO 类型字符串。
/// 取消令牌。
/// 结果响应。
public async Task SetIoAsync(int port, bool value, string ioType, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackSetIoCommand(FanucIoTypes.FromName(ioType), port, value);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseResultResponse(response));
}
///
/// 释放客户端资源。
///
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_stream?.Dispose();
_stream = null;
_tcpClient?.Dispose();
_tcpClient = null;
_sendLock.Dispose();
}
///
/// 直接发送已封装的帧并读取响应。
///
private async Task SendRawFrameAsync(byte[] frame, CancellationToken cancellationToken)
{
if (_stream is null)
{
throw new InvalidOperationException("命令通道未连接。");
}
await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
await _stream.WriteAsync(frame, cancellationToken).ConfigureAwait(false);
return await ReadResponseFrameAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_sendLock.Release();
}
}
///
/// 校验普通命令响应结果码,失败时抛出包含消息号和结果码的诊断异常。
///
private static FanucCommandResultResponse EnsureSuccess(FanucCommandResultResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
///
/// 校验程序状态响应结果码,失败时抛出包含消息号和结果码的诊断异常。
///
private static FanucProgramStatusResponse EnsureSuccess(FanucProgramStatusResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
///
/// 校验速度倍率响应结果码,失败时抛出包含消息号和结果码的诊断异常。
///
private static FanucSpeedRatioResponse EnsureSuccess(FanucSpeedRatioResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
///
/// 校验 TCP 位姿响应结果码,失败时抛出包含消息号和结果码的诊断异常。
///
private static FanucTcpResponse EnsureSuccess(FanucTcpResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
///
/// 校验 IO 读取响应结果码,失败时抛出包含消息号和结果码的诊断异常。
///
private static FanucIoResponse EnsureSuccess(FanucIoResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
///
/// 构造包含 FANUC 命令上下文的失败异常。
///
private static InvalidOperationException CreateCommandFailureException(uint messageId, uint resultCode)
{
return new InvalidOperationException(
$"FANUC command 0x{messageId:X4} failed with result_code {resultCode}.");
}
///
/// 从流中读取一条完整的 doz/zod 响应帧。
///
private async Task ReadResponseFrameAsync(CancellationToken cancellationToken)
{
if (_stream is null)
{
throw new InvalidOperationException("命令通道未连接。");
}
// 先读取 11 字节头:doz(3) + length(4) + msg_id(4)
var header = new byte[11];
await ReadExactAsync(header, cancellationToken).ConfigureAwait(false);
if (header[0] != (byte)'d' || header[1] != (byte)'o' || header[2] != (byte)'z')
{
throw new InvalidDataException("响应帧头 magic 不正确。");
}
var declaredLength = System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(header.AsSpan(3, 4));
if (declaredLength < 14)
{
throw new InvalidDataException("响应帧声明长度过短。");
}
var remaining = (int)declaredLength - 11;
var frame = new byte[declaredLength];
header.CopyTo(frame, 0);
await ReadExactAsync(frame.AsMemory(11, remaining), cancellationToken).ConfigureAwait(false);
// 校验帧尾
if (frame[^3] != (byte)'z' || frame[^2] != (byte)'o' || frame[^1] != (byte)'d')
{
throw new InvalidDataException("响应帧尾 magic 不正确。");
}
return frame;
}
///
/// 从流中精确读取指定长度的字节。
///
private async Task ReadExactAsync(Memory buffer, CancellationToken cancellationToken)
{
if (_stream is null)
{
throw new InvalidOperationException("命令通道未连接。");
}
var totalRead = 0;
while (totalRead < buffer.Length)
{
var read = await _stream.ReadAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false);
if (read == 0)
{
throw new IOException("命令通道已断开,读取到 EOF。");
}
totalRead += read;
}
}
}