From 50660caff617c215747f1b6ccc448c15ca5a6ca3 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Tue, 19 Nov 2024 15:15:45 +1000 Subject: [PATCH 1/3] tools: native_bt: initial native Bluetooth gateway Add initial support for using the native Bluetooth adapter as a gateway. This is currently sufficient to observe Bluetooth advertising packets. Signed-off-by: Jordan Yates --- pyproject.toml | 1 + src/infuse_iot/tools/native_bt.py | 76 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/infuse_iot/tools/native_bt.py diff --git a/pyproject.toml b/pyproject.toml index 4f0d8ec..0c54f09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "argcomplete", "aiohttp", "attrs", + "bleak", "cryptography", "colorama", "httpx", diff --git a/src/infuse_iot/tools/native_bt.py b/src/infuse_iot/tools/native_bt.py new file mode 100644 index 0000000..6088769 --- /dev/null +++ b/src/infuse_iot/tools/native_bt.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +"""Native Bluetooth gateway tool""" + +__author__ = "Jordan Yates" +__copyright__ = "Copyright 2024, Embeint Inc" + +import asyncio + +from bleak import BleakScanner +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from infuse_iot.util.argparse import BtLeAddress +from infuse_iot.util.console import Console +from infuse_iot.epacket import CtypeBtAdvFrame, PacketReceived, HopReceived, Interface, Auth, Flags, InterfaceAddress +from infuse_iot.commands import InfuseCommand +from infuse_iot.socket_comms import LocalServer, default_multicast_address +from infuse_iot.database import DeviceDatabase + +class SubCommand(InfuseCommand): + NAME = "native_bt" + HELP = "Native Bluetooth gateway" + DESCRIPTION = "Use the local Bluetooth adapater for Bluetooth interaction" + + @classmethod + def add_parser(cls, parser): + pass + + def __init__(self, args): + self.infuse_manu = 0x0DE4 + self.infuse_service = '0000fc74-0000-1000-8000-00805f9b34fb' + self.database = DeviceDatabase() + Console.init() + + def simple_callback(self, device: BLEDevice, data: AdvertisementData): + addr = InterfaceAddress.BluetoothLeAddr(0, BtLeAddress(device.address)) + rssi = data.rssi + payload = data.manufacturer_data[self.infuse_manu] + + hdr, decr = CtypeBtAdvFrame.decrypt(self.database, payload) + + hop = HopReceived( + hdr.device_id, + Interface.BT_ADV, + addr, + ( + Auth.DEVICE + if hdr.flags & Flags.ENCR_DEVICE + else Auth.NETWORK + ), + hdr.key_metadata, + hdr.gps_time, + hdr.sequence, + rssi, + ) + + Console.log_rx(hdr.type, len(payload)) + pkt = PacketReceived([hop], hdr.type, decr) + self.server.broadcast(pkt) + + async def async_run(self): + self.server = LocalServer(default_multicast_address()) + + scanner = BleakScanner( + self.simple_callback, [self.infuse_service] , cb=dict(use_bdaddr=True) + ) + + while True: + Console.log_info("Starting scanner") + async with scanner: + # Run the scanner forever + await asyncio.Future() + + def run(self): + asyncio.run(self.async_run()) From c4431892af92bf728626b4685a17947af9277176 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Wed, 20 Nov 2024 11:15:13 +1000 Subject: [PATCH 2/3] tools: localhost: minimal logging Add some minor logging to the application on client conn/disconnection. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/localhost.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/infuse_iot/tools/localhost.py b/src/infuse_iot/tools/localhost.py index 6d37aa8..11a1013 100644 --- a/src/infuse_iot/tools/localhost.py +++ b/src/infuse_iot/tools/localhost.py @@ -7,13 +7,14 @@ import asyncio import ctypes -import json import pathlib import threading import time from aiohttp import web +from aiohttp.web_request import BaseRequest from aiohttp.web_runner import GracefulExit +from infuse_iot.util.console import Console from infuse_iot.epacket import InfuseType, Interface from infuse_iot.commands import InfuseCommand from infuse_iot.socket_comms import LocalClient, default_multicast_address @@ -41,10 +42,12 @@ async def handle_index(self, request): return web.FileResponse(this_folder / "localhost" / "index.html") - async def websocket_handler(self, request): + async def websocket_handler(self, request: BaseRequest): ws = web.WebSocketResponse() await ws.prepare(request) + Console.log_info(f"Websocket client connected ({request.remote})") + try: while True: # Example data sent to the client @@ -102,13 +105,14 @@ async def websocket_handler(self, request): } self._data_lock.release() - await ws.send_str(json.dumps(message)) + await ws.send_json(message) await asyncio.sleep(1) - except asyncio.CancelledError: - print("WebSocket connection closed") + except (asyncio.CancelledError, ConnectionResetError): + pass finally: await ws.close() + Console.log_info(f"Websocket client disconnected ({request.remote})") return ws def tdf_columns(self, tdf): @@ -198,6 +202,7 @@ def recv_thread(self): self._data_lock.release() def run(self): + Console.init() app = web.Application() # Route for serving the HTML file app.router.add_get("/", self.handle_index) From d25610ef6bc68ff4ba3b94d1b736397a4507adc3 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Wed, 20 Nov 2024 11:47:53 +1000 Subject: [PATCH 3/3] tools: localhost: improved `ctrl-c` handling Better handle `ctrl-c` when there are no clients connected. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/localhost.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/infuse_iot/tools/localhost.py b/src/infuse_iot/tools/localhost.py index 11a1013..4d3ee51 100644 --- a/src/infuse_iot/tools/localhost.py +++ b/src/infuse_iot/tools/localhost.py @@ -216,5 +216,7 @@ def run(self): try: web.run_app(app, host="localhost", port=8080) except GracefulExit: + pass + finally: self._thread_end.set() rx_thread.join(1.0)