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..903fc1c8 100644 --- a/config-tool-web/examples.js +++ b/config-tool-web/examples.js @@ -10069,4 +10069,22 @@ 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; + 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); +} + 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..1ed3e1c5 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,560 @@ 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 = 0x0100; + + 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 uint16_t switch_pro_axis_from_state_or_report(uint32_t usage, uint16_t report_value) { + bool found = false; + int32_t value = get_input_state_value(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]) { + switch_pro_axis_min[axis] = value; + } + if (value > switch_pro_axis_max[axis]) { + switch_pro_axis_max[axis] = 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) { + /* + * 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); + 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( + 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( + 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); + 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 +951,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 +968,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 +982,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 +1077,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 +1124,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 +1260,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 +1296,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 +1319,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 +1359,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 +1414,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 +1475,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 +1547,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 +1566,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 +1595,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..7211fc21 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; @@ -360,6 +361,14 @@ inline int32_t* get_state_ptr(uint32_t usage, uint8_t hub_port, bool assign_if_a return NULL; } +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; + } + return state_ptr == NULL ? 0 : *state_ptr; +} + 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) { @@ -1397,7 +1406,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 +2058,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..8e06817d 100644 --- a/firmware/src/remapper.h +++ b/firmware/src/remapper.h @@ -38,6 +38,9 @@ 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(); +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); 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