Files
FlyShotHost/src/Flyshot.Runtime.Fanuc/Protocol/FanucStateProtocol.cs
yunxiao.zhu af65ca03a0 feat(compat): 补齐飞拍执行等待与 FANUC 状态驱动链路
- 为 ExecuteFlyShotTraj 补齐 wait 语义,并让 move_to_start
  先完成临时 PTP 运动后再启动正式飞拍轨迹
- 将 J519 命令发送改为由机器人 UDP status sequence 驱动,
  避免在未收到状态包时主动发周期命令
- 将 10010 状态通道关节字段统一按 JointRadians 命名,
  同步更新运行时读取逻辑与协议测试
- 新增 FANUC 10010 状态帧、流运动手册和 Python client
  逆向文档,并更新 README 与兼容需求说明
- 补充兼容层编排测试与 HTTP 集成测试,覆盖 wait 和
  move_to_start 串行化行为
2026-05-03 19:29:31 +08:00

188 lines
6.5 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.Buffers.Binary;
namespace Flyshot.Runtime.Fanuc.Protocol;
/// <summary>
/// 表示 FANUC TCP 10010 状态通道中的单个状态帧。
/// </summary>
public sealed class FanucStateFrame
{
private readonly double[] _pose;
private readonly double[] _jointOrExtensionValues;
private readonly double[] _jointRadians;
private readonly double[] _externalAxes;
private readonly uint[] _tailWords;
/// <summary>
/// 初始化状态帧解析结果。
/// </summary>
/// <param name="messageId">状态帧消息号或序号。</param>
/// <param name="pose">控制器回传的笛卡尔位姿。</param>
/// <param name="jointOrExtensionValues">控制器回传的关节或扩展轴状态。</param>
/// <param name="tailWords">状态帧尾部状态槽位。</param>
public FanucStateFrame(
uint messageId,
IEnumerable<double> pose,
IEnumerable<double> jointOrExtensionValues,
IEnumerable<uint> tailWords)
{
MessageId = messageId;
_pose = pose?.ToArray() ?? throw new ArgumentNullException(nameof(pose));
_jointOrExtensionValues = jointOrExtensionValues?.ToArray() ?? throw new ArgumentNullException(nameof(jointOrExtensionValues));
_tailWords = tailWords?.ToArray() ?? throw new ArgumentNullException(nameof(tailWords));
if (_pose.Length != 6)
{
throw new ArgumentException("状态帧位姿必须包含 6 个 float。", nameof(pose));
}
if (_jointOrExtensionValues.Length != 9)
{
throw new ArgumentException("状态帧关节/扩展轴必须包含 9 个 float。", nameof(jointOrExtensionValues));
}
if (_tailWords.Length != 4)
{
throw new ArgumentException("状态帧尾部状态字必须包含 4 个 u32。", nameof(tailWords));
}
_jointRadians = _jointOrExtensionValues.Take(6).ToArray();
_externalAxes = _jointOrExtensionValues.Skip(6).ToArray();
}
/// <summary>
/// 获取状态帧消息号或序号。
/// </summary>
public uint MessageId { get; }
/// <summary>
/// 获取控制器回传的笛卡尔位姿。
/// </summary>
public IReadOnlyList<double> Pose => _pose;
/// <summary>
/// 获取控制器回传的笛卡尔位姿 X/Y/Z/W/P/R单位来自 FANUC 状态服务器。
/// </summary>
public IReadOnlyList<double> CartesianPose => _pose;
/// <summary>
/// 获取控制器回传的关节或扩展轴状态。
/// </summary>
public IReadOnlyList<double> JointOrExtensionValues => _jointOrExtensionValues;
/// <summary>
/// 获取前 6 个机器人关节角度,当前现场抓包更支持按弧度制理解。
/// </summary>
public IReadOnlyList<double> JointRadians => _jointRadians;
/// <summary>
/// 获取后 3 个扩展轴槽位。当前现场样本中这些值通常为 0。
/// </summary>
public IReadOnlyList<double> ExternalAxes => _externalAxes;
/// <summary>
/// 获取状态帧尾部状态槽位。
/// </summary>
public IReadOnlyList<uint> TailWords => _tailWords;
/// <summary>
/// 获取原始尾部状态字。当前抓包中恒为 [2,0,0,1],语义暂不强行推断。
/// </summary>
public IReadOnlyList<uint> RawTailWords => _tailWords;
/// <summary>
/// 获取第 0 个原始尾部状态字。
/// </summary>
public uint StatusWord0 => _tailWords[0];
/// <summary>
/// 获取第 1 个原始尾部状态字。
/// </summary>
public uint StatusWord1 => _tailWords[1];
/// <summary>
/// 获取第 2 个原始尾部状态字。
/// </summary>
public uint StatusWord2 => _tailWords[2];
/// <summary>
/// 获取第 3 个原始尾部状态字。
/// </summary>
public uint StatusWord3 => _tailWords[3];
}
/// <summary>
/// 提供 FANUC TCP 10010 状态通道固定帧解析能力。
/// </summary>
public static class FanucStateProtocol
{
/// <summary>
/// FANUC 状态通道抓包确认的完整帧长度。
/// </summary>
public const int StateFrameLength = 90;
/// <summary>
/// 解析 TCP 10010 状态通道中的单个完整状态帧。
/// </summary>
/// <param name="frame">完整状态帧。</param>
/// <returns>状态帧解析结果。</returns>
public static FanucStateFrame ParseFrame(ReadOnlySpan<byte> frame)
{
ValidateFrame(frame);
var pose = new double[6];
var jointOrExtensionValues = new double[9];
var tailWords = new uint[4];
// 状态帧采用固定布局,偏移来自抓包与 StateServer 逆向结论。
for (var index = 0; index < pose.Length; index++)
{
pose[index] = BinaryPrimitives.ReadSingleBigEndian(frame.Slice(11 + (index * sizeof(float)), sizeof(float)));
}
for (var index = 0; index < jointOrExtensionValues.Length; index++)
{
jointOrExtensionValues[index] = BinaryPrimitives.ReadSingleBigEndian(frame.Slice(35 + (index * sizeof(float)), sizeof(float)));
}
for (var index = 0; index < tailWords.Length; index++)
{
tailWords[index] = BinaryPrimitives.ReadUInt32BigEndian(frame.Slice(71 + (index * sizeof(uint)), sizeof(uint)));
}
return new FanucStateFrame(
BinaryPrimitives.ReadUInt32BigEndian(frame.Slice(7, sizeof(uint))),
pose,
jointOrExtensionValues,
tailWords);
}
/// <summary>
/// 校验状态帧的长度、magic 和长度字段。
/// </summary>
/// <param name="frame">完整状态帧。</param>
private static void ValidateFrame(ReadOnlySpan<byte> frame)
{
if (frame.Length != StateFrameLength)
{
throw new InvalidDataException("FANUC 状态帧长度不符合 TCP 10010 固定帧布局。");
}
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 状态帧长度字段与实际长度不一致。");
}
}
}