diff --git a/controller_hotkey_config.h b/controller_hotkey_config.h index 883d657..ebd2cc0 100644 --- a/controller_hotkey_config.h +++ b/controller_hotkey_config.h @@ -19,8 +19,8 @@ // Local confirmation pulse sent only to the controller that toggled. #define SWITCH_ABXY_FEEDBACK_DURATION_MS 120 -#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0x80 -#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0x80 +#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0xFF +#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0xFF // A longer pulse confirms disabled; a shorter pulse confirms enabled. #define SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS 180 diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index aba0438..f0fa733 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 cb0802c..6f93f85 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 90e27e8..5ba7ef7 100644 --- a/patches/bluepad32-sdl3-imu.patch +++ b/patches/bluepad32-sdl3-imu.patch @@ -259,10 +259,10 @@ index 0265f93..5c0f2bb 100644 return (psmove_instance_t*)&d->parser_data[0]; } diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c -index 599fc35..c72f056 100644 +index 599fc35..9f073b4 100644 --- a/src/components/bluepad32/parser/uni_hid_parser_switch.c +++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c -@@ -51,7 +51,8 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0; +@@ -51,13 +51,15 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0; static const int16_t DEFAULT_ACCEL_SCALE = 16384; static const int16_t DEFAULT_GYRO_OFFSET = 0; static const int16_t DEFAULT_GYRO_SCALE = 13371; @@ -272,7 +272,97 @@ index 599fc35..c72f056 100644 #define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24 static const uint16_t SWITCH_FACTORY_IMU_CAL_DATA_ADDR = 0x6020; -@@ -823,19 +824,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { + + #define SWITCH_DUMP_ROM_DATA_SIZE 24 // Max size is 24 + #define SWITCH_SETUP_TIMEOUT_MS 800 ++#define SWITCH_RUMBLE_REFRESH_MS 40 + #if ENABLE_SPI_FLASH_DUMP + static const uint32_t SWITCH_DUMP_ROM_DATA_ADDR_START = 0x20000; + static const uint32_t SWITCH_DUMP_ROM_DATA_ADDR_END = 0x30000; +@@ -72,6 +74,7 @@ enum switch_state { + STATE_READ_FACTORY_IMU_CALIBRATION, // Factory IMU calibration info + STATE_SET_FULL_REPORT, // Request report 0x30 + STATE_ENABLE_IMU, // Enable/Disable gyro/accel ++ STATE_ENABLE_RUMBLE, // Enable controller vibration + STATE_DUMP_FLASH, // Dump SPI Flash memory + STATE_UPDATE_LED, // Update LEDs + STATE_READY, // Gamepad setup ready! +@@ -111,6 +114,7 @@ enum switch_subcmd { + SUBCMD_SPI_FLASH_READ = 0x10, + SUBCMD_SET_PLAYER_LEDS = 0x30, + SUBCMD_ENABLE_IMU = 0x40, ++ SUBCMD_ENABLE_RUMBLE = 0x48, + }; + + typedef enum { +@@ -137,6 +141,7 @@ typedef struct switch_instance_s { + // Although technically, we can use one timer for delay and duration, easier to debug/maintain if we have two. + btstack_timer_source_t rumble_timer_duration; + btstack_timer_source_t rumble_timer_delayed_start; ++ btstack_timer_source_t rumble_timer_refresh; + switch_state_rumble_t rumble_state; + + btstack_timer_source_t setup_timer; +@@ -322,6 +327,7 @@ static void fsm_read_user_stick_calibration(struct uni_hid_device_s* d); + static void fsm_read_factory_imu_calibration(struct uni_hid_device_s* d); + static void fsm_set_full_report(struct uni_hid_device_s* d); + static void fsm_enable_imu(struct uni_hid_device_s* d); ++static void fsm_enable_rumble(struct uni_hid_device_s* d); + static void fsm_update_led(struct uni_hid_device_s* d); + static void fsm_ready(struct uni_hid_device_s* d); + static void process_reply_read_spi_dump(struct uni_hid_device_s* d, const uint8_t* data, int len); +@@ -333,11 +339,16 @@ static void process_reply_set_report_mode(struct uni_hid_device_s* d, const stru + static void process_reply_spi_flash_read(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len); + static void process_reply_set_player_leds(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len); + static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len); ++static void process_reply_enable_rumble(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len); + static int32_t calibrate_axis(int32_t v, switch_cal_stick_t cal); + static void set_led(uni_hid_device_t* d, uint8_t leds); + static void on_switch_set_rumble_on(btstack_timer_source_t* ts); + static void on_switch_set_rumble_off(btstack_timer_source_t* ts); ++static void on_switch_refresh_rumble(btstack_timer_source_t* ts); + static void switch_stop_rumble_now(uni_hid_device_t* d); ++static void switch_send_dual_rumble_now(uni_hid_device_t* d, ++ uint8_t weak_magnitude, ++ uint8_t strong_magnitude); + static void switch_play_dual_rumble_now(uni_hid_device_t* d, + uint16_t duration_ms, + uint8_t weak_magnitude, +@@ -451,6 +462,10 @@ static void process_fsm(struct uni_hid_device_s* d) { + break; + case STATE_ENABLE_IMU: + logd("STATE_ENABLE_IMU\n"); ++ fsm_enable_rumble(d); ++ break; ++ case STATE_ENABLE_RUMBLE: ++ logd("STATE_ENABLE_RUMBLE\n"); + fsm_dump_rom(d); + break; + case STATE_DUMP_FLASH: +@@ -725,6 +740,12 @@ static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct sw + ARG_UNUSED(r); + ARG_UNUSED(len); + } ++static void process_reply_enable_rumble(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len) { ++ ARG_UNUSED(d); ++ ARG_UNUSED(r); ++ ARG_UNUSED(len); ++} ++ + + // Process 0x21 input report: SWITCH_INPUT_SUBCMD_REPLY + static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t* report, int len) { +@@ -752,6 +773,9 @@ static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t + case SUBCMD_ENABLE_IMU: + process_reply_enable_imu(d, r, len); + break; ++ case SUBCMD_ENABLE_RUMBLE: ++ process_reply_enable_rumble(d, r, len); ++ break; + default: + loge("Switch: Error, unexpected subcmd_id=0x%02x in report 0x21\n", r->subcmd_id); + break; +@@ -823,19 +847,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { switch_instance_t* ins = get_switch_instance(d); uni_controller_t* ctl = &d->controller; @@ -308,7 +398,7 @@ index 599fc35..c72f056 100644 if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) { accel[1] = -accel[1]; accel[2] = -accel[2]; -@@ -843,10 +851,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { +@@ -843,10 +874,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { gyro[2] = -gyro[2]; } @@ -326,6 +416,121 @@ index 599fc35..c72f056 100644 } // Process 0x30 input report: SWITCH_INPUT_IMU_DATA +@@ -1172,6 +1206,18 @@ static void fsm_enable_imu(struct uni_hid_device_s* d) { + req->data[0] = (ins->mode == SWITCH_MODE_IMU); + send_subcmd(d, req, sizeof(out)); + } ++static void fsm_enable_rumble(struct uni_hid_device_s* d) { ++ switch_instance_t* ins = get_switch_instance(d); ++ ins->state = STATE_ENABLE_RUMBLE; ++ ++ uint8_t out[sizeof(struct switch_subcmd_request) + 1] = {0}; ++ struct switch_subcmd_request* req = (struct switch_subcmd_request*)&out[0]; ++ req->report_id = OUTPUT_RUMBLE_AND_SUBCMD; ++ req->subcmd_id = SUBCMD_ENABLE_RUMBLE; ++ req->data[0] = 0x01; ++ send_subcmd(d, req, sizeof(out)); ++} ++ + + static void fsm_update_led(struct uni_hid_device_s* d) { + switch_instance_t* ins = get_switch_instance(d); +@@ -1203,6 +1249,10 @@ static struct switch_rumble_freq_data find_rumble_freq(uint16_t freq) { + return rumble_freqs[i]; + } + ++static uint16_t switch_magnitude_to_amp(uint8_t magnitude) { ++ return (uint16_t)(((uint32_t)magnitude * 1003 + 127) / 255); ++} ++ + static struct switch_rumble_amp_data find_rumble_amp(uint16_t amp) { + unsigned int i = 0; + if (amp > rumble_amps[0].amp) { +@@ -1259,6 +1309,7 @@ void uni_hid_parser_switch_play_dual_rumble(struct uni_hid_device_s* d, + break; + case SWITCH_STATE_RUMBLE_IN_PROGRESS: + btstack_run_loop_remove_timer(&ins->rumble_timer_duration); ++ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh); + break; + default: + // Do nothing +@@ -1366,6 +1417,7 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { + + // No need to protect it with a mutex since it runs in the same main thread + assert(ins->rumble_state == SWITCH_STATE_RUMBLE_IN_PROGRESS); ++ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh); + ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED; + + struct switch_subcmd_request req = {0}; +@@ -1379,6 +1431,22 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { + send_subcmd(d, (struct switch_subcmd_request*)&req, sizeof(req) - 1); + } + ++static void switch_send_dual_rumble_now(uni_hid_device_t* d, ++ uint8_t weak_magnitude, ++ uint8_t strong_magnitude) { ++ struct switch_subcmd_request req = { ++ .report_id = OUTPUT_RUMBLE_ONLY, ++ }; ++ // Fixed frequencies match the standard Switch LRA envelope and the ++ // 8BitDo Switch-mode implementation. Magnitudes control amplitude only. ++ switch_encode_rumble(req.rumble_left, 453, 135, ++ switch_magnitude_to_amp(weak_magnitude)); ++ switch_encode_rumble(req.rumble_right, 453, 99, ++ switch_magnitude_to_amp(strong_magnitude)); ++ // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id ++ send_subcmd(d, &req, sizeof(req) - 1); ++} ++ + static void switch_play_dual_rumble_now(uni_hid_device_t* d, + uint16_t duration_ms, + uint8_t weak_magnitude, +@@ -1391,14 +1459,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d, + return; + } + +- struct switch_subcmd_request req = { +- .report_id = OUTPUT_RUMBLE_ONLY, +- }; +- switch_encode_rumble(req.rumble_left, weak_magnitude << 2, weak_magnitude, 500); +- switch_encode_rumble(req.rumble_right, strong_magnitude << 2, strong_magnitude, 500); ++ ins->rumble_weak_magnitude = weak_magnitude; ++ ins->rumble_strong_magnitude = strong_magnitude; ++ switch_send_dual_rumble_now(d, weak_magnitude, strong_magnitude); + +- // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id +- send_subcmd(d, &req, sizeof(req) - 1); ++ // Refresh active rumble for Switch-compatible controllers that do not ++ // retain a single output packet, including 8BitDo Switch mode. ++ ins->rumble_timer_refresh.process = &on_switch_refresh_rumble; ++ ins->rumble_timer_refresh.context = d; ++ btstack_run_loop_set_timer(&ins->rumble_timer_refresh, ++ SWITCH_RUMBLE_REFRESH_MS); ++ btstack_run_loop_add_timer(&ins->rumble_timer_refresh); + + // Set timer to turn off rumble + ins->rumble_timer_duration.process = &on_switch_set_rumble_off; +@@ -1414,6 +1485,20 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) { + + switch_play_dual_rumble_now(d, ins->rumble_duration_ms, ins->rumble_weak_magnitude, ins->rumble_strong_magnitude); + } ++static void on_switch_refresh_rumble(btstack_timer_source_t* ts) { ++ uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts); ++ switch_instance_t* ins = get_switch_instance(d); ++ if (ins->rumble_state != SWITCH_STATE_RUMBLE_IN_PROGRESS) { ++ return; ++ } ++ switch_send_dual_rumble_now( ++ d, (uint8_t)ins->rumble_weak_magnitude, ++ (uint8_t)ins->rumble_strong_magnitude); ++ btstack_run_loop_set_timer(&ins->rumble_timer_refresh, ++ SWITCH_RUMBLE_REFRESH_MS); ++ btstack_run_loop_add_timer(&ins->rumble_timer_refresh); ++} ++ + + static void on_switch_set_rumble_off(btstack_timer_source_t* ts) { + uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts); diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/components/bluepad32/parser/uni_hid_parser_wii.c index be2103e..4819639 100644 --- a/src/components/bluepad32/parser/uni_hid_parser_wii.c diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp index 957bd37..cca33a1 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -889,8 +889,10 @@ void test_abxy_hotkey() { require(slot_zero.rumble_calls == 1 && slot_zero.last_high == kAbxyFeedbackWeakMagnitude && slot_zero.last_low == kAbxyFeedbackStrongMagnitude && + slot_zero.last_high == UINT8_MAX && + slot_zero.last_low == UINT8_MAX && g_slots[0].rumble_pending, - "ABXY confirmation did not take priority over host rumble"); + "ABXY confirmation was not full-strength or did not take priority"); platform_on_controller_data(&slot_zero, &input); process_rumble_timer(&g_rumble_timer);