Line Sensors
Six PixArt J3 sensors ring the base, each pointing down and forward, each reporting 320 range bins at ~30 Hz. Together they see the floor immediately around the robot: obstacles, and cliffs.
The firmware streams newline-delimited JSON over a USB CDC port (/dev/hello-pixart-j3). A background process decodes it; the robot server publishes it; clients consume it over ZMQ.
The status codes come first
A bin does not always carry a distance. The chip has two status codes:
0xFF80
5.11
No detection at all. No reflection came back. Ambiguous: a dark floor, a lighting condition, or a void.
0xFE80
5.09
Detected, but beyond the range limit. Something returned from further than the sensor can measure.
Because the sensors point down, 5.09 is the stronger cliff evidence: the beam travelled past where the floor should have been and found something far away.
These are classified once, at decode, in line_sensor/protocol.py, before any conversion or correction:
ranges[bin]is set toNaNwherever the bin is not a distance;codes[bin]carries the identity (CODE_VALID,CODE_BEYOND_LIMIT,CODE_NO_RETURN,CODE_OTHER_INVALID).
Status schema
health — subsystem-wide
streaming
decoding is running. False means no cliff detection.
port_open
the serial port is open. False during a dropout.
disabled_sensors
names switched off at runtime, by choice
sensors_dead
names not reporting, by fault. Empty during normal operation.
rate_hz
whole-frame rate, ~30 Hz
reader_restarts
serial recoveries so far. A climbing number means a flaky cable.
decode_errors, frame_advance_err, frame_not_full_err, not_six_sensors_err
counters; all should stay flat
disabled and dead are deliberately different: one is a choice, the other is a fault. A disabled sensor never appears in sensors_dead.
sensor_0 … sensor_5 — per sensor
ranges
float64[320], metres, NaN at every non-measurement bin
codes
uint8[320], protocol.CODE_* — the authority on 5.09 vs 5.11
n_no_return, n_beyond_limit
counts of each code this report
missed_frames
consecutive frames not reported; 0 while healthy
enabled
False if switched off at runtime
frame_id, ts_last_read, rate_hz
calibration — served by the body
The tare is loaded, fingerprint-checked and published by the robot, so no consumer opens a YAML file or repeats the validation
loaded
names with an accepted tare
rejected
{name: why} for every refusal
id
changes when the calibration changes; cache on this
sensor_N
packed tare
Parameters
Under line_sensor_loop in the robot params:
loop_rate_Hz— 250. The reader polls far faster than the sensors report.sensor_names—sensor_0…sensor_5, clockwise from robot forward.bus_sensor_map—[[1, 0], [3, 2], [5, 4]]. Mapsdistances<bus><dev>in the JSON to a logical sensor index. Change it if the cables are plugged in differently.flip_range_ordering— whether to reverse each 320-bin array.line_sensor_geometry— FOV, mounting angles, emitter height and pitch diameter,pixart_report_num.
bus_sensor_map and flip_range_ordering raise if missing rather than defaulting. A wrong bin order mirrors every reading left-to-right and nothing downstream can detect it; a wrong bus map rotates the whole ring. Six identical 60-degree wedges tile the circle, so any mapping error produces a complete, plausible-looking result. Only a physical observation can catch it — occlude one wedge and confirm the expected sensor index responds.
Two lossy hops, and what that forces
Both transports drop:
q_status.put()discards the oldest message when full. The reader puts at 250 Hz; the control loop drains at 48–100 Hz; the queue is 3 deep.The ZMQ status socket sets
CONFLATE=1on both ends, so a subscriber only ever holds the newest message.
That single fact drives two rules:
Every status message carries all six sensors. a dropped delta loses that sensor's frame permanently, and the symptom — one sensor frozen while five stream — is indistinguishable from a hardware fault.
The calibration rides every message. Publishing it once would be conflated away for any client that connected a moment later, leaving it silently uncalibrated.
q_cmd is the exception: it is 100 deep, because dropping the oldest command means a set_streaming(False) quietly does not happen.
Calibration
Flat-floor tare: park on a clean, light, flat floor with nothing within ~0.5 m, record, and subtract the difference between what each bin measures and the ideal floor depth.
What it will and will not do:
Status-code samples never enter the arithmetic. A bin that mostly returns 5.09/5.11 is rejected, not averaged.
A run is refused if too many bins fail — a partial tare is worse than none, because nothing downstream can tell a bad correction from a good one. A dark or glossy floor produces exactly this.
Each raw session is one
session.npz, so a tare can be recomputed later without the robot.A stored tare records a configuration fingerprint (bus map, flip, report count, geometry, code mapping). On mismatch it is refused, never downgraded to a warning and never quietly replaced with an older file.
Runtime control
Neither setting persists. A restart always comes up streaming with all six enabled.
While paused the port is still read and the bytes discarded, so the kernel buffer cannot fill and hand back a corrupt part-report on resume. A disabled sensor is skipped before json.loads, which is where the reader's time actually goes, so it costs less rather than merely publishing less.
Recovery
A serial error closes the port, marks every enabled sensor dead, and retries the open on a 0.5 → 5 s backoff, incrementing reader_restarts on success. Verified on hardware: deauthorizing the USB node drops the subsystem and it returns by itself within ~3 s.
Closing the port matters. It is opened exclusive=True, so a descriptor left open blocks every later reopen with "device busy" — which is how a single transient fault used to kill the subsystem until the next reboot, silently, while the process stayed alive.
Tools
REx_line_sensor_calibrate— flat-floor tare.stretch_line_sensor_ranges— live per-bin plot of what each bin reports. Status codes get their own coloured rows so 5.09 and 5.11 are distinguishable at a glance.--caliboverlays the tare the body serves.stretch_line_sensor_viz_3d— interactive 3D view of the projected points, with clustering and cost-map overlays.
Both viewers run on the robot and need a display.
Consumer checklist
Read
codes, never a float comparison, to ask what a bin was.Check
health['streaming']andhealth['disabled_sensors']before trusting a clear floor. A disabled sensor reports nothing, which is not the same as reporting that nothing is there.Check
sensors_dead.Use
is_sensor_updated(name)to skip repeats: status publishes at 100 Hz while sensors report at ~30 Hz, so about 70% of polls carry no new frame.Apply the tare with
LineSensorLoopClient.apply_tare()rather than subtracting offsets yourself — it leaves status-code bins untouched.
Last updated
Was this helpful?
