Files
FlyShotHost/src/Flyshot.Runtime.Fanuc/Protocol/FanucCommandClient.cs
yunxiao.zhu 69fa3edd89 feat(runtime): 完善 FANUC 命令参数与状态通道重连
* 在 FanucCommandProtocol/Client 中补齐速度倍率、TCP 位姿和
  IO 的封包/解析,并引入 FanucIoTypes 字符串到枚举映射
* FanucControllerRuntime 在非仿真模式下接入真机命令通道,本地
  缓存仅作为兜底,TCP 操作扩展为 7 维 Pose
* FanucStateClient 增加帧超时检测、退避自动重连和诊断状态接口,
  超时或重连期间不再把陈旧帧当作当前机器人状态
* FanucStateProtocol 锁定 90B 帧字段为 pose[6]、joint[6]、
  external_axes[3] 和 raw_tail_words[4],并保留状态字诊断槽位
* ICspPlanner 增加 global_scale > 1.0 失败判定,self-adapt-icsp
  内部禁用该判定以保留补点重试链路
* 同步更新 README/AGENTS/计划文档的 todo 状态和实现说明
2026-04-27 00:18:50 +08:00

406 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net.Sockets;
namespace Flyshot.Runtime.Fanuc.Protocol;
/// <summary>
/// FANUC TCP 10012 命令通道客户端,提供 Req/Res 同步命令下发能力。
/// </summary>
public sealed class FanucCommandClient : IDisposable
{
private readonly SemaphoreSlim _sendLock = new(1, 1);
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private bool _disposed;
/// <summary>
/// 获取当前是否已建立连接。
/// </summary>
public bool IsConnected => _tcpClient?.Connected ?? false;
/// <summary>
/// 建立到 FANUC 控制柜 TCP 10012 命令通道的连接。
/// </summary>
/// <param name="ip">控制柜 IP 地址。</param>
/// <param name="port">命令通道端口,默认 10012。</param>
/// <param name="cancellationToken">取消令牌。</param>
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();
}
/// <summary>
/// 断开命令通道并释放资源。
/// </summary>
public void Disconnect()
{
ObjectDisposedException.ThrowIf(_disposed, this);
_stream?.Dispose();
_stream = null;
_tcpClient?.Dispose();
_tcpClient = null;
}
/// <summary>
/// 发送通用命令并等待响应。
/// </summary>
/// <param name="messageId">命令消息号。</param>
/// <param name="body">命令业务体。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>原始响应帧。</returns>
public async Task<byte[]> SendCommandAsync(uint messageId, ReadOnlyMemory<byte> 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();
}
}
/// <summary>
/// 发送携带程序名的命令并等待响应。
/// </summary>
/// <param name="messageId">命令消息号。</param>
/// <param name="programName">程序名。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>结果响应。</returns>
public async Task<FanucCommandResultResponse> 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));
}
/// <summary>
/// 停止指定程序。
/// </summary>
/// <param name="programName">程序名。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>结果响应。</returns>
public Task<FanucCommandResultResponse> StopProgramAsync(string programName, CancellationToken cancellationToken = default)
{
return SendProgramCommandAsync(FanucCommandMessageIds.StopProgram, programName, cancellationToken);
}
/// <summary>
/// 复位控制器。
/// </summary>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>结果响应。</returns>
public async Task<FanucCommandResultResponse> ResetRobotAsync(CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackEmptyCommand(FanucCommandMessageIds.ResetRobot);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseResultResponse(response));
}
/// <summary>
/// 查询指定程序状态。
/// </summary>
/// <param name="programName">程序名。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>程序状态响应。</returns>
public async Task<FanucProgramStatusResponse> 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));
}
/// <summary>
/// 启动指定程序。
/// </summary>
/// <param name="programName">程序名。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>结果响应。</returns>
public Task<FanucCommandResultResponse> StartProgramAsync(string programName, CancellationToken cancellationToken = default)
{
return SendProgramCommandAsync(FanucCommandMessageIds.StartProgram, programName, cancellationToken);
}
/// <summary>
/// 读取控制器速度倍率。
/// </summary>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>速度倍率响应。</returns>
public async Task<FanucSpeedRatioResponse> GetSpeedRatioAsync(CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackGetSpeedRatioCommand();
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseSpeedRatioResponse(response));
}
/// <summary>
/// 设置控制器速度倍率。
/// </summary>
/// <param name="ratio">目标速度倍率。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>结果响应。</returns>
public async Task<FanucCommandResultResponse> SetSpeedRatioAsync(double ratio, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackSetSpeedRatioCommand(ratio);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseResultResponse(response));
}
/// <summary>
/// 读取控制器 TCP 位姿。
/// </summary>
/// <param name="tcpId">TCP ID。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>TCP 位姿响应。</returns>
public async Task<FanucTcpResponse> 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));
}
/// <summary>
/// 设置控制器 TCP 位姿。
/// </summary>
/// <param name="tcpId">TCP ID。</param>
/// <param name="pose">7 维 TCP 位姿。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>结果响应。</returns>
public async Task<FanucCommandResultResponse> SetTcpAsync(uint tcpId, IReadOnlyList<double> pose, CancellationToken cancellationToken = default)
{
var frame = FanucCommandProtocol.PackSetTcpCommand(tcpId, pose);
var response = await SendRawFrameAsync(frame, cancellationToken).ConfigureAwait(false);
return EnsureSuccess(FanucCommandProtocol.ParseResultResponse(response));
}
/// <summary>
/// 读取控制器 IO。
/// </summary>
/// <param name="port">IO 索引。</param>
/// <param name="ioType">IO 类型字符串。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>IO 读取响应。</returns>
public async Task<FanucIoResponse> 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));
}
/// <summary>
/// 设置控制器 IO。
/// </summary>
/// <param name="port">IO 索引。</param>
/// <param name="value">目标 IO 值。</param>
/// <param name="ioType">IO 类型字符串。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>结果响应。</returns>
public async Task<FanucCommandResultResponse> 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));
}
/// <summary>
/// 释放客户端资源。
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_stream?.Dispose();
_stream = null;
_tcpClient?.Dispose();
_tcpClient = null;
_sendLock.Dispose();
}
/// <summary>
/// 直接发送已封装的帧并读取响应。
/// </summary>
private async Task<byte[]> 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();
}
}
/// <summary>
/// 校验普通命令响应结果码,失败时抛出包含消息号和结果码的诊断异常。
/// </summary>
private static FanucCommandResultResponse EnsureSuccess(FanucCommandResultResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
/// <summary>
/// 校验程序状态响应结果码,失败时抛出包含消息号和结果码的诊断异常。
/// </summary>
private static FanucProgramStatusResponse EnsureSuccess(FanucProgramStatusResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
/// <summary>
/// 校验速度倍率响应结果码,失败时抛出包含消息号和结果码的诊断异常。
/// </summary>
private static FanucSpeedRatioResponse EnsureSuccess(FanucSpeedRatioResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
/// <summary>
/// 校验 TCP 位姿响应结果码,失败时抛出包含消息号和结果码的诊断异常。
/// </summary>
private static FanucTcpResponse EnsureSuccess(FanucTcpResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
/// <summary>
/// 校验 IO 读取响应结果码,失败时抛出包含消息号和结果码的诊断异常。
/// </summary>
private static FanucIoResponse EnsureSuccess(FanucIoResponse response)
{
if (!response.IsSuccess)
{
throw CreateCommandFailureException(response.MessageId, response.ResultCode);
}
return response;
}
/// <summary>
/// 构造包含 FANUC 命令上下文的失败异常。
/// </summary>
private static InvalidOperationException CreateCommandFailureException(uint messageId, uint resultCode)
{
return new InvalidOperationException(
$"FANUC command 0x{messageId:X4} failed with result_code {resultCode}.");
}
/// <summary>
/// 从流中读取一条完整的 doz/zod 响应帧。
/// </summary>
private async Task<byte[]> 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;
}
/// <summary>
/// 从流中精确读取指定长度的字节。
/// </summary>
private async Task ReadExactAsync(Memory<byte> 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;
}
}
}