Skip to content

Make OpenBitdo work on Linux hardware; rework the TUI around live device state - #19

Open
bybrooklyn wants to merge 48 commits into
mainfrom
linux-hidraw-and-tui
Open

bybrooklyn wants to merge 48 commits into
mainfrom
linux-hidraw-and-tui

Conversation

@bybrooklyn

@bybrooklyn bybrooklyn commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

What this does

Makes OpenBitdo work on Linux against real hardware, replaces the TUI, and moves it towards parity with the vendor's Windows application. docs/spec/parity_matrix.md lists, feature by feature, what the vendor application offers and where this stands.

Devices can be opened

  • Linux talks to devices through hidraw instead of a libusb-backed library. The old backend needed raw USB access the shipped udev rule never granted, and rejected multi-interface devices as ambiguous. The rule is now 70-openbitdo.rules.
  • Retried reads resend their request; stale replies are drained; writes are confirmed by reading back.

TUI

  • Sidebar of devices with tabs (Overview, Checks, Mapping, Buttons). Each device gets a plain verdict and a list of what you can and can't do with it, and why.
  • A 2.4G receiver whose controller is off is shown as "Controller off".

Retro 108 keyboard (0x5209)

  • Its real protocol (33-byte reports, 0x52/0x54), confirmed on hardware.
  • Mapping tab: any of the 111 keys and buttons to a key with an optional modifier, a media key or a mouse action; key locks; volume; rename and erase the profile.
  • Macros: m on a key builds one from taps, holds, releases and pauses, with repeat and interval.
  • Names are written in the vendor application's byte order, so each program reads the other's.

Controllers

  • The Ultimate 2's real protocol (checksummed header over a configuration record per platform), replacing frames no controller ever answered.
  • Mapping tab: three slots, 22-input button map with back paddles, stick and trigger ranges, vibration, invert/swap switches, motion-to-stick, stick-ring lights, and four recorded macros per slot.
  • The same editor covers the Pro 3, Ultimate 2 Bluetooth and Arcade Controller, each over its own record layout (the Arcade Controller adds the opposite-directions choice).
  • A controller wired or in XInput mode enumerates under a shared ID (0x310b); it is asked which product it is and routed to that product's editor.

Profile files

  • E saves the draft as a readable TOML file, I loads one back into the draft.

What is and is not confirmed on hardware

Status
Linux open, diagnostics, live Buttons tab confirmed (Retro 108, Ultimate 2)
Retro 108: read whole profile; A and B button assignment confirmed
Retro 108: other keys, media/mouse targets, locks, volume writes, erase same commands, tested against a simulator only
Retro 108: macro writes simulator only; behind advanced mode on real hardware
Pro 3, Ultimate 2 Bluetooth, Arcade Controller, shared-ID routing simulator only; no hardware tried
Ultimate 2: "is the controller connected" query confirmed on the receiver
Ultimate 2: everything else simulator only; behind advanced mode on real hardware. Needs a controller that is switched on.

Not done: calibration; recording macros from live input; Arcade Controller Pro; the Retro 87/68/Riviera keyboards; mice; the N64 receiver's pak manager; firmware update.

Checks

gofmt, go vet (also -tags manual), go test -race ./..., golangci-lint v2.13.1 (0 issues), and the repo's docs, clean-room, manual-tag, registry and evidence scripts all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh

bybrooklyn and others added 30 commits October 8, 2026 12:53
The libusb-backed HID library could not open either tested device on Linux:

- It needs read-write access to the raw USB node, and the shipped
  99-openbitdo.rules set the uaccess tag after 73-seat-late.rules had
  already run, so no ACL was ever applied.
- It reports usage page/usage as 0/0 for every interface, so a device with
  several interfaces (Retro 108) was rejected as ambiguous before any open.
- Controller navigation looked for its report descriptor under a hidraw
  path the library never returned.

