From 89aab00c2db39e7c59ad67ea0684b7eab8f59efc Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Thu, 19 Jun 2025 08:41:18 +0200 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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; From 8435ba1ebb332c421495d25488ee21cf09c16957 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 17:09:27 +0200 Subject: [PATCH 09/21] Enhance dual mode functionality for Bluetooth firmware - Updated the Bluetooth firmware to support simultaneous host and peripheral modes. - Introduced coordination flags to manage scanning and connection states between modes. - Implemented LED status updates to reflect active modes and connections. - Added JavaScript functionality for enabling physical gamepad forwarding in the web interface. - Improved handling of gamepad input processing based on forwarding state. - Updated HTML to include a toggle for enabling gamepad forwarding. --- firmware-bluetooth/src/main.cc | 281 ++++++++++++++++++++++----------- transmitter-ble-web/code.js | 90 ++++++----- transmitter-ble-web/index.html | 7 +- 3 files changed, 245 insertions(+), 133 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 74d17dc2..8d4c0b2d 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -32,13 +32,9 @@ 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; +// Both modes active simultaneously +static bool host_mode_enabled = true; // Always start enabled +static bool peripheral_mode_enabled = true; // Nordic UART Service UUID - using standard Nordic UUIDs for Web Bluetooth compatibility static struct bt_uuid_128 uart_service_uuid = BT_UUID_INIT_128( @@ -104,6 +100,8 @@ 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 void update_led_status(void); +static void enable_dual_mode(void); 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); @@ -211,6 +209,10 @@ static bool peers_only = true; static struct bt_le_conn_param* conn_param = BT_LE_CONN_PARAM(12, 24, 0, 3000); +// Coordination flags for peripheral-host mode interaction +static volatile bool request_scan_stop = false; +static volatile bool request_scan_resume = false; + static void activity_led_off_work_fn(struct k_work* work) { gpio_pin_set_dt(&led0, false); } @@ -269,11 +271,13 @@ 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) { + if (peripheral_mode_enabled) { process_peripheral_command((uint8_t*)buf, len); } return len; @@ -292,38 +296,39 @@ static void peripheral_connected(struct bt_conn *conn, uint8_t err) return; } - if (current_mode == MODE_PERIPHERAL) { + if (peripheral_mode_enabled) { peripheral_conn = bt_conn_ref(conn); LOG_INF("Peripheral connected"); - set_led_mode(LedMode::ON); + update_led_status(); - // 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"); + // CRITICAL FIX: Stop host scanning when peripheral connects to prevent interference + if (host_mode_enabled && scanning) { + LOG_INF("Stopping host scanning to avoid peripheral interference"); + request_scan_stop = true; } + + // Don't aggressively update connection parameters - let them stabilize + // The original working version didn't force parameter updates + LOG_INF("Peripheral connection established successfully"); } } static void peripheral_disconnected(struct bt_conn *conn, uint8_t reason) { - if (current_mode == MODE_PERIPHERAL && conn == peripheral_conn) { + if (peripheral_mode_enabled && conn == peripheral_conn) { LOG_INF("Peripheral disconnected (reason %u)", reason); bt_conn_unref(peripheral_conn); peripheral_conn = NULL; - set_led_mode(LedMode::BLINK); + update_led_status(); // Restart advertising start_peripheral_advertising(); + + // CRITICAL FIX: Resume host scanning when peripheral disconnects + if (host_mode_enabled && !scanning) { + LOG_INF("Resuming host scanning after peripheral disconnect"); + request_scan_resume = true; + } } } @@ -366,6 +371,34 @@ static int count_connections() { return conn_count; } +static void update_led_status() { + // LED status indication for dual mode: + // Solid ON: Both modes active with connections + // Fast blink: Host scanning or peripheral advertising + // Slow blink: Only one mode active + // OFF: No modes active (shouldn't happen normally) + + bool host_active = host_mode_enabled && (scanning || count_connections() > 0); + bool peripheral_active = peripheral_mode_enabled && (peripheral_conn != NULL); + + if (host_active && peripheral_active) { + // Both modes with connections - solid on + set_led_mode(LedMode::ON); + } else if (host_mode_enabled && peripheral_mode_enabled) { + // Both modes enabled but not all connected - fast blink + atomic_set(&led_blink_count, 2); // Fast blink pattern + set_led_mode(LedMode::BLINK); + } else if (host_active || peripheral_active) { + // Only one mode active - slow blink + atomic_set(&led_blink_count, 1); // Slow blink pattern + set_led_mode(LedMode::BLINK); + } else { + // No active connections - blink to show searching/advertising + atomic_set(&led_blink_count, 3); // Different pattern for searching + set_led_mode(LedMode::BLINK); + } +} + static bool scan_setup_filters() { bt_scan_filter_remove_all(); @@ -400,9 +433,18 @@ 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"); + // Don't scan if host mode is disabled + if (!host_mode_enabled) { + LOG_DBG("Skipping scan - host mode disabled"); + return; + } + + // CRITICAL: Stop scanning when peripheral has active connection to avoid interference + if (peripheral_mode_enabled && peripheral_conn) { + LOG_DBG("Stopping scan - peripheral mode has active connection to avoid interference"); + if (scanning) { + scan_stop(); + } return; } @@ -411,9 +453,9 @@ static void scan_start_work_fn(struct k_work* work) { } if (scan_setup_filters()) { scan_start(); - set_led_mode(peers_only ? LedMode::BLINK : LedMode::ON); + update_led_status(); } else { - set_led_mode(LedMode::BLINK); + update_led_status(); } } static K_WORK_DELAYABLE_DEFINE(scan_start_work, scan_start_work_fn); @@ -550,18 +592,12 @@ static void button_cb(const struct device* dev, struct gpio_callback* cb, uint32 button_pressed_at = k_uptime_get(); } else { if (k_uptime_get() - button_pressed_at > CLEAR_BONDS_BUTTON_PRESS_MS) { + // Long press: clear bonds clear_bonds(); } else { - // 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(); - } + // Short press: pair new device (host mode always enabled) + LOG_INF("Pairing new device"); + pair_new_device(); } } } @@ -590,25 +626,27 @@ static int32_t virtual_gamepad_default_value(uint32_t usage) { // Peripheral mode implementation functions static void peripheral_mode_init(void) { - // Stop host mode scanning - if (scanning) { - scan_stop(); + // Only stop scanning and disconnect if host mode is completely disabled + // When both modes are enabled, let them coexist + if (!host_mode_enabled) { + if (scanning) { + scan_stop(); + } + bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_conn, NULL); } - // Disconnect any existing host connections - bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_conn, NULL); - // Initialize packet reception state bytes_read = 0; escaped = false; // 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; // Will be changed to actual interface after parsing + uint16_t virtual_interface = 0xFF00; // Use proper interface 0x0000 - // 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. + // 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(); // Virtual gamepad descriptor with Report ID to match firmware format expectations static const uint8_t virtual_gamepad_descriptor[] = { @@ -654,16 +692,6 @@ static void peripheral_mode_init(void) { 0xC0, // End Collection }; - // 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), @@ -673,11 +701,10 @@ static void peripheral_mode_init(void) { // 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); - - // Start advertising - start_peripheral_advertising(); + // Start advertising if peripheral mode is enabled + if (peripheral_mode_enabled) { + start_peripheral_advertising(); + } LOG_INF("Peripheral mode initialized with virtual gamepad descriptor"); } @@ -813,8 +840,8 @@ 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); - // Only restart scanning if we're in host mode - if (current_mode == MODE_HOST) { + // Only restart scanning if host mode is enabled + if (host_mode_enabled) { k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } return; @@ -843,8 +870,8 @@ static void disconnected(struct bt_conn* conn, uint8_t reason) { count_connections(); - // Only restart scanning if we're in host mode - if (current_mode == MODE_HOST) { + // Only restart scanning if host mode is enabled + if (host_mode_enabled) { k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } } @@ -857,8 +884,8 @@ 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; - // Only start discovery if we're in host mode - if (current_mode == MODE_HOST) { + // Only start discovery if host mode is enabled + if (host_mode_enabled) { gatt_discover(conn); } } else { @@ -921,11 +948,11 @@ static uint8_t hogp_notify_cb(struct bt_hogp* hogp, struct bt_hogp_rep_info* rep return BT_GATT_ITER_STOP; } - // Don't aggressively restart scanning if we're in peripheral mode - if (scanning && current_mode != MODE_PERIPHERAL) { + // Manage scanning based on host mode status + if (scanning && host_mode_enabled) { 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 if (current_mode != MODE_PERIPHERAL) { + } else if (host_mode_enabled) { k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } @@ -1299,6 +1326,58 @@ void clear_bonds() { k_work_submit(&clear_bonds_work); } +void enable_host_mode() { + if (!host_mode_enabled) { + host_mode_enabled = true; + LOG_INF("Host mode enabled"); + k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + } +} + +void disable_host_mode() { + if (host_mode_enabled) { + host_mode_enabled = false; + LOG_INF("Host mode disabled"); + if (scanning) { + scan_stop(); + } + // Disconnect host connections + bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_conn, NULL); + } +} + +void enable_peripheral_mode() { + if (!peripheral_mode_enabled) { + peripheral_mode_enabled = true; + LOG_INF("Peripheral mode enabled"); + peripheral_mode_init(); + } +} + +void disable_peripheral_mode() { + if (peripheral_mode_enabled) { + peripheral_mode_enabled = false; + LOG_INF("Peripheral mode disabled"); + // Stop advertising + CHK(bt_le_adv_stop()); + // Disconnect peripheral connection + if (peripheral_conn) { + CHK(bt_conn_disconnect(peripheral_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN)); + } + } +} + +void enable_dual_mode() { + // Enable both modes for full functionality + if (!peripheral_mode_enabled) { + enable_peripheral_mode(); + } + if (!host_mode_enabled) { + enable_host_mode(); + } + LOG_INF("Dual mode enabled - both host and peripheral active"); +} + void my_mutexes_init() { for (int i = 0; i < (int8_t) MutexId::N; i++) { k_mutex_init(&mutexes[i]); @@ -1352,12 +1431,17 @@ int main() { parse_our_descriptor(); set_mapping_from_config(); - // Start in the appropriate mode - if (current_mode == MODE_HOST) { - k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); - } else { + // Initialize peripheral mode first (like original) + if (peripheral_mode_enabled) { peripheral_mode_init(); } + + // Start host mode (always enabled) + if (host_mode_enabled) { + k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + } + + LOG_INF("Initialization complete - dual mode active (peripheral:enabled, host:enabled)"); struct report_type incoming_report; struct descriptor_type incoming_descriptor; @@ -1366,18 +1450,15 @@ int main() { static uint8_t get_report_tmp_buf[64]; bool process_pending = false; bool get_report_response_pending = false; + int64_t last_led_update = 0; while (true) { - // Host mode functionality - if (current_mode == MODE_HOST) { + // Host mode functionality - always process if enabled + if (host_mode_enabled) { 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); @@ -1395,13 +1476,29 @@ int main() { } } - // 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; - } + // CRITICAL: Process peripheral-host coordination flags + if (request_scan_stop && scanning) { + LOG_INF("Processing scan stop request from peripheral connection"); + scan_stop(); + request_scan_stop = false; + } + if (request_scan_resume && !scanning && host_mode_enabled) { + LOG_INF("Processing scan resume request from peripheral disconnection"); + k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + request_scan_resume = false; + } + + // Process periodic ticks for remapping (needed for both modes) + if (atomic_test_and_clear_bit(tick_pending, 0)) { + process_mapping(true); + process_pending = false; + } + + // Update LED status periodically (every 5 seconds) + int64_t current_time = k_uptime_get(); + if (current_time - last_led_update > 5000) { + update_led_status(); + last_led_update = current_time; } if (!k_sem_take(&usb_sem0, K_NO_WAIT)) { if (!send_report(do_send_report)) { diff --git a/transmitter-ble-web/code.js b/transmitter-ble-web/code.js index a3934e9c..4bf8015d 100644 --- a/transmitter-ble-web/code.js +++ b/transmitter-ble-web/code.js @@ -42,10 +42,18 @@ let virtualSticks = { ry: 128 }; +// Gamepad forwarding state +let gamepadForwardingEnabled = false; + document.addEventListener("DOMContentLoaded", function () { document.getElementById("connect_ble").addEventListener("click", connect_ble); document.getElementById("disconnect_ble").addEventListener("click", disconnect_ble); + // Add event handler for gamepad forwarding toggle + document.getElementById("enable_gamepad").addEventListener("change", function(e) { + gamepadForwardingEnabled = e.target.checked; + }); + // Add event handlers for all virtual buttons const buttonMappings = { 'button_a': 'a', @@ -325,48 +333,50 @@ async function loop() { 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(" "); + // Process physical gamepads (if enabled) + if (gamepadForwardingEnabled) { + for (const gamepad of navigator.getGamepads()) { + if (!gamepad) { + continue; } - for (const b of gamepad.axes) { - write(b); - write(" "); + 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].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].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"); } write("\n"); - 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].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"); } - write("\n"); } // Include virtual button states @@ -388,8 +398,8 @@ async function loop() { 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'))) { + // Include virtual analog stick values (only if no physical gamepad is moving them or gamepad forwarding is disabled) + if (!gamepadForwardingEnabled || navigator.getGamepads().every(gamepad => !gamepad || gamepad.mapping !== 'standard' || gamepad.id.includes('HID Receiver'))) { lx = virtualSticks.lx; ly = virtualSticks.ly; rx = virtualSticks.rx; diff --git a/transmitter-ble-web/index.html b/transmitter-ble-web/index.html index 5f088504..42b88ca1 100644 --- a/transmitter-ble-web/index.html +++ b/transmitter-ble-web/index.html @@ -155,7 +155,12 @@

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 2bfaab02e27e7f3741005168fe540ac20c8a03ad Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 17:38:56 +0200 Subject: [PATCH 10/21] working dual mode --- firmware-bluetooth/src/main.cc | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 8d4c0b2d..04008da2 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -209,10 +209,6 @@ static bool peers_only = true; static struct bt_le_conn_param* conn_param = BT_LE_CONN_PARAM(12, 24, 0, 3000); -// Coordination flags for peripheral-host mode interaction -static volatile bool request_scan_stop = false; -static volatile bool request_scan_resume = false; - static void activity_led_off_work_fn(struct k_work* work) { gpio_pin_set_dt(&led0, false); } @@ -301,12 +297,6 @@ static void peripheral_connected(struct bt_conn *conn, uint8_t err) LOG_INF("Peripheral connected"); update_led_status(); - // CRITICAL FIX: Stop host scanning when peripheral connects to prevent interference - if (host_mode_enabled && scanning) { - LOG_INF("Stopping host scanning to avoid peripheral interference"); - request_scan_stop = true; - } - // Don't aggressively update connection parameters - let them stabilize // The original working version didn't force parameter updates LOG_INF("Peripheral connection established successfully"); @@ -323,12 +313,6 @@ static void peripheral_disconnected(struct bt_conn *conn, uint8_t reason) // Restart advertising start_peripheral_advertising(); - - // CRITICAL FIX: Resume host scanning when peripheral disconnects - if (host_mode_enabled && !scanning) { - LOG_INF("Resuming host scanning after peripheral disconnect"); - request_scan_resume = true; - } } } @@ -640,7 +624,7 @@ static void peripheral_mode_init(void) { escaped = false; // Create a virtual transmitter device that represents the input source - uint16_t virtual_interface = 0xFF00; // Use proper interface 0x0000 + uint16_t 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 @@ -1476,18 +1460,6 @@ int main() { } } - // CRITICAL: Process peripheral-host coordination flags - if (request_scan_stop && scanning) { - LOG_INF("Processing scan stop request from peripheral connection"); - scan_stop(); - request_scan_stop = false; - } - if (request_scan_resume && !scanning && host_mode_enabled) { - LOG_INF("Processing scan resume request from peripheral disconnection"); - k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); - request_scan_resume = false; - } - // Process periodic ticks for remapping (needed for both modes) if (atomic_test_and_clear_bit(tick_pending, 0)) { process_mapping(true); From 652f420b2dcbadeaa576e179714ba764a4c1b9bc Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 18:48:53 +0200 Subject: [PATCH 11/21] remove useless comments --- firmware-bluetooth/prj.conf | 3 - firmware-bluetooth/src/main.cc | 180 +++++++++++++++------------------ 2 files changed, 81 insertions(+), 102 deletions(-) diff --git a/firmware-bluetooth/prj.conf b/firmware-bluetooth/prj.conf index 1b915f2a..cb1cca00 100644 --- a/firmware-bluetooth/prj.conf +++ b/firmware-bluetooth/prj.conf @@ -45,11 +45,8 @@ CONFIG_BT_GATT_DM_MAX_ATTRS=100 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 04008da2..4a553bc0 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -26,17 +26,16 @@ #include "our_descriptor.h" #include "platform.h" #include "remapper.h" -#include "crc.h" // If you have this from your receiver project +#include "crc.h" 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; }) -// Both modes active simultaneously -static bool host_mode_enabled = true; // Always start enabled +static bool host_mode_enabled = true; static bool peripheral_mode_enabled = true; -// Nordic UART Service UUID - using standard Nordic UUIDs for Web Bluetooth compatibility +// 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); @@ -49,7 +48,6 @@ 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 @@ -66,7 +64,6 @@ 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; @@ -79,6 +76,15 @@ static uint8_t or_head = 0; static uint8_t or_tail = 0; static uint8_t or_items = 0; +typedef struct { + uint8_t interface; + uint8_t len; + uint8_t data[65]; // Report with ID +} usb_hid_msg_t; + +#define USB_HID_QUEUE_SIZE 16 +K_MSGQ_DEFINE(usb_hid_tx_q, sizeof(usb_hid_msg_t), USB_HID_QUEUE_SIZE, 4); + 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!"); @@ -91,7 +97,6 @@ static void queue_outgoing_report(uint8_t report_id, uint8_t* data, uint8_t len) 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; static bool escaped = false; @@ -165,10 +170,8 @@ 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, @@ -207,7 +210,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(12, 24, 0, 3000); +static struct bt_le_conn_param* conn_param = BT_LE_CONN_PARAM(6, 6, 0, 400); static void activity_led_off_work_fn(struct k_work* work) { gpio_pin_set_dt(&led0, false); @@ -228,6 +231,10 @@ static bool next_blink_state = true; static void led_work_fn(struct k_work* work); static K_WORK_DELAYABLE_DEFINE(led_work, led_work_fn); +// USB HID transmit work function for non-blocking queue processing +static void usb_hid_tx_work_fn(struct k_work* work); +static K_WORK_DEFINE(usb_hid_tx_work, usb_hid_tx_work_fn); + static void led_work_fn(struct k_work* work) { LedMode my_led_mode = (LedMode) atomic_get(&led_mode); switch (my_led_mode) { @@ -267,9 +274,46 @@ static void set_led_mode(LedMode led_mode_) { } } +static void usb_hid_tx_work_fn(struct k_work* work) { + usb_hid_msg_t msg; + + while (k_msgq_get(&usb_hid_tx_q, &msg, K_NO_WAIT) == 0) { + if (msg.interface == 0) { + if (!k_sem_take(&usb_sem0, K_NO_WAIT)) { + if (CHK(hid_int_ep_write(hid_dev0, msg.data, msg.len, NULL))) { + continue; + } else { + k_sem_give(&usb_sem0); + k_msgq_put(&usb_hid_tx_q, &msg, K_NO_WAIT); + break; + } + } else { + // Can't get semaphore - requeue and try later + k_msgq_put(&usb_hid_tx_q, &msg, K_NO_WAIT); + break; + } + } else if (msg.interface == 1) { + if (!k_sem_take(&usb_sem1, K_NO_WAIT)) { + if (CHK(hid_int_ep_write(hid_dev1, msg.data, msg.len, NULL))) { + // Success - semaphore will be released by callback + continue; + } else { + // Failed - give back semaphore and requeue + k_sem_give(&usb_sem1); + k_msgq_put(&usb_hid_tx_q, &msg, K_NO_WAIT); + break; + } + } else { + // Can't get semaphore - requeue and try later + k_msgq_put(&usb_hid_tx_q, &msg, K_NO_WAIT); + break; + } + } + } +} + -// 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) { @@ -282,7 +326,6 @@ static ssize_t uart_write_cb(struct bt_conn *conn, const struct bt_gatt_attr *at 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) @@ -297,8 +340,6 @@ static void peripheral_connected(struct bt_conn *conn, uint8_t err) LOG_INF("Peripheral connected"); update_led_status(); - // Don't aggressively update connection parameters - let them stabilize - // The original working version didn't force parameter updates LOG_INF("Peripheral connection established successfully"); } } @@ -311,7 +352,6 @@ static void peripheral_disconnected(struct bt_conn *conn, uint8_t reason) peripheral_conn = NULL; update_led_status(); - // Restart advertising start_peripheral_advertising(); } } @@ -356,28 +396,18 @@ static int count_connections() { } static void update_led_status() { - // LED status indication for dual mode: - // Solid ON: Both modes active with connections - // Fast blink: Host scanning or peripheral advertising - // Slow blink: Only one mode active - // OFF: No modes active (shouldn't happen normally) - bool host_active = host_mode_enabled && (scanning || count_connections() > 0); bool peripheral_active = peripheral_mode_enabled && (peripheral_conn != NULL); if (host_active && peripheral_active) { - // Both modes with connections - solid on set_led_mode(LedMode::ON); } else if (host_mode_enabled && peripheral_mode_enabled) { - // Both modes enabled but not all connected - fast blink atomic_set(&led_blink_count, 2); // Fast blink pattern set_led_mode(LedMode::BLINK); } else if (host_active || peripheral_active) { - // Only one mode active - slow blink atomic_set(&led_blink_count, 1); // Slow blink pattern set_led_mode(LedMode::BLINK); } else { - // No active connections - blink to show searching/advertising atomic_set(&led_blink_count, 3); // Different pattern for searching set_led_mode(LedMode::BLINK); } @@ -417,21 +447,11 @@ static bool scan_setup_filters() { } static void scan_start_work_fn(struct k_work* work) { - // Don't scan if host mode is disabled if (!host_mode_enabled) { LOG_DBG("Skipping scan - host mode disabled"); return; } - // CRITICAL: Stop scanning when peripheral has active connection to avoid interference - if (peripheral_mode_enabled && peripheral_conn) { - LOG_DBG("Stopping scan - peripheral mode has active connection to avoid interference"); - if (scanning) { - scan_stop(); - } - return; - } - if (scanning) { scan_stop(); } @@ -576,20 +596,16 @@ static void button_cb(const struct device* dev, struct gpio_callback* cb, uint32 button_pressed_at = k_uptime_get(); } else { if (k_uptime_get() - button_pressed_at > CLEAR_BONDS_BUTTON_PRESS_MS) { - // Long press: clear bonds clear_bonds(); } else { - // Short press: pair new device (host mode always enabled) LOG_INF("Pairing new device"); pair_new_device(); } } } -// 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)); } @@ -608,10 +624,7 @@ static int32_t virtual_gamepad_default_value(uint32_t usage) { } } -// Peripheral mode implementation functions static void peripheral_mode_init(void) { - // Only stop scanning and disconnect if host mode is completely disabled - // When both modes are enabled, let them coexist if (!host_mode_enabled) { if (scanning) { scan_stop(); @@ -619,18 +632,12 @@ static void peripheral_mode_init(void) { bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_conn, NULL); } - // Initialize packet reception state bytes_read = 0; escaped = false; // Create a virtual transmitter device that represents the input source uint16_t 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(); // Virtual gamepad descriptor with Report ID to match firmware format expectations static const uint8_t virtual_gamepad_descriptor[] = { @@ -682,10 +689,8 @@ static void peripheral_mode_init(void) { virtual_interface, 0); device_connected_callback(virtual_interface, 0x0F0D, 0x00C1, 0); - // Force update of their descriptor derivates for the virtual device their_descriptor_updated = true; - // Start advertising if peripheral mode is enabled if (peripheral_mode_enabled) { start_peripheral_advertising(); } @@ -740,18 +745,14 @@ static void handle_received_packet(const uint8_t* data, uint16_t len) { return; } - // Handle descriptor change if (msg->our_descriptor_number != our_descriptor_number) { our_descriptor_number = msg->our_descriptor_number; - // Signal that configuration needs to be updated config_updated = true; LOG_INF("Descriptor number changed to %d", our_descriptor_number); } - // 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", @@ -780,7 +781,6 @@ static void process_byte_with_framing(uint8_t c) { case END: if (bytes_read > 4) { uint32_t crc = crc32(packet_buffer, bytes_read - 4); - // Read CRC in little-endian format (matches transmitter) uint32_t received_crc = (packet_buffer[bytes_read - 4] << 0) | (packet_buffer[bytes_read - 3] << 8) | @@ -806,7 +806,6 @@ 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]); @@ -824,7 +823,6 @@ 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); - // Only restart scanning if host mode is enabled if (host_mode_enabled) { k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } @@ -854,7 +852,6 @@ static void disconnected(struct bt_conn* conn, uint8_t reason) { count_connections(); - // Only restart scanning if host mode is enabled if (host_mode_enabled) { k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } @@ -868,7 +865,6 @@ 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; - // Only start discovery if host mode is enabled if (host_mode_enabled) { gatt_discover(conn); } @@ -884,10 +880,8 @@ 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); - - // 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->interval_min < 6) param->interval_min = 6; // Minimum 7.5ms (esports-grade) + if (param->interval_max > 24) param->interval_max = 24; // Cap at 30ms to prevent 1s+ intervals if (param->timeout < 100) param->timeout = 100; // Minimum 1s timeout if (param->timeout > 3200) param->timeout = 3200; // Maximum 32s timeout @@ -932,7 +926,6 @@ static uint8_t hogp_notify_cb(struct bt_hogp* hogp, struct bt_hogp_rep_info* rep return BT_GATT_ITER_STOP; } - // Manage scanning based on host mode status if (scanning && host_mode_enabled) { 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); @@ -947,13 +940,11 @@ 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"); } return BT_GATT_ITER_CONTINUE; } -// XXX is this ready for simultaneous connection setup? is discovery ready? do we care? static struct descriptor_type their_descriptor; static void hogp_map_read_cb(struct bt_hogp* hogp, uint8_t err, const uint8_t* data, size_t size, size_t offset) { @@ -1116,6 +1107,7 @@ static int get_report_cb(const struct device* dev, struct usb_setup_packet* setu static void int_in_ready_cb0(const struct device* dev) { k_sem_give(&usb_sem0); + k_work_submit(&usb_hid_tx_work); } static void int_out_ready_cb0(const struct device* dev) { @@ -1130,6 +1122,7 @@ static void int_out_ready_cb0(const struct device* dev) { static void int_in_ready_cb1(const struct device* dev) { k_sem_give(&usb_sem1); + k_work_submit(&usb_hid_tx_work); } static const struct hid_ops ops0 = { @@ -1150,22 +1143,19 @@ static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uin report_with_id++; len--; } - if (interface == 0) { - // 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)); + + // OPTIMIZATION 7: Queue USB writes to prevent blocking on flow control stalls + usb_hid_msg_t msg; + msg.interface = interface; + msg.len = len; + memcpy(msg.data, report_with_id, len); + + if (k_msgq_put(&usb_hid_tx_q, &msg, K_NO_WAIT) != 0) { + LOG_WRN("USB HID TX queue full - dropping report"); + return false; } - return false; // Default case - interface not supported + + return true; // Queued successfully - non-blocking } static void button_init() { @@ -1472,33 +1462,25 @@ int main() { update_led_status(); last_led_update = current_time; } - if (!k_sem_take(&usb_sem0, K_NO_WAIT)) { - if (!send_report(do_send_report)) { - k_sem_give(&usb_sem0); - } - } - if (!k_sem_take(&usb_sem1, K_NO_WAIT)) { - if (!send_monitor_report(do_send_report)) { - k_sem_give(&usb_sem1); - } - } - - // Process queued outgoing reports when USB HID is ready - if ((or_items > 0) && (k_sem_take(&usb_sem0, K_NO_WAIT) == 0)) { + // OPTIMIZATION 9: Non-blocking USB HID report transmission using work queue + send_report(do_send_report); + send_monitor_report(do_send_report); + + // Process queued outgoing reports (legacy support) + if (or_items > 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 + // Successfully queued, remove from legacy queue or_head = (or_head + 1) % OR_BUFSIZE; or_items--; - } else { - // Failed to send, give back semaphore - k_sem_give(&usb_sem0); } } + + // Trigger USB HID work processing + k_work_submit(&usb_hid_tx_work); if (!k_msgq_get(&set_report_q, &set_report_item, K_NO_WAIT)) { if (set_report_item.interface == 0) { From 655f1a2028ed259d31dbce5b9b31bf096c60b672 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 18:57:43 +0200 Subject: [PATCH 12/21] Update Bluetooth firmware and submodule reference - Adjusted connection parameter handling in le_param_req to set interval_max based on interval_min. - Removed redundant comments for clarity. --- firmware-bluetooth/src/main.cc | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 4a553bc0..418d9763 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -878,13 +878,8 @@ 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); - if (param->interval_min < 6) param->interval_min = 6; // Minimum 7.5ms (esports-grade) - if (param->interval_max > 24) param->interval_max = 24; // Cap at 30ms to prevent 1s+ intervals - if (param->timeout < 100) param->timeout = 100; // Minimum 1s timeout - if (param->timeout > 3200) param->timeout = 3200; // Maximum 32s timeout - + 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; return true; } @@ -1143,8 +1138,8 @@ static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uin report_with_id++; len--; } - - // OPTIMIZATION 7: Queue USB writes to prevent blocking on flow control stalls + + // Queue USB writes to prevent blocking on flow control stalls usb_hid_msg_t msg; msg.interface = interface; msg.len = len; From fd28fa3322ebf7bc3c0185b699f07c5578a16352 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 19:07:18 +0200 Subject: [PATCH 13/21] remove comments --- firmware-bluetooth/src/main.cc | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 418d9763..be517c47 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -1259,8 +1259,6 @@ static int remapper_settings_set(const char* name, size_t len, settings_read_cb return -EINVAL; } - // LOG_HEXDUMP_DBG(buffer, len, ""); - load_config(buffer); return 0; @@ -1310,7 +1308,6 @@ void disable_host_mode() { if (scanning) { scan_stop(); } - // Disconnect host connections bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_conn, NULL); } } @@ -1327,9 +1324,7 @@ void disable_peripheral_mode() { if (peripheral_mode_enabled) { peripheral_mode_enabled = false; LOG_INF("Peripheral mode disabled"); - // Stop advertising CHK(bt_le_adv_stop()); - // Disconnect peripheral connection if (peripheral_conn) { CHK(bt_conn_disconnect(peripheral_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN)); } @@ -1337,7 +1332,6 @@ void disable_peripheral_mode() { } void enable_dual_mode() { - // Enable both modes for full functionality if (!peripheral_mode_enabled) { enable_peripheral_mode(); } @@ -1400,12 +1394,10 @@ int main() { parse_our_descriptor(); set_mapping_from_config(); - // Initialize peripheral mode first (like original) if (peripheral_mode_enabled) { peripheral_mode_init(); } - // Start host mode (always enabled) if (host_mode_enabled) { k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } @@ -1422,7 +1414,6 @@ int main() { int64_t last_led_update = 0; while (true) { - // Host mode functionality - always process if enabled if (host_mode_enabled) { 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); @@ -1445,36 +1436,30 @@ int main() { } } - // Process periodic ticks for remapping (needed for both modes) if (atomic_test_and_clear_bit(tick_pending, 0)) { process_mapping(true); process_pending = false; } - // Update LED status periodically (every 5 seconds) int64_t current_time = k_uptime_get(); if (current_time - last_led_update > 5000) { update_led_status(); last_led_update = current_time; } - // OPTIMIZATION 9: Non-blocking USB HID report transmission using work queue send_report(do_send_report); send_monitor_report(do_send_report); - // Process queued outgoing reports (legacy support) if (or_items > 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 queued, remove from legacy queue or_head = (or_head + 1) % OR_BUFSIZE; or_items--; } } - // Trigger USB HID work processing k_work_submit(&usb_hid_tx_work); if (!k_msgq_get(&set_report_q, &set_report_item, K_NO_WAIT)) { From eec4aea02e2acad2ad7740e1b91fd7726e2fd900 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 19:12:10 +0200 Subject: [PATCH 14/21] Refactor Bluetooth firmware by removing unused functions and comments - Eliminated the `queue_outgoing_report`, `virtual_gamepad_clear_report`, and `virtual_gamepad_default_value` functions to streamline the code. - Removed redundant comments to enhance code clarity. --- firmware-bluetooth/src/main.cc | 43 ---------------------------------- 1 file changed, 43 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index be517c47..ea3c8ba1 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -85,18 +85,6 @@ typedef struct { #define USB_HID_QUEUE_SIZE 16 K_MSGQ_DEFINE(usb_hid_tx_q, sizeof(usb_hid_msg_t), USB_HID_QUEUE_SIZE, 4); -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++; -} - static uint8_t packet_buffer[SERIAL_MAX_PACKET_SIZE]; static uint16_t bytes_read = 0; static bool escaped = false; @@ -106,7 +94,6 @@ 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 void update_led_status(void); -static void enable_dual_mode(void); 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); @@ -604,26 +591,6 @@ static void button_cb(const struct device* dev, struct gpio_callback* cb, uint32 } } -static const uint8_t virtual_gamepad_neutral[] = { 0x00, 0x00, 0x0F, 0x80, 0x80, 0x80, 0x80, 0x00 }; - -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; - } -} - static void peripheral_mode_init(void) { if (!host_mode_enabled) { if (scanning) { @@ -1331,16 +1298,6 @@ void disable_peripheral_mode() { } } -void enable_dual_mode() { - if (!peripheral_mode_enabled) { - enable_peripheral_mode(); - } - if (!host_mode_enabled) { - enable_host_mode(); - } - LOG_INF("Dual mode enabled - both host and peripheral active"); -} - void my_mutexes_init() { for (int i = 0; i < (int8_t) MutexId::N; i++) { k_mutex_init(&mutexes[i]); From 49196486d767bedffca4f145cc07026174c84e5a Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Mon, 23 Jun 2025 19:40:37 +0200 Subject: [PATCH 15/21] Update Bluetooth firmware by removing unused outgoing report structures and related logic --- firmware-bluetooth/src/main.cc | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index ea3c8ba1..72399948 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -64,18 +64,6 @@ typedef struct __attribute__((packed)) { uint8_t data[0]; } packet_t; -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; - typedef struct { uint8_t interface; uint8_t len; @@ -1406,17 +1394,6 @@ int main() { send_report(do_send_report); send_monitor_report(do_send_report); - if (or_items > 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)) { - or_head = (or_head + 1) % OR_BUFSIZE; - or_items--; - } - } - k_work_submit(&usb_hid_tx_work); if (!k_msgq_get(&set_report_q, &set_report_item, K_NO_WAIT)) { From f5b3d816fb5f80c58468afc24d5cb50754350344 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Thu, 26 Jun 2025 11:31:05 +0200 Subject: [PATCH 16/21] add itsybitsy --- .../arm/adafruit_itsybitsy_nrf52840/Kconfig | 10 ++ .../adafruit_itsybitsy_nrf52840/Kconfig.board | 8 + .../Kconfig.defconfig | 14 ++ .../arm/adafruit_itsybitsy_nrf52840/README.md | 48 ++++++ .../adafruit_itsybitsy_nrf52840-pinctrl.dtsi | 76 +++++++++ .../adafruit_itsybitsy_nrf52840.dts | 148 ++++++++++++++++++ .../adafruit_itsybitsy_nrf52840.yaml | 19 +++ .../adafruit_itsybitsy_nrf52840_defconfig | 23 +++ .../adafruit_itsybitsy_nrf52840/board.cmake | 9 ++ .../pre_dt_board.cmake | 7 + 10 files changed, 362 insertions(+) create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.board create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.defconfig create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/README.md create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840-pinctrl.dtsi create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.dts create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.yaml create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840_defconfig create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/board.cmake create mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/pre_dt_board.cmake diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig new file mode 100644 index 00000000..bde5f4d3 --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig @@ -0,0 +1,10 @@ +# Adafruit ItsyBitsy nRF52840 board configuration + +# Copyright (c) 2024 +# SPDX-License-Identifier: Apache-2.0 + +config BOARD_ENABLE_DCDC + bool "DCDC mode" + select SOC_DCDC_NRF52X + default y + depends on BOARD_ADAFRUIT_ITSYBITSY_NRF52840 \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.board b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.board new file mode 100644 index 00000000..d11cdfcd --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.board @@ -0,0 +1,8 @@ +# Adafruit ItsyBitsy nRF52840 board configuration + +# Copyright (c) 2024 +# SPDX-License-Identifier: Apache-2.0 + +config BOARD_ADAFRUIT_ITSYBITSY_NRF52840 + bool "Adafruit ItsyBitsy nRF52840" + depends on SOC_NRF52840_QIAA \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.defconfig b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.defconfig new file mode 100644 index 00000000..5648613a --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.defconfig @@ -0,0 +1,14 @@ +# Adafruit ItsyBitsy nRF52840 board configuration + +# Copyright (c) 2024 +# SPDX-License-Identifier: Apache-2.0 + +if BOARD_ADAFRUIT_ITSYBITSY_NRF52840 + +config BOARD + default "adafruit_itsybitsy_nrf52840" + +config BT_CTLR + default BT + +endif # BOARD_ADAFRUIT_ITSYBITSY_NRF52840 \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/README.md b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/README.md new file mode 100644 index 00000000..d674a8f4 --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/README.md @@ -0,0 +1,48 @@ +# Adafruit ItsyBitsy nRF52840 + +This directory contains the board configuration for the Adafruit ItsyBitsy nRF52840. + +## Hardware + +- **MCU**: nRF52840 (ARM Cortex-M4) +- **Flash**: 1MB +- **RAM**: 256KB +- **LED**: Connected to GPIO0.6 +- **Button**: Connected to GPIO0.29 (with internal pull-up) +- **UART**: TX=GPIO0.24, RX=GPIO0.25 +- **I2C**: SDA=GPIO0.12, SCL=GPIO0.11 +- **SPI**: SCK=GPIO0.14, MOSI=GPIO0.13, MISO=GPIO0.15 + +## Features + +- Bluetooth Low Energy (BLE) +- USB Device support +- ADC support +- I2C support +- SPI support +- QSPI flash support +- PWM support +- Watchdog support +- Counter support + +## Building + +To build for this board, use: + +```bash +west build -b adafruit_itsybitsy_nrf52840 +``` + +## Flashing + +The board supports various flashing methods: + +- J-Link +- pyOCD +- Black Magic Probe +- nRFjprog + +Example with J-Link: +```bash +west flash +``` \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840-pinctrl.dtsi b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840-pinctrl.dtsi new file mode 100644 index 00000000..eaa8e537 --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840-pinctrl.dtsi @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 + * SPDX-License-Identifier: Apache-2.0 + */ + +&pinctrl { + uart0_default: uart0_default { + group1 { + psels = , + ; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + i2c0_default: i2c0_default { + group1 { + psels = , + ; + }; + }; + + i2c0_sleep: i2c0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + qspi_default: qspi_default { + group1 { + psels = , + , + , + , + , + ; + }; + }; + + qspi_sleep: qspi_sleep { + group1 { + psels = , + , + , + , + , + ; + low-power-enable; + }; + }; +}; \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.dts b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.dts new file mode 100644 index 00000000..c0c19866 --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.dts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/dts-v1/; +#include +#include "adafruit_itsybitsy_nrf52840-pinctrl.dtsi" + +/ { + model = "Adafruit ItsyBitsy nRF52840"; + compatible = "adafruit,itsybitsy-nrf52840"; + + chosen { + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,uart-mcumgr = &uart0; + zephyr,bt-mon-uart = &uart0; + zephyr,bt-c2h-uart = &uart0; + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &code_partition; + }; + + leds { + compatible = "gpio-leds"; + led0: led_0 { + gpios = <&gpio0 6 0>; + label = "Red LED"; + }; + led1: led_1 { + gpios = <&gpio0 7 0>; + label = "Blue LED"; + }; + }; + + buttons { + compatible = "gpio-keys"; + button0: button_0 { + gpios = <&gpio0 29 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + label = "Push button switch"; + }; + }; + + /* These aliases are provided for compatibility with samples */ + aliases { + led0 = &led0; + led1 = &led1; + sw0 = &button0; + }; +}; + +&adc { + status = "okay"; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&uart0 { + compatible = "nordic,nrf-uart"; + current-speed = <921600>; + status = "okay"; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&i2c0 { + compatible = "nordic,nrf-twi"; + status = "okay"; + pinctrl-0 = <&i2c0_default>; + pinctrl-1 = <&i2c0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi1 { + compatible = "nordic,nrf-spi"; + status = "okay"; + pinctrl-0 = <&spi1_default>; + pinctrl-1 = <&spi1_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&qspi { + status = "okay"; + pinctrl-0 = <&qspi_default>; + pinctrl-1 = <&qspi_sleep>; + pinctrl-names = "default", "sleep"; + gd25q16: gd25q16@0 { + compatible = "nordic,qspi-nor"; + reg = <0>; + writeoc = "pp4o"; + readoc = "read4io"; + sck-frequency = <16000000>; + label = "GD25Q16"; + jedec-id = [c8 40 15]; + size = <16777216>; + has-dpd; + t-enter-dpd = <20000>; + t-exit-dpd = <20000>; + quad-enable-requirements = "S2B1v1"; + }; +}; + +&flash0 { + + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + sd_partition: partition@0 { + label = "softdevice"; + reg = <0x00000000 0x00026000>; + }; + + code_partition: partition@26000 { + label = "code_partition"; + reg = <0x00026000 0x000c6000>; + }; + + storage_partition: partition@ec000 { + label = "storage"; + reg = <0x000ec000 0x00008000>; + }; + + boot_partition: partition@f4000 { + label = "adafruit_boot"; + reg = <0x000f4000 0x0000c000>; + }; + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; +}; \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.yaml b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.yaml new file mode 100644 index 00000000..7050fb52 --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.yaml @@ -0,0 +1,19 @@ +identifier: adafruit_itsybitsy_nrf52840 +name: Adafruit ItsyBitsy nRF52840 +type: mcu +arch: arm +toolchain: + - zephyr + - gnuarmemb + - xtools +supported: + - adc + - usb_device + - usb_cdc + - ble + - ieee802154 + - watchdog + - counter + - i2c + - spi + - pwm \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840_defconfig b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840_defconfig new file mode 100644 index 00000000..b38245f2 --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840_defconfig @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_SOC_SERIES_NRF52X=y +CONFIG_SOC_NRF52840_QIAA=y +CONFIG_BOARD_ADAFRUIT_ITSYBITSY_NRF52840=y + +# Enable MPU +CONFIG_ARM_MPU=y + +# enable GPIO +CONFIG_GPIO=y + +# enable uart driver +CONFIG_SERIAL=y + +# enable console +CONFIG_CONSOLE=y +CONFIG_UART_CONSOLE=y + +# additional board options +CONFIG_GPIO_AS_PINRESET=y + +CONFIG_PINCTRL=y \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/board.cmake b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/board.cmake new file mode 100644 index 00000000..c89066c5 --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/board.cmake @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000") +board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000") +board_runner_args(blackmagicprobe "--gdb-serial=/dev/ttyBmpGdb") +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) +include(${ZEPHYR_BASE}/boards/common/blackmagicprobe.board.cmake) \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/pre_dt_board.cmake b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/pre_dt_board.cmake new file mode 100644 index 00000000..206a6108 --- /dev/null +++ b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/pre_dt_board.cmake @@ -0,0 +1,7 @@ +# Copyright (c) 2024 +# SPDX-License-Identifier: Apache-2.0 + +# Suppress "unique_unit_address_if_enabled" to handle the following overlaps: +# - power@40000000 & clock@40000000 & bprot@40000000 +# - acl@4001e000 & flash-controller@4001e000 +list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled") \ No newline at end of file From 3056ad7c52809ac4a79ad6ccf65356f37404819b Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Fri, 27 Jun 2025 09:56:21 +0200 Subject: [PATCH 17/21] better disconnect process in peripheral mode --- firmware-bluetooth/src/main.cc | 112 ++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 15 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 72399948..83599d28 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -319,15 +319,31 @@ static void peripheral_connected(struct bt_conn *conn, uint8_t err) } } +static void restart_advertising_work_fn(struct k_work* work); +static K_WORK_DELAYABLE_DEFINE(restart_advertising_work, restart_advertising_work_fn); + +static void restart_advertising_work_fn(struct k_work* work) { + if (peripheral_mode_enabled && peripheral_conn == NULL) { + LOG_INF("Restarting peripheral advertising after disconnect"); + start_peripheral_advertising(); + } +} + static void peripheral_disconnected(struct bt_conn *conn, uint8_t reason) { if (peripheral_mode_enabled && conn == peripheral_conn) { LOG_INF("Peripheral disconnected (reason %u)", reason); bt_conn_unref(peripheral_conn); peripheral_conn = NULL; + + // Reset packet parsing state on disconnect + bytes_read = 0; + escaped = false; + update_led_status(); - start_peripheral_advertising(); + // Delay before restarting advertising to ensure connection cleanup + k_work_reschedule(&restart_advertising_work, K_MSEC(100)); } } @@ -654,6 +670,15 @@ static void peripheral_mode_init(void) { } static void start_peripheral_advertising(void) { + if (!peripheral_mode_enabled) { + LOG_DBG("Peripheral mode disabled, skipping advertising"); + return; + } + + // Stop any existing advertising first + int stop_result = bt_le_adv_stop(); + LOG_DBG("Advertising stop result: %d (0=success, -EALREADY=not running)", stop_result); + struct bt_le_adv_param adv_param = { .id = BT_ID_DEFAULT, .sid = 0, @@ -672,8 +697,15 @@ static void start_peripheral_advertising(void) { 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 'HID Remapper'"); + int result = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), NULL, 0); + if (result == 0) { + LOG_INF("Peripheral advertising started as 'HID Remapper'"); + } else { + LOG_ERR("Failed to start peripheral advertising, error: %d", result); + if (result == -EALREADY) { + LOG_WRN("Advertising already running"); + } + } } static void handle_received_packet(const uint8_t* data, uint16_t len) { @@ -770,10 +802,6 @@ static void process_peripheral_command(uint8_t* buf, int count) { static void connected(struct bt_conn* conn, uint8_t conn_err) { char addr[BT_ADDR_LE_STR_LEN]; - scanning = false; - count_connections(); - set_led_mode(LedMode::BLINK); - bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); if (conn_err) { @@ -784,9 +812,25 @@ static void connected(struct bt_conn* conn, uint8_t conn_err) { return; } - LOG_INF("%s", addr); + // Check if this is an incoming connection (peripheral mode) + struct bt_conn_info info; + bt_conn_get_info(conn, &info); + + if (info.role == BT_CONN_ROLE_PERIPHERAL) { + LOG_INF("Incoming peripheral connection: %s", addr); + // Don't force security for incoming connections - let client decide + return; + } + + LOG_INF("Outgoing host connection: %s", addr); - CHK(bt_conn_set_security(conn, BT_SECURITY_L2)); + // This is a host mode connection (outgoing) + if (host_mode_enabled) { + scanning = false; + count_connections(); + set_led_mode(LedMode::BLINK); + CHK(bt_conn_set_security(conn, BT_SECURITY_L2)); + } } static void disconnected(struct bt_conn* conn, uint8_t reason) { @@ -794,8 +838,25 @@ static void disconnected(struct bt_conn* conn, uint8_t reason) { bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); - LOG_INF("%s (reason=%u)", addr, reason); + LOG_INF("Disconnected: %s (reason=%u)", addr, reason); + + // Handle peripheral mode disconnection (in case peripheral_disconnected wasn't called) + if (peripheral_mode_enabled && conn == peripheral_conn) { + LOG_INF("Peripheral connection cleanup in generic disconnect handler"); + bt_conn_unref(peripheral_conn); + peripheral_conn = NULL; + + // Reset packet parsing state on disconnect + bytes_read = 0; + escaped = false; + + // Delay before restarting advertising to ensure connection cleanup + k_work_reschedule(&restart_advertising_work, K_MSEC(100)); + update_led_status(); + return; // Don't process as host connection + } + // Handle host mode disconnections uint8_t conn_idx = bt_conn_index(conn); if (bt_hogp_assign_check(&hogps[conn_idx])) { @@ -810,6 +871,8 @@ static void disconnected(struct bt_conn* conn, uint8_t reason) { if (host_mode_enabled) { k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } + + update_led_status(); } static void security_changed(struct bt_conn* conn, bt_security_t level, enum bt_security_err err) { @@ -818,13 +881,19 @@ static void security_changed(struct bt_conn* conn, bt_security_t level, enum bt_ bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); if (!err) { - LOG_INF("%s, level=%u.", addr, level); - peers_only = true; - if (host_mode_enabled) { + LOG_INF("Security level changed: %s, level=%u", addr, level); + + // Only do GATT discovery for host mode connections + if (host_mode_enabled && conn != peripheral_conn) { + peers_only = true; gatt_discover(conn); } } else { - LOG_ERR("security failed: %s, level=%u, err=%d", addr, level, err); + LOG_WRN("Security change failed: %s, level=%u, err=%d", addr, level, err); + // Don't force disconnect on security failure for peripheral connections + if (conn != peripheral_conn) { + LOG_ERR("Security failure on host connection, may disconnect"); + } } } @@ -834,7 +903,20 @@ 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; + + // Be more permissive with connection parameters for web clients + // Allow the requested parameters within reasonable bounds + if (param->interval_min >= 6 && param->interval_max <= 3200 && + param->latency <= 30 && param->timeout >= 10 && param->timeout <= 3200) { + return true; // Accept the client's requested parameters + } + + // If parameters are out of bounds, suggest reasonable defaults + param->interval_min = 6; // 7.5ms + param->interval_max = 12; // 15ms + param->latency = 0; + param->timeout = 400; // 4s + return true; } From 674d8e274cd0560c886eaf743ddc96707ca10384 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Fri, 27 Jun 2025 10:07:59 +0200 Subject: [PATCH 18/21] remove itsybitsy --- .../arm/adafruit_itsybitsy_nrf52840/Kconfig | 10 -- .../adafruit_itsybitsy_nrf52840/Kconfig.board | 8 - .../Kconfig.defconfig | 14 -- .../arm/adafruit_itsybitsy_nrf52840/README.md | 48 ------ .../adafruit_itsybitsy_nrf52840-pinctrl.dtsi | 76 --------- .../adafruit_itsybitsy_nrf52840.dts | 148 ------------------ .../adafruit_itsybitsy_nrf52840.yaml | 19 --- .../adafruit_itsybitsy_nrf52840_defconfig | 23 --- .../adafruit_itsybitsy_nrf52840/board.cmake | 9 -- .../pre_dt_board.cmake | 7 - 10 files changed, 362 deletions(-) delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.board delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.defconfig delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/README.md delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840-pinctrl.dtsi delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.dts delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.yaml delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840_defconfig delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/board.cmake delete mode 100644 firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/pre_dt_board.cmake diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig deleted file mode 100644 index bde5f4d3..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig +++ /dev/null @@ -1,10 +0,0 @@ -# Adafruit ItsyBitsy nRF52840 board configuration - -# Copyright (c) 2024 -# SPDX-License-Identifier: Apache-2.0 - -config BOARD_ENABLE_DCDC - bool "DCDC mode" - select SOC_DCDC_NRF52X - default y - depends on BOARD_ADAFRUIT_ITSYBITSY_NRF52840 \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.board b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.board deleted file mode 100644 index d11cdfcd..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.board +++ /dev/null @@ -1,8 +0,0 @@ -# Adafruit ItsyBitsy nRF52840 board configuration - -# Copyright (c) 2024 -# SPDX-License-Identifier: Apache-2.0 - -config BOARD_ADAFRUIT_ITSYBITSY_NRF52840 - bool "Adafruit ItsyBitsy nRF52840" - depends on SOC_NRF52840_QIAA \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.defconfig b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.defconfig deleted file mode 100644 index 5648613a..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/Kconfig.defconfig +++ /dev/null @@ -1,14 +0,0 @@ -# Adafruit ItsyBitsy nRF52840 board configuration - -# Copyright (c) 2024 -# SPDX-License-Identifier: Apache-2.0 - -if BOARD_ADAFRUIT_ITSYBITSY_NRF52840 - -config BOARD - default "adafruit_itsybitsy_nrf52840" - -config BT_CTLR - default BT - -endif # BOARD_ADAFRUIT_ITSYBITSY_NRF52840 \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/README.md b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/README.md deleted file mode 100644 index d674a8f4..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Adafruit ItsyBitsy nRF52840 - -This directory contains the board configuration for the Adafruit ItsyBitsy nRF52840. - -## Hardware - -- **MCU**: nRF52840 (ARM Cortex-M4) -- **Flash**: 1MB -- **RAM**: 256KB -- **LED**: Connected to GPIO0.6 -- **Button**: Connected to GPIO0.29 (with internal pull-up) -- **UART**: TX=GPIO0.24, RX=GPIO0.25 -- **I2C**: SDA=GPIO0.12, SCL=GPIO0.11 -- **SPI**: SCK=GPIO0.14, MOSI=GPIO0.13, MISO=GPIO0.15 - -## Features - -- Bluetooth Low Energy (BLE) -- USB Device support -- ADC support -- I2C support -- SPI support -- QSPI flash support -- PWM support -- Watchdog support -- Counter support - -## Building - -To build for this board, use: - -```bash -west build -b adafruit_itsybitsy_nrf52840 -``` - -## Flashing - -The board supports various flashing methods: - -- J-Link -- pyOCD -- Black Magic Probe -- nRFjprog - -Example with J-Link: -```bash -west flash -``` \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840-pinctrl.dtsi b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840-pinctrl.dtsi deleted file mode 100644 index eaa8e537..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840-pinctrl.dtsi +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2024 - * SPDX-License-Identifier: Apache-2.0 - */ - -&pinctrl { - uart0_default: uart0_default { - group1 { - psels = , - ; - }; - }; - - uart0_sleep: uart0_sleep { - group1 { - psels = , - ; - low-power-enable; - }; - }; - - i2c0_default: i2c0_default { - group1 { - psels = , - ; - }; - }; - - i2c0_sleep: i2c0_sleep { - group1 { - psels = , - ; - low-power-enable; - }; - }; - - spi1_default: spi1_default { - group1 { - psels = , - , - ; - }; - }; - - spi1_sleep: spi1_sleep { - group1 { - psels = , - , - ; - low-power-enable; - }; - }; - - qspi_default: qspi_default { - group1 { - psels = , - , - , - , - , - ; - }; - }; - - qspi_sleep: qspi_sleep { - group1 { - psels = , - , - , - , - , - ; - low-power-enable; - }; - }; -}; \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.dts b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.dts deleted file mode 100644 index c0c19866..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.dts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2024 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -/dts-v1/; -#include -#include "adafruit_itsybitsy_nrf52840-pinctrl.dtsi" - -/ { - model = "Adafruit ItsyBitsy nRF52840"; - compatible = "adafruit,itsybitsy-nrf52840"; - - chosen { - zephyr,console = &uart0; - zephyr,shell-uart = &uart0; - zephyr,uart-mcumgr = &uart0; - zephyr,bt-mon-uart = &uart0; - zephyr,bt-c2h-uart = &uart0; - zephyr,sram = &sram0; - zephyr,flash = &flash0; - zephyr,code-partition = &code_partition; - }; - - leds { - compatible = "gpio-leds"; - led0: led_0 { - gpios = <&gpio0 6 0>; - label = "Red LED"; - }; - led1: led_1 { - gpios = <&gpio0 7 0>; - label = "Blue LED"; - }; - }; - - buttons { - compatible = "gpio-keys"; - button0: button_0 { - gpios = <&gpio0 29 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; - label = "Push button switch"; - }; - }; - - /* These aliases are provided for compatibility with samples */ - aliases { - led0 = &led0; - led1 = &led1; - sw0 = &button0; - }; -}; - -&adc { - status = "okay"; -}; - -&gpiote { - status = "okay"; -}; - -&gpio0 { - status = "okay"; -}; - -&gpio1 { - status = "okay"; -}; - -&uart0 { - compatible = "nordic,nrf-uart"; - current-speed = <921600>; - status = "okay"; - pinctrl-0 = <&uart0_default>; - pinctrl-1 = <&uart0_sleep>; - pinctrl-names = "default", "sleep"; -}; - -&i2c0 { - compatible = "nordic,nrf-twi"; - status = "okay"; - pinctrl-0 = <&i2c0_default>; - pinctrl-1 = <&i2c0_sleep>; - pinctrl-names = "default", "sleep"; -}; - -&spi1 { - compatible = "nordic,nrf-spi"; - status = "okay"; - pinctrl-0 = <&spi1_default>; - pinctrl-1 = <&spi1_sleep>; - pinctrl-names = "default", "sleep"; -}; - -&qspi { - status = "okay"; - pinctrl-0 = <&qspi_default>; - pinctrl-1 = <&qspi_sleep>; - pinctrl-names = "default", "sleep"; - gd25q16: gd25q16@0 { - compatible = "nordic,qspi-nor"; - reg = <0>; - writeoc = "pp4o"; - readoc = "read4io"; - sck-frequency = <16000000>; - label = "GD25Q16"; - jedec-id = [c8 40 15]; - size = <16777216>; - has-dpd; - t-enter-dpd = <20000>; - t-exit-dpd = <20000>; - quad-enable-requirements = "S2B1v1"; - }; -}; - -&flash0 { - - partitions { - compatible = "fixed-partitions"; - #address-cells = <1>; - #size-cells = <1>; - - sd_partition: partition@0 { - label = "softdevice"; - reg = <0x00000000 0x00026000>; - }; - - code_partition: partition@26000 { - label = "code_partition"; - reg = <0x00026000 0x000c6000>; - }; - - storage_partition: partition@ec000 { - label = "storage"; - reg = <0x000ec000 0x00008000>; - }; - - boot_partition: partition@f4000 { - label = "adafruit_boot"; - reg = <0x000f4000 0x0000c000>; - }; - }; -}; - -zephyr_udc0: &usbd { - compatible = "nordic,nrf-usbd"; - status = "okay"; -}; \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.yaml b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.yaml deleted file mode 100644 index 7050fb52..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840.yaml +++ /dev/null @@ -1,19 +0,0 @@ -identifier: adafruit_itsybitsy_nrf52840 -name: Adafruit ItsyBitsy nRF52840 -type: mcu -arch: arm -toolchain: - - zephyr - - gnuarmemb - - xtools -supported: - - adc - - usb_device - - usb_cdc - - ble - - ieee802154 - - watchdog - - counter - - i2c - - spi - - pwm \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840_defconfig b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840_defconfig deleted file mode 100644 index b38245f2..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/adafruit_itsybitsy_nrf52840_defconfig +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 - -CONFIG_SOC_SERIES_NRF52X=y -CONFIG_SOC_NRF52840_QIAA=y -CONFIG_BOARD_ADAFRUIT_ITSYBITSY_NRF52840=y - -# Enable MPU -CONFIG_ARM_MPU=y - -# enable GPIO -CONFIG_GPIO=y - -# enable uart driver -CONFIG_SERIAL=y - -# enable console -CONFIG_CONSOLE=y -CONFIG_UART_CONSOLE=y - -# additional board options -CONFIG_GPIO_AS_PINRESET=y - -CONFIG_PINCTRL=y \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/board.cmake b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/board.cmake deleted file mode 100644 index c89066c5..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/board.cmake +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 - -board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000") -board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000") -board_runner_args(blackmagicprobe "--gdb-serial=/dev/ttyBmpGdb") -include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) -include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) -include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) -include(${ZEPHYR_BASE}/boards/common/blackmagicprobe.board.cmake) \ No newline at end of file diff --git a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/pre_dt_board.cmake b/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/pre_dt_board.cmake deleted file mode 100644 index 206a6108..00000000 --- a/firmware-bluetooth/boards/arm/adafruit_itsybitsy_nrf52840/pre_dt_board.cmake +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) 2024 -# SPDX-License-Identifier: Apache-2.0 - -# Suppress "unique_unit_address_if_enabled" to handle the following overlaps: -# - power@40000000 & clock@40000000 & bprot@40000000 -# - acl@4001e000 & flash-controller@4001e000 -list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled") \ No newline at end of file From aeefc76e1e64dd8c18660536f0d98fb32738c053 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Fri, 27 Jun 2025 12:11:38 +0200 Subject: [PATCH 19/21] fix led and add variables --- firmware-bluetooth/src/main.cc | 48 ++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index 83599d28..e7c9f099 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -1,6 +1,3 @@ -#include -#include - #include #include #include @@ -14,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -89,8 +85,16 @@ 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); +// Timing constants static const int SCAN_DELAY_MS = 1000; static const int CLEAR_BONDS_BUTTON_PRESS_MS = 3000; +static const int LED_BLINK_ON_MS = 100; // LED on duration during blink +static const int LED_BLINK_OFF_MS = 100; // LED off duration during blink +static const int LED_BLINK_CYCLE_MS = 1000; // Time between blink cycles +static const int ACTIVITY_LED_DURATION_MS = 50; // Activity LED on duration +static const int RESTART_ADVERTISING_DELAY_MS = 100; // Delay before restarting advertising +static const int LED_STATUS_UPDATE_INTERVAL_MS = 5000; // LED status check interval +static const int MAIN_LOOP_SLEEP_US = 1; // Main loop sleep to prevent pairing issues // these macros don't work in C++ when used directly ("taking address of temporary array") static auto const BT_UUID_HIDS_ = (struct bt_uuid_16) BT_UUID_INIT_16(BT_UUID_HIDS_VAL); @@ -200,8 +204,8 @@ enum class LedMode { static atomic_t led_blink_count = ATOMIC_INIT(0); static atomic_t led_mode = (atomic_t) ATOMIC_INIT(LedMode::OFF); -static int led_blinks_left = 0; -static bool next_blink_state = true; +static atomic_t led_blinks_left = ATOMIC_INIT(0); +static atomic_t next_blink_state = ATOMIC_INIT(true); static void led_work_fn(struct k_work* work); static K_WORK_DELAYABLE_DEFINE(led_work, led_work_fn); @@ -221,21 +225,21 @@ static void led_work_fn(struct k_work* work) { break; case LedMode::BLINK: { int next_work = 0; - if (next_blink_state) { - if (led_blinks_left > 0) { - led_blinks_left--; + if (atomic_get(&next_blink_state)) { + if (atomic_get(&led_blinks_left) > 0) { + atomic_dec(&led_blinks_left); gpio_pin_set_dt(&led1, true); - next_blink_state = false; - next_work = 100; + atomic_set(&next_blink_state, false); + next_work = LED_BLINK_ON_MS; } else { - led_blinks_left = atomic_get(&led_blink_count); + atomic_set(&led_blinks_left, atomic_get(&led_blink_count)); gpio_pin_set_dt(&led1, false); - next_work = 1000; + next_work = LED_BLINK_CYCLE_MS; } } else { gpio_pin_set_dt(&led1, false); - next_blink_state = true; - next_work = 100; + atomic_set(&next_blink_state, true); + next_work = LED_BLINK_OFF_MS; } k_work_reschedule(&led_work, K_MSEC(next_work)); break; @@ -245,6 +249,8 @@ static void led_work_fn(struct k_work* work) { static void set_led_mode(LedMode led_mode_) { if (atomic_set(&led_mode, (atomic_val_t) led_mode_) != (atomic_val_t) led_mode_) { + // Cancel any pending LED work before rescheduling to prevent race conditions + k_work_cancel_delayable(&led_work); k_work_reschedule(&led_work, K_NO_WAIT); } } @@ -343,7 +349,7 @@ static void peripheral_disconnected(struct bt_conn *conn, uint8_t reason) update_led_status(); // Delay before restarting advertising to ensure connection cleanup - k_work_reschedule(&restart_advertising_work, K_MSEC(100)); + k_work_reschedule(&restart_advertising_work, K_MSEC(RESTART_ADVERTISING_DELAY_MS)); } } @@ -851,7 +857,7 @@ static void disconnected(struct bt_conn* conn, uint8_t reason) { escaped = false; // Delay before restarting advertising to ensure connection cleanup - k_work_reschedule(&restart_advertising_work, K_MSEC(100)); + k_work_reschedule(&restart_advertising_work, K_MSEC(RESTART_ADVERTISING_DELAY_MS)); update_led_status(); return; // Don't process as host connection } @@ -951,8 +957,10 @@ static int8_t hogp_index(struct bt_hogp* hogp) { } static uint8_t hogp_notify_cb(struct bt_hogp* hogp, struct bt_hogp_rep_info* rep, uint8_t err, const uint8_t* data) { - k_work_reschedule(&activity_led_off_work, K_MSEC(50)); // XXX what if work_fn is currently running? it might turn the led off after we turn it on + // Cancel any pending activity LED work to prevent race conditions + k_work_cancel_delayable(&activity_led_off_work); gpio_pin_set_dt(&led0, true); + k_work_reschedule(&activity_led_off_work, K_MSEC(ACTIVITY_LED_DURATION_MS)); if (!data) { return BT_GATT_ITER_STOP; @@ -1469,7 +1477,7 @@ int main() { } int64_t current_time = k_uptime_get(); - if (current_time - last_led_update > 5000) { + if (current_time - last_led_update > LED_STATUS_UPDATE_INTERVAL_MS) { update_led_status(); last_led_update = current_time; } @@ -1531,7 +1539,7 @@ int main() { } // without this sleep, some devices won't pair; some thread priority issue? - k_sleep(K_USEC(1)); // XXX + k_sleep(K_USEC(MAIN_LOOP_SLEEP_US)); // XXX } return 0; From 9c64c3f68c7d1e957f717da0be2caac04ec7def7 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Sat, 29 Nov 2025 19:13:17 +0100 Subject: [PATCH 20/21] optimization and descriptor --- firmware-bluetooth/src/main.cc | 348 ++++++++++++++++++++++----------- 1 file changed, 239 insertions(+), 109 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index e7c9f099..ee4a3dc5 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -31,6 +31,24 @@ LOG_MODULE_REGISTER(remapper, LOG_LEVEL_DBG); static bool host_mode_enabled = true; static bool peripheral_mode_enabled = true; +static constexpr uint8_t VIRTUAL_PERIPHERAL_DEV_ADDR = 0xFE; +static constexpr uint16_t VIRTUAL_PERIPHERAL_INTERFACE = (VIRTUAL_PERIPHERAL_DEV_ADDR << 8); +static bool virtual_peripheral_registered = false; + +// Helper function to convert connection index to interface address, avoiding collision with virtual peripheral +static uint16_t conn_idx_to_interface(uint8_t conn_idx) { + uint16_t interface = conn_idx << 8; + // Check for collision with virtual peripheral interface (0xFE00) + if (interface == VIRTUAL_PERIPHERAL_INTERFACE) { + LOG_ERR("Connection index collision detected! conn_idx=%d would create interface 0x%04X (same as virtual peripheral)", + conn_idx, interface); + // Use a different interface address to avoid collision + // Use 0xFF00 instead (conn_idx 0xFF is invalid, so this is safe) + return 0xFF00; + } + return interface; +} + // Nordic UART Service UUID static struct bt_uuid_128 uart_service_uuid = BT_UUID_INIT_128( 0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, @@ -76,8 +94,12 @@ 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); +static void advertising_retry_work_fn(struct k_work* work); static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uint8_t len); static void update_led_status(void); +static void unregister_virtual_peripheral(void); +static void handle_peripheral_disconnect(uint8_t reason, bool restart_advertising); +static bool conn_is_peripheral(struct bt_conn* conn); 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); @@ -95,6 +117,8 @@ static const int ACTIVITY_LED_DURATION_MS = 50; // Activity LED on duration static const int RESTART_ADVERTISING_DELAY_MS = 100; // Delay before restarting advertising static const int LED_STATUS_UPDATE_INTERVAL_MS = 5000; // LED status check interval static const int MAIN_LOOP_SLEEP_US = 1; // Main loop sleep to prevent pairing issues +static const int ADVERTISING_RETRY_DELAY_MS = 2000; // Delay before retrying advertising on failure +static const int ADVERTISING_MAX_RETRIES = 5; // Maximum number of advertising retry attempts // these macros don't work in C++ when used directly ("taking address of temporary array") static auto const BT_UUID_HIDS_ = (struct bt_uuid_16) BT_UUID_INIT_16(BT_UUID_HIDS_VAL); @@ -319,6 +343,8 @@ static void peripheral_connected(struct bt_conn *conn, uint8_t err) if (peripheral_mode_enabled) { peripheral_conn = bt_conn_ref(conn); LOG_INF("Peripheral connected"); + // Reset retry count on successful connection + atomic_set(&advertising_retry_count, 0); update_led_status(); LOG_INF("Peripheral connection established successfully"); @@ -328,6 +354,8 @@ static void peripheral_connected(struct bt_conn *conn, uint8_t err) static void restart_advertising_work_fn(struct k_work* work); static K_WORK_DELAYABLE_DEFINE(restart_advertising_work, restart_advertising_work_fn); +static atomic_t advertising_retry_count = ATOMIC_INIT(0); + static void restart_advertising_work_fn(struct k_work* work) { if (peripheral_mode_enabled && peripheral_conn == NULL) { LOG_INF("Restarting peripheral advertising after disconnect"); @@ -335,21 +363,37 @@ static void restart_advertising_work_fn(struct k_work* work) { } } +static bool conn_is_peripheral(struct bt_conn* conn) { + struct bt_conn_info info; + if (bt_conn_get_info(conn, &info) != 0) { + return false; + } + return info.role == BT_CONN_ROLE_PERIPHERAL; +} + +static void handle_peripheral_disconnect(uint8_t reason, bool restart_advertising) { + ARG_UNUSED(reason); + + if (peripheral_conn != NULL) { + bt_conn_unref(peripheral_conn); + peripheral_conn = NULL; + } + + bytes_read = 0; + escaped = false; + + update_led_status(); + + if (restart_advertising && peripheral_mode_enabled) { + k_work_reschedule(&restart_advertising_work, K_MSEC(RESTART_ADVERTISING_DELAY_MS)); + } +} + static void peripheral_disconnected(struct bt_conn *conn, uint8_t reason) { if (peripheral_mode_enabled && conn == peripheral_conn) { LOG_INF("Peripheral disconnected (reason %u)", reason); - bt_conn_unref(peripheral_conn); - peripheral_conn = NULL; - - // Reset packet parsing state on disconnect - bytes_read = 0; - escaped = false; - - update_led_status(); - - // Delay before restarting advertising to ensure connection cleanup - k_work_reschedule(&restart_advertising_work, K_MSEC(RESTART_ADVERTISING_DELAY_MS)); + handle_peripheral_disconnect(reason, true); } } @@ -463,11 +507,17 @@ static K_WORK_DELAYABLE_DEFINE(scan_start_work, scan_start_work_fn); static void scan_stop_work_fn(struct k_work* work) { scan_stop(); - k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + if (host_mode_enabled) { + k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); + } } static K_WORK_DEFINE(scan_stop_work, scan_stop_work_fn); static void disconnect_conn(struct bt_conn* conn, void* data) { + // Don't disconnect peripheral connections when disabling host mode + if (peripheral_mode_enabled && conn_is_peripheral(conn)) { + return; + } CHK(bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN)); } @@ -612,59 +662,12 @@ static void peripheral_mode_init(void) { bytes_read = 0; escaped = false; - // Create a virtual transmitter device that represents the input source - uint16_t virtual_interface = 0x0000; // Use proper interface 0x0000 + unregister_virtual_peripheral(); - - // 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) - 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, 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) - 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) - 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) - 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 - }; - - 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, 0x0F0D, 0x00C1, 0); + // Initialize with default descriptor (2 = horipad gamepad, matching previous behavior) + // The descriptor will be updated when the first packet arrives with a different descriptor number + our_descriptor_number = 2; + register_virtual_peripheral_with_descriptor(our_descriptor_number); their_descriptor_updated = true; @@ -675,9 +678,75 @@ static void peripheral_mode_init(void) { LOG_INF("Peripheral mode initialized with virtual gamepad descriptor"); } +static void unregister_virtual_peripheral(void) { + if (!virtual_peripheral_registered) { + return; + } + device_disconnected_callback(VIRTUAL_PERIPHERAL_DEV_ADDR); + virtual_peripheral_registered = false; +} + +static void register_virtual_peripheral_with_descriptor(uint8_t descriptor_idx) { + if (descriptor_idx >= NOUR_DESCRIPTORS) { + LOG_ERR("Invalid descriptor index: %d (max %d)", descriptor_idx, NOUR_DESCRIPTORS - 1); + return; + } + + // Unregister current virtual peripheral if registered + if (virtual_peripheral_registered) { + unregister_virtual_peripheral(); + } + + // Get descriptor from our_descriptors array + const our_descriptor_def_t* desc = &our_descriptors[descriptor_idx]; + + // Get VID/PID from descriptor, or use defaults + uint16_t vid = (desc->vid != 0) ? desc->vid : 0x0F0D; + uint16_t pid = (desc->pid != 0) ? desc->pid : 0x00C1; + + // Parse the descriptor + parse_descriptor(vid, pid, + desc->descriptor, + desc->descriptor_length, + VIRTUAL_PERIPHERAL_INTERFACE, 0); + + // Notify that device is connected + device_connected_callback(VIRTUAL_PERIPHERAL_INTERFACE, vid, pid, 0); + virtual_peripheral_registered = true; + + LOG_INF("Virtual peripheral registered with descriptor %d (VID: 0x%04X, PID: 0x%04X)", + descriptor_idx, vid, pid); +} + +static K_WORK_DELAYABLE_DEFINE(advertising_retry_work, advertising_retry_work_fn); + +static void advertising_retry_work_fn(struct k_work* work) { + if (!peripheral_mode_enabled) { + atomic_set(&advertising_retry_count, 0); + return; + } + + if (peripheral_conn != NULL) { + // Already connected, no need to retry + atomic_set(&advertising_retry_count, 0); + return; + } + + int retry_count = atomic_get(&advertising_retry_count); + if (retry_count >= ADVERTISING_MAX_RETRIES) { + LOG_ERR("Advertising failed after %d retries, giving up", ADVERTISING_MAX_RETRIES); + atomic_set(&advertising_retry_count, 0); + return; + } + + LOG_INF("Retrying peripheral advertising (attempt %d/%d)", retry_count + 1, ADVERTISING_MAX_RETRIES); + start_peripheral_advertising(); +} + static void start_peripheral_advertising(void) { if (!peripheral_mode_enabled) { LOG_DBG("Peripheral mode disabled, skipping advertising"); + atomic_set(&advertising_retry_count, 0); return; } @@ -706,50 +775,96 @@ static void start_peripheral_advertising(void) { int result = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), NULL, 0); if (result == 0) { LOG_INF("Peripheral advertising started as 'HID Remapper'"); + atomic_set(&advertising_retry_count, 0); // Reset retry count on success } else { LOG_ERR("Failed to start peripheral advertising, error: %d", result); if (result == -EALREADY) { LOG_WRN("Advertising already running"); + atomic_set(&advertising_retry_count, 0); // Not a real failure + } else { + // Schedule retry + int retry_count = atomic_inc(&advertising_retry_count); + if (retry_count < ADVERTISING_MAX_RETRIES) { + LOG_INF("Scheduling advertising retry in %d ms", ADVERTISING_RETRY_DELAY_MS); + k_work_reschedule(&advertising_retry_work, K_MSEC(ADVERTISING_RETRY_DELAY_MS)); + } else { + LOG_ERR("Advertising failed, max retries reached"); + atomic_set(&advertising_retry_count, 0); + } } } } static void handle_received_packet(const uint8_t* data, uint16_t len) { - if (len < sizeof(packet_t)) { - LOG_WRN("Packet too small: %d < %d", len, sizeof(packet_t)); + // TypeScript protocol format: [protocol_version, command, payload_length, descriptor_number, ...payload...] + // Minimum packet size: 4 bytes header + if (len < 4) { + LOG_WRN("Packet too small: %d < 4", len); return; } - packet_t* msg = (packet_t*) data; - uint16_t payload_len = len - sizeof(packet_t); + // Parse header bytes + uint8_t protocol_version = data[0]; + uint8_t command = data[1]; + uint8_t payload_length = data[2]; + uint8_t descriptor_number = data[3]; - 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); + // Calculate actual payload length (total length minus 4-byte header) + uint16_t actual_payload_len = len - 4; - if ((msg->protocol_version != PROTOCOL_VERSION) || - (msg->len != payload_len) || - (payload_len > 64) || - (msg->our_descriptor_number >= NOUR_DESCRIPTORS) || - ((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); + LOG_DBG("Packet validation: proto=%d (expect %d), cmd=%d, len=%d, payload=%d, desc=%d", + protocol_version, PROTOCOL_VERSION, command, payload_length, actual_payload_len, + descriptor_number); + + // Validate protocol version + if (protocol_version != PROTOCOL_VERSION) { + LOG_WRN("Invalid protocol version: %d (expect %d)", protocol_version, PROTOCOL_VERSION); return; } - if (msg->our_descriptor_number != our_descriptor_number) { - our_descriptor_number = msg->our_descriptor_number; - + // Validate command (7 = gamepad report) + if (command != 7) { + LOG_WRN("Invalid command: %d (expect 7 for gamepad report)", command); + return; + } + + // Validate payload length matches + if (payload_length != actual_payload_len) { + LOG_WRN("Payload length mismatch: header says %d, actual %d", payload_length, actual_payload_len); + return; + } + + // Validate payload length is reasonable + if (actual_payload_len > 64) { + LOG_WRN("Payload too large: %d (max 64)", actual_payload_len); + return; + } + + // Validate descriptor number (0-5) + if (descriptor_number >= NOUR_DESCRIPTORS) { + LOG_WRN("Invalid descriptor number: %d (max %d)", descriptor_number, NOUR_DESCRIPTORS - 1); + return; + } + + // Check if descriptor number changed and re-register virtual peripheral if needed + if (descriptor_number != our_descriptor_number) { + our_descriptor_number = descriptor_number; config_updated = true; LOG_INF("Descriptor number changed to %d", our_descriptor_number); + + // Re-register virtual peripheral with new descriptor + register_virtual_peripheral_with_descriptor(our_descriptor_number); } - handle_received_report(msg->data, len, 0x0000, msg->report_id); + // Extract payload (skip 4-byte header) + const uint8_t* payload = data + 4; + + // Process HID report with report_id = 1 (gamepad reports always use report ID 1) + handle_received_report(payload, actual_payload_len, VIRTUAL_PERIPHERAL_INTERFACE, 1); - LOG_DBG("Packet processed: proto=%d, desc=%d, report_id=%d, len=%d", - msg->protocol_version, msg->our_descriptor_number, msg->report_id, len); + LOG_DBG("Packet processed: proto=%d, cmd=%d, desc=%d, report_id=1, payload_len=%d", + protocol_version, command, descriptor_number, actual_payload_len); } static void process_byte_with_framing(uint8_t c) { @@ -824,6 +939,11 @@ static void connected(struct bt_conn* conn, uint8_t conn_err) { if (info.role == BT_CONN_ROLE_PERIPHERAL) { LOG_INF("Incoming peripheral connection: %s", addr); + // Ensure peripheral_conn is set (in case peripheral_connected callback wasn't called first) + if (peripheral_mode_enabled && peripheral_conn == NULL) { + peripheral_conn = bt_conn_ref(conn); + update_led_status(); + } // Don't force security for incoming connections - let client decide return; } @@ -846,19 +966,12 @@ static void disconnected(struct bt_conn* conn, uint8_t reason) { LOG_INF("Disconnected: %s (reason=%u)", addr, reason); + bool is_peripheral = peripheral_mode_enabled && conn_is_peripheral(conn); + // Handle peripheral mode disconnection (in case peripheral_disconnected wasn't called) - if (peripheral_mode_enabled && conn == peripheral_conn) { + if (is_peripheral) { LOG_INF("Peripheral connection cleanup in generic disconnect handler"); - bt_conn_unref(peripheral_conn); - peripheral_conn = NULL; - - // Reset packet parsing state on disconnect - bytes_read = 0; - escaped = false; - - // Delay before restarting advertising to ensure connection cleanup - k_work_reschedule(&restart_advertising_work, K_MSEC(RESTART_ADVERTISING_DELAY_MS)); - update_led_status(); + handle_peripheral_disconnect(reason, true); return; // Don't process as host connection } @@ -970,11 +1083,17 @@ static uint8_t hogp_notify_cb(struct bt_hogp* hogp, struct bt_hogp_rep_info* rep 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 if (host_mode_enabled) { + // Only reschedule if host mode is still enabled k_work_reschedule(&scan_start_work, K_MSEC(SCAN_DELAY_MS)); } static struct report_type buf; - buf.interface = hogp_index(hogp) << 8; + int8_t hogp_idx = hogp_index(hogp); + if (hogp_idx < 0) { + LOG_ERR("Invalid hogp index, dropping report"); + return BT_GATT_ITER_STOP; + } + buf.interface = conn_idx_to_interface((uint8_t)hogp_idx); buf.len = bt_hogp_rep_size(rep) + 1; buf.data[0] = bt_hogp_rep_id(rep); @@ -1028,7 +1147,8 @@ static void hogp_ready_work_fn(struct k_work* work) { bt_addr_le_copy(&find_bond.addr, bt_conn_get_dst(bt_hogp_conn(item.hogp))); bt_foreach_bond(BT_ID_DEFAULT, find_bond_cb, &find_bond); LOG_DBG("found bond idx: %d", find_bond.found_idx); - device_connected_callback(bt_conn_index(bt_hogp_conn(item.hogp)) << 8, 1, 1, find_bond.found_idx); + uint8_t conn_idx = bt_conn_index(bt_hogp_conn(item.hogp)); + device_connected_callback(conn_idx_to_interface(conn_idx), 1, 1, find_bond.found_idx); while (NULL != (rep = bt_hogp_rep_next(item.hogp, rep))) { if (bt_hogp_rep_type(rep) == BT_HIDS_REPORT_TYPE_INPUT) { @@ -1350,10 +1470,19 @@ void disable_host_mode() { if (host_mode_enabled) { host_mode_enabled = false; LOG_INF("Host mode disabled"); + + // Cancel pending scan work items + k_work_cancel_delayable(&scan_start_work); + k_work_cancel(&scan_stop_work); + if (scanning) { scan_stop(); } + + // Disconnect only host mode connections (not peripheral connections) bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_conn, NULL); + + update_led_status(); } } @@ -1369,10 +1498,20 @@ void disable_peripheral_mode() { if (peripheral_mode_enabled) { peripheral_mode_enabled = false; LOG_INF("Peripheral mode disabled"); + + // Cancel pending advertising restart and retry work + k_work_cancel_delayable(&restart_advertising_work); + k_work_cancel_delayable(&advertising_retry_work); + atomic_set(&advertising_retry_count, 0); + CHK(bt_le_adv_stop()); if (peripheral_conn) { CHK(bt_conn_disconnect(peripheral_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN)); + handle_peripheral_disconnect(BT_HCI_ERR_REMOTE_USER_TERM_CONN, false); } + unregister_virtual_peripheral(); + + update_led_status(); } } @@ -1462,7 +1601,8 @@ int main() { 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); + uint16_t interface = conn_idx_to_interface(incoming_descriptor.conn_idx); + parse_descriptor(1, 1, incoming_descriptor.data, incoming_descriptor.size, interface, 0); } if (their_descriptor_updated) { @@ -1506,16 +1646,6 @@ int main() { } } - 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 (resume_pending) { resume_pending = false; suspended = false; From 054a045f9771bec22cb64b619a91cc21c35446c8 Mon Sep 17 00:00:00 2001 From: Valentin Squirelo Date: Sat, 29 Nov 2025 20:00:56 +0100 Subject: [PATCH 21/21] fix forward declaration and removed duplicate declaration --- firmware-bluetooth/src/main.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firmware-bluetooth/src/main.cc b/firmware-bluetooth/src/main.cc index ee4a3dc5..82d60f0e 100644 --- a/firmware-bluetooth/src/main.cc +++ b/firmware-bluetooth/src/main.cc @@ -98,6 +98,7 @@ static void advertising_retry_work_fn(struct k_work* work); static bool do_send_report(uint8_t interface, const uint8_t* report_with_id, uint8_t len); static void update_led_status(void); static void unregister_virtual_peripheral(void); +static void register_virtual_peripheral_with_descriptor(uint8_t descriptor_idx); static void handle_peripheral_disconnect(uint8_t reason, bool restart_advertising); static bool conn_is_peripheral(struct bt_conn* conn); @@ -333,6 +334,8 @@ static void uart_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value LOG_DBG("UART CCC changed: %d", value); } +static atomic_t advertising_retry_count = ATOMIC_INIT(0); + static void peripheral_connected(struct bt_conn *conn, uint8_t err) { if (err) { @@ -354,8 +357,6 @@ static void peripheral_connected(struct bt_conn *conn, uint8_t err) static void restart_advertising_work_fn(struct k_work* work); static K_WORK_DELAYABLE_DEFINE(restart_advertising_work, restart_advertising_work_fn); -static atomic_t advertising_retry_count = ATOMIC_INIT(0); - static void restart_advertising_work_fn(struct k_work* work) { if (peripheral_mode_enabled && peripheral_conn == NULL) { LOG_INF("Restarting peripheral advertising after disconnect");