From 89aab00c2db39e7c59ad67ea0684b7eab8f59efc Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Thu, 19 Jun 2025 08:41:18 +0200 Subject: [PATCH 1/8] wip peripheral mode --- firmware-bluetooth/prj.conf | 5 + firmware-bluetooth/src/main.cc | 257 +++++++++++++++++++++++++++++++-- 2 files changed, 252 insertions(+), 10 deletions(-) diff --git a/firmware-bluetooth/prj.conf b/firmware-bluetooth/prj.conf index 99e32201..1b915f2a 100644 --- a/firmware-bluetooth/prj.conf +++ b/firmware-bluetooth/prj.conf @@ -26,6 +26,7 @@ CONFIG_GPIO=y CONFIG_BT=y CONFIG_BT_DEBUG_LOG=y CONFIG_BT_CENTRAL=y +CONFIG_BT_PERIPHERAL=y CONFIG_BT_SMP=y CONFIG_BT_L2CAP_TX_BUF_COUNT=5 CONFIG_BT_GATT_CLIENT=y @@ -45,6 +46,10 @@ CONFIG_BT_HOGP_REPORTS_MAX=32 CONFIG_BT_DEVICE_NAME="HID Remapper Bluetooth" CONFIG_ENABLE_HID_INT_OUT_EP=y +# Peripheral mode support for UART service +CONFIG_BT_GATT_SERVICE_CHANGED=y +CONFIG_BT_GATT_DYNAMIC_DB=y + CONFIG_USB_DEVICE_STACK=y CONFIG_USB_DEVICE_HID=y CONFIG_USB_HID_DEVICE_COUNT=2 diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 913618aa..cbf51df7 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -31,6 +31,38 @@ LOG_MODULE_REGISTER(remapper, LOG_LEVEL_DBG); #define CHK(X) ({ int err = X; if (err != 0) { LOG_ERR("%s returned %d (%s:%d)", #X, err, __FILE__, __LINE__); } err == 0; }) +// Peripheral mode support +enum operation_mode { + MODE_HOST = 0, + MODE_PERIPHERAL = 1 +}; + +static enum operation_mode current_mode = MODE_PERIPHERAL; + +// Nordic UART Service UUID +static struct bt_uuid_128 uart_service_uuid = BT_UUID_INIT_128( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, + 0x93, 0xF3, 0xA3, 0xB5, 0x01, 0x00, 0x40, 0x6E); + +static struct bt_uuid_128 uart_rx_uuid = BT_UUID_INIT_128( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, + 0x93, 0xF3, 0xA3, 0xB5, 0x02, 0x00, 0x40, 0x6E); + +static struct bt_uuid_128 uart_tx_uuid = BT_UUID_INIT_128( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, + 0x93, 0xF3, 0xA3, 0xB5, 0x03, 0x00, 0x40, 0x6E); + +static void peripheral_mode_init(void); +static void process_peripheral_command(uint8_t* buf, int count); +static void start_peripheral_advertising(void); +static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uint8_t len); + +static ssize_t uart_write_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags); +static void uart_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value); +static void peripheral_connected(struct bt_conn *conn, uint8_t err); +static void peripheral_disconnected(struct bt_conn *conn, uint8_t reason); + static const int SCAN_DELAY_MS = 1000; static const int CLEAR_BONDS_BUTTON_PRESS_MS = 3000; @@ -87,6 +119,23 @@ K_MSGQ_DEFINE(disconnected_q, sizeof(struct disconnected_type), CONFIG_BT_MAX_CO K_MSGQ_DEFINE(set_report_q, sizeof(struct set_report_type), 8, 4); ATOMIC_DEFINE(tick_pending, 1); +// Peripheral mode GATT service definitions +static struct bt_conn *peripheral_conn; + +// Use Zephyr's simplified GATT service macro +BT_GATT_SERVICE_DEFINE(uart_service, + BT_GATT_PRIMARY_SERVICE(&uart_service_uuid), + BT_GATT_CHARACTERISTIC(&uart_rx_uuid.uuid, + BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP, + BT_GATT_PERM_WRITE, NULL, uart_write_cb, NULL), + BT_GATT_CHARACTERISTIC(&uart_tx_uuid.uuid, + BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_NONE, NULL, NULL, NULL), + BT_GATT_CCC(uart_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), +); + + + #define SW0_NODE DT_ALIAS(sw0) #if !DT_NODE_HAS_STATUS(SW0_NODE, okay) #error "Unsupported board: sw0 devicetree alias is not defined" @@ -172,6 +221,54 @@ static void set_led_mode(LedMode led_mode_) { } } +// Peripheral mode callbacks +static ssize_t uart_write_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) +{ + if (current_mode == MODE_PERIPHERAL) { + process_peripheral_command((uint8_t*)buf, len); + } + return len; +} + +static void uart_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + LOG_DBG("UART CCC changed: %d", value); + // Handle notification enable/disable +} + +static void peripheral_connected(struct bt_conn *conn, uint8_t err) +{ + if (err) { + LOG_ERR("Peripheral connection failed (err %u)", err); + return; + } + + if (current_mode == MODE_PERIPHERAL) { + peripheral_conn = bt_conn_ref(conn); + LOG_INF("Peripheral connected"); + set_led_mode(LedMode::ON); + } +} + +static void peripheral_disconnected(struct bt_conn *conn, uint8_t reason) +{ + if (current_mode == MODE_PERIPHERAL && conn == peripheral_conn) { + LOG_INF("Peripheral disconnected (reason %u)", reason); + bt_conn_unref(peripheral_conn); + peripheral_conn = NULL; + set_led_mode(LedMode::BLINK); + + // Restart advertising + start_peripheral_advertising(); + } +} + +BT_CONN_CB_DEFINE(peripheral_conn_callbacks) = { + .connected = peripheral_connected, + .disconnected = peripheral_disconnected, +}; + static void scan_start() { if (CHK(bt_scan_start(BT_SCAN_TYPE_SCAN_PASSIVE))) { LOG_DBG("Scanning started."); @@ -336,7 +433,7 @@ static void patch_broken_uuids(struct bt_gatt_dm* dm) { bt_uuid_to_str(attr->uuid, str1, sizeof(str2)); *((bt_uuid_16*) attr->uuid) = { .uuid = { BT_UUID_TYPE_16 }, - .val = (BT_UUID_128(attr->uuid)->val[13] << 8 | BT_UUID_128(attr->uuid)->val[12]) + .val = (uint16_t)(BT_UUID_128(attr->uuid)->val[13] << 8 | BT_UUID_128(attr->uuid)->val[12]) }; bt_uuid_to_str(attr->uuid, str2, sizeof(str2)); LOG_INF("%s -> %s", str1, str2); @@ -386,11 +483,128 @@ static void button_cb(const struct device* dev, struct gpio_callback* cb, uint32 if (k_uptime_get() - button_pressed_at > CLEAR_BONDS_BUTTON_PRESS_MS) { clear_bonds(); } else { - pair_new_device(); + // Toggle between host and peripheral mode + if (current_mode == MODE_HOST) { + current_mode = MODE_PERIPHERAL; + LOG_INF("Switching to peripheral mode"); + peripheral_mode_init(); + } else { + current_mode = MODE_HOST; + LOG_INF("Switching to host mode"); + pair_new_device(); + } } } } +// Peripheral mode implementation functions +static void peripheral_mode_init(void) { + // Stop host mode scanning + if (scanning) { + scan_stop(); + } + + // Disconnect any existing host connections + bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_conn, NULL); + + // Simulate connecting a virtual gamepad so the HID remapper system has something to work with + // This creates a virtual "peripheral gamepad" interface that the system can map from + uint16_t virtual_interface = 0xFF00; // Use a special interface ID for peripheral + + // Simple gamepad descriptor exactly matching app byte layout + static const uint8_t virtual_gamepad_descriptor[] = { + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x05, // Usage (Game Pad) + 0xA1, 0x01, // Collection (Application) + 0x85, 0x01, // Report ID (1) + + // All buttons as one block (20 buttons total) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (Button 1) + 0x29, 0x14, // Usage Maximum (Button 20) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x14, // Report Count (20) + 0x81, 0x02, // Input (Data,Var,Abs) + + // 4 bits padding to complete 3 bytes (24 bits total) + 0x75, 0x04, // Report Size (4) + 0x95, 0x01, // Report Count (1) + 0x81, 0x03, // Input (Const,Var,Abs) - padding + + // 4 analog stick axes + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x30, // Usage (X) - Left stick X + 0x09, 0x31, // Usage (Y) - Left stick Y + 0x09, 0x32, // Usage (Z) - Right stick X + 0x09, 0x35, // Usage (Rz) - Right stick Y + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x04, // Report Count (4) + 0x81, 0x02, // Input (Data,Var,Abs) + + // 1 unused byte + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0x81, 0x03, // Input (Const,Var,Abs) - padding + + 0xC0 // End Collection + }; + + // Parse the virtual descriptor + parse_descriptor(0xCAFE, 0xBABE, virtual_gamepad_descriptor, sizeof(virtual_gamepad_descriptor), virtual_interface, 0); + device_connected_callback(virtual_interface, 0xCAFE, 0xBABE, 0); + + // Set LED to indicate peripheral mode (blinking = advertising/waiting for connection) + set_led_mode(LedMode::BLINK); + + // Start advertising + start_peripheral_advertising(); + + LOG_INF("Peripheral mode initialized with virtual gamepad"); + +} + +static void start_peripheral_advertising(void) { + struct bt_le_adv_param adv_param = { + .id = BT_ID_DEFAULT, + .sid = 0, + .secondary_max_skip = 0, + .options = BT_LE_ADV_OPT_CONNECTABLE | BT_LE_ADV_OPT_USE_NAME, + .interval_min = BT_GAP_ADV_FAST_INT_MIN_2, + .interval_max = BT_GAP_ADV_FAST_INT_MAX_2, + .peer = NULL, + }; + + static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA(BT_DATA_NAME_COMPLETE, "playAbility", 11), + }; + + CHK(bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), NULL, 0)); + LOG_INF("Peripheral advertising started as 'playAbility'"); +} + +static void process_peripheral_command(uint8_t* buf, int count) { + // Handle raw gamepad data (8 bytes matching app format) + if (count >= 8) { + uint8_t report[9]; + report[0] = 1; // Report ID + memcpy(report + 1, buf, 8); // Copy 8 bytes of gamepad data directly + + // Inject into HID remapper system + handle_received_report(report, sizeof(report), 0xFF00, 1); + LOG_INF("Received 8 bytes: %02X %02X %02X %02X %02X %02X %02X %02X", + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]); + LOG_INF("Report sent: %02X %02X %02X %02X %02X %02X %02X %02X %02X", + report[0], report[1], report[2], report[3], report[4], report[5], report[6], report[7], report[8]); + } +} + + + static void connected(struct bt_conn* conn, uint8_t conn_err) { char addr[BT_ADDR_LE_STR_LEN]; @@ -915,7 +1129,12 @@ int main() { parse_our_descriptor(); set_mapping_from_config(); - k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + // Start in the appropriate mode + if (current_mode == MODE_HOST) { + k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + } else { + peripheral_mode_init(); + } struct report_type incoming_report; struct descriptor_type incoming_descriptor; @@ -926,13 +1145,31 @@ 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 (atomic_test_and_clear_bit(tick_pending, 0)) { - process_mapping(true); - process_pending = false; + // Host mode functionality + if (current_mode == MODE_HOST) { + 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 (atomic_test_and_clear_bit(tick_pending, 0)) { + process_mapping(true); + process_pending = false; + } + + while (!k_msgq_get(&disconnected_q, &disconnected_item, K_NO_WAIT)) { + LOG_INF("device_disconnected_callback conn_idx=%d", disconnected_item.conn_idx); + device_disconnected_callback(disconnected_item.conn_idx); + } + + while (!k_msgq_get(&descriptor_q, &incoming_descriptor, K_NO_WAIT)) { + LOG_HEXDUMP_DBG(incoming_descriptor.data, incoming_descriptor.size, "incoming_descriptor"); + parse_descriptor(1, 1, incoming_descriptor.data, incoming_descriptor.size, incoming_descriptor.conn_idx << 8, 0); + } + + if (their_descriptor_updated) { + update_their_descriptor_derivates(); + their_descriptor_updated = false; + } } if (!k_sem_take(&usb_sem0, K_NO_WAIT)) { if (!send_report(do_send_report)) { From e1e925e218f63e4a1d32feee4a231c890267df31 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Thu, 19 Jun 2025 10:05:00 +0200 Subject: [PATCH 2/8] Update Bluetooth firmware to implement packet protocol handling - Added packet structure definitions and handling for a new protocol. - Introduced functions for processing received packets with SLIP-like framing. - Updated peripheral mode initialization to reflect changes in descriptor handling. - Modified advertising name to use the configured Bluetooth device name. - Cleaned up and removed unused virtual gamepad descriptor code. --- firmware-bluetooth/src/main.cc | 191 ++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 65 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index cbf51df7..a48ae66c 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -26,6 +26,7 @@ #include "our_descriptor.h" #include "platform.h" #include "remapper.h" +#include "crc.h" // If you have this from your receiver project LOG_MODULE_REGISTER(remapper, LOG_LEVEL_DBG); @@ -52,6 +53,28 @@ static struct bt_uuid_128 uart_tx_uuid = BT_UUID_INIT_128( 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x03, 0x00, 0x40, 0x6E); +// Add packet structure definitions similar to the receiver +#define PROTOCOL_VERSION 1 +#define SERIAL_MAX_PACKET_SIZE 512 + +#define END 0300 /* indicates end of packet */ +#define ESC 0333 /* indicates byte stuffing */ +#define ESC_END 0334 /* ESC ESC_END means END data byte */ +#define ESC_ESC 0335 /* ESC ESC_ESC means ESC data byte */ + +typedef struct __attribute__((packed)) { + uint8_t protocol_version; + uint8_t our_descriptor_number; + uint8_t len; + uint8_t report_id; + uint8_t data[0]; +} packet_t; + +// Packet buffer and state for SLIP-like framing +static uint8_t packet_buffer[SERIAL_MAX_PACKET_SIZE]; +static uint16_t bytes_read = 0; +static bool escaped = false; + static void peripheral_mode_init(void); static void process_peripheral_command(uint8_t* buf, int count); static void start_peripheral_advertising(void); @@ -507,55 +530,22 @@ static void peripheral_mode_init(void) { // Disconnect any existing host connections bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_conn, NULL); - // Simulate connecting a virtual gamepad so the HID remapper system has something to work with - // This creates a virtual "peripheral gamepad" interface that the system can map from - uint16_t virtual_interface = 0xFF00; // Use a special interface ID for peripheral + // Initialize packet reception state + bytes_read = 0; + escaped = false; - // Simple gamepad descriptor exactly matching app byte layout - static const uint8_t virtual_gamepad_descriptor[] = { - 0x05, 0x01, // Usage Page (Generic Desktop) - 0x09, 0x05, // Usage (Game Pad) - 0xA1, 0x01, // Collection (Application) - 0x85, 0x01, // Report ID (1) - - // All buttons as one block (20 buttons total) - 0x05, 0x09, // Usage Page (Button) - 0x19, 0x01, // Usage Minimum (Button 1) - 0x29, 0x14, // Usage Maximum (Button 20) - 0x15, 0x00, // Logical Minimum (0) - 0x25, 0x01, // Logical Maximum (1) - 0x75, 0x01, // Report Size (1) - 0x95, 0x14, // Report Count (20) - 0x81, 0x02, // Input (Data,Var,Abs) - - // 4 bits padding to complete 3 bytes (24 bits total) - 0x75, 0x04, // Report Size (4) - 0x95, 0x01, // Report Count (1) - 0x81, 0x03, // Input (Const,Var,Abs) - padding - - // 4 analog stick axes - 0x05, 0x01, // Usage Page (Generic Desktop) - 0x09, 0x30, // Usage (X) - Left stick X - 0x09, 0x31, // Usage (Y) - Left stick Y - 0x09, 0x32, // Usage (Z) - Right stick X - 0x09, 0x35, // Usage (Rz) - Right stick Y - 0x15, 0x00, // Logical Minimum (0) - 0x26, 0xFF, 0x00, // Logical Maximum (255) - 0x75, 0x08, // Report Size (8) - 0x95, 0x04, // Report Count (4) - 0x81, 0x02, // Input (Data,Var,Abs) - - // 1 unused byte - 0x75, 0x08, // Report Size (8) - 0x95, 0x01, // Report Count (1) - 0x81, 0x03, // Input (Const,Var,Abs) - padding - - 0xC0 // End Collection - }; + // Create a virtual device that can handle the packet protocol + // Use the current our_descriptor_number configuration + uint16_t virtual_interface = 0xFF00; - // Parse the virtual descriptor - parse_descriptor(0xCAFE, 0xBABE, virtual_gamepad_descriptor, sizeof(virtual_gamepad_descriptor), virtual_interface, 0); - device_connected_callback(virtual_interface, 0xCAFE, 0xBABE, 0); + // Parse the appropriate descriptor based on our_descriptor_number + if (our_descriptor_number < NOUR_DESCRIPTORS) { + parse_descriptor(0xCAFE, 0xBABE, + our_descriptors[our_descriptor_number].descriptor, + our_descriptors[our_descriptor_number].descriptor_length, + virtual_interface, 0); + device_connected_callback(virtual_interface, 0xCAFE, 0xBABE, 0); + } // Set LED to indicate peripheral mode (blinking = advertising/waiting for connection) set_led_mode(LedMode::BLINK); @@ -563,8 +553,7 @@ static void peripheral_mode_init(void) { // Start advertising start_peripheral_advertising(); - LOG_INF("Peripheral mode initialized with virtual gamepad"); - + LOG_INF("Peripheral mode initialized with packet protocol (descriptor %d)", our_descriptor_number); } static void start_peripheral_advertising(void) { @@ -580,30 +569,101 @@ static void start_peripheral_advertising(void) { static const struct bt_data ad[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), - BT_DATA(BT_DATA_NAME_COMPLETE, "playAbility", 11), + BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1), }; CHK(bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), NULL, 0)); - LOG_INF("Peripheral advertising started as 'playAbility'"); + LOG_INF("Peripheral advertising started as '%s'", CONFIG_BT_DEVICE_NAME); } -static void process_peripheral_command(uint8_t* buf, int count) { - // Handle raw gamepad data (8 bytes matching app format) - if (count >= 8) { - uint8_t report[9]; - report[0] = 1; // Report ID - memcpy(report + 1, buf, 8); // Copy 8 bytes of gamepad data directly - - // Inject into HID remapper system - handle_received_report(report, sizeof(report), 0xFF00, 1); - LOG_INF("Received 8 bytes: %02X %02X %02X %02X %02X %02X %02X %02X", - buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]); - LOG_INF("Report sent: %02X %02X %02X %02X %02X %02X %02X %02X %02X", - report[0], report[1], report[2], report[3], report[4], report[5], report[6], report[7], report[8]); +static void handle_received_packet(const uint8_t* data, uint16_t len) { + if (len < sizeof(packet_t)) { + LOG_WRN("Packet too small: %d", len); + return; + } + + packet_t* msg = (packet_t*) data; + len = len - sizeof(packet_t); + + if ((msg->protocol_version != PROTOCOL_VERSION) || + (msg->len != len) || + (len > 64) || + (msg->our_descriptor_number >= NOUR_DESCRIPTORS) || + ((msg->report_id == 0) && (len >= 64))) { + LOG_WRN("Invalid packet: proto=%d, len=%d, desc=%d, report_id=%d", + msg->protocol_version, msg->len, msg->our_descriptor_number, msg->report_id); + return; + } + + // Handle descriptor change + if (msg->our_descriptor_number != our_descriptor_number) { + our_descriptor_number = msg->our_descriptor_number; + // Persist config if needed + LOG_INF("Descriptor number changed to %d", our_descriptor_number); + } + + // Create HID report with report ID + uint8_t report[65]; + report[0] = msg->report_id; + memcpy(report + 1, msg->data, len); + + // Inject into HID remapper system + handle_received_report(report, len + 1, 0xFF00, msg->report_id); + + LOG_INF("Packet processed: proto=%d, desc=%d, report_id=%d, len=%d", + msg->protocol_version, msg->our_descriptor_number, msg->report_id, len); +} + +static void process_byte_with_framing(uint8_t c) { + bytes_read %= sizeof(packet_buffer); + + if (escaped) { + switch (c) { + case ESC_END: + packet_buffer[bytes_read++] = END; + break; + case ESC_ESC: + packet_buffer[bytes_read++] = ESC; + break; + default: + // this shouldn't happen + packet_buffer[bytes_read++] = c; + break; + } + escaped = false; + } else { + switch (c) { + case END: + if (bytes_read > 4) { + uint32_t crc = crc32(packet_buffer, bytes_read - 4); + uint32_t received_crc = 0; + for (int i = 0; i < 4; i++) { + received_crc = (received_crc << 8) | packet_buffer[bytes_read - 1 - i]; + } + if (crc == received_crc) { + handle_received_packet(packet_buffer, bytes_read - 4); + } else { + LOG_WRN("CRC error: expected 0x%08X, got 0x%08X", crc, received_crc); + } + } + bytes_read = 0; + break; + case ESC: + escaped = true; + break; + default: + packet_buffer[bytes_read++] = c; + break; + } } } - +static void process_peripheral_command(uint8_t* buf, int count) { + // Process each byte with SLIP-like framing + for (int i = 0; i < count; i++) { + process_byte_with_framing(buf[i]); + } +} static void connected(struct bt_conn* conn, uint8_t conn_err) { char addr[BT_ADDR_LE_STR_LEN]; @@ -932,6 +992,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; // Default case - interface not supported } static void button_init() { From 777737d138c8f8056cc5d07c702b65538efaf535 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Fri, 20 Jun 2025 15:43:54 +0200 Subject: [PATCH 3/8] try --- firmware-bluetooth/src/main.cc | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index a48ae66c..05d0d305 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -40,18 +40,18 @@ enum operation_mode { static enum operation_mode current_mode = MODE_PERIPHERAL; -// Nordic UART Service UUID +// Nordic UART Service UUID - using standard Nordic UUIDs for Web Bluetooth compatibility static struct bt_uuid_128 uart_service_uuid = BT_UUID_INIT_128( - 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, - 0x93, 0xF3, 0xA3, 0xB5, 0x01, 0x00, 0x40, 0x6E); + 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, + 0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e); static struct bt_uuid_128 uart_rx_uuid = BT_UUID_INIT_128( - 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, - 0x93, 0xF3, 0xA3, 0xB5, 0x02, 0x00, 0x40, 0x6E); + 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, + 0x93, 0xf3, 0xa3, 0xb5, 0x02, 0x00, 0x40, 0x6e); static struct bt_uuid_128 uart_tx_uuid = BT_UUID_INIT_128( - 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, - 0x93, 0xF3, 0xA3, 0xB5, 0x03, 0x00, 0x40, 0x6E); + 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, + 0x93, 0xf3, 0xa3, 0xb5, 0x03, 0x00, 0x40, 0x6e); // Add packet structure definitions similar to the receiver #define PROTOCOL_VERSION 1 @@ -569,11 +569,14 @@ static void start_peripheral_advertising(void) { static const struct bt_data ad[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), - BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1), + BT_DATA(BT_DATA_NAME_COMPLETE, "HID Remapper", 12), + BT_DATA_BYTES(BT_DATA_UUID128_ALL, + 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, + 0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e), }; CHK(bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), NULL, 0)); - LOG_INF("Peripheral advertising started as '%s'", CONFIG_BT_DEVICE_NAME); + LOG_INF("Peripheral advertising started as 'HID Remapper'"); } static void handle_received_packet(const uint8_t* data, uint16_t len) { From 92e8aca1fa798b2f2a7fa789634353d913c9577a Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Sun, 22 Jun 2025 18:26:55 +0200 Subject: [PATCH 4/8] faster and better connection --- firmware-bluetooth/src/main.cc | 55 ++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 05d0d305..c9a835ef 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -184,7 +184,7 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); static bool scanning = false; static bool peers_only = true; -static struct bt_le_conn_param* conn_param = BT_LE_CONN_PARAM(6, 6, 44, 400); +static struct bt_le_conn_param* conn_param = BT_LE_CONN_PARAM(12, 24, 0, 3000); static void activity_led_off_work_fn(struct k_work* work) { gpio_pin_set_dt(&led0, false); @@ -271,6 +271,21 @@ static void peripheral_connected(struct bt_conn *conn, uint8_t err) peripheral_conn = bt_conn_ref(conn); LOG_INF("Peripheral connected"); set_led_mode(LedMode::ON); + + // Request stable connection parameters for peripheral mode + struct bt_le_conn_param param = { + .interval_min = 12, // 15ms minimum + .interval_max = 24, // 30ms maximum + .latency = 0, // No latency for real-time data + .timeout = 3000 // 30 second timeout + }; + + int ret = bt_conn_le_param_update(conn, ¶m); + if (ret) { + LOG_WRN("Failed to update connection parameters: %d", ret); + } else { + LOG_INF("Requested stable connection parameters"); + } } } @@ -360,6 +375,12 @@ static bool scan_setup_filters() { } static void scan_start_work_fn(struct k_work* work) { + // Don't scan aggressively when in peripheral mode to avoid interference + if (current_mode == MODE_PERIPHERAL && peripheral_conn) { + LOG_DBG("Skipping scan - peripheral mode with active connection"); + return; + } + if (scanning) { scan_stop(); } @@ -679,8 +700,10 @@ static void connected(struct bt_conn* conn, uint8_t conn_err) { if (conn_err) { LOG_ERR("Failed to connect to %s (conn_err=%u).", addr, conn_err); - k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); - + // Only restart scanning if we're in host mode + if (current_mode == MODE_HOST) { + k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + } return; } @@ -707,7 +730,10 @@ static void disconnected(struct bt_conn* conn, uint8_t reason) { count_connections(); - k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + // Only restart scanning if we're in host mode + if (current_mode == MODE_HOST) { + k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + } } static void security_changed(struct bt_conn* conn, bt_security_t level, enum bt_security_err err) { @@ -718,7 +744,10 @@ static void security_changed(struct bt_conn* conn, bt_security_t level, enum bt_ if (!err) { LOG_INF("%s, level=%u.", addr, level); peers_only = true; - gatt_discover(conn); + // Only start discovery if we're in host mode + if (current_mode == MODE_HOST) { + gatt_discover(conn); + } } else { LOG_ERR("security failed: %s, level=%u, err=%d", addr, level, err); } @@ -729,8 +758,15 @@ static void le_param_updated(struct bt_conn* conn, uint16_t interval, uint16_t l } static bool le_param_req(struct bt_conn* conn, struct bt_le_conn_param* param) { - LOG_INF("interval_min=%d, interval_max=%d, latency=%d, timeout=%d", param->interval_min, param->interval_max, param->latency, param->timeout); - param->interval_max = param->interval_min; + LOG_INF("interval_min=%d, interval_max=%d, latency=%d, timeout=%d", + param->interval_min, param->interval_max, param->latency, param->timeout); + + // Accept wider range of parameters for better compatibility + if (param->interval_min < 6) param->interval_min = 6; // Minimum 7.5ms + if (param->interval_max > 800) param->interval_max = 800; // Maximum 1000ms + if (param->timeout < 100) param->timeout = 100; // Minimum 1s timeout + if (param->timeout > 3200) param->timeout = 3200; // Maximum 32s timeout + return true; } @@ -772,10 +808,11 @@ static uint8_t hogp_notify_cb(struct bt_hogp* hogp, struct bt_hogp_rep_info* rep return BT_GATT_ITER_STOP; } - if (scanning) { + // Don't aggressively restart scanning if we're in peripheral mode + if (scanning && current_mode != MODE_PERIPHERAL) { scanning = false; // more reports can come in before we actually stop scanning; there's probably a scenario where this causes trouble though k_work_submit(&scan_stop_work); - } else { + } else if (current_mode != MODE_PERIPHERAL) { k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } From 4c62f79b14df2b8afcc363912cd9fe0ed4f73ff7 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Sun, 22 Jun 2025 19:23:35 +0200 Subject: [PATCH 5/8] Add BLE gamepad transmitter implementation - Introduced a new JavaScript module for handling Bluetooth Low Energy (BLE) connections with a gamepad. - Implemented virtual button states and event handlers for user interaction. - Added functionality for connecting, disconnecting, and sending data to BLE devices. - Included CRC32 checksum calculation for data integrity. - Created an HTML interface for user controls and output display. - Ensured compatibility with Web Bluetooth API and provided user feedback for connection status. --- transmitter-ble-web/code.js | 488 +++++++++++++++++++++++++++++++++ transmitter-ble-web/crc.js | 53 ++++ transmitter-ble-web/index.html | 131 +++++++++ 3 files changed, 672 insertions(+) create mode 100644 transmitter-ble-web/code.js create mode 100644 transmitter-ble-web/crc.js create mode 100644 transmitter-ble-web/index.html diff --git a/transmitter-ble-web/code.js b/transmitter-ble-web/code.js new file mode 100644 index 00000000..7771ac86 --- /dev/null +++ b/transmitter-ble-web/code.js @@ -0,0 +1,488 @@ +import crc32 from './crc.js'; + +const dpad_lut = [15, 6, 2, 15, 0, 7, 1, 0, 4, 5, 3, 4, 15, 6, 2, 15]; + +// BLE Service and Characteristic UUIDs +// Using Nordic UART Service UUIDs as they're commonly supported +const UART_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'; +const UART_TX_CHARACTERISTIC_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; // Write +const UART_RX_CHARACTERISTIC_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'; // Notify + +const END = 0o300; /* indicates end of packet */ +const ESC = 0o333; /* indicates byte stuffing */ +const ESC_END = 0o334; /* ESC ESC_END means END data byte */ +const ESC_ESC = 0o335; /* ESC ESC_ESC means ESC data byte */ + +// Virtual button states +let virtualButtons = { + a: false, + b: false, + x: false, + y: false, + l: false, + r: false, + zl: false, + zr: false, + minus: false, + plus: false, + home: false, + dpad_up: false, + dpad_down: false, + dpad_left: false, + dpad_right: false +}; + +document.addEventListener("DOMContentLoaded", function () { + document.getElementById("connect_ble").addEventListener("click", connect_ble); + document.getElementById("disconnect_ble").addEventListener("click", disconnect_ble); + + // Setup virtual button event handlers + setupVirtualButton("button_a", "a"); + setupVirtualButton("button_b", "b"); + setupVirtualButton("button_x", "x"); + setupVirtualButton("button_y", "y"); + setupVirtualButton("button_l", "l"); + setupVirtualButton("button_r", "r"); + setupVirtualButton("button_zl", "zl"); + setupVirtualButton("button_zr", "zr"); + setupVirtualButton("button_minus", "minus"); + setupVirtualButton("button_plus", "plus"); + setupVirtualButton("button_home", "home"); + setupVirtualButton("dpad_up", "dpad_up"); + setupVirtualButton("dpad_down", "dpad_down"); + setupVirtualButton("dpad_left", "dpad_left"); + setupVirtualButton("dpad_right", "dpad_right"); + + output = document.getElementById("output"); + setInterval(loop, 8); +}); + +function setupVirtualButton(elementId, buttonKey) { + const button = document.getElementById(elementId); + if (button) { + button.addEventListener("click", function(e) { + e.preventDefault(); + virtualButtons[buttonKey] = !virtualButtons[buttonKey]; + + if (virtualButtons[buttonKey]) { + button.classList.add("pressed"); + } else { + button.classList.remove("pressed"); + } + }); + } +} + +// Check if Web Bluetooth is supported +if (!navigator.bluetooth) { + document.body.innerHTML = '