Enumerate from /sys/class/hidraw and open /dev/hidrawN directly. Usage
metadata comes from each node's report descriptor, reads have a real
timeout, and the kernel HID driver stays attached, so a keyboard keeps
typing while it is open. Open failures keep the OS error, so permission
denied is reported as such.

Controller navigation now opens only gamepad/joystick interfaces, reports
one note per physical device, and is not started in --mock mode.

Ship the udev rule as 70-openbitdo.rules, matching hidraw nodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while running diagnostics against a real Ultimate 2:

- A retried read never resent its request: the write sat outside the
  attempt loop, so every retry waited again on a reply that was never
  coming. Reads are now resent; writes are still sent exactly once.
- Input queued before a request is discarded first, so a late reply to an
  earlier command cannot be validated as the reply to this one.
- A reply that arrives short is reported with the bytes that did arrive
  instead of as no reply.
- GetPid's PID field held 0x32a0 on a 0x6013 device and changed between
  runs; the PID arrives in the GetReportRevision reply. A PID is now only
  reported as the device's own when it matches the enumerated one.

JP108 mapping:

- A table reply too short to hold every entry is an error. It used to be
  padded with zeros, which became the backup that rollback wrote back.
- A write is confirmed by reading the table back; a mismatch rolls back.

errorCode and the timeout check use errors.As/errors.Is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A device row said "Supported" from the static registry tier alone. An
Ultimate 2 that could not be opened and a Retro 108 with no usable
interface both showed as fully supported while every check failed.

- AppDevice.ConfigChannel records whether enumeration found the vendor
  configuration interface, so an unreachable device is known up front.
- Health() combines that with the last probe: responding (n of m),
  silent, no channel, no permission, disconnected.
- A probe that could not run is cached like a result, instead of being
  dropped and retried on every device reload. ForgetFailedDiags clears
  those when the cause may have changed.
- Permission-denied and no-configuration-interface failures get their own
  error kinds so callers can explain them.
- The diagnostic summary no longer ends "This device is full-support"
  after 0/0 checks.

Devices carry a display name: what the device calls itself, else the
catalog name, generated into the registry from device_name_catalog.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DiagProbe decided between "disconnected" and "failed" by re-enumerating
host HID devices directly, so any test that drove a failed probe read the
machine's real hardware. Go through enumerateDevices instead.

Drop the "8BitDo" vendor prefix from device-reported names. Every listed
device is an 8BitDo one, and the catalog names devices without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugs:

- q, ? and x were handled before the device filter saw them, so typing a
  name containing q quit the app.
- The filter title nested one style in another and printed the inner
  style's escape code as text.
- At 80x24 the device list was replaced by a card for one device that
  said "choose a controller".
- The help overlay repeated the footer. Sub-views (the report, the slot
  preview) kept their parent's footer and advertised keys that did
  nothing there.
- A brick-risk dialog confirmed on enter the moment it appeared, and the
  write probe's dialog described a firmware write.
- After a failed rollback the mapping screen stayed up until the next
  key, and a click on Apply could issue another write.
- A reload kept the cursor's index, so the selection and an open actions
  pane could move to a different device.
- The slot preview's "p for the next slot" did nothing, and the first p
  previewed the slot already loaded.
- "more below" was printed both above and below the same list.
- No selection marker on the device list without colour.

Changes:

- Devices shows each device's name and what it is doing right now
  (responding n of m, not answering, unreachable, no permission), with
  the same two panels stacked on a narrow terminal.
- Text wraps at spaces instead of being cut at the panel edge; a panel
  too short for its content scrolls to the selection and says so.
- Footer hints and the help overlay come from one per-view list, so a
  key is only shown where it works. Hints are dropped from the end to
  fit; a long notice is shortened with an ellipsis, not replaced by the
  word "warning".
- Diagnostics names checks in plain words, explains an unanswered check,
  and explains why it could not run at all (no permission, no
  configuration interface, unplugged). The report can be viewed for any
  device, copied (OSC 52) and saved.
- Firmware is not listed as an action in a build where it cannot run.
  Settings and Quit are keys, not rows in every device's action list.
