Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
release:
types: [published]

permissions:
contents: read

jobs:
deploy:
runs-on: ubuntu-24.04
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: Test Python Package

on: [ pull_request, workflow_dispatch ]

permissions:
contents: read

jobs:
test-docs:
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -85,16 +88,14 @@ jobs:
sudo apt-get update
sudo apt-get install -y qemu-system-x86 qemu-utils
- name: Download qemu images
env:
image_repo: "spacecheese/bluez_images"
run: |
image_id=${{ matrix.image_id }}
mkdir -p tests/loopback/assets
curl -L -o tests/loopback/assets/id_ed25519 \
https://github.com/$image_repo/releases/latest/download/id_ed25519
https://github.com/spacecheese/bluez_images/releases/latest/download/id_ed25519
chmod 600 tests/loopback/assets/id_ed25519
curl -L -o tests/loopback/assets/image.qcow2 \
https://github.com/$image_repo/releases/latest/download/${image_id}.qcow2
https://github.com/spacecheese/bluez_images/releases/latest/download/${image_id}.qcow2
- name: Run loopback Tests
run: |
tests/loopback/test.sh tests/loopback/assets/image.qcow2 tests/loopback/assets/id_ed25519 .
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ You can then run all pre-commit checks manually:
pre-commit run --hook-stage manual --all-files
```

For instructions on running tests locally please consult the respective markdown files- this process more more complex since bluez, dbus and the hci_vhci kernel module are required.
For instructions on running tests locally please consult the respective markdown files- these require some specific environment setup.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Characteristics may operate in a number of modes depending on their purpose. By

## Usage

When using this library please remember that services are not implicitly threaded. **The thread used to register your service must regularly yeild otherwise your service will not work** (particularly notifications). Therefore you must frequently yield to the asyncio event loop (for example using asyncio.sleep) and ideally use multithreading.
When using this library please remember that services are not implicitly threaded. **The thread used to register your service must regularly yield otherwise your service will not work** (particularly notifications). Therefore you must frequently yield to the asyncio event loop (for example using asyncio.sleep) and ideally use multithreading.

The easiest way to use the library is to create a class describing the service that you wish to provide.

Expand Down
49 changes: 32 additions & 17 deletions bluez_peripheral/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ async def get_uuids(self) -> Collection[UUIDLike]:

async def get_manufacturer_data(self) -> Dict[int, bytes]:
"""Returns the manufacturer data."""
return await self._device_interface.get_manufacturer_data() # type: ignore
data = await self._device_interface.get_manufacturer_data() # type: ignore
return {k: v.value for k, v in data.items()}

async def get_service_data(self) -> List[Tuple[UUIDLike, bytes]]:
async def get_service_data(self) -> Dict[UUIDLike, bytes]:
"""Returns the service data."""
data = await self._device_interface.get_service_data() # type: ignore
return [(UUID16.parse_uuid(u), d) for u, d in data]
return {UUID16.parse_uuid(k): v.value for k, v in data.items()}


class Adapter:
Expand Down Expand Up @@ -203,40 +204,55 @@ async def discover_devices(self, duration: float = 10.0) -> AsyncIterator[Device
asyncio.Queue()
)

adapter_path = self._adapter_interface.path
bus = self._adapter_interface.bus

def _interface_added(path: str, intfs_and_props: Dict[str, Dict[str, Variant]]): # type: ignore
queue.put_nowait((path, intfs_and_props))

introspection = await self._adapter_interface.bus.introspect("org.bluez", "/")
proxy = self._adapter_interface.bus.get_proxy_object(
"org.bluez", "/", introspection
)
introspection = await bus.introspect("org.bluez", "/")
proxy = bus.get_proxy_object("org.bluez", "/", introspection)
object_manager_interface = proxy.get_interface(
"org.freedesktop.DBus.ObjectManager"
)
object_manager_interface.on_interfaces_added(_interface_added) # type: ignore

yielded_paths = set()

# Yield any devices which are already present.
device_nodes = (await bus.introspect("org.bluez", adapter_path)).nodes
for node in device_nodes:
if node.name is None:
continue

node_path = adapter_path + "/" + node.name
yield await self._get_device(node_path)
yielded_paths.add(node_path)

async def _stop_discovery() -> None:
await asyncio.sleep(duration)
await self.stop_discovery()

await self.start_discovery()
stop_task = None
timeout_task = None
if duration > 0:
stop_task = asyncio.create_task(_stop_discovery())
timeout_task = asyncio.create_task(_stop_discovery())

adapter_path = self._adapter_interface.path
while not self._discovery_stopped.is_set():
if stop_task is not None:
if timeout_task is None:
path, intfs_and_props = await queue.get()
else:
queue_task = asyncio.create_task(queue.get())

# Block until either a timeout or we find a device.
done, _ = await asyncio.wait(
[queue_task, stop_task],
[queue_task, timeout_task],
return_when=asyncio.FIRST_COMPLETED,
)

if stop_task in done and queue_task not in done:
# Break out if we timed out and didn't find a device.
if timeout_task in done and queue_task not in done:
queue_task.cancel()
try:
await queue_task
Expand All @@ -245,8 +261,6 @@ async def _stop_discovery() -> None:
break

path, intfs_and_props = queue_task.result()
else:
path, intfs_and_props = await queue.get()

if (
path.startswith(adapter_path)
Expand All @@ -258,10 +272,11 @@ async def _stop_discovery() -> None:
yield await self._get_device(path)
yielded_paths.add(path)

if stop_task is not None and stop_task.done():
stop_task.cancel()
# Cancel the timeout task if it's still pending (discovery must have been cancelled by someone else).
if timeout_task is not None and not timeout_task.done():
timeout_task.cancel()
try:
await stop_task
await timeout_task
except asyncio.CancelledError:
pass

Expand Down
Loading
Loading