Web Bluetooth API not supported

Please use Chrome/Edge with HTTPS or enable experimental features.

'; +} + +let device = null; +let server = null; +let service = null; +let txCharacteristic = null; +let rxCharacteristic = null; +let prev_report = new Uint8Array([0, 0, 15, 0, 0, 0, 0, 0]); +let output; +let keepAliveCounter = 0; +let connectionLostDetected = false; + +async function connect_ble() { + try { + write("Searching for BLE devices...\n"); + + // Request a device with more flexible filtering + // This allows connecting to devices like "playAbility" that might not advertise the service UUID + device = await navigator.bluetooth.requestDevice({ + // Accept all devices but prefer those with UART service or specific names + acceptAllDevices: true, + optionalServices: [UART_SERVICE_UUID] + }); + + write(`Selected device: ${device.name || 'Unknown'}\n`); + + // Add disconnection event listener + device.addEventListener('gattserverdisconnected', onDisconnected); + + // Connect to GATT server + server = await device.gatt.connect(); + write("Connected to GATT server\n"); + + // Try to get the UART service + try { + service = await server.getPrimaryService(UART_SERVICE_UUID); + write("Found UART service\n"); + } catch (serviceError) { + write(`UART service not found: ${serviceError.message}\n`); + throw new Error("Device doesn't have Nordic UART Service. Please select a compatible device."); + } + + // Get the TX characteristic (for writing data to device) + try { + txCharacteristic = await service.getCharacteristic(UART_TX_CHARACTERISTIC_UUID); + write("Got TX characteristic\n"); + } catch (txError) { + write(`TX characteristic error: ${txError.message}\n`); + throw new Error("Cannot find TX characteristic"); + } + + // Get the RX characteristic (for reading data from device) + try { + rxCharacteristic = await service.getCharacteristic(UART_RX_CHARACTERISTIC_UUID); + await rxCharacteristic.startNotifications(); + rxCharacteristic.addEventListener('characteristicvaluechanged', handleNotification); + write("Got RX characteristic with notifications\n"); + } catch (rxError) { + write("RX characteristic not available (write-only mode)\n"); + // This is OK, the device might be write-only + } + + // Update UI + document.getElementById("connect_ble").style.display = "none"; + document.getElementById("disconnect_ble").style.display = "inline"; + + // Reset connection state + connectionLostDetected = false; + keepAliveCounter = 0; + + write("BLE connection established!\n"); + + // Send initial data immediately to establish communication + write("Sending initial gamepad data...\n"); + const initialReport = new Uint8Array([0, 0, 15, 128, 128, 128, 128, 0]); // Neutral state + await send_report(initialReport); + write("Initial data sent successfully!\n\n"); + + } catch (error) { + write(`Error: ${error.message}\n`); + console.error('BLE connection error:', error); + + // Clean up on error + if (server && server.connected) { + server.disconnect(); + } + device = null; + server = null; + service = null; + txCharacteristic = null; + rxCharacteristic = null; + } +} + +function onDisconnected() { + write("Device disconnected unexpectedly!\n"); + connectionLostDetected = true; + + // Clean up connection state + server = null; + service = null; + txCharacteristic = null; + rxCharacteristic = null; + + // Update UI + document.getElementById("connect_ble").style.display = "inline"; + document.getElementById("disconnect_ble").style.display = "none"; + + // Auto-reconnect after 2 seconds + setTimeout(() => { + if (device && connectionLostDetected) { + write("Attempting to reconnect...\n"); + connect_ble(); + } + }, 2000); +} + +async function disconnect_ble() { + try { + connectionLostDetected = false; // Prevent auto-reconnect + + if (rxCharacteristic) { + await rxCharacteristic.stopNotifications(); + rxCharacteristic.removeEventListener('characteristicvaluechanged', handleNotification); + } + if (device) { + device.removeEventListener('gattserverdisconnected', onDisconnected); + } + if (server && server.connected) { + server.disconnect(); + } + + device = null; + server = null; + service = null; + txCharacteristic = null; + rxCharacteristic = null; + + // Update UI + document.getElementById("connect_ble").style.display = "inline"; + document.getElementById("disconnect_ble").style.display = "none"; + + write("Disconnected from BLE device\n\n"); + + } catch (error) { + write(`Disconnect error: ${error.message}\n`); + console.error('BLE disconnect error:', error); + } +} + +function handleNotification(event) { + const value = event.target.value; + const decoder = new TextDecoder(); + const data = decoder.decode(value); + write(`Received: ${data}\n`); +} + +let transmit_buffer = []; + +function ble_write(c) { + transmit_buffer.push(c); +} + +async function flush() { + if (!txCharacteristic || transmit_buffer.length === 0) { + return; + } + + try { + // BLE characteristics typically have a 20-byte MTU limit + const MTU_SIZE = 20; + const data = new Uint8Array(transmit_buffer); + + // Send data in chunks if it exceeds MTU + for (let i = 0; i < data.length; i += MTU_SIZE) { + const chunk = data.slice(i, i + MTU_SIZE); + await txCharacteristic.writeValue(chunk); + } + + transmit_buffer = []; + } catch (error) { + write(`Write error: ${error.message}\n`); + console.error('BLE write error:', error); + } +} + +function send_escaped_byte(b) { + switch (b) { + case END: + ble_write(ESC); + ble_write(ESC_END); + break; + + case ESC: + ble_write(ESC); + ble_write(ESC_ESC); + break; + + default: + ble_write(b); + } +} + +async function send_report(report) { + if (!server || !server.connected || !txCharacteristic || connectionLostDetected) { + return; + } + + try { + let data = new Uint8Array(4 + 8 + 4); + data[0] = 1; // protocol_version + data[1] = 2; // descriptor_number (Switch gamepad) + data[2] = 8; // length + data[3] = 0; // report_id + data.set(report, 4); + const crc = crc32(new DataView(data.buffer), 12); + data[12] = (crc >> 0) & 0xFF; + data[13] = (crc >> 8) & 0xFF; + data[14] = (crc >> 16) & 0xFF; + data[15] = (crc >> 24) & 0xFF; + + ble_write(END); + for (let i = 0; i < 16; i++) { + send_escaped_byte(data[i]); + } + ble_write(END); + await flush(); + } catch (error) { + write(`Send error: ${error.message}\n`); + console.error('BLE send error:', error); + // Connection might be lost + if (error.name === 'NetworkError' || error.name === 'NotConnectedError') { + connectionLostDetected = true; + } + } +} + +async function loop() { + try { + clear_output(); + if (server && server.connected && !connectionLostDetected) { + write(`BLE CONNECTED (${device.name || 'Unknown'})\n\n`); + } else { + write("BLE NOT CONNECTED\n\n"); + } + + let b = false; + let a = false; + let y = false; + let x = false; + let l = false; + let r = false; + let zl = false; + let zr = false; + let minus = false; + let plus = false; + let ls = false; + let rs = false; + let home = false; + let capture = false; + let dpad_left = false; + let dpad_right = false; + let dpad_up = false; + let dpad_down = false; + let lx = 128; // Center stick positions + let ly = 128; + let rx = 128; + let ry = 128; + + // Process physical gamepads + for (const gamepad of navigator.getGamepads()) { + if (!gamepad) { + continue; + } + write(gamepad.id); + write("\n"); + if ((gamepad.mapping == 'standard') && !gamepad.id.includes('HID Receiver')) { + for (const b of gamepad.buttons) { + write(b.value); + write(" "); + } + for (const b of gamepad.axes) { + write(b); + write(" "); + } + write("\n"); + b |= gamepad.buttons[0].value; + a |= gamepad.buttons[1].value; + y |= gamepad.buttons[2].value; + x |= gamepad.buttons[3].value; + l |= gamepad.buttons[4].value; + r |= gamepad.buttons[5].value; + zl |= gamepad.buttons[6].value > 0.25; + zr |= gamepad.buttons[7].value > 0.25; + minus |= gamepad.buttons[8].value; + plus |= gamepad.buttons[9].value; + ls |= gamepad.buttons[10].value; + rs |= gamepad.buttons[11].value; + home |= gamepad.buttons[16].value; + dpad_up |= gamepad.buttons[12].value; + dpad_down |= gamepad.buttons[13].value; + dpad_left |= gamepad.buttons[14].value; + dpad_right |= gamepad.buttons[15].value; + + // Fix stick calculation - don't accumulate, convert from -1.0..1.0 to 0..255 + lx = Math.max(0, Math.min(255, Math.round(128 + gamepad.axes[0] * 127))); + ly = Math.max(0, Math.min(255, Math.round(128 + gamepad.axes[1] * 127))); + rx = Math.max(0, Math.min(255, Math.round(128 + gamepad.axes[2] * 127))); + ry = Math.max(0, Math.min(255, Math.round(128 + gamepad.axes[3] * 127))); + } else { + write("IGNORED\n"); + } + write("\n"); + } + + // Include virtual button states + a |= virtualButtons.a; + b |= virtualButtons.b; + x |= virtualButtons.x; + y |= virtualButtons.y; + l |= virtualButtons.l; + r |= virtualButtons.r; + zl |= virtualButtons.zl; + zr |= virtualButtons.zr; + minus |= virtualButtons.minus; + plus |= virtualButtons.plus; + home |= virtualButtons.home; + dpad_up |= virtualButtons.dpad_up; + dpad_down |= virtualButtons.dpad_down; + dpad_left |= virtualButtons.dpad_left; + dpad_right |= virtualButtons.dpad_right; + + // Show virtual button status + let virtualPressed = []; + for (const [key, value] of Object.entries(virtualButtons)) { + if (value) virtualPressed.push(key.toUpperCase()); + } + if (virtualPressed.length > 0) { + write(`VIRTUAL: ${virtualPressed.join(', ')} PRESSED\n`); + } + + let report = new Uint8Array(8); + + // Switch Pro Controller button layout: + // Byte 0: Y|B|A|X|L|R|ZL|ZR + // Byte 1: MINUS|PLUS|LS|RS|HOME|CAPTURE|0|0 + // Byte 2: D-pad (hat switch value) + // Byte 3: LX (left stick X) + // Byte 4: LY (left stick Y) + // Byte 5: RX (right stick X) + // Byte 6: RY (right stick Y) + // Byte 7: Reserved (0x00) + + report[0] = (y ? 1 : 0) | (b ? 2 : 0) | (a ? 4 : 0) | (x ? 8 : 0) | + (l ? 16 : 0) | (r ? 32 : 0) | (zl ? 64 : 0) | (zr ? 128 : 0); + report[1] = (minus ? 1 : 0) | (plus ? 2 : 0) | (ls ? 4 : 0) | (rs ? 8 : 0) | + (home ? 16 : 0) | (capture ? 32 : 0); + report[2] = dpad_lut[(dpad_left ? 1 : 0) | (dpad_right ? 2 : 0) | + (dpad_up ? 4 : 0) | (dpad_down ? 8 : 0)]; + report[3] = lx; + report[4] = ly; + report[5] = rx; + report[6] = ry; + report[7] = 0; // Reserved byte + + write("OUTPUT\n"); + for (let i = 0; i < 8; i++) { + write(report[i].toString(16).padStart(2, '0')); + write(" "); + } + write("\n"); + + // Send data regularly to keep connection alive + // Send every 3 seconds (375 loops × 8ms) to stay within the 4-second receiver timeout + keepAliveCounter++; + const shouldSendKeepAlive = keepAliveCounter >= 375; // ~3 seconds + + if (!reports_equal(prev_report, report) || shouldSendKeepAlive) { + await send_report(report); + prev_report = new Uint8Array(report); // Create a copy + + if (shouldSendKeepAlive) { + write("Keep-alive data sent (3s interval)\n"); + keepAliveCounter = 0; + } + } + } catch (e) { + console.log(e); + } +} + +function write(s) { + output.innerText += s; +} + +function clear_output() { + output.innerHTML = ''; +} + +function reports_equal(a, b) { + if (a.length != b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} \ No newline at end of file diff --git a/transmitter-ble-web/crc.js b/transmitter-ble-web/crc.js new file mode 100644 index 00000000..61ae74ba --- /dev/null +++ b/transmitter-ble-web/crc.js @@ -0,0 +1,53 @@ +const crc_table = [ + 0x0, 0x77073096, 0xEE0E612C, 0x990951BA, 0x76DC419, 0x706AF48F, 0xE963A535, + 0x9E6495A3, 0xEDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x9B64C2B, + 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, + 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, + 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, + 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, + 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, + 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, + 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, + 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, + 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x1DB7106, 0x98D220BC, + 0xEFD5102A, 0x71B18589, 0x6B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, + 0xF00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x86D3D2D, 0x91646C97, + 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, + 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, + 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, + 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, + 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, + 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, + 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, + 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, + 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x3B6E20C, 0x74B1D29A, 0xEAD54739, + 0x9DD277AF, 0x4DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0xD6D6A3E, + 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0xA00AE27, 0x7D079EB1, 0xF00F9344, + 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, + 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, + 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, + 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, + 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, + 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, + 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, + 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, + 0xEC63F226, 0x756AA39C, 0x26D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, + 0x5005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0xCB61B38, 0x92D28E9B, + 0xE5D5BE0D, 0x7CDCEFB7, 0xBDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, + 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, + 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, + 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, + 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, + 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, + 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, + 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, + 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D +]; + +export default function crc32(buf, length) { + let c = 0xffffffff; + for (let n = 0; n < length; n++) { + c = crc_table[(c ^ buf.getUint8(n)) & 0xff] ^ (c >>> 8); + } + return (c ^ 0xffffffff) >>> 0; +} diff --git a/transmitter-ble-web/index.html b/transmitter-ble-web/index.html new file mode 100644 index 00000000..84fced75 --- /dev/null +++ b/transmitter-ble-web/index.html @@ -0,0 +1,131 @@ + + + + + + + + + +

