Files
FlyShotHost/tests/Flyshot.Core.Tests/FanucProtocolTests.cs
yunxiao.zhu 70b0ccd414 feat(fanuc): 优化 J519 实时下发与飞拍起停整形
- 改为高优先级 J519 接收线程与复用缓冲区发送链路
- 增加稠密执行前的 J519 就绪重试与状态诊断
- 修正程序状态响应字段顺序与 EnableRobot 默认参数
- 为飞拍轨迹补充平滑起停时间轴与首尾整形验证
- 补充真实运行配置、报警窗口与边界对比测试
- 同步更新限值文档、分析脚本与 .NET 8 SDK 固定配置
2026-05-06 22:37:31 +08:00

239 lines
11 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;
using Flyshot.Runtime.Fanuc.Protocol;
namespace Flyshot.Core.Tests;
/// <summary>
/// 验证 FANUC 真机三条通信链路的二进制协议基础与逆向抓包样本一致。
/// </summary>
public sealed class FanucProtocolTests
{
/// <summary>
/// 验证 TCP 10012 程序命令封包与抓包中的 StopProg("RVBUSTSM") 完全一致。
/// </summary>
[Fact]
public void CommandProtocol_PacksCapturedStopProgramFrame()
{
var frame = FanucCommandProtocol.PackProgramCommand(FanucCommandMessageIds.StopProgram, "RVBUSTSM");
Assert.Equal(
Convert.FromHexString("646f7a0000001a0000210300000008525642555354534d7a6f64"),
frame);
}
/// <summary>
/// 验证 TCP 10012 短响应和程序状态响应可以按抓包字段解析。
/// </summary>
[Fact]
public void CommandProtocol_ParsesCapturedResponses()
{
var stopResponse = FanucCommandProtocol.ParseResultResponse(
Convert.FromHexString("646f7a0000001200002103000000007a6f64"));
var statusResponse = FanucCommandProtocol.ParseProgramStatusResponse(
Convert.FromHexString("646f7a000000160000200300000001000000007a6f64"));
Assert.Equal(FanucCommandMessageIds.StopProgram, stopResponse.MessageId);
Assert.True(stopResponse.IsSuccess);
Assert.Equal(FanucCommandMessageIds.GetProgramStatus, statusResponse.MessageId);
Assert.True(statusResponse.IsSuccess);
Assert.Equal(1u, statusResponse.ProgramStatus);
}
/// <summary>
/// 验证 TCP 10012 的速度倍率、TCP 和 IO 请求体字段顺序与逆向文档一致。
/// </summary>
[Fact]
public void CommandProtocol_PacksParameterCommandBodies()
{
var setTcpFrame = FanucCommandProtocol.PackSetTcpCommand(1, [1.0, 2.0, 3.0, 0.0, 0.0, 0.0, 1.0]);
Assert.Equal(
Convert.FromHexString("646f7a0000000e000022067a6f64"),
FanucCommandProtocol.PackGetSpeedRatioCommand());
Assert.Equal(
Convert.FromHexString("646f7a0000001200002207000000507a6f64"),
FanucCommandProtocol.PackSetSpeedRatioCommand(0.8));
Assert.Equal(
Convert.FromHexString("646f7a0000001200002200000000017a6f64"),
FanucCommandProtocol.PackGetTcpCommand(1));
Assert.Equal(
Convert.FromHexString("646f7a000000160000220800000002000000077a6f64"),
FanucCommandProtocol.PackGetIoCommand(FanucIoTypes.DigitalOutput, 7));
Assert.Equal(
Convert.FromHexString("646f7a0000001a0000220900000002000000073f8000007a6f64"),
FanucCommandProtocol.PackSetIoCommand(FanucIoTypes.DigitalOutput, 7, true));
Assert.Equal(FanucCommandMessageIds.SetTcp, BinaryPrimitives.ReadUInt32BigEndian(setTcpFrame.AsSpan(7, 4)));
Assert.Equal(1u, BinaryPrimitives.ReadUInt32BigEndian(setTcpFrame.AsSpan(11, 4)));
Assert.Equal(1.0f, BinaryPrimitives.ReadSingleBigEndian(setTcpFrame.AsSpan(15, 4)));
Assert.Equal(1.0f, BinaryPrimitives.ReadSingleBigEndian(setTcpFrame.AsSpan(39, 4)));
}
/// <summary>
/// 验证 TCP 10012 参数响应解析使用各自不同的字段顺序。
/// </summary>
[Fact]
public void CommandProtocol_ParsesParameterResponses()
{
var speedRatioResponse = FanucCommandProtocol.ParseSpeedRatioResponse(
FanucCommandProtocol.PackFrame(
FanucCommandMessageIds.GetSpeedRatio,
Convert.FromHexString("0000005000000000")));
var tcpResponse = FanucCommandProtocol.ParseTcpResponse(
FanucCommandProtocol.PackFrame(
FanucCommandMessageIds.GetTcp,
Convert.FromHexString("00000000000000013f80000040000000404000000000000000000000000000003f800000")));
var ioResponse = FanucCommandProtocol.ParseIoResponse(
FanucCommandProtocol.PackFrame(
FanucCommandMessageIds.GetIo,
Convert.FromHexString("000000003f800000")));
Assert.True(speedRatioResponse.IsSuccess);
Assert.Equal(0.8, speedRatioResponse.Ratio, precision: 6);
Assert.True(tcpResponse.IsSuccess);
Assert.Equal(1u, tcpResponse.TcpId);
Assert.Equal([1.0, 2.0, 3.0, 0.0, 0.0, 0.0, 1.0], tcpResponse.Pose);
Assert.True(ioResponse.IsSuccess);
Assert.True(ioResponse.Value);
Assert.Equal(1.0, ioResponse.NumericValue, precision: 6);
}
/// <summary>
/// 验证 TCP 10010 状态帧可以从抓包样本解析出尾部状态槽位。
/// </summary>
[Fact]
public void StateProtocol_ParsesCapturedStateFrame()
{
var frame = FanucStateProtocol.ParseFrame(Convert.FromHexString(
"646f7a0000005a000000004388a23243f1ed7f43e9de6bc265031ec2b33cc3c278e0153f8742f53c3f128dbc929529bc7861d63cb0184c3c1ca1a7000000000000000000000000000000020000000000000000000000017a6f64"));
Assert.Equal(0u, frame.MessageId);
Assert.Equal(6, frame.Pose.Count);
Assert.Equal(9, frame.JointOrExtensionValues.Count);
Assert.Equal([2u, 0u, 0u, 1u], frame.TailWords);
Assert.Equal(frame.Pose, frame.CartesianPose);
Assert.Equal(frame.JointOrExtensionValues.Take(6), frame.JointRadians);
Assert.Equal(frame.JointOrExtensionValues.Skip(6), frame.ExternalAxes);
Assert.Equal(frame.TailWords, frame.RawTailWords);
Assert.Equal(2u, frame.StatusWord0);
Assert.Equal(0u, frame.StatusWord1);
Assert.Equal(0u, frame.StatusWord2);
Assert.Equal(1u, frame.StatusWord3);
}
/// <summary>
/// 验证 pcap 中多条唯一 TCP 10010 状态帧都符合固定 90B 布局。
/// </summary>
[Theory]
[InlineData("646f7a0000005a0000000040eac85a43b2ef4043aba8e9421ed9c1c2828105c2ed981f3fbdbda0bed4764ebe92aacc3efd9f0a3f317ce9be5d4580000000000000000000000000000000020000000000000000000000017a6f64")]
[InlineData("646f7a0000005a00000000415aab64440a5302439adef542b39739c293c441431d50423fcdb7003d862fe3beca5730bf60eab23f148e403f89269d000000000000000000000000000000020000000000000000000000017a6f64")]
[InlineData("646f7a0000005a000000004221b6f9440b9ce043a129ac42b292bac29cba78431bddcb3fc743213d90268dbeba5351bf64bc1b3f0cbdf73f826864000000000000000000000000000000020000000000000000000000017a6f64")]
public void StateProtocol_ParsesMultipleCapturedPcapFrames(string frameHex)
{
var frameBytes = Convert.FromHexString(frameHex);
var frame = FanucStateProtocol.ParseFrame(frameBytes);
Assert.Equal(FanucStateProtocol.StateFrameLength, frameBytes.Length);
Assert.Equal(6, frame.CartesianPose.Count);
Assert.Equal(6, frame.JointRadians.Count);
Assert.Equal(3, frame.ExternalAxes.Count);
Assert.Equal([2u, 0u, 0u, 1u], frame.RawTailWords);
}
/// <summary>
/// 验证 TCP 10010 状态帧会拒绝损坏的长度和 magic避免后台循环缓存坏帧。
/// </summary>
[Fact]
public void StateProtocol_RejectsMalformedStateFrames()
{
var validFrame = Convert.FromHexString(
"646f7a0000005a000000004388a23243f1ed7f43e9de6bc265031ec2b33cc3c278e0153f8742f53c3f128dbc929529bc7861d63cb0184c3c1ca1a7000000000000000000000000000000020000000000000000000000017a6f64");
var wrongMagic = validFrame.ToArray();
wrongMagic[0] = 0;
var wrongLength = validFrame.ToArray();
wrongLength[6] = 0x59;
Assert.Throws<InvalidDataException>(() => FanucStateProtocol.ParseFrame(validFrame.AsSpan(0, validFrame.Length - 1)));
Assert.Throws<InvalidDataException>(() => FanucStateProtocol.ParseFrame(wrongMagic));
Assert.Throws<InvalidDataException>(() => FanucStateProtocol.ParseFrame(wrongLength));
}
/// <summary>
/// 验证 UDP 60015 的 J519 初始化、结束和命令包字段布局。
/// </summary>
[Fact]
public void J519Protocol_PacksControlAndCommandPackets()
{
var command = new FanucJ519Command(
sequence: 2,
targetJoints: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
var packet = FanucJ519Protocol.PackCommandPacket(command);
Assert.Equal(Convert.FromHexString("0000000000000001"), FanucJ519Protocol.PackInitPacket());
Assert.Equal(Convert.FromHexString("0000000200000001"), FanucJ519Protocol.PackEndPacket());
Assert.Equal(FanucJ519Protocol.CommandPacketLength, packet.Length);
Assert.Equal(1u, BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(0x00, 4)));
Assert.Equal(1u, BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(0x04, 4)));
Assert.Equal(2u, BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(0x08, 4)));
Assert.Equal(2, packet[0x0d]);
Assert.Equal(1, packet[0x12]);
Assert.Equal(1.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x1c, 4)));
Assert.Equal(6.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x30, 4)));
Assert.Equal(0.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x38, 4)));
}
/// <summary>
/// 验证实时发送路径可以复用命令对象,并用机器人状态包序号覆盖出包序号。
/// </summary>
[Fact]
public void J519Protocol_PacksCommandWithOverrideSequenceIntoReusableBuffer()
{
var command = new FanucJ519Command(
sequence: 2,
targetJoints: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
var packet = new byte[FanucJ519Protocol.CommandPacketLength];
FanucJ519Protocol.PackCommandPacket(command, sequence: 456, packet);
Assert.Equal(456u, BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(0x08, 4)));
Assert.Equal(1.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x1c, 4)));
Assert.Equal(6.0f, BinaryPrimitives.ReadSingleBigEndian(packet.AsSpan(0x30, 4)));
}
/// <summary>
/// 验证 UDP 60015 的 132 字节响应包字段可以被解析成状态位和关节反馈。
/// </summary>
[Fact]
public void J519Protocol_ParsesResponsePacket()
{
var packet = new byte[FanucJ519Protocol.ResponsePacketLength];
BinaryPrimitives.WriteUInt32BigEndian(packet.AsSpan(0x00, 4), 0);
BinaryPrimitives.WriteUInt32BigEndian(packet.AsSpan(0x04, 4), 1);
BinaryPrimitives.WriteUInt32BigEndian(packet.AsSpan(0x08, 4), 12);
packet[0x0c] = 15;
packet[0x0d] = 2;
BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(0x0e, 2), 1);
BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(0x10, 2), 255);
BinaryPrimitives.WriteUInt16BigEndian(packet.AsSpan(0x12, 2), 10);
BinaryPrimitives.WriteUInt32BigEndian(packet.AsSpan(0x14, 4), 1234);
BinaryPrimitives.WriteSingleBigEndian(packet.AsSpan(0x18, 4), 100.5f);
BinaryPrimitives.WriteSingleBigEndian(packet.AsSpan(0x3c, 4), 1.25f);
BinaryPrimitives.WriteSingleBigEndian(packet.AsSpan(0x60, 4), 2.5f);
var response = FanucJ519Protocol.ParseResponse(packet);
Assert.Equal(12u, response.Sequence);
Assert.Equal(15, response.Status);
Assert.True(response.AcceptsCommand);
Assert.True(response.ReceivedCommand);
Assert.True(response.SystemReady);
Assert.True(response.RobotInMotion);
Assert.Equal(10, response.ReadIoValue);
Assert.Equal(1234u, response.Timestamp);
Assert.Equal(100.5, response.Pose[0], precision: 6);
Assert.Equal(1.25, response.JointDegrees[0], precision: 6);
Assert.Equal(2.5, response.MotorCurrents[0], precision: 6);
}
}