Correct USB power reporting for translated native controllers

This commit is contained in:
Joey Yakimowich-Payne 2026-09-13 09:28:46 -06:00
commit a407e84a39
7 changed files with 62 additions and 7 deletions

View file

@ -985,6 +985,28 @@ region matched the pre-update backup. Native R/L descriptors, EP0 identity,
initialization and bulk-isolation checks passed while editor traffic was active.
No physical Switch L+R button press was claimed by that transport check.
**USB power-status correction under qualification:** a subsequent Switch trial
accepted both halves and delivered roughly 38,000 reports per side before
input stopped around 313 seconds. Wii Bluetooth input remained active, and the
host cleared endpoint halts. No explicit sleep command or five-minute firmware
timer was observed; the cause of the timeout is not yet established.
That trace exposed a separate concrete metadata error: translated reports had
the external-power flag clear. The published
[native Power Info format](https://github.com/ndeadly/switch2_controller_research/blob/master/hid_reports.md#input-report-0x07)
defines bit 0 as external power. Translated `GAMEPAD` and dedicated `WII` USB
outputs now set it while preserving source battery telemetry and leaving the
charging bit clear. An unknown battery does not become a fabricated full charge;
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.
For sensorless hardware, the checker supports `--input-only`: press real buttons
and keep changing controls on both halves during the run. Neutral fallback
alone cannot qualify. The result explicitly records that IMU was not required;

View file

@ -917,6 +917,32 @@ void native_hub_task(void) {
" lock=%"PRIu32" blocked=%08"PRIx32"/%08"PRIx32" rootnak=%"PRIu32"\n",
token_hits[0],token_hits[1],token_hits[2],missed_lock,
blocked_buffers,blocked_sie,root_naks);
#if SWITCH2_PROBE_TRACE_NATIVE_INPUT
probe_router_stats observer;
probe_router_snapshot(&observer);
uint32_t interrupt_mask = save_and_disable_interrupts();
restore_interrupts(interrupt_mask);
probe_debug_printf("[HUB_HEALTH] router_ready=%"PRIu32" default=%u irq_mask=%"PRIu32
" buf_status=%08"PRIx32" irq_status=%08"PRIx32
" hub_ep=%08"PRIx32"/%08"PRIx32" busy=%u\n",
observer.ready,default_device,interrupt_mask,
usb_hw->buf_status,usb_hw->ints,
usb_dpram->ep_ctrl[14].in,buffer_regs()[30],devices[0].ep[2].busy);
probe_debug_printf("[HUB_RAW] intr=%08"PRIx32" inte=%08"PRIx32" sof=%"PRIu32
" mtime=%08"PRIx32" watchdog=%08"PRIx32" nak_poll=%08"PRIx32"\n",
usb_hw->intr,usb_hw->inte,usb_hw->sof_rd,sio_hw->mtime,
usb_hw->dev_sm_watchdog,usb_hw->nak_poll);
probe_debug_printf("[HUB_PORTS] status=%04x/%04x change=%04x/%04x\n",
ports[0].status,ports[1].status,ports[0].change,ports[1].change);
for (uint8_t slot = 0; slot < DEVICES; ++slot) {
const device_t* d = &devices[slot];
probe_debug_printf("[HUB_EP0] slot=%u stage=%u gen=%"PRIu32"/%"PRIu32
" reset=%"PRIu32" request=%02x/%02x pos=%u/%u buffers=%08"PRIx32"/%08"PRIx32"\n",
slot,(unsigned)d->control.stage,d->control.generation,d->generation,
d->reset_generation,d->control.request.bmRequestType,d->control.request.bRequest,
d->control.position,d->control.length,d->buffers[0],d->buffers[1]);
}
#endif
}
watchdog_update();
// This qualification firmware must remain recoverable if the hub never

View file

@ -121,6 +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;
publish();
// Neither an absent source nor an uncalibrated child masquerades as active.
assert(!peek(0) && !controls[0].active);
@ -131,6 +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.
// Actual profile transforms can move controls across native children.
profile.button_map[static_cast<unsigned>(ControllerProfileLogicalButton::kSouth)] =
static_cast<uint8_t>(ControllerProfileLogicalButton::kDpadRight);
@ -362,6 +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.
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.
@ -372,6 +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.
source.controller.state.button_west = false;
source.controller.state.button_north = true; // Z -> L.
publish(false); pair();

View file

@ -145,6 +145,7 @@ int main() {
memcpy(source.gyro_q10, bias_q10, sizeof(bias_q10));
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.
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

@ -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 = 0x24; // Nominal battery until source status; no USB-power flag.
uint8_t g_power_info = 0x01; // USB powered; no invented source charge or charging state.
uint8_t g_report_counter;
uint32_t g_report_serial;
uint32_t g_pending_serial;
@ -254,8 +254,8 @@ void poll_wii_source(uint32_t now_ms) {
#ifdef SWITCH2_PROBE_TRACE_NATIVE_INPUT
g_last_ir_trace_us = now_us;
#endif
g_power_info = 0x24;
probe_debug_printf("[PROBE] Wii source active in slot %u; keep still for native motion calibration\n",
g_power_info = 0x01;
probe_debug_printf("[PROBE] Wii source active in slot %u; native bias learns in background\n",
g_wii.slot);
}
g_wii_active = true;
@ -273,7 +273,7 @@ void poll_wii_source(uint32_t now_ms) {
}
if (g_wii.battery != 0) {
const unsigned level = (static_cast<unsigned>(g_wii.battery) * 9u + 127u) / 255u;
g_power_info = static_cast<uint8_t>(level << 2);
g_power_info = static_cast<uint8_t>((level << 2) | 0x01u);
}
ProbeNativeMotionSample motion{};
motion.accel_valid = g_wii.accel_valid;

View file

@ -290,9 +290,11 @@ 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;
// Bluepad32 zero denotes unknown; do not manufacture a full battery.
// 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.
const unsigned battery_level = (static_cast<unsigned>(g_source.battery) * 9u + 127u) / 255u;
child.pending_report[1] = static_cast<uint8_t>(battery_level << 2);
child.pending_report[1] = static_cast<uint8_t>((battery_level << 2) | 0x01u);
memcpy(child.pending_report + 2, child.input.buttons, sizeof(child.input.buttons));
child.pending_report[4] = 7;
memcpy(child.pending_report + 5, child.input.stick, sizeof(child.input.stick));

View file

@ -369,7 +369,7 @@ 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-gamepad-trace")
pico_set_program_version(${target} "0.72-native-usb-power")
else()
pico_set_program_version(${target} "0.72-native-gamepad")
endif()