BB.Ufactory.Report (bb_ufactory v0.1.0)

Copy Markdown View Source

Pure parser for xArm auto-push report frames.

The arm continuously pushes state frames on the report socket (port 30003 for real-time, port 30001 for normal, port 30002 for rich). This module handles the real-time and normal report frame layouts as documented in xArm Developer Manual V1.10.0 and the Python SDK report.py.

No network I/O is performed here. All functions are pure binary transforms.

Frame Layout (real-time report, port 30003)

Each push frame has a 4-byte length prefix:

Bytes 03:    frame_length  u32 big-endian (total bytes of the frame INCLUDING
                            these 4 bytes  the payload is frame_length - 4
                            bytes)
Byte  4:      state_mode    lower nibble = state, upper nibble = mode
Bytes 56:    cmd_count     u16 big-endian
Bytes 734:   angles        7× fp32 little-endian (radians, J1J7)
Bytes 3558:  pose          6× fp32 little-endian (mm for x/y/z, rad for roll/pitch/yaw)
Bytes 5986:  torques       7× fp32 little-endian (Nm, J1J7)
 frame_length >= 135 (F/T sensor enabled) 
Bytes 87110:  ft_filtered  6× fp32 little-endian ([Fx, Fy, Fz, Tx, Ty, Tz])
Bytes 111134: ft_raw       6× fp32 little-endian ([Fx, Fy, Fz, Tx, Ty, Tz])

The SDK's Python report.py uses len(rx_data) (the payload size) as its length field. Here we use the wire frame_length value from bytes 0–3, which includes the 4-byte header prefix. The minimum complete base frame is 87 bytes total (4-byte prefix + 83-byte payload), so frame_length >= 87.

The parser tolerates frames larger than expected (future firmware versions may add fields). Extra trailing bytes are ignored.

Normal Report (port 30001, report_type: :normal)

In addition to the base real-time fields, bytes 87+ contain controller status:

  • Byte 87: mtbrake brake-state bitfield (J1=bit0 .. J7=bit6)
  • Byte 88: mtable enable-state bitfield
  • Byte 89: error_code current error code (0 = no error)
  • Byte 90: warn_code current warning code
  • Bytes 91–114: tcp_offset 6× fp32 LE (mm + rad)
  • Bytes 115–130: tcp_load 4× fp32 LE (kg, mm×3)
  • Byte 131: collis_sens collision sensitivity (0–5)
  • Byte 132: teach_sens teach sensitivity (0–5)

Normal-report frames and F/T-extended real-time frames overlap in size and cannot be distinguished by frame_length alone (current firmware sends normal reports well over 135 bytes). The caller must state which stream the buffer came from via the report_type argument — :realtime for port 30003 (the default), :normal for port 30001.

Return Value

parse_report/2 returns:

  • {:ok, report, rest} — successfully parsed one frame; rest is any trailing data in the buffer (may contain the start of the next frame).
  • {:more} — not enough bytes in the buffer yet; accumulate more data.
  • {:error, {:bad_frame_length, n}} — the length prefix is impossible (below the 87-byte minimum or above the 512-byte sanity cap). The stream is desynchronized or corrupt; the caller should reset the connection rather than continue parsing.

The report map always contains: :state, :mode, :cmd_count, :angles, :pose, :torques.

Optional fields present only when the frame is large enough:

  • :ft_filtered, :ft_raw — 6-element float lists when report_type: :realtime and frame_length >= 135
  • :mtbrake, :mtable (8-element bit lists), :error_code, :warn_code, :tcp_offset, :tcp_load, :collis_sens, :teach_sens — when report_type: :normal and frame_length >= 133

Summary

Functions

Parses one report frame from the start of buffer.

Functions

parse_report(buffer, report_type \\ :realtime)

@spec parse_report(binary(), :realtime | :normal) ::
  {:ok, map(), binary()}
  | {:more}
  | {:error, {:bad_frame_length, non_neg_integer()}}

Parses one report frame from the start of buffer.

report_type selects the frame layout: :realtime (port 30003, the default) or :normal (port 30001).

Returns {:ok, report_map, rest} on success, {:more} if the buffer does not yet contain a complete frame, or {:error, {:bad_frame_length, n}} if the length prefix is impossible (desynchronized/corrupt stream).

This function is designed to be called in a loop to drain the buffer:

defp drain_buffer(buffer, state) do
  case BB.Ufactory.Report.parse_report(buffer) do
    {:ok, report, rest} ->
      state = handle_report(report, state)
      drain_buffer(rest, state)
    {:more} ->
      %{state | buffer: buffer}
    {:error, reason} ->
      reset_report_connection(reason, state)
  end
end