Parallax Propeller2 USB driver - the MashUp

HID gamepad/joystick support + user mapping API (ticket 2150) — design
Login

HID gamepad/joystick support + user mapping API (ticket 2150) — design

Status 2026-08-17: implemented and validated live on a Sony DualShock 4 ($054C:$09CC) in standard HID mode. Bind-time parse + poll-once interrupt IN as designed; joy_poll uses last_rx_len() (the host int_in return is a boolean, not a byte count) and does not take the kbd/mouse hid_bus.

The problem

Keyboards and mice have a boot protocol — a fixed report layout any host can assume. Gamepads and joysticks do not: a pad's buttons, sticks, hats, triggers and pedals live wherever its HID report descriptor says they live, at arbitrary bit offsets, with arbitrary logical ranges and optional report-ID prefixes. Two pads from one vendor can differ. "Support gamepads" therefore means "parse report descriptors" — there is no shortcut that isn't a per-device hardcode.

Revisiting the 2010 decision

Ticket 2010 excluded a report-descriptor interpreter from the one-cog host path — correctly, for the runtime path in the dogfood era. The distinction that unlocks this feature: parse once at bind time, on the caller's cog, in Spin. The PHY cog never sees the parser; the runtime poll stays exactly what HID polling already is (one poll-once interrupt IN), plus a handful of shift-and-mask field extractions from precomputed offsets. Cog budget: zero. Hub budget: the parser object + its tables (measured in the README footprint table; USB_JOY-gated so kbd/mouse users pay nothing).

Architecture

 usb_app (USB_JOY gate)                 joy_* facade + mapping primitive
   ├── usb_hub_host                     +$21 HID-descriptor length capture,
   │                                    +get_report_desc() into cfg[]
   └── usb_hid_report.spin2 (NEW)       bind-time parser + field extractor
        no cog, no wire access - pure descriptor/report byte-walking

The user mapping API (the point of the feature)

Introspection — the application can discover the pad:

Call Meaning
joy_kind() Application usage: $04 joystick / $05 gamepad / $08 multi-axis.
joy_axes(), joy_axis_usage(i) How many axes, and which each one is (page<<16 | usage — $30=X, $31=Y, $39=hat, …).
joy_axis_min/max(i) The device's declared logical range.
joy_button_count() Total buttons (≤ 32).

Reading:

Call Meaning
joy_poll() One poll-once IN; nonzero = fresh report.
joy_axis(i) / joy_axis_scaled(i) Raw (sign-extended per logical min) / normalized to −32768..+32767 across the declared range.
joy_buttons() / joy_button(i) 32-bit mask / single button.
joy_hat() First hat as 0..7 (N=0, clockwise), −1 centered.

Mapping — the primitive that lets an application run a "press the control you want for FIRE" wizard:

Call Meaning
joy_mark() Snapshot the current state as the baseline.
joy_changed() First control now differing from the baseline: (kind<<8) \| index, kind 1 = button, 2 = axis (past a per-axis threshold of ¼ span — big enough to ignore stick noise/drift, small enough that any deliberate motion trips it), 3 = hat. 0 = nothing yet.

Wizard shape (worked in examples/top_joy_example.spin2): for each game action — joy_mark(), prompt, poll until joy_changed() returns nonzero, store the code, wait for release. The stored codes are then read back through one joy_control(code) accessor. The example also demonstrates plain introspection ("this pad: gamepad, 6 axes: X Y Z Rz + hat, 12 buttons") and a live mapped-state display.

v1 caps (all stated in code)

Validation plan (bench, when free)

Any commodity USB pad behind the hub: introspection matches the physical device (count the buttons), every control moves its field, wizard maps a 4-action set, kbd/mouse regression (top_hid_example) unchanged. The usb_emu_hid_joy device emulator (ticket 3030) can dogfood the poll path but NOT the parser — its min-peer reports are fixed-layout; a real pad's descriptor is the acceptance gate.