diff --git a/README.md b/README.md index 84b9351..e918518 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ The Pico 2 W onboard LED reports the overall Bluetooth state: - **Disconnect a controller**: its slot immediately publishes neutral buttons, sticks, and motion. Other connected controllers are unaffected. - **Reconnect a paired controller**: power it on normally with its Home, PS, or Xbox button. +- **8BitDo Ultimate Bluetooth reconnect**: leave its selector in Bluetooth mode, press Home once, then shake it. After an abrupt controller power-off, the Pico can remain solid for up to four seconds while Bluetooth link supervision confirms the disconnect; scanning restarts immediately afterward. - **Pair a new controller**: hold BOOTSEL until the LED double-blinks, then put the controller into its explicit Bluetooth pairing mode. - **Pairing window expires**: new authentication is disabled; discovery and remembered-controller autoconnect continue while a slot is free. - **Clear all pairings**: hold BOOTSEL continuously for 10 seconds, through the initial double blink, until the rapid confirmation blink starts. All controllers are disconnected and must be paired again. diff --git a/bluepad32_input_backend.cpp b/bluepad32_input_backend.cpp index 9115c79..1e84c6a 100644 --- a/bluepad32_input_backend.cpp +++ b/bluepad32_input_backend.cpp @@ -25,6 +25,8 @@ constexpr uint32_t kRumblePollIntervalMs = 5; constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; constexpr uint32_t kPairingWindowDurationMs = 60000; constexpr uint32_t kPairingResetFeedbackDurationMs = 2000; +// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds. +constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900; constexpr uint8_t kAllBlePairingMethods = SM_STK_GENERATION_METHOD_JUST_WORKS | SM_STK_GENERATION_METHOD_OOB | @@ -749,6 +751,7 @@ void platform_init(int argc, const char** argv) { } void platform_on_init_complete() { + gap_set_link_supervision_timeout(kClassicLinkSupervisionTimeout); gap_set_bondable_mode(false); sm_set_accepted_stk_generation_methods(0); gap_ssp_set_auto_accept(false); @@ -830,6 +833,10 @@ void platform_on_device_disconnected(uni_hid_device_t* device) { critical_section_exit(&g_state_lock); if (disconnected_tracked_device) { + // A controller can disconnect while the policy is already Open. + // Restart both scans so host-initiated reconnect controllers such as + // 8BitDo Ultimate become reachable without rebooting the Pico. + g_connection_policy_state = ConnectionPolicyState::Uninitialized; recompute_connection_status(); } } diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index 6b8c7fe..aba0438 100755 Binary files a/firmware/switch-pico-aio.elf and b/firmware/switch-pico-aio.elf differ diff --git a/firmware/switch-pico-aio.uf2 b/firmware/switch-pico-aio.uf2 index aa5273f..cb0802c 100644 Binary files a/firmware/switch-pico-aio.uf2 and b/firmware/switch-pico-aio.uf2 differ diff --git a/patches/bluepad32-sdl3-imu.patch b/patches/bluepad32-sdl3-imu.patch index fa7355b..90e27e8 100644 --- a/patches/bluepad32-sdl3-imu.patch +++ b/patches/bluepad32-sdl3-imu.patch @@ -1,287 +1,26 @@ -diff --git a/src/components/bluepad32/include/parser/uni_hid_parser_imu.h b/src/components/bluepad32/include/parser/uni_hid_parser_imu.h -new file mode 100644 -index 0000000..dbd3024 ---- /dev/null -+++ b/src/components/bluepad32/include/parser/uni_hid_parser_imu.h -@@ -0,0 +1,278 @@ -+// SPDX-License-Identifier: Apache-2.0 -+// Fixed-point IMU normalization helpers shared by controller parsers. -+ -+#ifndef UNI_HID_PARSER_IMU_H -+#define UNI_HID_PARSER_IMU_H -+ -+#include -+#include -+#include -+ -+#define UNI_IMU_ACCEL_RES_PER_G 8192 -+#define UNI_IMU_GYRO_RES_PER_DEG_S 1024 -+ -+typedef struct { -+ int32_t accel[3]; -+ int32_t gyro[3]; -+} uni_imu_fixed_sample_t; -+ -+static inline void uni_imu_normalize_wii_accel(int16_t x, -+ int16_t y, -+ int16_t z, -+ int32_t out[3]) { -+ // SDL's Wii convention is (-X, Z, Y), with about 100 raw counts per g. -+ out[0] = -(int32_t)x * UNI_IMU_ACCEL_RES_PER_G / 100; -+ out[1] = (int32_t)z * UNI_IMU_ACCEL_RES_PER_G / 100; -+ out[2] = (int32_t)y * UNI_IMU_ACCEL_RES_PER_G / 100; -+} -+ -+typedef enum { -+ UNI_PSMOVE_IMU_MODEL_ZCM1, -+ UNI_PSMOVE_IMU_MODEL_ZCM2, -+} uni_psmove_imu_model_t; -+ -+typedef enum { -+ UNI_PSMOVE_CALIBRATION_IGNORED, -+ UNI_PSMOVE_CALIBRATION_INCOMPLETE, -+ UNI_PSMOVE_CALIBRATION_COMPLETE, -+ UNI_PSMOVE_CALIBRATION_INVALID, -+} uni_psmove_calibration_result_t; -+ -+#define UNI_PSMOVE_CALIBRATION_REPORT_SIZE 49 -+#define UNI_PSMOVE_ZCM1_CALIBRATION_SIZE 143 -+#define UNI_PSMOVE_ZCM2_CALIBRATION_SIZE 96 -+ -+typedef struct { -+ uint8_t blob[UNI_PSMOVE_ZCM1_CALIBRATION_SIZE]; -+ uint8_t received_blocks; -+ bool valid; -+} uni_psmove_imu_calibration_t; -+ -+static inline int32_t uni_psmove_decode_calibration_value( -+ const uni_psmove_imu_calibration_t* calibration, -+ uni_psmove_imu_model_t model, -+ uint8_t offset) { -+ const uint16_t value = -+ (uint16_t)calibration->blob[offset] | -+ ((uint16_t)calibration->blob[offset + 1] << 8u); -+ return model == UNI_PSMOVE_IMU_MODEL_ZCM1 -+ ? (int32_t)value - 0x8000 -+ : (int32_t)(int16_t)value; -+} -+ -+static inline int32_t uni_psmove_decode_input_value( -+ uni_psmove_imu_model_t model, -+ uint16_t value) { -+ return model == UNI_PSMOVE_IMU_MODEL_ZCM1 -+ ? (int32_t)value - 0x8000 -+ : (int32_t)(int16_t)value; -+} -+ -+static inline void uni_psmove_get_accel_bounds( -+ const uni_psmove_imu_calibration_t* calibration, -+ uni_psmove_imu_model_t model, -+ uint8_t axis, -+ int32_t* low, -+ int32_t* high) { -+ static const uint8_t zcm1_low_offsets[3] = {0x0a, 0x24, 0x14}; -+ static const uint8_t zcm1_high_offsets[3] = {0x16, 0x1e, 0x08}; -+ static const uint8_t zcm2_low_offsets[3] = {0x08, 0x16, 0x24}; -+ static const uint8_t zcm2_high_offsets[3] = {0x02, 0x10, 0x1e}; -+ const uint8_t* low_offsets = -+ model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_low_offsets -+ : zcm2_low_offsets; -+ const uint8_t* high_offsets = -+ model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_high_offsets -+ : zcm2_high_offsets; -+ *low = uni_psmove_decode_calibration_value(calibration, model, -+ low_offsets[axis]); -+ *high = uni_psmove_decode_calibration_value(calibration, model, -+ high_offsets[axis]); -+} -+ -+static inline void uni_psmove_get_gyro_calibration( -+ const uni_psmove_imu_calibration_t* calibration, -+ uni_psmove_imu_model_t model, -+ uint8_t axis, -+ int32_t* offset, -+ int32_t* divisor, -+ int32_t* full_scale) { -+ static const uint8_t zcm1_bias_offsets[3] = {0x2a, 0x2c, 0x2e}; -+ static const uint8_t zcm1_high_offsets[3] = {0x46, 0x50, 0x5a}; -+ static const uint8_t zcm2_bias_offsets[3] = {0x26, 0x28, 0x2a}; -+ static const uint8_t zcm2_low_offsets[3] = {0x42, 0x4a, 0x52}; -+ static const uint8_t zcm2_high_offsets[3] = {0x30, 0x38, 0x40}; -+ -+ if (model == UNI_PSMOVE_IMU_MODEL_ZCM1) { -+ *offset = uni_psmove_decode_calibration_value( -+ calibration, model, zcm1_bias_offsets[axis]); -+ const int32_t high = uni_psmove_decode_calibration_value( -+ calibration, model, zcm1_high_offsets[axis]); -+ *divisor = high - *offset; -+ // ZCM1 gyro points are measured at +80 RPM = +480 degrees/s. -+ *full_scale = 480 * UNI_IMU_GYRO_RES_PER_DEG_S; -+ return; -+ } -+ -+ *offset = uni_psmove_decode_calibration_value( -+ calibration, model, zcm2_bias_offsets[axis]); -+ const int32_t low = uni_psmove_decode_calibration_value( -+ calibration, model, zcm2_low_offsets[axis]); -+ const int32_t high = uni_psmove_decode_calibration_value( -+ calibration, model, zcm2_high_offsets[axis]); -+ *divisor = high - low; -+ // ZCM2 points span -90 to +90 RPM = 1080 degrees/s total. -+ *full_scale = 1080 * UNI_IMU_GYRO_RES_PER_DEG_S; -+} -+ -+static inline bool uni_psmove_validate_calibration( -+ const uni_psmove_imu_calibration_t* calibration, -+ uni_psmove_imu_model_t model) { -+ for (uint8_t axis = 0; axis < 3; ++axis) { -+ int32_t low; -+ int32_t high; -+ uni_psmove_get_accel_bounds(calibration, model, axis, &low, &high); -+ if (high <= low) { -+ return false; -+ } -+ -+ int32_t offset; -+ int32_t divisor; -+ int32_t full_scale; -+ uni_psmove_get_gyro_calibration(calibration, model, axis, &offset, -+ &divisor, &full_scale); -+ (void)offset; -+ (void)full_scale; -+ if (divisor <= 0) { -+ return false; -+ } -+ } -+ return true; -+} -+ -+static inline uni_psmove_calibration_result_t -+uni_psmove_add_calibration_report( -+ uni_psmove_imu_calibration_t* calibration, -+ uni_psmove_imu_model_t model, -+ const uint8_t* report, -+ uint16_t len) { -+ if (calibration == NULL || report == NULL || -+ len < UNI_PSMOVE_CALIBRATION_REPORT_SIZE || report[0] != 0x10) { -+ return UNI_PSMOVE_CALIBRATION_IGNORED; -+ } -+ -+ size_t destination; -+ size_t source; -+ uint8_t block_mask; -+ if (report[1] == 0x00) { -+ destination = 0; -+ source = 0; -+ block_mask = 0x01; -+ } else if (model == UNI_PSMOVE_IMU_MODEL_ZCM1 && report[1] == 0x01) { -+ destination = UNI_PSMOVE_CALIBRATION_REPORT_SIZE; -+ source = 2; -+ block_mask = 0x02; -+ } else if (model == UNI_PSMOVE_IMU_MODEL_ZCM1 && report[1] == 0x82) { -+ destination = 2 * UNI_PSMOVE_CALIBRATION_REPORT_SIZE - 2; -+ source = 2; -+ block_mask = 0x04; -+ } else if (model == UNI_PSMOVE_IMU_MODEL_ZCM2 && report[1] == 0x81) { -+ destination = UNI_PSMOVE_CALIBRATION_REPORT_SIZE; -+ source = 2; -+ block_mask = 0x02; -+ } else { -+ calibration->valid = false; -+ return UNI_PSMOVE_CALIBRATION_INVALID; -+ } -+ -+ memcpy(calibration->blob + destination, report + source, -+ UNI_PSMOVE_CALIBRATION_REPORT_SIZE - source); -+ calibration->received_blocks |= block_mask; -+ -+ const uint8_t required_blocks = -+ model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? 0x07 : 0x03; -+ if ((calibration->received_blocks & required_blocks) != required_blocks) { -+ return UNI_PSMOVE_CALIBRATION_INCOMPLETE; -+ } -+ -+ calibration->valid = -+ uni_psmove_validate_calibration(calibration, model); -+ return calibration->valid ? UNI_PSMOVE_CALIBRATION_COMPLETE -+ : UNI_PSMOVE_CALIBRATION_INVALID; -+} -+ -+static inline int32_t uni_psmove_scale_accel(int32_t raw, -+ int32_t low, -+ int32_t high) { -+ const int64_t numerator = -+ (int64_t)(raw - low) * (2 * UNI_IMU_ACCEL_RES_PER_G); -+ return (int32_t)(numerator / (high - low)) - UNI_IMU_ACCEL_RES_PER_G; -+} -+ -+static inline int32_t uni_imu_clamp_i64(int64_t value) { -+ if (value > INT32_MAX) { -+ return INT32_MAX; -+ } -+ if (value < INT32_MIN) { -+ return INT32_MIN; -+ } -+ return (int32_t)value; -+} -+ -+static inline int32_t uni_psmove_scale_gyro(int32_t raw, -+ int32_t offset, -+ int32_t divisor, -+ int32_t full_scale) { -+ const int64_t scaled = -+ ((int64_t)(raw - offset) * full_scale) / divisor; -+ return uni_imu_clamp_i64(scaled); -+} -+ -+static inline bool uni_psmove_normalize_imu( -+ uni_psmove_imu_model_t model, -+ const uni_psmove_imu_calibration_t* calibration, -+ const uint16_t accel_first[3], -+ const uint16_t accel_second[3], -+ const uint16_t gyro_first[3], -+ const uint16_t gyro_second[3], -+ uni_imu_fixed_sample_t* out) { -+ if (out == NULL) { -+ return false; -+ } -+ memset(out, 0, sizeof(*out)); -+ if (calibration == NULL || !calibration->valid || accel_first == NULL || -+ accel_second == NULL || gyro_first == NULL || gyro_second == NULL) { -+ return false; -+ } -+ -+ for (uint8_t axis = 0; axis < 3; ++axis) { -+ int32_t accel = uni_psmove_decode_input_value(model, -+ accel_first[axis]); -+ int32_t gyro = uni_psmove_decode_input_value(model, -+ gyro_first[axis]); -+ if (model == UNI_PSMOVE_IMU_MODEL_ZCM1) { -+ accel = (accel + uni_psmove_decode_input_value( -+ model, accel_second[axis])) / -+ 2; -+ gyro = (gyro + uni_psmove_decode_input_value( -+ model, gyro_second[axis])) / -+ 2; -+ } -+ -+ int32_t low; -+ int32_t high; -+ uni_psmove_get_accel_bounds(calibration, model, axis, &low, &high); -+ out->accel[axis] = uni_psmove_scale_accel(accel, low, high); -+ -+ int32_t offset; -+ int32_t divisor; -+ int32_t full_scale; -+ uni_psmove_get_gyro_calibration(calibration, model, axis, &offset, -+ &divisor, &full_scale); -+ out->gyro[axis] = uni_psmove_scale_gyro( -+ gyro, offset, divisor, full_scale); -+ } -+ return true; -+} -+ -+#endif // UNI_HID_PARSER_IMU_H +diff --git a/src/components/bluepad32/bt/uni_bt_bredr.c b/src/components/bluepad32/bt/uni_bt_bredr.c +index 955cc6f..4013cc1 100644 +--- a/src/components/bluepad32/bt/uni_bt_bredr.c ++++ b/src/components/bluepad32/bt/uni_bt_bredr.c +@@ -423,13 +423,14 @@ void uni_bt_bredr_on_l2cap_channel_opened(uint16_t channel, const uint8_t* packe + status = l2cap_event_channel_opened_get_status(packet); + if (status) { + logi("L2CAP Connection failed: 0x%02x.\n", status); +- // Practice showed that if the connection fails, just disconnect/remove +- // so that the connection can start again. ++ // Channel-open failures also include transient page timeouts when a ++ // paired controller powers down or is temporarily unreachable. Keep ++ // the persistent key so the controller can reconnect later. Users can ++ // remove genuinely stale keys through the explicit pairing reset. + if (status == L2CAP_CONNECTION_RESPONSE_RESULT_REFUSED_SECURITY) { + logi("Probably GAP-security-related issues. Set GAP security to 2\n"); + } +- logi("Removing key for device: %s.\n", bd_addr_to_str(address)); +- gap_drop_link_key_for_bd_addr(device->conn.btaddr); ++ logi("Removing failed device instance for: %s; preserving link key.\n", bd_addr_to_str(address)); + uni_hid_device_disconnect(device); + uni_hid_device_delete(device); + /* 'device' is destroyed, don't use */ diff --git a/src/components/bluepad32/include/parser/uni_hid_parser_psmove.h b/src/components/bluepad32/include/parser/uni_hid_parser_psmove.h index 6af4969..0aebb0a 100644 --- a/src/components/bluepad32/include/parser/uni_hid_parser_psmove.h diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp index 865732d..957bd37 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -23,6 +23,7 @@ uint32_t now_ms = 0; bool bondable = true; bool ssp_auto_accept = true; uint8_t accepted_stk_methods = 0xff; +uint16_t link_supervision_timeout = 0; btstack_packet_handler_t pairing_event_handler = nullptr; int confirmation_accepts = 0; int confirmation_rejections = 0; @@ -188,6 +189,10 @@ void gap_set_bondable_mode(int enabled) { bondable = enabled != 0; } +void gap_set_link_supervision_timeout(uint16_t timeout) { + link_supervision_timeout = timeout; +} + void gap_ssp_set_auto_accept(int auto_accept) { ssp_auto_accept = auto_accept != 0; } @@ -303,9 +308,11 @@ void start_backend() { platform_on_init_complete(); require(incoming_connections && scanning_enabled && classic_scanning_enabled && scan_starts == 1 && + link_supervision_timeout == + kClassicLinkSupervisionTimeout && !bondable && accepted_stk_methods == 0 && !ssp_auto_accept && pairing_event_handler != nullptr, - "initialization must scan while keeping new pairing disabled"); + "initialization must configure liveness and pairing policy"); } void start_pairing_backend() { start_backend(); @@ -455,6 +462,11 @@ void test_independent_lifecycle() { "connected device must remain identifiable while becoming ready"); const int starts_before_aborted_disconnect = scan_starts; + const int classic_starts_before_aborted_disconnect = + classic_scan_starts; + const int stops_before_aborted_disconnect = scan_stops; + const int classic_stops_before_aborted_disconnect = + classic_scan_stops; platform_on_device_disconnected(&aborted); require(g_slots[0].device == nullptr && !g_slots[0].active, "pre-ready disconnect must clear its pending slot identity"); @@ -463,8 +475,13 @@ void test_independent_lifecycle() { require(g_connection_status == ConnectionStatus::Scanning && scanning_enabled && classic_scanning_enabled && incoming_connections && - scan_starts == starts_before_aborted_disconnect, - "pre-ready disconnect must preserve the open pairing scan"); + scan_starts == starts_before_aborted_disconnect + 1 && + classic_scan_starts == + classic_starts_before_aborted_disconnect + 1 && + scan_stops == stops_before_aborted_disconnect + 1 && + classic_scan_stops == + classic_stops_before_aborted_disconnect + 1, + "pre-ready disconnect must restart Classic and BLE scans"); uni_hid_device_t devices[kSlotCount] = { device(0), device(1), device(2), device(3)}; diff --git a/tests/bluepad32_native_stubs/uni.h b/tests/bluepad32_native_stubs/uni.h index 38805f0..7ee86a8 100644 --- a/tests/bluepad32_native_stubs/uni.h +++ b/tests/bluepad32_native_stubs/uni.h @@ -171,6 +171,7 @@ int le_device_db_max_count(); void le_device_db_info( int index, int* address_type, bd_addr_t address, sm_key_t irk); void gap_set_bondable_mode(int enabled); +void gap_set_link_supervision_timeout(uint16_t link_supervision_timeout); void gap_ssp_set_auto_accept(int auto_accept); void sm_set_accepted_stk_generation_methods( uint8_t accepted_stk_generation_methods);