From a407e84a398e3826ead02948436bd58715b771e9 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sun, 13 Sep 2026 09:28:46 -0600 Subject: [PATCH] Correct USB power reporting for translated native controllers --- README.md | 22 ++++++++++++++++ src/firmware/usb/native_hub/native_hub.c | 26 +++++++++++++++++++ tests/switch2_native_gamepad_bridge_test.cpp | 4 +++ tests/switch2_wii_bridge_test.cpp | 1 + tools/switch2_usb_probe/controller_input.cpp | 8 +++--- .../native_gamepad_input.cpp | 6 +++-- tools/switch2_usb_probe/probe_build.cmake | 2 +- 7 files changed, 62 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 565f9e8..01875ea 100644 --- a/README.md +++ b/README.md @@ -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; diff --git a/src/firmware/usb/native_hub/native_hub.c b/src/firmware/usb/native_hub/native_hub.c index 5225a9d..0318326 100644 --- a/src/firmware/usb/native_hub/native_hub.c +++ b/src/firmware/usb/native_hub/native_hub.c @@ -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 diff --git a/tests/switch2_native_gamepad_bridge_test.cpp b/tests/switch2_native_gamepad_bridge_test.cpp index 3827dd1..2e361e2 100644 --- a/tests/switch2_native_gamepad_bridge_test.cpp +++ b/tests/switch2_native_gamepad_bridge_test.cpp @@ -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(ControllerProfileLogicalButton::kSouth)] = static_cast(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(); diff --git a/tests/switch2_wii_bridge_test.cpp b/tests/switch2_wii_bridge_test.cpp index 691e923..0cd3917 100644 --- a/tests/switch2_wii_bridge_test.cpp +++ b/tests/switch2_wii_bridge_test.cpp @@ -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)); diff --git a/tools/switch2_usb_probe/controller_input.cpp b/tools/switch2_usb_probe/controller_input.cpp index b8145ca..6b0856b 100644 --- a/tools/switch2_usb_probe/controller_input.cpp +++ b/tools/switch2_usb_probe/controller_input.cpp @@ -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(g_wii.battery) * 9u + 127u) / 255u; - g_power_info = static_cast(level << 2); + g_power_info = static_cast((level << 2) | 0x01u); } ProbeNativeMotionSample motion{}; motion.accel_valid = g_wii.accel_valid; diff --git a/tools/switch2_usb_probe/native_gamepad_input.cpp b/tools/switch2_usb_probe/native_gamepad_input.cpp index 1f3d047..a50ef09 100644 --- a/tools/switch2_usb_probe/native_gamepad_input.cpp +++ b/tools/switch2_usb_probe/native_gamepad_input.cpp @@ -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(g_source.battery) * 9u + 127u) / 255u; - child.pending_report[1] = static_cast(battery_level << 2); + child.pending_report[1] = static_cast((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)); diff --git a/tools/switch2_usb_probe/probe_build.cmake b/tools/switch2_usb_probe/probe_build.cmake index 94e9131..5bec805 100644 --- a/tools/switch2_usb_probe/probe_build.cmake +++ b/tools/switch2_usb_probe/probe_build.cmake @@ -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()