- Settings uses plain wording and shows recent messages, the reports
  directory, and which controller can drive the menus.
- A rescan says what it found; an unfinished first scan says "looking",
  not "no devices".

Adds TestDumpFrame (OPENBITDO_FRAME=80x24) to print a frame as text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diagnostics rows no longer show internal error text for a check that
passed. Firmware is still not an action, and the one line that says so
carries the release label ("Deferred in 0.0.3"). The guarded write
probe's reason comes from candidateUnlockDisabledReason again, and the
now-unused blockedLinesForDevice is removed. A keyboard is no longer
listed as a controller that "cannot drive the menus".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- TestDumpFrame renders mock devices; rendering attached hardware needs
  -tags manual, like every other live probe.
- The Ultimate 2 interface gate enumerated through the generic HID
  library, which reports usage 0/0 on Linux and so reported the vendor
  interface as missing when it is present. It uses the app's own
  enumeration now.
- The manual dashboard test follows the new action names and filters to
  the controller, since an unreachable device offers no diagnostics.

Removes two helpers the TUI rework left unused (golangci-lint: unused).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds docs/clean-room-evidence/hardware_run_linux_2026-10-08.md: the frames
an Ultimate 2 (0x6013) and a Retro 108 (0x5209) returned to the documented
safe reads over hidraw, and what they do and do not establish.

- The Ultimate 2 answers 4 of 12 safe reads; the macOS "wrote 64, read 0"
  result does not reproduce on Linux.
- GetPid's reply does not carry the PID on 0x6013; GetReportRevision's does.
- The Retro 108 over USB exposes no interface that carries the documented
  64-byte framing.

README, CHANGELOG, RC checklist and the gamepad-input note follow the new
transport, udev rule and dashboard. CONTRIBUTING covers the pinned
toolchain and the frame dump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The recipes refused to run unless the installed Go was exactly 1.27.0, so
a machine with a newer patch release could not use just at all. Export
GOTOOLCHAIN=go1.27.0 so the go command selects (and, once, downloads) the
pinned release itself. toolchain-check is unchanged and still verifies it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Default to go1.27.0 only when the caller has not chosen a toolchain.
toolchain-check still rejects any choice that is not the pinned release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two-panel dashboard was still hard to get around: focus moved between
a device list and an actions list, and a status like "responding, 5 of 12"
did not say whether the device was usable.

Layout: one frame. Devices are always listed down the left, each with a
verdict. The right side shows the selected device in three tabs: Overview,
Checks, Mapping. It is the same at every size, so there is no compact
layout to differ from the wide one.

Getting around: there is one cursor, always in the right-hand pane. tab
(or 1-3, or left/right where a tab has no use for them) changes section
and d changes device, from anywhere. Nothing has to be focused first and
esc is never needed to get out of a region. Leaving the mapping editor
with unapplied changes asks first, then carries on to where the user was
heading.

Verdicts: Working (settings can be changed), Limited (answers, but can
only be read), Can't connect, No access, Not answering. Answering some
checks is no longer presented as support.

Overview splits a device's actions into "You can" and "Not yet". Only
what can be done is selectable; what cannot is listed with its reason. A
device that cannot be reached gets one explanation instead of the same
reason on every row. The Mapping tab explains why there is no editor
instead of that being a notice on a disabled row.

Checks opens on the result and what it means. enter shows a check's raw
details (d now changes device).

A controller reaches everything: the d-pad changes section and its third
button changes device. Controller input now takes the same path as a key
press.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A device with no configuration interface was reported as "Can't connect:
plugged in, but not in a mode OpenBitdo can talk to". For an Ultimate 2 in
its gamepad mode (0x6012) and for a Retro 108 that is wrong where it
matters: the device is working, only its settings are out of reach.

Enumeration now records what a device presents itself as (a Generic
Desktop gamepad or keyboard interface). Such a device is shown as
"Playing" or "Typing", with the settings limitation stated as a separate
point and advice that fits the kind of device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In gamepad mode an Ultimate 2 has no configuration interface, so the app
could only say its settings were out of reach. It can still be useful
there: the controller reports 16 buttons, four of which (3, 6, 17, 18) are
outside the usual layout and are its back and extra shoulder buttons.

