Skip to content

Feature/controller battery label - #68

Draft
trexx wants to merge 2 commits into
masterfrom
feature/controller-battery-label
Draft

trexx wants to merge 2 commits into
masterfrom
feature/controller-battery-label

Conversation

@trexx

@trexx trexx commented Sep 16, 2026

Copy link
Copy Markdown
Owner

No description provided.

trexx and others added 2 commits September 16, 2026 21:54
…follow them

Battery state was already collected for every supported pad - GIP pads through
Controller::statusReceived, Shield and Bluetooth pads through the 120 s poll -
but only ever forwarded to the host, so nothing on the box could show it.

The label: while any level of the in-game menu is up, a small end-aligned line
along the bottom of the screen lists every controller's battery by host player
number - "Controller 1: 75%  ·  Controller 2: 50%, charging  ·  Controller 3:
wired". A new Android-free ControllerBatteries store, fed by both battery
paths, keeps one reading per number; Game formats it. The menu is a chain of
AlertDialogs whose onDismiss fires for a parent after its child has opened, so
a DialogChain token decides which dismissal is the last one and hides the label.
The dialog's dim stays; the text is the primary colour so it reads through it.

The LED: "Battery level" is a fifth value of the Xbox button brightness
setting. The guide LED is monochrome, so it steps through the presets - full
Bright, medium Normal, low Dim - and slow-blinks at critical, since a warning
has to look like one. GuideButtonLed holds every LED constant and the ladder
on the Java side, where it is tested; the native side gains Controller::setLed,
which stores the command and sends it once the device has started, and
startDevice() applies whatever is stored through the same applyLed() so the
frame is built in one place. ledBrightness loses its const and gains a
pattern, as std::atomic against the startThread fallback.

Two pre-existing faults fixed on the way, both found while wiring this up:

- A GIP pad's first status packet usually lands before its first input
  report, which is what assigns its player number, so its first battery report
  went to the host as player 0. It is now held on the context until the number
  is assigned.
- On the cabled path the read thread runs before the Java object exists, and a
  status in that window armed the driver's change filter with nobody
  listening, so Java heard nothing until the battery actually moved. The
  filter now stays unarmed until a report is delivered; MS-GIPUSB 2.2.9 has the
  pad repeat status every second for its first ten seconds.

Latency: nothing on a per-frame path. Battery writes happen at battery-change
frequency on driver threads; the label is built once per menu level on the UI
thread while the decoder is already stalled behind the dialog, and is a child
View of the window rather than a layer; the LED sends one GIP frame per level
change on the driver's read thread, deduplicated against charge-only changes.

Tests: ControllerBatteriesTest, DialogChainTest, GuideButtonLedTest, and the
guide LED cases in PreferenceConfigurationTest. HARDWARE_TESTING.md 31 lists
what needs a device - in particular whether a pad honours LED_BLINK_SLOW at
all, which Table 42 leaves open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pad-audio submenu counted GIP pads by their position in the driver's
list, while the battery label - and the game on the host - use the player
number ControllerHandler assigns. The two disagreed whenever the Shield
controller or a Bluetooth pad held a slot: the first GIP pad read "Controller
1" in the menu and was player 2 everywhere else.

The number now lives on the AbstractController too, written when the handler
assigns it and cleared when the pad goes, so the menu - which holds the
controller and nothing else - can read it without reaching into the handler's
maps from the UI thread. GameMenuLayout.padNumber() is the rule, tested: the
player number, or "Unused controller" for a pad that has not sent input yet,
or the position when multi-controller is off and every pad is player 0.

Numbering is still assigned on a pad's first input, deliberately. Assigning
at deviceAdded() was considered and dropped: the USB service binds in
onCreate(), and LiSendControllerArrivalEvent() returns -2 until the stream's
input channel exists with nothing re-sending it, so a pad attached at launch
would have been announced into the void and the host would never learn its
type or capabilities.

Latency: none; the menu builds its rows once per open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@trexx
trexx marked this pull request as draft September 16, 2026 21:03
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