diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a1bbd53..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 diff --git a/lib/pbio/drv/bluetooth/bluetooth.c b/lib/pbio/drv/bluetooth/bluetooth.c index f1d03f46b..1739b33dc 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; @@ -179,6 +179,37 @@ 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, 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.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; + } + + // 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. @@ -212,7 +243,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; @@ -550,20 +581,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 +607,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 +631,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 +692,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 +725,18 @@ 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. + // 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)); - // Disconnect peripheral. - pbdrv_bluetooth_peripheral_disconnect(peri); - 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 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 +747,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 +759,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..0a6f9ad6c 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) @@ -377,13 +376,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; @@ -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; - pbdrv_bluetooth_ad_match_result_flags_t flags; 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,19 +690,24 @@ 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); - 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); + bool advertising_matched = + event_type <= PBDRV_BLUETOOTH_AD_TYPE_ADV_DIRECT_IND && + 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,10 +715,10 @@ 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) { + 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; @@ -722,49 +726,56 @@ 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); + // 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 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); - char *detected_name = (char *)&data[2]; 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 & PBDRV_BLUETOOTH_AD_MATCH_VALUE) && (flags & PBDRV_BLUETOOTH_AD_MATCH_ADDRESS); + // 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 (pbio_os_timer_is_expired(&peri->timer) || peri->cancel) { + 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 (flags & PBDRV_BLUETOOTH_AD_MATCH_NAME_FAILED) { + // 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)) { + // 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); + 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)); } - - DEBUG_PRINT("Scan response matched, initiate connection to %s.\n", bd_addr_to_str(peri->bdaddr)); + // 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(); // 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) { @@ -787,7 +798,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; } @@ -1264,20 +1275,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 +1293,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..23af6c0a6 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) { @@ -366,25 +366,24 @@ 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)); + // 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; } @@ -401,20 +400,23 @@ 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)); + // 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; } @@ -569,6 +571,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); @@ -944,8 +950,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; @@ -1246,20 +1252,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 +1286,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..f2142bfeb 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. @@ -443,24 +443,25 @@ 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)); + + // 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; } @@ -489,21 +490,21 @@ 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)); + // 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; } @@ -523,7 +524,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 +546,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 +1306,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; @@ -1855,18 +1856,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 +1901,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..652d5f925 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,15 +114,23 @@ 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 */ 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. */ @@ -160,7 +152,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. */ @@ -388,7 +380,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. @@ -398,6 +390,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. * @@ -501,15 +504,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. // @@ -672,6 +666,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; } @@ -711,9 +709,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/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/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"); 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) { diff --git a/pybricks/iodevices/pb_type_iodevices_lwp3device.c b/pybricks/iodevices/pb_type_iodevices_lwp3device.c index 8d460f463..79f6d9d50 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. @@ -145,14 +153,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,66 +167,68 @@ 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; +// 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 flags; + return; } - // 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 */ - && (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; + // 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; } - // Compare address in advertisement to previously scanned address. - if (memcmp(addr, match_addr, 6) == 0) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_ADDRESS; + // 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; } - return flags; -} -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) { + 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; - pbdrv_bluetooth_ad_match_result_flags_t flags = PBDRV_BLUETOOTH_AD_MATCH_NONE; + // 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; if (!self) { - return flags; + return false; } - // 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; - } + // Whether this looks like a LWP3 advertisement of the correct hub kind. + 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; +} - // Compare address in response to previously scanned address. - if (memcmp(addr, match_addr, 6) == 0) { - flags |= PBDRV_BLUETOOTH_AD_MATCH_ADDRESS; - } +static bool lwp3_advertisement_response_matches(void *user, const uint8_t *data, uint8_t length) { - // 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; + pb_lwp3device_obj_t *self = user; + if (!self) { + 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 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 +301,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. @@ -408,17 +407,13 @@ 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; +/** + * 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) { - // 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; + 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; @@ -433,13 +428,36 @@ 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; +static mp_obj_t pb_lwp3device_connect(mp_obj_t self_in) { + + 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)); + + // 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 = pb_lwp3device_handle_notification, + .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)); pb_type_async_t config = { .iter_once = pb_lwp3device_connect_thread, @@ -447,7 +465,19 @@ 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); } +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); @@ -488,24 +518,56 @@ 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); + } + + // 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); + } +} + 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(); 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_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_intialize_connection(MP_OBJ_FROM_PTR(self), connect_in); return MP_OBJ_FROM_PTR(self); } @@ -555,18 +617,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), @@ -575,6 +625,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) }, }; @@ -590,43 +641,15 @@ 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), PB_ARG_DEFAULT_NONE(name), PB_ARG_DEFAULT_INT(timeout, 10000), 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); + 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) { @@ -634,17 +657,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_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), name_in, timeout_in, hub_kind, handle_lwp3_generic_notification, pair); - + pb_lwp3device_intialize_connection(MP_OBJ_FROM_PTR(self), connect_in); return MP_OBJ_FROM_PTR(self); } @@ -705,6 +724,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) }, diff --git a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c index 44b82c19e..bd9c4cd7b 100644 --- a/pybricks/iodevices/pb_type_iodevices_xbox_controller.c +++ b/pybricks/iodevices/pb_type_iodevices_xbox_controller.c @@ -58,6 +58,18 @@ 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. + **/ + bool disconnect_host; /** * Timer used to delay between connection attempts. */ @@ -89,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) { @@ -103,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 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 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. @@ -137,38 +149,20 @@ 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; +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; } - - 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 xbox_input_map_t *pb_type_xbox_get_input(mp_obj_t self_in) { @@ -266,13 +260,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 +286,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 = 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, + }; + 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)); @@ -393,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); @@ -425,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 @@ -447,20 +448,47 @@ 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; + 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 - 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); } #endif // PYBRICKS_HUB_TECHNICHUB - pb_type_xbox_connect(MP_OBJ_FROM_PTR(self)); + 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); } @@ -631,6 +659,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) },