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 0–3: 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 5–6: cmd_count u16 big-endian
Bytes 7–34: angles 7× fp32 little-endian (radians, J1–J7)
Bytes 35–58: pose 6× fp32 little-endian (mm for x/y/z, rad for roll/pitch/yaw)
Bytes 59–86: torques 7× fp32 little-endian (Nm, J1–J7)
—— frame_length >= 135 (F/T sensor enabled) ——
Bytes 87–110: ft_filtered 6× fp32 little-endian ([Fx, Fy, Fz, Tx, Ty, Tz])
Bytes 111–134: 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:
mtbrakebrake-state bitfield (J1=bit0 .. J7=bit6) - Byte 88:
mtableenable-state bitfield - Byte 89:
error_codecurrent error code (0 = no error) - Byte 90:
warn_codecurrent warning code - Bytes 91–114:
tcp_offset6× fp32 LE (mm + rad) - Bytes 115–130:
tcp_load4× fp32 LE (kg, mm×3) - Byte 131:
collis_senscollision sensitivity (0–5) - Byte 132:
teach_sensteach 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;restis 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 whenreport_type: :realtimeandframe_length >= 135:mtbrake,:mtable(8-element bit lists),:error_code,:warn_code,:tcp_offset,:tcp_load,:collis_sens,:teach_sens— whenreport_type: :normalandframe_length >= 133
Summary
Functions
Parses one report frame from the start of buffer.
Functions
@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