1
0
Fork 0

Introduce a Bluetooth LE client for FreakWAN nodes

This commit is contained in:
Daniele Tricoli 2023-02-26 22:41:33 +01:00
parent 84a43fa364
commit ade2820ed2
4 changed files with 93 additions and 50 deletions

View File

@ -4,20 +4,18 @@
"""A simple tool to send messages into FreakWAN over Bluetooth low energy."""
import asyncio
import logging
import sys
from .cli import get_cli
def run():
def run() -> None:
"""Main entrypoint."""
# ble-serial fire a warning on disconnect, but our main use case is to just
# send a message and disconnect, so we disable logging here.
# TODO: Make configurable by the user.
logging.disable()
try:
asyncio.run(get_cli())
except RuntimeError:
except RuntimeError as e:
sys.exit(str(e))
except KeyboardInterrupt:
pass

View File

@ -4,42 +4,90 @@
"""BLE related stuff for freakble."""
import asyncio
import logging
from typing import Any, Callable
from ble_serial.bluetooth.ble_interface import BLE_interface
from bleak import BleakScanner
from bleak import BleakClient, BleakScanner
from .repl import REPL
__all__ = [
"connect",
"Client",
"repl_loop",
"scan",
"send_text",
]
DEFAULT_TIMEOUT = 5.0
NORDIC_UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
NORDIC_UART_TX_CHARACTERISTIC = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
NORDIC_UART_RX_CHARACTERISTIC = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
async def scan(adapter, timeout=5.0):
"""Scan for BLE devices."""
async def scan(adapter: str, timeout: float = DEFAULT_TIMEOUT):
"""Scan for Bluetooth LE devices."""
return await BleakScanner.discover(adapter=adapter, timeout=timeout)
async def connect(ble: BLE_interface, device: str, timeout: float = 10.0):
"""Connect to the specified device."""
await ble.connect(device, "public", timeout)
# TODO: Handle WRITE_UUID and READ_UUID.
await ble.setup_chars(None, None, "rw")
class Client:
"""Simple client for UART devices."""
def __init__(self, adapter: str, address: str) -> None:
self.adapter = adapter
self.address = address
async def send(ble: BLE_interface, data: bytes, loop: bool, sleep_time: float):
"""Send data over BLE.
self._receive_callback = None
self.disconnect_event = asyncio.Event()
self._client = None
Raise asyncio.CancelledError if loop == False after data is sent once.
"""
while True:
ble.queue_send(data)
if not loop:
raise asyncio.CancelledError
await asyncio.sleep(sleep_time)
async def connect(self, timeout: float = DEFAULT_TIMEOUT):
device = await BleakScanner.find_device_by_address(
self.address, adapter=self.adapter, timeout=timeout
)
if device is None:
raise RuntimeError(f"device with address {self.address} not found")
self._client = BleakClient(device, disconnected_callback=self.on_disconnect)
await self._client.__aenter__()
async def disconnect(self):
if self._client is not None:
await self.stop()
await self._client.disconnect()
async def start(self):
if self._client is not None:
await self._client.start_notify(NORDIC_UART_RX_CHARACTERISTIC, self.on_rx)
async def stop(self):
if self._client is not None:
await self._client.stop_notify(NORDIC_UART_RX_CHARACTERISTIC)
def set_receive_callback(self, callback: Callable[[Any], None]):
self._receive_callback = callback
def on_rx(self, characteristic, data):
logging.debug(characteristic, data)
if self._receive_callback is not None:
self._receive_callback(data)
async def wait_until_disconnect(self):
await self.disconnect_event.wait()
def on_disconnect(self, client: BleakClient):
logging.debug("Disconnect...")
self.disconnect_event.set()
async def send(self, data):
if self._client is not None:
await self._client.write_gatt_char(NORDIC_UART_TX_CHARACTERISTIC, data)
async def send_forever(self, data: bytes, sleep_time: float):
while True:
await self.send(data)
await asyncio.sleep(sleep_time)
async def send_text(
@ -48,29 +96,28 @@ async def send_text(
device: str,
loop: bool,
sleep_time: float,
ble_connection_timeout: float,
timeout: float,
callback=None,
):
"""Send text over BLE.
"""Send text over Bluetooth LE.
This is a facade that handle also connection/disconnection.
"""
ble = BLE_interface(adapter, None)
client = Client(adapter, device)
if callback is not None:
ble.set_receiver(callback)
client.set_receive_callback(callback)
try:
await connect(ble, device, ble_connection_timeout)
await asyncio.gather(
ble.send_loop(),
send(ble, bytes(text, "utf-8"), loop, sleep_time),
)
except asyncio.CancelledError:
pass
except AssertionError:
raise
await client.connect(timeout)
await client.start()
if loop:
await asyncio.gather(
client.wait_until_disconnect(),
client.send_forever(bytes(text, "utf-8"), sleep_time),
)
else:
await client.send(bytes(text, "utf-8"))
finally:
await ble.disconnect()
await client.disconnect()
async def repl_loop(
@ -85,7 +132,7 @@ async def repl_loop(
ble = BLE_interface(adapter, None)
repl = REPL(ble)
try:
await connect(ble, device, ble_connection_timeout)
# await connect(ble, device, ble_connection_timeout)
await asyncio.gather(ble.send_loop(), repl.shell())
except asyncio.CancelledError:
pass

View File

@ -14,7 +14,7 @@ from .gui import App
def ble_receive_callback(data: bytes):
"""Print data received from BLE."""
click.echo(data)
click.echo(data.strip())
@click.group()

View File

@ -15,9 +15,7 @@ try:
except ImportError:
ARE_THEMES_AVAILABLE = False
from .ble import BLE_interface
from .ble import connect as ble_connect
from .ble import scan
from .ble import BLE_interface, scan
WINDOW_SIZE = "800x600"
@ -226,11 +224,11 @@ class DeviceWindow(ttk.Frame):
async def ble_loop(self):
self.ble = BLE_interface(self.main_window.app.adapter, "")
self.ble.set_receiver(self.on_ble_data_received)
await ble_connect(
self.ble,
self.main_window.app.device,
self.main_window.app.ble_connection_timeout,
)
# await ble_connect(
# self.ble,
# self.main_window.app.device,
# self.main_window.app.ble_connection_timeout,
# )
await self.ble.send_loop()
def send_over_ble(self, data):