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; } } }