✨ feat(fanuc): 添加协议编解码与状态页" -m "* 固化 10010 状态帧、10012 命令帧和 60015 J519 包编解码
* 扩展 ControllerClient 兼容层的执行参数和运行时编排 * 新增 /status 页面与 /api/status/snapshot 状态快照接口 * 补充 FANUC 协议、客户端和状态接口的最小验证测试 * 更新 README、兼容要求和真机 Socket 通信实现计划
This commit is contained in:
272
src/Flyshot.Runtime.Fanuc/Protocol/FanucCommandProtocol.cs
Normal file
272
src/Flyshot.Runtime.Fanuc/Protocol/FanucCommandProtocol.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace Flyshot.Runtime.Fanuc.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// 定义 FANUC TCP 10012 命令通道已经由抓包和逆向资料确认的消息号。
|
||||
/// </summary>
|
||||
public static class FanucCommandMessageIds
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取控制器程序状态的消息号。
|
||||
/// </summary>
|
||||
public const uint GetProgramStatus = 0x2003;
|
||||
|
||||
/// <summary>
|
||||
/// 复位控制器的消息号。
|
||||
/// </summary>
|
||||
public const uint ResetRobot = 0x2100;
|
||||
|
||||
/// <summary>
|
||||
/// 启动控制器程序的消息号。
|
||||
/// </summary>
|
||||
public const uint StartProgram = 0x2102;
|
||||
|
||||
/// <summary>
|
||||
/// 停止控制器程序的消息号。
|
||||
/// </summary>
|
||||
public const uint StopProgram = 0x2103;
|
||||
|
||||
/// <summary>
|
||||
/// 读取控制器 TCP 的消息号。
|
||||
/// </summary>
|
||||
public const uint GetTcp = 0x2200;
|
||||
|
||||
/// <summary>
|
||||
/// 设置控制器 TCP 的消息号。
|
||||
/// </summary>
|
||||
public const uint SetTcp = 0x2201;
|
||||
|
||||
/// <summary>
|
||||
/// 读取控制器速度倍率的消息号。
|
||||
/// </summary>
|
||||
public const uint GetSpeedRatio = 0x2206;
|
||||
|
||||
/// <summary>
|
||||
/// 设置控制器速度倍率的消息号。
|
||||
/// </summary>
|
||||
public const uint SetSpeedRatio = 0x2207;
|
||||
|
||||
/// <summary>
|
||||
/// 读取控制器 IO 的消息号。
|
||||
/// </summary>
|
||||
public const uint GetIo = 0x2208;
|
||||
|
||||
/// <summary>
|
||||
/// 设置控制器 IO 的消息号。
|
||||
/// </summary>
|
||||
public const uint SetIo = 0x2209;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 FANUC TCP 10012 命令通道中只携带结果码的响应。
|
||||
/// </summary>
|
||||
public sealed class FanucCommandResultResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化命令结果响应。
|
||||
/// </summary>
|
||||
/// <param name="messageId">响应对应的消息号。</param>
|
||||
/// <param name="resultCode">控制器返回的结果码。</param>
|
||||
public FanucCommandResultResponse(uint messageId, uint resultCode)
|
||||
{
|
||||
MessageId = messageId;
|
||||
ResultCode = resultCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取响应对应的消息号。
|
||||
/// </summary>
|
||||
public uint MessageId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取控制器返回的结果码。
|
||||
/// </summary>
|
||||
public uint ResultCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前响应是否表示成功。
|
||||
/// </summary>
|
||||
public bool IsSuccess => ResultCode == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示 FANUC TCP 10012 程序状态响应。
|
||||
/// </summary>
|
||||
public sealed class FanucProgramStatusResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化程序状态响应。
|
||||
/// </summary>
|
||||
/// <param name="messageId">响应对应的消息号。</param>
|
||||
/// <param name="resultCode">控制器返回的结果码。</param>
|
||||
/// <param name="programStatus">控制器程序状态。</param>
|
||||
public FanucProgramStatusResponse(uint messageId, uint resultCode, uint programStatus)
|
||||
{
|
||||
MessageId = messageId;
|
||||
ResultCode = resultCode;
|
||||
ProgramStatus = programStatus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取响应对应的消息号。
|
||||
/// </summary>
|
||||
public uint MessageId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取控制器返回的结果码。
|
||||
/// </summary>
|
||||
public uint ResultCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取控制器程序状态值。
|
||||
/// </summary>
|
||||
public uint ProgramStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前响应是否表示成功。
|
||||
/// </summary>
|
||||
public bool IsSuccess => ResultCode == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供 FANUC TCP 10012 命令通道的基础封包与响应解析能力。
|
||||
/// </summary>
|
||||
public static class FanucCommandProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// 将无业务体命令封装为 TCP 10012 二进制帧。
|
||||
/// </summary>
|
||||
/// <param name="messageId">命令消息号。</param>
|
||||
/// <returns>可直接写入命令通道 Socket 的完整帧。</returns>
|
||||
public static byte[] PackEmptyCommand(uint messageId)
|
||||
{
|
||||
return PackFrame(messageId, ReadOnlySpan<byte>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将程序名命令封装为 TCP 10012 二进制帧。
|
||||
/// </summary>
|
||||
/// <param name="messageId">命令消息号。</param>
|
||||
/// <param name="programName">控制器程序名。</param>
|
||||
/// <returns>可直接写入命令通道 Socket 的完整帧。</returns>
|
||||
public static byte[] PackProgramCommand(uint messageId, string programName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(programName))
|
||||
{
|
||||
throw new ArgumentException("程序名不能为空。", nameof(programName));
|
||||
}
|
||||
|
||||
var programNameBytes = Encoding.ASCII.GetBytes(programName);
|
||||
var body = new byte[sizeof(uint) + programNameBytes.Length];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(0, sizeof(uint)), (uint)programNameBytes.Length);
|
||||
programNameBytes.CopyTo(body.AsSpan(sizeof(uint)));
|
||||
|
||||
return PackFrame(messageId, body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析只携带结果码的 TCP 10012 响应帧。
|
||||
/// </summary>
|
||||
/// <param name="frame">完整响应帧。</param>
|
||||
/// <returns>命令结果响应。</returns>
|
||||
public static FanucCommandResultResponse ParseResultResponse(ReadOnlySpan<byte> frame)
|
||||
{
|
||||
var messageId = ValidateAndReadMessageId(frame);
|
||||
var body = GetBody(frame);
|
||||
if (body.Length < sizeof(uint))
|
||||
{
|
||||
throw new InvalidDataException("FANUC 命令响应体长度不足。");
|
||||
}
|
||||
|
||||
return new FanucCommandResultResponse(
|
||||
messageId,
|
||||
BinaryPrimitives.ReadUInt32BigEndian(body[..sizeof(uint)]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 GetProgStatus 的 TCP 10012 响应帧。
|
||||
/// </summary>
|
||||
/// <param name="frame">完整响应帧。</param>
|
||||
/// <returns>程序状态响应。</returns>
|
||||
public static FanucProgramStatusResponse ParseProgramStatusResponse(ReadOnlySpan<byte> frame)
|
||||
{
|
||||
var messageId = ValidateAndReadMessageId(frame);
|
||||
var body = GetBody(frame);
|
||||
if (body.Length < sizeof(uint) * 2)
|
||||
{
|
||||
throw new InvalidDataException("FANUC 程序状态响应体长度不足。");
|
||||
}
|
||||
|
||||
// 抓包样本中的字段顺序为 result_code 后接 prog_status。
|
||||
var resultCode = BinaryPrimitives.ReadUInt32BigEndian(body[..sizeof(uint)]);
|
||||
var programStatus = BinaryPrimitives.ReadUInt32BigEndian(body.Slice(sizeof(uint), sizeof(uint)));
|
||||
return new FanucProgramStatusResponse(messageId, resultCode, programStatus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 FANUC 命令通道 framing 规则封装完整帧。
|
||||
/// </summary>
|
||||
/// <param name="messageId">命令消息号。</param>
|
||||
/// <param name="body">业务体。</param>
|
||||
/// <returns>完整命令帧。</returns>
|
||||
internal static byte[] PackFrame(uint messageId, ReadOnlySpan<byte> body)
|
||||
{
|
||||
var frameLength = 3 + sizeof(uint) + sizeof(uint) + body.Length + 3;
|
||||
var frame = new byte[frameLength];
|
||||
|
||||
frame[0] = (byte)'d';
|
||||
frame[1] = (byte)'o';
|
||||
frame[2] = (byte)'z';
|
||||
BinaryPrimitives.WriteUInt32BigEndian(frame.AsSpan(3, sizeof(uint)), (uint)frameLength);
|
||||
BinaryPrimitives.WriteUInt32BigEndian(frame.AsSpan(7, sizeof(uint)), messageId);
|
||||
body.CopyTo(frame.AsSpan(11));
|
||||
frame[^3] = (byte)'z';
|
||||
frame[^2] = (byte)'o';
|
||||
frame[^1] = (byte)'d';
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验完整帧并读取消息号。
|
||||
/// </summary>
|
||||
/// <param name="frame">完整响应帧。</param>
|
||||
/// <returns>响应消息号。</returns>
|
||||
private static uint ValidateAndReadMessageId(ReadOnlySpan<byte> frame)
|
||||
{
|
||||
if (frame.Length < 14)
|
||||
{
|
||||
throw new InvalidDataException("FANUC 命令帧长度不足。");
|
||||
}
|
||||
|
||||
if (frame[0] != (byte)'d' || frame[1] != (byte)'o' || frame[2] != (byte)'z')
|
||||
{
|
||||
throw new InvalidDataException("FANUC 命令帧头 magic 不正确。");
|
||||
}
|
||||
|
||||
if (frame[^3] != (byte)'z' || frame[^2] != (byte)'o' || frame[^1] != (byte)'d')
|
||||
{
|
||||
throw new InvalidDataException("FANUC 命令帧尾 magic 不正确。");
|
||||
}
|
||||
|
||||
var declaredLength = BinaryPrimitives.ReadUInt32BigEndian(frame.Slice(3, sizeof(uint)));
|
||||
if (declaredLength != frame.Length)
|
||||
{
|
||||
throw new InvalidDataException("FANUC 命令帧长度字段与实际长度不一致。");
|
||||
}
|
||||
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(frame.Slice(7, sizeof(uint)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取完整帧中的业务体切片。
|
||||
/// </summary>
|
||||
/// <param name="frame">完整响应帧。</param>
|
||||
/// <returns>业务体切片。</returns>
|
||||
private static ReadOnlySpan<byte> GetBody(ReadOnlySpan<byte> frame)
|
||||
{
|
||||
return frame.Slice(11, frame.Length - 14);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user