[POC] add basic SInput support (PlayerLED) - #337
Draft
LeeNX wants to merge 23 commits into
Draft
Conversation
Explains why the HID Service and NUS service are reachable from different host-side software (SDL/hidapi/game input stack vs. a companion GATT app), and how that determines where to add capabilities like rumble or RGB/player LEDs.
Implements SInput (https://github.com/HandHeldLegend/SInput-HID) as a new mode via configuration.setEnableSInput(true), mutually exclusive with the classic configurable Output/Feature Report since it owns Report IDs 0x01-0x03 outright. The report layout (BleSInput.h/.cpp) was reverse engineered from SDL's reference driver (SDL_hidapi_sinput.c, added in libsdl-org/SDL#13343), since that's the authoritative implementation SDL actually runs against -- there is no machine-readable spec. Implements: real buttons/axes on Input Report 0x01, a correct Features response (0x02) advertising this device's actual capabilities, and Player LED handling on Output Report 0x03 (bleGamepad.isPlayerLedReceived() / getPlayerLedIndex()). Haptics and RGB commands are accepted but not acted on -- no rumble motor or RGB LED to drive yet. setEnableSInput(true) also defaults the advertised VID/PID to 0x2E8A/0x10C6, since SDL's SInput driver only recognizes that exact hardcoded pair. Verified by building the modified library and the new example (plus a regression build of TestFeatureReports.ino) against real NimBLE-Arduino via a local PlatformIO project -- both link cleanly. GattVsHid.md is updated to describe the real report layout (it previously, incorrectly, assumed SInput rode on the HID Feature Report).
…and SDL3 testing docs sendReport()'s SInput branch now derives plug_status/charge_level from the same setBatteryLevel()/setBatteryPowerInformation()/setChargingState()/ setDischargingState() state already used for the standard Battery Service -- SInput doesn't read that GATT characteristic, so it needs its own copy. SInputPlayerLED.ino now slides its reported battery level between 25% and 90% so there's something visibly changing to test against, alongside the existing Player LED and button demo. Added SDL3Testing.md and sdl3_gamepad_test.c: a standalone SDL3 program (verified building against a real local SDL3 3.4.14 install, zero warnings under -Wall -Wextra) that opens the device, cycles Player LED, and prints buttons/battery as they change, plus Linux-specific setup notes -- Debian/ Ubuntu's libsdl3-dev is still 3.2.x (no SInput driver) as of this writing, so building 3.4.x from source is covered.
Verified end-to-end on real hardware: an ESP32 running SInputPlayerLED.ino against SDL3 3.4.14 (built from source) on a separate Linux host over SSH, with sdl3_gamepad_test.c's output cross-checked against the ESP32's own Serial log for buttons, the battery ramp, and Player LED round-trips -- all matched. Getting there surfaced a real bug: the SInput branch's HID Report Descriptor used a Vendor Defined top-level usage page. SDL's hidapi layer discards any device from enumeration entirely unless its top-level usage is Generic Desktop Joystick/Gamepad/MultiAxisController (SDL_HIDAPI_ShouldIgnoreDevice(), gated by the default-on SDL_HINT_HIDAPI_ENUMERATE_ONLY_CONTROLLERS hint) -- before VID/PID is ever checked. So the device was invisible to SDL regardless of everything else being correct. Fixed by using Generic Desktop/Gamepad for the outer collection, matching this library's classic descriptor; the field-level report content is unchanged since SDL's SInput driver reads it by fixed byte offset regardless of descriptor semantics. Also fixes sdl3_gamepad_test.c never terminating on SIGTERM/Ctrl-C: SDL installs its own signal handler that turns the signal into a queued SDL_EVENT_QUIT rather than killing the process, and the test loop wasn't checking for it -- `timeout`-wrapped runs were leaking orphaned processes. Now polls events and exits cleanly on that event. SDL3Testing.md updated with what this session actually found: SInput mode never creates an evdev/joystick node (its Vendor Defined field-level usage means hid-generic skips that registration, correctly), and reflashing with a changed Report Map after a previous bond can fail pairing with AuthenticationFailed until the ESP32's own flash -- not just the host's bond -- is cleared.
…ks it up as a joystick too The top-level Generic Desktop/Gamepad usage fixed in the previous commit only satisfies SDL's own enumeration filter. The kernel's hid-generic/hid-input driver is a separate consumer: it maps evdev BTN_*/ABS_* codes from per-field HID usages, and Input Report 0x01 had none -- one opaque 63-byte blob, since SDL's SInput driver reads it by fixed byte offset regardless of usage annotations. So SDL worked, but nothing else did: no /dev/input/js*, no jstest/evtest, no non-SInput-aware app. Added real usages over the exact same bytes: Button page (32 buttons) over bytes 2-5, Generic Desktop X/Y/Z/Rz/Rx/Ry over bytes 6-17 (matching this library's own classic descriptor's axis usage/ordering convention), with the rest (plug status/charge level, and the unused IMU/touchpad/reserved tail) padded out as Const fields. Reports 0x02/0x03 (SInput's own command/feature-response and output-command reports) are untouched -- still opaque, since only SDL's SInput driver or an app speaking the same protocol ever reads/writes those. Verified on real hardware: /dev/input/js1 now appears, jstest shows live 32 buttons / 6 axes with BUTTON_1's toggle visible in real time, and a follow-up SDL3 regression run (buttons/battery/Player LED) still matches the ESP32's own Serial log exactly -- both paths work off the same bytes.
… too GattVsHid.md and SDL3Testing.md both said the SInput Input Report was only reachable through SInput-aware software (SDL's driver or a custom hidapi client). That was true when written, but the previous commit added real field-level HID usages to Input Report 0x01's buttons/axes, so evdev/jstest now work against it directly. Player LED/features/haptics/RGB (Reports 0x02/0x03) are unaffected -- still SInput-only, since those have no field-level usages for evdev to map.
… in the test tool SInputPlayerLED.ino now lights the LED only when playerLedIndex == 1 (SInput's Player LED index is 1-based; 0 means unassigned), matching how a real controller's single-LED player indicator behaves, instead of lighting for any nonzero index. sdl3_gamepad_test.c now checks SDL_SetGamepadPlayerIndex()'s return value and prints SDL_GetError() on failure -- useful in its own right, and what surfaced (indirectly) that SDL was reporting "success" unconditionally regardless of whether the underlying hidapi write actually happened. Verified: wire-level Features response bytes are byte-for-byte correct (confirmed via btmon and by rebuilding SDL3 3.4.14 with its own DEBUG_SINPUT_INIT/DEBUG_SINPUT_PROTOCOL logging) -- this library's firmware side is not at fault. But SDL's own capability parsing was intermittently unreliable during testing (wrong buttons-count/gyro-range decoded from an identical, verified-correct wire payload), alongside a kernel dmesg "Event data for report N was too short" warning pointing at SDL's Linux hidapi read path rather than anything in this repo. Documented as a known open question in SDL3Testing.md rather than something fixed here.
setBatteryLevel() rides on the standard BLE Battery Service (0x180F), which BlueZ bridges to its Battery1 D-Bus interface once paired -- upower picks it up the same way it would any Bluetooth peripheral, independent of HID Reports, SInput mode, or any code at all. Verified against real hardware: upower -e lists it as gaming_input_dev_<address>, and upower -i shows a live-updating percentage. Added as its own section (not renumbered into the existing steps, to avoid breaking the step-number cross-references from GattVsHid.md and SDL3Testing.md), plus a troubleshooting entry for when upower doesn't list the device at all.
Promoted from a troubleshooting bullet to its own section with full evidence, since this is now confirmed reproducible across multiple independent sessions/runs rather than an occasional flake: - SDL_SetGamepadPlayerIndex() always reports "succeeded" regardless of whether anything reached the device -- it's driven by internal player- slot bookkeeping, not the underlying (void, no error propagation) driver->SetDevicePlayerIndex() call. - Firmware/LED logic proven correct independent of SDL: a raw write direct to hidraw (bypassing SDL/hidapi entirely) reliably triggers the correct Serial log line and physically observed LED toggling, repeatedly. - Ruled out on this library's side: reverting recent descriptor commits for an A/B test, and a full bluetoothd restart + re-pair -- both still failed identically. - Traced into SDL: HIDAPI_DriverSInput_SetDevicePlayerIndex() gates on ctx->player_leds_supported, which SDL's own debug-instrumented build shows false despite a wire-level Features response confirmed byte-for- byte correct (replicated SDL's own parsing logic in Python against the captured bytes and got the right answer, while SDL's live output showed different/wrong numbers from what should be the same payload) -- plus a kernel dmesg "Event data for report N was too short" warning pointing at SDL's Linux hidapi read path. - Listed next investigation steps and noted this looks worth filing upstream against libsdl-org/SDL if reproducible outside this setup, since none of the evidence points at anything specific to this library. Also corrected two now-stale claims elsewhere in the doc that implied Player LED reliably works end-to-end via SDL.
examples/SInputPlayerLED now ships a standalone Python HID test script, which leaves these behind for anyone who runs it locally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…se layout SDL's SInput driver picked up a 2-byte protocol_version field (PR #13667, already baked into the 3.4.14 tag) after this library's Features response layout was originally reverse-engineered from the driver's initial PR (#13343). That shifted every field after it by 2 bytes, so SDL was reading caps0/PLAYERLED from this library's always-zero `type` byte and always computing player_leds_supported=false -- not an SDL bug, a stale offset table on this side. BleSInput.h/.cpp now match SDL's current layout. Verified end-to-end on real hardware (not just re-derivation): reflashed the board, and sdl3_gamepad_test now reports "Player LED capable: true" and produces real "Player LED index: N" lines on the ESP32's Serial Monitor via actual SDL_SetGamepadPlayerIndex() calls, not just the hidraw bypass. Also adds sdl3_gamepad_test.c debug output (-v verbose logging, SDL's own player-LED capability property, the underlying hidraw path) and sinput_hid_test.py, a standalone script that talks the raw SInput protocol directly for isolating firmware bugs from driver-layer ones -- exactly how this one was root-caused. SDL3Testing.md's "Known issue" section is rewritten to "Resolved" with the corrected root cause. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut the "Resolved" section's debugging-journey writeup and other in-the-moment narration (confirmations, "verified on real hardware", struck-through wrong conclusions) so the doc reads as a working example guide rather than a bug-hunt transcript. Also fixes a leftover "Otherwise" bullet under "Raw hidapi enumeration finds nothing at all" that had lost its paired first bullet, reading as a non sequitur. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit's message claimed this bullet was fixed, but it was only ever reworded here -- correcting that now: "Otherwise, this is likely a permissions issue..." had no first case left to be "otherwise" than, since an earlier edit removed its paired bullet. Standing alone as fact rather than a fallback reads correctly either way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…_test/ arduino-cli compiles every .c/.cpp file sitting directly in a sketch's root folder alongside its .ino, and only skips subfolders other than a special recursive src/. sdl3_gamepad_test.c is a standalone gcc+SDL3 host program, not ESP32 code, so CI was trying to build it against the Arduino toolchain and failing on the missing SDL3/SDL.h header. Moving it (and the two other host-side test files it's paired with) into a plain subfolder keeps them out of the sketch build entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # GattVsHid.md # LinuxHIDTesting.md
The module docstring and per-constant comments had grown into a full debugging-history narrative (the resolved Player LED byte-layout bug, cross-references to every BleSInput.h constant name). Cut it down to what a reader actually needs to run and maintain the script. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ous CI error arduino/compile-sketches' deltas report recompiles every sketch at the base ref to diff sizes against. For a sketch that doesn't exist there yet (like examples/SInputPlayerLED on this branch, since master doesn't have it), that recompile fails with "Can't open sketch: no such file or directory" and the action unconditionally logs ::error::, producing a red annotation on an otherwise-green run. Split "Find example sketches" into two outputs -- sketches present at the base ref (existing, safe to diff) and sketches that aren't (new, this branch's own compile-sketches call runs with enable-deltas-report: false so there's nothing to fail). Mirrors compile-sketches' own base-ref resolution: the PR's base branch for pull_request events, the immediate parent commit otherwise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # README.md
Owner
|
As long as everything else keeps working, then fine by me |
Collaborator
Author
|
Cool, then I will keep adding to it or at least get it to basic feature and then we can merge, but not just yet. Thanks @lemmingDev |
This was referenced Aug 27, 2026
|
Memory usage change @ c94fca4
Click for full report table
Click for full report CSV |
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.
SInput has some features that I think people, including myself are asking. It's mostly documented protocol that supports PlayerLED, RGB LED, rumble and a few other features and is support by SDL3 via HIDApi.
Current implementation loses much of the flexibility of ESP32-BLE-Gamepad, which might not be the right place to build on.
Looking for feedback and suggestions.