using System.Buffers.Binary; using System.Text; namespace Flyshot.Runtime.Fanuc.Protocol; /// /// 定义 FANUC TCP 10012 命令通道已经由抓包和逆向资料确认的消息号。 /// public static class FanucCommandMessageIds { /// /// 获取控制器程序状态的消息号。 /// public const uint GetProgramStatus = 0x2003; /// /// 复位控制器的消息号。 /// public const uint ResetRobot = 0x2100; /// /// 启动控制器程序的消息号。 /// public const uint StartProgram = 0x2102; /// /// 停止控制器程序的消息号。 /// public const uint StopProgram = 0x2103; /// /// 读取控制器 TCP 的消息号。 /// public const uint GetTcp = 0x2200; /// /// 设置控制器 TCP 的消息号。 /// public const uint SetTcp = 0x2201; /// /// 读取控制器速度倍率的消息号。 /// public const uint GetSpeedRatio = 0x2206; /// /// 设置控制器速度倍率的消息号。 /// public const uint SetSpeedRatio = 0x2207; /// /// 读取控制器 IO 的消息号。 /// public const uint GetIo = 0x2208; /// /// 设置控制器 IO 的消息号。 /// public const uint SetIo = 0x2209; } /// /// 定义旧 ControllerClient 公开的 FANUC IO 类型枚举值。 /// public static class FanucIoTypes { /// /// FANUC 数字输入 DI。 /// public const uint DigitalInput = 1; /// /// FANUC 数字输出 DO。 /// public const uint DigitalOutput = 2; /// /// FANUC 机器人输入 RI。 /// public const uint RobotInput = 8; /// /// FANUC 机器人输出 RO。 /// public const uint RobotOutput = 9; /// /// 将 HTTP/兼容层传入的 IO 类型字符串转换为 FANUC 命令通道枚举值。 /// /// IO 类型字符串,例如 DI、DO、RI、RO。 /// 命令通道使用的 IO 类型数值。 public static uint FromName(string ioType) { if (string.IsNullOrWhiteSpace(ioType)) { throw new ArgumentException("IO 类型不能为空。", nameof(ioType)); } return ioType.Trim().ToUpperInvariant() switch { "DI" or "KIOTYPEDI" => DigitalInput, "DO" or "KIOTYPEDO" => DigitalOutput, "RI" or "KIOTYPERI" => RobotInput, "RO" or "KIOTYPERO" => RobotOutput, _ => throw new ArgumentOutOfRangeException(nameof(ioType), ioType, "未知 IO 类型。") }; } } /// /// 表示 FANUC TCP 10012 命令通道中只携带结果码的响应。 /// public sealed class FanucCommandResultResponse { /// /// 初始化命令结果响应。 /// /// 响应对应的消息号。 /// 控制器返回的结果码。 public FanucCommandResultResponse(uint messageId, uint resultCode) { MessageId = messageId; ResultCode = resultCode; } /// /// 获取响应对应的消息号。 /// public uint MessageId { get; } /// /// 获取控制器返回的结果码。 /// public uint ResultCode { get; } /// /// 获取当前响应是否表示成功。 /// public bool IsSuccess => ResultCode == 0; } /// /// 表示 FANUC TCP 10012 速度倍率响应。 /// public sealed class FanucSpeedRatioResponse { /// /// 初始化速度倍率响应。 /// /// 响应对应的消息号。 /// 控制器返回的整数百分比。 /// 控制器返回的结果码。 public FanucSpeedRatioResponse(uint messageId, uint ratioInt, uint resultCode) { MessageId = messageId; RatioInt = ratioInt; ResultCode = resultCode; } /// /// 获取响应对应的消息号。 /// public uint MessageId { get; } /// /// 获取控制器返回的整数百分比。 /// public uint RatioInt { get; } /// /// 获取控制器返回的结果码。 /// public uint ResultCode { get; } /// /// 获取转换后的 0.0 到 1.0 速度倍率。 /// public double Ratio => RatioInt / 100.0; /// /// 获取当前响应是否表示成功。 /// public bool IsSuccess => ResultCode == 0; } /// /// 表示 FANUC TCP 10012 TCP 位姿响应。 /// public sealed class FanucTcpResponse { /// /// 初始化 TCP 位姿响应。 /// /// 响应对应的消息号。 /// 控制器返回的结果码。 /// 控制器返回的 TCP ID。 /// 7 维 TCP 位姿。 public FanucTcpResponse(uint messageId, uint resultCode, uint tcpId, IReadOnlyList pose) { MessageId = messageId; ResultCode = resultCode; TcpId = tcpId; Pose = pose.ToArray(); } /// /// 获取响应对应的消息号。 /// public uint MessageId { get; } /// /// 获取控制器返回的结果码。 /// public uint ResultCode { get; } /// /// 获取控制器返回的 TCP ID。 /// public uint TcpId { get; } /// /// 获取 7 维 TCP 位姿。 /// public IReadOnlyList Pose { get; } /// /// 获取当前响应是否表示成功。 /// public bool IsSuccess => ResultCode == 0; } /// /// 表示 FANUC TCP 10012 IO 读取响应。 /// public sealed class FanucIoResponse { /// /// 初始化 IO 读取响应。 /// /// 响应对应的消息号。 /// 控制器返回的结果码。 /// 控制器返回的 float IO 数值。 public FanucIoResponse(uint messageId, uint resultCode, double numericValue) { MessageId = messageId; ResultCode = resultCode; NumericValue = numericValue; } /// /// 获取响应对应的消息号。 /// public uint MessageId { get; } /// /// 获取控制器返回的结果码。 /// public uint ResultCode { get; } /// /// 获取控制器返回的原始数值。 /// public double NumericValue { get; } /// /// 获取按布尔 IO 解释后的值。 /// public bool Value => Math.Abs(NumericValue) > double.Epsilon; /// /// 获取当前响应是否表示成功。 /// public bool IsSuccess => ResultCode == 0; } /// /// 表示 FANUC TCP 10012 程序状态响应。 /// public sealed class FanucProgramStatusResponse { /// /// 初始化程序状态响应。 /// /// 响应对应的消息号。 /// 控制器返回的结果码。 /// 控制器程序状态。 public FanucProgramStatusResponse(uint messageId, uint resultCode, uint programStatus) { MessageId = messageId; ResultCode = resultCode; ProgramStatus = programStatus; } /// /// 获取响应对应的消息号。 /// public uint MessageId { get; } /// /// 获取控制器返回的结果码。 /// public uint ResultCode { get; } /// /// 获取控制器程序状态值。 /// public uint ProgramStatus { get; } /// /// 获取当前响应是否表示成功。 /// public bool IsSuccess => ResultCode == 0; } /// /// 提供 FANUC TCP 10012 命令通道的基础封包与响应解析能力。 /// public static class FanucCommandProtocol { /// /// 将无业务体命令封装为 TCP 10012 二进制帧。 /// /// 命令消息号。 /// 可直接写入命令通道 Socket 的完整帧。 public static byte[] PackEmptyCommand(uint messageId) { return PackFrame(messageId, ReadOnlySpan.Empty); } /// /// 将程序名命令封装为 TCP 10012 二进制帧。 /// /// 命令消息号。 /// 控制器程序名。 /// 可直接写入命令通道 Socket 的完整帧。 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); } /// /// 封装读取速度倍率命令。 /// /// 可直接写入命令通道 Socket 的完整帧。 public static byte[] PackGetSpeedRatioCommand() { return PackEmptyCommand(FanucCommandMessageIds.GetSpeedRatio); } /// /// 封装设置速度倍率命令,按旧系统逻辑转换为 0..100 的整数百分比。 /// /// 目标速度倍率。 /// 可直接写入命令通道 Socket 的完整帧。 public static byte[] PackSetSpeedRatioCommand(double ratio) { if (double.IsNaN(ratio) || double.IsInfinity(ratio)) { throw new ArgumentOutOfRangeException(nameof(ratio), "ratio 必须是有限数值。"); } var ratioInt = (uint)Math.Clamp((int)(ratio * 100.0), 0, 100); var body = new byte[sizeof(uint)]; BinaryPrimitives.WriteUInt32BigEndian(body, ratioInt); return PackFrame(FanucCommandMessageIds.SetSpeedRatio, body); } /// /// 封装读取 TCP 位姿命令。 /// /// 目标 TCP ID。 /// 可直接写入命令通道 Socket 的完整帧。 public static byte[] PackGetTcpCommand(uint tcpId) { var body = new byte[sizeof(uint)]; BinaryPrimitives.WriteUInt32BigEndian(body, tcpId); return PackFrame(FanucCommandMessageIds.GetTcp, body); } /// /// 封装设置 TCP 位姿命令。 /// /// 目标 TCP ID。 /// 7 维 TCP 位姿。 /// 可直接写入命令通道 Socket 的完整帧。 public static byte[] PackSetTcpCommand(uint tcpId, IReadOnlyList pose) { ArgumentNullException.ThrowIfNull(pose); if (pose.Count != 7) { throw new ArgumentException("TCP 位姿必须包含 7 个数值。", nameof(pose)); } var body = new byte[sizeof(uint) + sizeof(float) * 7]; BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(0, sizeof(uint)), tcpId); for (int i = 0; i < 7; i++) { BinaryPrimitives.WriteSingleBigEndian(body.AsSpan(sizeof(uint) + i * sizeof(float), sizeof(float)), (float)pose[i]); } return PackFrame(FanucCommandMessageIds.SetTcp, body); } /// /// 封装读取 IO 命令,字段顺序为 io_type 后接 io_index。 /// /// IO 类型数值。 /// IO 索引。 /// 可直接写入命令通道 Socket 的完整帧。 public static byte[] PackGetIoCommand(uint ioType, int ioIndex) { if (ioIndex < 0) { throw new ArgumentOutOfRangeException(nameof(ioIndex), "IO 索引不能为负数。"); } var body = new byte[sizeof(uint) * 2]; BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(0, sizeof(uint)), ioType); BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(sizeof(uint), sizeof(uint)), (uint)ioIndex); return PackFrame(FanucCommandMessageIds.GetIo, body); } /// /// 封装设置 IO 命令,字段顺序为 io_type、io_index、float io_value。 /// /// IO 类型数值。 /// IO 索引。 /// 目标 IO 布尔值。 /// 可直接写入命令通道 Socket 的完整帧。 public static byte[] PackSetIoCommand(uint ioType, int ioIndex, bool value) { if (ioIndex < 0) { throw new ArgumentOutOfRangeException(nameof(ioIndex), "IO 索引不能为负数。"); } var body = new byte[sizeof(uint) * 2 + sizeof(float)]; BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(0, sizeof(uint)), ioType); BinaryPrimitives.WriteUInt32BigEndian(body.AsSpan(sizeof(uint), sizeof(uint)), (uint)ioIndex); BinaryPrimitives.WriteSingleBigEndian(body.AsSpan(sizeof(uint) * 2, sizeof(float)), value ? 1.0f : 0.0f); return PackFrame(FanucCommandMessageIds.SetIo, body); } /// /// 解析只携带结果码的 TCP 10012 响应帧。 /// /// 完整响应帧。 /// 命令结果响应。 public static FanucCommandResultResponse ParseResultResponse(ReadOnlySpan 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)])); } /// /// 解析 GetSpeedRatio 的 TCP 10012 响应帧。 /// /// 完整响应帧。 /// 速度倍率响应。 public static FanucSpeedRatioResponse ParseSpeedRatioResponse(ReadOnlySpan frame) { var messageId = ValidateAndReadMessageId(frame); var body = GetBody(frame); if (body.Length < sizeof(uint) * 2) { throw new InvalidDataException("FANUC 速度倍率响应体长度不足。"); } // GetSpeedRatio 的字段顺序特殊:ratio_int 在前,result_code 在后。 var ratioInt = BinaryPrimitives.ReadUInt32BigEndian(body[..sizeof(uint)]); var resultCode = BinaryPrimitives.ReadUInt32BigEndian(body.Slice(sizeof(uint), sizeof(uint))); return new FanucSpeedRatioResponse(messageId, ratioInt, resultCode); } /// /// 解析 GetTCP 的 TCP 10012 响应帧。 /// /// 完整响应帧。 /// TCP 位姿响应。 public static FanucTcpResponse ParseTcpResponse(ReadOnlySpan frame) { var messageId = ValidateAndReadMessageId(frame); var body = GetBody(frame); if (body.Length < sizeof(uint) * 2 + sizeof(float) * 7) { throw new InvalidDataException("FANUC TCP 响应体长度不足。"); } var resultCode = BinaryPrimitives.ReadUInt32BigEndian(body[..sizeof(uint)]); var tcpId = BinaryPrimitives.ReadUInt32BigEndian(body.Slice(sizeof(uint), sizeof(uint))); var pose = new double[7]; for (int i = 0; i < pose.Length; i++) { pose[i] = BinaryPrimitives.ReadSingleBigEndian(body.Slice(sizeof(uint) * 2 + i * sizeof(float), sizeof(float))); } return new FanucTcpResponse(messageId, resultCode, tcpId, pose); } /// /// 解析 GetIO 的 TCP 10012 响应帧。 /// /// 完整响应帧。 /// IO 读取响应。 public static FanucIoResponse ParseIoResponse(ReadOnlySpan frame) { var messageId = ValidateAndReadMessageId(frame); var body = GetBody(frame); if (body.Length < sizeof(uint) + sizeof(float)) { throw new InvalidDataException("FANUC IO 响应体长度不足。"); } var resultCode = BinaryPrimitives.ReadUInt32BigEndian(body[..sizeof(uint)]); var ioValue = BinaryPrimitives.ReadSingleBigEndian(body.Slice(sizeof(uint), sizeof(float))); return new FanucIoResponse(messageId, resultCode, ioValue); } /// /// 解析 GetProgStatus 的 TCP 10012 响应帧。 /// /// 完整响应帧。 /// 程序状态响应。 public static FanucProgramStatusResponse ParseProgramStatusResponse(ReadOnlySpan 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); } /// /// 按 FANUC 命令通道 framing 规则封装完整帧。 /// /// 命令消息号。 /// 业务体。 /// 完整命令帧。 internal static byte[] PackFrame(uint messageId, ReadOnlySpan 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; } /// /// 校验完整帧并读取消息号。 /// /// 完整响应帧。 /// 响应消息号。 private static uint ValidateAndReadMessageId(ReadOnlySpan 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))); } /// /// 获取完整帧中的业务体切片。 /// /// 完整响应帧。 /// 业务体切片。 private static ReadOnlySpan GetBody(ReadOnlySpan frame) { return frame.Slice(11, frame.Length - 14); } }