The Buttons tab lights up each button's number as it is pressed, with the
d-pad direction, the last button and everything seen so far. It reads only
the input reports the controller already sends. While the tab is open a
press is shown and nothing else: it does not act as enter or change device.

The decoder is now tested against the report descriptor and reports
captured from a real Ultimate 2 (0x6012).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing sent to a Retro 108 (0x5209) was ever answered: the registry gave
it the 64-byte, report-0x81 frames the controllers use, and the keyboard
has no such report. Its configuration interface (usage page 0x8c) takes
33-byte commands on output report 0x52 and answers on input report 0x54.

The framing and command bytes were worked out from static analysis of the
vendor's application, kept outside this repository, and are recorded as
wire facts in docs/clean-room-evidence/dossiers/5209/jp108_hid.toml. Each
command marked as hardware-confirmed there was then sent to a real
keyboard and its reply recorded.

- command_matrix.csv: the JP108 mapping, feature-flag and (new) profile
  name rows carry the real frames. The voice rows still have the old
  framing; they are no longer applied to 0x5209 and say so.
- A JP108 is sent only its own commands. The generic 64-byte reads the
  other families share are refused for it.
- The configuration interface is chosen per device: 0x8c for a JP108,
  0xffa0 otherwise. A Retro 108 is therefore reachable, not "can't connect".
- Mappings are read one button at a time by key id (A is 233, B is 232;
  the ids are not in order). A mapping type that cannot be represented is
  an error rather than read as unassigned, so a backup can never erase it.
- A write names a profile first if the keyboard has none, as the vendor
  flow does, and is checked by reading the button back.
- The editor can set a button to "(none)".

Adds JP108Simulator, a Transport that behaves like the keyboard, since a
fixed queue of replies cannot answer a request that names its key.

On hardware: all reads answered; a profile name and assignments for the A
and B buttons were acknowledged and read back. Not yet observed: the
buttons sending the assigned keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
Observed on a Retro 108: a stored mapping is only used while the
keyboard's Profile button is on, and the keyboard sends the value it was
given unchanged. A button assigned 0x76 sent usage 0x76.

That makes the vendor table's codes for F13-F24 (0x76-0x81) wrong for
this purpose: they are other keys. The editor now offers F13-F24 as
0x68-0x73, and after applying a keyboard mapping says that the Profile
button must be on for the buttons to use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The button is marked with an icon, not the word, so "the Profile button"
sent a user looking for something that is not labelled. Per the vendor
manual it is the third small button at the keyboard's top left, with an
indicator above it. The dossier records the control panel layout and the
keyboard's own software-free fast key mapping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The keyboard's Mapping tab is now a full editor: all 104 assignable keys,
each assignable to a key (optionally with a modifier held), a media key
or a mouse action through a searchable picker, plus the Win / Alt+Tab /
Alt+F4 locks and the volume level. Nothing is written until Apply; each
written setting is read back and what was written is put back on failure.

core gains KeyboardProfile, KeyboardReadProfile, KeyboardApply and
KeyboardClearProfile, with backups that restore the whole profile.
protocol's JP108 code moves to jp108.go and grows the mapped-key list,
macro list and volume commands. Mock mode talks to a simulated Retro 108
through the real protocol code, so the editor can be tried without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The editor gains a Profile name row (typed in place, up to 14 characters)
and X to erase the profile, which asks first with Cancel focused. A rename
writes every existing mapping again after the name, so the result does not
depend on whether the keyboard keeps mappings across a rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
…s off

The 81 05 4x frames the Ultimate 2 rows carried were never answered by
hardware. The controller's actual protocol is a 16-byte header (cmd, arg,
len, CRC-16/MODBUS, total, offset) over one 1592-byte configuration record
per platform: read in 45-byte chunks, written by byte range, committed
separately. protocol gains that layer (u2.go), a simulator with staged
writes, and seven command rows; the dossier records the wire facts.