HID Transmitter (BLE) - Switch Gamepad

+

+ + +

+ +
+
+

Face Buttons

+ + + + +
+ +
+

D-Pad

+
+
+ +
+ +
+ +
+ +
+
+
+ +
+

Shoulder/System

+ + + + +
+ + + +
+
+ +

Keep this window visible. Virtual buttons work in toggle mode - click to turn on/off.

+

Note: Web Bluetooth requires HTTPS in Chrome. Use chrome://flags/#enable-experimental-web-platform-features for testing.

+
+    
+ + + \ No newline at end of file From 574fa5223245600a445b3cab64654c596b6afeb3 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 08:04:12 +0200 Subject: [PATCH 6/8] doing shit --- firmware-bluetooth/src/main.cc | 120 ++++++++++++++++++++++++++++----- transmitter-ble-web/code.js | 77 +++++++++++---------- 2 files changed, 146 insertions(+), 51 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index c9a835ef..1dda4d1d 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -70,6 +70,31 @@ typedef struct __attribute__((packed)) { uint8_t data[0]; } packet_t; +// Add after the packet structure definitions +typedef struct { + uint8_t report_id; + uint8_t len; + uint8_t data[64]; +} outgoing_report_t; + +#define OR_BUFSIZE 8 +static outgoing_report_t outgoing_reports[OR_BUFSIZE]; +static uint8_t or_head = 0; +static uint8_t or_tail = 0; +static uint8_t or_items = 0; + +static void queue_outgoing_report(uint8_t report_id, uint8_t* data, uint8_t len) { + if (or_items == OR_BUFSIZE) { + LOG_WRN("Report queue overflow!"); + return; + } + outgoing_reports[or_tail].report_id = report_id; + outgoing_reports[or_tail].len = len; + memcpy(outgoing_reports[or_tail].data, data, len); + or_tail = (or_tail + 1) % OR_BUFSIZE; + or_items++; +} + // Packet buffer and state for SLIP-like framing static uint8_t packet_buffer[SERIAL_MAX_PACKET_SIZE]; static uint16_t bytes_read = 0; @@ -555,18 +580,52 @@ static void peripheral_mode_init(void) { bytes_read = 0; escaped = false; - // Create a virtual device that can handle the packet protocol - // Use the current our_descriptor_number configuration + // Create a virtual transmitter device that represents the input source + // This should match the descriptor of the device that's transmitting to us uint16_t virtual_interface = 0xFF00; - // Parse the appropriate descriptor based on our_descriptor_number - if (our_descriptor_number < NOUR_DESCRIPTORS) { - parse_descriptor(0xCAFE, 0xBABE, - our_descriptors[our_descriptor_number].descriptor, - our_descriptors[our_descriptor_number].descriptor_length, - virtual_interface, 0); - device_connected_callback(virtual_interface, 0xCAFE, 0xBABE, 0); - } + // For peripheral mode, we need to set up a virtual input device descriptor + // that matches what the transmitter is sending. Use a standard gamepad descriptor + // that should work with most common input formats. + + // Use HID standard gamepad descriptor for the virtual transmitter + const uint8_t virtual_gamepad_descriptor[] = { + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x05, // Usage (Game Pad) + 0xa1, 0x01, // Collection (Application) + 0x85, 0x01, // Report ID (1) + 0x09, 0x01, // Usage (Pointer) + 0xa1, 0x00, // Collection (Physical) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x09, 0x32, // Usage (Z) + 0x09, 0x35, // Usage (Rz) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xff, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x04, // Report Count (4) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xc0, // End Collection + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (0x01) + 0x29, 0x10, // Usage Maximum (0x10) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x10, // Report Count (16) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0xc0, // End Collection + }; + + // Parse the virtual transmitter descriptor + parse_descriptor(0xCAFE, 0xBABE, + virtual_gamepad_descriptor, + sizeof(virtual_gamepad_descriptor), + virtual_interface, 0); + device_connected_callback(virtual_interface, 0xCAFE, 0xBABE, 0); + + // Force update of their descriptor derivates for the virtual device + their_descriptor_updated = true; // Set LED to indicate peripheral mode (blinking = advertising/waiting for connection) set_led_mode(LedMode::BLINK); @@ -574,7 +633,7 @@ static void peripheral_mode_init(void) { // Start advertising start_peripheral_advertising(); - LOG_INF("Peripheral mode initialized with packet protocol (descriptor %d)", our_descriptor_number); + LOG_INF("Peripheral mode initialized with virtual gamepad descriptor"); } static void start_peripheral_advertising(void) { @@ -622,19 +681,22 @@ static void handle_received_packet(const uint8_t* data, uint16_t len) { // Handle descriptor change if (msg->our_descriptor_number != our_descriptor_number) { our_descriptor_number = msg->our_descriptor_number; - // Persist config if needed + + // Signal that configuration needs to be updated + config_updated = true; + LOG_INF("Descriptor number changed to %d", our_descriptor_number); } - // Create HID report with report ID + // Create HID report with report ID for the remapper system uint8_t report[65]; report[0] = msg->report_id; memcpy(report + 1, msg->data, len); - // Inject into HID remapper system + // Inject into HID remapper system which will handle the actual sending handle_received_report(report, len + 1, 0xFF00, msg->report_id); - LOG_INF("Packet processed: proto=%d, desc=%d, report_id=%d, len=%d", + LOG_DBG("Packet processed: proto=%d, desc=%d, report_id=%d, len=%d", msg->protocol_version, msg->our_descriptor_number, msg->report_id, len); } @@ -1027,7 +1089,16 @@ static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uin len--; } if (interface == 0) { - return CHK(hid_int_ep_write(hid_dev0, report_with_id, len, NULL)); + // Try to send immediately + if (CHK(hid_int_ep_write(hid_dev0, report_with_id, len, NULL))) { + return true; + } else { + // Failed to send, queue it + if (len > 0) { + queue_outgoing_report(report_with_id[0], (uint8_t*)(report_with_id + 1), len - 1); + } + return false; + } } if (interface == 1) { return CHK(hid_int_ep_write(hid_dev1, report_with_id, len, NULL)); @@ -1283,6 +1354,23 @@ int main() { } } + // Process queued outgoing reports when USB HID is ready + if ((or_items > 0) && (k_sem_take(&usb_sem0, K_NO_WAIT) == 0)) { + uint8_t report_with_id[65]; + report_with_id[0] = outgoing_reports[or_head].report_id; + memcpy(report_with_id + 1, outgoing_reports[or_head].data, outgoing_reports[or_head].len); + + if (do_send_report(0, report_with_id, outgoing_reports[or_head].len + 1)) { + // Successfully sent, remove from queue + // Semaphore will be released by USB callback + or_head = (or_head + 1) % OR_BUFSIZE; + or_items--; + } else { + // Failed to send, give back semaphore + k_sem_give(&usb_sem0); + } + } + 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); diff --git a/transmitter-ble-web/code.js b/transmitter-ble-web/code.js index 7771ac86..e2868fdb 100644 --- a/transmitter-ble-web/code.js +++ b/transmitter-ble-web/code.js @@ -83,7 +83,7 @@ let server = null; let service = null; let txCharacteristic = null; let rxCharacteristic = null; -let prev_report = new Uint8Array([0, 0, 15, 0, 0, 0, 0, 0]); +let prev_report = new Uint8Array([128, 128, 128, 128, 0, 0]); // Updated to 6 bytes: neutral axes + no buttons let output; let keepAliveCounter = 0; let connectionLostDetected = false; @@ -285,20 +285,20 @@ async function send_report(report) { } try { - let data = new Uint8Array(4 + 8 + 4); + let data = new Uint8Array(4 + 6 + 4); // Changed from 8 to 6 bytes for report data[0] = 1; // protocol_version - data[1] = 2; // descriptor_number (Switch gamepad) - data[2] = 8; // length - data[3] = 0; // report_id + data[1] = 0; // descriptor_number (use virtual gamepad descriptor) + data[2] = 6; // length (changed from 8 to 6) + data[3] = 1; // report_id (changed from 0 to 1 to match virtual gamepad) data.set(report, 4); - const crc = crc32(new DataView(data.buffer), 12); - data[12] = (crc >> 0) & 0xFF; - data[13] = (crc >> 8) & 0xFF; - data[14] = (crc >> 16) & 0xFF; - data[15] = (crc >> 24) & 0xFF; + const crc = crc32(new DataView(data.buffer), 10); // Changed from 12 to 10 + data[10] = (crc >> 0) & 0xFF; // Changed indices + data[11] = (crc >> 8) & 0xFF; + data[12] = (crc >> 16) & 0xFF; + data[13] = (crc >> 24) & 0xFF; ble_write(END); - for (let i = 0; i < 16; i++) { + for (let i = 0; i < 14; i++) { // Changed from 16 to 14 send_escaped_byte(data[i]); } ble_write(END); @@ -417,32 +417,39 @@ async function loop() { write(`VIRTUAL: ${virtualPressed.join(', ')} PRESSED\n`); } - let report = new Uint8Array(8); - - // Switch Pro Controller button layout: - // Byte 0: Y|B|A|X|L|R|ZL|ZR - // Byte 1: MINUS|PLUS|LS|RS|HOME|CAPTURE|0|0 - // Byte 2: D-pad (hat switch value) - // Byte 3: LX (left stick X) - // Byte 4: LY (left stick Y) - // Byte 5: RX (right stick X) - // Byte 6: RY (right stick Y) - // Byte 7: Reserved (0x00) - - report[0] = (y ? 1 : 0) | (b ? 2 : 0) | (a ? 4 : 0) | (x ? 8 : 0) | - (l ? 16 : 0) | (r ? 32 : 0) | (zl ? 64 : 0) | (zr ? 128 : 0); - report[1] = (minus ? 1 : 0) | (plus ? 2 : 0) | (ls ? 4 : 0) | (rs ? 8 : 0) | - (home ? 16 : 0) | (capture ? 32 : 0); - report[2] = dpad_lut[(dpad_left ? 1 : 0) | (dpad_right ? 2 : 0) | - (dpad_up ? 4 : 0) | (dpad_down ? 8 : 0)]; - report[3] = lx; - report[4] = ly; - report[5] = rx; - report[6] = ry; - report[7] = 0; // Reserved byte + let report = new Uint8Array(6); // Changed from 8 to 6 bytes + + // Virtual gamepad format: + // Byte 0: X axis (left stick X) + // Byte 1: Y axis (left stick Y) + // Byte 2: Z axis (right stick X) + // Byte 3: Rz axis (right stick Y) + // Byte 4-5: 16 buttons packed into 2 bytes + + report[0] = lx; // X axis + report[1] = ly; // Y axis + report[2] = rx; // Z axis + report[3] = ry; // Rz axis + + // Pack 16 buttons into 2 bytes + // Buttons 0-7 in byte 4, buttons 8-15 in byte 5 + let buttons_low = (a ? 1 : 0) | (b ? 2 : 0) | (x ? 4 : 0) | (y ? 8 : 0) | + (l ? 16 : 0) | (r ? 32 : 0) | (zl ? 64 : 0) | (zr ? 128 : 0); + + let buttons_high = (minus ? 1 : 0) | (plus ? 2 : 0) | (ls ? 4 : 0) | (rs ? 8 : 0) | + (home ? 16 : 0) | (capture ? 32 : 0) | + (dpad_up ? 64 : 0) | (dpad_down ? 128 : 0); + + // Handle d-pad left/right by combining with other buttons or using different mapping + // Since we only have 16 buttons total, map dpad_left to ls and dpad_right to rs if not already used + if (dpad_left && !ls) buttons_high |= 4; // Use ls bit if not already pressed + if (dpad_right && !rs) buttons_high |= 8; // Use rs bit if not already pressed + + report[4] = buttons_low; + report[5] = buttons_high; write("OUTPUT\n"); - for (let i = 0; i < 8; i++) { + for (let i = 0; i < 6; i++) { // Changed from 8 to 6 write(report[i].toString(16).padStart(2, '0')); write(" "); } From 249a6d2cd63101b34c3e0e826c86ddd238512de5 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 13:07:51 +0200 Subject: [PATCH 7/8] working but descriptor still weird --- firmware-bluetooth/src/main.cc | 76 +++++--- transmitter-ble-web/code.js | 343 ++++++++++++++++----------------- transmitter-ble-web/index.html | 63 ++++-- 3 files changed, 268 insertions(+), 214 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 1dda4d1d..8dd9c9d0 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -588,32 +588,38 @@ static void peripheral_mode_init(void) { // that matches what the transmitter is sending. Use a standard gamepad descriptor // that should work with most common input formats. - // Use HID standard gamepad descriptor for the virtual transmitter + // Nintendo Switch Pro Controller compatible descriptor for 8-byte reports const uint8_t virtual_gamepad_descriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x05, // Usage (Game Pad) 0xa1, 0x01, // Collection (Application) - 0x85, 0x01, // Report ID (1) - 0x09, 0x01, // Usage (Pointer) - 0xa1, 0x00, // Collection (Physical) - 0x09, 0x30, // Usage (X) - 0x09, 0x31, // Usage (Y) - 0x09, 0x32, // Usage (Z) - 0x09, 0x35, // Usage (Rz) - 0x15, 0x00, // Logical Minimum (0) - 0x26, 0xff, 0x00, // Logical Maximum (255) - 0x75, 0x08, // Report Size (8) - 0x95, 0x04, // Report Count (4) - 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) - 0xc0, // End Collection + // Note: No Report ID specified = Report ID 0 (matches transmitter) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (0x01) 0x29, 0x10, // Usage Maximum (0x10) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) - 0x95, 0x10, // Report Count (16) + 0x95, 0x10, // Report Count (16) - 2 bytes of buttons 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x39, // Usage (Hat switch) - D-pad + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x0F, // Logical Maximum (15) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x09, 0x30, // Usage (X) - Left stick X + 0x09, 0x31, // Usage (Y) - Left stick Y + 0x09, 0x32, // Usage (Z) - Right stick X + 0x09, 0x35, // Usage (Rz) - Right stick Y + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xff, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x04, // Report Count (4) - 4 axes + 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x01, // Report Count (1) - 1 padding byte + 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xc0, // End Collection }; @@ -661,20 +667,25 @@ static void start_peripheral_advertising(void) { static void handle_received_packet(const uint8_t* data, uint16_t len) { if (len < sizeof(packet_t)) { - LOG_WRN("Packet too small: %d", len); + LOG_WRN("Packet too small: %d < %d", len, sizeof(packet_t)); return; } packet_t* msg = (packet_t*) data; - len = len - sizeof(packet_t); + uint16_t payload_len = len - sizeof(packet_t); + + LOG_DBG("Packet validation: proto=%d (expect %d), len=%d, payload=%d, desc=%d, report_id=%d", + msg->protocol_version, PROTOCOL_VERSION, msg->len, payload_len, + msg->our_descriptor_number, msg->report_id); if ((msg->protocol_version != PROTOCOL_VERSION) || - (msg->len != len) || - (len > 64) || + (msg->len != payload_len) || + (payload_len > 64) || (msg->our_descriptor_number >= NOUR_DESCRIPTORS) || - ((msg->report_id == 0) && (len >= 64))) { - LOG_WRN("Invalid packet: proto=%d, len=%d, desc=%d, report_id=%d", - msg->protocol_version, msg->len, msg->our_descriptor_number, msg->report_id); + ((msg->report_id == 0) && (payload_len >= 64))) { + LOG_WRN("Invalid packet: proto=%d (expect %d), len=%d vs %d, desc=%d (max %d), report_id=%d", + msg->protocol_version, PROTOCOL_VERSION, msg->len, payload_len, + msg->our_descriptor_number, NOUR_DESCRIPTORS, msg->report_id); return; } @@ -722,12 +733,15 @@ static void process_byte_with_framing(uint8_t c) { case END: if (bytes_read > 4) { uint32_t crc = crc32(packet_buffer, bytes_read - 4); - uint32_t received_crc = 0; - for (int i = 0; i < 4; i++) { - received_crc = (received_crc << 8) | packet_buffer[bytes_read - 1 - i]; - } + // Read CRC in little-endian format (matches transmitter) + uint32_t received_crc = + (packet_buffer[bytes_read - 4] << 0) | + (packet_buffer[bytes_read - 3] << 8) | + (packet_buffer[bytes_read - 2] << 16) | + (packet_buffer[bytes_read - 1] << 24); if (crc == received_crc) { handle_received_packet(packet_buffer, bytes_read - 4); + LOG_DBG("Packet received successfully, CRC: 0x%08X", crc); } else { LOG_WRN("CRC error: expected 0x%08X, got 0x%08X", crc, received_crc); } @@ -746,6 +760,7 @@ static void process_byte_with_framing(uint8_t c) { static void process_peripheral_command(uint8_t* buf, int count) { // Process each byte with SLIP-like framing + LOG_DBG("Received %d bytes from BLE", count); for (int i = 0; i < count; i++) { process_byte_with_framing(buf[i]); } @@ -1343,6 +1358,15 @@ int main() { their_descriptor_updated = false; } } + + // Peripheral mode functionality - process reports from BLE transmitter + if (current_mode == MODE_PERIPHERAL) { + // In peripheral mode, we also need to process periodic ticks for remapping + if (atomic_test_and_clear_bit(tick_pending, 0)) { + process_mapping(true); + process_pending = false; + } + } if (!k_sem_take(&usb_sem0, K_NO_WAIT)) { if (!send_report(do_send_report)) { k_sem_give(&usb_sem0); diff --git a/transmitter-ble-web/code.js b/transmitter-ble-web/code.js index e2868fdb..d7196dc7 100644 --- a/transmitter-ble-web/code.js +++ b/transmitter-ble-web/code.js @@ -26,53 +26,85 @@ let virtualButtons = { minus: false, plus: false, home: false, + ls: false, + rs: false, dpad_up: false, dpad_down: false, dpad_left: false, dpad_right: false }; +// Virtual analog stick states +let virtualSticks = { + lx: 128, + ly: 128, + rx: 128, + ry: 128 +}; + document.addEventListener("DOMContentLoaded", function () { document.getElementById("connect_ble").addEventListener("click", connect_ble); document.getElementById("disconnect_ble").addEventListener("click", disconnect_ble); - // Setup virtual button event handlers - setupVirtualButton("button_a", "a"); - setupVirtualButton("button_b", "b"); - setupVirtualButton("button_x", "x"); - setupVirtualButton("button_y", "y"); - setupVirtualButton("button_l", "l"); - setupVirtualButton("button_r", "r"); - setupVirtualButton("button_zl", "zl"); - setupVirtualButton("button_zr", "zr"); - setupVirtualButton("button_minus", "minus"); - setupVirtualButton("button_plus", "plus"); - setupVirtualButton("button_home", "home"); - setupVirtualButton("dpad_up", "dpad_up"); - setupVirtualButton("dpad_down", "dpad_down"); - setupVirtualButton("dpad_left", "dpad_left"); - setupVirtualButton("dpad_right", "dpad_right"); + // Add event handlers for all virtual buttons + const buttonMappings = { + 'button_a': 'a', + 'button_b': 'b', + 'button_x': 'x', + 'button_y': 'y', + 'button_l': 'l', + 'button_r': 'r', + 'button_zl': 'zl', + 'button_zr': 'zr', + 'button_minus': 'minus', + 'button_plus': 'plus', + 'button_home': 'home', + 'button_ls': 'ls', + 'button_rs': 'rs', + 'dpad_up': 'dpad_up', + 'dpad_down': 'dpad_down', + 'dpad_left': 'dpad_left', + 'dpad_right': 'dpad_right' + }; + + // Set up event handlers for each button + for (const [elementId, buttonKey] of Object.entries(buttonMappings)) { + const button = document.getElementById(elementId); + if (button) { + button.addEventListener("click", function(e) { + e.preventDefault(); + virtualButtons[buttonKey] = !virtualButtons[buttonKey]; // Toggle the state + + if (virtualButtons[buttonKey]) { + button.classList.add("pressed"); + } else { + button.classList.remove("pressed"); + } + }); + } + } + + // Add event handlers for analog stick sliders + const stickMappings = { + 'left_stick_x': 'lx', + 'left_stick_y': 'ly', + 'right_stick_x': 'rx', + 'right_stick_y': 'ry' + }; + + for (const [elementId, stickKey] of Object.entries(stickMappings)) { + const slider = document.getElementById(elementId); + if (slider) { + slider.addEventListener("input", function(e) { + virtualSticks[stickKey] = parseInt(e.target.value); + }); + } + } output = document.getElementById("output"); setInterval(loop, 8); }); -function setupVirtualButton(elementId, buttonKey) { - const button = document.getElementById(elementId); - if (button) { - button.addEventListener("click", function(e) { - e.preventDefault(); - virtualButtons[buttonKey] = !virtualButtons[buttonKey]; - - if (virtualButtons[buttonKey]) { - button.classList.add("pressed"); - } else { - button.classList.remove("pressed"); - } - }); - } -} - // Check if Web Bluetooth is supported if (!navigator.bluetooth) { document.body.innerHTML = '

