fix(native-usb): prevent transmit-bank starvation and retain battery telemetry

This commit is contained in:
Joey Yakimowich-Payne 2026-09-13 21:00:17 -06:00
commit a699a6920b
16 changed files with 342 additions and 82 deletions

View file

@ -1002,10 +1002,53 @@ the old standalone Wii nominal-charge fallback is removed.
Power-field regressions fail before the correction and pass afterward. Eight
focused regressions and both firmware builds pass; a PC capture of real
motion-bearing input verified Power Info `0x01` on both halves. The
`0.72-native-usb-power` trial is installed with persistent storage verified
unchanged. A sustained Switch/idle test is still required before attributing
the timeout to this flag or claiming recovery. Additional trace-only logs
include raw USB interrupts, frame count and device-watchdog state.
`0.72-native-usb-power` trial was installed with persistent storage verified
unchanged. Later `0.76` passed a ten-minute PC mixed input/control/bulk soak but
still lost input on Switch. Captured failures end after a completed root hub
endpoint-halt clear, with addressed-token and root NAK counters stopped while
SOF continues. This also occurs without USB receive-error flags; the disconnect
cause is not established. Trace-only logs include raw USB interrupts, frame
count and device-watchdog state.
**Wii battery telemetry (0.77):** the Wii parser previously ignored the battery
byte in status report `0x20` and cleared the battery field on every input report.
It now retains the latest measurement between reports, rejects truncated status
updates and clears the old measurement on parser setup. The voltage-derived
capacity uses the same 5/20/70/100-percent bands as
[SDL's Wii driver](https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_wii.c).
The USB bridge forwards that measured level with external power set and charging
clear, rather than forcing full battery. Updates occur when a status report
arrives; this change adds no periodic status requests. Parser regressions cover
retention, low-battery updates and reconnection.
The installed `0.77` PC smoke test read source battery `179/255` (the 70-percent
band) and Power Info `0x19` from both native children, with live IMU and mixed
control/bulk checks passing. Saved flash data was verified unchanged. The
connected-controller JSON percentage also uses the full `1..255` battery range,
so full charge is 100 percent rather than 102. The subsequent Switch trial still
disconnected around 240 seconds while delivering Power Info `0x19`; correcting
false-empty battery reporting alone did not resolve the failure.
**Private transmit-bank publication (0.78):** the earlier
PC checker submitted R/L reads sequentially. USBmon confirmed zero overlapping
child reads. With one reader per child and 500-ms application timeouts, `0.77`
stopped delivering fresh input within about three seconds. Short 10-ms timeouts
had repeatedly cancelled those pending reads and obscured this starvation.
Bank selection had disabled every IN buffer until Core 0 restored the bank,
including HID and bulk payloads already stored in private DPRAM. `0.78` publishes
those prepared private buffers during selection; only the shared EP0 IN image
still waits for Core 0 copying. A regression polls alternating child HID/bulk
endpoints without a foreground task and checks payload isolation, completion,
and newly queued data while EP0 restoration is pending. It fails before the fix
and passes afterward; 14 focused tests and both native firmware builds pass.
The image is installed with persistent storage verified unchanged. With the Wii
reconnected, the same concurrent 500-ms-read test passed 600 seconds: 145,746 R
packets and 149,567 L packets, including 113,316 and 114,276 fresh IMU packets.
USBmon measured overlapping child reads during 94.4% of the steady-state window
with no failed read completions there and no capture drops. A subsequent
15-second mixed control/bulk/input check passed 57 rounds with no errors.
This confirms the PC starvation reproduction is corrected. The user's subsequent
Switch gameplay trial also stopped reproducing the L+R disconnect.
For sensorless hardware, the checker supports `--input-only`: press real buttons
and keep changing controls on both halves during the run. Neutral fallback

View file

@ -2037,7 +2037,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
// Constants
static const char* wii_devtype_names[] = {
@@ -217,124 +297,450 @@
@@ -217,124 +297,456 @@
// process_ functions
@ -2409,6 +2409,12 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
+ if (len < 7)
+ return;
+ wii_instance_t* ins = get_wii_instance(d);
+ // Wii Remote status contains a voltage-derived level; use SDL's capacity bands.
+ if (ins->dev_type != WII_DEVTYPE_PRO_CONTROLLER) {
+ const unsigned raw = report[6];
+ const unsigned percent = raw > 178 ? 100 : raw > 51 ? 70 : raw > 13 ? 20 : 5;
+ d->controller.battery = (uint8_t)((percent * 255u + 50u) / 100u);
+ }
+ bool connected = (report[3] & 2) != 0;
if (ins->state == WII_FSM_DID_REQ_STATUS) {
- if (d->product_id == 0x0306) {
@ -2598,7 +2604,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
}
static void process_req_data_read_calibration_data(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
@@ -444,17 +850,90 @@
@@ -444,17 +856,90 @@
// Defined here: http://wiibrew.org/wiki/Wiimote#0x21:_Read_Memory_Data
static void process_req_data(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
@ -2696,7 +2702,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
case WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION:
process_req_data_read_calibration_data(d, report, len);
break;
@@ -465,7 +944,6 @@
@@ -465,7 +950,6 @@
process_req_data_dump_eeprom(d, report, len);
break;
default:
@ -2704,7 +2710,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
break;
}
}
@@ -473,29 +951,44 @@
@@ -473,29 +957,44 @@
// Defined here:
// http://wiibrew.org/wiki/Wiimote#0x22:_Acknowledge_output_report.2C_return_function_result
static void process_req_return(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
@ -2772,7 +2778,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
}
}
@@ -514,15 +1007,26 @@
@@ -514,15 +1013,26 @@
const uint8_t* data = &report[1];
wii_instance_t* ins = get_wii_instance(d);
@ -2808,7 +2814,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
}
// Process misc buttons
ctl->gamepad.misc_buttons |= (data[1] & 0x80) ? MISC_BUTTON_SYSTEM : 0; // Button "home"
@@ -563,46 +1067,168 @@
@@ -563,46 +1073,168 @@
// Used for WiiMote in Accelerometer Mode. Defined here:
// http://wiibrew.org/wiki/Wiimote#0x31:_Core_Buttons_and_Accelerometer
static void process_drm_ka(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
@ -3017,7 +3023,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
}
// Used in WiiMote + Nunchuk Mode
@@ -617,31 +1243,17 @@
@@ -617,31 +1249,17 @@
}
wii_instance_t* ins = get_wii_instance(d);
@ -3057,7 +3063,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
// Better if uDraw reports its own "type", but for the moment
// it gets reported and the 2nd half of a gamepad.
@@ -668,59 +1280,59 @@
@@ -668,59 +1286,59 @@
ctl->gamepad.buttons |= n.button_lower ? BUTTON_Y : 0;
}
@ -3165,7 +3171,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
static udraw_tablet_t process_udraw_tablet(const uint8_t* e, uint16_t len) {
// uDraw Tablet format here:
@@ -1002,52 +1614,18 @@
@@ -1002,52 +1620,18 @@
logi("fsm: req_status\n");
wii_instance_t* ins = get_wii_instance(d);
ins->state = WII_FSM_DID_REQ_STATUS;
@ -3223,7 +3229,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
}
static void wii_fsm_ext_read_register(uni_hid_device_t* d) {
@@ -1055,8 +1633,7 @@
@@ -1055,8 +1639,7 @@
wii_instance_t* ins = get_wii_instance(d);
ins->state = WII_FSM_EXT_DID_READ_REGISTER;
@ -3233,7 +3239,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
uint16_t bytes_to_read = 6;
wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read);
}
@@ -1066,8 +1643,7 @@
@@ -1066,8 +1649,7 @@
wii_instance_t* ins = get_wii_instance(d);
ins->state = WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION;
@ -3243,7 +3249,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
uint16_t bytes_to_read = 16;
wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read);
}
@@ -1077,8 +1653,7 @@
@@ -1077,8 +1659,7 @@
wii_instance_t* ins = get_wii_instance(d);
ins->state = WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION2;
@ -3253,7 +3259,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
uint16_t bytes_to_read = 8;
wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read);
}
@@ -1086,6 +1661,10 @@
@@ -1086,6 +1667,10 @@
static void wii_fsm_assign_device(uni_hid_device_t* d) {
logi("fsm: assign_device\n");
wii_instance_t* ins = get_wii_instance(d);
@ -3264,7 +3270,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
uint8_t dev = ins->dev_type;
switch (dev) {
case WII_DEVTYPE_UNK:
@@ -1100,18 +1679,8 @@
@@ -1100,18 +1685,8 @@
}
uint8_t report_type = 0xff;
if (ins->ext_type == WII_EXT_NUNCHUK) {
@ -3285,7 +3291,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
} else if (ins->ext_type == WII_EXT_CLASSIC_CONTROLLER) {
logi("Wii: requesting E (Classic Controller)\n");
d->controller_subtype = CONTROLLER_SUBTYPE_WII_CLASSIC;
@@ -1125,21 +1694,17 @@
@@ -1125,21 +1700,17 @@
d->controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_UDRAW_TABLET;
report_type = WII_REPORT_TYPE_KE;
} else {
@ -3317,7 +3323,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
uni_hid_parser_wii_request_report_type(d, report_type);
break;
}
@@ -1164,7 +1729,10 @@
@@ -1164,7 +1735,10 @@
ins->state = WII_FSM_LED_UPDATED;
wii_process_fsm(d);
@ -3329,7 +3335,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
}
static void wii_fsm_dump_eeprom(struct uni_hid_device_s* d) {
@@ -1218,7 +1786,15 @@
@@ -1218,7 +1792,15 @@
// Do nothing
break;
case WII_FSM_DEV_GUESSED:
@ -3346,7 +3352,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
break;
case WII_FSM_BALANCE_BOARD_READ_CALIBRATION:
wii_fsm_balance_board_read_calibration(d);
@@ -1234,6 +1810,19 @@
@@ -1234,6 +1816,19 @@
wii_fsm_update_led(d);
break;
case WII_FSM_LED_UPDATED:
@ -3366,10 +3372,11 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
break;
default:
loge("Wii: wii_process_fsm() unexpected state: %d\n", ins->state);
@@ -1249,10 +1838,10 @@
@@ -1249,10 +1844,11 @@
ins->mode = WII_MODE_HORIZONTAL;
ins->state = WII_FSM_SETUP;
+ d->controller.battery = UNI_CONTROLLER_BATTERY_NOT_AVAILABLE;
-
- // Start with 0xa40000 (all Wii devices, except for the Wii Remote Plus)
- // If it fails it will use 0xa60000
@ -3381,7 +3388,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
// Dump EEPROM
#if ENABLE_EEPROM_DUMP
@@ -1266,14 +1855,49 @@
@@ -1266,14 +1862,51 @@
wii_process_fsm(d);
}
@ -3418,8 +3425,10 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
void uni_hid_parser_wii_init_report(uni_hid_device_t* d) {
- // Reset old state. Each report contains a full-state.
+ wii_instance_t* ins = get_wii_instance(d);
+ const uint8_t battery = d->controller.battery;
memset(&d->controller, 0, sizeof(d->controller));
d->controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
+ d->controller.battery = battery;
+ // Status/ack and Nunchuk interleaves do not contain new motion samples.
+ for (int i = 0; i < 3; i++) {
+ d->controller.gamepad.accel[i] = ins->accel[i];
@ -3433,7 +3442,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
return;
switch (report[0]) {
case WIIPROTO_REQ_STATUS:
@@ -1291,6 +1915,14 @@
@@ -1291,6 +1924,14 @@
case WII_REPORT_TYPE_KAE:
process_drm_kae(d, report, len);
break;
@ -3448,7 +3457,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
case WII_REPORT_TYPE_KEE:
process_drm_kee(d, report, len);
break;
@@ -1307,6 +1939,9 @@
@@ -1307,6 +1948,9 @@
logi("Wii parser: unknown report type: 0x%02x\n", report[0]);
printf_hexdump(report, len);
}
@ -3458,7 +3467,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
}
void uni_hid_parser_wii_set_player_leds(uni_hid_device_t* d, uint8_t leds) {
@@ -1371,29 +2006,25 @@
@@ -1371,29 +2015,25 @@
void uni_hid_parser_wii_set_mode(uni_hid_device_t* d, wii_mode_t mode) {
wii_instance_t* ins = get_wii_instance(d);
@ -3503,7 +3512,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
uni_hid_device_send_intr_report(d, report, sizeof(report));
}
@@ -1480,10 +2111,13 @@
@@ -1480,10 +2120,13 @@
static void wii_read_mem(uni_hid_device_t* d, wii_read_type_t t, uint32_t offset, uint16_t size) {
logi("****** read_mem: offset=0x%04x, size=%d from=%d\n", offset, size, t);
@ -3518,7 +3527,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen
(offset & 0xff0000) >> 16, (offset & 0xff00) >> 8, (offset & 0xff), // Offset
(size & 0xff00) >> 8, (size & 0xff), // Size in bytes
// clang-format on
@@ -1491,6 +2125,15 @@
@@ -1491,6 +2134,15 @@
uni_hid_device_send_intr_report(d, report, sizeof(report));
}

View file

@ -163,7 +163,7 @@ static __force_inline void buffer_settle(void) {
static __force_inline void set_buffer(uint8_t device, uint8_t channel, uint32_t value) {
devices[device].buffers[channel] = value;
__dmb();
if ((active_device == device && (!bank_restore_pending || (channel & 1u))) ||
if ((active_device == device && (!bank_restore_pending || channel != 0)) ||
(device == 0 && channel == 2)) {
unsigned physical = physical_channel(device,channel);
buffer_regs()[physical] = value & ~USB_BUF_CTRL_AVAIL;
@ -191,8 +191,8 @@ bool __not_in_flash_func(native_hub_select_device)(uint8_t address, uint8_t owne
const device_t* restrict incoming = &devices[owner];
volatile uint32_t* buffers = (volatile uint32_t*)&usb_dpram->ep_buf_ctrl[0];
volatile uint32_t* controls = (volatile uint32_t*)&usb_dpram->ep_ctrl[0];
// Receive PID/availability must be selected before accepting an OUT token.
// Transmit buffers can safely NAK until Core0 publishes their contents.
// Select metadata before accepting a token. Only EP0 IN needs a later
// shared-buffer copy; other endpoints already have private DPRAM data.
for (unsigned i = 0; i < CHANNELS; ++i) {
uint32_t value = owner == 0 && i >= 2 ? 0 : incoming->buffers[i];
buffers[i] = value & ~USB_BUF_CTRL_AVAIL;
@ -200,7 +200,7 @@ bool __not_in_flash_func(native_hub_select_device)(uint8_t address, uint8_t owne
usb_dpram->ep_ctrl[14].in = owner == 0 ? hub_endpoint_control : 0;
for (unsigned i = 0; i < 4; ++i) controls[i] = incoming->endpoint_controls[i];
buffer_settle();
for (unsigned i = 1; i < CHANNELS; i += 2)
for (unsigned i = 1; i < CHANNELS; ++i)
buffers[i] = owner == 0 && i >= 2 ? 0 : incoming->buffers[i];
__dmb();
usb_hw->dev_addr_ctrl = address;
@ -220,7 +220,9 @@ bool __not_in_flash_func(native_hub_select_device)(uint8_t address, uint8_t owne
spin_unlock_unsafe(bank_lock);
return true;
}
static void __not_in_flash_func(restore_selected_bank)(void) {
// Section placement alone permits inlining into the flash-backed task. Keep
// bank-lock ownership independent of XIP instruction-cache refill latency.
static void __no_inline_not_in_flash_func(restore_selected_bank)(void) {
if (!bank_restore_pending) return;
uint32_t flags = spin_lock_blocking(bank_lock);
if (!bank_restore_pending || (usb_hw->sie_status & USB_SIE_STATUS_SETUP_REC_BITS) ||
@ -238,15 +240,11 @@ static void __not_in_flash_func(restore_selected_bank)(void) {
unsigned words = ((incoming->buffers[0] & USB_BUF_CTRL_LEN_MASK) + 3u) / 4u;
for (unsigned i = 0; i < words; ++i) to[i] = from[i];
}
for (unsigned i = 0; i < CHANNELS; i += 2) {
uint32_t value = owner == 0 && i >= 2 ? 0 : incoming->buffers[i];
buffers[i] = value & ~USB_BUF_CTRL_AVAIL;
}
buffers[0] = incoming->buffers[0] & ~USB_BUF_CTRL_AVAIL;
usb_hw->ep_stall_arm = ((incoming->buffers[0] & USB_BUF_CTRL_STALL) ? 1u : 0u) |
((incoming->buffers[1] & USB_BUF_CTRL_STALL) ? 2u : 0u);
buffer_settle();
for (unsigned i = 0; i < CHANNELS; i += 2)
buffers[i] = owner == 0 && i >= 2 ? 0 : incoming->buffers[i];
buffers[0] = incoming->buffers[0];
bank_restore_pending = false;
spin_unlock(bank_lock,flags);
}
@ -334,10 +332,24 @@ static void __not_in_flash_func(usb_interrupt)(void) {
uint8_t actual_owner = hardware_owner();
uint8_t setup[8];
copy_from_usb(setup, usb_dpram->setup_packet, sizeof(setup));
// A new SETUP revokes the preceding control transfer. Reclaim both
// physical EP0 buffers through the controller's ownership handshake,
// as the SDK DCD does, before changing their metadata or PID.
hw_set_bits(&usb_hw->abort, 3u);
unsigned abort_wait = 4096u;
while ((usb_hw->abort_done & 3u) != 3u && --abort_wait) {}
if ((usb_hw->abort_done & 3u) != 3u) {
// Never acknowledge new work over a buffer the SIE still owns.
failed = true;
usb_hw->inte = 0;
spin_unlock(bank_lock, flags);
return;
}
if (actual_owner < DEVICES && actual_owner == owner) {
++devices[owner].generation;
devices[owner].buffers[0] = devices[owner].buffers[1] = 0;
buffer_regs()[0] = buffer_regs()[1] = 0;
devices[owner].buffers[0] = devices[owner].buffers[1] =
USB_BUF_CTRL_DATA1_PID | USB_BUF_CTRL_SEL;
buffer_regs()[0] = buffer_regs()[1] = USB_BUF_CTRL_DATA1_PID | USB_BUF_CTRL_SEL;
devices[owner].ep[0].next_pid = devices[owner].ep[1].next_pid = 1;
push_event(owner,0,1,sizeof(setup),setup);
} else {
@ -345,6 +357,8 @@ static void __not_in_flash_func(usb_interrupt)(void) {
hw_set_bits(&usb_hw->ep_stall_arm,3u);
buffer_regs()[0] = buffer_regs()[1] = USB_BUF_CTRL_STALL;
}
hw_clear_bits(&usb_hw->abort_done, 3u);
hw_clear_bits(&usb_hw->abort, 3u);
hw_clear_bits(&usb_hw->sie_status,USB_SIE_STATUS_SETUP_REC_BITS);
}
if (status & USB_INTS_DEV_SUSPEND_BITS) {
@ -368,7 +382,7 @@ static void stall(uint8_t slot) {
set_buffer(slot,0,USB_BUF_CTRL_STALL); set_buffer(slot,1,USB_BUF_CTRL_STALL);
spin_unlock(bank_lock, flags);
}
static void __not_in_flash_func(arm_packet)(uint8_t slot, uint8_t channel, const uint8_t* data, uint16_t length) {
static void __no_inline_not_in_flash_func(arm_packet)(uint8_t slot, uint8_t channel, const uint8_t* data, uint16_t length) {
endpoint_t* ep = &devices[slot].ep[channel];
uint32_t generation = channel < 2 ? devices[slot].control.generation :
devices[slot].endpoint_generation[channel];
@ -726,7 +740,7 @@ static void transmit_next(uint8_t slot, uint8_t channel) {
uint8_t packet[64]; if (size) memcpy(packet,ep->data+ep->sent,size);
arm_packet(slot,channel,packet,size);
}
static void transfer_complete(const event_t* event) {
static void __no_inline_not_in_flash_func(transfer_complete)(const event_t* event) {
uint8_t slot = event->device, channel = event->channel;
device_t* d = &devices[slot];
if (event->reset_generation != d->reset_generation) return;
@ -742,6 +756,16 @@ static void transfer_complete(const event_t* event) {
if (event->generation != c->generation) return;
if ((c->stage == STATUS_IN && channel == 0) || (c->stage == STATUS_OUT && channel == 1)) {
if (event->length) stall(slot);
else if (c->stage == STATUS_OUT && (c->request.bmRequestType & 0x80u)) {
// Claim the completed read before a reset can revoke it, but
// let USB IRQs run while its read-only ACK callback executes.
uint32_t flags = save_and_disable_interrupts();
bool acknowledged = event->reset_generation == d->reset_generation;
if (acknowledged) c->stage = IDLE;
restore_interrupts(flags);
if (acknowledged && c->vendor)
tud_vendor_control_xfer_cb(slot,CONTROL_STAGE_ACK,&c->request);
}
else {
// Do not let a reset IRQ revoke ownership between checking it
// and publishing the acknowledged service transaction.

View file

@ -192,7 +192,7 @@ size_t encode_transaction(uint8_t* output, size_t output_size) {
size_t encode_info(uint8_t* output, size_t output_size) {
uint8_t payload[8] = {
#if SWITCH2_PROBE_HUB
0, 72, 0, 2,
0, 78, 0, 2,
kNativeHubActiveMode,
USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE |
USB_OUTPUT_CAPABILITY_MOTION,

View file

@ -1108,7 +1108,7 @@ class ProfilePlaytest:
"right": self.triggers[1],
},
"battery": (
round((self.battery - 1) / 250 * 100) if self.battery != 0 else None
round((self.battery - 1) / 254 * 100) if self.battery != 0 else None
),
"capabilities": [
name

View file

@ -17,6 +17,9 @@ bool native_test_setup(uint8_t, const tusb_control_request_t*, bool);
bool native_test_out(uint8_t, const uint8_t*, uint16_t, bool);
bool native_test_in(uint8_t, uint8_t*, uint16_t*, bool);
void native_test_bus_reset(bool);
void native_test_hold_abort(bool);
bool native_test_select(uint8_t);
bool native_test_private_in(uint8_t, uint8_t, uint8_t*, uint16_t*);
}
namespace {
@ -26,6 +29,7 @@ uint32_t programs = 0;
uint32_t erases = 0;
uint32_t bootsel_calls = 0;
std::array<uint8_t, 64> child_identity[2];
bool interleave_identity_ack = false;
void require(bool condition, const char* message) {
if (!condition) { std::cerr << message << '\n'; std::exit(1); }
@ -155,9 +159,9 @@ void test_profile_transport() {
const auto original = encoded_profile(0);
const auto edited = encoded_profile(4);
auto info = read_operation(Operation::kInfo);
require(info[kResponseHeaderSize] == 0 && info[kResponseHeaderSize + 1] == 72 &&
info[kResponseHeaderSize + 2] == 0 && info[kResponseHeaderSize + 4] == 5 &&
info[kResponseHeaderSize + 5] == 7, "native INFO does not describe the fixed image");
require(info.size() == kResponseHeaderSize + 8 &&
info[kResponseHeaderSize + 4] == 5 && info[kResponseHeaderSize + 5] == 7,
"native INFO does not describe the fixed output and its capabilities");
auto list = read_operation(Operation::kProfileList);
require(list[kResponseHeaderSize] == 1 && list.size() > 64, "root catalog omitted the global profile owner");
auto playtest = read_operation(Operation::kProfilePlaytest);
@ -261,6 +265,74 @@ void test_interrupted_transactions() {
require(!native_test_in(0, packet, &length, true), "completed request retained a second status ACK");
}
void test_pending_control_buffer_ownership() {
const auto info = request(Operation::kInfo, true, kMaximumResponseSize);
require(native_test_setup(0, &info, true), "pending INFO setup failed");
const unsigned programs_before = programs, erases_before = erases;
native_test_hold_abort(true);
const auto replacement = request(Operation::kProfileSelect, false, kRequestHeaderSize + 15);
require(!native_test_setup(0, &replacement, false),
"new SETUP replaced an EP0 buffer before the controller released ownership");
uint8_t packet[64]; uint16_t length;
require(!native_test_in(0, packet, &length, false),
"an unquiesced control endpoint acknowledged replacement work");
profile_service_task_on_storage_core(4000);
require(programs == programs_before && erases == erases_before,
"unquiesced replacement changed saved profiles");
native_test_initialize();
}
void test_read_ack_allows_usb_progress() {
interleave_identity_ack = true;
read_child(1);
const auto next = receive(2);
require(next == std::vector<uint8_t>(child_identity[1].begin(), child_identity[1].end()),
"SETUP received during read ACK did not retain the next child's response");
}
void test_private_transmit_survives_round_robin_tokens() {
native_test_initialize();
tusb_control_request_t configuration{};
configuration.bRequest = TUSB_REQ_SET_CONFIGURATION;
configuration.wValue = 1;
for (uint8_t slot : {1, 2}) {
require(native_test_setup(slot, &configuration, true), "child configuration failed");
acknowledge(slot);
}
const uint8_t payloads[2][3] = {{0x11, 0x22, 0x33}, {0x44, 0x55, 0x66}};
for (uint8_t instance : {0, 1}) {
require(native_hub_hid_report(instance, instance ? 7 : 8, payloads[instance], 3),
"could not queue HID packet");
require(native_hub_vendor_write(instance, payloads[instance], 3) == 3 &&
native_hub_vendor_write_flush(instance) == 3, "could not queue bulk packet");
}
uint8_t packet[64];
uint16_t length = 0;
for (uint8_t endpoint : {0x81, 0x82}) {
for (uint8_t slot : {1, 2}) {
require(native_test_private_in(slot, endpoint, packet, &length),
"queued private IN packet required foreground work after bank selection");
const unsigned prefix = endpoint == 0x81 ? 1 : 0;
require(length == 3 + prefix &&
(!prefix || packet[0] == (slot == 1 ? 8 : 7)) &&
std::memcmp(packet + prefix, payloads[slot - 1], 3) == 0,
"round-robin IN token received another endpoint's payload");
}
}
native_test_drain();
require(native_hub_hid_ready(0) && native_hub_hid_ready(1),
"acknowledged HID packets did not release their queues");
require(!native_test_private_in(1, 0x81, packet, &length),
"acknowledged HID packet was retransmitted");
// The idle poll selected R without restoring its shared EP0 image.
require(native_hub_hid_report(0, 8, payloads[0], 3), "could not queue the next HID packet");
require(native_test_private_in(1, 0x81, packet, &length) && length == 4 &&
std::memcmp(packet + 1, payloads[0], 3) == 0,
"pending shared EP0 restoration blocked a newly queued private IN packet");
native_test_drain();
native_test_initialize();
}
void test_private_bootsel() {
const auto bytes = envelope(Operation::kBootselReboot, {});
const auto setup = request(Operation::kBootselReboot, false, bytes.size());
@ -336,6 +408,14 @@ extern "C" bool tud_vendor_control_xfer_cb(uint8_t slot, uint8_t stage, const tu
if (probe_management_vendor_control(slot, stage, setup)) return true;
if (slot < 1 || slot > 2 || setup->bmRequestType != 0xc0 ||
setup->bRequest != 3 || setup->wValue || setup->wIndex) return false;
if (stage == CONTROL_STAGE_ACK && slot == 1 && interleave_identity_ack) {
interleave_identity_ack = false;
const auto next = *setup;
require(native_test_setup(2, &next, false), "next child's SETUP was rejected during read ACK");
// SETUP must be serviced before another token can change the bank.
// This observes IRQ progress, rather than inspecting the CPU mask.
require(native_test_select(0), "read ACK callback blocked servicing the next USB SETUP");
}
return stage != CONTROL_STAGE_SETUP || native_hub_control_xfer(slot, setup,
child_identity[slot - 1].data(), child_identity[slot - 1].size());
}
@ -347,6 +427,9 @@ int main() {
native_test_initialize();
test_profile_transport();
test_interrupted_transactions();
test_pending_control_buffer_ownership();
test_read_ack_allows_usb_progress();
test_private_transmit_survives_round_robin_tokens();
test_private_bootsel();
std::cout << "native root management packet and persistence regressions passed\n";
}

View file

@ -5,14 +5,28 @@
#include <string.h>
#define __not_in_flash_func(name) name
#define __no_inline_not_in_flash_func(name) __attribute__((noinline)) name
#define __force_inline inline __attribute__((always_inline))
#define __dmb() ((void)0)
typedef struct { unsigned unused; } spin_lock_t;
static inline uint32_t save_and_disable_interrupts(void) { return 0; }
static inline void restore_interrupts(uint32_t flags) { (void)flags; }
static inline uint32_t spin_lock_blocking(spin_lock_t* lock) { (void)lock; return 0; }
static inline void spin_unlock(spin_lock_t* lock, uint32_t flags) { (void)lock; (void)flags; }
extern uint32_t native_test_interrupt_mask;
void native_test_service_interrupt(void);
static inline uint32_t save_and_disable_interrupts(void) {
uint32_t flags = native_test_interrupt_mask;
native_test_interrupt_mask = 1;
return flags;
}
static inline void restore_interrupts(uint32_t flags) {
native_test_interrupt_mask = flags;
native_test_service_interrupt();
}
static inline uint32_t spin_lock_blocking(spin_lock_t* lock) {
(void)lock; return save_and_disable_interrupts();
}
static inline void spin_unlock(spin_lock_t* lock, uint32_t flags) {
(void)lock; restore_interrupts(flags);
}
static inline bool spin_try_lock_unsafe(spin_lock_t* lock) { (void)lock; return true; }
static inline void spin_unlock_unsafe(spin_lock_t* lock) { (void)lock; }
static inline int spin_lock_claim_unused(bool required) { (void)required; return 0; }
@ -20,13 +34,13 @@ static inline spin_lock_t* spin_lock_instance(unsigned index) {
static spin_lock_t lock; (void)index; return &lock;
}
static inline void hw_clear_bits(volatile uint32_t* address, uint32_t bits) { *address &= ~bits; }
static inline void hw_set_bits(volatile uint32_t* address, uint32_t bits) { *address |= bits; }
typedef struct {
volatile uint32_t ints, sie_status, buf_status, dev_addr_ctrl, inte;
volatile uint32_t ep_stall_arm, muxing, phy_direct, phy_direct_override;
volatile uint32_t pwr, main_ctrl, sie_ctrl, ep_nak_stall_status;
volatile uint32_t ep_tx_error, ep_rx_error;
volatile uint32_t abort, abort_done;
} usb_hw_t;
typedef struct { volatile uint32_t in, out; } usb_pair_t;
typedef struct {
@ -46,6 +60,13 @@ extern sio_hw_t native_test_sio;
#define USBCTRL_DPRAM_BASE ((uintptr_t)usb_dpram)
#define USB_DPRAM_SIZE sizeof(*usb_dpram)
extern bool native_test_abort_stuck;
static inline void hw_set_bits(volatile uint32_t* address, uint32_t bits) {
*address |= bits;
if (address == &usb_hw->abort && !native_test_abort_stuck)
usb_hw->abort_done |= bits;
}
#define USB_BUF_CTRL_LEN_MASK 0x3ffu
#define USB_BUF_CTRL_AVAIL (1u << 10)
#define USB_BUF_CTRL_STALL (1u << 11)

View file

@ -4,6 +4,17 @@
usb_hw_t native_test_usb;
usb_device_dpram_t native_test_dpram;
sio_hw_t native_test_sio;
bool native_test_abort_stuck;
uint32_t native_test_interrupt_mask;
static bool servicing_interrupt;
void native_test_service_interrupt(void) {
if (native_test_interrupt_mask || servicing_interrupt || !usb_hw->ints) return;
servicing_interrupt = true;
usb_interrupt();
usb_hw->ints = 0;
servicing_interrupt = false;
}
void probe_router_init(uint32_t hz) { (void)hz; }
void probe_router_core1(void) {}
@ -36,6 +47,9 @@ void native_test_initialize(void) {
memset(usb_hw,0,sizeof(*usb_hw));
memset(usb_dpram,0,sizeof(*usb_dpram));
event_head = event_tail = 0;
native_test_abort_stuck = false;
native_test_interrupt_mask = 0;
servicing_interrupt = false;
failed = bus_suspended = bank_restore_pending = false;
bank_lock = spin_lock_instance(0);
active_device = default_device = 0;
@ -49,6 +63,8 @@ static bool select_slot(uint8_t slot) {
return true;
}
bool native_test_select(uint8_t slot) { return select_slot(slot); }
void native_test_drain(void) { native_hub_task(); }
bool native_test_setup(uint8_t slot, const tusb_control_request_t* request, bool drain) {
@ -56,14 +72,16 @@ bool native_test_setup(uint8_t slot, const tusb_control_request_t* request, bool
memcpy(usb_dpram->setup_packet,request,sizeof(*request));
usb_hw->sie_status = USB_SIE_STATUS_SETUP_REC_BITS;
usb_hw->ints = USB_INTS_SETUP_REQ_BITS;
usb_interrupt();
usb_hw->ints = 0;
native_test_service_interrupt();
if (drain) native_hub_task();
return !failed && devices[slot].control.stage != STALLED;
}
void native_test_hold_abort(bool hold) { native_test_abort_stuck = hold; }
bool native_test_out(uint8_t slot, const uint8_t* data, uint16_t length, bool drain) {
if (!select_slot(slot)) return false;
if (usb_hw->abort & 2u) return false;
uint32_t value = buffer_regs()[1];
if (!(value & USB_BUF_CTRL_AVAIL) || (value & USB_BUF_CTRL_STALL) ||
length > (value & USB_BUF_CTRL_LEN_MASK)) return false;
@ -71,14 +89,14 @@ bool native_test_out(uint8_t slot, const uint8_t* data, uint16_t length, bool dr
buffer_regs()[1] = (value & ~(USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_LEN_MASK)) | length;
usb_hw->buf_status = 2;
usb_hw->ints = USB_INTS_BUFF_STATUS_BITS;
usb_interrupt();
usb_hw->ints = 0;
native_test_service_interrupt();
if (drain) native_hub_task();
return !failed && devices[slot].control.stage != STALLED;
}
bool native_test_in(uint8_t slot, uint8_t* data, uint16_t* length, bool drain) {
if (!select_slot(slot)) return false;
if (usb_hw->abort & 1u) return false;
uint32_t value = buffer_regs()[0];
if (!(value & USB_BUF_CTRL_AVAIL) || !(value & USB_BUF_CTRL_FULL) ||
(value & USB_BUF_CTRL_STALL)) return false;
@ -87,17 +105,35 @@ bool native_test_in(uint8_t slot, uint8_t* data, uint16_t* length, bool drain) {
buffer_regs()[0] = value & ~USB_BUF_CTRL_AVAIL;
usb_hw->buf_status = 1;
usb_hw->ints = USB_INTS_BUFF_STATUS_BITS;
usb_interrupt();
usb_hw->ints = 0;
native_test_service_interrupt();
if (drain) native_hub_task();
return !failed && devices[slot].control.stage != STALLED;
}
bool native_test_private_in(uint8_t slot, uint8_t endpoint, uint8_t* data, uint16_t* length) {
if (slot < 1 || slot > 2 || (endpoint != 0x81 && endpoint != 0x82)) return false;
// A host token selects the bank, but cannot wait for a foreground task.
if (!native_hub_select_device(addresses[slot],slot,UINT32_MAX / 2)) return false;
unsigned channel = (endpoint & 15u) * 2u;
uint32_t control = endpoint_regs()[channel - 2u];
uint32_t value = buffer_regs()[channel];
if (!(control & EP_CTRL_ENABLE_BITS) || !(value & USB_BUF_CTRL_AVAIL) ||
!(value & USB_BUF_CTRL_FULL) || (value & USB_BUF_CTRL_STALL)) return false;
*length = value & USB_BUF_CTRL_LEN_MASK;
if (*length > PACKET) return false;
if (*length) copy_from_usb(data,
(const volatile uint8_t*)USBCTRL_DPRAM_BASE + (control & 0xffffu), *length);
buffer_regs()[channel] = value & ~USB_BUF_CTRL_AVAIL;
usb_hw->buf_status |= 1u << channel;
usb_hw->ints |= USB_INTS_BUFF_STATUS_BITS;
native_test_service_interrupt();
return !failed;
}
void native_test_bus_reset(bool drain) {
usb_hw->sie_status = USB_SIE_STATUS_BUS_RESET_BITS;
usb_hw->ints = USB_INTS_BUS_RESET_BITS;
usb_interrupt();
usb_hw->ints = 0;
native_test_service_interrupt();
if (drain) native_hub_task();
// Assign fixture addresses after reset, independently of EP0 state.
addresses[0] = 0; addresses[1] = 1; addresses[2] = 2;

View file

@ -121,7 +121,7 @@ void mapped_halves_and_calibration() {
source.controller.active = true;
source.controller.connection_generation = 7;
source.controller.identity = controller_identity_global();
source.battery = 255;
source.battery = 128;
publish();
// Neither an absent source nor an uncalibrated child masquerades as active.
assert(!peek(0) && !controls[0].active);
@ -132,7 +132,7 @@ void mapped_halves_and_calibration() {
pair();
assert(stick_x(0) == 2000 && stick_y(0) == 2100);
assert(stick_x(1) == 1800 && stick_y(1) == 1900);
assert(reports[0][1] == 0x25 && reports[1][1] == 0x25); // Known full level, USB powered, not charging.
assert(reports[0][1] == 0x15 && reports[1][1] == 0x15); // Measured half battery, USB powered, not charging.
// Actual profile transforms can move controls across native children.
profile.button_map[static_cast<unsigned>(ControllerProfileLogicalButton::kSouth)] =
static_cast<uint8_t>(ControllerProfileLogicalButton::kDpadRight);
@ -364,7 +364,7 @@ void nunchuk_buttons_map_to_native_left_shoulders() {
++source.controller.connection_generation;
source.controller.state = {};
source.accel_valid = source.gyro_valid = false;
source.battery = 0; // Unknown source battery must not invent a full charge.
source.battery = 0;
profile = controller_profile_default(controller_identity_global(), 0);
// The real Wii parser maps Nunchuk C to west and Z to north. These are
// ordinary profile inputs, not the unrelated Switch2 extra "C" control.
@ -375,7 +375,7 @@ void nunchuk_buttons_map_to_native_left_shoulders() {
source.controller.state.button_west = true; // C -> ZL.
publish(false); pair();
assert(reports[0][2] == 0 && reports[1][2] == 0x20);
assert(reports[0][1] == 0x01 && reports[1][1] == 0x01); // USB power remains real even without battery telemetry.
assert(reports[0][1] == 0x01 && reports[1][1] == 0x01);
source.controller.state.button_west = false;
source.controller.state.button_north = true; // Z -> L.
publish(false); pair();

View file

@ -143,9 +143,10 @@ int main() {
source.accel_valid = source.gyro_valid = true;
source.accel_q13[1] = 8192; // SDL up -> virtual native rail-down +X.
memcpy(source.gyro_q10, bias_q10, sizeof(bias_q10));
source.battery = 128;
assert(poll());
assert(controls.active && packet[15] == 30); // Wii IMU starts before background bias learning.
assert(packet[1] == 0x01); // USB power, no fabricated charge level or charging state.
assert(packet[1] == 0x15); // Measured half battery, USB powered, not charging.
for (unsigned i = 1; i < 450; ++i) assert(poll());
assert(controls.active && packet[15] == 30 && packet[19] == 0x0c);
assert(signed32(packet+32) == (1 << 28));

View file

@ -2801,6 +2801,8 @@ def test_profile_playtest_decodes_raw_controller_state() -> None:
"dpad_up",
"dpad_right",
]
assert replace(playtest, battery=255).to_json_object()["battery"] == 100
assert replace(playtest, battery=1).to_json_object()["battery"] == 0
device.playtest_connected = False
disconnected = config_manager.read_profile_playtest(device)
@ -2818,6 +2820,7 @@ def test_profile_playtest_decodes_raw_controller_state() -> None:
capabilities=0,
motion=None,
)
assert disconnected.to_json_object()["battery"] is None
device.playtest_connected = True
payload, flags = device._profile_playtest_payload()
malformed = bytearray(payload)

View file

@ -1232,6 +1232,33 @@ static void nunchuk_accel_detach_and_replacement_require_fresh_calibration(void)
}
}
static void battery_status_survives_input_reports(void) {
reset_fixture(0x0306, false, EXT_NONE);
assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_NOT_AVAILABLE);
uni_hid_parser_wii_setup(&f.device);
finish_setup();
assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_FULL);
send_core_and_accel();
assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_FULL);
uint8_t status[] = {0x20, 0, 0, 0, 0, 0, 52};
feed(status, sizeof(status));
finish_setup();
assert(f.device.controller.battery == 179); // Measured 70% band, not a full-charge placeholder.
send_core_and_accel();
assert(f.device.controller.battery == 179);
status[6] = 0;
feed(status, sizeof(status) - 1);
assert(f.device.controller.battery == 179); // Truncation cannot erase the last measurement.
feed(status, sizeof(status));
finish_setup();
assert(f.device.controller.battery == 13); // Measured low band remains distinct from unknown.
send_core_and_accel();
assert(f.device.controller.battery == 13);
uni_hid_parser_wii_setup(&f.device);
assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_NOT_AVAILABLE);
}
static void run_case(const char* name, void (*test)(void)) {
printf("Wii parser: %s\n", name);
fflush(stdout);
@ -1239,6 +1266,7 @@ static void run_case(const char* name, void (*test)(void)) {
}
int main(void) {
run_case("real battery retained between status and input reports", battery_status_survives_input_reports);
run_case("fresh calibrated accelerometer snapshots without MotionPlus", accelerometer_snapshot_requires_fresh_calibrated_reports);
run_case("independent fresh calibrated MotionPlus gyro snapshots", gyro_snapshot_advances_only_on_calibrated_motionplus_packets);
run_case("gyro validity through hotplug, replacement and teardown", gyro_snapshot_invalidates_on_topology_and_teardown);

View file

@ -43,7 +43,7 @@ _Static_assert(PROBE_ROUTER_SLOTS == 3u, "Packed setup owner has three slots");
typedef struct {
uint8_t owner[128];
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
uint8_t early_address[2][16];
uint8_t early_address[2][256];
#endif
} routing_table;
@ -149,6 +149,9 @@ static void build_table(routing_table* table,
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
// A unique observed prefix can preselect the SIE sooner. It still compares
// the complete hardware address and CRC before accepting the transaction.
// Index the four captured line pairs directly. Packing their D+ bits in
// the sampling window delays bit 20 even when the prefix is ambiguous.
memset(table->early_address, PROBE_ROUTER_UNASSIGNED, sizeof(table->early_address));
for (unsigned kind = 0; kind < 2; ++kind) {
for (unsigned prefix = 0; prefix < 16; ++prefix) {
uint8_t candidate = PROBE_ROUTER_UNASSIGNED;
@ -161,7 +164,10 @@ static void build_table(routing_table* table,
}
candidate = address;
}
table->early_address[kind][prefix] = candidate;
unsigned raw_prefix = 0u;
for (unsigned bit = 0; bit < 4; ++bit)
raw_prefix |= ((prefix >> bit) & 1u ? LINE_J : LINE_K) << (2u * bit);
table->early_address[kind][raw_prefix] = candidate;
}
}
#endif
@ -331,14 +337,12 @@ static bool observe_idle_j(void);
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
static raw_packet __no_inline_not_in_flash_func(capture_packet)(
uint32_t phase, const routing_table* table, bool draining) {
prepare_capture:;
#else
static raw_packet __no_inline_not_in_flash_func(capture_packet)(
uint32_t phase, const routing_table* table) {
#endif
raw_packet result = {0};
uint32_t word0 = LINE_K, word1 = 0u, word2 = 0u;
uint32_t address_wire = 0u;
const uint8_t* decoder = NULL;
uint32_t expected_word = 0u;
const uint32_t initial_address = usb_hw->dev_addr_ctrl;
@ -351,6 +355,7 @@ static raw_packet __no_inline_not_in_flash_func(capture_packet)(
// forcing them into live registers adds spills and unnecessary ORs.
__asm volatile ("" : "+r"(word0), "+m"(result) : : "memory");
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
drain_prepared_capture:;
if (draining) {
const uint32_t stop = cycles_now() + FS_CLOCK_HZ / 10000u;
bool saw_se0 = false;
@ -422,10 +427,16 @@ edge:
// The first stored K is the observed SOP above, not an invented SYNC bit.
#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB
#define SET_EARLY_DECODER(kind) (early_decoder = table->early_address[kind])
#define DISCARD_NON_TOKEN() do { draining = true; goto prepare_capture; } while (0)
// PID rejection happens before any address/body samples. Reuse the prepared
// frame: rebuilding its stack image here can miss the end of a short ACK/NAK
// and the following token. All other result/address accumulators are still zero.
#define DISCARD_NON_TOKEN() do { \
result.sop = false; word0 = LINE_K; draining = true; \
goto drain_prepared_capture; \
} while (0)
#define ROUTE_EARLY(bit, base) do { \
if ((base) + (bit) == 19u && decoder != NULL) { \
uint8_t candidate = early_decoder[address_wire]; \
uint8_t candidate = early_decoder[word1 & 0xffu]; \
if (candidate < 128u) \
route_header(table, candidate, word0 == 0x9a56a666u, initial_address, \
deadline + 11u * FS_BIT_CYCLES, &result); \
@ -447,10 +458,13 @@ edge:
decoder = NULL; \
DISCARD_NON_TOKEN(); \
} \
if ((base) + (bit) >= 16u && (base) + (bit) <= 23u) \
address_wire |= (line & 1u) << (bit); \
ROUTE_EARLY(bit, base); \
if ((base) + (bit) == 23u && decoder != NULL) { \
/* Compact observed D+ bits only after all eight symbols exist. */ \
uint32_t address_wire = word1 & 0x5555u; \
address_wire = (address_wire | (address_wire >> 1u)) & 0x3333u; \
address_wire = (address_wire | (address_wire >> 2u)) & 0x0f0fu; \
address_wire = (address_wire | (address_wire >> 4u)) & 0xffu; \
route_header(table, decoder[address_wire], word0 == 0x9a56a666u, \
initial_address, deadline + 7u * FS_BIT_CYCLES, &result); \
if (result.late) { result.count = (base) + (bit) + 1u; goto done; } \

View file

@ -80,7 +80,7 @@ int g_sensor_status = -1;
uint16_t g_stick_center[2]{2048, 2048};
uint16_t g_stick_positive[2]{2047, 2047};
uint16_t g_stick_negative[2]{2048, 2048};
uint8_t g_power_info = 0x01; // USB powered; no invented source charge or charging state.
uint8_t g_power_info = 0x01;
uint8_t g_report_counter;
uint32_t g_report_serial;
uint32_t g_pending_serial;

View file

@ -290,9 +290,7 @@ uint32_t probe_native_gamepad_input_peek_native_report(uint8_t instance, uint32_
if (g_report_token == UINT32_MAX) return 0; // Boot-unique, including across children/resets.
memset(child.pending_report, 0, sizeof(child.pending_report));
child.pending_report[0] = child.counter;
// Battery telemetry belongs to the source; external power belongs to
// these USB-powered virtual controllers. Unknown is not a full charge,
// and the Pico does not charge the wireless source.
// Source battery level and the virtual controller's USB power are separate.
const unsigned battery_level = (static_cast<unsigned>(g_source.battery) * 9u + 127u) / 255u;
child.pending_report[1] = static_cast<uint8_t>((battery_level << 2) | 0x01u);
memcpy(child.pending_report + 2, child.input.buttons, sizeof(child.input.buttons));

View file

@ -369,12 +369,12 @@ function(switch2_usb_probe_configure target)
endif()
if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.72-native-usb-power")
pico_set_program_version(${target} "0.78-native-tx-bank-trace")
else()
pico_set_program_version(${target} "0.72-native-gamepad")
pico_set_program_version(${target} "0.78-native-gamepad")
endif()
elseif(SWITCH2_PROBE_HUB)
pico_set_program_version(${target} "0.72-native-hub-editor")
pico_set_program_version(${target} "0.78-native-hub-tx-bank")
elseif(SWITCH2_PROBE_JOIN_CHORD_GATE)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.37-pair-chord-trace")