A 2.4G receiver answers for itself while its controller is off. It is now
asked whether the controller is connected, and a receiver that says no is
shown as "Controller off" instead of a working device. Confirmed on a real
0x6013 receiver; everything past that query still needs the controller on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
PadReadProfile decodes the controller's configuration record into three
slots, each with a name, a 22-input button map (face buttons, triggers,
d-pad, Star, Home, both back paddles and the two extra buttons), stick and
trigger ranges, vibration strength and option switches. PadApply writes
only the sections that changed, commits, reads the record back and puts
the old bytes back if the controller did not keep them; everything this
program does not model is preserved byte for byte.

Real writes stay behind advanced mode until a controller has confirmed the
exchange. Mock mode gets a simulated Ultimate 2 behind the real protocol
code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The Mapping tab for a controller is now a real editor over the profile
core reads: pick one of the three slots, name it, assign each of the 22
inputs from a searchable list (back paddles and extra buttons listed
first), set stick and trigger ranges, vibration strength and the invert
and swap switches. It replaces the mock-only preview, whose slot browsing
and button list were built on frames no controller answered.

In --mock it runs against the simulated controller end to end. On real
hardware it stays behind advanced mode until a controller has confirmed
the exchange; the tab says so, and says when the controller is simply off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
Six rows (81 05 4x) were written before any hardware evidence. A real
controller never replied to them, the diagnostics probe kept sending them,
and a working controller looked half-answered as a result. They are gone,
along with the core profile path, mock tables, hard-block error and the
button diagram that only existed to serve them. The real protocol
(u2.go, pad.go) replaces all of it. Tests that covered behaviour which
still exists were ported to the new commands; the rest were deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
KeyMacro is a macro as the keyboard stores it: press, release and wait
steps, a repeat count and an interval, with validation that refuses what
would misbehave (a key left held, a release with no press, over 200
steps). Nothing sends one yet.

docs/spec/parity_matrix.md lists what the vendor application offers per
product and where OpenBitdo stands on each, hardware-confirmed or not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
…e order

protocol can list, read, write and clear a keyboard's macros (a name and a
value per key, chunked across reports), and the simulator holds them.
core's keyboard profile carries macros through read, apply, undo and
backup restore, reading each one back after writing it. A macro that
would leave a key held, or is otherwise invalid, is refused before
anything is sent. Real macro writes stay behind advanced mode until one
has been confirmed on a keyboard.

Profile and macro names are now written UTF-16 high byte first, as the
vendor's application does, so each program reads the other's names. Names
OpenBitdo wrote the other way round before are still read correctly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
Press m on a key to build the macro it plays: add key taps, holds and
releases from a searchable key list, add pauses and adjust their length,
set how many times it plays and the pause between plays, name it. A key
row shows its macro by name with the keys it types. Saving puts the macro
in the draft; assigning the key a plain mapping removes it, since a key
does one or the other. Unsaveable macros (no name, a key left held) say
why. On a real keyboard the editor stays shut outside advanced mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
E in the keyboard or controller editor saves the draft as a TOML file a
person can read (each entry names the key and what it does) under the
config directory's profiles/; I lists the saved files and loads one into
the draft, undoably. Nothing reaches a device until Apply. A file is
checked as strictly as an edit: unknown keys, settings or functions, a
macro that would misbehave, or a controller slot saved for the other
mode-switch position are refused with the reason. An existing file is
replaced only on a second E.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The controller profile gains the motion section (steer either stick with
the motion sensor while a chosen button is held or toggled, with
sensitivity and dead zone) and the three lighting sections (tracing, fire
ring with speed, a colour per ring light). The effect in use is read and
set with its own command and read back. The editor has rows for all of
it; colours are typed as six hex digits or stepped through swatches.
Profile files carry both. Simulator-tested only, like the rest of the
controller path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
A slot's four macros live in two places: a header section of the record
(name, trigger button, step count, repeat, interval) and a 16 KiB macro
storage area holding ten-byte steps. protocol can read, erase and write
that storage; the simulator models it as flash (a write only clears bits,
so a region written without an erase comes back wrong). core reads macros
with the profile, and on apply erases and writes the steps, commits, reads
them back, and only then writes the header, so a header never describes
steps that are not there. A macro that ends still holding something is
refused before anything is sent. Simulator-tested only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
Each slot's four macros are rows in the controller editor. Enter opens
one: name it, pick the button that plays it, set repeat and interval, and
build it from steps. t adds a button tap, a an empty step; on a step, b
holds or releases a button, l and r turn the sticks, and left/right change
how long it lasts. A macro that ends still holding something, or has no
name, is not saved and says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
Both use the Ultimate 2's protocol with a differently laid-out record, so
the layout is now per model. Before this an Ultimate 2 Bluetooth was
treated as an Ultimate 2: its macro, motion and light sections would have
been read from and written to the wrong offsets. A Pro 3 gains the profile
capability (keeping its standard firmware path) and has no motion or
light settings; the editor says so on those rows and core refuses to write
them. Each model resolves its platform record its own way, and the
Bluetooth model's reversed P1/P2 macro triggers are translated.

No hardware for either model has been tried; real writes stay behind
advanced mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
bybrooklyn and others added 18 commits October 8, 2026 18:08
Wired, or with its receiver in XInput mode, a Pro 3, Ultimate 2 or
Ultimate 2 Bluetooth enumerates as 0x310b, which several products share,
and that is the only way the vendor's application configures them. Such a
device is now asked which product it is (once per serial, re-asked on a
rescan) and listed as that product with the profile capability only:
firmware and boot paths are not known to work through the shared id. Its
profile is read and written with the product's layout while the device is
opened under the id it enumerates as. A product with no editor here is
left detect-only, as before.

Untested on hardware: whether a controller in that mode exposes a
configuration interface on Linux is not known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
Left and right Ctrl, Shift and Alt and the left Win key are in the
keyboard's remappable set; the editor listed 104 keys and now lists all
111. A modifier's default is itself as a modifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
An Arcade Controller keeps its settings in a record laid out as a Pro 3's
and is reached under the shared XInput id, so it rides on the same code:
button map, macros, profile name, and the choice of which direction wins
when two opposite ones are pressed together (neither, up, first pressed,
last pressed). It has no sticks, triggers or vibration to tune; the editor
marks those rows as not on this controller, and core refuses the
opposite-directions setting on anything that is not an arcade controller.
Its platform record is chosen by asking the device.

Untested on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
R in the macro editor starts recording: each character typed becomes a
key tap, with Left Shift held around the characters that need it on a US
layout, and Enter, Tab and Backspace recorded as themselves. Esc stops. A
character with no US key is left out and said so.

The parity matrix no longer lists calibration as missing for the
Ultimate 2: the vendor application offers it only for the N64 products.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The configuration interface differs by product: usage page 0xffa0 for most,
0x008c for the Retro keyboards and their receivers, and 0xff7a for the
shared controller id, the Ultimate 2C's id, the Arcade Controller Pro and
two others. A controller under the shared id was being looked for on
0xffa0, where it is not, so the shared-id routing added earlier could not
have opened one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The 0x5200 keyboard takes the Retro 108's commands on the same report and
interface, with its own numbering: modifiers are keys 100-106, the A and B
buttons 109 and 108, K1-K8 116 down to 110, there is no numpad, and
F13-F24 as targets are 118-129. core now has a key table per keyboard
model and translates between them, and the read commands are allowed for
it. It stays a read-only-tier device: writes still need the existing
unlock, and the editor is not offered. No hardware has been tried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The previous commit picked up a local agent worktree directory as an
embedded repository. It is not part of the project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
A Retro 87 (Xbox edition and UK), a Retro 68 and the Riviera keyboard keep
their whole profile in one 1532-byte record. This adds the wire commands
for it (record read and write by byte range, macro storage, the per-key
colour block, the key-report switch), a simulator that answers them, and a
core model: every key's assignment, the lock options, volume, sleep
timers, lighting themes, per-key colours and recorded macros, read and
applied with a backup, a readback of everything written and a rollback
when the keyboard did not keep it. Bytes the model does not name are
written back as they were read.

The framing is not the Ultimate 2's: an 8-byte header with a one-byte
sum, 53 data bytes a report, and no commit.

The keyboards stay candidate-readonly: a session refuses every write
without the write-unlock ceremony, and core reads a real one only in
advanced mode. The Riviera keyboard is still detect-only, so nothing is
exchanged with it. There is no screen for any of this yet.

Untested on hardware: every row is inferred from static evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
The Retro 87 and Retro 68 keyboards were given the Retro 108 mapping
capability, so a diagnostic probe sent them 33-byte 0x52 reports the
vendor library never sends them. They now have only the record-keyboard
capability merged in the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
A Retro R8 mouse (by cable and through its 2.4G receiver) has a command
for each setting, in 64-byte reports with no report id on usage page
0xff00; the Riviera mouse keeps one 140-byte record, read and written in
the frames a record keyboard uses. This adds the wire commands for both, a
simulator for each, and a core model: the profile name, button
assignments, left-handed mode, DPI stages and the one in use, polling
rate, lift-off distance, wheel speed and direction, the Retro R8's four
macros and its receiver's "is a mouse linked" question, read and applied
with a backup, a readback of everything written and a rollback when the
mouse did not keep it. What the model does not name goes back as it was
read.

A mouse is given the mouse commands and nothing else, the identity
queries included: the vendor library never sends it one.

Nothing is exchanged with a real mouse yet. The Retro R8 and its receiver
are detect-only in pid_matrix.csv and the Riviera mouse has no row there;
the tests raise the tier for their own duration. Once it is raised a
session still refuses every write without the write-unlock ceremony, and
core reads a real mouse only in advanced mode. The SN30 Pro mouse is
driven by a different library and is not covered. There is no screen for
any of this.

Untested on hardware: every row is inferred from static evidence. The
vendor application reads the polling rate table two bytes earlier than it
writes it; both are reproduced as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfoS1okgCva2z2Q2ZYK3vh
Extends the controller-profile code to the Arcade Controller Pro (0x2062,
also enumerating as 0x20aa): a two-slot record layout with a 24-input
button map, the opposite-directions choice, button lights, combos and
macros, with the same backup, readback and rollback as the other models.
Adds its session mark (cmd 0x16) and report switch (cmd 0x106).

Everything is inferred from static analysis; no hardware has been tried.
The product has no row in pid_matrix.csv, so nothing is sent to a real
one; tests raise its tier for their own duration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extends the controller-profile code to the first-generation Ultimate
Bluetooth Controller (0x6007, adapter 0x3106): a three-slot record of
0x914 bytes whose button map covers 20 inputs with the face buttons
stored B, A, Y, X, with the same backup, readback and rollback as the
other models.

Its requests travel in a counted wrapper (81 n 04 ...), with a crc for
0x6007 and none through the adapter; the record is written whole; the
platform (Switch or XInput) is chosen by the caller (PadReadProfileOn)
rather than asked; macro steps on the XInput record hold the triggers
as analog values. The hot-key macro section and the xinput rumble range
are preserved, not modelled.

Both ids are granted the profile capability and listed on the eight
record commands they take; the connection, mode-switch, light and arcade
commands are refused for them, so their diagnostic, mode, boot and
firmware paths are unchanged. Tiers in pid_matrix.csv are untouched.

Everything is inferred from static analysis; no hardware has been tried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record or macro read, and a platform select, are followed in the vendor
library by as many filler bytes as the request names, with their checksum
(0xffff for none). The direct-framed controllers were being sent a zero
checksum and no filler, which a controller that checks the checksum would
reject. They now get the same bytes the first-generation Ultimate
Bluetooth path, merged just before this, already sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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