Web Bluetooth API not supported

Please use Chrome/Edge with HTTPS or enable experimental features.

'; @@ -83,10 +115,8 @@ let server = null; let service = null; let txCharacteristic = null; let rxCharacteristic = null; -let prev_report = new Uint8Array([128, 128, 128, 128, 0, 0]); // Updated to 6 bytes: neutral axes + no buttons +let prev_report = new Uint8Array([0, 0, 15, 0, 0, 0, 0, 0, 0]); let output; -let keepAliveCounter = 0; -let connectionLostDetected = false; async function connect_ble() { try { @@ -102,9 +132,6 @@ async function connect_ble() { write(`Selected device: ${device.name || 'Unknown'}\n`); - // Add disconnection event listener - device.addEventListener('gattserverdisconnected', onDisconnected); - // Connect to GATT server server = await device.gatt.connect(); write("Connected to GATT server\n"); @@ -142,17 +169,7 @@ async function connect_ble() { document.getElementById("connect_ble").style.display = "none"; document.getElementById("disconnect_ble").style.display = "inline"; - // Reset connection state - connectionLostDetected = false; - keepAliveCounter = 0; - - write("BLE connection established!\n"); - - // Send initial data immediately to establish communication - write("Sending initial gamepad data...\n"); - const initialReport = new Uint8Array([0, 0, 15, 128, 128, 128, 128, 0]); // Neutral state - await send_report(initialReport); - write("Initial data sent successfully!\n\n"); + write("BLE connection established!\n\n"); } catch (error) { write(`Error: ${error.message}\n`); @@ -170,40 +187,12 @@ async function connect_ble() { } } -function onDisconnected() { - write("Device disconnected unexpectedly!\n"); - connectionLostDetected = true; - - // Clean up connection state - server = null; - service = null; - txCharacteristic = null; - rxCharacteristic = null; - - // Update UI - document.getElementById("connect_ble").style.display = "inline"; - document.getElementById("disconnect_ble").style.display = "none"; - - // Auto-reconnect after 2 seconds - setTimeout(() => { - if (device && connectionLostDetected) { - write("Attempting to reconnect...\n"); - connect_ble(); - } - }, 2000); -} - async function disconnect_ble() { try { - connectionLostDetected = false; // Prevent auto-reconnect - if (rxCharacteristic) { await rxCharacteristic.stopNotifications(); rxCharacteristic.removeEventListener('characteristicvaluechanged', handleNotification); } - if (device) { - device.removeEventListener('gattserverdisconnected', onDisconnected); - } if (server && server.connected) { server.disconnect(); } @@ -280,43 +269,34 @@ function send_escaped_byte(b) { } async function send_report(report) { - if (!server || !server.connected || !txCharacteristic || connectionLostDetected) { + if (!server || !server.connected || !txCharacteristic) { return; } - try { - let data = new Uint8Array(4 + 6 + 4); // Changed from 8 to 6 bytes for report - data[0] = 1; // protocol_version - data[1] = 0; // descriptor_number (use virtual gamepad descriptor) - data[2] = 6; // length (changed from 8 to 6) - data[3] = 1; // report_id (changed from 0 to 1 to match virtual gamepad) - data.set(report, 4); - const crc = crc32(new DataView(data.buffer), 10); // Changed from 12 to 10 - data[10] = (crc >> 0) & 0xFF; // Changed indices - data[11] = (crc >> 8) & 0xFF; - data[12] = (crc >> 16) & 0xFF; - data[13] = (crc >> 24) & 0xFF; - - ble_write(END); - for (let i = 0; i < 14; i++) { // Changed from 16 to 14 - send_escaped_byte(data[i]); - } - ble_write(END); - await flush(); - } catch (error) { - write(`Send error: ${error.message}\n`); - console.error('BLE send error:', error); - // Connection might be lost - if (error.name === 'NetworkError' || error.name === 'NotConnectedError') { - connectionLostDetected = true; - } + let data = new Uint8Array(4 + 8 + 4); + data[0] = 1; + data[1] = 2; + data[2] = 8; + data[3] = 0; + data.set(report, 4); + const crc = crc32(new DataView(data.buffer), 12); + data[12] = (crc >> 0) & 0xFF; + data[13] = (crc >> 8) & 0xFF; + data[14] = (crc >> 16) & 0xFF; + data[15] = (crc >> 24) & 0xFF; + + ble_write(END); + for (let i = 0; i < 16; i++) { + send_escaped_byte(data[i]); } + ble_write(END); + await flush(); } async function loop() { try { clear_output(); - if (server && server.connected && !connectionLostDetected) { + if (server && server.connected) { write(`BLE CONNECTED (${device.name || 'Unknown'})\n\n`); } else { write("BLE NOT CONNECTED\n\n"); @@ -340,7 +320,7 @@ async function loop() { let dpad_right = false; let dpad_up = false; let dpad_down = false; - let lx = 128; // Center stick positions + let lx = 128; let ly = 128; let rx = 128; let ry = 128; @@ -362,29 +342,27 @@ async function loop() { write(" "); } write("\n"); - b |= gamepad.buttons[0].value; - a |= gamepad.buttons[1].value; - y |= gamepad.buttons[2].value; - x |= gamepad.buttons[3].value; - l |= gamepad.buttons[4].value; - r |= gamepad.buttons[5].value; + b |= gamepad.buttons[0].pressed; + a |= gamepad.buttons[1].pressed; + y |= gamepad.buttons[2].pressed; + x |= gamepad.buttons[3].pressed; + l |= gamepad.buttons[4].pressed; + r |= gamepad.buttons[5].pressed; zl |= gamepad.buttons[6].value > 0.25; zr |= gamepad.buttons[7].value > 0.25; - minus |= gamepad.buttons[8].value; - plus |= gamepad.buttons[9].value; - ls |= gamepad.buttons[10].value; - rs |= gamepad.buttons[11].value; - home |= gamepad.buttons[16].value; - dpad_up |= gamepad.buttons[12].value; - dpad_down |= gamepad.buttons[13].value; - dpad_left |= gamepad.buttons[14].value; - dpad_right |= gamepad.buttons[15].value; - - // Fix stick calculation - don't accumulate, convert from -1.0..1.0 to 0..255 - lx = Math.max(0, Math.min(255, Math.round(128 + gamepad.axes[0] * 127))); - ly = Math.max(0, Math.min(255, Math.round(128 + gamepad.axes[1] * 127))); - rx = Math.max(0, Math.min(255, Math.round(128 + gamepad.axes[2] * 127))); - ry = Math.max(0, Math.min(255, Math.round(128 + gamepad.axes[3] * 127))); + minus |= gamepad.buttons[8].pressed; + plus |= gamepad.buttons[9].pressed; + ls |= gamepad.buttons[10].pressed; + rs |= gamepad.buttons[11].pressed; + home |= gamepad.buttons[16].pressed; + dpad_up |= gamepad.buttons[12].pressed; + dpad_down |= gamepad.buttons[13].pressed; + dpad_left |= gamepad.buttons[14].pressed; + dpad_right |= gamepad.buttons[15].pressed; + lx = Math.max(0, Math.min(255, 128 + gamepad.axes[0] * 128)); + ly = Math.max(0, Math.min(255, 128 + gamepad.axes[1] * 128)); + rx = Math.max(0, Math.min(255, 128 + gamepad.axes[2] * 128)); + ry = Math.max(0, Math.min(255, 128 + gamepad.axes[3] * 128)); } else { write("IGNORED\n"); } @@ -403,71 +381,88 @@ async function loop() { minus |= virtualButtons.minus; plus |= virtualButtons.plus; home |= virtualButtons.home; + ls |= virtualButtons.ls; + rs |= virtualButtons.rs; dpad_up |= virtualButtons.dpad_up; dpad_down |= virtualButtons.dpad_down; dpad_left |= virtualButtons.dpad_left; dpad_right |= virtualButtons.dpad_right; + // Include virtual analog stick values (only if no physical gamepad is moving them) + if (navigator.getGamepads().every(gamepad => !gamepad || gamepad.mapping !== 'standard' || gamepad.id.includes('HID Receiver'))) { + lx = virtualSticks.lx; + ly = virtualSticks.ly; + rx = virtualSticks.rx; + ry = virtualSticks.ry; + } + // Show virtual button status - let virtualPressed = []; - for (const [key, value] of Object.entries(virtualButtons)) { - if (value) virtualPressed.push(key.toUpperCase()); + if (virtualButtons.a) { + write("VIRTUAL: Button A PRESSED\n"); } - if (virtualPressed.length > 0) { - write(`VIRTUAL: ${virtualPressed.join(', ')} PRESSED\n`); + if (virtualButtons.b) { + write("VIRTUAL: Button B PRESSED\n"); + } + if (virtualButtons.x) { + write("VIRTUAL: Button X PRESSED\n"); + } + if (virtualButtons.y) { + write("VIRTUAL: Button Y PRESSED\n"); + } + if (virtualButtons.l) { + write("VIRTUAL: Button L PRESSED\n"); + } + if (virtualButtons.r) { + write("VIRTUAL: Button R PRESSED\n"); + } + if (virtualButtons.zl) { + write("VIRTUAL: Button ZL PRESSED\n"); + } + if (virtualButtons.zr) { + write("VIRTUAL: Button ZR PRESSED\n"); + } + if (virtualButtons.minus) { + write("VIRTUAL: Button MINUS PRESSED\n"); + } + if (virtualButtons.plus) { + write("VIRTUAL: Button PLUS PRESSED\n"); + } + if (virtualButtons.home) { + write("VIRTUAL: Button HOME PRESSED\n"); + } + if (virtualButtons.dpad_up) { + write("VIRTUAL: D-Pad UP PRESSED\n"); + } + if (virtualButtons.dpad_down) { + write("VIRTUAL: D-Pad DOWN PRESSED\n"); + } + if (virtualButtons.dpad_left) { + write("VIRTUAL: D-Pad LEFT PRESSED\n"); + } + if (virtualButtons.dpad_right) { + write("VIRTUAL: D-Pad RIGHT PRESSED\n"); } - let report = new Uint8Array(6); // Changed from 8 to 6 bytes - - // Virtual gamepad format: - // Byte 0: X axis (left stick X) - // Byte 1: Y axis (left stick Y) - // Byte 2: Z axis (right stick X) - // Byte 3: Rz axis (right stick Y) - // Byte 4-5: 16 buttons packed into 2 bytes - - report[0] = lx; // X axis - report[1] = ly; // Y axis - report[2] = rx; // Z axis - report[3] = ry; // Rz axis + let report = new Uint8Array(8); - // Pack 16 buttons into 2 bytes - // Buttons 0-7 in byte 4, buttons 8-15 in byte 5 - let buttons_low = (a ? 1 : 0) | (b ? 2 : 0) | (x ? 4 : 0) | (y ? 8 : 0) | - (l ? 16 : 0) | (r ? 32 : 0) | (zl ? 64 : 0) | (zr ? 128 : 0); - - let buttons_high = (minus ? 1 : 0) | (plus ? 2 : 0) | (ls ? 4 : 0) | (rs ? 8 : 0) | - (home ? 16 : 0) | (capture ? 32 : 0) | - (dpad_up ? 64 : 0) | (dpad_down ? 128 : 0); - - // Handle d-pad left/right by combining with other buttons or using different mapping - // Since we only have 16 buttons total, map dpad_left to ls and dpad_right to rs if not already used - if (dpad_left && !ls) buttons_high |= 4; // Use ls bit if not already pressed - if (dpad_right && !rs) buttons_high |= 8; // Use rs bit if not already pressed - - report[4] = buttons_low; - report[5] = buttons_high; + report[0] = (y << 0) | (b << 1) | (a << 2) | (x << 3) | (l << 4) | (r << 5) | (zl << 6) | (zr << 7); + report[1] = (minus << 0) | (plus << 1) | (ls << 2) | (rs << 3) | (home << 4) | (capture << 5); + report[2] = dpad_lut[(dpad_left << 0) | (dpad_right << 1) | (dpad_up << 2) | (dpad_down << 3)]; + report[3] = Math.max(0, Math.min(255, lx)); + report[4] = Math.max(0, Math.min(255, ly)); + report[5] = Math.max(0, Math.min(255, rx)); + report[6] = Math.max(0, Math.min(255, ry)); write("OUTPUT\n"); - for (let i = 0; i < 6; i++) { // Changed from 8 to 6 + for (let i = 0; i < 8; i++) { write(report[i].toString(16).padStart(2, '0')); write(" "); } write("\n"); - // Send data regularly to keep connection alive - // Send every 3 seconds (375 loops × 8ms) to stay within the 4-second receiver timeout - keepAliveCounter++; - const shouldSendKeepAlive = keepAliveCounter >= 375; // ~3 seconds - - if (!reports_equal(prev_report, report) || shouldSendKeepAlive) { + if (!reports_equal(prev_report, report)) { await send_report(report); - prev_report = new Uint8Array(report); // Create a copy - - if (shouldSendKeepAlive) { - write("Keep-alive data sent (3s interval)\n"); - keepAliveCounter = 0; - } + prev_report = report; } } catch (e) { console.log(e); diff --git a/transmitter-ble-web/index.html b/transmitter-ble-web/index.html index 84fced75..5f088504 100644 --- a/transmitter-ble-web/index.html +++ b/transmitter-ble-web/index.html @@ -88,10 +88,14 @@

HID Transmitter (BLE) - Switch Gamepad

Face Buttons

- - - - +
+
+ + + +
+ +
@@ -111,19 +115,50 @@

D-Pad

Shoulder/System

- - - - -
- - - +
+ + + + +
+
+ + + +
-

Keep this window visible. Virtual buttons work in toggle mode - click to turn on/off.

-

Note: Web Bluetooth requires HTTPS in Chrome. Use chrome://flags/#enable-experimental-web-platform-features for testing.

+
+
+

Analog Sticks

+
+
Left Stick
+ + +
+ +
+
+ +
+

Right Stick

+
+
Right Stick
+ + +
+ +
+
+ +
+

Instructions

+

Virtual buttons work in toggle mode - click to turn on/off.

+

Physical gamepads are also supported.

+

Note: Web Bluetooth requires HTTPS in Chrome.

+
+
     
From cdd2d4fff76be04738ba1b5e4bea8d5943df4ea2 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 14:07:25 +0200 Subject: [PATCH 8/8] working --- firmware-bluetooth/src/main.cc | 100 ++++++++++++++++++++++----------- transmitter-ble-web/code.js | 8 +-- 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 8dd9c9d0..74d17dc2 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -566,6 +566,28 @@ static void button_cb(const struct device* dev, struct gpio_callback* cb, uint32 } } +// Virtual gamepad neutral state following horipad pattern +static const uint8_t virtual_gamepad_neutral[] = { 0x00, 0x00, 0x0F, 0x80, 0x80, 0x80, 0x80, 0x00 }; + +// Virtual gamepad helper functions following horipad pattern +static void virtual_gamepad_clear_report(uint8_t* report, uint8_t report_id, uint16_t len) { + memcpy(report, virtual_gamepad_neutral, sizeof(virtual_gamepad_neutral)); +} + +static int32_t virtual_gamepad_default_value(uint32_t usage) { + switch (usage) { + case 0x00010039: // Hat switch + return 15; // Neutral hat switch position (matches horipad) + case 0x00010030: // X axis + case 0x00010031: // Y axis + case 0x00010032: // Z axis + case 0x00010035: // Rz axis + return 0x80; // Center position for analog sticks (matches horipad) + default: + return 0; + } +} + // Peripheral mode implementation functions static void peripheral_mode_init(void) { // Stop host mode scanning @@ -582,53 +604,71 @@ static void peripheral_mode_init(void) { // Create a virtual transmitter device that represents the input source // This should match the descriptor of the device that's transmitting to us - uint16_t virtual_interface = 0xFF00; + uint16_t virtual_interface = 0xFF00; // Will be changed to actual interface after parsing // For peripheral mode, we need to set up a virtual input device descriptor // that matches what the transmitter is sending. Use a standard gamepad descriptor // that should work with most common input formats. - // Nintendo Switch Pro Controller compatible descriptor for 8-byte reports - const uint8_t virtual_gamepad_descriptor[] = { + // Virtual gamepad descriptor with Report ID to match firmware format expectations + static const uint8_t virtual_gamepad_descriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x05, // Usage (Game Pad) - 0xa1, 0x01, // Collection (Application) - // Note: No Report ID specified = Report ID 0 (matches transmitter) - 0x05, 0x09, // Usage Page (Button) - 0x19, 0x01, // Usage Minimum (0x01) - 0x29, 0x10, // Usage Maximum (0x10) + 0xA1, 0x01, // Collection (Application) + 0x85, 0x01, // Report ID (1) - Add report ID to match firmware format 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) + 0x35, 0x00, // Physical Minimum (0) + 0x45, 0x01, // Physical Maximum (1) 0x75, 0x01, // Report Size (1) - 0x95, 0x10, // Report Count (16) - 2 bytes of buttons + 0x95, 0x0E, // Report Count (14) - 14 buttons like horipad + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (0x01) + 0x29, 0x0E, // Usage Maximum (0x0E) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x95, 0x02, // Report Count (2) - 2-bit padding like horipad + 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) - 0x09, 0x39, // Usage (Hat switch) - D-pad - 0x15, 0x00, // Logical Minimum (0) - 0x25, 0x0F, // Logical Maximum (15) - 0x75, 0x08, // Report Size (8) + 0x25, 0x0F, // Logical Maximum (15) - to match transmitter dpad_lut + 0x46, 0x3B, 0x01, // Physical Maximum (315) + 0x75, 0x04, // Report Size (4) 0x95, 0x01, // Report Count (1) - 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) - 0x09, 0x30, // Usage (X) - Left stick X - 0x09, 0x31, // Usage (Y) - Left stick Y - 0x09, 0x32, // Usage (Z) - Right stick X - 0x09, 0x35, // Usage (Rz) - Right stick Y - 0x15, 0x00, // Logical Minimum (0) - 0x26, 0xff, 0x00, // Logical Maximum (255) + 0x65, 0x14, // Unit (System: English Rotation, Length: Centimeter) + 0x09, 0x39, // Usage (Hat switch) + 0x81, 0x42, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State) + 0x65, 0x00, // Unit (None) + 0x95, 0x01, // Report Count (1) - 4-bit padding + 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x26, 0xFF, 0x00, // Logical Maximum (255) + 0x46, 0xFF, 0x00, // Physical Maximum (255) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x09, 0x32, // Usage (Z) + 0x09, 0x35, // Usage (Rz) 0x75, 0x08, // Report Size (8) - 0x95, 0x04, // Report Count (4) - 4 axes + 0x95, 0x04, // Report Count (4) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) + 0x75, 0x08, // Report Size (8) - like horipad 0x95, 0x01, // Report Count (1) - 1 padding byte 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) - 0xc0, // End Collection + 0xC0, // End Collection }; - // Parse the virtual transmitter descriptor - parse_descriptor(0xCAFE, 0xBABE, + // Parse the virtual transmitter descriptor following horipad pattern + // Use interface 0 (like first real HID interface in host mode) + virtual_interface = 0x0000; // Use proper interface 0x0000 + + // CRITICAL: Clear any existing interface indexes to ensure virtual device gets index 0 + // This prevents button offset issues where virtual device gets assigned index 8 + // and buttons appear as 9,10,11,12 instead of 1,2,3,4 + interface_index_in_use = 0; + interface_index.clear(); + + parse_descriptor(0x0F0D, 0x00C1, // Use horipad VID/PID as reference virtual_gamepad_descriptor, sizeof(virtual_gamepad_descriptor), virtual_interface, 0); - device_connected_callback(virtual_interface, 0xCAFE, 0xBABE, 0); + device_connected_callback(virtual_interface, 0x0F0D, 0x00C1, 0); // Force update of their descriptor derivates for the virtual device their_descriptor_updated = true; @@ -699,13 +739,9 @@ static void handle_received_packet(const uint8_t* data, uint16_t len) { LOG_INF("Descriptor number changed to %d", our_descriptor_number); } - // Create HID report with report ID for the remapper system - uint8_t report[65]; - report[0] = msg->report_id; - memcpy(report + 1, msg->data, len); - - // Inject into HID remapper system which will handle the actual sending - handle_received_report(report, len + 1, 0xFF00, msg->report_id); + // Pass the raw gamepad data with external report ID + // Don't prepend report_id since it's passed as external_report_id parameter + handle_received_report(msg->data, len, 0x0000, msg->report_id); LOG_DBG("Packet processed: proto=%d, desc=%d, report_id=%d, len=%d", msg->protocol_version, msg->our_descriptor_number, msg->report_id, len); diff --git a/transmitter-ble-web/code.js b/transmitter-ble-web/code.js index d7196dc7..a3934e9c 100644 --- a/transmitter-ble-web/code.js +++ b/transmitter-ble-web/code.js @@ -274,10 +274,10 @@ async function send_report(report) { } let data = new Uint8Array(4 + 8 + 4); - data[0] = 1; - data[1] = 2; - data[2] = 8; - data[3] = 0; + data[0] = 1; // protocol version + data[1] = 2; // descriptor number + data[2] = 8; // length + data[3] = 1; // report_id (now 1 to match virtual gamepad descriptor) data.set(report, 4); const crc = crc32(new DataView(data.buffer), 12); data[12] = (crc >> 0) & 0xFF;