Files
FlyShotHost/tests/Flyshot.Core.Tests/FanucProtocolTests.cs

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