pybricks/bluetooth: Add bt classic inquiry scans.
Any Pybricks device running btstack can now issue Bluetooth classic inquiry scans to find and report discoverable devices.
This API is experimental and we can change it later if we don't like it. But it's nice because it can be used to concretely demonstrate that the EV3's BTStack layer is doing something! This also should give some indication of the structure by which we'll add further classic functionality (more functions and switch entries in bluetooth_btstack_classic.c). If it's preferred to have it in the same file as the LE code, we can, but this structure is scarcely more expensive and a little more neatly organized.
You can test it with this program:
from pybricks.tools import run_task
from pybricks.experimental.btc import scan
async def do_scan():
print("Start bluetooth scan...")
devices = await scan()
print("Scan complete, devices found: ")
for device in devices:
print(device)
run_task(do_scan())
Download the artifacts for this pull request:
- pr_number.zip
- mpy-cross.zip
- movehub-firmware-build-4581-git4638309d.zip
- ev3-firmware-build-4581-git4638309d.zip
- technichub-firmware-build-4581-git4638309d.zip
- nxt-firmware-build-4581-git4638309d.zip
- cityhub-firmware-build-4581-git4638309d.zip
- primehub-firmware-build-4581-git4638309d.zip
- essentialhub-firmware-build-4581-git4638309d.zip
- buildhat-firmware-build-4581-git4638309d.zip
coverage: 50.566% (+0.02%) from 50.55% when pulling ebf3c1e97c35f28f7ec6a234d3007c4fade8484d on jaguilar:ev3-bluetooth-scan into fcfdf4e0eb20e84211685d589c5fed42e3deb472 on pybricks:master.
Rebased this commit atop all the other refactoring work that has happened recently to btstack, and verified that everything is still working.
Thanks for submitting this! I can confirm that this is giving scan results.
After working on Bluetooth/BLE for the past couple of weeks, I have a few ideas on how we might integrate this into the Bluetooth driver.
I'll post an alternate draft PR soon. Hope that's OK. My goal isn't to rewrite all PRs. Once we've established a good pattern I think we can extend it quite easily to other functionality.
I think we can do this without the manual memory management done here (multiple root pointers, and manual control of m_del).
MicroPython is able to do most of this for us. We can instead use mp_obj_malloc_var_with_finaliser to allocate count_max of pbdrv_bluetooth_inquiry_result_t. The finalizer gives us a hook to safely tell the Bluetooth process to stop processing when the data is freed by the garbage collector.
In #445, I've been working on making Bluetooth tasks chronological. I think the scanner lends itself well to this pattern too:
pbio_error_t pbdrv_bluetooth_inquiry_scan_func(pbio_os_state_t *state, void *context) {
PBIO_OS_ASYNC_BEGIN(state);
gap_inquiry_start(duration);
// Wait until the number of devices are found or scan timeout
PBIO_OS_AWAIT_UNTIL(state, count == count_max || ({
if (hci_event_is_type(event_packet, GAP_EVENT_INQUIRY_RESULT)) {
// process a single result contained in event_packet here
// no callbacks needed, just store it in the context provided array of results
}
// The wait until condition: inquiry complete.
hci_event_is_type(event_packet, GAP_EVENT_INQUIRY_COMPLETE);
}));
PBIO_OS_ASYNC_END(PBIO_SUCCESS);
}
This is also nicely extensible if we make a scan-and-connect task later.
We'll also need to make these tasks cancellable. So that we stop scanning when the program ends or the user interrupts this action. With this pattern, we can do that relatively easily.
I think we're also going to need some kind of Bluetooth loop that can queue/refuse multiple Bluetooth calls, just like we have with BLE. It wouldn't be safe to have two scans running, for example.
The current BLE loop is something like this:
until shutdown:
send notification if something was buffered
do broadcast task if queued
for N peripherals:
do peripheral task if queued
I haven't quite decided if our Bluetooth classic loop should be entirely parallel or just add to the existing loop like:
do classic task if queued
I'm leaning towards managing it in the same loop, so we can keep ensuring that Bluetooth activity is somewhat sequential. Running them in parallel could save a little bit of time if the user was doing something with BLE and classic at the same time, but this doesn't seem worth the extra complexity.
Sounds good. If you are intending to rewrite the PR, I will just wait until that happens. Once it's done, I'll adjust the following PR to have the same format, hopefully. Not sure if it will be possible to follow exactly but we'll see.
It's mostly done, but it needs some polishing like adding cancellation.
OK, here is a draft before calling it a day. The last !deleteme commit contains an example and debugging via USB. The rationale is mostly covered above, but I'm happy to elaborate another day.
Basic testing done on Virtual Hub and EV3 (both Bluetooth chipsets). For Virtual Hub, there's a ready made vscode launch config (virtualhub0).
looks good to me. once it's merged i'll rebase the rfcomm functional changes off of it and try redoing them in a similar style.
Is the idea here that eventually callers will pass in the task context rather than having a single statically allocated task? Seems fine to me just checking my understanding.
Sort of. I think we might have N bluetooth classic devices/connections, each with a static instance, somewhat like the N BLE peripherals we just introduced. Anything that involves outgoing data that isn't copied by BTstack needs to be static since MicroPython might go away any time. We used to have a linked list of tasks allocated in MicroPython, but this was prone to use-after-free issues.
Incoming data is a little different, as shown in the branch above. This can be stored in allocated memory, since we can just reject/ignore the incoming event if the memory was already freed. This is what we rely on here, so we don't need to statically allocate N_SCAN * 256 bytes just for scanning. So incoming RFCOMM data could work a bit like the notification handler, with the background event handler passing data to MicroPython.
The generic rfcomm_scan is perhaps the odd one out since it isn't technically related to any specific instance. Unlike, say, a inquire-and-connect, listen-and-accept, or find-hid-device task.
Alternate version merged in https://github.com/pybricks/pybricks-micropython/commit/bec0e12b1d29b8dbad4e6358b38648ba5b301fac https://github.com/pybricks/pybricks-micropython/commit/f79b9bf0054e85149f2f802ac70f91e205fc6b34 https://github.com/pybricks/pybricks-micropython/commit/5df84531a68bcd77c1e468de23ce9f37a5bf3188
Thanks @jaguilar!