Skip to content

Build plan: virtual pad rig (#136) — uhid primitive → structural rig → benchmark pilot #157

Description

@aradanmn

Build plan for the virtual pad rig. Recon: docs/RESEARCH-136-VIRTUAL-PAD-RIG.md (PR #156) — verdict: build on /dev/uhid, not uinput, because uhid takes a caller-supplied uniq and gives each pad its own device parent.

This issue is the build breakdown, in the shape #142 used for #38. Nothing here starts before PR-0 comes back green from the Deck.

Placement decisions

  • The rig is test tooling — nothing lands in modules/. /proc/bus/input parsing is owned by controller_monitor.sh (ARCHITECTURE.md §2), so the rig never re-implements enumeration: it sources controller_monitor.sh and calls _list_raw_external_pads to assert what the production code sees. One encoding (PRINCIPLES feat: Add Microsoft account OAuth setup during installation #9).
  • Files:
    • tests/lib/uhid_pad.py — the device primitive (create / destroy / inject). Dependency-free stdlib; uhid is write() on a chardev, no ioctls, no compiler, no container.
    • tests/lib/uhid_rig.sh — bash surface the suites call; owns PID tracking and cleanup.
    • tests/probe-uhid-feasibility.sh — the Deck gate (PR-0).
    • tests/test_uhid_pad.sh — CI suite for the pure parts.
  • No production code change is expected. The research doc predicts each uhid pad gets a distinct sysfs parent, so the step-6 dedup needs no edit. PR-0 proves or disproves that before anything depends on it.

Proposed surface

uhid_pad.py create --uniq aa:bb:cc:00:00:01 --name "MCSS Test Pad 1" \
                   --vendor 0x054c --product 0x0001
    # holds the device open until killed; that IS the pad's lifetime
uhid_pad.py --emit-descriptor            # pure: report descriptor hex
uhid_pad.py --encode-report BTN_SOUTH=1,LX=200   # pure: report bytes hex

# stdin command stream to a live pad:
press BTN_SOUTH | hold BTN_SOUTH 500 | axis LX 200 | release all | destroy
uhid_rig.sh:  rig_create_pad N | rig_destroy_pad N | rig_inject N "<cmd>" | rig_cleanup

rig_cleanup kills only PIDs the rig started, from its own tracked pidfile — PRINCIPLES #7, no name matching.

PRs

PR-0 — feasibility probe (Deck-gated, blocks everything)

Deliverable: tests/probe-uhid-feasibility.sh + a minimal uhid_pad.py, answering in one bounded run:

  1. /dev/uhid present; openable as deck without sudo?
  2. One pad created → does it appear in /proc/bus/input/devices with our U: Uniq=?
  3. What is its S: Sysfs= — does the step-6 parent-key strip yield a unique key?
  4. Does joydev give it a jsN?
  5. Does _has_gamepad_buttons accept its B: KEY= bitmap?
  6. Does _list_raw_external_pads enumerate it as an external pad?
  7. Two pads → do BOTH enumerate? (the dedup question, settled definitively)
  8. Destroy + recreate with the same uniq → new eventN, same identity?
  9. Does Steam mint a 28de virtual for it, as it does for a real pad?

Acceptance: every question answered with a verdict line; no lingering uhid devices or processes on exit. Bounded waits with busy-vs-waiting indicators (PRINCIPLES #6). Read-only checks run first; device creation is a separate opt-in step.

PR-1 — pad primitive + CI tests

Harden uhid_pad.py; tests/test_uhid_pad.sh covers the pure parts — descriptor bytes, report encoding, uniq/VID:PID formatting — via --emit-descriptor / --encode-report, asserting hex. Registered in the ci.yml baseline map like every other suite; no new CI machinery. Device I/O stays Deck-only. Mutation-test the new suite (PRINCIPLES #4).

PR-2 — rig control surface

tests/lib/uhid_rig.sh: create/destroy/inject/cleanup, pidfile-tracked, cleanup trap. Backgrounded work redirected off any capture pipe (PRINCIPLES #8 — this bit CI twice already, in #80/#103 and #133).

PR-3 — automate stage3_hotplug

Flag-gated MCSS_VIRTUAL_PADS=1; human-prompt mode stays the default until a full green run says otherwise (PRINCIPLES #2, dark-first). Replaces the "Plug in a SECOND/THIRD/FOURTH external controller" prompts with timed pad creation, keeping every existing geometry assertion.

PR-4 — #71 burst-spawn repro

Create 4 pads in one burst pre-launch — the case that is near-impossible to stage by hand. Either reproduces the reflow race or gives evidence it is architecturally mitigated.

PR-5 — reconnect rig (#38 / #151)

Destroy and recreate by uniq to reproduce a battery-death cycle. #151 specifically: destroy two pads, recreate in swapped order so each lands on the other's freed eventN. Turns the swap race into a repeatable test instead of a post-ship anecdote.

PR-6+ — benchmark pilot (separate track)

Do not start until PR-1..5 are trusted, and until the §5 methodology question in the research doc is decided: open-loop timed input is self-damping under load, so a slower configuration flies less far and incurs less load, flattening the A/B difference being measured. Recommendation on the table is distance-as-covariate plus log-gated transitions. Also needs --server/quickPlay confirmed from our launch path so the pilot never navigates a menu.

Gate

PR-0 green on the Deck. If Q7 says two pads still collapse, the plan changes: step-6 dedup needs a (behaviour-neutral) fix first and it becomes PR-0b.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions