Repository navigation
Make OpenBitdo work on Linux hardware; rework the TUI around live device state - #19
Open
bybrooklyn wants to merge 48 commits into
Open
bybrooklyn wants to merge 48 commits into
bybrooklyn wants to merge 48 commits into
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdlists, feature by feature, what the vendor application offers and where this stands.Devices can be opened
70-openbitdo.rules.TUI
Retro 108 keyboard (0x5209)
mon a key builds one from taps, holds, releases and pauses, with repeat and interval.Controllers
Profile files
Esaves the draft as a readable TOML file,Iloads one back into the draft.What is and is not confirmed on hardware
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