Skip to content

docs(#38): reconnect wiring design — slot manager + resume-first-with-grace#139

Merged
aradanmn merged 3 commits into
mainfrom
feat/38-reconnect
Jul 25, 2026
Merged

docs(#38): reconnect wiring design — slot manager + resume-first-with-grace#139
aradanmn merged 3 commits into
mainfrom
feat/38-reconnect

Conversation

@aradanmn

Copy link
Copy Markdown
Owner

Draft for design review — no code, just the design doc (docs/DESIGN-38-RECONNECT-WIRING.md). Grounded in this session's on-Deck #112 evdev captures + a full code trace of the reconnect path.

The problem

controller_proxy.sh — the whole of #38's seamless-reconnect mechanism — is DARK (zero callers). The live path raw-binds a pad's jsN into bwrap at launch; a reconnected pad is a new node the sandbox can't see (#61/#62). Sticky slots keep the instance alive but input-dead — exactly HW-2's "P2 dropped, session persisted, un-controllable."

Core structure (the manager)

Event handlers hold no policy. A slot_manager owns the slot record schema and every transition, so CONTROLLER_ADD/REMOVE share one get/set/release surface and the intent policy lives in exactly one verb, slot_claim, which returns SPAWN/RESUME/ADOPT/REJECT. Handlers just execute the outcome. The manager is pure state+policy → the whole reconnect decision matrix is a hardware-free unit test.

Intent policy — resume-first-with-grace

Known MAC → always resume its slot. Unknown MAC → adopt an orphaned slot when the session is full (4-up ceiling, no 5th instance) or within a grace window, else spawn a new instance, else reject.

Hardware-derived decisions (#112)

Rollout

Incremental, behind MCSS_PROXY_ENABLE, raw-binding stays the working default until the on-Deck verification passes. PR-a (manager + state, behavior-neutral) → PR-b (launch wiring) → PR-c (policy) → PR-d (watchdog + verify → flip default).

Please weigh in on §14 open questions

  1. Grace default — 180s (covers a battery swap, excludes a genuine new player)?
  2. ADOPT device-id flip — a different pad adopting a slot changes the virtual's vendor:product → Controlify may re-detect controller type mid-session (glyph flip, cf. Controlify shows SNES button glyphs for the Steam virtual pad (wrong prompts) #36). Acceptable, or suppress ADOPT when vendor:product differs?
  3. proxy-OFF reconnect — keep today's no-reconnect (chosen), or teardown+relaunch the slot (restores input, loses game state)?
  4. Steam-grab — evsieve grab may starve Steam's own 28de virtual (guide button/overlay?) — verify before default-ON.
  5. REJECT-when-full → wire the Docked launch with no external controller exits silently — show a user-visible message #125 user message here?

🤖 Generated with Claude Code

Scott and others added 2 commits July 25, 2026 01:35
…-grace

Design for lighting up the DARK controller_proxy (#38 seamless reconnect),
grounded in this session's on-Deck #112 evdev captures and a full code trace.

Core structure (Scott's call): a slot_manager owns the slot record schema and
every transition, so CONTROLLER_ADD/REMOVE share one get/set/release surface and
the intent policy lives in exactly one function (slot_claim). Handlers become
thin — they translate an event to a manager verb and execute the returned
outcome (SPAWN/RESUME/ADOPT/REJECT). The manager is pure state+policy, so the
whole reconnect decision matrix is a hardware-free unit test.

Intent policy (Scott): resume-first-with-grace. Known MAC always resumes its
slot; an unknown MAC adopts an orphaned slot when the session is full or within a
grace window, else starts a new instance, else is rejected.

Key hardware-derived decisions (see #112):
- Identity anchor = the physical pad's MAC (uniq); eventN and the UHID instance
  are per-connection and must never be cached.
- Role = "has a js handler" — but select physical pads by excluding the 28de
  Steam virtuals (which also carry js, with an empty uniq); the js-owner event
  node is the #112 crash fix (evsieve reopens a pad, not the touchpad).
- RESUME (same pad) re-points the proxy; ADOPT (different pad, device-id changes)
  restarts it. The one open verification — whether evsieve persist=reopen honors
  a moved symlink — only decides seamless-vs-blip; the restart fallback is correct
  either way. Deferred to a Deck session (#112 step-2).

Incremental rollout keeps raw-binding the working default behind MCSS_PROXY_ENABLE
until that on-Deck verification passes. Includes a unit-test decision matrix and
open questions for review (grace default, ADOPT device-id flip vs #36, proxy-off
reconnect behavior, Steam-grab interaction, #125 full-session message).

Refs #38, #112.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZuoMpX28CbtGJbigVTv5H
…y-off no-op; full=log-only)

Scott's review calls: Q1 180s, Q2 A (device-id follows the physical pad),
Q3 A (proxy-off reconnect is a no-op), Q4 verify-only on-Deck, Q5 log-only
(4-player limit documented in README, not a message surface).

Refs #38.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZuoMpX28CbtGJbigVTv5H
The earlier '3 DS4s share a MAC' was a misread of our own capture: all four
uniqs were distinct; what repeats across same-model pads is the UHID
vendor:product (054C:09CC), never the MAC. MAC is the device-agnostic identity
key, present and unique on DS4 AND Xbox pads — corroborated by Steam telling
lightbar-less Xbox pads apart (Steam keys on the MAC, not the DS4 lightbar; we
use the same key, so no Steam dependency for identity). Downgrades the
'identical-MAC floor' to a theoretical counterfeit-only safety net, not a real
constraint for this all-Bluetooth hardware.

Refs #38.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZuoMpX28CbtGJbigVTv5H
@aradanmn
aradanmn marked this pull request as ready for review July 25, 2026 02:16
@aradanmn
aradanmn merged commit 5048163 into main Jul 25, 2026
4 checks passed
@aradanmn
aradanmn deleted the feat/38-reconnect branch July 25, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant