feat: add opt-in native Nintendo rumble backend

This commit is contained in:
Joey Yakimowich-Payne 2026-09-05 23:58:50 -06:00
commit 3fc28b1fa4
44 changed files with 4834 additions and 240 deletions

View file

@ -265,10 +265,9 @@ 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..0105c53 100644
--- a/src/components/bluepad32/parser/uni_hid_parser_switch.c
+++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c
@@ -51,13 +51,22 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0;
@@ -51,13 +51,22 @@
static const int16_t DEFAULT_ACCEL_SCALE = 16384;
static const int16_t DEFAULT_GYRO_OFFSET = 0;
static const int16_t DEFAULT_GYRO_SCALE = 13371;
@ -292,7 +291,7 @@ index 599fc35..0105c53 100644
#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 +81,7 @@ enum switch_state {
@@ -72,6 +81,7 @@
STATE_READ_FACTORY_IMU_CALIBRATION, // Factory IMU calibration info
STATE_SET_FULL_REPORT, // Request report 0x30
STATE_ENABLE_IMU, // Enable/Disable gyro/accel
@ -300,7 +299,7 @@ index 599fc35..0105c53 100644
STATE_DUMP_FLASH, // Dump SPI Flash memory
STATE_UPDATE_LED, // Update LEDs
STATE_READY, // Gamepad setup ready!
@@ -111,6 +121,7 @@ enum switch_subcmd {
@@ -111,6 +121,7 @@
SUBCMD_SPI_FLASH_READ = 0x10,
SUBCMD_SET_PLAYER_LEDS = 0x30,
SUBCMD_ENABLE_IMU = 0x40,
@ -308,15 +307,20 @@ index 599fc35..0105c53 100644
};
typedef enum {
@@ -137,6 +148,7 @@ typedef struct switch_instance_s {
@@ -137,7 +148,12 @@
// 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;
+ bool native_owned;
+ bool device_info_valid;
+ uint8_t packet_num;
+ uint8_t effective_rumble[8];
btstack_timer_source_t setup_timer;
@@ -322,6 +334,7 @@ static void fsm_read_user_stick_calibration(struct uni_hid_device_s* d);
@@ -322,6 +338,7 @@
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);
@ -324,7 +328,7 @@ index 599fc35..0105c53 100644
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 +346,16 @@ static void process_reply_set_report_mode(struct uni_hid_device_s* d, const stru
@@ -333,23 +350,33 @@
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);
@ -341,7 +345,24 @@ index 599fc35..0105c53 100644
static void switch_play_dual_rumble_now(uni_hid_device_t* d,
uint16_t duration_ms,
uint8_t weak_magnitude,
@@ -451,6 +469,10 @@ static void process_fsm(struct uni_hid_device_s* d) {
uint8_t strong_magnitude);
static void switch_setup_timeout_callback(btstack_timer_source_t* ts);
static void parse_stick_calibration(switch_cal_stick_t* x, switch_cal_stick_t* y, const uint8_t* data, bool is_left);
+static const uint8_t switch_neutral_rumble[8] = {
+ 0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40,
+};
+static void switch_cancel_rumble_timers(switch_instance_t* ins);
void uni_hid_parser_switch_setup(struct uni_hid_device_s* d) {
switch_instance_t* ins = get_switch_instance(d);
memset(ins, 0, sizeof(*ins));
+ memcpy(ins->effective_rumble, switch_neutral_rumble, sizeof(ins->effective_rumble));
ins->state = STATE_SETUP;
ins->mode = SWITCH_MODE_NONE;
// In case the controller doesn't answer to SUBCMD_REQ_DEV_INFO command, set a default one.
@@ -451,6 +478,10 @@
break;
case STATE_ENABLE_IMU:
logd("STATE_ENABLE_IMU\n");
@ -352,7 +373,25 @@ index 599fc35..0105c53 100644
fsm_dump_rom(d);
break;
case STATE_DUMP_FLASH:
@@ -725,6 +747,12 @@ static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct sw
@@ -648,7 +679,8 @@
// Reply to SUBCMD_REQ_DEV_INFO
static void process_reply_req_dev_info(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len) {
- ARG_UNUSED(len);
+ if (len < (int)sizeof(*r) + 3 || !(r->ack & 0x80))
+ return;
switch_instance_t* ins = get_switch_instance(d);
if (ins->state > STATE_SETUP && ins->mode == SWITCH_MODE_NONE) {
bool enable_imu;
@@ -669,6 +701,7 @@
ins->firmware_version_hi = r->data[0];
ins->firmware_version_lo = r->data[1];
ins->controller_type = r->data[2];
+ ins->device_info_valid = true;
logi("Switch: Firmware version: %d.%d. Controller type=%d\n", r->data[0], r->data[1], r->data[2]);
}
@@ -725,6 +758,12 @@
ARG_UNUSED(r);
ARG_UNUSED(len);
}
@ -365,17 +404,26 @@ index 599fc35..0105c53 100644
// 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 +780,9 @@ static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t
@@ -733,6 +772,8 @@
// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
// 00
const struct switch_report_21_s* r = (const struct switch_report_21_s*)report;
+ if (len < (int)sizeof(*r))
+ return;
if ((r->ack & 0b10000000) == 0) {
loge("Switch: Error, subcommand id=0x%02x was not successful.\n", r->subcmd_id);
}
@@ -751,6 +792,9 @@
break;
case SUBCMD_ENABLE_IMU:
process_reply_enable_imu(d, r, len);
break;
+ break;
+ case SUBCMD_ENABLE_RUMBLE:
+ process_reply_enable_rumble(d, r, len);
+ break;
break;
default:
loge("Switch: Error, unexpected subcmd_id=0x%02x in report 0x21\n", r->subcmd_id);
break;
@@ -823,19 +854,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
@@ -823,19 +867,26 @@
switch_instance_t* ins = get_switch_instance(d);
uni_controller_t* ctl = &d->controller;
@ -391,6 +439,9 @@ index 599fc35..0105c53 100644
- accel[i] = (r->accel[i] * ins->cal_accel.scale[i]) / ins->imu_cal_accel_divisor[i];
- gyro[i] = mult_frac((SWITCH_IMU_PREC_RANGE_SCALE * (r->gyro[i] - ins->cal_gyro.offset[i])),
- ins->cal_gyro.scale[i], ins->imu_cal_gyro_divisor[i]);
- }
-
- // Right joycon has Y and Z axes negated.
+ if (ins->imu_cal_accel_divisor[i] == 0) {
+ accel[i] = r->accel[i] * 2;
+ } else {
@ -404,14 +455,13 @@ index 599fc35..0105c53 100644
+ 936 * SWITCH_IMU_GYRO_RES_PER_DEG_S,
+ ins->imu_cal_gyro_divisor[i]);
+ }
}
- // Right joycon has Y and Z axes negated.
+ }
+
+ // Right Joy-Con has native Y and Z axes negated.
if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) {
accel[1] = -accel[1];
accel[2] = -accel[2];
@@ -843,10 +881,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
@@ -843,10 +894,13 @@
gyro[2] = -gyro[2];
}
@ -429,7 +479,7 @@ index 599fc35..0105c53 100644
}
// Process 0x30 input report: SWITCH_INPUT_IMU_DATA
@@ -1172,6 +1213,18 @@ static void fsm_enable_imu(struct uni_hid_device_s* d) {
@@ -1172,6 +1226,18 @@
req->data[0] = (ins->mode == SWITCH_MODE_IMU);
send_subcmd(d, req, sizeof(out));
}
@ -448,7 +498,7 @@ index 599fc35..0105c53 100644
static void fsm_update_led(struct uni_hid_device_s* d) {
switch_instance_t* ins = get_switch_instance(d);
@@ -1203,6 +1256,13 @@ static struct switch_rumble_freq_data find_rumble_freq(uint16_t freq) {
@@ -1203,6 +1269,13 @@
return rumble_freqs[i];
}
@ -462,7 +512,15 @@ index 599fc35..0105c53 100644
static struct switch_rumble_amp_data find_rumble_amp(uint16_t amp) {
unsigned int i = 0;
if (amp > rumble_amps[0].amp) {
@@ -1259,6 +1319,7 @@ void uni_hid_parser_switch_play_dual_rumble(struct uni_hid_device_s* d,
@@ -1253,12 +1326,15 @@
}
switch_instance_t* ins = get_switch_instance(d);
+ if (ins->native_owned || ins->state == STATE_UNINIT)
+ return;
switch (ins->rumble_state) {
case SWITCH_STATE_RUMBLE_DELAYED:
btstack_run_loop_remove_timer(&ins->rumble_timer_delayed_start);
break;
case SWITCH_STATE_RUMBLE_IN_PROGRESS:
btstack_run_loop_remove_timer(&ins->rumble_timer_duration);
@ -470,15 +528,181 @@ index 599fc35..0105c53 100644
break;
default:
// Do nothing
@@ -1366,6 +1427,7 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) {
@@ -1338,12 +1414,157 @@
}
// 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);
static void send_subcmd(uni_hid_device_t* d, struct switch_subcmd_request* r, int len) {
- static uint8_t packet_num = 0;
- r->packet_num = packet_num++;
- if (packet_num > 0x0f)
- packet_num = 0;
+ // Counter and effective rumble are refreshed by the device-send hook at
+ // actual submission, not here: this report may spend time in the queue.
r->transaction_type = (HID_MESSAGE_TYPE_DATA << 4) | HID_REPORT_TYPE_OUTPUT;
uni_hid_device_send_intr_report(d, (const uint8_t*)r, len);
+}
+
+static void switch_cancel_rumble_timers(switch_instance_t* ins) {
+ btstack_run_loop_remove_timer(&ins->rumble_timer_duration);
+ btstack_run_loop_remove_timer(&ins->rumble_timer_delayed_start);
+ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh);
+ ins->rumble_timer_duration.context = NULL;
+ ins->rumble_timer_delayed_start.context = NULL;
+ ins->rumble_timer_refresh.context = NULL;
+ ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED;
+}
+
+// Preserve queued setup/LED subcommands, but retire every old rumble writer
+// across ownership changes. Compact in place with a fixed queue-size bound.
+static void switch_discard_queued_rumble(uni_hid_device_t* d) {
+ uni_circular_buffer_t* queue = &d->outgoing_buffer;
+ int read_idx = queue->head_idx;
+ int write_idx = read_idx;
+ for (int n = 0; n < UNI_CIRCULAR_BUFFER_SIZE && read_idx != queue->tail_idx; ++n) {
+ const uni_circular_buffer_data_t* entry = &queue->buffer[read_idx];
+ const bool rumble = entry->cid == d->conn.interrupt_cid &&
+ entry->data_len >= 2 && entry->data[0] == 0xa2 &&
+ entry->data[1] == OUTPUT_RUMBLE_ONLY;
+ if (!rumble) {
+ if (write_idx != read_idx)
+ queue->buffer[write_idx] = *entry;
+ write_idx = (write_idx + 1) % UNI_CIRCULAR_BUFFER_SIZE;
+ }
+ read_idx = (read_idx + 1) % UNI_CIRCULAR_BUFFER_SIZE;
+ }
+ queue->tail_idx = write_idx;
+}
+
+// Both writers use this at the actual L2CAP boundary. Failed sends neither
+// consume a counter nor change the effective actuator-word cache.
+static int switch_submit_report(uni_hid_device_t* d, uint16_t cid, uint8_t* report, uint16_t len) {
+ switch_instance_t* ins = get_switch_instance(d);
+ report[2] = ins->packet_num;
+ const int status = l2cap_send(cid, report, len);
+ if (status == ERROR_CODE_SUCCESS) {
+ ins->packet_num = (ins->packet_num + 1) & 0x0f;
+ memcpy(ins->effective_rumble, &report[3], sizeof(ins->effective_rumble));
+ }
+ return status;
+}
+
+int uni_hid_parser_switch_send_report(uni_hid_device_t* d, uint16_t cid,
+ const uint8_t* report, uint16_t len) {
+ switch_instance_t* ins = get_switch_instance(d);
+ if (ins->state == STATE_UNINIT)
+ return ERROR_CODE_SUCCESS; // A retired device must not enqueue work.
+ if (cid != d->conn.interrupt_cid || len < 11 || report[0] != 0xa2 ||
+ (report[1] != OUTPUT_RUMBLE_ONLY && report[1] != OUTPUT_RUMBLE_AND_SUBCMD))
+ return l2cap_send(cid, (uint8_t*)report, len);
+ if (ins->native_owned && report[1] == OUTPUT_RUMBLE_ONLY)
+ return ERROR_CODE_SUCCESS; // Only native_send may write this owner.
+ if (len > UNI_CIRCULAR_BUFFER_DATA_SIZE)
+ return ERROR_CODE_COMMAND_DISALLOWED;
+
+ uint8_t outgoing[UNI_CIRCULAR_BUFFER_DATA_SIZE];
+ memcpy(outgoing, report, len);
+ if (report[1] == OUTPUT_RUMBLE_AND_SUBCMD) {
+ // Repeating the SAME last accepted actuator word is Nintendo's hold
+ // behavior, including compressed words. Never use the queued copy:
+ // replaying an earlier word after intervening audio reapplies deltas.
+ // Do not synthesize an absolute endpoint here either: it changes the
+ // repeated-word history. Physical acceptance is model-qualified.
+ memcpy(&outgoing[3], ins->effective_rumble, sizeof(ins->effective_rumble));
+ if (len >= 13 && outgoing[11] == SUBCMD_SET_PLAYER_LEDS)
+ outgoing[12] = ins->gamepad_seat & 0x0f;
+ }
+ return switch_submit_report(d, cid, outgoing, len);
+}
+
+bool uni_hid_parser_switch_native_info(uni_hid_device_t* d,
+ uint8_t* type, uint8_t* firmware_hi,
+ uint8_t* firmware_lo) {
+ if (!d || d->report_parser.setup != uni_hid_parser_switch_setup)
+ return false;
+ switch_instance_t* ins = get_switch_instance(d);
+ if (ins->state != STATE_READY || !ins->device_info_valid)
+ return false;
+ if (type)
+ *type = ins->controller_type;
+ if (firmware_hi)
+ *firmware_hi = ins->firmware_version_hi;
+ if (firmware_lo)
+ *firmware_lo = ins->firmware_version_lo;
+ return true;
+}
+
+bool uni_hid_parser_switch_native_acquire(uni_hid_device_t* d) {
+ uint8_t type;
+ if (!uni_hid_parser_switch_native_info(d, &type, NULL, NULL) ||
+ type < SWITCH_CONTROLLER_TYPE_JCL || type > SWITCH_CONTROLLER_TYPE_PRO ||
+ !d->conn.connected || !d->conn.interrupt_cid)
+ return false;
+ switch_instance_t* ins = get_switch_instance(d);
+ switch_cancel_rumble_timers(ins);
+ switch_discard_queued_rumble(d);
+ ins->native_owned = true;
+ return true;
+}
+
+bool uni_hid_parser_switch_native_send(uni_hid_device_t* d, const uint8_t rumble[8]) {
+ if (!d || !rumble || d->report_parser.setup != uni_hid_parser_switch_setup)
+ return false;
+ switch_instance_t* ins = get_switch_instance(d);
+ if (!ins->native_owned || ins->state != STATE_READY || !d->conn.connected ||
+ !d->conn.interrupt_cid || !l2cap_can_send_packet_now(d->conn.interrupt_cid))
+ return false;
+ uint8_t report[11] = {0xa2, OUTPUT_RUMBLE_ONLY, 0};
+ memcpy(&report[3], rumble, 8);
+ return switch_submit_report(d, d->conn.interrupt_cid, report, sizeof(report)) == ERROR_CODE_SUCCESS;
+}
+
+void uni_hid_parser_switch_native_release(uni_hid_device_t* d) {
+ if (!d || d->report_parser.setup != uni_hid_parser_switch_setup)
+ return;
+ switch_instance_t* ins = get_switch_instance(d);
+ if (!ins->native_owned)
+ return;
+ switch_cancel_rumble_timers(ins);
+ switch_discard_queued_rumble(d);
+ ins->native_owned = false;
+ if (ins->state == STATE_READY && d->conn.connected && d->conn.interrupt_cid &&
+ memcmp(ins->effective_rumble, switch_neutral_rumble, sizeof(ins->effective_rumble)) != 0) {
+ // A neutral stop may queue under congestion, never an audio command.
+ // The owner normally stops first; never queue a redundant delayed stop.
+ struct switch_subcmd_request req = {.report_id = OUTPUT_RUMBLE_ONLY};
+ memcpy(req.rumble_left, switch_neutral_rumble, 4);
+ memcpy(req.rumble_right, &switch_neutral_rumble[4], 4);
+ send_subcmd(d, &req, 11);
+ }
+}
+
+void uni_hid_parser_switch_teardown(uni_hid_device_t* d) {
+ if (!d || d->report_parser.setup != uni_hid_parser_switch_setup)
+ return;
+ switch_instance_t* ins = get_switch_instance(d);
+ switch_cancel_rumble_timers(ins);
+ btstack_run_loop_remove_timer(&ins->setup_timer);
+ ins->setup_timer.context = NULL;
+ ins->native_owned = false;
+ ins->device_info_valid = false;
+ ins->state = STATE_UNINIT;
+ uni_circular_buffer_reset(&d->outgoing_buffer);
}
static int32_t calibrate_axis(int32_t v, switch_cal_stick_t cal) {
@@ -1364,8 +1585,9 @@
static void switch_stop_rumble_now(uni_hid_device_t* d) {
switch_instance_t* ins = get_switch_instance(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);
+ if (ins->native_owned || ins->state == STATE_UNINIT)
+ return;
+ switch_cancel_rumble_timers(ins);
ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED;
struct switch_subcmd_request req = {0};
@@ -1379,6 +1441,24 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) {
@@ -1379,11 +1601,31 @@
send_subcmd(d, (struct switch_subcmd_request*)&req, sizeof(req) - 1);
}
@ -503,7 +727,14 @@ index 599fc35..0105c53 100644
static void switch_play_dual_rumble_now(uni_hid_device_t* d,
uint16_t duration_ms,
uint8_t weak_magnitude,
@@ -1391,14 +1471,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d,
uint8_t strong_magnitude) {
switch_instance_t* ins = get_switch_instance(d);
+ if (ins->native_owned || ins->state == STATE_UNINIT)
+ return;
if (duration_ms == 0) {
if (ins->rumble_state != SWITCH_STATE_RUMBLE_DISABLED)
@@ -1391,14 +1633,17 @@
return;
}
@ -512,12 +743,13 @@ index 599fc35..0105c53 100644
- };
- switch_encode_rumble(req.rumble_left, weak_magnitude << 2, weak_magnitude, 500);
- switch_encode_rumble(req.rumble_right, strong_magnitude << 2, strong_magnitude, 500);
-
- // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id
- send_subcmd(d, &req, sizeof(req) - 1);
+ 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;
@ -528,14 +760,28 @@ index 599fc35..0105c53 100644
// Set timer to turn off rumble
ins->rumble_timer_duration.process = &on_switch_set_rumble_off;
@@ -1414,6 +1497,20 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) {
@@ -1410,19 +1655,52 @@
static void on_switch_set_rumble_on(btstack_timer_source_t* ts) {
uni_hid_device_t* d = ts->context;
- switch_instance_t* ins = get_switch_instance(d);
+ if (!d || d->report_parser.setup != uni_hid_parser_switch_setup)
+ return;
+ switch_instance_t* ins = get_switch_instance(d);
+ if (ts != &ins->rumble_timer_delayed_start || ins->native_owned ||
+ ins->rumble_state != SWITCH_STATE_RUMBLE_DELAYED)
+ return;
+ ts->context = NULL;
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);
+ if (!d || d->report_parser.setup != uni_hid_parser_switch_setup)
+ return;
+ switch_instance_t* ins = get_switch_instance(d);
+ if (ins->rumble_state != SWITCH_STATE_RUMBLE_IN_PROGRESS) {
+ if (ts != &ins->rumble_timer_refresh || ins->native_owned ||
+ ins->rumble_state != SWITCH_STATE_RUMBLE_IN_PROGRESS) {
+ return;
+ }
+ switch_send_dual_rumble_now(
@ -549,6 +795,26 @@ index 599fc35..0105c53 100644
static void on_switch_set_rumble_off(btstack_timer_source_t* ts) {
uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts);
+ if (!d || d->report_parser.setup != uni_hid_parser_switch_setup)
+ return;
+ switch_instance_t* ins = get_switch_instance(d);
+ if (ts != &ins->rumble_timer_duration || ins->native_owned ||
+ ins->rumble_state != SWITCH_STATE_RUMBLE_IN_PROGRESS)
+ return;
switch_stop_rumble_now(d);
}
void switch_setup_timeout_callback(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 (!d || d->report_parser.setup != uni_hid_parser_switch_setup)
+ return;
+ switch_instance_t* ins = get_switch_instance(d);
+ if (ts != &ins->setup_timer || ins->state == STATE_UNINIT)
+ return;
logi("Switch: setup timer timeout, failed state: 0x%02x\n", ins->state);
process_fsm(d);
}
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
@ -573,10 +839,29 @@ index be2103e..4819639 100644
// Dpad works as dpad, useful to navigate menus.
ctl->gamepad.dpad |= (report[1] & 0x01) ? DPAD_DOWN : 0;
diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad32/uni_hid_device.c
index 67841e8..9fe7134 100644
--- a/src/components/bluepad32/uni_hid_device.c
+++ b/src/components/bluepad32/uni_hid_device.c
@@ -655,6 +655,7 @@ void uni_hid_device_guess_controller_type_from_pid_vid(uni_hid_device_t* d) {
@@ -447,6 +447,9 @@
return;
}
+ if (d->report_parser.setup == uni_hid_parser_switch_setup)
+ uni_hid_parser_switch_teardown(d);
+
// Disconnect child first
if (d->child)
uni_hid_device_disconnect(d->child);
@@ -490,6 +493,9 @@
loge("uni_hid_device_delete: invalid hid device: NULL\n");
return;
}
+ if (d->report_parser.setup == uni_hid_parser_switch_setup)
+ uni_hid_parser_switch_teardown(d);
+
// Delete child first
if (d->child)
@@ -655,6 +661,7 @@
d->report_parser.setup = uni_hid_parser_psmove_setup;
d->report_parser.init_report = uni_hid_parser_psmove_init_report;
d->report_parser.parse_input_report = uni_hid_parser_psmove_parse_input_report;
@ -584,6 +869,19 @@ index 67841e8..9fe7134 100644
d->report_parser.set_lightbar_color = uni_hid_parser_psmove_set_lightbar_color;
d->report_parser.play_dual_rumble = uni_hid_parser_psmove_play_dual_rumble;
logi("Device detected as PS Move: 0x%02x\n", type);
@@ -809,7 +816,11 @@
return;
}
- int err = l2cap_send(cid, (uint8_t*)report, len);
+ int err;
+ if (d->report_parser.setup == uni_hid_parser_switch_setup)
+ err = uni_hid_parser_switch_send_report(d, cid, report, len);
+ else
+ err = l2cap_send(cid, (uint8_t*)report, len);
if (err != 0) {
logd("Could not send report (error=0x%04x). Adding it to queue\n", err);
if (uni_circular_buffer_put(&d->outgoing_buffer, cid, report, len) != 0) {
diff --git a/src/components/bluepad32/bt/uni_bt.c b/src/components/bluepad32/bt/uni_bt.c
--- a/src/components/bluepad32/bt/uni_bt.c
+++ b/src/components/bluepad32/bt/uni_bt.c
@ -611,3 +909,29 @@ diff --git a/src/components/bluepad32/bt/uni_bt.c b/src/components/bluepad32/bt/
uni_hid_device_send_queued_reports(device);
}
break;
diff --git a/src/components/bluepad32/include/parser/uni_hid_parser_switch.h b/src/components/bluepad32/include/parser/uni_hid_parser_switch.h
--- a/src/components/bluepad32/include/parser/uni_hid_parser_switch.h
+++ b/src/components/bluepad32/include/parser/uni_hid_parser_switch.h
@@ -23,4 +23,22 @@
bool uni_hid_parser_switch_does_name_match(struct uni_hid_device_s* d, const char* name);
void uni_hid_parser_switch_device_dump(struct uni_hid_device_s* d);
+// BTstack-thread-only native owner. Qualification belongs to the caller:
+// type and firmware are unverified bytes from a successful device-info reply.
+bool uni_hid_parser_switch_native_info(struct uni_hid_device_s* d,
+ uint8_t* type, uint8_t* firmware_hi,
+ uint8_t* firmware_lo);
+bool uni_hid_parser_switch_native_acquire(struct uni_hid_device_s* d);
+// Retires compatibility timers and requests neutral; no audio is queued.
+void uni_hid_parser_switch_native_release(struct uni_hid_device_s* d);
+// Exactly 11 bytes on interrupt L2CAP. False means NOT submitted, never queued.
+// Caller must retry/resynchronize; true is transport acceptance, not playback.
+bool uni_hid_parser_switch_native_send(struct uni_hid_device_s* d, const uint8_t rumble[8]);
+
+// Internal device-send/lifecycle hooks, including queued subcommand retries.
+// Returns the L2CAP status, or success when discarding an obsolete writer.
+int uni_hid_parser_switch_send_report(struct uni_hid_device_s* d, uint16_t cid,
+ const uint8_t* report, uint16_t len);
+void uni_hid_parser_switch_teardown(struct uni_hid_device_s* d);
+
#endif // UNI_HID_PARSER_SWITCH_H