From 8e41a2752366a0ec609adb74ccb8f9aab15e2bf1 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 8 Jun 2026 10:21:06 -0700 Subject: [PATCH 1/3] adding switch 1 pro controller --- config-tool-web/code.js | 81 ++++- config-tool-web/examples.js | 8 + config-tool-web/index.html | 4 + config-tool-web/usages.js | 27 ++ firmware-bluetooth/src/main.cc | 640 ++++++++++++++++++++++++++++++++- firmware/src/config.cc | 14 +- firmware/src/our_descriptor.cc | 133 +++++++ firmware/src/our_descriptor.h | 4 +- firmware/src/platform.h | 1 + firmware/src/remapper.cc | 30 ++ firmware/src/remapper.h | 2 + firmware/src/tinyusb_stuff.cc | 7 + firmware/src/types.h | 5 + 13 files changed, 943 insertions(+), 13 deletions(-) diff --git a/config-tool-web/code.js b/config-tool-web/code.js index 01adc610..489a3650 100644 --- a/config-tool-web/code.js +++ b/config-tool-web/code.js @@ -60,6 +60,7 @@ const SET_MONITOR_ENABLED = 22; const CLEAR_QUIRKS = 23; const ADD_QUIRK = 24; const GET_QUIRK = 25; +const GET_SWITCH_PRO_DIAG = 26; const PERSIST_CONFIG_SUCCESS = 1; const PERSIST_CONFIG_CONFIG_TOO_BIG = 2; @@ -190,6 +191,7 @@ document.addEventListener("DOMContentLoaded", function () { document.getElementById("flash_b_side").addEventListener("click", flash_b_side); document.getElementById("pair_new_device").addEventListener("click", pair_new_device); document.getElementById("clear_bonds").addEventListener("click", clear_bonds); + document.getElementById("switch_pro_diag").addEventListener("click", switch_pro_diag); document.getElementById("monitor_clear").addEventListener("click", monitor_clear); document.getElementById("file_input").addEventListener("change", file_uploaded); document.getElementById("add_quirk").addEventListener("click", add_empty_quirk); @@ -866,6 +868,82 @@ async function clear_bonds() { await send_feature_command(CLEAR_BONDS); } +function hex_byte(value) { + return (value & 0xff).toString(16).padStart(2, '0'); +} + +function hex_word(value) { + return (value >>> 0).toString(16).padStart(8, '0'); +} + +async function switch_pro_diag() { + clear_error(); + try { + const pages = []; + for (let page = 0; page < 10; page++) { + await send_feature_command(GET_SWITCH_PRO_DIAG, [[UINT32, page]]); + pages.push(await read_config_feature([UINT32, UINT32, UINT32, UINT32, UINT32, UINT32, UINT32])); + } + + const format_pages = (label, offset) => { + const p0 = pages[offset]; + const p1 = pages[offset + 1]; + const p2 = pages[offset + 2]; + const p3 = pages[offset + 3]; + const p4 = pages[offset + 4]; + const buttons = p3[0]; + const left_stick = p3[1]; + const right_stick = p3[2]; + const bt_connected = p3[5] & 0xffff; + const hogp_ready = p3[5] >>> 16; + const bt_disconnected = p3[6] & 0xffff; + const last_disconnect = (p3[6] >>> 16) & 0xff; + const conn_high = p3[6] >>> 24; + + return [ + label, + 'magic=0x' + hex_word(p0[0]) + ' version=' + p0[1] + ' enabled=' + p0[2], + 'report_q=' + (p0[3] & 0xffff) + ' high=' + (p0[3] >>> 16) + + ' response_q=' + (p0[4] & 0xffff) + ' high=' + (p0[4] >>> 16) + + ' out_q=' + p0[5] + ' out_overflows=' + p0[6], + 'ble=' + p1[0] + ' ble_drop=' + p1[1] + + ' host=' + p1[2] + ' host_drop=' + p1[3] + + ' set=' + p1[4] + ' translated=' + p1[5] + ' response_drop=' + p1[6], + 'mapped=' + p2[0] + ' mapped_fail=' + p2[1] + + ' heartbeat=' + p2[2] + ' heartbeat_fail=' + p2[3] + + ' response=' + p2[4] + ' response_fail=' + p2[5] + ' timer=' + p2[6], + 'buttons=' + hex_byte(buttons) + ' ' + hex_byte(buttons >> 8) + ' ' + hex_byte(buttons >> 16) + + ' left_stick=' + hex_byte(left_stick) + ' ' + hex_byte(left_stick >> 8) + ' ' + hex_byte(left_stick >> 16) + + ' right_stick=' + hex_byte(right_stick) + ' ' + hex_byte(right_stick >> 8) + ' ' + hex_byte(right_stick >> 16) + + ' last_input_ms=' + p3[4], + 'bt_connected=' + bt_connected + ' hogp_ready=' + hogp_ready + + ' bt_disconnected=' + bt_disconnected + ' last_disconnect=' + last_disconnect + ' conn_high=' + conn_high, + 'axis_last lx=0x' + (p4[0] & 0xffff).toString(16).padStart(4, '0') + + ' ly=0x' + (p4[0] >>> 16).toString(16).padStart(4, '0') + + ' rx=0x' + (p4[1] & 0xffff).toString(16).padStart(4, '0') + + ' ry=0x' + (p4[1] >>> 16).toString(16).padStart(4, '0'), + 'axis_range lx=0x' + (p4[2] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[2] >>> 16).toString(16).padStart(4, '0') + + ' ly=0x' + (p4[3] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[3] >>> 16).toString(16).padStart(4, '0') + + ' rx=0x' + (p4[4] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[4] >>> 16).toString(16).padStart(4, '0') + + ' ry=0x' + (p4[5] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[5] >>> 16).toString(16).padStart(4, '0'), + ]; + }; + + const lines = [ + 'Switch Pro diagnostics', + ...format_pages('live', 0), + '', + ...format_pages('saved', 5), + '', + 'raw=' + pages.map((page) => page.map((value) => '0x' + hex_word(value)).join(' ')).join(' | '), + ]; + + display_error_html('
' + lines.join('\n') + '
'); + } catch (e) { + display_error(e); + } +} + function file_uploaded() { const reader = new FileReader(); reader.onload = function (e) { @@ -1484,6 +1562,7 @@ function device_buttons_set_disabled_state(state) { document.getElementById("flash_b_side").disabled = state; document.getElementById("pair_new_device").disabled = state; document.getElementById("clear_bonds").disabled = state; + document.getElementById("switch_pro_diag").disabled = state; } function bluetooth_buttons_set_visibility(visible) { @@ -1845,4 +1924,4 @@ function usage_search_onchange(element) { } element.closest('div').querySelector('.have_preferred_result_checkmark').classList.toggle('d-none', !have_preferred_result); } -} \ No newline at end of file +} diff --git a/config-tool-web/examples.js b/config-tool-web/examples.js index e0a208d5..c2db7bb1 100644 --- a/config-tool-web/examples.js +++ b/config-tool-web/examples.js @@ -10069,4 +10069,12 @@ const examples = [ } ]; +const xbox_bluetooth_switch_idx = examples.findIndex((example) => example.description == 'Xbox controller (Bluetooth) adapter for Switch'); +if (xbox_bluetooth_switch_idx >= 0) { + const switch_pro_example = JSON.parse(JSON.stringify(examples[xbox_bluetooth_switch_idx])); + switch_pro_example.description = 'Xbox controller (Bluetooth) adapter for Switch Pro Controller'; + switch_pro_example.config.our_descriptor_number = 6; + examples.splice(xbox_bluetooth_switch_idx + 1, 0, switch_pro_example); +} + export default examples; diff --git a/config-tool-web/index.html b/config-tool-web/index.html index 43d396bc..4eb1bcd1 100644 --- a/config-tool-web/index.html +++ b/config-tool-web/index.html @@ -186,6 +186,7 @@

HID Remapper Configuration

+ @@ -294,6 +295,9 @@
Custom usages
+
+ +
diff --git a/config-tool-web/usages.js b/config-tool-web/usages.js index 3bce62d8..16f1f338 100644 --- a/config-tool-web/usages.js +++ b/config-tool-web/usages.js @@ -527,6 +527,32 @@ const usages = { "0x00090007": { 'name': 'View', 'class': 'mouse' }, "0x00090008": { 'name': 'Menu', 'class': 'mouse' }, }, + 6: { + "0x00090001": { 'name': 'Y', 'class': 'mouse' }, + "0x00090002": { 'name': 'B', 'class': 'mouse' }, + "0x00090003": { 'name': 'A', 'class': 'mouse' }, + "0x00090004": { 'name': 'X', 'class': 'mouse' }, + "0x00090005": { 'name': 'L', 'class': 'mouse' }, + "0x00090006": { 'name': 'R', 'class': 'mouse' }, + "0x00090007": { 'name': 'ZL', 'class': 'mouse' }, + "0x00090008": { 'name': 'ZR', 'class': 'mouse' }, + "0x00090009": { 'name': 'Minus', 'class': 'mouse' }, + "0x0009000a": { 'name': 'Plus', 'class': 'mouse' }, + "0x0009000b": { 'name': 'LS', 'class': 'mouse' }, + "0x0009000c": { 'name': 'RS', 'class': 'mouse' }, + "0x0009000d": { 'name': 'Home', 'class': 'mouse' }, + "0x0009000e": { 'name': 'Capture', 'class': 'mouse' }, + "0x0009000f": { 'name': 'Button 15', 'class': 'mouse' }, + "0x00090010": { 'name': 'Button 16', 'class': 'mouse' }, + "0xfff90001": { 'name': 'D-pad left', 'class': 'mouse' }, + "0xfff90002": { 'name': 'D-pad right', 'class': 'mouse' }, + "0xfff90003": { 'name': 'D-pad up', 'class': 'mouse' }, + "0xfff90004": { 'name': 'D-pad down', 'class': 'mouse' }, + "0x00010030": { 'name': 'Left stick X', 'class': 'mouse' }, + "0x00010031": { 'name': 'Left stick Y', 'class': 'mouse' }, + "0x00010032": { 'name': 'Right stick X', 'class': 'mouse' }, + "0x00010035": { 'name': 'Right stick Y', 'class': 'mouse' }, + }, }; const common_target_usages = { @@ -645,6 +671,7 @@ Object.assign(usages[2], common_target_usages); Object.assign(usages[3], common_target_usages); Object.assign(usages[4], common_target_usages); Object.assign(usages[5], common_target_usages); +Object.assign(usages[6], common_target_usages); usages[1] = usages[0]; // absolute mouse & keyboard is the same as regular mouse & keyboard export default usages; diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 913618aa..9141c9f2 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -85,6 +86,7 @@ K_MSGQ_DEFINE(descriptor_q, sizeof(struct descriptor_type), 2, 4); K_MSGQ_DEFINE(hogp_ready_q, sizeof(struct hogp_ready_type), CONFIG_BT_MAX_CONN, 4); K_MSGQ_DEFINE(disconnected_q, sizeof(struct disconnected_type), CONFIG_BT_MAX_CONN, 4); K_MSGQ_DEFINE(set_report_q, sizeof(struct set_report_type), 8, 4); +K_MSGQ_DEFINE(switch_pro_response_q, 64, 8, 4); ATOMIC_DEFINE(tick_pending, 1); #define SW0_NODE DT_ALIAS(sw0) @@ -114,6 +116,526 @@ static bool peers_only = true; static struct bt_le_conn_param* conn_param = BT_LE_CONN_PARAM(6, 6, 44, 400); +static bool switch_pro_input_enabled = false; +static uint8_t switch_pro_timer = 0; +static int64_t switch_pro_last_input_ms = 0; +static int64_t switch_pro_last_stats_ms = 0; +static int64_t switch_pro_last_button_log_ms = 0; +static uint8_t switch_pro_current_input[64]; + +static uint32_t switch_pro_ble_reports = 0; +static uint32_t switch_pro_ble_report_drops = 0; +static uint32_t switch_pro_host_reports = 0; +static uint32_t switch_pro_host_report_drops = 0; +static uint32_t switch_pro_set_reports = 0; +static uint32_t switch_pro_translated_reports = 0; +static uint32_t switch_pro_mapped_writes = 0; +static uint32_t switch_pro_mapped_write_fails = 0; +static uint32_t switch_pro_heartbeat_writes = 0; +static uint32_t switch_pro_heartbeat_write_fails = 0; +static uint32_t switch_pro_response_writes = 0; +static uint32_t switch_pro_response_write_fails = 0; +static uint32_t switch_pro_response_drops = 0; +static uint32_t switch_pro_report_q_highwater = 0; +static uint32_t switch_pro_response_q_highwater = 0; +static uint32_t switch_pro_bt_connected_events = 0; +static uint32_t switch_pro_bt_disconnected_events = 0; +static uint32_t switch_pro_hogp_ready_events = 0; +static uint32_t switch_pro_conn_count_highwater = 0; +static uint32_t switch_pro_last_disconnect_reason = 0; + +#define SWITCH_PRO_DIAG_PAGES 5 +#define SWITCH_PRO_DIAG_VALUES 7 + +static uint32_t switch_pro_saved_diag[SWITCH_PRO_DIAG_PAGES][SWITCH_PRO_DIAG_VALUES]; +static int64_t switch_pro_last_diag_persist_ms = 0; +static uint16_t switch_pro_axis_last[4] = { 0x8000, 0x8000, 0x8000, 0x8000 }; +static uint16_t switch_pro_axis_min[4] = { 0xffff, 0xffff, 0xffff, 0xffff }; +static uint16_t switch_pro_axis_max[4] = { 0, 0, 0, 0 }; + +static bool is_switch_pro_mode() { + return our_descriptor_number == 6; +} + +static void switch_pro_reset_input() { + memset(switch_pro_current_input, 0, sizeof(switch_pro_current_input)); + switch_pro_current_input[0] = 0x30; + switch_pro_current_input[2] = 0x91; + switch_pro_current_input[4] = 0x80; + switch_pro_current_input[6] = 0x00; + switch_pro_current_input[7] = 0x08; + switch_pro_current_input[8] = 0x80; + switch_pro_current_input[9] = 0x00; + switch_pro_current_input[10] = 0x08; + switch_pro_current_input[11] = 0x80; + switch_pro_current_input[12] = 0x0c; +} + +static void switch_pro_reset_axis_diagnostics() { + for (uint8_t axis = 0; axis < 4; axis++) { + switch_pro_axis_last[axis] = 0x8000; + switch_pro_axis_min[axis] = 0xffff; + switch_pro_axis_max[axis] = 0; + } +} + +static void switch_pro_reset_session() { + switch_pro_input_enabled = false; + switch_pro_timer = 0; + switch_pro_last_input_ms = 0; + switch_pro_last_stats_ms = 0; + switch_pro_last_button_log_ms = 0; + k_msgq_purge(&switch_pro_response_q); + switch_pro_reset_input(); + switch_pro_reset_axis_diagnostics(); +} + +static uint32_t queue_depth(struct k_msgq* queue) { + return k_msgq_num_used_get(queue); +} + +static void note_switch_pro_report_q_depth() { + uint32_t depth = queue_depth(&report_q); + if (depth > switch_pro_report_q_highwater) { + switch_pro_report_q_highwater = depth; + } +} + +static void note_switch_pro_response_q_depth() { + uint32_t depth = queue_depth(&switch_pro_response_q); + if (depth > switch_pro_response_q_highwater) { + switch_pro_response_q_highwater = depth; + } +} + +static bool report_button_pressed(const uint8_t* report, size_t button_index) { + const size_t bit = button_index - 1; + return (report[1 + bit / 8] & BIT(bit % 8)) != 0; +} + +static uint16_t report_axis_16(const uint8_t* report, size_t offset) { + return (uint16_t) report[offset] | ((uint16_t) report[offset + 1] << 8); +} + +static uint16_t switch_pro_apply_axis_deadzone(uint16_t value) { + const uint16_t center = 0x8000; + const uint16_t threshold = 0x0800; + + if (value >= center - threshold && value <= center + threshold) { + return center; + } + return value; +} + +static uint16_t switch_pro_expand_axis(uint16_t value) { + if (value <= 0x00ff) { + return value * 0x0101; + } + return value; +} + +static uint16_t switch_pro_invert_axis(uint16_t value) { + return 0xffff - value; +} + +static uint16_t switch_pro_normalize_axis(uint16_t value, bool invert) { + value = switch_pro_expand_axis(value); + if (invert) { + value = switch_pro_invert_axis(value); + } + return switch_pro_apply_axis_deadzone(value); +} + +static void switch_pro_note_axis(uint8_t axis, uint16_t value) { + switch_pro_axis_last[axis] = value; + if (value < switch_pro_axis_min[axis]) { + switch_pro_axis_min[axis] = value; + } + if (value > switch_pro_axis_max[axis]) { + switch_pro_axis_max[axis] = value; + } +} + +static void pack_switch_pro_stick(uint8_t* out, uint16_t x16, uint16_t y16) { + /* + * The Switch expects 12-bit packed axes. Values passed here are already + * normalized to the 16-bit HID Remapper output range. + */ + uint16_t x = x16 >> 4; + uint16_t y = y16 >> 4; + + out[0] = x & 0xff; + out[1] = ((x >> 8) & 0x0f) | ((y & 0x0f) << 4); + out[2] = (y >> 4) & 0xff; +} + +static void apply_switch_pro_hat(uint8_t hat, uint8_t* buttons2) { + if (hat > 7) { + return; + } + + if (hat == 3 || hat == 4 || hat == 5) { + *buttons2 |= BIT(0); // Down + } + if (hat == 7 || hat == 0 || hat == 1) { + *buttons2 |= BIT(1); // Up + } + if (hat == 1 || hat == 2 || hat == 3) { + *buttons2 |= BIT(2); // Right + } + if (hat == 5 || hat == 6 || hat == 7) { + *buttons2 |= BIT(3); // Left + } +} + +static void switch_pro_translate_report(const uint8_t* report_with_id, uint8_t len) { + if ((len < 12) || (report_with_id[0] != 0x30)) { + return; + } + + uint8_t next[64]; + memcpy(next, switch_pro_current_input, sizeof(next)); + next[0] = 0x30; + next[2] = 0x91; + next[3] = 0; + next[4] = 0x80; + next[5] = 0; + + if (report_button_pressed(report_with_id, 1)) next[3] |= BIT(0); // Y + if (report_button_pressed(report_with_id, 4)) next[3] |= BIT(1); // X + if (report_button_pressed(report_with_id, 2)) next[3] |= BIT(2); // B + if (report_button_pressed(report_with_id, 3)) next[3] |= BIT(3); // A + if (report_button_pressed(report_with_id, 6)) next[3] |= BIT(6); // R + if (report_button_pressed(report_with_id, 8)) next[3] |= BIT(7); // ZR + if (report_button_pressed(report_with_id, 9)) next[4] |= BIT(0); // Minus + if (report_button_pressed(report_with_id, 10)) next[4] |= BIT(1); // Plus + if (report_button_pressed(report_with_id, 12)) next[4] |= BIT(2); // RS + if (report_button_pressed(report_with_id, 11)) next[4] |= BIT(3); // LS + if (report_button_pressed(report_with_id, 13)) next[4] |= BIT(4); // Home + if (report_button_pressed(report_with_id, 14)) next[4] |= BIT(5); // Capture + if (report_button_pressed(report_with_id, 5)) next[5] |= BIT(6); // L + if (report_button_pressed(report_with_id, 7)) next[5] |= BIT(7); // ZL + + apply_switch_pro_hat(report_with_id[11] & 0x0f, &next[5]); + uint16_t lx = switch_pro_normalize_axis(report_axis_16(report_with_id, 3), false); + uint16_t ly = switch_pro_normalize_axis(report_axis_16(report_with_id, 5), true); + uint16_t rx = switch_pro_normalize_axis(report_axis_16(report_with_id, 7), false); + uint16_t ry = switch_pro_normalize_axis(report_axis_16(report_with_id, 9), true); + switch_pro_note_axis(0, lx); + switch_pro_note_axis(1, ly); + switch_pro_note_axis(2, rx); + switch_pro_note_axis(3, ry); + pack_switch_pro_stick(next + 6, lx, ly); + pack_switch_pro_stick(next + 9, rx, ry); + next[12] = 0x0c; + + bool buttons_changed = next[3] != switch_pro_current_input[3] || next[4] != switch_pro_current_input[4] || next[5] != switch_pro_current_input[5]; + memcpy(switch_pro_current_input, next, sizeof(switch_pro_current_input)); + switch_pro_translated_reports++; + + int64_t now = k_uptime_get(); + if (buttons_changed && (now - switch_pro_last_button_log_ms >= 20)) { + switch_pro_last_button_log_ms = now; + LOG_INF("switch_pro buttons=%02x %02x %02x sticks=%02x %02x %02x %02x %02x %02x out_q=%u report_q=%u", + switch_pro_current_input[3], switch_pro_current_input[4], switch_pro_current_input[5], + switch_pro_current_input[6], switch_pro_current_input[7], switch_pro_current_input[8], + switch_pro_current_input[9], switch_pro_current_input[10], switch_pro_current_input[11], + debug_outgoing_report_count(), queue_depth(&report_q)); + } +} + +static void switch_pro_queue_response(const uint8_t* response, size_t len) { + uint8_t buf[64] = {}; + if (len > sizeof(buf)) { + len = sizeof(buf); + } + memcpy(buf, response, len); + if (k_msgq_put(&switch_pro_response_q, buf, K_NO_WAIT)) { + switch_pro_response_drops++; + } + note_switch_pro_response_q_depth(); +} + +static void switch_pro_queue_81(uint8_t command) { + uint8_t response[64] = { 0x81, command }; + + if (command == 0x01) { + static const uint8_t mac_response[] = { 0x81, 0x01, 0x00, 0x03, 0x1f, 0x86, 0x1d, 0xd6, 0x03, 0x04 }; + memcpy(response, mac_response, sizeof(mac_response)); + } + + switch_pro_queue_response(response, sizeof(response)); +} + +static void switch_pro_queue_21(uint8_t subcommand, uint8_t ack, const uint8_t* data, uint8_t data_len) { + uint8_t response[64] = {}; + response[0] = 0x21; + response[1] = switch_pro_timer++; + memcpy(response + 2, switch_pro_current_input + 2, 11); + response[13] = ack; + response[14] = subcommand; + if (data && data_len) { + if (data_len > sizeof(response) - 15) { + data_len = sizeof(response) - 15; + } + memcpy(response + 15, data, data_len); + } + switch_pro_queue_response(response, sizeof(response)); +} + +static void switch_pro_spi_read(const uint8_t* args, uint8_t args_len) { + uint8_t data[48]; + memset(data, 0xff, sizeof(data)); + if (args_len < 5) { + switch_pro_queue_21(0x10, 0x90, data, sizeof(data)); + return; + } + + uint32_t address = (uint32_t) args[0] | ((uint32_t) args[1] << 8) | ((uint32_t) args[2] << 16) | ((uint32_t) args[3] << 24); + uint8_t read_len = args[4]; + memcpy(data, args, 5); + + static const uint8_t stick_cal[] = { + 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, + 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80 + }; + static const uint8_t body_color[] = { 0x46, 0x46, 0x46 }; + static const uint8_t button_color[] = { 0xff, 0xff, 0xff }; + + if (address == 0x0000603d) { + memcpy(data + 5, stick_cal, MIN(read_len, (uint8_t) sizeof(stick_cal))); + } else if (address == 0x00006050) { + memcpy(data + 5, stick_cal, MIN(read_len, (uint8_t) sizeof(stick_cal))); + } else if (address == 0x00006086) { + memcpy(data + 5, body_color, MIN(read_len, (uint8_t) sizeof(body_color))); + } else if (address == 0x00006089) { + memcpy(data + 5, button_color, MIN(read_len, (uint8_t) sizeof(button_color))); + } + + switch_pro_queue_21(0x10, 0x90, data, MIN((uint8_t) sizeof(data), (uint8_t) (read_len + 5))); +} + +static void switch_pro_handle_subcommand(const uint8_t* report, uint8_t len, bool has_report_id) { + uint8_t base = has_report_id ? 1 : 0; + if (len <= base + 9) { + return; + } + + uint8_t subcommand = report[base + 9]; + const uint8_t* args = report + base + 10; + uint8_t args_len = len - base - 10; + + switch (subcommand) { + case 0x01: { + static const uint8_t pairing_response[] = { 0x03 }; + switch_pro_queue_21(subcommand, 0x81, pairing_response, sizeof(pairing_response)); + break; + } + case 0x02: { + static const uint8_t device_info[] = { 0x03, 0x48, 0x03, 0x02, 0x53, 0x50, 0x00, 0x01, 0x03, 0x04, 0x01, 0x01 }; + switch_pro_queue_21(subcommand, 0x82, device_info, sizeof(device_info)); + break; + } + case 0x04: + switch_pro_queue_21(subcommand, 0x83, NULL, 0); + break; + case 0x10: + switch_pro_spi_read(args, args_len); + break; + case 0x21: { + static const uint8_t imu_status[] = { 0x01, 0x00, 0xff, 0x00, 0x03, 0x00, 0x05, 0x01 }; + switch_pro_queue_21(subcommand, 0xa0, imu_status, sizeof(imu_status)); + break; + } + case 0x30: + switch_pro_input_enabled = args_len == 0 || args[0] != 0; + switch_pro_last_input_ms = 0; + switch_pro_queue_21(subcommand, 0x80, NULL, 0); + break; + default: + switch_pro_queue_21(subcommand, 0x80, NULL, 0); + break; + } +} + +static bool switch_pro_handle_output_report(const uint8_t* report, uint8_t len) { + if (!is_switch_pro_mode() || len == 0) { + return false; + } + + uint8_t report_id = report[0]; + bool has_report_id = true; + + if (report_id != 0x01 && report_id != 0x10 && report_id != 0x80 && report_id != 0x82) { + has_report_id = false; + report_id = len == 1 ? 0x80 : report[0]; + } + + if (report_id == 0x80) { + uint8_t command = has_report_id ? (len > 1 ? report[1] : 0) : report[0]; + switch (command) { + case 0x04: + switch_pro_input_enabled = true; + switch_pro_last_input_ms = 0; + break; + case 0x05: + switch_pro_input_enabled = false; + break; + case 0x01: + case 0x02: + case 0x03: + default: + switch_pro_queue_81(command); + break; + } + return true; + } + + if (report_id == 0x01) { + switch_pro_handle_subcommand(report, len, has_report_id); + return true; + } + + return true; +} + +static bool switch_pro_send_response() { + uint8_t response[64]; + if (!is_switch_pro_mode() || k_msgq_get(&switch_pro_response_q, response, K_NO_WAIT)) { + return false; + } + bool sent = CHK(hid_int_ep_write(hid_dev0, response, sizeof(response), NULL)); + if (sent) { + switch_pro_response_writes++; + } else { + switch_pro_response_write_fails++; + } + return sent; +} + +static bool switch_pro_send_input_heartbeat() { + if (!is_switch_pro_mode() || !switch_pro_input_enabled) { + return false; + } + + int64_t now = k_uptime_get(); + if (switch_pro_last_input_ms && (now - switch_pro_last_input_ms < 8)) { + return false; + } + + switch_pro_current_input[1] = switch_pro_timer++; + switch_pro_last_input_ms = now; + bool sent = CHK(hid_int_ep_write(hid_dev0, switch_pro_current_input, sizeof(switch_pro_current_input), NULL)); + if (sent) { + switch_pro_heartbeat_writes++; + } else { + switch_pro_heartbeat_write_fails++; + } + return sent; +} + +static void switch_pro_fill_diagnostics(uint32_t page, uint32_t values[SWITCH_PRO_DIAG_VALUES]) { + memset(values, 0, SWITCH_PRO_DIAG_VALUES * sizeof(uint32_t)); + + switch (page) { + case 0: + values[0] = 0x44315053; // SP1D + values[1] = 1; + values[2] = switch_pro_input_enabled; + values[3] = (queue_depth(&report_q) & 0xffff) | (switch_pro_report_q_highwater << 16); + values[4] = (queue_depth(&switch_pro_response_q) & 0xffff) | (switch_pro_response_q_highwater << 16); + values[5] = debug_outgoing_report_count(); + values[6] = debug_outgoing_report_overflows(); + break; + case 1: + values[0] = switch_pro_ble_reports; + values[1] = switch_pro_ble_report_drops; + values[2] = switch_pro_host_reports; + values[3] = switch_pro_host_report_drops; + values[4] = switch_pro_set_reports; + values[5] = switch_pro_translated_reports; + values[6] = switch_pro_response_drops; + break; + case 2: + values[0] = switch_pro_mapped_writes; + values[1] = switch_pro_mapped_write_fails; + values[2] = switch_pro_heartbeat_writes; + values[3] = switch_pro_heartbeat_write_fails; + values[4] = switch_pro_response_writes; + values[5] = switch_pro_response_write_fails; + values[6] = switch_pro_timer; + break; + case 3: + values[0] = switch_pro_current_input[3] | (switch_pro_current_input[4] << 8) | (switch_pro_current_input[5] << 16); + values[1] = switch_pro_current_input[6] | (switch_pro_current_input[7] << 8) | (switch_pro_current_input[8] << 16); + values[2] = switch_pro_current_input[9] | (switch_pro_current_input[10] << 8) | (switch_pro_current_input[11] << 16); + values[3] = switch_pro_current_input[1]; + values[4] = switch_pro_last_input_ms; + values[5] = (switch_pro_bt_connected_events & 0xffff) | ((switch_pro_hogp_ready_events & 0xffff) << 16); + values[6] = (switch_pro_bt_disconnected_events & 0xffff) | ((switch_pro_last_disconnect_reason & 0xff) << 16) | ((switch_pro_conn_count_highwater & 0xff) << 24); + break; + case 4: + values[0] = switch_pro_axis_last[0] | (switch_pro_axis_last[1] << 16); + values[1] = switch_pro_axis_last[2] | (switch_pro_axis_last[3] << 16); + values[2] = switch_pro_axis_min[0] | (switch_pro_axis_max[0] << 16); + values[3] = switch_pro_axis_min[1] | (switch_pro_axis_max[1] << 16); + values[4] = switch_pro_axis_min[2] | (switch_pro_axis_max[2] << 16); + values[5] = switch_pro_axis_min[3] | (switch_pro_axis_max[3] << 16); + break; + default: + break; + } +} + +static void switch_pro_persist_diagnostics() { + if (!is_switch_pro_mode() || !switch_pro_input_enabled) { + return; + } + if (switch_pro_ble_reports == 0 && switch_pro_translated_reports <= 2) { + return; + } + + int64_t now = k_uptime_get(); + if (switch_pro_last_diag_persist_ms && (now - switch_pro_last_diag_persist_ms < 2000)) { + return; + } + switch_pro_last_diag_persist_ms = now; + + for (uint32_t page = 0; page < SWITCH_PRO_DIAG_PAGES; page++) { + switch_pro_fill_diagnostics(page, switch_pro_saved_diag[page]); + } + settings_save_one("remapper/switch_pro_diag", switch_pro_saved_diag, sizeof(switch_pro_saved_diag)); +} + +static void switch_pro_log_stats() { + if (!is_switch_pro_mode()) { + return; + } + + int64_t now = k_uptime_get(); + if (switch_pro_last_stats_ms && (now - switch_pro_last_stats_ms < 1000)) { + return; + } + switch_pro_last_stats_ms = now; + + LOG_INF("switch_pro_stats enabled=%u report_q=%u/%u response_q=%u/%u out_q=%u out_overflows=%u ble=%u ble_drop=%u host=%u host_drop=%u set=%u translated=%u mapped=%u/%u heartbeat=%u/%u response=%u/%u response_drop=%u buttons=%02x %02x %02x", + switch_pro_input_enabled, + queue_depth(&report_q), switch_pro_report_q_highwater, + queue_depth(&switch_pro_response_q), switch_pro_response_q_highwater, + debug_outgoing_report_count(), debug_outgoing_report_overflows(), + switch_pro_ble_reports, switch_pro_ble_report_drops, + switch_pro_host_reports, switch_pro_host_report_drops, + switch_pro_set_reports, + switch_pro_translated_reports, + switch_pro_mapped_writes, switch_pro_mapped_write_fails, + switch_pro_heartbeat_writes, switch_pro_heartbeat_write_fails, + switch_pro_response_writes, switch_pro_response_write_fails, + switch_pro_response_drops, + switch_pro_current_input[3], switch_pro_current_input[4], switch_pro_current_input[5]); + + switch_pro_persist_diagnostics(); +} + static void activity_led_off_work_fn(struct k_work* work) { gpio_pin_set_dt(&led0, false); } @@ -395,7 +917,10 @@ static void connected(struct bt_conn* conn, uint8_t conn_err) { char addr[BT_ADDR_LE_STR_LEN]; scanning = false; - count_connections(); + int conn_count = count_connections(); + if (is_switch_pro_mode() && conn_count > (int) switch_pro_conn_count_highwater) { + switch_pro_conn_count_highwater = conn_count; + } set_led_mode(LedMode::BLINK); bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); @@ -409,6 +934,10 @@ static void connected(struct bt_conn* conn, uint8_t conn_err) { LOG_INF("%s", addr); + if (is_switch_pro_mode()) { + switch_pro_bt_connected_events++; + } + CHK(bt_conn_set_security(conn, BT_SECURITY_L2)); } @@ -419,6 +948,11 @@ static void disconnected(struct bt_conn* conn, uint8_t reason) { LOG_INF("%s (reason=%u)", addr, reason); + if (is_switch_pro_mode()) { + switch_pro_bt_disconnected_events++; + switch_pro_last_disconnect_reason = reason; + } + uint8_t conn_idx = bt_conn_index(conn); if (bt_hogp_assign_check(&hogps[conn_idx])) { @@ -509,7 +1043,12 @@ static uint8_t hogp_notify_cb(struct bt_hogp* hogp, struct bt_hogp_rep_info* rep memcpy(buf.data + 1, data, buf.len); if (k_msgq_put(&report_q, &buf, K_NO_WAIT)) { - // printk("error in k_msg_put(report_q\n"); + if (is_switch_pro_mode()) { + switch_pro_ble_report_drops++; + } + } else if (is_switch_pro_mode()) { + switch_pro_ble_reports++; + note_switch_pro_report_q_depth(); } return BT_GATT_ITER_CONTINUE; @@ -551,6 +1090,9 @@ static void hogp_ready_work_fn(struct k_work* work) { while (!k_msgq_get(&hogp_ready_q, &item, K_NO_WAIT)) { LOG_INF("hogp_ready"); + if (is_switch_pro_mode()) { + switch_pro_hogp_ready_events++; + } struct find_bond_t find_bond = { .i = 0, @@ -684,9 +1226,21 @@ static void int_out_ready_cb0(const struct device* dev) { static struct report_type buf; uint32_t len; if (CHK(hid_int_ep_read(hid_dev0, buf.data, sizeof(buf.data), &len))) { + if (is_switch_pro_mode() && switch_pro_handle_output_report(buf.data, len)) { + switch_pro_host_reports++; + return; + } + buf.interface = OUR_OUT_INTERFACE; buf.len = len; - CHK(k_msgq_put(&report_q, &buf, K_NO_WAIT)); + if (k_msgq_put(&report_q, &buf, K_NO_WAIT)) { + if (is_switch_pro_mode()) { + switch_pro_host_report_drops++; + } + } else if (is_switch_pro_mode()) { + switch_pro_host_reports++; + note_switch_pro_report_q_depth(); + } } } @@ -708,6 +1262,19 @@ static const struct hid_ops ops1 = { }; static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uint8_t len) { + if (is_switch_pro_mode() && interface == 0 && len > 0 && report_with_id[0] == 0x30) { + switch_pro_translate_report(report_with_id, len); + switch_pro_current_input[1] = switch_pro_timer++; + switch_pro_last_input_ms = k_uptime_get(); + bool sent = CHK(hid_int_ep_write(hid_dev0, switch_pro_current_input, sizeof(switch_pro_current_input), NULL)); + if (sent) { + switch_pro_mapped_writes++; + } else { + switch_pro_mapped_write_fails++; + } + return sent; + } + if (report_with_id[0] == 0) { report_with_id++; len--; @@ -718,6 +1285,7 @@ static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uin if (interface == 1) { return CHK(hid_int_ep_write(hid_dev1, report_with_id, len, NULL)); } + return false; } static void button_init() { @@ -757,6 +1325,10 @@ static void leds_init() { static void status_cb(enum usb_dc_status_code status, const uint8_t* param) { if (status == USB_DC_SOF) { atomic_set_bit(tick_pending, 0); + } else if (status == USB_DC_RESET || status == USB_DC_CONFIGURED || status == USB_DC_SUSPEND) { + if (is_switch_pro_mode()) { + switch_pro_reset_session(); + } } } @@ -808,7 +1380,23 @@ static void bt_init() { } static int remapper_settings_set(const char* name, size_t len, settings_read_cb read_cb, void* cb_arg) { - LOG_INF("len=%d", len); + LOG_INF("%s len=%d", name, len); + + if (!strcmp(name, "switch_pro_diag")) { + if (len != sizeof(switch_pro_saved_diag)) { + return -EINVAL; + } + + int bytes_read = read_cb(cb_arg, switch_pro_saved_diag, len); + if (bytes_read < 0) { + return bytes_read; + } + return bytes_read == sizeof(switch_pro_saved_diag) ? 0 : -EINVAL; + } + + if (strcmp(name, "config")) { + return -ENOENT; + } static uint8_t buffer[PERSISTED_CONFIG_SIZE]; @@ -853,6 +1441,21 @@ void reset_to_bootloader() { void flash_b_side() { } +void get_switch_pro_diagnostics(uint32_t page, uint32_t values[7]) { + if (page < SWITCH_PRO_DIAG_PAGES) { + switch_pro_fill_diagnostics(page, values); + return; + } + + page -= SWITCH_PRO_DIAG_PAGES; + if (page < SWITCH_PRO_DIAG_PAGES) { + memcpy(values, switch_pro_saved_diag[page], SWITCH_PRO_DIAG_VALUES * sizeof(uint32_t)); + return; + } + + memset(values, 0, SWITCH_PRO_DIAG_VALUES * sizeof(uint32_t)); +} + void pair_new_device() { peers_only = false; k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); @@ -910,6 +1513,9 @@ int main() { CHK(settings_register(&our_settings_handlers)); settings_load(); descriptor_init(); + if (is_switch_pro_mode()) { + switch_pro_reset_session(); + } usb_init(); scan_init(); parse_our_descriptor(); @@ -926,16 +1532,24 @@ int main() { bool get_report_response_pending = false; while (true) { - if (!process_pending && !k_msgq_get(&report_q, &incoming_report, K_NO_WAIT)) { - handle_received_report(incoming_report.data, incoming_report.len, (uint16_t) incoming_report.interface); - process_pending = true; + if (!process_pending) { + uint8_t reports_to_drain = is_switch_pro_mode() ? 6 : 1; + while (reports_to_drain-- && !k_msgq_get(&report_q, &incoming_report, K_NO_WAIT)) { + if (incoming_report.interface == OUR_OUT_INTERFACE && switch_pro_handle_output_report(incoming_report.data, incoming_report.len)) { + // Switch Pro output reports are host commands, not remapper inputs. + } else { + handle_received_report(incoming_report.data, incoming_report.len, (uint16_t) incoming_report.interface); + } + process_pending = true; + } } if (atomic_test_and_clear_bit(tick_pending, 0)) { process_mapping(true); process_pending = false; + switch_pro_log_stats(); } if (!k_sem_take(&usb_sem0, K_NO_WAIT)) { - if (!send_report(do_send_report)) { + if (!switch_pro_send_response() && !send_report(do_send_report) && !switch_pro_send_input_heartbeat()) { k_sem_give(&usb_sem0); } } @@ -947,7 +1561,15 @@ int main() { if (!k_msgq_get(&set_report_q, &set_report_item, K_NO_WAIT)) { if (set_report_item.interface == 0) { - handle_set_report0(set_report_item.report_id, set_report_item.data, set_report_item.len); + uint8_t switch_report[65]; + switch_report[0] = set_report_item.report_id; + memcpy(switch_report + 1, set_report_item.data, set_report_item.len); + if (is_switch_pro_mode()) { + switch_pro_set_reports++; + } + if (!switch_pro_handle_output_report(switch_report, set_report_item.len + 1)) { + handle_set_report0(set_report_item.report_id, set_report_item.data, set_report_item.len); + } } if (set_report_item.interface == 1) { handle_set_report1(set_report_item.report_id, set_report_item.data, set_report_item.len); diff --git a/firmware/src/config.cc b/firmware/src/config.cc index 18764e54..e6b39b02 100644 --- a/firmware/src/config.cc +++ b/firmware/src/config.cc @@ -23,6 +23,10 @@ ConfigCommand last_config_command = ConfigCommand::NO_COMMAND; uint32_t requested_index = 0; uint32_t requested_secondary_index = 0; +__attribute__((weak)) void get_switch_pro_diagnostics(uint32_t page, uint32_t values[7]) { + memset(values, 0, 7 * sizeof(uint32_t)); +} + bool checksum_ok(const uint8_t* buffer, uint16_t data_size) { return crc32(buffer, data_size - 4) == ((crc32_t*) (buffer + data_size - 4))->crc32; } @@ -934,6 +938,13 @@ uint16_t handle_get_report1(uint8_t report_id, uint8_t* buffer, uint16_t reqlen) my_mutex_exit(MutexId::QUIRKS); break; } + case ConfigCommand::GET_SWITCH_PRO_DIAG: { + switch_pro_diag_t* diag = (switch_pro_diag_t*) config_buffer; + uint32_t values[7]; + get_switch_pro_diagnostics(requested_index, values); + memcpy(diag->values, values, sizeof(values)); + break; + } case ConfigCommand::PERSIST_CONFIG: { persist_config_response_t* returned = (persist_config_response_t*) config_buffer; if (persist_config_return_code == PersistConfigReturnCode::UNKNOWN) { @@ -999,7 +1010,8 @@ void handle_set_report1(uint8_t report_id, uint8_t const* buffer, uint16_t bufsi case ConfigCommand::GET_MAPPING: case ConfigCommand::GET_OUR_USAGES: case ConfigCommand::GET_THEIR_USAGES: - case ConfigCommand::GET_QUIRK: { + case ConfigCommand::GET_QUIRK: + case ConfigCommand::GET_SWITCH_PRO_DIAG: { get_indexed_t* get_indexed = (get_indexed_t*) config_buffer->data; requested_index = get_indexed->requested_index; break; diff --git a/firmware/src/our_descriptor.cc b/firmware/src/our_descriptor.cc index 0902867c..d1de6f21 100644 --- a/firmware/src/our_descriptor.cc +++ b/firmware/src/our_descriptor.cc @@ -309,6 +309,100 @@ const uint8_t our_report_descriptor_horipad[] = { 0xC0, // End Collection }; +const uint8_t our_report_descriptor_switch_pro[] = { + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x15, 0x00, // Logical Minimum (0) + 0x09, 0x04, // Usage (Joystick) + 0xA1, 0x01, // Collection (Application) + 0x85, 0x30, // Report ID (48) + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (0x01) + 0x29, 0x0A, // Usage Maximum (0x0A) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x0A, // Report Count (10) + 0x55, 0x00, // Unit Exponent (0) + 0x65, 0x00, // Unit (None) + 0x81, 0x02, // Input (Data,Var,Abs) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x0B, // Usage Minimum (0x0B) + 0x29, 0x0E, // Usage Maximum (0x0E) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x04, // Report Count (4) + 0x81, 0x02, // Input (Data,Var,Abs) + 0x75, 0x01, // Report Size (1) + 0x95, 0x02, // Report Count (2) + 0x81, 0x03, // Input (Const,Var,Abs) + 0x0B, 0x01, 0x00, 0x01, 0x00, // Usage (Pointer) + 0xA1, 0x00, // Collection (Physical) + 0x0B, 0x30, 0x00, 0x01, 0x00, // Usage (X) + 0x0B, 0x31, 0x00, 0x01, 0x00, // Usage (Y) + 0x0B, 0x32, 0x00, 0x01, 0x00, // Usage (Z) + 0x0B, 0x35, 0x00, 0x01, 0x00, // Usage (Rz) + 0x15, 0x00, // Logical Minimum (0) + 0x27, 0xFF, 0xFF, 0x00, 0x00, // Logical Maximum (65534) + 0x75, 0x10, // Report Size (16) + 0x95, 0x04, // Report Count (4) + 0x81, 0x02, // Input (Data,Var,Abs) + 0xC0, // End Collection + 0x0B, 0x39, 0x00, 0x01, 0x00, // Usage (Hat switch) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x07, // Logical Maximum (7) + 0x35, 0x00, // Physical Minimum (0) + 0x46, 0x3B, 0x01, // Physical Maximum (315) + 0x65, 0x14, // Unit (English Rotation) + 0x75, 0x04, // Report Size (4) + 0x95, 0x01, // Report Count (1) + 0x81, 0x02, // Input (Data,Var,Abs) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x0F, // Usage Minimum (0x0F) + 0x29, 0x12, // Usage Maximum (0x12) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x04, // Report Count (4) + 0x81, 0x02, // Input (Data,Var,Abs) + 0x75, 0x08, // Report Size (8) + 0x95, 0x34, // Report Count (52) + 0x81, 0x03, // Input (Const,Var,Abs) + 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) + 0x85, 0x21, // Report ID (33) + 0x09, 0x01, // Usage (0x01) + 0x75, 0x08, // Report Size (8) + 0x95, 0x3F, // Report Count (63) + 0x81, 0x03, // Input (Const,Var,Abs) + 0x85, 0x81, // Report ID (129) + 0x09, 0x02, // Usage (0x02) + 0x75, 0x08, // Report Size (8) + 0x95, 0x3F, // Report Count (63) + 0x81, 0x03, // Input (Const,Var,Abs) + 0x85, 0x01, // Report ID (1) + 0x09, 0x03, // Usage (0x03) + 0x75, 0x08, // Report Size (8) + 0x95, 0x3F, // Report Count (63) + 0x91, 0x83, // Output (Const,Var,Abs,Volatile) + 0x85, 0x10, // Report ID (16) + 0x09, 0x04, // Usage (0x04) + 0x75, 0x08, // Report Size (8) + 0x95, 0x3F, // Report Count (63) + 0x91, 0x83, // Output (Const,Var,Abs,Volatile) + 0x85, 0x80, // Report ID (128) + 0x09, 0x05, // Usage (0x05) + 0x75, 0x08, // Report Size (8) + 0x95, 0x3F, // Report Count (63) + 0x91, 0x83, // Output (Const,Var,Abs,Volatile) + 0x85, 0x82, // Report ID (130) + 0x09, 0x06, // Usage (0x06) + 0x75, 0x08, // Report Size (8) + 0x95, 0x3F, // Report Count (63) + 0x91, 0x83, // Output (Const,Var,Abs,Volatile) + 0xC0, // End Collection +}; + uint8_t const our_report_descriptor_ps4[] = { 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x05, // Usage (Game Pad) @@ -567,6 +661,21 @@ void horipad_clear_report(uint8_t* report, uint8_t report_id, uint16_t len) { memcpy(report, horipad_neutral, sizeof(horipad_neutral)); } +void switch_pro_clear_report(uint8_t* report, uint8_t report_id, uint16_t len) { + memset(report, 0, len); + if (report_id == 0x30 && len >= 11) { + report[2] = 0x00; + report[3] = 0x80; + report[4] = 0x00; + report[5] = 0x80; + report[6] = 0x00; + report[7] = 0x80; + report[8] = 0x00; + report[9] = 0x80; + report[10] = 8; + } +} + void ps4_clear_report(uint8_t* report, uint8_t report_id, uint16_t len) { memset(report, 0, len); report[0] = report[1] = report[2] = report[3] = 0x80; @@ -614,6 +723,20 @@ int32_t ps4_stadia_default_value(uint32_t usage) { } } +int32_t switch_pro_default_value(uint32_t usage) { + switch (usage) { + case 0x00010039: + return 8; + case 0x00010030: + case 0x00010031: + case 0x00010032: + case 0x00010035: + return 0x8000; + default: + return 0; + } +} + void stadia_sanitize_report(uint8_t report_id, uint8_t* buffer, uint16_t len) { if (buffer[3] == 0) { buffer[3] = 1; @@ -696,6 +819,16 @@ const our_descriptor_def_t our_descriptors[] = { .clear_report = xac_compat_clear_report, .default_value = ps4_stadia_default_value, // sic }, + { + .idx = 6, + .descriptor = our_report_descriptor_switch_pro, + .descriptor_length = sizeof(our_report_descriptor_switch_pro), + .vid = 0x057E, + .pid = 0x2009, + .handle_received_report = do_handle_received_report, + .clear_report = switch_pro_clear_report, + .default_value = switch_pro_default_value, + }, }; const uint8_t config_report_descriptor[] = { diff --git a/firmware/src/our_descriptor.h b/firmware/src/our_descriptor.h index e09a03af..975828a1 100644 --- a/firmware/src/our_descriptor.h +++ b/firmware/src/our_descriptor.h @@ -11,9 +11,9 @@ #define REPORT_ID_CONFIG 100 #define REPORT_ID_MONITOR 101 -#define MAX_INPUT_REPORT_ID 3 +#define MAX_INPUT_REPORT_ID 0x81 -#define NOUR_DESCRIPTORS 6 +#define NOUR_DESCRIPTORS 7 typedef void (*device_connected_t)(uint16_t interface, uint16_t vid, uint16_t pid); typedef void (*device_disconnected_t)(uint8_t dev_addr); diff --git a/firmware/src/platform.h b/firmware/src/platform.h index e27b41a3..33cc1868 100644 --- a/firmware/src/platform.h +++ b/firmware/src/platform.h @@ -10,6 +10,7 @@ void reset_to_bootloader(); void pair_new_device(); void clear_bonds(); void flash_b_side(); +void get_switch_pro_diagnostics(uint32_t page, uint32_t values[7]); void my_mutexes_init(); void my_mutex_enter(MutexId id); diff --git a/firmware/src/remapper.cc b/firmware/src/remapper.cc index c1a50734..3cfc1bc1 100644 --- a/firmware/src/remapper.cc +++ b/firmware/src/remapper.cc @@ -76,6 +76,7 @@ uint8_t outgoing_reports[OR_BUFSIZE][MAX_REPORT_SIZE + 1]; uint8_t or_head = 0; uint8_t or_tail = 0; uint8_t or_items = 0; +uint32_t or_overflows = 0; std::vector report_ids; @@ -1397,7 +1398,28 @@ void process_mapping(bool auto_repeat) { our_descriptor->sanitize_report(report_id, reports[report_id], report_sizes[report_id]); } if (needs_to_be_sent(report_id)) { + bool replaced_pending = false; + if ((our_descriptor->idx == 6) && (report_id == 0x30)) { + for (uint8_t n = 0; n < or_items; n++) { + uint8_t idx = (or_head + n) % OR_BUFSIZE; + if (outgoing_reports[idx][0] == report_id) { + memcpy(outgoing_reports[idx] + 1, reports[report_id], report_sizes[report_id]); + memcpy(prev_reports[report_id], reports[report_id], report_sizes[report_id]); + replaced_pending = true; + break; + } + } + } + if (replaced_pending) { + if (our_descriptor->clear_report != nullptr) { + our_descriptor->clear_report(reports[report_id], report_id, report_sizes[report_id]); + } else { + memset(reports[report_id], 0, report_sizes[report_id]); + } + continue; + } if (or_items == OR_BUFSIZE) { + or_overflows++; printf("overflow!\n"); break; } @@ -2028,6 +2050,14 @@ void print_stats() { processing_time = 0; } +uint8_t debug_outgoing_report_count() { + return or_items; +} + +uint32_t debug_outgoing_report_overflows() { + return or_overflows; +} + void reset_state() { memset(registers, 0, sizeof(registers)); accumulated.clear(); diff --git a/firmware/src/remapper.h b/firmware/src/remapper.h index fc6226f0..56424b8c 100644 --- a/firmware/src/remapper.h +++ b/firmware/src/remapper.h @@ -38,6 +38,8 @@ void send_out_report(); bool send_monitor_report(send_report_t do_send_report); void print_stats(); void reset_state(); +uint8_t debug_outgoing_report_count(); +uint32_t debug_outgoing_report_overflows(); void set_monitor_enabled(bool enabled); void monitor_usage(uint32_t usage, int32_t value, uint8_t hub_port); diff --git a/firmware/src/tinyusb_stuff.cc b/firmware/src/tinyusb_stuff.cc index 52a49a44..9485f693 100644 --- a/firmware/src/tinyusb_stuff.cc +++ b/firmware/src/tinyusb_stuff.cc @@ -93,6 +93,12 @@ const uint8_t configuration_descriptor5[] = { TUD_HID_DESCRIPTOR(1, 0, HID_ITF_PROTOCOL_NONE, config_report_descriptor_length, 0x83, CFG_TUD_HID_EP_BUFSIZE, 1), }; +const uint8_t configuration_descriptor6[] = { + TUD_CONFIG_DESCRIPTOR(1, 2, 0, TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN + TUD_HID_DESC_LEN, 0, 100), + TUD_HID_INOUT_DESCRIPTOR(0, 0, HID_ITF_PROTOCOL_NONE, our_descriptors[6].descriptor_length, 0x02, 0x81, CFG_TUD_HID_EP_BUFSIZE, 1), + TUD_HID_DESCRIPTOR(1, 0, HID_ITF_PROTOCOL_NONE, config_report_descriptor_length, 0x83, CFG_TUD_HID_EP_BUFSIZE, 1), +}; + const uint8_t* configuration_descriptors[] = { configuration_descriptor0, configuration_descriptor1, @@ -100,6 +106,7 @@ const uint8_t* configuration_descriptors[] = { configuration_descriptor3, configuration_descriptor4, configuration_descriptor5, + configuration_descriptor6, }; char const* string_desc_arr[] = { diff --git a/firmware/src/types.h b/firmware/src/types.h index 3bfa1be6..f6443e18 100644 --- a/firmware/src/types.h +++ b/firmware/src/types.h @@ -32,6 +32,7 @@ enum class ConfigCommand : int8_t { CLEAR_QUIRKS = 23, ADD_QUIRK = 24, GET_QUIRK = 25, + GET_SWITCH_PRO_DIAG = 26, }; struct usage_def_t { @@ -434,4 +435,8 @@ struct __attribute__((packed)) uint16_val_t { uint16_t val; }; +struct __attribute__((packed)) switch_pro_diag_t { + uint32_t values[7]; +}; + #endif From 5f8bea54a62c399e38461e5e24697362dd2aac94 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 8 Jun 2026 18:54:14 -0700 Subject: [PATCH 2/3] Checkpoint Switch Pro stick debugging state --- config-tool-web/code.js | 158 +++++++++++- config-tool-web/examples.js | 10 + firmware-bluetooth/src/main.cc | 453 +++++++++++++++++++++++++++++++-- firmware/src/remapper.cc | 92 +++++++ firmware/src/remapper.h | 3 + 5 files changed, 691 insertions(+), 25 deletions(-) diff --git a/config-tool-web/code.js b/config-tool-web/code.js index 489a3650..3abf9723 100644 --- a/config-tool-web/code.js +++ b/config-tool-web/code.js @@ -876,11 +876,30 @@ function hex_word(value) { return (value >>> 0).toString(16).padStart(8, '0'); } +function switch_pro_trace_dirs(flags) { + let dirs = ''; + if (flags & 1) dirs += 'L'; + if (flags & 2) dirs += 'R'; + if (flags & 4) dirs += 'U'; + if (flags & 8) dirs += 'D'; + return dirs || '-'; +} + +function format_parsed_axis(label, page) { + return label + ' seq=' + page[0] + + ' usage=0x' + hex_word(page[6]) + + ' raw=' + page[1] + ' scaled=' + page[2] + + ' logical=' + page[3] + '..' + page[4] + + ' report_id=0x' + hex_byte(page[5] >> 24) + + ' bitpos=' + (page[5] & 0xffff) + + ' size=' + ((page[5] >> 16) & 0xff); +} + async function switch_pro_diag() { clear_error(); try { const pages = []; - for (let page = 0; page < 10; page++) { + for (let page = 0; page < 82; page++) { await send_feature_command(GET_SWITCH_PRO_DIAG, [[UINT32, page]]); pages.push(await read_config_feature([UINT32, UINT32, UINT32, UINT32, UINT32, UINT32, UINT32])); } @@ -891,6 +910,14 @@ async function switch_pro_diag() { const p2 = pages[offset + 2]; const p3 = pages[offset + 3]; const p4 = pages[offset + 4]; + const p5 = pages[offset + 5]; + const p6 = pages[offset + 6]; + const p7 = pages[offset + 7]; + const p8 = pages[offset + 8]; + const p9 = pages[offset + 9]; + const p10 = pages[offset + 10]; + const p11 = pages[offset + 11]; + const p12 = pages[offset + 12]; const buttons = p3[0]; const left_stick = p3[1]; const right_stick = p3[2]; @@ -899,8 +926,19 @@ async function switch_pro_diag() { const bt_disconnected = p3[6] & 0xffff; const last_disconnect = (p3[6] >>> 16) & 0xff; const conn_high = p3[6] >>> 24; - - return [ + const left_out = p5[0]; + const right_out = p5[1]; + const hb_left_run = p6[4] & 0xffff; + const hb_left_run_max = p6[4] >>> 16; + const hb_up_run = p6[5] & 0xffff; + const hb_up_run_max = p6[5] >>> 16; + const hb_last_left = p6[6]; + const source_meta = p7[0]; + const source_iface = source_meta & 0xffff; + const source_report_id = (source_meta >>> 16) & 0xff; + const source_len = source_meta >>> 24; + + const lines = [ label, 'magic=0x' + hex_word(p0[0]) + ' version=' + p0[1] + ' enabled=' + p0[2], 'report_q=' + (p0[3] & 0xffff) + ' high=' + (p0[3] >>> 16) + @@ -926,14 +964,126 @@ async function switch_pro_diag() { ' ly=0x' + (p4[3] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[3] >>> 16).toString(16).padStart(4, '0') + ' rx=0x' + (p4[4] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[4] >>> 16).toString(16).padStart(4, '0') + ' ry=0x' + (p4[5] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[5] >>> 16).toString(16).padStart(4, '0'), + 'out_last left=' + hex_byte(left_out) + ' ' + hex_byte(left_out >> 8) + ' ' + hex_byte(left_out >> 16) + + ' right=' + hex_byte(right_out) + ' ' + hex_byte(right_out >> 8) + ' ' + hex_byte(right_out >> 16), + 'out_range left_b0=' + hex_byte(p5[2]) + '..' + hex_byte(p5[2] >> 8) + + ' left_b1=' + hex_byte(p5[2] >> 16) + '..' + hex_byte(p5[2] >> 24) + + ' left_b2=' + hex_byte(p5[3]) + '..' + hex_byte(p5[3] >> 8) + + ' right_b0=' + hex_byte(p5[3] >> 16) + '..' + hex_byte(p5[3] >> 24) + + ' right_b1=' + hex_byte(p5[4]) + '..' + hex_byte(p5[4] >> 8) + + ' right_b2=' + hex_byte(p5[4] >> 16) + '..' + hex_byte(p5[4] >> 24), + 'heartbeat_dir left=' + p6[0] + ' up=' + p6[1] + ' right=' + p6[2] + ' down=' + p6[3] + + ' left_run=' + hb_left_run + '/' + hb_left_run_max + + ' up_run=' + hb_up_run + '/' + hb_up_run_max + + ' hb_last_left=' + hex_byte(hb_last_left) + ' ' + hex_byte(hb_last_left >> 8) + ' ' + hex_byte(hb_last_left >> 16), + 'source_last iface=0x' + source_iface.toString(16).padStart(4, '0') + + ' report_id=0x' + hex_byte(source_report_id) + ' len=' + source_len + + ' ms=' + p7[5] + + ' data=' + [p7[1], p7[2], p7[3], p7[4]].map((word) => + hex_byte(word) + ' ' + hex_byte(word >> 8) + ' ' + hex_byte(word >> 16) + ' ' + hex_byte(word >> 24) + ).join(' | '), + 'up_snap_candidates=' + p7[6], + 'state_axes scaled/raw lx=' + (p8[0] & 0xffff) + '/0x' + (p8[0] >>> 16).toString(16).padStart(4, '0') + + ' ly=' + (p8[1] & 0xffff) + '/0x' + (p8[1] >>> 16).toString(16).padStart(4, '0') + + ' rx=' + (p8[2] & 0xffff) + '/0x' + (p8[2] >>> 16).toString(16).padStart(4, '0') + + ' ry=' + (p8[3] & 0xffff) + '/0x' + (p8[3] >>> 16).toString(16).padStart(4, '0') + + ' found=0x' + p8[4].toString(16), + format_parsed_axis('parsed_lx', p9), + format_parsed_axis('parsed_ly', p10), + format_parsed_axis('parsed_rx', p11), + format_parsed_axis('parsed_ry', p12), ]; + + lines.push('parsed left-Y history newest first:'); + for (let i = 0; i < 8; i++) { + const axis = pages[offset + 13 + i]; + if (axis[0] === 0) { + continue; + } + lines.push(format_parsed_axis('parsed_ly[' + i + ']', axis)); + } + + lines.push('trace newest first:'); + for (let i = 0; i < 8; i++) { + const trace = pages[offset + 21 + i]; + const seq = trace[0]; + if (seq === 0) { + continue; + } + const rawLx = trace[1] & 0xffff; + const rawLy = trace[1] >>> 16; + const normLx = trace[2] & 0xffff; + const normLy = trace[2] >>> 16; + const finalLx = trace[3] & 0xffff; + const finalLy = trace[3] >>> 16; + const out = trace[4]; + const flags = trace[5]; + lines.push( + 'trace[' + i + '] seq=' + seq + + ' raw=' + rawLx.toString(16).padStart(4, '0') + ',' + rawLy.toString(16).padStart(4, '0') + + ' norm=' + normLx.toString(16).padStart(4, '0') + ',' + normLy.toString(16).padStart(4, '0') + + ' final=' + finalLx.toString(16).padStart(4, '0') + ',' + finalLy.toString(16).padStart(4, '0') + + ' out=' + hex_byte(out) + ' ' + hex_byte(out >> 8) + ' ' + hex_byte(out >> 16) + + ' dirs=' + switch_pro_trace_dirs(flags) + ' flags=0x' + flags.toString(16) + ); + } + + lines.push('extreme snapshots:'); + const extremeNames = ['left', 'right', 'forward', 'back']; + for (let i = 0; i < 4; i++) { + const trace = pages[offset + 29 + i]; + const seq = trace[0]; + if (seq === 0) { + continue; + } + const rawLx = trace[1] & 0xffff; + const rawLy = trace[1] >>> 16; + const normLx = trace[2] & 0xffff; + const normLy = trace[2] >>> 16; + const finalLx = trace[3] & 0xffff; + const finalLy = trace[3] >>> 16; + const out = trace[4]; + const flags = trace[5]; + lines.push( + 'extreme[' + extremeNames[i] + '] seq=' + seq + + ' raw=' + rawLx.toString(16).padStart(4, '0') + ',' + rawLy.toString(16).padStart(4, '0') + + ' norm=' + normLx.toString(16).padStart(4, '0') + ',' + normLy.toString(16).padStart(4, '0') + + ' final=' + finalLx.toString(16).padStart(4, '0') + ',' + finalLy.toString(16).padStart(4, '0') + + ' out=' + hex_byte(out) + ' ' + hex_byte(out >> 8) + ' ' + hex_byte(out >> 16) + + ' dirs=' + switch_pro_trace_dirs(flags) + ' flags=0x' + flags.toString(16) + ); + } + + lines.push('source trace newest first:'); + for (let i = 0; i < 8; i++) { + const trace = pages[offset + 33 + i]; + const seq = trace[0]; + if (seq === 0) { + continue; + } + const meta = trace[1]; + const iface = meta & 0xffff; + const reportId = (meta >>> 16) & 0xff; + const len = meta >>> 24; + lines.push( + 'source[' + i + '] seq=' + seq + + ' iface=0x' + iface.toString(16).padStart(4, '0') + + ' report_id=0x' + hex_byte(reportId) + ' len=' + len + + ' ms=' + trace[6] + + ' data=' + [trace[2], trace[3], trace[4], trace[5]].map((word) => + hex_byte(word) + ' ' + hex_byte(word >> 8) + ' ' + hex_byte(word >> 16) + ' ' + hex_byte(word >> 24) + ).join(' | ') + ); + } + + return lines; }; const lines = [ 'Switch Pro diagnostics', ...format_pages('live', 0), '', - ...format_pages('saved', 5), + ...format_pages('saved', 41), '', 'raw=' + pages.map((page) => page.map((value) => '0x' + hex_word(value)).join(' ')).join(' | '), ]; diff --git a/config-tool-web/examples.js b/config-tool-web/examples.js index c2db7bb1..903fc1c8 100644 --- a/config-tool-web/examples.js +++ b/config-tool-web/examples.js @@ -10074,6 +10074,16 @@ if (xbox_bluetooth_switch_idx >= 0) { const switch_pro_example = JSON.parse(JSON.stringify(examples[xbox_bluetooth_switch_idx])); switch_pro_example.description = 'Xbox controller (Bluetooth) adapter for Switch Pro Controller'; switch_pro_example.config.our_descriptor_number = 6; + for (const mapping of switch_pro_example.config.mappings) { + if ([ + '0x00010030', + '0x00010031', + '0x00010032', + '0x00010035', + ].includes(mapping.source_usage) && mapping.source_usage === mapping.target_usage) { + mapping.scaling = 257000; + } + } examples.splice(xbox_bluetooth_switch_idx + 1, 0, switch_pro_example); } diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 9141c9f2..b1c33501 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -143,15 +143,67 @@ static uint32_t switch_pro_bt_disconnected_events = 0; static uint32_t switch_pro_hogp_ready_events = 0; static uint32_t switch_pro_conn_count_highwater = 0; static uint32_t switch_pro_last_disconnect_reason = 0; - -#define SWITCH_PRO_DIAG_PAGES 5 +static uint32_t switch_pro_hb_left = 0; +static uint32_t switch_pro_hb_up = 0; +static uint32_t switch_pro_hb_right = 0; +static uint32_t switch_pro_hb_down = 0; +static uint32_t switch_pro_hb_left_run = 0; +static uint32_t switch_pro_hb_left_run_max = 0; +static uint32_t switch_pro_hb_up_run = 0; +static uint32_t switch_pro_hb_up_run_max = 0; +static uint32_t switch_pro_hb_last_left = 0x00800800; +static uint32_t switch_pro_source_meta = 0; +static uint32_t switch_pro_source_words[4] = {}; +static uint32_t switch_pro_source_ms = 0; +static bool switch_pro_snap_latched = false; +static bool switch_pro_snap_save_pending = false; +static uint32_t switch_pro_last_trace_flags = 0; +static uint32_t switch_pro_up_snap_candidates = 0; + +#define SWITCH_PRO_TRACE_FRAMES 8 +#define SWITCH_PRO_EXTREME_FRAMES 4 +#define SWITCH_PRO_SOURCE_TRACE_FRAMES 8 +#define SWITCH_PRO_PARSED_AXIS_HISTORY_PAGES 8 +#define SWITCH_PRO_DIAG_BASE_PAGES 21 +#define SWITCH_PRO_DIAG_PAGES (SWITCH_PRO_DIAG_BASE_PAGES + SWITCH_PRO_TRACE_FRAMES + SWITCH_PRO_EXTREME_FRAMES + SWITCH_PRO_SOURCE_TRACE_FRAMES) #define SWITCH_PRO_DIAG_VALUES 7 +struct switch_pro_trace_frame_t { + uint32_t seq; + uint16_t raw_lx; + uint16_t raw_ly; + uint16_t norm_lx; + uint16_t norm_ly; + uint16_t final_lx; + uint16_t final_ly; + uint32_t out_left; + uint32_t flags; +}; + +struct switch_pro_source_trace_frame_t { + uint32_t seq; + uint32_t meta; + uint32_t words[4]; + uint32_t ms; +}; + static uint32_t switch_pro_saved_diag[SWITCH_PRO_DIAG_PAGES][SWITCH_PRO_DIAG_VALUES]; static int64_t switch_pro_last_diag_persist_ms = 0; static uint16_t switch_pro_axis_last[4] = { 0x8000, 0x8000, 0x8000, 0x8000 }; static uint16_t switch_pro_axis_min[4] = { 0xffff, 0xffff, 0xffff, 0xffff }; static uint16_t switch_pro_axis_max[4] = { 0, 0, 0, 0 }; +static uint32_t switch_pro_output_stick_last[2] = { 0x00800800, 0x00800800 }; +static uint8_t switch_pro_output_stick_min[2][3] = { { 0xff, 0xff, 0xff }, { 0xff, 0xff, 0xff } }; +static uint8_t switch_pro_output_stick_max[2][3] = { { 0, 0, 0 }, { 0, 0, 0 } }; +static switch_pro_trace_frame_t switch_pro_trace[SWITCH_PRO_TRACE_FRAMES]; +static switch_pro_trace_frame_t switch_pro_extreme[SWITCH_PRO_EXTREME_FRAMES]; +static uint32_t switch_pro_trace_seq = 0; +static uint8_t switch_pro_trace_write = 0; +static switch_pro_source_trace_frame_t switch_pro_source_trace[SWITCH_PRO_SOURCE_TRACE_FRAMES]; +static uint32_t switch_pro_source_trace_seq = 0; +static uint8_t switch_pro_source_trace_write = 0; + +static void switch_pro_capture_saved_diagnostics(); static bool is_switch_pro_mode() { return our_descriptor_number == 6; @@ -177,6 +229,35 @@ static void switch_pro_reset_axis_diagnostics() { switch_pro_axis_min[axis] = 0xffff; switch_pro_axis_max[axis] = 0; } + for (uint8_t stick = 0; stick < 2; stick++) { + switch_pro_output_stick_last[stick] = 0x00800800; + for (uint8_t byte = 0; byte < 3; byte++) { + switch_pro_output_stick_min[stick][byte] = 0xff; + switch_pro_output_stick_max[stick][byte] = 0; + } + } + switch_pro_hb_left = 0; + switch_pro_hb_up = 0; + switch_pro_hb_right = 0; + switch_pro_hb_down = 0; + switch_pro_hb_left_run = 0; + switch_pro_hb_left_run_max = 0; + switch_pro_hb_up_run = 0; + switch_pro_hb_up_run_max = 0; + switch_pro_hb_last_left = 0x00800800; + switch_pro_source_meta = 0; + memset(switch_pro_source_words, 0, sizeof(switch_pro_source_words)); + switch_pro_source_ms = 0; + memset(switch_pro_trace, 0, sizeof(switch_pro_trace)); + memset(switch_pro_extreme, 0, sizeof(switch_pro_extreme)); + memset(switch_pro_source_trace, 0, sizeof(switch_pro_source_trace)); + switch_pro_trace_seq = 0; + switch_pro_trace_write = 0; + switch_pro_source_trace_seq = 0; + switch_pro_source_trace_write = 0; + switch_pro_snap_latched = false; + switch_pro_snap_save_pending = false; + switch_pro_last_trace_flags = 0; } static void switch_pro_reset_session() { @@ -219,7 +300,7 @@ static uint16_t report_axis_16(const uint8_t* report, size_t offset) { static uint16_t switch_pro_apply_axis_deadzone(uint16_t value) { const uint16_t center = 0x8000; - const uint16_t threshold = 0x0800; + const uint16_t threshold = 0x0100; if (value >= center - threshold && value <= center + threshold) { return center; @@ -246,6 +327,21 @@ static uint16_t switch_pro_normalize_axis(uint16_t value, bool invert) { return switch_pro_apply_axis_deadzone(value); } +static uint16_t switch_pro_axis_from_state_or_report(uint32_t usage, uint16_t report_value) { + bool found = false; + int32_t value = debug_input_state(usage, 0, false, &found); + if (!found) { + return report_value; + } + if (value < 0) { + return 0; + } + if (value > 255) { + value = 255; + } + return (uint16_t) (value * 0x0101); +} + static void switch_pro_note_axis(uint8_t axis, uint16_t value) { switch_pro_axis_last[axis] = value; if (value < switch_pro_axis_min[axis]) { @@ -256,11 +352,27 @@ static void switch_pro_note_axis(uint8_t axis, uint16_t value) { } } +static uint16_t switch_pro_clamp_axis_range(uint16_t value) { + const uint16_t min = 0x2000; + const uint16_t max = 0xe000; + + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +} + static void pack_switch_pro_stick(uint8_t* out, uint16_t x16, uint16_t y16) { /* - * The Switch expects 12-bit packed axes. Values passed here are already - * normalized to the 16-bit HID Remapper output range. + * Report 0x30 uses packed 12-bit stick positions. This matches the + * wired capture in the GBATemp thread: report, timer, status, buttons, + * then two 3-byte stick fields. */ + x16 = switch_pro_clamp_axis_range(x16); + y16 = switch_pro_clamp_axis_range(y16); uint16_t x = x16 >> 4; uint16_t y = y16 >> 4; @@ -269,6 +381,151 @@ static void pack_switch_pro_stick(uint8_t* out, uint16_t x16, uint16_t y16) { out[2] = (y >> 4) & 0xff; } +static void switch_pro_note_output_stick(uint8_t stick, const uint8_t* bytes) { + switch_pro_output_stick_last[stick] = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16); + for (uint8_t byte = 0; byte < 3; byte++) { + if (bytes[byte] < switch_pro_output_stick_min[stick][byte]) { + switch_pro_output_stick_min[stick][byte] = bytes[byte]; + } + if (bytes[byte] > switch_pro_output_stick_max[stick][byte]) { + switch_pro_output_stick_max[stick][byte] = bytes[byte]; + } + } +} + +static void switch_pro_note_trace(uint16_t raw_lx, uint16_t raw_ly, uint16_t norm_lx, uint16_t norm_ly, + uint16_t final_lx, uint16_t final_ly, const uint8_t* out_left, uint32_t flags) { + switch_pro_trace_frame_t* frame = &switch_pro_trace[switch_pro_trace_write]; + frame->seq = ++switch_pro_trace_seq; + frame->raw_lx = raw_lx; + frame->raw_ly = raw_ly; + frame->norm_lx = norm_lx; + frame->norm_ly = norm_ly; + frame->final_lx = final_lx; + frame->final_ly = final_ly; + frame->out_left = out_left[0] | (out_left[1] << 8) | (out_left[2] << 16); + frame->flags = flags; + + if (switch_pro_extreme[0].seq == 0 || frame->norm_lx < switch_pro_extreme[0].norm_lx) { + switch_pro_extreme[0] = *frame; + } + if (switch_pro_extreme[1].seq == 0 || frame->norm_lx > switch_pro_extreme[1].norm_lx) { + switch_pro_extreme[1] = *frame; + } + if (switch_pro_extreme[2].seq == 0 || frame->norm_ly > switch_pro_extreme[2].norm_ly) { + switch_pro_extreme[2] = *frame; + } + if (switch_pro_extreme[3].seq == 0 || frame->norm_ly < switch_pro_extreme[3].norm_ly) { + switch_pro_extreme[3] = *frame; + } + + switch_pro_trace_write = (switch_pro_trace_write + 1) % SWITCH_PRO_TRACE_FRAMES; + + bool was_up = (switch_pro_last_trace_flags & BIT(2)) != 0; + bool is_up = (flags & BIT(2)) != 0; + bool state_found = false; + int32_t source_y = debug_input_state(0x00010031, 0, false, &state_found); + bool source_still_up = state_found && source_y <= 16; + bool real_input_seen = switch_pro_input_enabled && + (switch_pro_ble_reports > 8) && + (switch_pro_source_ms != 0) && + (switch_pro_translated_reports > 8); + if (real_input_seen && was_up && !is_up) { + switch_pro_up_snap_candidates++; + } + if (!switch_pro_snap_latched && real_input_seen && was_up && !is_up && source_still_up) { + switch_pro_snap_latched = true; + switch_pro_snap_save_pending = true; + switch_pro_capture_saved_diagnostics(); + LOG_INF("switch_pro_snap_latched seq=%u raw=%04x,%04x norm=%04x,%04x out=%06x flags=0x%08x source_y=%d", + frame->seq, frame->raw_lx, frame->raw_ly, frame->norm_lx, frame->norm_ly, frame->out_left, frame->flags, source_y); + } + switch_pro_last_trace_flags = flags; +} + +static uint16_t unpack_switch_pro_x(const uint8_t* bytes) { + return bytes[0] | ((bytes[1] & 0x0f) << 8); +} + +static uint16_t unpack_switch_pro_y(const uint8_t* bytes) { + return ((bytes[1] >> 4) & 0x0f) | (bytes[2] << 4); +} + +static void switch_pro_note_heartbeat_output() { + const uint8_t* left = switch_pro_current_input + 6; + uint16_t x = unpack_switch_pro_x(left); + uint16_t y = unpack_switch_pro_y(left); + bool left_active = x <= 0x500; + bool right_active = x >= 0xb00; + bool up_active = y >= 0xb00; + bool down_active = y <= 0x500; + + switch_pro_hb_last_left = left[0] | (left[1] << 8) | (left[2] << 16); + if (left_active) { + switch_pro_hb_left++; + switch_pro_hb_left_run++; + if (switch_pro_hb_left_run > switch_pro_hb_left_run_max) { + switch_pro_hb_left_run_max = switch_pro_hb_left_run; + } + } else { + switch_pro_hb_left_run = 0; + } + if (up_active) { + switch_pro_hb_up++; + switch_pro_hb_up_run++; + if (switch_pro_hb_up_run > switch_pro_hb_up_run_max) { + switch_pro_hb_up_run_max = switch_pro_hb_up_run; + } + } else { + switch_pro_hb_up_run = 0; + } + if (right_active) { + switch_pro_hb_right++; + } + if (down_active) { + switch_pro_hb_down++; + } +} + +static void switch_pro_note_source_report(uint16_t interface, uint8_t report_id, uint8_t len, const uint8_t* data) { + switch_pro_source_meta = (interface & 0xffff) | ((uint32_t) report_id << 16) | ((uint32_t) len << 24); + memset(switch_pro_source_words, 0, sizeof(switch_pro_source_words)); + for (uint8_t i = 0; i < MIN(len, (uint8_t) 16); i++) { + switch_pro_source_words[i / 4] |= (uint32_t) data[i] << (8 * (i % 4)); + } + switch_pro_source_ms = k_uptime_get_32(); + + switch_pro_source_trace_frame_t* frame = &switch_pro_source_trace[switch_pro_source_trace_write]; + frame->seq = ++switch_pro_source_trace_seq; + frame->meta = switch_pro_source_meta; + memcpy(frame->words, switch_pro_source_words, sizeof(frame->words)); + frame->ms = switch_pro_source_ms; + switch_pro_source_trace_write = (switch_pro_source_trace_write + 1) % SWITCH_PRO_SOURCE_TRACE_FRAMES; + + LOG_INF("switch_pro_source seq=%u iface=0x%04x rid=0x%02x len=%u data=%08x %08x %08x %08x", + frame->seq, interface, report_id, len, frame->words[0], frame->words[1], frame->words[2], frame->words[3]); +} + +static uint32_t switch_pro_output_direction_flags(const uint8_t* left) { + uint16_t x = unpack_switch_pro_x(left); + uint16_t y = unpack_switch_pro_y(left); + uint32_t flags = 0; + + if (x <= 0x500) { + flags |= BIT(0); // left + } + if (x >= 0xb00) { + flags |= BIT(1); // right + } + if (y >= 0xb00) { + flags |= BIT(2); // up/forward after source Y inversion + } + if (y <= 0x500) { + flags |= BIT(3); // down/back + } + return flags; +} + static void apply_switch_pro_hat(uint8_t hat, uint8_t* buttons2) { if (hat > 7) { return; @@ -317,16 +574,27 @@ static void switch_pro_translate_report(const uint8_t* report_with_id, uint8_t l if (report_button_pressed(report_with_id, 7)) next[5] |= BIT(7); // ZL apply_switch_pro_hat(report_with_id[11] & 0x0f, &next[5]); - uint16_t lx = switch_pro_normalize_axis(report_axis_16(report_with_id, 3), false); - uint16_t ly = switch_pro_normalize_axis(report_axis_16(report_with_id, 5), true); - uint16_t rx = switch_pro_normalize_axis(report_axis_16(report_with_id, 7), false); - uint16_t ry = switch_pro_normalize_axis(report_axis_16(report_with_id, 9), true); + uint16_t raw_lx = switch_pro_axis_from_state_or_report(0x00010030, report_axis_16(report_with_id, 3)); + uint16_t raw_ly = switch_pro_axis_from_state_or_report(0x00010031, report_axis_16(report_with_id, 5)); + uint16_t lx = switch_pro_normalize_axis(raw_lx, false); + uint16_t ly = switch_pro_normalize_axis(raw_ly, true); + uint16_t rx = switch_pro_normalize_axis( + switch_pro_axis_from_state_or_report(0x00010032, report_axis_16(report_with_id, 7)), false); + uint16_t ry = switch_pro_normalize_axis( + switch_pro_axis_from_state_or_report(0x00010035, report_axis_16(report_with_id, 9)), true); switch_pro_note_axis(0, lx); switch_pro_note_axis(1, ly); switch_pro_note_axis(2, rx); switch_pro_note_axis(3, ry); + uint16_t norm_lx = lx; + uint16_t norm_ly = ly; pack_switch_pro_stick(next + 6, lx, ly); pack_switch_pro_stick(next + 9, rx, ry); + uint32_t trace_flags = switch_pro_output_direction_flags(next + 6); + switch_pro_note_output_stick(0, next + 6); + switch_pro_note_output_stick(1, next + 9); + switch_pro_note_trace(raw_lx, raw_ly, norm_lx, norm_ly, lx, ly, next + 6, trace_flags); + next[12] = 0x0c; bool buttons_changed = next[3] != switch_pro_current_input[3] || next[4] != switch_pro_current_input[4] || next[5] != switch_pro_current_input[5]; @@ -395,21 +663,47 @@ static void switch_pro_spi_read(const uint8_t* args, uint8_t args_len) { uint8_t read_len = args[4]; memcpy(data, args, 5); - static const uint8_t stick_cal[] = { - 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, - 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80 + /* + * Switch stick calibration is three packed 12-bit pairs per stick. For + * the left stick the groups are max-above-center, center, min-below-center; + * for the right stick they are center, min-below-center, max-above-center. + * Use center 0x800 and +/-0x600 travel so held low-side directions are + * inside the advertised calibrated range. + */ + static const uint8_t left_stick_cal[] = { + 0x00, 0x06, 0x60, // X/Y max above center: 0x600, 0x600 + 0x00, 0x08, 0x80, // X/Y center: 0x800, 0x800 + 0x00, 0x06, 0x60, // X/Y min below center: 0x600, 0x600 + }; + static const uint8_t right_stick_cal[] = { + 0x00, 0x08, 0x80, // X/Y center: 0x800, 0x800 + 0x00, 0x06, 0x60, // X/Y min below center: 0x600, 0x600 + 0x00, 0x06, 0x60, // X/Y max above center: 0x600, 0x600 + }; + static const uint8_t stick_params[] = { + 0x19, 0xd0, 0x4c, 0xae, 0x40, 0xe1, + 0xee, 0xe2, 0x2e, 0xee, 0xe2, 0x2e, + 0xb4, 0x4a, 0xab, 0x96, 0x64, 0x49, }; static const uint8_t body_color[] = { 0x46, 0x46, 0x46 }; static const uint8_t button_color[] = { 0xff, 0xff, 0xff }; + static const uint8_t grip_color[] = { 0x46, 0x46, 0x46 }; if (address == 0x0000603d) { + uint8_t stick_cal[sizeof(left_stick_cal) + sizeof(right_stick_cal)]; + memcpy(stick_cal, left_stick_cal, sizeof(left_stick_cal)); + memcpy(stick_cal + sizeof(left_stick_cal), right_stick_cal, sizeof(right_stick_cal)); memcpy(data + 5, stick_cal, MIN(read_len, (uint8_t) sizeof(stick_cal))); + } else if (address == 0x00006046) { + memcpy(data + 5, right_stick_cal, MIN(read_len, (uint8_t) sizeof(right_stick_cal))); } else if (address == 0x00006050) { - memcpy(data + 5, stick_cal, MIN(read_len, (uint8_t) sizeof(stick_cal))); - } else if (address == 0x00006086) { memcpy(data + 5, body_color, MIN(read_len, (uint8_t) sizeof(body_color))); - } else if (address == 0x00006089) { + } else if (address == 0x00006053) { memcpy(data + 5, button_color, MIN(read_len, (uint8_t) sizeof(button_color))); + } else if (address == 0x00006056 || address == 0x00006059) { + memcpy(data + 5, grip_color, MIN(read_len, (uint8_t) sizeof(grip_color))); + } else if (address == 0x00006086 || address == 0x00006098) { + memcpy(data + 5, stick_params, MIN(read_len, (uint8_t) sizeof(stick_params))); } switch_pro_queue_21(0x10, 0x90, data, MIN((uint8_t) sizeof(data), (uint8_t) (read_len + 5))); @@ -528,6 +822,7 @@ static bool switch_pro_send_input_heartbeat() { bool sent = CHK(hid_int_ep_write(hid_dev0, switch_pro_current_input, sizeof(switch_pro_current_input), NULL)); if (sent) { switch_pro_heartbeat_writes++; + switch_pro_note_heartbeat_output(); } else { switch_pro_heartbeat_write_fails++; } @@ -536,6 +831,7 @@ static bool switch_pro_send_input_heartbeat() { static void switch_pro_fill_diagnostics(uint32_t page, uint32_t values[SWITCH_PRO_DIAG_VALUES]) { memset(values, 0, SWITCH_PRO_DIAG_VALUES * sizeof(uint32_t)); + static const uint32_t axis_usages[4] = { 0x00010030, 0x00010031, 0x00010032, 0x00010035 }; switch (page) { case 0: @@ -582,11 +878,115 @@ static void switch_pro_fill_diagnostics(uint32_t page, uint32_t values[SWITCH_PR values[4] = switch_pro_axis_min[2] | (switch_pro_axis_max[2] << 16); values[5] = switch_pro_axis_min[3] | (switch_pro_axis_max[3] << 16); break; + case 5: + values[0] = switch_pro_output_stick_last[0]; + values[1] = switch_pro_output_stick_last[1]; + values[2] = switch_pro_output_stick_min[0][0] | (switch_pro_output_stick_max[0][0] << 8) | + (switch_pro_output_stick_min[0][1] << 16) | (switch_pro_output_stick_max[0][1] << 24); + values[3] = switch_pro_output_stick_min[0][2] | (switch_pro_output_stick_max[0][2] << 8) | + (switch_pro_output_stick_min[1][0] << 16) | (switch_pro_output_stick_max[1][0] << 24); + values[4] = switch_pro_output_stick_min[1][1] | (switch_pro_output_stick_max[1][1] << 8) | + (switch_pro_output_stick_min[1][2] << 16) | (switch_pro_output_stick_max[1][2] << 24); + break; + case 6: + values[0] = switch_pro_hb_left; + values[1] = switch_pro_hb_up; + values[2] = switch_pro_hb_right; + values[3] = switch_pro_hb_down; + values[4] = switch_pro_hb_left_run | (switch_pro_hb_left_run_max << 16); + values[5] = switch_pro_hb_up_run | (switch_pro_hb_up_run_max << 16); + values[6] = switch_pro_hb_last_left; + break; + case 7: + values[0] = switch_pro_source_meta; + values[1] = switch_pro_source_words[0]; + values[2] = switch_pro_source_words[1]; + values[3] = switch_pro_source_words[2]; + values[4] = switch_pro_source_words[3]; + values[5] = switch_pro_source_ms; + values[6] = switch_pro_up_snap_candidates; + break; + case 8: + for (uint8_t i = 0; i < 4; i++) { + bool scaled_found = false; + bool raw_found = false; + int32_t scaled = debug_input_state(axis_usages[i], 0, false, &scaled_found); + int32_t raw = debug_input_state(axis_usages[i], 0, true, &raw_found); + values[i] = ((uint32_t) (uint16_t) raw << 16) | (uint16_t) scaled; + if (scaled_found) { + values[4] |= BIT(i); + } + if (raw_found) { + values[4] |= BIT(i + 8); + } + } + break; + case 9: + debug_parsed_input(axis_usages[0], values); + break; + case 10: + debug_parsed_input(axis_usages[1], values); + break; + case 11: + debug_parsed_input(axis_usages[2], values); + break; + case 12: + debug_parsed_input(axis_usages[3], values); + break; default: + if (page >= 13 && page < 13 + SWITCH_PRO_PARSED_AXIS_HISTORY_PAGES) { + debug_parsed_axis_history(1, page - 13, values); + break; + } + if (page >= SWITCH_PRO_DIAG_BASE_PAGES && page < SWITCH_PRO_DIAG_PAGES) { + uint32_t trace_age = page - SWITCH_PRO_DIAG_BASE_PAGES; + if (trace_age < SWITCH_PRO_TRACE_FRAMES) { + const switch_pro_trace_frame_t* frame; + uint8_t trace_idx = (switch_pro_trace_write + SWITCH_PRO_TRACE_FRAMES - 1 - trace_age) % SWITCH_PRO_TRACE_FRAMES; + frame = &switch_pro_trace[trace_idx]; + values[0] = frame->seq; + values[1] = frame->raw_lx | (frame->raw_ly << 16); + values[2] = frame->norm_lx | (frame->norm_ly << 16); + values[3] = frame->final_lx | (frame->final_ly << 16); + values[4] = frame->out_left; + values[5] = frame->flags; + values[6] = trace_age; + } else if (trace_age < SWITCH_PRO_TRACE_FRAMES + SWITCH_PRO_EXTREME_FRAMES) { + const switch_pro_trace_frame_t* frame; + frame = &switch_pro_extreme[trace_age - SWITCH_PRO_TRACE_FRAMES]; + values[0] = frame->seq; + values[1] = frame->raw_lx | (frame->raw_ly << 16); + values[2] = frame->norm_lx | (frame->norm_ly << 16); + values[3] = frame->final_lx | (frame->final_ly << 16); + values[4] = frame->out_left; + values[5] = frame->flags; + values[6] = trace_age; + } else { + uint32_t source_age = trace_age - SWITCH_PRO_TRACE_FRAMES - SWITCH_PRO_EXTREME_FRAMES; + if (source_age < SWITCH_PRO_SOURCE_TRACE_FRAMES) { + const switch_pro_source_trace_frame_t* frame; + uint8_t trace_idx = (switch_pro_source_trace_write + SWITCH_PRO_SOURCE_TRACE_FRAMES - 1 - source_age) % SWITCH_PRO_SOURCE_TRACE_FRAMES; + frame = &switch_pro_source_trace[trace_idx]; + values[0] = frame->seq; + values[1] = frame->meta; + values[2] = frame->words[0]; + values[3] = frame->words[1]; + values[4] = frame->words[2]; + values[5] = frame->words[3]; + values[6] = frame->ms; + } + } + } break; } } +static void switch_pro_capture_saved_diagnostics() { + for (uint32_t page = 0; page < SWITCH_PRO_DIAG_PAGES; page++) { + switch_pro_fill_diagnostics(page, switch_pro_saved_diag[page]); + } +} + static void switch_pro_persist_diagnostics() { if (!is_switch_pro_mode() || !switch_pro_input_enabled) { return; @@ -596,14 +996,21 @@ static void switch_pro_persist_diagnostics() { } int64_t now = k_uptime_get(); + if (switch_pro_snap_save_pending) { + switch_pro_last_diag_persist_ms = now; + switch_pro_snap_save_pending = false; + settings_save_one("remapper/switch_pro_diag", switch_pro_saved_diag, sizeof(switch_pro_saved_diag)); + return; + } + if (switch_pro_snap_latched) { + return; + } if (switch_pro_last_diag_persist_ms && (now - switch_pro_last_diag_persist_ms < 2000)) { return; } switch_pro_last_diag_persist_ms = now; - for (uint32_t page = 0; page < SWITCH_PRO_DIAG_PAGES; page++) { - switch_pro_fill_diagnostics(page, switch_pro_saved_diag[page]); - } + switch_pro_capture_saved_diagnostics(); settings_save_one("remapper/switch_pro_diag", switch_pro_saved_diag, sizeof(switch_pro_saved_diag)); } @@ -1041,7 +1448,10 @@ static uint8_t hogp_notify_cb(struct bt_hogp* hogp, struct bt_hogp_rep_info* rep buf.len = bt_hogp_rep_size(rep) + 1; buf.data[0] = bt_hogp_rep_id(rep); - memcpy(buf.data + 1, data, buf.len); + memcpy(buf.data + 1, data, buf.len - 1); + if (is_switch_pro_mode()) { + switch_pro_note_source_report(buf.interface, buf.data[0], buf.len, buf.data); + } if (k_msgq_put(&report_q, &buf, K_NO_WAIT)) { if (is_switch_pro_mode()) { switch_pro_ble_report_drops++; @@ -1383,15 +1793,16 @@ static int remapper_settings_set(const char* name, size_t len, settings_read_cb LOG_INF("%s len=%d", name, len); if (!strcmp(name, "switch_pro_diag")) { - if (len != sizeof(switch_pro_saved_diag)) { + if (len > sizeof(switch_pro_saved_diag) || (len % (SWITCH_PRO_DIAG_VALUES * sizeof(uint32_t))) != 0) { return -EINVAL; } + memset(switch_pro_saved_diag, 0, sizeof(switch_pro_saved_diag)); int bytes_read = read_cb(cb_arg, switch_pro_saved_diag, len); if (bytes_read < 0) { return bytes_read; } - return bytes_read == sizeof(switch_pro_saved_diag) ? 0 : -EINVAL; + return bytes_read == (int) len ? 0 : -EINVAL; } if (strcmp(name, "config")) { diff --git a/firmware/src/remapper.cc b/firmware/src/remapper.cc index 3cfc1bc1..1492bfcb 100644 --- a/firmware/src/remapper.cc +++ b/firmware/src/remapper.cc @@ -112,6 +112,23 @@ uint8_t monitor_usages_queued = 0; monitor_report_t monitor_report[2] = { { .report_id = REPORT_ID_MONITOR }, { .report_id = REPORT_ID_MONITOR } }; uint8_t monitor_report_idx = 0; +struct debug_parsed_input_t { + uint32_t seq; + int32_t raw_value; + int32_t scaled_value; + int32_t logical_minimum; + int32_t logical_maximum; + uint32_t meta; +}; + +#define DEBUG_PARSED_AXIS_COUNT 4 +#define DEBUG_PARSED_AXIS_HISTORY 8 + +std::unordered_map debug_parsed_inputs; +debug_parsed_input_t debug_parsed_axis_history_frames[DEBUG_PARSED_AXIS_COUNT][DEBUG_PARSED_AXIS_HISTORY]; +uint8_t debug_parsed_axis_history_write[DEBUG_PARSED_AXIS_COUNT] = {}; +uint32_t debug_parsed_input_seq = 0; + #define NREGISTERS 32 int32_t registers[NREGISTERS] = { 0 }; std::vector register_ptrs; @@ -361,6 +378,50 @@ inline int32_t* get_state_ptr(uint32_t usage, uint8_t hub_port, bool assign_if_a return NULL; } +int32_t debug_input_state(uint32_t usage, uint8_t hub_port, bool raw, bool* found) { + int32_t* state_ptr = get_state_ptr(usage, hub_port, false, raw); + if (found != NULL) { + *found = state_ptr != NULL; + } + return state_ptr == NULL ? 0 : *state_ptr; +} + +bool debug_parsed_input(uint32_t usage, uint32_t values[7]) { + auto search = debug_parsed_inputs.find(usage); + if (search == debug_parsed_inputs.end()) { + memset(values, 0, 7 * sizeof(uint32_t)); + return false; + } + + const debug_parsed_input_t& parsed = search->second; + values[0] = parsed.seq; + values[1] = (uint32_t) parsed.raw_value; + values[2] = (uint32_t) parsed.scaled_value; + values[3] = (uint32_t) parsed.logical_minimum; + values[4] = (uint32_t) parsed.logical_maximum; + values[5] = parsed.meta; + values[6] = usage; + return true; +} + +bool debug_parsed_axis_history(uint8_t axis, uint8_t age, uint32_t values[7]) { + if (axis >= DEBUG_PARSED_AXIS_COUNT || age >= DEBUG_PARSED_AXIS_HISTORY) { + memset(values, 0, 7 * sizeof(uint32_t)); + return false; + } + + uint8_t idx = (debug_parsed_axis_history_write[axis] + DEBUG_PARSED_AXIS_HISTORY - 1 - age) % DEBUG_PARSED_AXIS_HISTORY; + const debug_parsed_input_t& parsed = debug_parsed_axis_history_frames[axis][idx]; + values[0] = parsed.seq; + values[1] = (uint32_t) parsed.raw_value; + values[2] = (uint32_t) parsed.scaled_value; + values[3] = (uint32_t) parsed.logical_minimum; + values[4] = (uint32_t) parsed.logical_maximum; + values[5] = parsed.meta; + values[6] = age; + return parsed.seq != 0; +} + inline tap_hold_state_t* get_tap_hold_state_ptr(uint32_t usage, uint8_t hub_port, bool assign_if_absent = false) { int32_t* state_ptr = get_state_ptr(usage, hub_port, assign_if_absent); if (state_ptr != NULL) { @@ -1535,6 +1596,37 @@ inline void read_input(const uint8_t* report, int len, uint32_t source_usage, co } else { scaled_value = value; } + if ((source_usage == 0x00010030) || + (source_usage == 0x00010031) || + (source_usage == 0x00010032) || + (source_usage == 0x00010035) || + (source_usage == 0x00010039)) { + debug_parsed_input_t parsed = { + .seq = ++debug_parsed_input_seq, + .raw_value = value, + .scaled_value = scaled_value, + .logical_minimum = their_usage.logical_minimum, + .logical_maximum = their_usage.logical_maximum, + .meta = (uint32_t) their_usage.bitpos | + ((uint32_t) their_usage.size << 16) | + ((uint32_t) their_usage.report_id << 24), + }; + debug_parsed_inputs[source_usage] = parsed; + uint8_t axis = DEBUG_PARSED_AXIS_COUNT; + if (source_usage == 0x00010030) { + axis = 0; + } else if (source_usage == 0x00010031) { + axis = 1; + } else if (source_usage == 0x00010032) { + axis = 2; + } else if (source_usage == 0x00010035) { + axis = 3; + } + if (axis < DEBUG_PARSED_AXIS_COUNT) { + debug_parsed_axis_history_frames[axis][debug_parsed_axis_history_write[axis]] = parsed; + debug_parsed_axis_history_write[axis] = (debug_parsed_axis_history_write[axis] + 1) % DEBUG_PARSED_AXIS_HISTORY; + } + } if (their_usage.input_state_0 != NULL) { if ((their_usage.size == 1) || their_usage.is_array) { if (value) { diff --git a/firmware/src/remapper.h b/firmware/src/remapper.h index 56424b8c..15bbca5a 100644 --- a/firmware/src/remapper.h +++ b/firmware/src/remapper.h @@ -40,6 +40,9 @@ void print_stats(); void reset_state(); uint8_t debug_outgoing_report_count(); uint32_t debug_outgoing_report_overflows(); +int32_t debug_input_state(uint32_t usage, uint8_t hub_port, bool raw, bool* found); +bool debug_parsed_input(uint32_t usage, uint32_t values[7]); +bool debug_parsed_axis_history(uint8_t axis, uint8_t age, uint32_t values[7]); void set_monitor_enabled(bool enabled); void monitor_usage(uint32_t usage, int32_t value, uint8_t hub_port); From 940ac988c1a7a664754d4b862922985cc6335eeb Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 8 Jun 2026 18:56:03 -0700 Subject: [PATCH 3/3] Trim Switch Pro stick diagnostics --- config-tool-web/code.js | 158 +------------ firmware-bluetooth/src/main.cc | 419 ++------------------------------- firmware/src/remapper.cc | 86 +------ firmware/src/remapper.h | 4 +- 4 files changed, 27 insertions(+), 640 deletions(-) diff --git a/config-tool-web/code.js b/config-tool-web/code.js index 3abf9723..489a3650 100644 --- a/config-tool-web/code.js +++ b/config-tool-web/code.js @@ -876,30 +876,11 @@ function hex_word(value) { return (value >>> 0).toString(16).padStart(8, '0'); } -function switch_pro_trace_dirs(flags) { - let dirs = ''; - if (flags & 1) dirs += 'L'; - if (flags & 2) dirs += 'R'; - if (flags & 4) dirs += 'U'; - if (flags & 8) dirs += 'D'; - return dirs || '-'; -} - -function format_parsed_axis(label, page) { - return label + ' seq=' + page[0] + - ' usage=0x' + hex_word(page[6]) + - ' raw=' + page[1] + ' scaled=' + page[2] + - ' logical=' + page[3] + '..' + page[4] + - ' report_id=0x' + hex_byte(page[5] >> 24) + - ' bitpos=' + (page[5] & 0xffff) + - ' size=' + ((page[5] >> 16) & 0xff); -} - async function switch_pro_diag() { clear_error(); try { const pages = []; - for (let page = 0; page < 82; page++) { + for (let page = 0; page < 10; page++) { await send_feature_command(GET_SWITCH_PRO_DIAG, [[UINT32, page]]); pages.push(await read_config_feature([UINT32, UINT32, UINT32, UINT32, UINT32, UINT32, UINT32])); } @@ -910,14 +891,6 @@ async function switch_pro_diag() { const p2 = pages[offset + 2]; const p3 = pages[offset + 3]; const p4 = pages[offset + 4]; - const p5 = pages[offset + 5]; - const p6 = pages[offset + 6]; - const p7 = pages[offset + 7]; - const p8 = pages[offset + 8]; - const p9 = pages[offset + 9]; - const p10 = pages[offset + 10]; - const p11 = pages[offset + 11]; - const p12 = pages[offset + 12]; const buttons = p3[0]; const left_stick = p3[1]; const right_stick = p3[2]; @@ -926,19 +899,8 @@ async function switch_pro_diag() { const bt_disconnected = p3[6] & 0xffff; const last_disconnect = (p3[6] >>> 16) & 0xff; const conn_high = p3[6] >>> 24; - const left_out = p5[0]; - const right_out = p5[1]; - const hb_left_run = p6[4] & 0xffff; - const hb_left_run_max = p6[4] >>> 16; - const hb_up_run = p6[5] & 0xffff; - const hb_up_run_max = p6[5] >>> 16; - const hb_last_left = p6[6]; - const source_meta = p7[0]; - const source_iface = source_meta & 0xffff; - const source_report_id = (source_meta >>> 16) & 0xff; - const source_len = source_meta >>> 24; - - const lines = [ + + return [ label, 'magic=0x' + hex_word(p0[0]) + ' version=' + p0[1] + ' enabled=' + p0[2], 'report_q=' + (p0[3] & 0xffff) + ' high=' + (p0[3] >>> 16) + @@ -964,126 +926,14 @@ async function switch_pro_diag() { ' ly=0x' + (p4[3] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[3] >>> 16).toString(16).padStart(4, '0') + ' rx=0x' + (p4[4] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[4] >>> 16).toString(16).padStart(4, '0') + ' ry=0x' + (p4[5] & 0xffff).toString(16).padStart(4, '0') + '..0x' + (p4[5] >>> 16).toString(16).padStart(4, '0'), - 'out_last left=' + hex_byte(left_out) + ' ' + hex_byte(left_out >> 8) + ' ' + hex_byte(left_out >> 16) + - ' right=' + hex_byte(right_out) + ' ' + hex_byte(right_out >> 8) + ' ' + hex_byte(right_out >> 16), - 'out_range left_b0=' + hex_byte(p5[2]) + '..' + hex_byte(p5[2] >> 8) + - ' left_b1=' + hex_byte(p5[2] >> 16) + '..' + hex_byte(p5[2] >> 24) + - ' left_b2=' + hex_byte(p5[3]) + '..' + hex_byte(p5[3] >> 8) + - ' right_b0=' + hex_byte(p5[3] >> 16) + '..' + hex_byte(p5[3] >> 24) + - ' right_b1=' + hex_byte(p5[4]) + '..' + hex_byte(p5[4] >> 8) + - ' right_b2=' + hex_byte(p5[4] >> 16) + '..' + hex_byte(p5[4] >> 24), - 'heartbeat_dir left=' + p6[0] + ' up=' + p6[1] + ' right=' + p6[2] + ' down=' + p6[3] + - ' left_run=' + hb_left_run + '/' + hb_left_run_max + - ' up_run=' + hb_up_run + '/' + hb_up_run_max + - ' hb_last_left=' + hex_byte(hb_last_left) + ' ' + hex_byte(hb_last_left >> 8) + ' ' + hex_byte(hb_last_left >> 16), - 'source_last iface=0x' + source_iface.toString(16).padStart(4, '0') + - ' report_id=0x' + hex_byte(source_report_id) + ' len=' + source_len + - ' ms=' + p7[5] + - ' data=' + [p7[1], p7[2], p7[3], p7[4]].map((word) => - hex_byte(word) + ' ' + hex_byte(word >> 8) + ' ' + hex_byte(word >> 16) + ' ' + hex_byte(word >> 24) - ).join(' | '), - 'up_snap_candidates=' + p7[6], - 'state_axes scaled/raw lx=' + (p8[0] & 0xffff) + '/0x' + (p8[0] >>> 16).toString(16).padStart(4, '0') + - ' ly=' + (p8[1] & 0xffff) + '/0x' + (p8[1] >>> 16).toString(16).padStart(4, '0') + - ' rx=' + (p8[2] & 0xffff) + '/0x' + (p8[2] >>> 16).toString(16).padStart(4, '0') + - ' ry=' + (p8[3] & 0xffff) + '/0x' + (p8[3] >>> 16).toString(16).padStart(4, '0') + - ' found=0x' + p8[4].toString(16), - format_parsed_axis('parsed_lx', p9), - format_parsed_axis('parsed_ly', p10), - format_parsed_axis('parsed_rx', p11), - format_parsed_axis('parsed_ry', p12), ]; - - lines.push('parsed left-Y history newest first:'); - for (let i = 0; i < 8; i++) { - const axis = pages[offset + 13 + i]; - if (axis[0] === 0) { - continue; - } - lines.push(format_parsed_axis('parsed_ly[' + i + ']', axis)); - } - - lines.push('trace newest first:'); - for (let i = 0; i < 8; i++) { - const trace = pages[offset + 21 + i]; - const seq = trace[0]; - if (seq === 0) { - continue; - } - const rawLx = trace[1] & 0xffff; - const rawLy = trace[1] >>> 16; - const normLx = trace[2] & 0xffff; - const normLy = trace[2] >>> 16; - const finalLx = trace[3] & 0xffff; - const finalLy = trace[3] >>> 16; - const out = trace[4]; - const flags = trace[5]; - lines.push( - 'trace[' + i + '] seq=' + seq + - ' raw=' + rawLx.toString(16).padStart(4, '0') + ',' + rawLy.toString(16).padStart(4, '0') + - ' norm=' + normLx.toString(16).padStart(4, '0') + ',' + normLy.toString(16).padStart(4, '0') + - ' final=' + finalLx.toString(16).padStart(4, '0') + ',' + finalLy.toString(16).padStart(4, '0') + - ' out=' + hex_byte(out) + ' ' + hex_byte(out >> 8) + ' ' + hex_byte(out >> 16) + - ' dirs=' + switch_pro_trace_dirs(flags) + ' flags=0x' + flags.toString(16) - ); - } - - lines.push('extreme snapshots:'); - const extremeNames = ['left', 'right', 'forward', 'back']; - for (let i = 0; i < 4; i++) { - const trace = pages[offset + 29 + i]; - const seq = trace[0]; - if (seq === 0) { - continue; - } - const rawLx = trace[1] & 0xffff; - const rawLy = trace[1] >>> 16; - const normLx = trace[2] & 0xffff; - const normLy = trace[2] >>> 16; - const finalLx = trace[3] & 0xffff; - const finalLy = trace[3] >>> 16; - const out = trace[4]; - const flags = trace[5]; - lines.push( - 'extreme[' + extremeNames[i] + '] seq=' + seq + - ' raw=' + rawLx.toString(16).padStart(4, '0') + ',' + rawLy.toString(16).padStart(4, '0') + - ' norm=' + normLx.toString(16).padStart(4, '0') + ',' + normLy.toString(16).padStart(4, '0') + - ' final=' + finalLx.toString(16).padStart(4, '0') + ',' + finalLy.toString(16).padStart(4, '0') + - ' out=' + hex_byte(out) + ' ' + hex_byte(out >> 8) + ' ' + hex_byte(out >> 16) + - ' dirs=' + switch_pro_trace_dirs(flags) + ' flags=0x' + flags.toString(16) - ); - } - - lines.push('source trace newest first:'); - for (let i = 0; i < 8; i++) { - const trace = pages[offset + 33 + i]; - const seq = trace[0]; - if (seq === 0) { - continue; - } - const meta = trace[1]; - const iface = meta & 0xffff; - const reportId = (meta >>> 16) & 0xff; - const len = meta >>> 24; - lines.push( - 'source[' + i + '] seq=' + seq + - ' iface=0x' + iface.toString(16).padStart(4, '0') + - ' report_id=0x' + hex_byte(reportId) + ' len=' + len + - ' ms=' + trace[6] + - ' data=' + [trace[2], trace[3], trace[4], trace[5]].map((word) => - hex_byte(word) + ' ' + hex_byte(word >> 8) + ' ' + hex_byte(word >> 16) + ' ' + hex_byte(word >> 24) - ).join(' | ') - ); - } - - return lines; }; const lines = [ 'Switch Pro diagnostics', ...format_pages('live', 0), '', - ...format_pages('saved', 41), + ...format_pages('saved', 5), '', 'raw=' + pages.map((page) => page.map((value) => '0x' + hex_word(value)).join(' ')).join(' | '), ]; diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index b1c33501..1ed3e1c5 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -143,67 +143,15 @@ static uint32_t switch_pro_bt_disconnected_events = 0; static uint32_t switch_pro_hogp_ready_events = 0; static uint32_t switch_pro_conn_count_highwater = 0; static uint32_t switch_pro_last_disconnect_reason = 0; -static uint32_t switch_pro_hb_left = 0; -static uint32_t switch_pro_hb_up = 0; -static uint32_t switch_pro_hb_right = 0; -static uint32_t switch_pro_hb_down = 0; -static uint32_t switch_pro_hb_left_run = 0; -static uint32_t switch_pro_hb_left_run_max = 0; -static uint32_t switch_pro_hb_up_run = 0; -static uint32_t switch_pro_hb_up_run_max = 0; -static uint32_t switch_pro_hb_last_left = 0x00800800; -static uint32_t switch_pro_source_meta = 0; -static uint32_t switch_pro_source_words[4] = {}; -static uint32_t switch_pro_source_ms = 0; -static bool switch_pro_snap_latched = false; -static bool switch_pro_snap_save_pending = false; -static uint32_t switch_pro_last_trace_flags = 0; -static uint32_t switch_pro_up_snap_candidates = 0; - -#define SWITCH_PRO_TRACE_FRAMES 8 -#define SWITCH_PRO_EXTREME_FRAMES 4 -#define SWITCH_PRO_SOURCE_TRACE_FRAMES 8 -#define SWITCH_PRO_PARSED_AXIS_HISTORY_PAGES 8 -#define SWITCH_PRO_DIAG_BASE_PAGES 21 -#define SWITCH_PRO_DIAG_PAGES (SWITCH_PRO_DIAG_BASE_PAGES + SWITCH_PRO_TRACE_FRAMES + SWITCH_PRO_EXTREME_FRAMES + SWITCH_PRO_SOURCE_TRACE_FRAMES) -#define SWITCH_PRO_DIAG_VALUES 7 - -struct switch_pro_trace_frame_t { - uint32_t seq; - uint16_t raw_lx; - uint16_t raw_ly; - uint16_t norm_lx; - uint16_t norm_ly; - uint16_t final_lx; - uint16_t final_ly; - uint32_t out_left; - uint32_t flags; -}; -struct switch_pro_source_trace_frame_t { - uint32_t seq; - uint32_t meta; - uint32_t words[4]; - uint32_t ms; -}; +#define SWITCH_PRO_DIAG_PAGES 5 +#define SWITCH_PRO_DIAG_VALUES 7 static uint32_t switch_pro_saved_diag[SWITCH_PRO_DIAG_PAGES][SWITCH_PRO_DIAG_VALUES]; static int64_t switch_pro_last_diag_persist_ms = 0; static uint16_t switch_pro_axis_last[4] = { 0x8000, 0x8000, 0x8000, 0x8000 }; static uint16_t switch_pro_axis_min[4] = { 0xffff, 0xffff, 0xffff, 0xffff }; static uint16_t switch_pro_axis_max[4] = { 0, 0, 0, 0 }; -static uint32_t switch_pro_output_stick_last[2] = { 0x00800800, 0x00800800 }; -static uint8_t switch_pro_output_stick_min[2][3] = { { 0xff, 0xff, 0xff }, { 0xff, 0xff, 0xff } }; -static uint8_t switch_pro_output_stick_max[2][3] = { { 0, 0, 0 }, { 0, 0, 0 } }; -static switch_pro_trace_frame_t switch_pro_trace[SWITCH_PRO_TRACE_FRAMES]; -static switch_pro_trace_frame_t switch_pro_extreme[SWITCH_PRO_EXTREME_FRAMES]; -static uint32_t switch_pro_trace_seq = 0; -static uint8_t switch_pro_trace_write = 0; -static switch_pro_source_trace_frame_t switch_pro_source_trace[SWITCH_PRO_SOURCE_TRACE_FRAMES]; -static uint32_t switch_pro_source_trace_seq = 0; -static uint8_t switch_pro_source_trace_write = 0; - -static void switch_pro_capture_saved_diagnostics(); static bool is_switch_pro_mode() { return our_descriptor_number == 6; @@ -229,35 +177,6 @@ static void switch_pro_reset_axis_diagnostics() { switch_pro_axis_min[axis] = 0xffff; switch_pro_axis_max[axis] = 0; } - for (uint8_t stick = 0; stick < 2; stick++) { - switch_pro_output_stick_last[stick] = 0x00800800; - for (uint8_t byte = 0; byte < 3; byte++) { - switch_pro_output_stick_min[stick][byte] = 0xff; - switch_pro_output_stick_max[stick][byte] = 0; - } - } - switch_pro_hb_left = 0; - switch_pro_hb_up = 0; - switch_pro_hb_right = 0; - switch_pro_hb_down = 0; - switch_pro_hb_left_run = 0; - switch_pro_hb_left_run_max = 0; - switch_pro_hb_up_run = 0; - switch_pro_hb_up_run_max = 0; - switch_pro_hb_last_left = 0x00800800; - switch_pro_source_meta = 0; - memset(switch_pro_source_words, 0, sizeof(switch_pro_source_words)); - switch_pro_source_ms = 0; - memset(switch_pro_trace, 0, sizeof(switch_pro_trace)); - memset(switch_pro_extreme, 0, sizeof(switch_pro_extreme)); - memset(switch_pro_source_trace, 0, sizeof(switch_pro_source_trace)); - switch_pro_trace_seq = 0; - switch_pro_trace_write = 0; - switch_pro_source_trace_seq = 0; - switch_pro_source_trace_write = 0; - switch_pro_snap_latched = false; - switch_pro_snap_save_pending = false; - switch_pro_last_trace_flags = 0; } static void switch_pro_reset_session() { @@ -329,7 +248,7 @@ static uint16_t switch_pro_normalize_axis(uint16_t value, bool invert) { static uint16_t switch_pro_axis_from_state_or_report(uint32_t usage, uint16_t report_value) { bool found = false; - int32_t value = debug_input_state(usage, 0, false, &found); + int32_t value = get_input_state_value(usage, 0, false, &found); if (!found) { return report_value; } @@ -367,9 +286,8 @@ static uint16_t switch_pro_clamp_axis_range(uint16_t value) { static void pack_switch_pro_stick(uint8_t* out, uint16_t x16, uint16_t y16) { /* - * Report 0x30 uses packed 12-bit stick positions. This matches the - * wired capture in the GBATemp thread: report, timer, status, buttons, - * then two 3-byte stick fields. + * Report 0x30 uses packed 12-bit stick positions. Values passed here are + * normalized to the 16-bit HID Remapper output range. */ x16 = switch_pro_clamp_axis_range(x16); y16 = switch_pro_clamp_axis_range(y16); @@ -381,151 +299,6 @@ static void pack_switch_pro_stick(uint8_t* out, uint16_t x16, uint16_t y16) { out[2] = (y >> 4) & 0xff; } -static void switch_pro_note_output_stick(uint8_t stick, const uint8_t* bytes) { - switch_pro_output_stick_last[stick] = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16); - for (uint8_t byte = 0; byte < 3; byte++) { - if (bytes[byte] < switch_pro_output_stick_min[stick][byte]) { - switch_pro_output_stick_min[stick][byte] = bytes[byte]; - } - if (bytes[byte] > switch_pro_output_stick_max[stick][byte]) { - switch_pro_output_stick_max[stick][byte] = bytes[byte]; - } - } -} - -static void switch_pro_note_trace(uint16_t raw_lx, uint16_t raw_ly, uint16_t norm_lx, uint16_t norm_ly, - uint16_t final_lx, uint16_t final_ly, const uint8_t* out_left, uint32_t flags) { - switch_pro_trace_frame_t* frame = &switch_pro_trace[switch_pro_trace_write]; - frame->seq = ++switch_pro_trace_seq; - frame->raw_lx = raw_lx; - frame->raw_ly = raw_ly; - frame->norm_lx = norm_lx; - frame->norm_ly = norm_ly; - frame->final_lx = final_lx; - frame->final_ly = final_ly; - frame->out_left = out_left[0] | (out_left[1] << 8) | (out_left[2] << 16); - frame->flags = flags; - - if (switch_pro_extreme[0].seq == 0 || frame->norm_lx < switch_pro_extreme[0].norm_lx) { - switch_pro_extreme[0] = *frame; - } - if (switch_pro_extreme[1].seq == 0 || frame->norm_lx > switch_pro_extreme[1].norm_lx) { - switch_pro_extreme[1] = *frame; - } - if (switch_pro_extreme[2].seq == 0 || frame->norm_ly > switch_pro_extreme[2].norm_ly) { - switch_pro_extreme[2] = *frame; - } - if (switch_pro_extreme[3].seq == 0 || frame->norm_ly < switch_pro_extreme[3].norm_ly) { - switch_pro_extreme[3] = *frame; - } - - switch_pro_trace_write = (switch_pro_trace_write + 1) % SWITCH_PRO_TRACE_FRAMES; - - bool was_up = (switch_pro_last_trace_flags & BIT(2)) != 0; - bool is_up = (flags & BIT(2)) != 0; - bool state_found = false; - int32_t source_y = debug_input_state(0x00010031, 0, false, &state_found); - bool source_still_up = state_found && source_y <= 16; - bool real_input_seen = switch_pro_input_enabled && - (switch_pro_ble_reports > 8) && - (switch_pro_source_ms != 0) && - (switch_pro_translated_reports > 8); - if (real_input_seen && was_up && !is_up) { - switch_pro_up_snap_candidates++; - } - if (!switch_pro_snap_latched && real_input_seen && was_up && !is_up && source_still_up) { - switch_pro_snap_latched = true; - switch_pro_snap_save_pending = true; - switch_pro_capture_saved_diagnostics(); - LOG_INF("switch_pro_snap_latched seq=%u raw=%04x,%04x norm=%04x,%04x out=%06x flags=0x%08x source_y=%d", - frame->seq, frame->raw_lx, frame->raw_ly, frame->norm_lx, frame->norm_ly, frame->out_left, frame->flags, source_y); - } - switch_pro_last_trace_flags = flags; -} - -static uint16_t unpack_switch_pro_x(const uint8_t* bytes) { - return bytes[0] | ((bytes[1] & 0x0f) << 8); -} - -static uint16_t unpack_switch_pro_y(const uint8_t* bytes) { - return ((bytes[1] >> 4) & 0x0f) | (bytes[2] << 4); -} - -static void switch_pro_note_heartbeat_output() { - const uint8_t* left = switch_pro_current_input + 6; - uint16_t x = unpack_switch_pro_x(left); - uint16_t y = unpack_switch_pro_y(left); - bool left_active = x <= 0x500; - bool right_active = x >= 0xb00; - bool up_active = y >= 0xb00; - bool down_active = y <= 0x500; - - switch_pro_hb_last_left = left[0] | (left[1] << 8) | (left[2] << 16); - if (left_active) { - switch_pro_hb_left++; - switch_pro_hb_left_run++; - if (switch_pro_hb_left_run > switch_pro_hb_left_run_max) { - switch_pro_hb_left_run_max = switch_pro_hb_left_run; - } - } else { - switch_pro_hb_left_run = 0; - } - if (up_active) { - switch_pro_hb_up++; - switch_pro_hb_up_run++; - if (switch_pro_hb_up_run > switch_pro_hb_up_run_max) { - switch_pro_hb_up_run_max = switch_pro_hb_up_run; - } - } else { - switch_pro_hb_up_run = 0; - } - if (right_active) { - switch_pro_hb_right++; - } - if (down_active) { - switch_pro_hb_down++; - } -} - -static void switch_pro_note_source_report(uint16_t interface, uint8_t report_id, uint8_t len, const uint8_t* data) { - switch_pro_source_meta = (interface & 0xffff) | ((uint32_t) report_id << 16) | ((uint32_t) len << 24); - memset(switch_pro_source_words, 0, sizeof(switch_pro_source_words)); - for (uint8_t i = 0; i < MIN(len, (uint8_t) 16); i++) { - switch_pro_source_words[i / 4] |= (uint32_t) data[i] << (8 * (i % 4)); - } - switch_pro_source_ms = k_uptime_get_32(); - - switch_pro_source_trace_frame_t* frame = &switch_pro_source_trace[switch_pro_source_trace_write]; - frame->seq = ++switch_pro_source_trace_seq; - frame->meta = switch_pro_source_meta; - memcpy(frame->words, switch_pro_source_words, sizeof(frame->words)); - frame->ms = switch_pro_source_ms; - switch_pro_source_trace_write = (switch_pro_source_trace_write + 1) % SWITCH_PRO_SOURCE_TRACE_FRAMES; - - LOG_INF("switch_pro_source seq=%u iface=0x%04x rid=0x%02x len=%u data=%08x %08x %08x %08x", - frame->seq, interface, report_id, len, frame->words[0], frame->words[1], frame->words[2], frame->words[3]); -} - -static uint32_t switch_pro_output_direction_flags(const uint8_t* left) { - uint16_t x = unpack_switch_pro_x(left); - uint16_t y = unpack_switch_pro_y(left); - uint32_t flags = 0; - - if (x <= 0x500) { - flags |= BIT(0); // left - } - if (x >= 0xb00) { - flags |= BIT(1); // right - } - if (y >= 0xb00) { - flags |= BIT(2); // up/forward after source Y inversion - } - if (y <= 0x500) { - flags |= BIT(3); // down/back - } - return flags; -} - static void apply_switch_pro_hat(uint8_t hat, uint8_t* buttons2) { if (hat > 7) { return; @@ -574,10 +347,10 @@ static void switch_pro_translate_report(const uint8_t* report_with_id, uint8_t l if (report_button_pressed(report_with_id, 7)) next[5] |= BIT(7); // ZL apply_switch_pro_hat(report_with_id[11] & 0x0f, &next[5]); - uint16_t raw_lx = switch_pro_axis_from_state_or_report(0x00010030, report_axis_16(report_with_id, 3)); - uint16_t raw_ly = switch_pro_axis_from_state_or_report(0x00010031, report_axis_16(report_with_id, 5)); - uint16_t lx = switch_pro_normalize_axis(raw_lx, false); - uint16_t ly = switch_pro_normalize_axis(raw_ly, true); + uint16_t lx = switch_pro_normalize_axis( + switch_pro_axis_from_state_or_report(0x00010030, report_axis_16(report_with_id, 3)), false); + uint16_t ly = switch_pro_normalize_axis( + switch_pro_axis_from_state_or_report(0x00010031, report_axis_16(report_with_id, 5)), true); uint16_t rx = switch_pro_normalize_axis( switch_pro_axis_from_state_or_report(0x00010032, report_axis_16(report_with_id, 7)), false); uint16_t ry = switch_pro_normalize_axis( @@ -586,15 +359,8 @@ static void switch_pro_translate_report(const uint8_t* report_with_id, uint8_t l switch_pro_note_axis(1, ly); switch_pro_note_axis(2, rx); switch_pro_note_axis(3, ry); - uint16_t norm_lx = lx; - uint16_t norm_ly = ly; pack_switch_pro_stick(next + 6, lx, ly); pack_switch_pro_stick(next + 9, rx, ry); - uint32_t trace_flags = switch_pro_output_direction_flags(next + 6); - switch_pro_note_output_stick(0, next + 6); - switch_pro_note_output_stick(1, next + 9); - switch_pro_note_trace(raw_lx, raw_ly, norm_lx, norm_ly, lx, ly, next + 6, trace_flags); - next[12] = 0x0c; bool buttons_changed = next[3] != switch_pro_current_input[3] || next[4] != switch_pro_current_input[4] || next[5] != switch_pro_current_input[5]; @@ -663,47 +429,21 @@ static void switch_pro_spi_read(const uint8_t* args, uint8_t args_len) { uint8_t read_len = args[4]; memcpy(data, args, 5); - /* - * Switch stick calibration is three packed 12-bit pairs per stick. For - * the left stick the groups are max-above-center, center, min-below-center; - * for the right stick they are center, min-below-center, max-above-center. - * Use center 0x800 and +/-0x600 travel so held low-side directions are - * inside the advertised calibrated range. - */ - static const uint8_t left_stick_cal[] = { - 0x00, 0x06, 0x60, // X/Y max above center: 0x600, 0x600 - 0x00, 0x08, 0x80, // X/Y center: 0x800, 0x800 - 0x00, 0x06, 0x60, // X/Y min below center: 0x600, 0x600 - }; - static const uint8_t right_stick_cal[] = { - 0x00, 0x08, 0x80, // X/Y center: 0x800, 0x800 - 0x00, 0x06, 0x60, // X/Y min below center: 0x600, 0x600 - 0x00, 0x06, 0x60, // X/Y max above center: 0x600, 0x600 - }; - static const uint8_t stick_params[] = { - 0x19, 0xd0, 0x4c, 0xae, 0x40, 0xe1, - 0xee, 0xe2, 0x2e, 0xee, 0xe2, 0x2e, - 0xb4, 0x4a, 0xab, 0x96, 0x64, 0x49, + static const uint8_t stick_cal[] = { + 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, + 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80 }; static const uint8_t body_color[] = { 0x46, 0x46, 0x46 }; static const uint8_t button_color[] = { 0xff, 0xff, 0xff }; - static const uint8_t grip_color[] = { 0x46, 0x46, 0x46 }; if (address == 0x0000603d) { - uint8_t stick_cal[sizeof(left_stick_cal) + sizeof(right_stick_cal)]; - memcpy(stick_cal, left_stick_cal, sizeof(left_stick_cal)); - memcpy(stick_cal + sizeof(left_stick_cal), right_stick_cal, sizeof(right_stick_cal)); memcpy(data + 5, stick_cal, MIN(read_len, (uint8_t) sizeof(stick_cal))); - } else if (address == 0x00006046) { - memcpy(data + 5, right_stick_cal, MIN(read_len, (uint8_t) sizeof(right_stick_cal))); } else if (address == 0x00006050) { + memcpy(data + 5, stick_cal, MIN(read_len, (uint8_t) sizeof(stick_cal))); + } else if (address == 0x00006086) { memcpy(data + 5, body_color, MIN(read_len, (uint8_t) sizeof(body_color))); - } else if (address == 0x00006053) { + } else if (address == 0x00006089) { memcpy(data + 5, button_color, MIN(read_len, (uint8_t) sizeof(button_color))); - } else if (address == 0x00006056 || address == 0x00006059) { - memcpy(data + 5, grip_color, MIN(read_len, (uint8_t) sizeof(grip_color))); - } else if (address == 0x00006086 || address == 0x00006098) { - memcpy(data + 5, stick_params, MIN(read_len, (uint8_t) sizeof(stick_params))); } switch_pro_queue_21(0x10, 0x90, data, MIN((uint8_t) sizeof(data), (uint8_t) (read_len + 5))); @@ -822,7 +562,6 @@ static bool switch_pro_send_input_heartbeat() { bool sent = CHK(hid_int_ep_write(hid_dev0, switch_pro_current_input, sizeof(switch_pro_current_input), NULL)); if (sent) { switch_pro_heartbeat_writes++; - switch_pro_note_heartbeat_output(); } else { switch_pro_heartbeat_write_fails++; } @@ -831,7 +570,6 @@ static bool switch_pro_send_input_heartbeat() { static void switch_pro_fill_diagnostics(uint32_t page, uint32_t values[SWITCH_PRO_DIAG_VALUES]) { memset(values, 0, SWITCH_PRO_DIAG_VALUES * sizeof(uint32_t)); - static const uint32_t axis_usages[4] = { 0x00010030, 0x00010031, 0x00010032, 0x00010035 }; switch (page) { case 0: @@ -878,115 +616,11 @@ static void switch_pro_fill_diagnostics(uint32_t page, uint32_t values[SWITCH_PR values[4] = switch_pro_axis_min[2] | (switch_pro_axis_max[2] << 16); values[5] = switch_pro_axis_min[3] | (switch_pro_axis_max[3] << 16); break; - case 5: - values[0] = switch_pro_output_stick_last[0]; - values[1] = switch_pro_output_stick_last[1]; - values[2] = switch_pro_output_stick_min[0][0] | (switch_pro_output_stick_max[0][0] << 8) | - (switch_pro_output_stick_min[0][1] << 16) | (switch_pro_output_stick_max[0][1] << 24); - values[3] = switch_pro_output_stick_min[0][2] | (switch_pro_output_stick_max[0][2] << 8) | - (switch_pro_output_stick_min[1][0] << 16) | (switch_pro_output_stick_max[1][0] << 24); - values[4] = switch_pro_output_stick_min[1][1] | (switch_pro_output_stick_max[1][1] << 8) | - (switch_pro_output_stick_min[1][2] << 16) | (switch_pro_output_stick_max[1][2] << 24); - break; - case 6: - values[0] = switch_pro_hb_left; - values[1] = switch_pro_hb_up; - values[2] = switch_pro_hb_right; - values[3] = switch_pro_hb_down; - values[4] = switch_pro_hb_left_run | (switch_pro_hb_left_run_max << 16); - values[5] = switch_pro_hb_up_run | (switch_pro_hb_up_run_max << 16); - values[6] = switch_pro_hb_last_left; - break; - case 7: - values[0] = switch_pro_source_meta; - values[1] = switch_pro_source_words[0]; - values[2] = switch_pro_source_words[1]; - values[3] = switch_pro_source_words[2]; - values[4] = switch_pro_source_words[3]; - values[5] = switch_pro_source_ms; - values[6] = switch_pro_up_snap_candidates; - break; - case 8: - for (uint8_t i = 0; i < 4; i++) { - bool scaled_found = false; - bool raw_found = false; - int32_t scaled = debug_input_state(axis_usages[i], 0, false, &scaled_found); - int32_t raw = debug_input_state(axis_usages[i], 0, true, &raw_found); - values[i] = ((uint32_t) (uint16_t) raw << 16) | (uint16_t) scaled; - if (scaled_found) { - values[4] |= BIT(i); - } - if (raw_found) { - values[4] |= BIT(i + 8); - } - } - break; - case 9: - debug_parsed_input(axis_usages[0], values); - break; - case 10: - debug_parsed_input(axis_usages[1], values); - break; - case 11: - debug_parsed_input(axis_usages[2], values); - break; - case 12: - debug_parsed_input(axis_usages[3], values); - break; default: - if (page >= 13 && page < 13 + SWITCH_PRO_PARSED_AXIS_HISTORY_PAGES) { - debug_parsed_axis_history(1, page - 13, values); - break; - } - if (page >= SWITCH_PRO_DIAG_BASE_PAGES && page < SWITCH_PRO_DIAG_PAGES) { - uint32_t trace_age = page - SWITCH_PRO_DIAG_BASE_PAGES; - if (trace_age < SWITCH_PRO_TRACE_FRAMES) { - const switch_pro_trace_frame_t* frame; - uint8_t trace_idx = (switch_pro_trace_write + SWITCH_PRO_TRACE_FRAMES - 1 - trace_age) % SWITCH_PRO_TRACE_FRAMES; - frame = &switch_pro_trace[trace_idx]; - values[0] = frame->seq; - values[1] = frame->raw_lx | (frame->raw_ly << 16); - values[2] = frame->norm_lx | (frame->norm_ly << 16); - values[3] = frame->final_lx | (frame->final_ly << 16); - values[4] = frame->out_left; - values[5] = frame->flags; - values[6] = trace_age; - } else if (trace_age < SWITCH_PRO_TRACE_FRAMES + SWITCH_PRO_EXTREME_FRAMES) { - const switch_pro_trace_frame_t* frame; - frame = &switch_pro_extreme[trace_age - SWITCH_PRO_TRACE_FRAMES]; - values[0] = frame->seq; - values[1] = frame->raw_lx | (frame->raw_ly << 16); - values[2] = frame->norm_lx | (frame->norm_ly << 16); - values[3] = frame->final_lx | (frame->final_ly << 16); - values[4] = frame->out_left; - values[5] = frame->flags; - values[6] = trace_age; - } else { - uint32_t source_age = trace_age - SWITCH_PRO_TRACE_FRAMES - SWITCH_PRO_EXTREME_FRAMES; - if (source_age < SWITCH_PRO_SOURCE_TRACE_FRAMES) { - const switch_pro_source_trace_frame_t* frame; - uint8_t trace_idx = (switch_pro_source_trace_write + SWITCH_PRO_SOURCE_TRACE_FRAMES - 1 - source_age) % SWITCH_PRO_SOURCE_TRACE_FRAMES; - frame = &switch_pro_source_trace[trace_idx]; - values[0] = frame->seq; - values[1] = frame->meta; - values[2] = frame->words[0]; - values[3] = frame->words[1]; - values[4] = frame->words[2]; - values[5] = frame->words[3]; - values[6] = frame->ms; - } - } - } break; } } -static void switch_pro_capture_saved_diagnostics() { - for (uint32_t page = 0; page < SWITCH_PRO_DIAG_PAGES; page++) { - switch_pro_fill_diagnostics(page, switch_pro_saved_diag[page]); - } -} - static void switch_pro_persist_diagnostics() { if (!is_switch_pro_mode() || !switch_pro_input_enabled) { return; @@ -996,21 +630,14 @@ static void switch_pro_persist_diagnostics() { } int64_t now = k_uptime_get(); - if (switch_pro_snap_save_pending) { - switch_pro_last_diag_persist_ms = now; - switch_pro_snap_save_pending = false; - settings_save_one("remapper/switch_pro_diag", switch_pro_saved_diag, sizeof(switch_pro_saved_diag)); - return; - } - if (switch_pro_snap_latched) { - return; - } if (switch_pro_last_diag_persist_ms && (now - switch_pro_last_diag_persist_ms < 2000)) { return; } switch_pro_last_diag_persist_ms = now; - switch_pro_capture_saved_diagnostics(); + for (uint32_t page = 0; page < SWITCH_PRO_DIAG_PAGES; page++) { + switch_pro_fill_diagnostics(page, switch_pro_saved_diag[page]); + } settings_save_one("remapper/switch_pro_diag", switch_pro_saved_diag, sizeof(switch_pro_saved_diag)); } @@ -1448,10 +1075,7 @@ static uint8_t hogp_notify_cb(struct bt_hogp* hogp, struct bt_hogp_rep_info* rep buf.len = bt_hogp_rep_size(rep) + 1; buf.data[0] = bt_hogp_rep_id(rep); - memcpy(buf.data + 1, data, buf.len - 1); - if (is_switch_pro_mode()) { - switch_pro_note_source_report(buf.interface, buf.data[0], buf.len, buf.data); - } + memcpy(buf.data + 1, data, buf.len); if (k_msgq_put(&report_q, &buf, K_NO_WAIT)) { if (is_switch_pro_mode()) { switch_pro_ble_report_drops++; @@ -1793,16 +1417,15 @@ static int remapper_settings_set(const char* name, size_t len, settings_read_cb LOG_INF("%s len=%d", name, len); if (!strcmp(name, "switch_pro_diag")) { - if (len > sizeof(switch_pro_saved_diag) || (len % (SWITCH_PRO_DIAG_VALUES * sizeof(uint32_t))) != 0) { + if (len != sizeof(switch_pro_saved_diag)) { return -EINVAL; } - memset(switch_pro_saved_diag, 0, sizeof(switch_pro_saved_diag)); int bytes_read = read_cb(cb_arg, switch_pro_saved_diag, len); if (bytes_read < 0) { return bytes_read; } - return bytes_read == (int) len ? 0 : -EINVAL; + return bytes_read == sizeof(switch_pro_saved_diag) ? 0 : -EINVAL; } if (strcmp(name, "config")) { diff --git a/firmware/src/remapper.cc b/firmware/src/remapper.cc index 1492bfcb..7211fc21 100644 --- a/firmware/src/remapper.cc +++ b/firmware/src/remapper.cc @@ -112,23 +112,6 @@ uint8_t monitor_usages_queued = 0; monitor_report_t monitor_report[2] = { { .report_id = REPORT_ID_MONITOR }, { .report_id = REPORT_ID_MONITOR } }; uint8_t monitor_report_idx = 0; -struct debug_parsed_input_t { - uint32_t seq; - int32_t raw_value; - int32_t scaled_value; - int32_t logical_minimum; - int32_t logical_maximum; - uint32_t meta; -}; - -#define DEBUG_PARSED_AXIS_COUNT 4 -#define DEBUG_PARSED_AXIS_HISTORY 8 - -std::unordered_map debug_parsed_inputs; -debug_parsed_input_t debug_parsed_axis_history_frames[DEBUG_PARSED_AXIS_COUNT][DEBUG_PARSED_AXIS_HISTORY]; -uint8_t debug_parsed_axis_history_write[DEBUG_PARSED_AXIS_COUNT] = {}; -uint32_t debug_parsed_input_seq = 0; - #define NREGISTERS 32 int32_t registers[NREGISTERS] = { 0 }; std::vector register_ptrs; @@ -378,7 +361,7 @@ inline int32_t* get_state_ptr(uint32_t usage, uint8_t hub_port, bool assign_if_a return NULL; } -int32_t debug_input_state(uint32_t usage, uint8_t hub_port, bool raw, bool* found) { +int32_t get_input_state_value(uint32_t usage, uint8_t hub_port, bool raw, bool* found) { int32_t* state_ptr = get_state_ptr(usage, hub_port, false, raw); if (found != NULL) { *found = state_ptr != NULL; @@ -386,42 +369,6 @@ int32_t debug_input_state(uint32_t usage, uint8_t hub_port, bool raw, bool* foun return state_ptr == NULL ? 0 : *state_ptr; } -bool debug_parsed_input(uint32_t usage, uint32_t values[7]) { - auto search = debug_parsed_inputs.find(usage); - if (search == debug_parsed_inputs.end()) { - memset(values, 0, 7 * sizeof(uint32_t)); - return false; - } - - const debug_parsed_input_t& parsed = search->second; - values[0] = parsed.seq; - values[1] = (uint32_t) parsed.raw_value; - values[2] = (uint32_t) parsed.scaled_value; - values[3] = (uint32_t) parsed.logical_minimum; - values[4] = (uint32_t) parsed.logical_maximum; - values[5] = parsed.meta; - values[6] = usage; - return true; -} - -bool debug_parsed_axis_history(uint8_t axis, uint8_t age, uint32_t values[7]) { - if (axis >= DEBUG_PARSED_AXIS_COUNT || age >= DEBUG_PARSED_AXIS_HISTORY) { - memset(values, 0, 7 * sizeof(uint32_t)); - return false; - } - - uint8_t idx = (debug_parsed_axis_history_write[axis] + DEBUG_PARSED_AXIS_HISTORY - 1 - age) % DEBUG_PARSED_AXIS_HISTORY; - const debug_parsed_input_t& parsed = debug_parsed_axis_history_frames[axis][idx]; - values[0] = parsed.seq; - values[1] = (uint32_t) parsed.raw_value; - values[2] = (uint32_t) parsed.scaled_value; - values[3] = (uint32_t) parsed.logical_minimum; - values[4] = (uint32_t) parsed.logical_maximum; - values[5] = parsed.meta; - values[6] = age; - return parsed.seq != 0; -} - inline tap_hold_state_t* get_tap_hold_state_ptr(uint32_t usage, uint8_t hub_port, bool assign_if_absent = false) { int32_t* state_ptr = get_state_ptr(usage, hub_port, assign_if_absent); if (state_ptr != NULL) { @@ -1596,37 +1543,6 @@ inline void read_input(const uint8_t* report, int len, uint32_t source_usage, co } else { scaled_value = value; } - if ((source_usage == 0x00010030) || - (source_usage == 0x00010031) || - (source_usage == 0x00010032) || - (source_usage == 0x00010035) || - (source_usage == 0x00010039)) { - debug_parsed_input_t parsed = { - .seq = ++debug_parsed_input_seq, - .raw_value = value, - .scaled_value = scaled_value, - .logical_minimum = their_usage.logical_minimum, - .logical_maximum = their_usage.logical_maximum, - .meta = (uint32_t) their_usage.bitpos | - ((uint32_t) their_usage.size << 16) | - ((uint32_t) their_usage.report_id << 24), - }; - debug_parsed_inputs[source_usage] = parsed; - uint8_t axis = DEBUG_PARSED_AXIS_COUNT; - if (source_usage == 0x00010030) { - axis = 0; - } else if (source_usage == 0x00010031) { - axis = 1; - } else if (source_usage == 0x00010032) { - axis = 2; - } else if (source_usage == 0x00010035) { - axis = 3; - } - if (axis < DEBUG_PARSED_AXIS_COUNT) { - debug_parsed_axis_history_frames[axis][debug_parsed_axis_history_write[axis]] = parsed; - debug_parsed_axis_history_write[axis] = (debug_parsed_axis_history_write[axis] + 1) % DEBUG_PARSED_AXIS_HISTORY; - } - } if (their_usage.input_state_0 != NULL) { if ((their_usage.size == 1) || their_usage.is_array) { if (value) { diff --git a/firmware/src/remapper.h b/firmware/src/remapper.h index 15bbca5a..8e06817d 100644 --- a/firmware/src/remapper.h +++ b/firmware/src/remapper.h @@ -40,9 +40,7 @@ void print_stats(); void reset_state(); uint8_t debug_outgoing_report_count(); uint32_t debug_outgoing_report_overflows(); -int32_t debug_input_state(uint32_t usage, uint8_t hub_port, bool raw, bool* found); -bool debug_parsed_input(uint32_t usage, uint32_t values[7]); -bool debug_parsed_axis_history(uint8_t axis, uint8_t age, uint32_t values[7]); +int32_t get_input_state_value(uint32_t usage, uint8_t hub_port, bool raw, bool* found); void set_monitor_enabled(bool enabled); void monitor_usage(uint32_t usage, int32_t value, uint8_t hub_port);