From aebd5f47a1b13652796a93ab303786705bd18059 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 2 Feb 2026 15:09:07 +0100 Subject: [PATCH 01/13] pbio/drv/bluetooth: Keep peripherals connected when program ends. Do it on shutdown instead, and simplify shutdown logic more generally. --- lib/pbio/drv/bluetooth/bluetooth.c | 78 +++++++------------ lib/pbio/drv/bluetooth/bluetooth.h | 2 +- lib/pbio/drv/bluetooth/bluetooth_btstack.c | 52 +++++++++---- .../drv/bluetooth/bluetooth_stm32_bluenrg.c | 36 ++++++--- .../drv/bluetooth/bluetooth_stm32_cc2640.c | 28 ++++--- lib/pbio/include/pbdrv/bluetooth.h | 12 --- lib/pbio/src/main.c | 6 +- 7 files changed, 110 insertions(+), 104 deletions(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth.c b/lib/pbio/drv/bluetooth/bluetooth.c index f1d03f46b..5f95a2c4c 100644 --- a/lib/pbio/drv/bluetooth/bluetooth.c +++ b/lib/pbio/drv/bluetooth/bluetooth.c @@ -550,20 +550,8 @@ pbio_error_t pbdrv_bluetooth_await_classic_task(pbio_os_state_t *state, void *co } #endif // PBDRV_CONFIG_BLUETOOTH_NUM_CLASSIC_CONNECTIONS -void pbdrv_bluetooth_cancel_operation_request(void) { - // Only some peripheral actions support cancellation. - DEBUG_PRINT("Bluetooth operation cancel requested.\n"); - for (uint8_t i = 0; i < PBDRV_CONFIG_BLUETOOTH_NUM_PERIPHERALS; i++) { - pbdrv_bluetooth_peripheral_t *peri = pbdrv_bluetooth_peripheral_get_by_index(i); - peri->cancel = true; - } - #if PBDRV_CONFIG_BLUETOOTH_NUM_CLASSIC_CONNECTIONS - // Revisit: Cancel all. - pbdrv_bluetooth_classic_task_context.cancel = true; - #endif // PBDRV_CONFIG_BLUETOOTH_NUM_CLASSIC_CONNECTIONS -} - -static bool shutting_down; +static bool pbdrv_bluetooth_shutting_down; +static pbio_os_timer_t pbdrv_bluetooth_shutting_down_watchdog; /** * This is the main high level pbdrv/bluetooth thread. It is driven forward by @@ -588,6 +576,11 @@ pbio_error_t pbdrv_bluetooth_process_thread(pbio_os_state_t *state, void *contex static uint8_t peri_index; static pbdrv_bluetooth_peripheral_t *peri; + // Force shutdown if Bluetooth fails to deinit gracefully. + if (pbdrv_bluetooth_shutting_down && pbio_os_timer_is_expired(&pbdrv_bluetooth_shutting_down_watchdog)) { + goto shutdown; + } + PBIO_OS_ASYNC_BEGIN(state); init: @@ -607,7 +600,7 @@ pbio_error_t pbdrv_bluetooth_process_thread(pbio_os_state_t *state, void *contex DEBUG_PRINT("Bluetooth is now on and initialized.\n"); // Service scheduled tasks as long as Bluetooth is enabled. - while (!shutting_down) { + while (!pbdrv_bluetooth_shutting_down) { // In principle, this wait is only needed if there is nothing to do. // In practice, leaving it here helps rather than hurts since it @@ -668,14 +661,22 @@ pbio_error_t pbdrv_bluetooth_process_thread(pbio_os_state_t *state, void *contex DEBUG_PRINT("Shutdown requested.\n"); - // Power down the chip. This will disconnect from the host first. - // The peripheral has already been disconnected in the cleanup that runs after - // every program. If we change that behavior, we can do the disconnect here. + // Gracefully disconnect from the hosts and peripherals. + PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_disconnect_all(&sub)); + + DEBUG_PRINT("Hosts and peripherals disconnected. Resetting Bluetooth controller.\n"); + +shutdown: PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_controller_reset(&sub, &timer)); + pbdrv_bluetooth_shutting_down = false; pbio_busy_count_down(); + // Due to the nested logic of the Bluetooth process, this may be called + // again after completion. Re-enter here if that happens for safety, so we + // don't run the code since the last checkpoint over and over. + PBIO_OS_ASYNC_SET_CHECKPOINT(state); PBIO_OS_ASYNC_END(PBIO_SUCCESS); } @@ -693,21 +694,15 @@ pbio_error_t pbdrv_bluetooth_close_user_tasks(pbio_os_state_t *state, pbio_os_ti PBIO_OS_ASYNC_BEGIN(state); - // Requests peripheral operations to cancel, if they support it. - pbdrv_bluetooth_cancel_operation_request(); - for (peri_index = 0; peri_index < PBDRV_CONFIG_BLUETOOTH_NUM_PERIPHERALS; peri_index++) { peri = pbdrv_bluetooth_peripheral_get_by_index(peri_index); - - // Await ongoing peripheral user task. - PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_await_peripheral_command(&sub, peri)); - - // Disconnect peripheral. - pbdrv_bluetooth_peripheral_disconnect(peri); + // Await ongoing peripheral user task, requesting cancellation to + // expedite this if the task supports it. Peripherals remain connected. + peri->cancel = true; PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_await_peripheral_command(&sub, peri)); } - // Let ongoing user task finish first. + // Let ongoing user advertising or scan task finish first. PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_await_advertise_or_scan_command(&sub, NULL)); // Stop scanning. @@ -718,6 +713,8 @@ pbio_error_t pbdrv_bluetooth_close_user_tasks(pbio_os_state_t *state, pbio_os_ti pbdrv_bluetooth_start_broadcasting(NULL, 0); PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_await_advertise_or_scan_command(&sub, NULL)); + // TODO: Close Bluetooth classic tasks. + PBIO_OS_ASYNC_END(PBIO_SUCCESS); } @@ -728,29 +725,10 @@ void pbdrv_bluetooth_deinit(void) { return; } - // Under normal operation ::pbdrv_bluetooth_close_user_tasks completes - // normally and there should be no user activity at this point. If there - // is, a task got stuck, so exit forcefully. - bool failed_to_stop = advertising_or_scan_err == PBIO_ERROR_AGAIN; - for (uint8_t i = 0; i < PBDRV_CONFIG_BLUETOOTH_NUM_PERIPHERALS; i++) { - pbdrv_bluetooth_peripheral_t *peri = pbdrv_bluetooth_peripheral_get_by_index(i); - if (peri->err == PBIO_ERROR_AGAIN) { - failed_to_stop = true; - break; - } - } - - if (failed_to_stop) { - // Hard reset without waiting on completion of any process. - DEBUG_PRINT("Bluetooth deinit: forcing hard reset due to busy tasks.\n"); - pbdrv_bluetooth_controller_reset_hard(); - return; - } - - // Gracefully disconnect from host and power down. + // Ask Bluetooth process to proceed to shutdown. + pbio_os_timer_set(&pbdrv_bluetooth_shutting_down_watchdog, 3000); + pbdrv_bluetooth_shutting_down = true; pbio_busy_count_up(); - pbdrv_bluetooth_cancel_operation_request(); - shutting_down = true; pbio_os_request_poll(); } diff --git a/lib/pbio/drv/bluetooth/bluetooth.h b/lib/pbio/drv/bluetooth/bluetooth.h index 602d3e8a5..86f146c91 100644 --- a/lib/pbio/drv/bluetooth/bluetooth.h +++ b/lib/pbio/drv/bluetooth/bluetooth.h @@ -18,9 +18,9 @@ #include void pbdrv_bluetooth_init_hci(void); -void pbdrv_bluetooth_controller_reset_hard(void); pbio_error_t pbdrv_bluetooth_controller_reset(pbio_os_state_t *state, pbio_os_timer_t *timer); pbio_error_t pbdrv_bluetooth_controller_initialize(pbio_os_state_t *state, pbio_os_timer_t *timer); +pbio_error_t pbdrv_bluetooth_disconnect_all(pbio_os_state_t *state); pbio_error_t pbdrv_bluetooth_start_broadcasting_func(pbio_os_state_t *state, void *context); pbio_error_t pbdrv_bluetooth_start_advertising_func(pbio_os_state_t *state, void *context); diff --git a/lib/pbio/drv/bluetooth/bluetooth_btstack.c b/lib/pbio/drv/bluetooth/bluetooth_btstack.c index 1e80da0b7..15a6c5b29 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_btstack.c +++ b/lib/pbio/drv/bluetooth/bluetooth_btstack.c @@ -1264,20 +1264,6 @@ pbio_error_t pbdrv_bluetooth_controller_reset(pbio_os_state_t *state, pbio_os_ti PBIO_OS_ASYNC_BEGIN(state); - // Disconnect gracefully if connected to host. - #if PBDRV_CONFIG_BLUETOOTH_BTSTACK_NUM_LE_HOSTS - static size_t i; - static pbdrv_bluetooth_btstack_host_connection_t *host; - for (i = 0; i < PBDRV_CONFIG_BLUETOOTH_BTSTACK_NUM_LE_HOSTS; i++) { - host = &host_connections[i]; - if (host->con_handle == HCI_CON_HANDLE_INVALID) { - continue; - } - gap_disconnect(host->con_handle); - PBIO_OS_AWAIT_UNTIL(state, host->con_handle == HCI_CON_HANDLE_INVALID); - } - #endif - // Wait for power off. PBIO_OS_AWAIT(state, &sub, bluetooth_btstack_handle_power_control(&sub, HCI_POWER_OFF, HCI_STATE_OFF)); @@ -1296,6 +1282,44 @@ pbio_error_t pbdrv_bluetooth_controller_initialize(pbio_os_state_t *state, pbio_ PBIO_OS_ASYNC_END(PBIO_SUCCESS); } +pbio_error_t pbdrv_bluetooth_disconnect_all(pbio_os_state_t *state) { + + if (!pbdrv_bluetooth_hci_is_enabled()) { + return PBIO_ERROR_INVALID_OP; + } + + static pbio_os_state_t sub; + static uint8_t i; + static pbdrv_bluetooth_peripheral_t *peri; + + PBIO_OS_ASYNC_BEGIN(state); + + // Disconnect gracefully if connected to peripherals. + #if PBDRV_CONFIG_BLUETOOTH_NUM_PERIPHERALS + for (i = 0; i < PBDRV_CONFIG_BLUETOOTH_NUM_PERIPHERALS; i++) { + peri = pbdrv_bluetooth_peripheral_get_by_index(i); + // Must call the platform specific function since this runs after + // the Bluetooth main loop ends. + PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_peripheral_disconnect_func(&sub, peri)); + } + #endif + + // Disconnect gracefully if connected to hosts. + #if PBDRV_CONFIG_BLUETOOTH_BTSTACK_NUM_LE_HOSTS + static pbdrv_bluetooth_btstack_host_connection_t *host; + for (i = 0; i < PBDRV_CONFIG_BLUETOOTH_BTSTACK_NUM_LE_HOSTS; i++) { + host = &host_connections[i]; + if (host->con_handle == HCI_CON_HANDLE_INVALID) { + continue; + } + gap_disconnect(host->con_handle); + PBIO_OS_AWAIT_UNTIL(state, host->con_handle == HCI_CON_HANDLE_INVALID); + } + #endif + + PBIO_OS_ASYNC_END(PBIO_SUCCESS); +} + static void bluetooth_btstack_run_loop_set_timer(btstack_timer_source_t *ts, uint32_t timeout_in_ms) { ts->timeout = pbdrv_clock_get_ms() + timeout_in_ms; } diff --git a/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c b/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c index aee534487..cbd78534e 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c +++ b/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c @@ -569,6 +569,10 @@ pbio_error_t pbdrv_bluetooth_peripheral_disconnect_func(pbio_os_state_t *state, PBIO_OS_ASYNC_BEGIN(state); + if (!pbdrv_bluetooth_peripheral_is_connected(peri)) { + return PBIO_SUCCESS; + } + PBIO_OS_AWAIT_WHILE(state, write_xfer_size); aci_gap_terminate_begin(peri->con_handle, HCI_OE_USER_ENDED_CONNECTION); PBIO_OS_AWAIT_UNTIL(state, hci_command_status); @@ -1246,20 +1250,8 @@ void pbdrv_bluetooth_controller_reset_hard(void) { pbio_error_t pbdrv_bluetooth_controller_reset(pbio_os_state_t *state, pbio_os_timer_t *timer) { PBIO_OS_ASYNC_BEGIN(state); - - // Disconnect gracefully if connected to host. - if (conn_handle) { - PBIO_OS_AWAIT_WHILE(state, write_xfer_size); - aci_gap_terminate_begin(conn_handle, HCI_OE_USER_ENDED_CONNECTION); - PBIO_OS_AWAIT_UNTIL(state, hci_command_status); - aci_gap_terminate_end(); - PBIO_OS_AWAIT_UNTIL(state, conn_handle == 0); - } - pbdrv_bluetooth_controller_reset_hard(); - PBIO_OS_AWAIT_MS(state, timer, 50); - PBIO_OS_ASYNC_END(PBIO_SUCCESS); } @@ -1292,6 +1284,26 @@ pbio_error_t pbdrv_bluetooth_controller_initialize(pbio_os_state_t *state, pbio_ PBIO_OS_ASYNC_END(PBIO_SUCCESS); } +pbio_error_t pbdrv_bluetooth_disconnect_all(pbio_os_state_t *state) { + + static pbio_os_state_t sub; + + PBIO_OS_ASYNC_BEGIN(state); + + PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_peripheral_disconnect_func(&sub, &peripheral_singleton)); + + // Disconnect gracefully if connected to host. + if (conn_handle) { + PBIO_OS_AWAIT_WHILE(state, write_xfer_size); + aci_gap_terminate_begin(conn_handle, HCI_OE_USER_ENDED_CONNECTION); + PBIO_OS_AWAIT_UNTIL(state, hci_command_status); + aci_gap_terminate_end(); + PBIO_OS_AWAIT_UNTIL(state, conn_handle == 0); + } + + PBIO_OS_ASYNC_END(PBIO_SUCCESS); +} + static pbio_os_process_t pbdrv_bluetooth_spi_process; static pbio_error_t pbdrv_bluetooth_spi_process_thread(pbio_os_state_t *state, void *context) { diff --git a/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c b/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c index bc597b67b..6cfc2c1d6 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c +++ b/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c @@ -1855,18 +1855,8 @@ void pbdrv_bluetooth_controller_reset_hard(void) { pbio_error_t pbdrv_bluetooth_controller_reset(pbio_os_state_t *state, pbio_os_timer_t *timer) { PBIO_OS_ASYNC_BEGIN(state); - - // Disconnect gracefully if connected to host. - if (conn_handle != NO_CONNECTION) { - PBIO_OS_AWAIT_WHILE(state, write_xfer_size); - GAP_TerminateLinkReq(conn_handle, 0x13); - PBIO_OS_AWAIT_UNTIL(state, conn_handle == NO_CONNECTION); - } - pbdrv_bluetooth_controller_reset_hard(); - PBIO_OS_AWAIT_MS(state, timer, 150); - PBIO_OS_ASYNC_END(PBIO_SUCCESS); } @@ -1910,6 +1900,24 @@ pbio_error_t pbdrv_bluetooth_controller_initialize(pbio_os_state_t *state, pbio_ PBIO_OS_ASYNC_END(PBIO_SUCCESS); } +pbio_error_t pbdrv_bluetooth_disconnect_all(pbio_os_state_t *state) { + + static pbio_os_state_t sub; + + PBIO_OS_ASYNC_BEGIN(state); + + PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_peripheral_disconnect_func(&sub, &peripheral_singleton)); + + // Disconnect gracefully if connected to host. + if (conn_handle != NO_CONNECTION) { + PBIO_OS_AWAIT_WHILE(state, write_xfer_size); + GAP_TerminateLinkReq(conn_handle, 0x13); + PBIO_OS_AWAIT_UNTIL(state, conn_handle == NO_CONNECTION); + } + + PBIO_OS_ASYNC_END(PBIO_SUCCESS); +} + static pbio_os_process_t pbdrv_bluetooth_spi_process; static pbio_error_t pbdrv_bluetooth_spi_process_thread(pbio_os_state_t *state, void *context) { diff --git a/lib/pbio/include/pbdrv/bluetooth.h b/lib/pbio/include/pbdrv/bluetooth.h index 2bcfb7909..1d5829b50 100644 --- a/lib/pbio/include/pbdrv/bluetooth.h +++ b/lib/pbio/include/pbdrv/bluetooth.h @@ -501,15 +501,6 @@ pbio_error_t pbdrv_bluetooth_peripheral_write_characteristic(pbdrv_bluetooth_per */ pbio_error_t pbdrv_bluetooth_await_peripheral_command(pbio_os_state_t *state, void *context); -/** - * Requests active Bluetooth tasks to be cancelled. It is up to the task - * implementation to respect or ignore it. The task should still be awaited - * with ::pbdrv_bluetooth_await_advertise_or_scan_command or - * with ::pbdrv_bluetooth_await_peripheral_command. Cancelling just allows - * some commands to exit earlier. - */ -void pbdrv_bluetooth_cancel_operation_request(void); - // // Functions related to advertising and scanning. // @@ -711,9 +702,6 @@ static inline pbio_error_t pbdrv_bluetooth_await_peripheral_command(pbio_os_stat return PBIO_ERROR_NOT_SUPPORTED; } -static inline void pbdrv_bluetooth_cancel_operation_request(void) { -} - static inline pbio_error_t pbdrv_bluetooth_start_advertising(bool start) { return PBIO_ERROR_NOT_SUPPORTED; } diff --git a/lib/pbio/src/main.c b/lib/pbio/src/main.c index c9e3701d6..2e9cd8e0a 100644 --- a/lib/pbio/src/main.c +++ b/lib/pbio/src/main.c @@ -57,12 +57,8 @@ void pbio_deinit(void) { * MicroPython REPL. */ void pbio_main_soft_stop(void) { - pbio_port_stop_user_actions(false); - pbdrv_sound_stop(); - - pbdrv_bluetooth_cancel_operation_request(); } /** @@ -91,7 +87,7 @@ pbio_error_t pbio_main_stop_application_resources(void) { pbio_error_t err; pbio_os_state_t state = 0; pbio_os_timer_t timer; - pbio_os_timer_set(&timer, 5000); + pbio_os_timer_set(&timer, 3000); DEBUG_PRINT("Waiting for Bluetooth user tasks to close...\n"); From da94f935901aa916024a352ff1fcf7500e4103a5 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 2 Feb 2026 15:18:59 +0100 Subject: [PATCH 02/13] pbio/drv/bluetooth: Allow new connection if not in use. Release user claim if it is not connected and not currently busy connecting. This allows reconnecting if disconnected due to the peripheral powering off. --- lib/pbio/drv/bluetooth/bluetooth.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth.c b/lib/pbio/drv/bluetooth/bluetooth.c index 5f95a2c4c..7d49e1937 100644 --- a/lib/pbio/drv/bluetooth/bluetooth.c +++ b/lib/pbio/drv/bluetooth/bluetooth.c @@ -166,7 +166,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_get_available(pbdrv_bluetooth_peripheral pbdrv_bluetooth_peripheral_t *peri = pbdrv_bluetooth_peripheral_get_by_index(i); // Test if not already in use, not connected, and not busy. - if (!pbdrv_bluetooth_peripheral_is_connected(peri) && !peri->user && !peri->func) { + if (!pbdrv_bluetooth_peripheral_is_connected(peri) && !peri->func) { // Claim this device for new user. peri->user = user; *peripheral = peri; @@ -700,6 +700,9 @@ pbio_error_t pbdrv_bluetooth_close_user_tasks(pbio_os_state_t *state, pbio_os_ti // expedite this if the task supports it. Peripherals remain connected. peri->cancel = true; PBIO_OS_AWAIT(state, &sub, pbdrv_bluetooth_await_peripheral_command(&sub, peri)); + + // Allow peripheral to be used again next time, even if still connected. + peri->user = NULL; } // Let ongoing user advertising or scan task finish first. From d40ac45911cf082e5d925e4c5f3c879670cf9c70 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 2 Feb 2026 16:04:37 +0100 Subject: [PATCH 03/13] pbio/drv/bluetooth: Move scan config to instance. Multiple peripherals of the same kind may have a different config. This was still in its singleton form after adding support for multiple peripherals earlier. With a singleton instance, there would be a conflict for e.g. a Powered Up remote and a Mario Hub. --- lib/pbio/drv/bluetooth/bluetooth.c | 2 +- lib/pbio/drv/bluetooth/bluetooth_btstack.c | 14 ++++----- .../drv/bluetooth/bluetooth_stm32_bluenrg.c | 10 +++--- .../drv/bluetooth/bluetooth_stm32_cc2640.c | 16 +++++----- lib/pbio/include/pbdrv/bluetooth.h | 2 +- .../iodevices/pb_type_iodevices_lwp3device.c | 31 ++++++++----------- .../pb_type_iodevices_xbox_controller.c | 29 ++++++++++------- 7 files changed, 52 insertions(+), 52 deletions(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth.c b/lib/pbio/drv/bluetooth/bluetooth.c index 7d49e1937..589aecd78 100644 --- a/lib/pbio/drv/bluetooth/bluetooth.c +++ b/lib/pbio/drv/bluetooth/bluetooth.c @@ -212,7 +212,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect(pbdrv_bluetooth_periphe memset(peri->bdaddr, 0, sizeof(peri->bdaddr)); // Initialize operation for handling on the main thread. - peri->config = config; + peri->config = *config; peri->func = pbdrv_bluetooth_peripheral_scan_and_connect_func; peri->err = PBIO_ERROR_AGAIN; peri->cancel = false; diff --git a/lib/pbio/drv/bluetooth/bluetooth_btstack.c b/lib/pbio/drv/bluetooth/bluetooth_btstack.c index 15a6c5b29..83f79ddfd 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_btstack.c +++ b/lib/pbio/drv/bluetooth/bluetooth_btstack.c @@ -377,13 +377,13 @@ static void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packe case GATT_EVENT_NOTIFICATION: for (uint8_t i = 0; i < PBDRV_CONFIG_BLUETOOTH_NUM_PERIPHERALS; i++) { pbdrv_bluetooth_peripheral_t *peri = pbdrv_bluetooth_peripheral_get_by_index(i); - if (!peri || !peri->config || !peri->config->notification_handler) { + if (!peri || !peri->config.notification_handler) { continue; } if (gatt_event_notification_get_handle(packet) == peri->con_handle) { uint16_t length = gatt_event_notification_get_value_length(packet); const uint8_t *value = gatt_event_notification_get_value(packet); - peri->config->notification_handler(peri->user, value, length); + peri->config.notification_handler(peri->user, value, length); } } break; @@ -691,7 +691,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s start_scan: // Wait for advertisement that matches the filter unless timed out or cancelled. - PBIO_OS_AWAIT_UNTIL(state, (peri->config->timeout && pbio_os_timer_is_expired(&peri->timer)) || + PBIO_OS_AWAIT_UNTIL(state, (peri->config.timeout && pbio_os_timer_is_expired(&peri->timer)) || peri->cancel || (hci_event_is_type(event_packet, GAP_EVENT_ADVERTISING_REPORT) && ({ uint8_t event_type = gap_event_advertising_report_get_advertising_event_type(event_packet); @@ -700,7 +700,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s gap_event_advertising_report_get_address(event_packet, address); // Match advertisement data against context-specific filter. - flags = peri->config->match_adv(peri->user, event_type, data, NULL, address, peri->bdaddr); + flags = peri->config.match_adv(peri->user, event_type, data, NULL, address, peri->bdaddr); // Store the address to compare with scan response later. if (flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) { @@ -714,7 +714,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s (flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) && !(flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS); }))); - if ((peri->config->timeout && pbio_os_timer_is_expired(&peri->timer)) || peri->cancel) { + if ((peri->config.timeout && pbio_os_timer_is_expired(&peri->timer)) || peri->cancel) { DEBUG_PRINT("Scan %s.\n", peri->cancel ? "canceled": "timed out"); gap_stop_scan(); return peri->cancel ? PBIO_ERROR_CANCELED : PBIO_ERROR_TIMEDOUT; @@ -736,7 +736,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s bd_addr_t address; gap_event_advertising_report_get_address(event_packet, address); - flags = peri->config->match_adv_rsp(peri->user, event_type, NULL, detected_name, address, peri->bdaddr); + flags = peri->config.match_adv_rsp(peri->user, event_type, NULL, detected_name, address, peri->bdaddr); (flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) && (flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS); }))); @@ -787,7 +787,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s DEBUG_PRINT("Connected with handle %d.\n", peri->con_handle); // We are done if no pairing is requested. - if (!peri->config->options & PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR) { + if (!(peri->config.options & PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR)) { DEBUG_PRINT("Simple connection done.\n"); return PBIO_SUCCESS; } diff --git a/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c b/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c index cbd78534e..898d73050 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c +++ b/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c @@ -334,7 +334,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s pbdrv_bluetooth_peripheral_t *peri = context; // Scan and connect timeout, if applicable. - bool timed_out = peri->config->timeout && pbio_os_timer_is_expired(&peri->timer); + bool timed_out = peri->config.timeout && pbio_os_timer_is_expired(&peri->timer); // Operation can be explicitly cancelled or automatically on inactivity. if (!peri->cancel) { @@ -367,7 +367,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s le_advertising_info *subevt = (void *)&read_buf[5]; // Context specific advertisement filter. - pbdrv_bluetooth_ad_match_result_flags_t adv_flags = peri->config->match_adv(peri->user, subevt->evt_type, subevt->data_RSSI, NULL, subevt->bdaddr, peri->bdaddr); + pbdrv_bluetooth_ad_match_result_flags_t adv_flags = peri->config.match_adv(peri->user, subevt->evt_type, subevt->data_RSSI, NULL, subevt->bdaddr, peri->bdaddr); // If it doesn't match context-specific filter, keep scanning. if (!(adv_flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE)) { @@ -404,7 +404,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // If the response data is not right or if the address doesn't match advertisement, keep scanning. le_advertising_info *subevt = (void *)&read_buf[5]; const char *detected_name = (char *)&subevt->data_RSSI[2]; - pbdrv_bluetooth_ad_match_result_flags_t rsp_flags = peri->config->match_adv_rsp(peri->user, subevt->evt_type, NULL, detected_name, subevt->bdaddr, peri->bdaddr); + pbdrv_bluetooth_ad_match_result_flags_t rsp_flags = peri->config.match_adv_rsp(peri->user, subevt->evt_type, NULL, detected_name, subevt->bdaddr, peri->bdaddr); if (!(rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) || !(rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS)) { continue; } @@ -948,8 +948,8 @@ static void handle_event(hci_event_pckt *event) { case EVT_BLUE_GATT_NOTIFICATION: { evt_gatt_attr_notification *subevt = (void *)evt->data; - if (peri->config->notification_handler) { - peri->config->notification_handler(peri->user, subevt->attr_value, subevt->event_data_length - 2); + if (peri->config.notification_handler) { + peri->config.notification_handler(peri->user, subevt->attr_value, subevt->event_data_length - 2); } } break; diff --git a/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c b/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c index 6cfc2c1d6..37b6c7866 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c +++ b/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c @@ -375,7 +375,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s static pbio_os_timer_t peripheral_scan_restart_timer; // Scan and connect timeout, if applicable. - bool timed_out = peri->config->timeout && pbio_os_timer_is_expired(&peri->timer); + bool timed_out = peri->config.timeout && pbio_os_timer_is_expired(&peri->timer); // Operation can be explicitly cancelled or automatically on inactivity. if (!peri->cancel) { @@ -388,7 +388,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // Optionally, disconnect from host (usually Pybricks Code). if (conn_handle != NO_CONNECTION && - (peri->config->options & PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_DISCONNECT_HOST)) { + (peri->config.options & PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_DISCONNECT_HOST)) { DEBUG_PRINT("Disconnect from Pybricks code (%d).\n", conn_handle); // Guard used in pbdrv_bluetooth_host_is_connected so higher level // processes won't try to send anything while we are disconnecting. @@ -444,7 +444,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s } // Context specific advertisement filter. - pbdrv_bluetooth_ad_match_result_flags_t adv_flags = peri->config->match_adv(peri->user, read_buf[9], &read_buf[19], NULL, &read_buf[11], peri->bdaddr); + pbdrv_bluetooth_ad_match_result_flags_t adv_flags = peri->config.match_adv(peri->user, read_buf[9], &read_buf[19], NULL, &read_buf[11], peri->bdaddr); // If it doesn't match context-specific filter, keep scanning. if (!(adv_flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE)) { @@ -491,7 +491,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s const char *detected_name = (const char *)&read_buf[21]; const uint8_t *response_address = &read_buf[11]; - pbdrv_bluetooth_ad_match_result_flags_t rsp_flags = peri->config->match_adv_rsp(peri->user, read_buf[9], NULL, detected_name, response_address, peri->bdaddr); + pbdrv_bluetooth_ad_match_result_flags_t rsp_flags = peri->config.match_adv_rsp(peri->user, read_buf[9], NULL, detected_name, response_address, peri->bdaddr); // If the response data is not right or if the address doesn't match advertisement, keep scanning. if (!(rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) || !(rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS)) { @@ -523,7 +523,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // Pybricks Code or it will try to pair with the PC. We do this just before // starting to advertise again, which covers all our use cases. PBIO_OS_AWAIT_WHILE(state, write_xfer_size); - bond_auth_mode_last = (peri->config->options & PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR) ? + bond_auth_mode_last = (peri->config.options & PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR) ? GAPBOND_PAIRING_MODE_INITIATE : GAPBOND_PAIRING_MODE_NO_PAIRING; GAP_BondMgrSetParameter(GAPBOND_PAIRING_MODE, sizeof(bond_auth_mode_last), &bond_auth_mode_last); PBIO_OS_AWAIT_UNTIL(state, hci_command_status); @@ -545,7 +545,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s DEBUG_PRINT("Connected.\n"); - if (!(peri->config->options & PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR)) { + if (!(peri->config.options & PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR)) { // Pairing not required, so we are done here. return PBIO_SUCCESS; } @@ -1305,8 +1305,8 @@ static void handle_event(uint8_t *packet) { case ATT_EVENT_HANDLE_VALUE_NOTI: { // TODO: match callback to handle // uint8_t attr_handle = (data[7] << 8) | data[6]; - if (peri->config->notification_handler) { - peri->config->notification_handler(peri->user, &data[8], pdu_len - 2); + if (peri->config.notification_handler) { + peri->config.notification_handler(peri->user, &data[8], pdu_len - 2); } } break; diff --git a/lib/pbio/include/pbdrv/bluetooth.h b/lib/pbio/include/pbdrv/bluetooth.h index 1d5829b50..7b9bd0c84 100644 --- a/lib/pbio/include/pbdrv/bluetooth.h +++ b/lib/pbio/include/pbdrv/bluetooth.h @@ -160,7 +160,7 @@ struct _pbdrv_bluetooth_peripheral_t { /** The characteristic currently being discovered. */ pbdrv_bluetooth_peripheral_char_discovery_t char_disc; /** Scan and connect configuration. */ - pbdrv_bluetooth_peripheral_connect_config_t *config; + pbdrv_bluetooth_peripheral_connect_config_t config; /** Currently ongoing peripheral function. */ pbio_os_process_func_t func; /** Most recent result of calling above function from main process. */ diff --git a/pybricks/iodevices/pb_type_iodevices_lwp3device.c b/pybricks/iodevices/pb_type_iodevices_lwp3device.c index 8d460f463..705805d00 100644 --- a/pybricks/iodevices/pb_type_iodevices_lwp3device.c +++ b/pybricks/iodevices/pb_type_iodevices_lwp3device.c @@ -220,12 +220,6 @@ static pbdrv_bluetooth_ad_match_result_flags_t lwp3_advertisement_response_match return flags; } -static pbdrv_bluetooth_peripheral_connect_config_t scan_config = { - .match_adv = lwp3_advertisement_matches, - .match_adv_rsp = lwp3_advertisement_response_matches, - // other options are variable. -}; - static pbio_error_t pb_type_pupdevices_Remote_write_light_msg(mp_obj_t self_in, const pbio_color_hsv_t *hsv) { struct { @@ -298,11 +292,7 @@ static pbio_error_t pb_lwp3device_connect_thread(pbio_os_state_t *state, mp_obj_ PBIO_OS_ASYNC_BEGIN(state); - // Get available peripheral instance. - pb_assert(pbdrv_bluetooth_peripheral_get_available(&self->peripheral, self)); - - // Scan and connect with timeout. - pb_assert(pbdrv_bluetooth_peripheral_scan_and_connect(self->peripheral, &scan_config)); + // Scan and connect operation was already started. Just await it here. PBIO_OS_AWAIT(state, &unused, err = pbdrv_bluetooth_await_peripheral_command(&unused, self->peripheral)); if (err != PBIO_SUCCESS) { // Not successful, release peripheral. @@ -434,12 +424,18 @@ static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in, mp_obj_t name_in, mp_obj strncpy(self->name, name, sizeof(self->name)); } - scan_config.notification_handler = notification_handler; - scan_config.options = PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_NONE; - if (pair) { - scan_config.options |= PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR; - } - scan_config.timeout = timeout_in == mp_const_none ? 0 : pb_obj_get_positive_int(timeout_in) + 1; + // Get available peripheral instance. + pb_assert(pbdrv_bluetooth_peripheral_get_available(&self->peripheral, self)); + + // Initiate scan and connect with timeout. + pbdrv_bluetooth_peripheral_connect_config_t scan_config = { + .match_adv = lwp3_advertisement_matches, + .match_adv_rsp = lwp3_advertisement_response_matches, + .notification_handler = notification_handler, + .options = pair ? PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR : PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_NONE, + .timeout = timeout_in == mp_const_none ? 0 : pb_obj_get_positive_int(timeout_in) + 1, + }; + pb_assert(pbdrv_bluetooth_peripheral_scan_and_connect(self->peripheral, &scan_config)); pb_type_async_t config = { .iter_once = pb_lwp3device_connect_thread, @@ -448,7 +444,6 @@ static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in, mp_obj_t name_in, mp_obj return pb_type_async_wait_or_await(&config, &self->iter, true); } - mp_obj_t pb_type_remote_button_pressed(mp_obj_t self_in) { pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); diff --git a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c index 44b82c19e..59f8a7f8a 100644 --- a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c +++ b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c @@ -58,6 +58,10 @@ typedef struct _pb_type_xbox_obj_t { * The peripheral instance associated with this MicroPython object. */ pbdrv_bluetooth_peripheral_t *peripheral; + /** + * Whether to disconnect from host before connecting to controller. + **/ + bool disconnect_host; /** * Timer used to delay between connection attempts. */ @@ -266,13 +270,6 @@ static mp_obj_t pb_type_xbox_button_pressed(mp_obj_t self_in) { return mp_obj_new_set(count, items); } -static pbdrv_bluetooth_peripheral_connect_config_t scan_config = { - .match_adv = xbox_advertisement_matches, - .match_adv_rsp = xbox_advertisement_response_matches, - .notification_handler = handle_notification, - // Option flags are variable. -}; - static mp_obj_t pb_type_xbox_close(mp_obj_t self_in) { pb_type_xbox_obj_t *self = MP_OBJ_TO_PTR(self_in); // Disables notification handler from accessing allocated memory. @@ -299,6 +296,17 @@ static pbio_error_t xbox_connect_thread(pbio_os_state_t *state, mp_obj_t parent_ // of disconnecting from Pybricks Code if needed. retry: DEBUG_PRINT("Attempt to find XBOX controller and connect and pair.\n"); + pbdrv_bluetooth_peripheral_connect_config_t scan_config = { + .match_adv = xbox_advertisement_matches, + .match_adv_rsp = xbox_advertisement_response_matches, + .notification_handler = handle_notification, + .options = PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR, + .timeout = 0, + }; + if (self->disconnect_host) { + scan_config.options |= PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_DISCONNECT_HOST; + } + pb_assert(pbdrv_bluetooth_peripheral_scan_and_connect(self->peripheral, &scan_config)); PBIO_OS_AWAIT(state, &unused, err = pbdrv_bluetooth_await_peripheral_command(&unused, self->peripheral)); @@ -447,14 +455,11 @@ static mp_obj_t pb_type_xbox_make_new(const mp_obj_type_t *type, size_t n_args, memset(input, 0, sizeof(xbox_input_map_t)); input->x = input->y = input->z = input->rz = INT16_MAX; - // Xbox Controller requires pairing. - scan_config.options = PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR; - // By default, disconnect Technic Hub from host, as this is required for // most hosts. Stay connected only if the user explicitly requests it. #if PYBRICKS_HUB_TECHNICHUB - if (!mp_obj_is_true(stay_connected_in) && pbdrv_bluetooth_host_is_connected()) { - scan_config.options |= PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_DISCONNECT_HOST; + self->disconnect_host = !mp_obj_is_true(stay_connected_in); + if (self->disconnect_host) { mp_printf(&mp_plat_print, "The hub may disconnect from the computer for better connectivity with the controller.\n"); mp_hal_delay_ms(500); } From d94b7252c5d898561a51fcff5046380f1722eff9 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 2 Feb 2026 16:50:44 +0100 Subject: [PATCH 04/13] pbio/sys/hmi_env_mpy: Run multiple times. This allows testing behaviors such as remaning connected to peripherals between program runs. --- lib/pbio/platform/virtual_hub/pbsysconfig.h | 1 + lib/pbio/sys/hmi_env_mpy.c | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pbio/platform/virtual_hub/pbsysconfig.h b/lib/pbio/platform/virtual_hub/pbsysconfig.h index 7a24a7240..c70f8707b 100644 --- a/lib/pbio/platform/virtual_hub/pbsysconfig.h +++ b/lib/pbio/platform/virtual_hub/pbsysconfig.h @@ -14,6 +14,7 @@ #define PBSYS_CONFIG_HMI (1) #define PBSYS_CONFIG_HMI_STOP_BUTTON (1 << 5) // center #define PBSYS_CONFIG_HMI_ENV_MPY (1) +#define PBSYS_CONFIG_HMI_ENV_MPY_NUM_RUNS (1) #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) #define PBSYS_CONFIG_MAIN (1) diff --git a/lib/pbio/sys/hmi_env_mpy.c b/lib/pbio/sys/hmi_env_mpy.c index 142b4ce26..8c95b4548 100644 --- a/lib/pbio/sys/hmi_env_mpy.c +++ b/lib/pbio/sys/hmi_env_mpy.c @@ -34,16 +34,16 @@ void pbsys_hmi_deinit(void) { uint8_t pbsys_hmi_native_program_buf[PBDRV_CONFIG_BLOCK_DEVICE_RAM_SIZE]; uint32_t pbsys_hmi_native_program_size; +static uint32_t pbsys_hmi_native_program_count; + pbio_error_t pbsys_hmi_await_program_selection(void) { pbio_os_run_processes_and_wait_for_event(); - // With this HMI, we run a script once and then exit. - static bool ran_once = false; - if (ran_once) { + // With this HMI, we run a script several times and then exit. + if (pbsys_hmi_native_program_count++ >= PBSYS_CONFIG_HMI_ENV_MPY_NUM_RUNS) { return PBIO_ERROR_CANCELED; } - ran_once = true; // Start REPL if no program given. if (pbsys_hmi_native_program_size == 0) { From 74df9128ad9ea74dd576b86438a54ff10d41ae85 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 11:48:00 +0100 Subject: [PATCH 05/13] pbio/drv/bluetooth: Simplify advertising filters. Reduce the number of partially parsed arguments that we pass around. This makes everything simpler, reduces code size, and we can keep the address out of the filters. This way we can re-use them outside of the scan-and-connect procedure. --- lib/pbio/drv/bluetooth/bluetooth_btstack.c | 63 ++++++++++++------- .../drv/bluetooth/bluetooth_stm32_bluenrg.c | 24 +++---- .../drv/bluetooth/bluetooth_stm32_cc2640.c | 24 +++---- lib/pbio/include/pbdrv/bluetooth.h | 24 ++----- .../iodevices/pb_type_iodevices_lwp3device.c | 45 +++---------- .../pb_type_iodevices_xbox_controller.c | 40 +++--------- 6 files changed, 80 insertions(+), 140 deletions(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth_btstack.c b/lib/pbio/drv/bluetooth/bluetooth_btstack.c index 83f79ddfd..715c0a536 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_btstack.c +++ b/lib/pbio/drv/bluetooth/bluetooth_btstack.c @@ -669,7 +669,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s } pbdrv_bluetooth_peripheral_t *peri = context; - pbdrv_bluetooth_ad_match_result_flags_t flags; + bool advertising_matched; uint8_t btstack_error; // Operation can be explicitly cancelled or automatically on inactivity. @@ -696,14 +696,20 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s uint8_t event_type = gap_event_advertising_report_get_advertising_event_type(event_packet); const uint8_t *data = gap_event_advertising_report_get_data(event_packet); - bd_addr_t address; - gap_event_advertising_report_get_address(event_packet, address); + uint8_t data_len = gap_event_advertising_report_get_data_length(event_packet); // Match advertisement data against context-specific filter. - flags = peri->config.match_adv(peri->user, event_type, data, NULL, address, peri->bdaddr); + advertising_matched = false; + if (event_type <= PBDRV_BLUETOOTH_AD_TYPE_ADV_DIRECT_IND) { + advertising_matched = peri->config.match_adv(peri->user, data, data_len); + } - // Store the address to compare with scan response later. - if (flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) { + // On match, store the address to compare with scan response later. + bool saw_before = false; + if (advertising_matched) { + bd_addr_t address; + gap_event_advertising_report_get_address(event_packet, address); + saw_before = !memcmp(peri->bdaddr, address, sizeof(bd_addr_t)); memcpy(peri->bdaddr, address, sizeof(bd_addr_t)); peri->bdaddr_type = gap_event_advertising_report_get_address_type(event_packet); } @@ -711,7 +717,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // Wait condition: Advertisement matched and it isn't the same as before. // If it was the same and we're here, it means the scan response didn't match // so we shouldn't try it again. - (flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) && !(flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS); + advertising_matched && !saw_before; }))); if ((peri->config.timeout && pbio_os_timer_is_expired(&peri->timer)) || peri->cancel) { @@ -732,33 +738,42 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s uint8_t event_type = gap_event_advertising_report_get_advertising_event_type(event_packet); const uint8_t *data = gap_event_advertising_report_get_data(event_packet); - char *detected_name = (char *)&data[2]; + uint8_t data_len = gap_event_advertising_report_get_data_length(event_packet); + + // We are looking for a scan response from the same device as before. bd_addr_t address; gap_event_advertising_report_get_address(event_packet, address); + bool is_valid_response = event_type == PBDRV_BLUETOOTH_AD_TYPE_SCAN_RSP && !memcmp(peri->bdaddr, address, sizeof(bd_addr_t)); - flags = peri->config.match_adv_rsp(peri->user, event_type, NULL, detected_name, address, peri->bdaddr); + advertising_matched = false; + if (is_valid_response) { + advertising_matched = peri->config.match_adv_rsp(peri->user, data, data_len); + } - (flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) && (flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS); - }))); + // If we're going to successfully proceed below, make a copy of the + // discovered device name while we're here. + if (advertising_matched && data[1] == BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME) { + memcpy(peri->name, &data[2], sizeof(peri->name)); + } - if (pbio_os_timer_is_expired(&peri->timer) || peri->cancel) { - DEBUG_PRINT("Scan response %s.\n", peri->cancel ? "canceled": "timed out"); - gap_stop_scan(); - return peri->cancel ? PBIO_ERROR_CANCELED : PBIO_ERROR_TIMEDOUT; - } + // We want to exit this state on a valid response, even if the filter + // did not match so we can go back to scanning. + is_valid_response; + }))); - if (flags & PBDRV_BLUETOOTH_AD_MATCH_NAME_FAILED) { + if (!advertising_matched) { + if (pbio_os_timer_is_expired(&peri->timer) || peri->cancel) { + DEBUG_PRINT("Scan response %s.\n", peri->cancel ? "canceled": "timed out"); + gap_stop_scan(); + return peri->cancel ? PBIO_ERROR_CANCELED : PBIO_ERROR_TIMEDOUT; + } + // If we get here, we got a valid scan response from the device that + // matched our advertising filter, but it did not match the response + // filter (e.g. requested name did not match), so scan again. DEBUG_PRINT("Name requested but did not match. Scan again.\n"); goto start_scan; } - // When we get here, we have just matched a scan response and we are still - // handling the same event packet, so we can still extract the name. - const uint8_t *data = gap_event_advertising_report_get_data(event_packet); - if (data[1] == BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME) { - memcpy(peri->name, &data[2], sizeof(peri->name)); - } - DEBUG_PRINT("Scan response matched, initiate connection to %s.\n", bd_addr_to_str(peri->bdaddr)); // We can stop scanning now. diff --git a/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c b/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c index 898d73050..811819a07 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c +++ b/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c @@ -366,25 +366,21 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s le_advertising_info *subevt = (void *)&read_buf[5]; - // Context specific advertisement filter. - pbdrv_bluetooth_ad_match_result_flags_t adv_flags = peri->config.match_adv(peri->user, subevt->evt_type, subevt->data_RSSI, NULL, subevt->bdaddr, peri->bdaddr); - - // If it doesn't match context-specific filter, keep scanning. - if (!(adv_flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE)) { + // If advertisement doesn't match context-specific filter, keep scanning. + if (subevt->evt_type > PBDRV_BLUETOOTH_AD_TYPE_ADV_DIRECT_IND || !peri->config.match_adv(peri->user, subevt->data_RSSI, subevt->data_length)) { continue; } // If the value matched but it's the same device as last time, we're // here because the scan response failed the last time. It probably // won't match now and we should try a different device. - if (adv_flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS) { + if (!memcmp(peri->bdaddr, subevt->bdaddr, sizeof(peri->bdaddr))) { goto try_again; } // save the Bluetooth address for later peri->bdaddr_type = subevt->bdaddr_type; - memcpy(peri->bdaddr, subevt->bdaddr, 6); - + memcpy(peri->bdaddr, subevt->bdaddr, sizeof(peri->bdaddr)); break; } @@ -401,20 +397,20 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s return peri->cancel ? PBIO_ERROR_CANCELED : PBIO_ERROR_TIMEDOUT; } - // If the response data is not right or if the address doesn't match advertisement, keep scanning. le_advertising_info *subevt = (void *)&read_buf[5]; - const char *detected_name = (char *)&subevt->data_RSSI[2]; - pbdrv_bluetooth_ad_match_result_flags_t rsp_flags = peri->config.match_adv_rsp(peri->user, subevt->evt_type, NULL, detected_name, subevt->bdaddr, peri->bdaddr); - if (!(rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) || !(rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS)) { + + // We are looking for a scan response from the same device as before, else keep scanning for responses. + if (subevt->evt_type != PBDRV_BLUETOOTH_AD_TYPE_SCAN_RSP || memcmp(peri->bdaddr, subevt->bdaddr, sizeof(peri->bdaddr))) { continue; } // If the device checks passed but the name doesn't match, start over. - if (rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_NAME_FAILED) { + if (!peri->config.match_adv_rsp(peri->user, subevt->data_RSSI, subevt->data_length)) { goto try_again; } - memcpy(peri->name, detected_name, sizeof(peri->name)); + // All checks passed, so copy the device name for later use. + memcpy(peri->name, &subevt->data_RSSI[2], sizeof(peri->name)); break; } diff --git a/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c b/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c index 37b6c7866..1a77a1ef1 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c +++ b/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c @@ -443,24 +443,21 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s } } - // Context specific advertisement filter. - pbdrv_bluetooth_ad_match_result_flags_t adv_flags = peri->config.match_adv(peri->user, read_buf[9], &read_buf[19], NULL, &read_buf[11], peri->bdaddr); - - // If it doesn't match context-specific filter, keep scanning. - if (!(adv_flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE)) { + // If advertisement doesn't match context-specific filter, keep scanning. + if (read_buf[9] > PBDRV_BLUETOOTH_AD_TYPE_ADV_DIRECT_IND || !peri->config.match_adv(peri->user, &read_buf[19], read_buf[18])) { continue; } // If the value matched but it's the same device as last time, we're // here because the scan response failed the last time. It probably // won't match now and we should try a different device. - if (adv_flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS) { + if (!memcmp(peri->bdaddr, &read_buf[11], sizeof(peri->bdaddr))) { goto try_again; } // Save the Bluetooth address for later comparison against response. peri->bdaddr_type = read_buf[10]; - memcpy(peri->bdaddr, &read_buf[11], 6); + memcpy(peri->bdaddr, &read_buf[11], sizeof(peri->bdaddr)); break; } @@ -489,21 +486,18 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s } } - const char *detected_name = (const char *)&read_buf[21]; - const uint8_t *response_address = &read_buf[11]; - pbdrv_bluetooth_ad_match_result_flags_t rsp_flags = peri->config.match_adv_rsp(peri->user, read_buf[9], NULL, detected_name, response_address, peri->bdaddr); - - // If the response data is not right or if the address doesn't match advertisement, keep scanning. - if (!(rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_VALUE) || !(rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS)) { + // We are looking for a scan response from the same device as before, else keep scanning for responses. + if (read_buf[9] != PBDRV_BLUETOOTH_AD_TYPE_SCAN_RSP || memcmp(peri->bdaddr, &read_buf[11], sizeof(peri->bdaddr))) { continue; } // If the device checks passed but the name doesn't match, start over. - if (rsp_flags & PBDRV_BLUETOOTH_AD_MATCH_NAME_FAILED) { + if (!peri->config.match_adv_rsp(peri->user, &read_buf[19], read_buf[18])) { goto try_again; } - memcpy(peri->name, detected_name, sizeof(peri->name)); + // All checks passed, so copy the device name for later use. + memcpy(peri->name, &read_buf[21], sizeof(peri->name)); break; } diff --git a/lib/pbio/include/pbdrv/bluetooth.h b/lib/pbio/include/pbdrv/bluetooth.h index 7b9bd0c84..62bea2646 100644 --- a/lib/pbio/include/pbdrv/bluetooth.h +++ b/lib/pbio/include/pbdrv/bluetooth.h @@ -38,31 +38,15 @@ typedef void (*pbdrv_bluetooth_send_done_t)(void); */ typedef pbio_pybricks_error_t (*pbdrv_bluetooth_receive_handler_t)(const uint8_t *data, uint32_t size); -/** Advertisement of scan response match result */ -typedef enum { - /** No match. */ - PBDRV_BLUETOOTH_AD_MATCH_NONE = 0, - /** Matched the expected value such as device type or manufacturer data. */ - PBDRV_BLUETOOTH_AD_MATCH_VALUE = 1 << 0, - /** Failed to matched the expected Bluetooth address.*/ - PBDRV_BLUETOOTH_AD_MATCH_ADDRESS = 1 << 1, - /** A name filter was given and it did NOT match. */ - PBDRV_BLUETOOTH_AD_MATCH_NAME_FAILED = 1 << 2, -} pbdrv_bluetooth_ad_match_result_flags_t; - /** * Callback to match an advertisement or scan response. * * @param [in] user The user of this peripheral, usually a high-level object. - * @param [in] event_type The type of advertisement. * @param [in] data The advertisement data. - * @param [in] name The name to match. If NULL, no name filter is applied. - * @param [in] addr The currently detected address if known, else NULL. - * @param [in] match_addr The address to match. If NULL, no address filter is applied. + * @param [in] length The advertisement data size. * @return True if the advertisement matches, false otherwise. */ -typedef pbdrv_bluetooth_ad_match_result_flags_t (*pbdrv_bluetooth_ad_match_t) - (void *user, uint8_t event_type, const uint8_t *data, const char *name, const uint8_t *addr, const uint8_t *match_addr); +typedef bool (*pbdrv_bluetooth_advertising_callback_t)(void *user, const uint8_t *data, uint8_t length); struct _pbdrv_bluetooth_send_context_t { /** Callback that is called when the data has been sent. */ @@ -130,9 +114,9 @@ typedef void (*pbdrv_bluetooth_peripheral_notification_handler_t)(void *user, co /** Peripheral scan and connection configuration */ typedef struct { /** Matcher for advertisement */ - pbdrv_bluetooth_ad_match_t match_adv; + pbdrv_bluetooth_advertising_callback_t match_adv; /** Matcher for scan response */ - pbdrv_bluetooth_ad_match_t match_adv_rsp; + pbdrv_bluetooth_advertising_callback_t match_adv_rsp; /** Handler for received notifications after connecting */ pbdrv_bluetooth_peripheral_notification_handler_t notification_handler; /** Option flags governing connection and pairing */ diff --git a/pybricks/iodevices/pb_type_iodevices_lwp3device.c b/pybricks/iodevices/pb_type_iodevices_lwp3device.c index 705805d00..f71a8b44a 100644 --- a/pybricks/iodevices/pb_type_iodevices_lwp3device.c +++ b/pybricks/iodevices/pb_type_iodevices_lwp3device.c @@ -166,58 +166,31 @@ static void handle_remote_notification(void *user, const uint8_t *value, uint32_ } } -static pbdrv_bluetooth_ad_match_result_flags_t lwp3_advertisement_matches(void *user, uint8_t event_type, const uint8_t *data, const char *name, const uint8_t *addr, const uint8_t *match_addr) { - - pbdrv_bluetooth_ad_match_result_flags_t flags = PBDRV_BLUETOOTH_AD_MATCH_NONE; +static bool lwp3_advertisement_matches(void *user, const uint8_t *data, uint8_t length) { pb_lwp3device_obj_t *self = user; if (!self) { - return flags; + return false; } // Whether this looks like a LWP3 advertisement of the correct hub kind. - if (event_type == PBDRV_BLUETOOTH_AD_TYPE_ADV_IND - && data[3] == 17 /* length */ + return + data[3] == 17 /* length */ && (data[4] == PBDRV_BLUETOOTH_AD_DATA_TYPE_128_BIT_SERV_UUID_COMPLETE_LIST || data[4] == PBDRV_BLUETOOTH_AD_DATA_TYPE_128_BIT_SERV_UUID_INCOMPLETE_LIST) && pbio_uuid128_reverse_compare(&data[5], lwp3_hub_service_uuid) - && data[26] == self->hub_kind) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_VALUE; - } - - // Compare address in advertisement to previously scanned address. - if (memcmp(addr, match_addr, 6) == 0) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_ADDRESS; - } - return flags; + && data[26] == self->hub_kind; } -static pbdrv_bluetooth_ad_match_result_flags_t lwp3_advertisement_response_matches(void *user, uint8_t event_type, const uint8_t *data, const char *name, const uint8_t *addr, const uint8_t *match_addr) { - - pbdrv_bluetooth_ad_match_result_flags_t flags = PBDRV_BLUETOOTH_AD_MATCH_NONE; +static bool lwp3_advertisement_response_matches(void *user, const uint8_t *data, uint8_t length) { pb_lwp3device_obj_t *self = user; if (!self) { - return flags; - } - - // This is the only value check we do on LWP3 response messages. - if (event_type == PBDRV_BLUETOOTH_AD_TYPE_SCAN_RSP) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_VALUE; - } - - // Compare address in response to previously scanned address. - if (memcmp(addr, match_addr, 6) == 0) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_ADDRESS; - } - - // Compare name to user-provided name if given, checking only up to the - // user provided name length. - if (self->name[0] != '\0' && strncmp(name, self->name, strlen(self->name)) != 0) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_NAME_FAILED; + return false; } - return flags; + // Pass if no name filter specified or the given name matches, checking only up to provided name length. + return self->name[0] == '\0' || strncmp((const char *)&data[2], self->name, strlen(self->name)) == 0; } static pbio_error_t pb_type_pupdevices_Remote_write_light_msg(mp_obj_t self_in, const pbio_color_hsv_t *hsv) { diff --git a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c index 59f8a7f8a..53e0a84cb 100644 --- a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c +++ b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c @@ -107,7 +107,7 @@ static void handle_notification(void *user, const uint8_t *value, uint32_t size) #define _16BIT_AS_LE(x) ((x) & 0xff), (((x) >> 8) & 0xff) -static pbdrv_bluetooth_ad_match_result_flags_t xbox_advertisement_matches(void *user, uint8_t event_type, const uint8_t *data, const char *name, const uint8_t *addr, const uint8_t *match_addr) { +static bool xbox_advertisement_matches(void *user, const uint8_t *data, uint8_t length) { // The controller seems to advertise three different packets, so allow all. @@ -141,38 +141,16 @@ static pbdrv_bluetooth_ad_match_result_flags_t xbox_advertisement_matches(void * memcpy(advertising_data3, advertising_data2, sizeof(advertising_data2)); advertising_data3[2] = 0x04; - // Exit if neither of the expected values match. - if (memcmp(data, advertising_data1, sizeof(advertising_data1)) && - memcmp(data, advertising_data2, sizeof(advertising_data2)) && - memcmp(data, advertising_data3, sizeof(advertising_data3))) { - return PBDRV_BLUETOOTH_AD_MATCH_NONE; - } - - // Expected value matches at this point. - pbdrv_bluetooth_ad_match_result_flags_t flags = PBDRV_BLUETOOTH_AD_MATCH_VALUE; - - // Compare address in advertisement to previously scanned address. - if (memcmp(addr, match_addr, 6) == 0) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_ADDRESS; - } - return flags; + // At least one must match. + return + !memcmp(data, advertising_data1, sizeof(advertising_data1)) || + !memcmp(data, advertising_data2, sizeof(advertising_data2)) || + !memcmp(data, advertising_data3, sizeof(advertising_data3)); } -static pbdrv_bluetooth_ad_match_result_flags_t xbox_advertisement_response_matches(void *user, uint8_t event_type, const uint8_t *data, const char *name, const uint8_t *addr, const uint8_t *match_addr) { - - pbdrv_bluetooth_ad_match_result_flags_t flags = PBDRV_BLUETOOTH_AD_MATCH_NONE; - - // This is currently the only requirement. - if (event_type == PBDRV_BLUETOOTH_AD_TYPE_SCAN_RSP) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_VALUE; - } - - // Compare address in response to previously scanned address. - if (memcmp(addr, match_addr, 6) == 0) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_ADDRESS; - } - - return flags; +static bool xbox_advertisement_response_matches(void *user, const uint8_t *data, uint8_t length) { + // No further filtering applied. + return true; } static xbox_input_map_t *pb_type_xbox_get_input(mp_obj_t self_in) { From ebbd56911192b4f233593bb9ddd040af7aefd94d Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 14:02:52 +0100 Subject: [PATCH 06/13] pybricks.iodevices.LWP3Device: Refactor connection init. Parse arguments only during init and reset the appropriate state only on reconnect. This is working towards a real and virtual reconnect. --- .../iodevices/pb_type_iodevices_lwp3device.c | 123 +++++++++--------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/pybricks/iodevices/pb_type_iodevices_lwp3device.c b/pybricks/iodevices/pb_type_iodevices_lwp3device.c index f71a8b44a..a06bf5f59 100644 --- a/pybricks/iodevices/pb_type_iodevices_lwp3device.c +++ b/pybricks/iodevices/pb_type_iodevices_lwp3device.c @@ -145,14 +145,7 @@ typedef struct { #endif // PYBRICKS_PY_IODEVICES } pb_lwp3device_obj_t; -// Handles LEGO Wireless protocol messages from the Powered Up Remote. -static void handle_remote_notification(void *user, const uint8_t *value, uint32_t size) { - - pb_lwp3device_obj_t *self = user; - if (!self) { - return; - } - +static void handle_remote_notification(pb_lwp3device_obj_t *self, const uint8_t *value) { if (value[0] == 5 && value[2] == LWP3_MSG_TYPE_HW_NET_CMDS && value[3] == LWP3_HW_NET_CMD_CONNECTION_REQ) { // This message is meant for something else, but contains the center button state self->center = value[4]; @@ -166,6 +159,41 @@ static void handle_remote_notification(void *user, const uint8_t *value, uint32_ } } +// Handles LEGO Wireless protocol messages. +static void pb_lwp3device_handle_notification(void *user, const uint8_t *value, uint32_t size) { + + pb_lwp3device_obj_t *self = user; + if (!self) { + return; + } + + // Remote has a dedicated handler. + if (mp_obj_get_type(MP_OBJ_FROM_PTR(user)) == &pb_type_pupdevices_Remote) { + handle_remote_notification(self, value); + return; + } + + #if PYBRICKS_PY_IODEVICES + if (!self->noti_num) { + // Allocated data not ready. + return; + } + + // Buffer is full, so drop oldest sample by advancing read index. + if (self->noti_data_full) { + self->noti_idx_read = (self->noti_idx_read + 1) % self->noti_num; + } + + memcpy(&self->notification_buffer[self->noti_idx_write * LWP3_MAX_MESSAGE_SIZE], &value[0], (size < LWP3_MAX_MESSAGE_SIZE) ? size : LWP3_MAX_MESSAGE_SIZE); + self->noti_idx_write = (self->noti_idx_write + 1) % self->noti_num; + + // After writing it is full if the _next_ write will override the + // to-be-read data. If it was already full when we started writing, both + // indexes have now advanced so it is still full now. + self->noti_data_full = self->noti_idx_read == self->noti_idx_write; + #endif +} + static bool lwp3_advertisement_matches(void *user, const uint8_t *data, uint8_t length) { pb_lwp3device_obj_t *self = user; @@ -371,18 +399,7 @@ static pbio_error_t pb_lwp3device_connect_thread(pbio_os_state_t *state, mp_obj_ PBIO_OS_ASYNC_END(PBIO_ERROR_IO); } -static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in, mp_obj_t name_in, mp_obj_t timeout_in, lwp3_hub_kind_t hub_kind, pbdrv_bluetooth_peripheral_notification_handler_t notification_handler, bool pair) { - - pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); - - self->iter = NULL; - - // needed to ensure that no buttons are "pressed" after reconnecting since - // we are using static memory - memset(&self->left, 0, sizeof(self->left)); - memset(&self->right, 0, sizeof(self->right)); - self->center = 0; - +static void pb_lwp3device_filter_hub_and_type(pb_lwp3device_obj_t *self, mp_obj_t name_in, lwp3_hub_kind_t hub_kind) { // Hub kind and name are set to filter advertisements and responses. self->hub_kind = hub_kind; if (name_in == mp_const_none) { @@ -396,6 +413,23 @@ static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in, mp_obj_t name_in, mp_obj } strncpy(self->name, name, sizeof(self->name)); } +} + +static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in, mp_obj_t timeout_in, bool pair) { + + pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); + + // Needed to ensure that no buttons are "pressed" after reconnecting + memset(&self->left, 0, sizeof(self->left)); + memset(&self->right, 0, sizeof(self->right)); + self->center = 0; + + #if PYBRICKS_PY_IODEVICES + memset(self->notification_buffer, 0, LWP3_MAX_MESSAGE_SIZE * self->noti_num); + self->noti_idx_read = 0; + self->noti_idx_write = 0; + self->noti_data_full = false; + #endif // Get available peripheral instance. pb_assert(pbdrv_bluetooth_peripheral_get_available(&self->peripheral, self)); @@ -404,7 +438,7 @@ static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in, mp_obj_t name_in, mp_obj pbdrv_bluetooth_peripheral_connect_config_t scan_config = { .match_adv = lwp3_advertisement_matches, .match_adv_rsp = lwp3_advertisement_response_matches, - .notification_handler = notification_handler, + .notification_handler = pb_lwp3device_handle_notification, .options = pair ? PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR : PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_NONE, .timeout = timeout_in == mp_const_none ? 0 : pb_obj_get_positive_int(timeout_in) + 1, }; @@ -465,14 +499,16 @@ static mp_obj_t pb_type_pupdevices_Remote_make_new(const mp_obj_type_t *type, si pb_module_tools_assert_blocking(); pb_lwp3device_obj_t *self = mp_obj_malloc_with_finaliser(pb_lwp3device_obj_t, type); - - self->buttons = pb_type_Keypad_obj_new(MP_OBJ_FROM_PTR(self), pb_type_remote_button_pressed); - self->light = pb_type_ColorLight_external_obj_new(MP_OBJ_FROM_PTR(self), pb_type_pupdevices_Remote_light_on); + self->iter = NULL; #if PYBRICKS_PY_IODEVICES self->noti_num = 0; #endif - pb_lwp3device_connect(MP_OBJ_FROM_PTR(self), name_in, timeout_in, LWP3_HUB_KIND_HANDSET, handle_remote_notification, false); + pb_lwp3device_filter_hub_and_type(self, name_in, LWP3_HUB_KIND_HANDSET); + self->buttons = pb_type_Keypad_obj_new(MP_OBJ_FROM_PTR(self), pb_type_remote_button_pressed); + self->light = pb_type_ColorLight_external_obj_new(MP_OBJ_FROM_PTR(self), pb_type_pupdevices_Remote_light_on); + + pb_lwp3device_connect(MP_OBJ_FROM_PTR(self), timeout_in, false); return MP_OBJ_FROM_PTR(self); } @@ -558,33 +594,6 @@ MP_DEFINE_CONST_OBJ_TYPE(pb_type_pupdevices_Remote, #if PYBRICKS_PY_IODEVICES_LWP3_DEVICE -/** - * Handles LEGO Wireless protocol messages from generic LWP3 devices. - */ -static void handle_lwp3_generic_notification(void *user, const uint8_t *value, uint32_t size) { - - pb_lwp3device_obj_t *self = user; - - if (!self || !self->noti_num) { - // Allocated data not ready. - return; - } - - // Buffer is full, so drop oldest sample by advancing read index. - if (self->noti_data_full) { - self->noti_idx_read = (self->noti_idx_read + 1) % self->noti_num; - } - - memcpy(&self->notification_buffer[self->noti_idx_write * LWP3_MAX_MESSAGE_SIZE], &value[0], (size < LWP3_MAX_MESSAGE_SIZE) ? size : LWP3_MAX_MESSAGE_SIZE); - self->noti_idx_write = (self->noti_idx_write + 1) % self->noti_num; - - // After writing it is full if the _next_ write will override the - // to-be-read data. If it was already full when we started writing, both - // indexes have now advanced so it is still full now. - self->noti_data_full = self->noti_idx_read == self->noti_idx_write; - return; -} - static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { PB_PARSE_ARGS_CLASS(n_args, n_kw, args, PB_ARG_REQUIRED(hub_kind), @@ -593,7 +602,6 @@ static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, PB_ARG_DEFAULT_FALSE(pair), PB_ARG_DEFAULT_INT(num_notifications, 8)); - uint8_t hub_kind = pb_obj_get_positive_int(hub_kind_in); bool pair = mp_obj_is_true(pair_in); size_t noti_num = mp_obj_get_int(num_notifications_in); @@ -602,16 +610,13 @@ static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, } pb_lwp3device_obj_t *self = mp_obj_malloc_var_with_finaliser(pb_lwp3device_obj_t, uint8_t, LWP3_MAX_MESSAGE_SIZE * noti_num, type); - - memset(self->notification_buffer, 0, LWP3_MAX_MESSAGE_SIZE * noti_num); + self->iter = NULL; self->noti_num = noti_num; - self->noti_idx_read = 0; - self->noti_idx_write = 0; - self->noti_data_full = false; + pb_lwp3device_filter_hub_and_type(self, name_in, mp_obj_get_int(hub_kind_in)); pb_module_tools_assert_blocking(); - pb_lwp3device_connect(MP_OBJ_FROM_PTR(self), name_in, timeout_in, hub_kind, handle_lwp3_generic_notification, pair); + pb_lwp3device_connect(MP_OBJ_FROM_PTR(self), timeout_in, pair); return MP_OBJ_FROM_PTR(self); } From 3c99e1386b7bfc14839bf1c3edf2b5fec8068900 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 14:12:06 +0100 Subject: [PATCH 07/13] pybricks.iodevices.LWP3Device: Cache init arguments. We want to re-use these for every connect call. --- .../iodevices/pb_type_iodevices_lwp3device.c | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/pybricks/iodevices/pb_type_iodevices_lwp3device.c b/pybricks/iodevices/pb_type_iodevices_lwp3device.c index a06bf5f59..3b07a1638 100644 --- a/pybricks/iodevices/pb_type_iodevices_lwp3device.c +++ b/pybricks/iodevices/pb_type_iodevices_lwp3device.c @@ -121,6 +121,14 @@ typedef struct { // Also used as the name of the device when setting the name, since this // is not updated in the driver until the next time it connects. char name[LWP3_MAX_HUB_PROPERTY_NAME_SIZE + 1]; + /** + * The timeout used during scan and connect. + */ + uint32_t scan_timeout; + /** + * Whether to use pairing during scan and connect. + */ + bool scan_needs_pairing; #if PYBRICKS_PY_IODEVICES /** * Maximum number of stored notifications. @@ -399,7 +407,14 @@ static pbio_error_t pb_lwp3device_connect_thread(pbio_os_state_t *state, mp_obj_ PBIO_OS_ASYNC_END(PBIO_ERROR_IO); } -static void pb_lwp3device_filter_hub_and_type(pb_lwp3device_obj_t *self, mp_obj_t name_in, lwp3_hub_kind_t hub_kind) { +/** + * Caches the make_new arguments so they can be re-used for all connect() calls. + */ +static void pb_lwp3device_set_connection_args(pb_lwp3device_obj_t *self, mp_obj_t name_in, mp_obj_t timeout_in, lwp3_hub_kind_t hub_kind, bool pair) { + + self->scan_timeout = timeout_in == mp_const_none ? 0 : pb_obj_get_positive_int(timeout_in) + 1; + self->scan_needs_pairing = pair; + // Hub kind and name are set to filter advertisements and responses. self->hub_kind = hub_kind; if (name_in == mp_const_none) { @@ -415,7 +430,7 @@ static void pb_lwp3device_filter_hub_and_type(pb_lwp3device_obj_t *self, mp_obj_ } } -static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in, mp_obj_t timeout_in, bool pair) { +static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in) { pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); @@ -439,8 +454,8 @@ static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in, mp_obj_t timeout_in, bo .match_adv = lwp3_advertisement_matches, .match_adv_rsp = lwp3_advertisement_response_matches, .notification_handler = pb_lwp3device_handle_notification, - .options = pair ? PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR : PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_NONE, - .timeout = timeout_in == mp_const_none ? 0 : pb_obj_get_positive_int(timeout_in) + 1, + .options = self->scan_needs_pairing ? PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR : PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_NONE, + .timeout = self->scan_timeout, }; pb_assert(pbdrv_bluetooth_peripheral_scan_and_connect(self->peripheral, &scan_config)); @@ -504,11 +519,11 @@ static mp_obj_t pb_type_pupdevices_Remote_make_new(const mp_obj_type_t *type, si self->noti_num = 0; #endif - pb_lwp3device_filter_hub_and_type(self, name_in, LWP3_HUB_KIND_HANDSET); + pb_lwp3device_set_connection_args(self, name_in, timeout_in, LWP3_HUB_KIND_HANDSET, false); self->buttons = pb_type_Keypad_obj_new(MP_OBJ_FROM_PTR(self), pb_type_remote_button_pressed); self->light = pb_type_ColorLight_external_obj_new(MP_OBJ_FROM_PTR(self), pb_type_pupdevices_Remote_light_on); - pb_lwp3device_connect(MP_OBJ_FROM_PTR(self), timeout_in, false); + pb_lwp3device_connect(MP_OBJ_FROM_PTR(self)); return MP_OBJ_FROM_PTR(self); } @@ -602,8 +617,6 @@ static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, PB_ARG_DEFAULT_FALSE(pair), PB_ARG_DEFAULT_INT(num_notifications, 8)); - bool pair = mp_obj_is_true(pair_in); - size_t noti_num = mp_obj_get_int(num_notifications_in); if (!noti_num) { noti_num = 1; @@ -612,11 +625,11 @@ static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, pb_lwp3device_obj_t *self = mp_obj_malloc_var_with_finaliser(pb_lwp3device_obj_t, uint8_t, LWP3_MAX_MESSAGE_SIZE * noti_num, type); self->iter = NULL; self->noti_num = noti_num; - pb_lwp3device_filter_hub_and_type(self, name_in, mp_obj_get_int(hub_kind_in)); + pb_lwp3device_set_connection_args(self, name_in, timeout_in, mp_obj_get_int(hub_kind_in), mp_obj_is_true(pair_in)); pb_module_tools_assert_blocking(); - pb_lwp3device_connect(MP_OBJ_FROM_PTR(self), timeout_in, pair); + pb_lwp3device_connect(MP_OBJ_FROM_PTR(self)); return MP_OBJ_FROM_PTR(self); } From e67c56ce5db36593c4d04818d9aecf5ded205af9 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 14:25:14 +0100 Subject: [PATCH 08/13] pybricks.iodevices.LWP3Device: Make connect optional. Allow instantiating without connecting, letting the user (asynchronously) connect later. Fixes https://github.com/pybricks/support/issues/1800 --- CHANGELOG.md | 3 +++ .../iodevices/pb_type_iodevices_lwp3device.c | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a1bbd53..7df5f539f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ with `drive_base.turn(angle, absolute=True)` ([pybricks-micropython#458]). - Added support for coordinate traversals with `drive_base.move_by(dx, dy)` for practical navigation ([pybricks-micropython#458]). +- Added `connect=True` parameter to the `Remote` and `LWP3Device` classes, + along with a `connect()` method to optionally connect later ([support#1800]). ### Changed - Reset IMU heading to `0.0` at the start of a user program for consistent @@ -20,6 +22,7 @@ - Fix missing classes in `pybricks.iodevices` on SPIKE Prime (regression in 4.0.0b2) ([pybricks-micropython#456]). +[support#1800]: https://github.com/pybricks/support/issues/1800 [pybricks-micropython#454]: https://github.com/pybricks/pybricks-micropython/pull/454 [pybricks-micropython#456]: https://github.com/pybricks/pybricks-micropython/pull/456 [pybricks-micropython#458]: https://github.com/pybricks/pybricks-micropython/pull/458 diff --git a/pybricks/iodevices/pb_type_iodevices_lwp3device.c b/pybricks/iodevices/pb_type_iodevices_lwp3device.c index 3b07a1638..b47ebb83d 100644 --- a/pybricks/iodevices/pb_type_iodevices_lwp3device.c +++ b/pybricks/iodevices/pb_type_iodevices_lwp3device.c @@ -465,6 +465,7 @@ static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in) { }; return pb_type_async_wait_or_await(&config, &self->iter, true); } +static MP_DEFINE_CONST_FUN_OBJ_1(pb_lwp3device_connect_obj, pb_lwp3device_connect); mp_obj_t pb_type_remote_button_pressed(mp_obj_t self_in) { pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); @@ -509,7 +510,9 @@ mp_obj_t pb_type_remote_button_pressed(mp_obj_t self_in) { static mp_obj_t pb_type_pupdevices_Remote_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { PB_PARSE_ARGS_CLASS(n_args, n_kw, args, PB_ARG_DEFAULT_NONE(name), - PB_ARG_DEFAULT_INT(timeout, 10000)); + PB_ARG_DEFAULT_INT(timeout, 10000), + PB_ARG_DEFAULT_TRUE(connect) + ); pb_module_tools_assert_blocking(); @@ -523,7 +526,9 @@ static mp_obj_t pb_type_pupdevices_Remote_make_new(const mp_obj_type_t *type, si self->buttons = pb_type_Keypad_obj_new(MP_OBJ_FROM_PTR(self), pb_type_remote_button_pressed); self->light = pb_type_ColorLight_external_obj_new(MP_OBJ_FROM_PTR(self), pb_type_pupdevices_Remote_light_on); - pb_lwp3device_connect(MP_OBJ_FROM_PTR(self)); + if (mp_obj_is_true(connect_in)) { + pb_lwp3device_connect(MP_OBJ_FROM_PTR(self)); + } return MP_OBJ_FROM_PTR(self); } @@ -594,6 +599,7 @@ static const pb_attr_dict_entry_t pb_type_pupdevices_Remote_attr_dict[] = { static const mp_rom_map_elem_t pb_type_pupdevices_Remote_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&pb_lwp3device_close_obj) }, + { MP_ROM_QSTR(MP_QSTR_connect), MP_ROM_PTR(&pb_lwp3device_connect_obj) }, { MP_ROM_QSTR(MP_QSTR_disconnect), MP_ROM_PTR(&pb_lwp3device_disconnect_obj) }, { MP_ROM_QSTR(MP_QSTR_name), MP_ROM_PTR(&pb_lwp3device_name_obj) }, }; @@ -615,7 +621,9 @@ static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, PB_ARG_DEFAULT_NONE(name), PB_ARG_DEFAULT_INT(timeout, 10000), PB_ARG_DEFAULT_FALSE(pair), - PB_ARG_DEFAULT_INT(num_notifications, 8)); + PB_ARG_DEFAULT_INT(num_notifications, 8), + PB_ARG_DEFAULT_TRUE(connect) + ); size_t noti_num = mp_obj_get_int(num_notifications_in); if (!noti_num) { @@ -629,7 +637,9 @@ static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, pb_module_tools_assert_blocking(); - pb_lwp3device_connect(MP_OBJ_FROM_PTR(self)); + if (mp_obj_is_true(connect_in)) { + pb_lwp3device_connect(MP_OBJ_FROM_PTR(self)); + } return MP_OBJ_FROM_PTR(self); } @@ -691,6 +701,7 @@ static MP_DEFINE_CONST_FUN_OBJ_1(lwp3device_read_obj, lwp3device_read); static const mp_rom_map_elem_t pb_type_iodevices_LWP3Device_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&pb_lwp3device_close_obj) }, + { MP_ROM_QSTR(MP_QSTR_connect), MP_ROM_PTR(&pb_lwp3device_connect_obj) }, { MP_ROM_QSTR(MP_QSTR_disconnect), MP_ROM_PTR(&pb_lwp3device_disconnect_obj) }, { MP_ROM_QSTR(MP_QSTR_name), MP_ROM_PTR(&pb_lwp3device_name_obj) }, { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&lwp3device_write_obj) }, From 74295e20dab29ab5463f915bac9d60b209b6c80b Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 15:28:48 +0100 Subject: [PATCH 09/13] pbio/drv/bluetooth_btstack: Fix resetting timeout after scan. If the timeout was long, resetting it in between the scan and scan response makes it shorter, which is not intended. Keep it simple by extending only for connect and pairing. Also refactor the waiting loops to make them easier to follow with a scan timeout shorthand. --- lib/pbio/drv/bluetooth/bluetooth_btstack.c | 73 +++++++++------------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth_btstack.c b/lib/pbio/drv/bluetooth/bluetooth_btstack.c index 715c0a536..13da6c288 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_btstack.c +++ b/lib/pbio/drv/bluetooth/bluetooth_btstack.c @@ -43,7 +43,6 @@ #endif // Timeouts for various steps in the scan and connect process. -#define PERIPHERAL_TIMEOUT_MS_SCAN_RESPONSE (2000) #define PERIPHERAL_TIMEOUT_MS_CONNECT (5000) #define PERIPHERAL_TIMEOUT_MS_PAIRING (5000) @@ -669,13 +668,13 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s } pbdrv_bluetooth_peripheral_t *peri = context; - bool advertising_matched; uint8_t btstack_error; // Operation can be explicitly cancelled or automatically on inactivity. if (!peri->cancel) { peri->cancel = pbio_os_timer_is_expired(&peri->watchdog); } + bool scan_timed_out = peri->config.timeout && pbio_os_timer_is_expired(&peri->timer); PBIO_OS_ASYNC_BEGIN(state); @@ -691,18 +690,17 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s start_scan: // Wait for advertisement that matches the filter unless timed out or cancelled. - PBIO_OS_AWAIT_UNTIL(state, (peri->config.timeout && pbio_os_timer_is_expired(&peri->timer)) || - peri->cancel || (hci_event_is_type(event_packet, GAP_EVENT_ADVERTISING_REPORT) && ({ + PBIO_OS_AWAIT_UNTIL(state, scan_timed_out || peri->cancel || + (hci_event_is_type(event_packet, GAP_EVENT_ADVERTISING_REPORT) && ({ uint8_t event_type = gap_event_advertising_report_get_advertising_event_type(event_packet); const uint8_t *data = gap_event_advertising_report_get_data(event_packet); uint8_t data_len = gap_event_advertising_report_get_data_length(event_packet); // Match advertisement data against context-specific filter. - advertising_matched = false; - if (event_type <= PBDRV_BLUETOOTH_AD_TYPE_ADV_DIRECT_IND) { - advertising_matched = peri->config.match_adv(peri->user, data, data_len); - } + bool advertising_matched = + event_type <= PBDRV_BLUETOOTH_AD_TYPE_ADV_DIRECT_IND && + peri->config.match_adv(peri->user, data, data_len); // On match, store the address to compare with scan response later. bool saw_before = false; @@ -720,7 +718,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s advertising_matched && !saw_before; }))); - if ((peri->config.timeout && pbio_os_timer_is_expired(&peri->timer)) || peri->cancel) { + if (scan_timed_out || peri->cancel) { DEBUG_PRINT("Scan %s.\n", peri->cancel ? "canceled": "timed out"); gap_stop_scan(); return peri->cancel ? PBIO_ERROR_CANCELED : PBIO_ERROR_TIMEDOUT; @@ -728,48 +726,37 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s DEBUG_PRINT("Advertisement matched, waiting for scan response\n"); - // The user timeout applies only to finding the device. We still want to - // have a reasonable timeout for the scan response, connecting and pairing. - pbio_os_timer_set(&peri->timer, PERIPHERAL_TIMEOUT_MS_SCAN_RESPONSE); - - // Wait for advertising response that matches the filter unless timed out or cancelled. - PBIO_OS_AWAIT_UNTIL(state, pbio_os_timer_is_expired(&peri->timer) || peri->cancel || + // Wait for advertising response unless timed out or cancelled. + PBIO_OS_AWAIT_UNTIL(state, scan_timed_out || peri->cancel || (hci_event_is_type(event_packet, GAP_EVENT_ADVERTISING_REPORT) && ({ uint8_t event_type = gap_event_advertising_report_get_advertising_event_type(event_packet); - const uint8_t *data = gap_event_advertising_report_get_data(event_packet); - uint8_t data_len = gap_event_advertising_report_get_data_length(event_packet); - - // We are looking for a scan response from the same device as before. bd_addr_t address; gap_event_advertising_report_get_address(event_packet, address); - bool is_valid_response = event_type == PBDRV_BLUETOOTH_AD_TYPE_SCAN_RSP && !memcmp(peri->bdaddr, address, sizeof(bd_addr_t)); - - advertising_matched = false; - if (is_valid_response) { - advertising_matched = peri->config.match_adv_rsp(peri->user, data, data_len); - } - // If we're going to successfully proceed below, make a copy of the - // discovered device name while we're here. - if (advertising_matched && data[1] == BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME) { - memcpy(peri->name, &data[2], sizeof(peri->name)); - } - - // We want to exit this state on a valid response, even if the filter - // did not match so we can go back to scanning. - is_valid_response; + // Wait for the scan response from the previously matching device. + event_type == PBDRV_BLUETOOTH_AD_TYPE_SCAN_RSP && !memcmp(peri->bdaddr, address, sizeof(bd_addr_t)); }))); - if (!advertising_matched) { - if (pbio_os_timer_is_expired(&peri->timer) || peri->cancel) { - DEBUG_PRINT("Scan response %s.\n", peri->cancel ? "canceled": "timed out"); - gap_stop_scan(); - return peri->cancel ? PBIO_ERROR_CANCELED : PBIO_ERROR_TIMEDOUT; + if (scan_timed_out || peri->cancel) { + DEBUG_PRINT("Scan response %s.\n", peri->cancel ? "canceled": "timed out"); + gap_stop_scan(); + return peri->cancel ? PBIO_ERROR_CANCELED : PBIO_ERROR_TIMEDOUT; + } + + // If we got here, we just finished waiting for a response and we still have + // that event data for processing. + const uint8_t *data = gap_event_advertising_report_get_data(event_packet); + uint8_t data_len = gap_event_advertising_report_get_data_length(event_packet); + if (peri->config.match_adv_rsp(peri->user, data, data_len)) { + // Copy name for later use. + if (data[1] == BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME) { + memcpy(peri->name, &data[2], sizeof(peri->name)); } - // If we get here, we got a valid scan response from the device that - // matched our advertising filter, but it did not match the response - // filter (e.g. requested name did not match), so scan again. + } else { + // We got a valid scan response from the device that matched our + // advertising filter, but it did not match the response filter (e.g. + // requested name did not match), so scan again. DEBUG_PRINT("Name requested but did not match. Scan again.\n"); goto start_scan; } @@ -780,6 +767,8 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s gap_stop_scan(); // Initiate connection and await connection complete event. + // The user timeout applies only to finding the device. We still want to + // have a reasonable timeout for connecting and pairing. pbio_os_timer_set(&peri->timer, PERIPHERAL_TIMEOUT_MS_CONNECT); btstack_error = gap_connect(peri->bdaddr, peri->bdaddr_type); if (btstack_error != ERROR_CODE_SUCCESS) { From bcc02247bb794eaa1787523118daab42ec63dd35 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 15:44:13 +0100 Subject: [PATCH 10/13] pybricks.iodevices.XboxController: Add connect, name, and timeout. Makes it consistent with the LWP3Device. Fixes https://github.com/pybricks/support/issues/1800 --- CHANGELOG.md | 8 +++- .../pb_type_iodevices_xbox_controller.c | 42 ++++++++++++++++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df5f539f..6529f3a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,16 @@ with `drive_base.turn(angle, absolute=True)` ([pybricks-micropython#458]). - Added support for coordinate traversals with `drive_base.move_by(dx, dy)` for practical navigation ([pybricks-micropython#458]). -- Added `connect=True` parameter to the `Remote` and `LWP3Device` classes, - along with a `connect()` method to optionally connect later ([support#1800]). +- Added `connect=True` parameter to the `Remote`, `LWP3Device` + and `XboxController` classes, along with a `connect()` method to optionally + connect later ([support#1800]). +- Added `timeout` and `name` parameters to the `XboxController`. ### Changed - Reset IMU heading to `0.0` at the start of a user program for consistent drivebase behavior. +- Changed the default `XboxController` connection timeout from indefinite + to 10 seconds, consistent with the `Remote`. ### Fixed - Fix missing classes in `pybricks.iodevices` on SPIKE Prime (regression in diff --git a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c index 53e0a84cb..1d8a0f2b3 100644 --- a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c +++ b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c @@ -58,6 +58,14 @@ typedef struct _pb_type_xbox_obj_t { * The peripheral instance associated with this MicroPython object. */ pbdrv_bluetooth_peripheral_t *peripheral; + /** + * Null-terminated name used to filter advertisements and responses. + */ + char name[PBDRV_BLUETOOTH_MAX_ADV_SIZE - 2]; + /** + * The timeout used during scan and connect. + */ + uint32_t scan_timeout; /** * Whether to disconnect from host before connecting to controller. **/ @@ -149,8 +157,12 @@ static bool xbox_advertisement_matches(void *user, const uint8_t *data, uint8_t } static bool xbox_advertisement_response_matches(void *user, const uint8_t *data, uint8_t length) { - // No further filtering applied. - return true; + pb_type_xbox_obj_t *self = user; + if (!self) { + return false; + } + // Pass if no name filter specified or the given name matches, checking only up to provided name length. + return self->name[0] == '\0' || strncmp((const char *)&data[2], self->name, strlen(self->name)) == 0; } static xbox_input_map_t *pb_type_xbox_get_input(mp_obj_t self_in) { @@ -279,7 +291,7 @@ static pbio_error_t xbox_connect_thread(pbio_os_state_t *state, mp_obj_t parent_ .match_adv_rsp = xbox_advertisement_response_matches, .notification_handler = handle_notification, .options = PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR, - .timeout = 0, + .timeout = self->scan_timeout, }; if (self->disconnect_host) { scan_config.options |= PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_DISCONNECT_HOST; @@ -379,13 +391,13 @@ static pbio_error_t xbox_connect_thread(pbio_os_state_t *state, mp_obj_t parent_ static mp_obj_t pb_type_xbox_connect(mp_obj_t self_in) { pb_type_xbox_obj_t *self = MP_OBJ_TO_PTR(self_in); - pb_type_async_t config = { .iter_once = xbox_connect_thread, .parent_obj = MP_OBJ_FROM_PTR(self), }; return pb_type_async_wait_or_await(&config, &self->iter, true); } +static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_xbox_connect_obj, pb_type_xbox_connect); static mp_obj_t pb_type_xbox_await_operation(mp_obj_t self_in) { pb_type_xbox_obj_t *self = MP_OBJ_TO_PTR(self_in); @@ -411,7 +423,10 @@ static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_xbox_disconnect_obj, pb_type_xbox_disco static mp_obj_t pb_type_xbox_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { PB_PARSE_ARGS_CLASS(n_args, n_kw, args, - PB_ARG_DEFAULT_INT(joystick_deadzone, 10) + PB_ARG_DEFAULT_INT(joystick_deadzone, 10), + PB_ARG_DEFAULT_NONE(name), + PB_ARG_DEFAULT_INT(timeout, 10000), + PB_ARG_DEFAULT_TRUE(connect) // Debug parameter to stay connected to the host on Technic Hub. // Works only on some hosts for the moment, so False by default. #if PYBRICKS_HUB_TECHNICHUB @@ -433,6 +448,18 @@ static mp_obj_t pb_type_xbox_make_new(const mp_obj_type_t *type, size_t n_args, memset(input, 0, sizeof(xbox_input_map_t)); input->x = input->y = input->z = input->rz = INT16_MAX; + self->scan_timeout = timeout_in == mp_const_none ? 0 : pb_obj_get_positive_int(timeout_in) + 1; + if (name_in == mp_const_none) { + self->name[0] = '\0'; + } else { + const char *name = mp_obj_str_get_str(name_in); + size_t len = strlen(name); + if (len > sizeof(self->name) - 1) { + mp_raise_ValueError(MP_ERROR_TEXT("Name too long")); + } + strncpy(self->name, name, sizeof(self->name)); + } + // By default, disconnect Technic Hub from host, as this is required for // most hosts. Stay connected only if the user explicitly requests it. #if PYBRICKS_HUB_TECHNICHUB @@ -443,7 +470,9 @@ static mp_obj_t pb_type_xbox_make_new(const mp_obj_type_t *type, size_t n_args, } #endif // PYBRICKS_HUB_TECHNICHUB - pb_type_xbox_connect(MP_OBJ_FROM_PTR(self)); + if (mp_obj_is_true(connect_in)) { + pb_type_xbox_connect(MP_OBJ_FROM_PTR(self)); + } return MP_OBJ_FROM_PTR(self); } @@ -614,6 +643,7 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_xbox_rumble_obj, 1, pb_type_xbox_rumbl static const mp_rom_map_elem_t pb_type_xbox_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_name), MP_ROM_PTR(&pb_type_xbox_name_obj) }, { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&pb_type_xbox_close_obj) }, + { MP_ROM_QSTR(MP_QSTR_connect), MP_ROM_PTR(&pb_type_xbox_connect_obj) }, { MP_ROM_QSTR(MP_QSTR_disconnect), MP_ROM_PTR(&pb_type_xbox_disconnect_obj) }, { MP_ROM_QSTR(MP_QSTR_state), MP_ROM_PTR(&pb_type_xbox_state_obj) }, { MP_ROM_QSTR(MP_QSTR_dpad), MP_ROM_PTR(&pb_type_xbox_dpad_obj) }, From dda7bb8578ec449253a94af2f133910a76bb44b1 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 16:40:21 +0100 Subject: [PATCH 11/13] pbio/drv/bluetooth: Reuse matching connected peripheral. This lets you stay connected and use your regular code. If you do Remote(args) and a matching device is connected and not already in use, you get this device. --- CHANGELOG.md | 22 ++++--- lib/pbio/drv/bluetooth/bluetooth.c | 27 +++++++++ lib/pbio/include/pbdrv/bluetooth.h | 17 +++++- .../iodevices/pb_type_iodevices_lwp3device.c | 58 ++++++++++++------- .../pb_type_iodevices_xbox_controller.c | 30 +++++++--- 5 files changed, 119 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6529f3a9a..58a698cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ ## [Unreleased] +### Added +- Added `connect=True` parameter to the `Remote`, `LWP3Device` + and `XboxController` classes, along with a `connect()` method to optionally + connect later ([support#1800]). +- Added `timeout` and `name` parameters to the `XboxController`. + +### Changed +- Changed the default `XboxController` connection timeout from indefinite + to 10 seconds, consistent with the `Remote`. +- Devices like the `Remote`, `LWP3Device`, and the `XboxController` now stay + connected when the program ends ([support#1382]). + +[support#1382]: https://github.com/pybricks/support/issues/1382 +[support#1800]: https://github.com/pybricks/support/issues/1800 + ## [4.0.0b5] - 2026-01-30 ### Added @@ -11,22 +26,15 @@ with `drive_base.turn(angle, absolute=True)` ([pybricks-micropython#458]). - Added support for coordinate traversals with `drive_base.move_by(dx, dy)` for practical navigation ([pybricks-micropython#458]). -- Added `connect=True` parameter to the `Remote`, `LWP3Device` - and `XboxController` classes, along with a `connect()` method to optionally - connect later ([support#1800]). -- Added `timeout` and `name` parameters to the `XboxController`. ### Changed - Reset IMU heading to `0.0` at the start of a user program for consistent drivebase behavior. -- Changed the default `XboxController` connection timeout from indefinite - to 10 seconds, consistent with the `Remote`. ### Fixed - Fix missing classes in `pybricks.iodevices` on SPIKE Prime (regression in 4.0.0b2) ([pybricks-micropython#456]). -[support#1800]: https://github.com/pybricks/support/issues/1800 [pybricks-micropython#454]: https://github.com/pybricks/pybricks-micropython/pull/454 [pybricks-micropython#456]: https://github.com/pybricks/pybricks-micropython/pull/456 [pybricks-micropython#458]: https://github.com/pybricks/pybricks-micropython/pull/458 diff --git a/lib/pbio/drv/bluetooth/bluetooth.c b/lib/pbio/drv/bluetooth/bluetooth.c index 589aecd78..7d1c16e5b 100644 --- a/lib/pbio/drv/bluetooth/bluetooth.c +++ b/lib/pbio/drv/bluetooth/bluetooth.c @@ -179,6 +179,33 @@ pbio_error_t pbdrv_bluetooth_peripheral_get_available(pbdrv_bluetooth_peripheral return PBIO_ERROR_BUSY; } +pbio_error_t pbdrv_bluetooth_peripheral_get_connected(pbdrv_bluetooth_peripheral_t **peripheral, void *user, pbdrv_bluetooth_peripheral_connect_config_t *config) { + for (uint8_t i = 0; i < PBDRV_CONFIG_BLUETOOTH_NUM_PERIPHERALS; i++) { + pbdrv_bluetooth_peripheral_t *peri = pbdrv_bluetooth_peripheral_get_by_index(i); + + // Should be connected and not already in use. + if (!pbdrv_bluetooth_peripheral_is_connected(peri) || peri->user) { + continue; + } + + // Callbacks must be the same. + if (peri->config.match_adv != config->match_adv || + peri->config.match_adv_rsp != config->match_adv_rsp || + peri->config.notification_handler != config->notification_handler) { + continue; + } + + // Claim this device for new user. + peri->user = user; + *peripheral = peri; + return PBIO_SUCCESS; + } + + // No more connected devices available. + *peripheral = NULL; + return PBIO_ERROR_NO_DEV; +} + void pbdrv_bluetooth_peripheral_release(pbdrv_bluetooth_peripheral_t *peripheral, void *user) { // Only release if the user matches. A new user may have already safely // claimed it, and this call to release may come from an orphaned user. diff --git a/lib/pbio/include/pbdrv/bluetooth.h b/lib/pbio/include/pbdrv/bluetooth.h index 62bea2646..4900b88b2 100644 --- a/lib/pbio/include/pbdrv/bluetooth.h +++ b/lib/pbio/include/pbdrv/bluetooth.h @@ -372,7 +372,7 @@ pbio_error_t pbdrv_bluetooth_send_event_notification(pbio_os_state_t *state, pbi // /** - * Gets an available peripheral instance. + * Gets an available (free and unconnected) peripheral instance. * * @param [out] peripheral Pointer to the peripheral instance if found. * @param [in] user Optional user reference to associate with the peripheral. @@ -382,6 +382,17 @@ pbio_error_t pbdrv_bluetooth_send_event_notification(pbio_os_state_t *state, pbi */ pbio_error_t pbdrv_bluetooth_peripheral_get_available(pbdrv_bluetooth_peripheral_t **peripheral, void *user); +/** + * Gets an matching connected peripheral if available, + * + * @param [out] peripheral Pointer to the peripheral instance if found. + * @param [in] user Optional user reference to associate with the peripheral. + * @param [in] config Config as in scan and connect, used to match previously connected device. + * @return ::PBIO_SUCCESS if a peripheral instance is connected and available. + * ::PBIO_ERROR_NO_DEV if no matching peripheral instance connected or is available. + */ +pbio_error_t pbdrv_bluetooth_peripheral_get_connected(pbdrv_bluetooth_peripheral_t **peripheral, void *user, pbdrv_bluetooth_peripheral_connect_config_t *config); + /** * Checks if the given peripheral is connected. * @@ -647,6 +658,10 @@ static inline pbio_error_t pbdrv_bluetooth_peripheral_get_available(pbdrv_blueto return PBIO_ERROR_NOT_SUPPORTED; } +static inline pbio_error_t pbdrv_bluetooth_peripheral_get_connected(pbdrv_bluetooth_peripheral_t **peripheral, void *user, pbdrv_bluetooth_peripheral_connect_config_t *config) { + return PBIO_ERROR_NOT_SUPPORTED; +} + static inline bool pbdrv_bluetooth_peripheral_is_connected(pbdrv_bluetooth_peripheral_t *peripheral) { return false; } diff --git a/pybricks/iodevices/pb_type_iodevices_lwp3device.c b/pybricks/iodevices/pb_type_iodevices_lwp3device.c index b47ebb83d..4beff9651 100644 --- a/pybricks/iodevices/pb_type_iodevices_lwp3device.c +++ b/pybricks/iodevices/pb_type_iodevices_lwp3device.c @@ -467,6 +467,18 @@ static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in) { } static MP_DEFINE_CONST_FUN_OBJ_1(pb_lwp3device_connect_obj, pb_lwp3device_connect); +static mp_obj_t pb_lwp3device_disconnect(mp_obj_t self_in) { + // Needed to release claim on allocated data so we can make a new + // connection later. + pb_lwp3device_close(self_in); + + pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); + + pb_assert(pbdrv_bluetooth_peripheral_disconnect(self->peripheral)); + return wait_or_await_operation(self_in); +} +static MP_DEFINE_CONST_FUN_OBJ_1(pb_lwp3device_disconnect_obj, pb_lwp3device_disconnect); + mp_obj_t pb_type_remote_button_pressed(mp_obj_t self_in) { pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); @@ -506,6 +518,30 @@ mp_obj_t pb_type_remote_button_pressed(mp_obj_t self_in) { #endif } +static void pb_lwp3device_intialize_connection(mp_obj_t self_in, mp_obj_t connect_in) { + pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); + + bool want_connection = mp_obj_is_true(connect_in); + + // Attempt to re-use existing connection. + pbdrv_bluetooth_peripheral_connect_config_t scan_config = { + .match_adv = lwp3_advertisement_matches, + .match_adv_rsp = lwp3_advertisement_response_matches, + .notification_handler = pb_lwp3device_handle_notification, + }; + pbio_error_t err = pbdrv_bluetooth_peripheral_get_connected(&self->peripheral, self, &scan_config); + + // If we aren't already connected, do so now if requested. + if (err == PBIO_ERROR_NO_DEV && want_connection) { + pb_lwp3device_connect(self_in); + } + // If being connected now is not desired, disconnect. + else if (err == PBIO_SUCCESS && !want_connection) { + pb_lwp3device_disconnect(self_in); + } + // Other combinations are already in the desired state, so do nothing else. +} + static mp_obj_t pb_type_pupdevices_Remote_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { PB_PARSE_ARGS_CLASS(n_args, n_kw, args, @@ -526,10 +562,7 @@ static mp_obj_t pb_type_pupdevices_Remote_make_new(const mp_obj_type_t *type, si self->buttons = pb_type_Keypad_obj_new(MP_OBJ_FROM_PTR(self), pb_type_remote_button_pressed); self->light = pb_type_ColorLight_external_obj_new(MP_OBJ_FROM_PTR(self), pb_type_pupdevices_Remote_light_on); - if (mp_obj_is_true(connect_in)) { - pb_lwp3device_connect(MP_OBJ_FROM_PTR(self)); - } - + pb_lwp3device_intialize_connection(MP_OBJ_FROM_PTR(self), connect_in); return MP_OBJ_FROM_PTR(self); } @@ -579,18 +612,6 @@ static mp_obj_t pb_lwp3device_name(size_t n_args, const mp_obj_t *args) { } static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(pb_lwp3device_name_obj, 1, 2, pb_lwp3device_name); -static mp_obj_t pb_lwp3device_disconnect(mp_obj_t self_in) { - // Needed to release claim on allocated data so we can make a new - // connection later. - pb_lwp3device_close(self_in); - - pb_lwp3device_obj_t *self = MP_OBJ_TO_PTR(self_in); - - pb_assert(pbdrv_bluetooth_peripheral_disconnect(self->peripheral)); - return wait_or_await_operation(self_in); -} -static MP_DEFINE_CONST_FUN_OBJ_1(pb_lwp3device_disconnect_obj, pb_lwp3device_disconnect); - static const pb_attr_dict_entry_t pb_type_pupdevices_Remote_attr_dict[] = { PB_DEFINE_CONST_ATTR_RO(MP_QSTR_buttons, pb_lwp3device_obj_t, buttons), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_light, pb_lwp3device_obj_t, light), @@ -637,10 +658,7 @@ static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, pb_module_tools_assert_blocking(); - if (mp_obj_is_true(connect_in)) { - pb_lwp3device_connect(MP_OBJ_FROM_PTR(self)); - } - + pb_lwp3device_intialize_connection(MP_OBJ_FROM_PTR(self), connect_in); return MP_OBJ_FROM_PTR(self); } diff --git a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c index 1d8a0f2b3..bd9c4cd7b 100644 --- a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c +++ b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c @@ -101,7 +101,7 @@ typedef struct _pb_type_xbox_obj_t { } pb_type_xbox_obj_t; // Handles LEGO Wireless protocol messages from the XBOX Device. -static void handle_notification(void *user, const uint8_t *value, uint32_t size) { +static void pb_type_xbox_handle_notification(void *user, const uint8_t *value, uint32_t size) { pb_type_xbox_obj_t *self = user; if (!self) { @@ -115,7 +115,7 @@ static void handle_notification(void *user, const uint8_t *value, uint32_t size) #define _16BIT_AS_LE(x) ((x) & 0xff), (((x) >> 8) & 0xff) -static bool xbox_advertisement_matches(void *user, const uint8_t *data, uint8_t length) { +static bool pb_type_xbox_advertisement_matches(void *user, const uint8_t *data, uint8_t length) { // The controller seems to advertise three different packets, so allow all. @@ -156,7 +156,7 @@ static bool xbox_advertisement_matches(void *user, const uint8_t *data, uint8_t !memcmp(data, advertising_data3, sizeof(advertising_data3)); } -static bool xbox_advertisement_response_matches(void *user, const uint8_t *data, uint8_t length) { +static bool pb_type_xbox_advertisement_response_matches(void *user, const uint8_t *data, uint8_t length) { pb_type_xbox_obj_t *self = user; if (!self) { return false; @@ -287,9 +287,9 @@ static pbio_error_t xbox_connect_thread(pbio_os_state_t *state, mp_obj_t parent_ retry: DEBUG_PRINT("Attempt to find XBOX controller and connect and pair.\n"); pbdrv_bluetooth_peripheral_connect_config_t scan_config = { - .match_adv = xbox_advertisement_matches, - .match_adv_rsp = xbox_advertisement_response_matches, - .notification_handler = handle_notification, + .match_adv = pb_type_xbox_advertisement_matches, + .match_adv_rsp = pb_type_xbox_advertisement_response_matches, + .notification_handler = pb_type_xbox_handle_notification, .options = PBDRV_BLUETOOTH_PERIPHERAL_OPTIONS_PAIR, .timeout = self->scan_timeout, }; @@ -470,9 +470,25 @@ static mp_obj_t pb_type_xbox_make_new(const mp_obj_type_t *type, size_t n_args, } #endif // PYBRICKS_HUB_TECHNICHUB - if (mp_obj_is_true(connect_in)) { + bool want_connection = mp_obj_is_true(connect_in); + + // Attempt to re-use existing connection. + pbdrv_bluetooth_peripheral_connect_config_t scan_config = { + .match_adv = pb_type_xbox_advertisement_matches, + .match_adv_rsp = pb_type_xbox_advertisement_response_matches, + .notification_handler = pb_type_xbox_handle_notification, + }; + pbio_error_t err = pbdrv_bluetooth_peripheral_get_connected(&self->peripheral, self, &scan_config); + + // If we aren't already connected, do so now if requested. + if (err == PBIO_ERROR_NO_DEV && want_connection) { pb_type_xbox_connect(MP_OBJ_FROM_PTR(self)); } + // If being connected now is not desired, disconnect. + else if (err == PBIO_SUCCESS && !want_connection) { + pb_type_xbox_disconnect(MP_OBJ_FROM_PTR(self)); + } + // Other combinations are already in the desired state, so do nothing else. return MP_OBJ_FROM_PTR(self); } From 98ba9d31db56774c4f320b7c36132f762b5a0c0f Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 17:11:18 +0100 Subject: [PATCH 12/13] pbio/drv/bluetooth: Cache matching advertisements. When re-using an existing connection, this lets us make sure all filters match as if making a new connection. --- lib/pbio/drv/bluetooth/bluetooth.c | 8 ++++++-- lib/pbio/drv/bluetooth/bluetooth_btstack.c | 19 +++++++++++++------ .../drv/bluetooth/bluetooth_stm32_bluenrg.c | 6 ++++++ .../drv/bluetooth/bluetooth_stm32_cc2640.c | 7 +++++++ lib/pbio/include/pbdrv/bluetooth.h | 8 ++++++++ 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/lib/pbio/drv/bluetooth/bluetooth.c b/lib/pbio/drv/bluetooth/bluetooth.c index 7d1c16e5b..1739b33dc 100644 --- a/lib/pbio/drv/bluetooth/bluetooth.c +++ b/lib/pbio/drv/bluetooth/bluetooth.c @@ -188,10 +188,14 @@ pbio_error_t pbdrv_bluetooth_peripheral_get_connected(pbdrv_bluetooth_peripheral continue; } - // Callbacks must be the same. + // Callbacks must be the same, and still match with the given user. + // This ensures that we fail as intended when the same classes are used + // but the user has configured different filters such as the name. if (peri->config.match_adv != config->match_adv || peri->config.match_adv_rsp != config->match_adv_rsp || - peri->config.notification_handler != config->notification_handler) { + peri->config.notification_handler != config->notification_handler || + !peri->config.match_adv(user, peri->config.match_adv_data, peri->config.match_adv_data_len) || + !peri->config.match_adv_rsp(user, peri->config.match_adv_rsp_data, peri->config.match_adv_rsp_data_len)) { continue; } diff --git a/lib/pbio/drv/bluetooth/bluetooth_btstack.c b/lib/pbio/drv/bluetooth/bluetooth_btstack.c index 13da6c288..0a6f9ad6c 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_btstack.c +++ b/lib/pbio/drv/bluetooth/bluetooth_btstack.c @@ -726,6 +726,10 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s DEBUG_PRINT("Advertisement matched, waiting for scan response\n"); + // Copy data to allow virtual re-connect in a new user program. + peri->config.match_adv_data_len = gap_event_advertising_report_get_data_length(event_packet); + memcpy(peri->config.match_adv_data, gap_event_advertising_report_get_data(event_packet), peri->config.match_adv_data_len); + // Wait for advertising response unless timed out or cancelled. PBIO_OS_AWAIT_UNTIL(state, scan_timed_out || peri->cancel || (hci_event_is_type(event_packet, GAP_EVENT_ADVERTISING_REPORT) && ({ @@ -748,12 +752,7 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // that event data for processing. const uint8_t *data = gap_event_advertising_report_get_data(event_packet); uint8_t data_len = gap_event_advertising_report_get_data_length(event_packet); - if (peri->config.match_adv_rsp(peri->user, data, data_len)) { - // Copy name for later use. - if (data[1] == BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME) { - memcpy(peri->name, &data[2], sizeof(peri->name)); - } - } else { + if (!peri->config.match_adv_rsp(peri->user, data, data_len)) { // We got a valid scan response from the device that matched our // advertising filter, but it did not match the response filter (e.g. // requested name did not match), so scan again. @@ -763,6 +762,14 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s DEBUG_PRINT("Scan response matched, initiate connection to %s.\n", bd_addr_to_str(peri->bdaddr)); + // Copy name for later use. + if (data[1] == BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME) { + memcpy(peri->name, &data[2], sizeof(peri->name)); + } + // Copy response data to allow virtual re-connect in a new user program. + peri->config.match_adv_rsp_data_len = data_len; + memcpy(peri->config.match_adv_rsp_data, data, data_len); + // We can stop scanning now. gap_stop_scan(); diff --git a/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c b/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c index 811819a07..23af6c0a6 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c +++ b/lib/pbio/drv/bluetooth/bluetooth_stm32_bluenrg.c @@ -381,6 +381,9 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // save the Bluetooth address for later peri->bdaddr_type = subevt->bdaddr_type; memcpy(peri->bdaddr, subevt->bdaddr, sizeof(peri->bdaddr)); + // Copy data to allow virtual re-connect in a new user program. + peri->config.match_adv_data_len = subevt->data_length; + memcpy(peri->config.match_adv_data, subevt->data_RSSI, subevt->data_length); break; } @@ -411,6 +414,9 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // All checks passed, so copy the device name for later use. memcpy(peri->name, &subevt->data_RSSI[2], sizeof(peri->name)); + // Copy data to allow virtual re-connect in a new user program. + peri->config.match_adv_rsp_data_len = subevt->data_length; + memcpy(peri->config.match_adv_rsp_data, subevt->data_RSSI, subevt->data_length); break; } diff --git a/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c b/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c index 1a77a1ef1..f2142bfeb 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c +++ b/lib/pbio/drv/bluetooth/bluetooth_stm32_cc2640.c @@ -458,6 +458,10 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // Save the Bluetooth address for later comparison against response. peri->bdaddr_type = read_buf[10]; memcpy(peri->bdaddr, &read_buf[11], sizeof(peri->bdaddr)); + + // Copy data to allow virtual re-connect in a new user program. + peri->config.match_adv_data_len = read_buf[18]; + memcpy(peri->config.match_adv_data, &read_buf[19], read_buf[18]); break; } @@ -498,6 +502,9 @@ pbio_error_t pbdrv_bluetooth_peripheral_scan_and_connect_func(pbio_os_state_t *s // All checks passed, so copy the device name for later use. memcpy(peri->name, &read_buf[21], sizeof(peri->name)); + // Copy data to allow virtual re-connect in a new user program. + peri->config.match_adv_rsp_data_len = read_buf[18]; + memcpy(peri->config.match_adv_rsp_data, &read_buf[19], read_buf[18]); break; } diff --git a/lib/pbio/include/pbdrv/bluetooth.h b/lib/pbio/include/pbdrv/bluetooth.h index 4900b88b2..652d5f925 100644 --- a/lib/pbio/include/pbdrv/bluetooth.h +++ b/lib/pbio/include/pbdrv/bluetooth.h @@ -123,6 +123,14 @@ typedef struct { pbdrv_bluetooth_peripheral_options_t options; /** Timeout before aborting scan and connect. Use 0 for no timeout. */ uint32_t timeout; + /** Last matching advertisement data for this peripheral. */ + uint8_t match_adv_data[PBDRV_BLUETOOTH_MAX_ADV_SIZE]; + /** Last matching advertisement data length for this peripheral. */ + uint8_t match_adv_data_len; + /** Last matching advertisement response data for this peripheral. */ + uint8_t match_adv_rsp_data[PBDRV_BLUETOOTH_MAX_ADV_SIZE]; + /** Last matching advertisement response data length for this peripheral. */ + uint8_t match_adv_rsp_data_len; } pbdrv_bluetooth_peripheral_connect_config_t; /** Platform-specific state needed to operate the peripheral. */ From 7ff2e5a744c3aeb0939572871657055d77bbf314 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 3 Feb 2026 19:23:37 +0100 Subject: [PATCH 13/13] pybricks.iodevices.LWP3Device: Set LWP3 handle if connection skipped. When we skip discovery, we'll still need the handle. There is only one handle for LWP3 devices, so we can reuse it. We might revisit this to skip only the connection but redo the discovery phase for generality. --- pybricks/iodevices/pb_type_iodevices_lwp3device.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pybricks/iodevices/pb_type_iodevices_lwp3device.c b/pybricks/iodevices/pb_type_iodevices_lwp3device.c index 4beff9651..79f6d9d50 100644 --- a/pybricks/iodevices/pb_type_iodevices_lwp3device.c +++ b/pybricks/iodevices/pb_type_iodevices_lwp3device.c @@ -539,7 +539,12 @@ static void pb_lwp3device_intialize_connection(mp_obj_t self_in, mp_obj_t connec else if (err == PBIO_SUCCESS && !want_connection) { pb_lwp3device_disconnect(self_in); } - // Other combinations are already in the desired state, so do nothing else. + + // If we have reconnected virtually, we're skipping the discovery phase, + // so use the result from last time. + if (pbdrv_bluetooth_peripheral_is_connected(self->peripheral)) { + self->lwp3_char_handle = pbdrv_bluetooth_peripheral_discover_characteristic_get_result(self->peripheral); + } }