diff --git a/bricks/_common/qstrdefs.h b/bricks/_common/qstrdefs.h index a7f3b53e1..dc432b4ac 100644 --- a/bricks/_common/qstrdefs.h +++ b/bricks/_common/qstrdefs.h @@ -15,6 +15,10 @@ Q(pybricks.ev3devices) Q(pybricks.experimental) #endif +#if PYBRICKS_PY_COMMON_BTC +Q(pybricks.experimental.btc) +#endif + #if MICROPY_PY_BUILTINS_FLOAT // backwards compatibility with Pybricks V3.2, maps to pybricks.tools Q(pybricks.geometry) #endif diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index aefbf87fb..e151fbfec 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -33,6 +33,7 @@ PYBRICKS_PYBRICKS_SRC_C = $(addprefix pybricks/,\ ev3devices/pb_type_ev3devices_infraredsensor.c \ ev3devices/pb_type_ev3devices_touchsensor.c \ ev3devices/pb_type_ev3devices_ultrasonicsensor.c \ + experimental/pb_module_btc.c \ experimental/pb_module_experimental.c \ hubs/pb_module_hubs.c \ hubs/pb_type_buildhat.c \ @@ -116,6 +117,7 @@ PBIO_SRC_C = $(addprefix lib/pbio/,\ drv/bluetooth/bluetooth.c \ drv/bluetooth/bluetooth_btstack_stm32_hal.c \ drv/bluetooth/bluetooth_btstack.c \ + drv/bluetooth/bluetooth_btstack_classic.c \ drv/bluetooth/bluetooth_btstack_ev3.c \ drv/bluetooth/bluetooth_simulation.c \ drv/bluetooth/bluetooth_stm32_bluenrg.c \ diff --git a/bricks/ev3/mpconfigport.h b/bricks/ev3/mpconfigport.h index 85f9cc1fe..55c423371 100644 --- a/bricks/ev3/mpconfigport.h +++ b/bricks/ev3/mpconfigport.h @@ -16,6 +16,7 @@ // Pybricks modules #define PYBRICKS_PY_COMMON (1) #define PYBRICKS_PY_COMMON_BLE (0) +#define PYBRICKS_PY_COMMON_BTC (1) #define PYBRICKS_PY_COMMON_CHARGER (0) #define PYBRICKS_PY_COMMON_COLOR_LIGHT (1) #define PYBRICKS_PY_COMMON_CONTROL (1) diff --git a/lib/pbio/drv/bluetooth/bluetooth_btstack.c b/lib/pbio/drv/bluetooth/bluetooth_btstack.c index 2fb80de4c..1e7ccbe8d 100644 --- a/lib/pbio/drv/bluetooth/bluetooth_btstack.c +++ b/lib/pbio/drv/bluetooth/bluetooth_btstack.c @@ -23,6 +23,7 @@ #include "bluetooth.h" #include "bluetooth_btstack.h" +#include "bluetooth_btstack_classic.h" #if PBDRV_CONFIG_BLUETOOTH_BTSTACK_LE #include "genhdr/pybricks_service.h" @@ -1181,6 +1182,10 @@ void pbdrv_bluetooth_init_hci(void) { nordic_spp_service_server_init(nordic_spp_packet_handler); #endif // PBDRV_CONFIG_BLUETOOTH_BTSTACK_LE + #if PBDRV_CONFIG_BLUETOOTH_CLASSIC + pbdrv_bluetooth_classic_init(); + #endif + bluetooth_thread_err = PBIO_ERROR_AGAIN; bluetooth_thread_state = 0; pbio_os_process_start(&pbdrv_bluetooth_hci_process, pbdrv_bluetooth_hci_process_thread, NULL); diff --git a/lib/pbio/drv/bluetooth/bluetooth_btstack_classic.c b/lib/pbio/drv/bluetooth/bluetooth_btstack_classic.c new file mode 100644 index 000000000..fae7d0dff --- /dev/null +++ b/lib/pbio/drv/bluetooth/bluetooth_btstack_classic.c @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +// This file defines the functions required to implement Pybricks' Bluetooth +// classic functionality using BTStack. + +#include + +#if PBDRV_CONFIG_BLUETOOTH_BTSTACK_CLASSIC + +#include + +#include +#include +#include + +#include + +#include + +static int32_t pending_inquiry_response_count; +static int32_t pending_inquiry_response_limit; +static void *pending_inquiry_result_handler_context; +static pbdrv_bluetooth_inquiry_result_handler_t pending_inquiry_result_handler; + +static void handle_hci_event_packet(uint8_t *packet, uint16_t size); + +void pbdrv_bluetooth_classic_handle_packet(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { + switch (packet_type) { + case HCI_EVENT_PACKET: + handle_hci_event_packet(packet, size); + } +} + +static void handle_hci_event_packet(uint8_t *packet, uint16_t size) { + switch (hci_event_packet_get_type(packet)) { + case GAP_EVENT_INQUIRY_RESULT: { + if (!pending_inquiry_result_handler) { + return; + } + pbdrv_bluetooth_inquiry_result_t result; + gap_event_inquiry_result_get_bd_addr(packet, result.bdaddr); + if (gap_event_inquiry_result_get_rssi_available(packet)) { + result.rssi = gap_event_inquiry_result_get_rssi(packet); + } + if (gap_event_inquiry_result_get_name_available(packet)) { + const uint8_t *name = gap_event_inquiry_result_get_name(packet); + const size_t name_len = gap_event_inquiry_result_get_name_len(packet); + snprintf(result.name, sizeof(result.name), "%.*s", (int)name_len, name); + } + result.class_of_device = gap_event_inquiry_result_get_class_of_device(packet); + pending_inquiry_result_handler(pending_inquiry_result_handler_context, &result); + if (pending_inquiry_response_limit > 0) { + pending_inquiry_response_count++; + if (pending_inquiry_response_count >= pending_inquiry_response_limit) { + gap_inquiry_stop(); + } + } + break; + } + case GAP_EVENT_INQUIRY_COMPLETE: { + if (pending_inquiry_result_handler) { + pending_inquiry_result_handler = NULL; + pending_inquiry_result_handler_context = NULL; + pbio_os_request_poll(); + } + break; + } + default: + break; + } +} + +void pbdrv_bluetooth_classic_init() { + static btstack_packet_callback_registration_t hci_event_handler_registration; + hci_event_handler_registration.callback = pbdrv_bluetooth_classic_handle_packet; + hci_add_event_handler(&hci_event_handler_registration); +} + +pbio_error_t pbdrv_bluetooth_inquiry_scan( + pbio_os_state_t *state, + int32_t max_responses, + int32_t timeout, + void *context, + pbdrv_bluetooth_inquiry_result_handler_t result_handler) { + PBIO_OS_ASYNC_BEGIN(state); + if (pending_inquiry_result_handler) { + return PBIO_ERROR_BUSY; + } + PBIO_OS_AWAIT_UNTIL(state, hci_get_state() == HCI_STATE_WORKING); + pending_inquiry_response_count = 0; + pending_inquiry_response_limit = max_responses; + pending_inquiry_result_handler = result_handler; + pending_inquiry_result_handler_context = context; + gap_inquiry_start(timeout); + + PBIO_OS_AWAIT_UNTIL(state, !pending_inquiry_result_handler); + PBIO_OS_ASYNC_END(PBIO_SUCCESS); +} + +#endif // PBDRV_CONFIG_BLUETOOTH_BTSTACK_CLASSIC diff --git a/lib/pbio/drv/bluetooth/bluetooth_btstack_classic.h b/lib/pbio/drv/bluetooth/bluetooth_btstack_classic.h new file mode 100644 index 000000000..eee0e70a8 --- /dev/null +++ b/lib/pbio/drv/bluetooth/bluetooth_btstack_classic.h @@ -0,0 +1,13 @@ +#ifndef PBDRV_BLUETOOTH_BLUETOOTH_BTSTACK_CLASSIC_H +#define PBDRV_BLUETOOTH_BLUETOOTH_BTSTACK_CLASSIC_H +#include + +#if PBDRV_CONFIG_BLUETOOTH_CLASSIC + +#include + +void pbdrv_bluetooth_classic_init(); + +#endif // PBDRV_CONFIG_BLUETOOTH_CLASSIC + +#endif // PBDRV_BLUETOOTH_BLUETOOTH_BTSTACK_CLASSIC_H diff --git a/lib/pbio/include/pbdrv/bluetooth.h b/lib/pbio/include/pbdrv/bluetooth.h index 67f50abee..88f95e737 100644 --- a/lib/pbio/include/pbdrv/bluetooth.h +++ b/lib/pbio/include/pbdrv/bluetooth.h @@ -607,9 +607,50 @@ static inline pbio_error_t pbdrv_bluetooth_close_user_tasks(pbio_os_state_t *sta return PBIO_SUCCESS; } - #endif // PBDRV_CONFIG_BLUETOOTH +#if PBDRV_CONFIG_BLUETOOTH_CLASSIC + +// A single result from an inquiry scan. +typedef struct { + uint8_t bdaddr[6]; + int8_t rssi; + char name[249]; + uint32_t class_of_device; +} pbdrv_bluetooth_inquiry_result_t; + +// Callback for handling inquiry results. +typedef void (*pbdrv_bluetooth_inquiry_result_handler_t)(void *context, const pbdrv_bluetooth_inquiry_result_t *result); + +/** + * Runs a bluetooth inquiry scan. Only one such scan can be active at a time. + * + * @param [in] state Protothread state. + * @param [in] max_responses Maximum number of responses to report. Use -1 for unlimited. + * @param [in] timeout Timeout in units of 1.28 seconds. Values less than one will be coerced to one. + * @param [in] context Context pointer to be passed to the result handler. + * @param [in] result_handler Callback that will be called for each inquiry result. + */ +pbio_error_t pbdrv_bluetooth_inquiry_scan( + pbio_os_state_t *state, + int32_t max_responses, + int32_t timeout, + void *context, + pbdrv_bluetooth_inquiry_result_handler_t result_handler); + +#else // PBDRV_CONFIG_BLUETOOTH_CLASSIC + +static inline pbio_error_t pbdrv_bluetooth_inquiry_scan( + pbio_os_state_t *state, + int32_t max_responses, + int32_t timeout, + void *context, + void *result_handler) { + return PBIO_ERROR_NOT_SUPPORTED; +} + +#endif // PBDRV_CONFIG_BLUETOOTH_CLASSIC + #endif // _PBDRV_BLUETOOTH_H_ /** @} */ diff --git a/lib/pbio/platform/ev3/pbdrvconfig.h b/lib/pbio/platform/ev3/pbdrvconfig.h index 0518d173f..256f8fddf 100644 --- a/lib/pbio/platform/ev3/pbdrvconfig.h +++ b/lib/pbio/platform/ev3/pbdrvconfig.h @@ -43,6 +43,7 @@ #define PBDRV_CONFIG_I2C_EV3 (1) #define PBDRV_CONFIG_BLUETOOTH (1) +#define PBDRV_CONFIG_BLUETOOTH_CLASSIC (1) #define PBDRV_CONFIG_BLUETOOTH_MAX_MTU_SIZE 515 #define PBDRV_CONFIG_BLUETOOTH_BTSTACK (1) #define PBDRV_CONFIG_BLUETOOTH_BTSTACK_CLASSIC (1) diff --git a/pybricks/experimental/pb_module_btc.c b/pybricks/experimental/pb_module_btc.c new file mode 100644 index 000000000..7ae724ad5 --- /dev/null +++ b/pybricks/experimental/pb_module_btc.c @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +#include "py/mpconfig.h" + +#if PYBRICKS_PY_COMMON_BTC + +#include +#include +#include + +#include + +#include +#include + +#include "py/obj.h" +#include "py/misc.h" +#include "py/mphal.h" +#include "py/runtime.h" + +#include +#include +#include +#include +#include +#include + +// Register allocated global variables as GC roots since they don't otherwise +// have any parent object to reference them. +MP_REGISTER_ROOT_POINTER(void *scan_pending_results); +MP_REGISTER_ROOT_POINTER(void *scan_awaitable); + +#define GET_SCAN_PENDING_RESULTS() ((pbdrv_bluetooth_inquiry_result_t *)MP_STATE_VM(scan_pending_results)) +#define SET_SCAN_PENDING_RESULTS(ptr) (MP_STATE_VM(scan_pending_results) = (struct pbdrv_bluetooth_inquiry_result_t *)(ptr)) +#define GET_SCAN_AWAITABLE() ((pb_type_async_t *)MP_STATE_VM(scan_awaitable)) +#define SET_SCAN_AWAITABLE(ptr) (MP_STATE_VM(scan_awaitable) = (pb_type_async_t *)(ptr)) + +static size_t scan_results_max_size = 0; +static size_t scan_results_current_index = 0; +static int32_t scan_timeout = 10; + +/** + * Handles inquiry result from the bluetooth driver. + * + * @param [in] context The result list (unused in this phase). + * @param [in] result The inquiry result from the Bluetooth Classic scan. + */ +static void handle_inquiry_result(void *context, const pbdrv_bluetooth_inquiry_result_t *result) { + if (result == NULL) { + return; + } + + // If we haven't reached the maximum number of results and allocation is valid, store this one + if (GET_SCAN_PENDING_RESULTS() != NULL && scan_results_current_index < scan_results_max_size) { + memcpy(&GET_SCAN_PENDING_RESULTS()[scan_results_current_index], result, sizeof(pbdrv_bluetooth_inquiry_result_t)); + scan_results_current_index++; + } +} + +static pbio_error_t pb_type_btc_scan_iterate(pbio_os_state_t *state, mp_obj_t parent_obj) { + return pbdrv_bluetooth_inquiry_scan(state, (int32_t)scan_results_max_size, scan_timeout, NULL, handle_inquiry_result); +} + +/** + * Creates the return value for the scan operation. + * + * @param [in] parent_obj The result list that will be populated. + * @return A Python list containing dictionaries with scan results. + */ +static mp_obj_t pb_type_btc_scan_return_map(mp_obj_t result_list) { + // Convert each stored result to a Python dictionary and add to the list + if (GET_SCAN_PENDING_RESULTS() != NULL) { + for (size_t i = 0; i < scan_results_current_index; i++) { + const pbdrv_bluetooth_inquiry_result_t *result = &GET_SCAN_PENDING_RESULTS()[i]; + mp_obj_t result_dict = mp_obj_new_dict(0); + + // Format Bluetooth address + char bdaddr_str[18]; + snprintf(bdaddr_str, sizeof(bdaddr_str), "%02x:%02x:%02x:%02x:%02x:%02x", + result->bdaddr[5], result->bdaddr[4], result->bdaddr[3], + result->bdaddr[2], result->bdaddr[1], result->bdaddr[0]); + + // Add fields to dictionary + mp_obj_dict_store(result_dict, + MP_ROM_QSTR(MP_QSTR_address), + mp_obj_new_str(bdaddr_str, strlen(bdaddr_str))); + mp_obj_dict_store(result_dict, + MP_ROM_QSTR(MP_QSTR_name), + mp_obj_new_bytes((const byte *)result->name, strlen(result->name))); + mp_obj_dict_store(result_dict, + MP_ROM_QSTR(MP_QSTR_rssi), + mp_obj_new_int(result->rssi)); + mp_obj_dict_store(result_dict, + MP_ROM_QSTR(MP_QSTR_class_of_device), + mp_obj_new_int(result->class_of_device)); + mp_obj_list_append(result_list, result_dict); + } + + // Free the allocated memory + m_del(pbdrv_bluetooth_inquiry_result_t, GET_SCAN_PENDING_RESULTS(), scan_results_max_size); + SET_SCAN_PENDING_RESULTS(NULL); + } + + return result_list; +} + +/** + * Starts a Bluetooth Classic inquiry scan. + * + * @param [in] n_args Number of positional arguments. + * @param [in] pos_args Positional arguments (none expected). + * @param [in] kw_args Keyword arguments (max_devices and timeout are optional). + * max_devices (default: 20) - Maximum number of devices to find + * timeout (default: 10) - Scan timeout in seconds + * @return An awaitable that will return a list of scan results. + */ +static mp_obj_t pb_type_btc_scan(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + // Define the allowed keyword arguments + static const mp_arg_t allowed_args[] = { + { MP_QSTR_max_devices, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 20} }, + { MP_QSTR_timeout, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 10} }, + }; + + // Parse the keyword arguments + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Get max_devices parameter + int32_t max_devices = args[0].u_int; + if (max_devices < 1) { + mp_raise_ValueError(MP_ERROR_TEXT("max_devices must be at least 1")); + return MP_OBJ_NULL; + } + + // Get timeout parameter + scan_timeout = args[1].u_int; + if (scan_timeout < 1) { + mp_raise_ValueError(MP_ERROR_TEXT("timeout must be at least 1")); + return MP_OBJ_NULL; + } + + // Free any previously allocated memory + if (GET_SCAN_PENDING_RESULTS() != NULL) { + m_del(pbdrv_bluetooth_inquiry_result_t, GET_SCAN_PENDING_RESULTS(), scan_results_max_size); + SET_SCAN_PENDING_RESULTS(NULL); + } + + // Allocate memory for the scan results + SET_SCAN_PENDING_RESULTS(m_new(pbdrv_bluetooth_inquiry_result_t, max_devices)); + if (GET_SCAN_PENDING_RESULTS() == NULL) { + mp_raise_OSError(MP_ENOMEM); + } + // Initialize variables for this scan + scan_results_max_size = (size_t)max_devices; + scan_results_current_index = 0; + + // Create the result list that will be populated during scanning + mp_obj_t result_list = mp_obj_new_list(0, NULL); + + pb_type_async_t config = { + .iter_once = pb_type_btc_scan_iterate, + .parent_obj = result_list, // Pass the result list as parent_obj + .close = NULL, // No close function needed for module + .return_map = pb_type_btc_scan_return_map, + }; + + pb_type_async_t *scan_awaitable_ptr = GET_SCAN_AWAITABLE(); + mp_obj_t result = pb_type_async_wait_or_await(&config, &scan_awaitable_ptr, true); + SET_SCAN_AWAITABLE(scan_awaitable_ptr); + + return result; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_btc_scan_obj, 0, pb_type_btc_scan); + +static const mp_rom_map_elem_t common_BTC_globals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_btc) }, + { MP_ROM_QSTR(MP_QSTR_scan), MP_ROM_PTR(&pb_type_btc_scan_obj) }, +}; +static MP_DEFINE_CONST_DICT(common_BTC_globals_dict, common_BTC_globals_dict_table); + +const mp_obj_module_t pb_module_btc = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&common_BTC_globals_dict, +}; + +#if !MICROPY_MODULE_BUILTIN_SUBPACKAGES +MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_experimental_dot_btc, pb_module_btc); +#endif + +#endif // PYBRICKS_PY_COMMON_BTC diff --git a/pybricks/experimental/pb_module_experimental.c b/pybricks/experimental/pb_module_experimental.c index 6f252a925..81f318486 100644 --- a/pybricks/experimental/pb_module_experimental.c +++ b/pybricks/experimental/pb_module_experimental.c @@ -20,6 +20,10 @@ #include +#if PYBRICKS_PY_COMMON_BTC +extern const mp_obj_module_t pb_module_btc; +#endif + // pybricks.experimental.hello_world static mp_obj_t experimental_hello_world(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args, @@ -61,6 +65,9 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(experimental_hello_world_obj, 0, experimental_ static const mp_rom_map_elem_t experimental_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_experimental) }, { MP_ROM_QSTR(MP_QSTR_hello_world), MP_ROM_PTR(&experimental_hello_world_obj) }, + #if PYBRICKS_PY_COMMON_BTC && MICROPY_MODULE_BUILTIN_SUBPACKAGES + { MP_ROM_QSTR(MP_QSTR_btc), MP_ROM_PTR(&pb_module_btc) }, + #endif }; static MP_DEFINE_CONST_DICT(pb_module_experimental_globals, experimental_globals_table);