feat: add Bluetooth LE mode, radio power budget and NINA firmware update path

This commit is contained in:
randogoth 2026-10-25 17:48:20 +02:00
parent 2da15b4356
commit d821b77d3b
122 changed files with 17008 additions and 1371 deletions

75
tools/ble_check.py Normal file
View file

@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Checks the STRIX firmware's Bluetooth LE API (arduino/strix/API.md) from this computer:
finds the device, sends commands over the command characteristic, reads the replies from the
reply notifications, and reads and streams the entropy characteristic."""
import argparse
import asyncio
import json
import sys
from bleak import BleakClient, BleakScanner
SERVICE = "53545249-5800-4000-8000-000000000001"
COMMAND = "53545249-5800-4000-8000-000000000002"
REPLY = "53545249-5800-4000-8000-000000000003"
ENTROPY = "53545249-5800-4000-8000-000000000004"
class Replies:
"""Reassembles reply notifications into newline-terminated lines."""
def __init__(self):
self.buffer = b""
self.lines: asyncio.Queue[str] = asyncio.Queue()
def handle(self, _sender, data: bytearray) -> None:
self.buffer += bytes(data)
while b"\n" in self.buffer:
line, self.buffer = self.buffer.split(b"\n", 1)
self.lines.put_nowait(line.decode(errors="replace").strip())
async def command(client: BleakClient, replies: Replies, text: str) -> str:
await client.write_gatt_char(COMMAND, text.encode(), response=True)
return await asyncio.wait_for(replies.lines.get(), timeout=10)
async def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--timeout", type=float, default=15, help="scan time in seconds")
args = parser.parse_args()
device = await BleakScanner.find_device_by_filter(
lambda d, adv: (d.name or "") == "STRIX" or SERVICE in adv.service_uuids, timeout=args.timeout)
if device is None:
sys.exit("no STRIX device found")
print(f"found {device.name} at {device.address}")
async with BleakClient(device) as client:
replies = Replies()
await client.start_notify(REPLY, replies.handle)
for text in ["id", "health", "ble", "entropy 32"]:
reply = await command(client, replies, text)
print(f"{text:<12} -> {reply[:160]}")
stats = json.loads(await command(client, replies, "stats"))
print(f"{'stats':<12} -> trial {stats.get('trial')}, combined total z "
f"{stats['channels'][-1]['total']['z'] if 'channels' in stats else None}")
first = await client.read_gatt_char(ENTROPY)
second = await client.read_gatt_char(ENTROPY)
print(f"entropy read -> {len(first)} bytes, then {len(second)} bytes, "
f"{'different' if first != second else 'IDENTICAL'}")
chunks = []
await client.start_notify(ENTROPY, lambda _s, data: chunks.append(bytes(data)))
await asyncio.sleep(3)
await client.stop_notify(ENTROPY)
total = sum(len(c) for c in chunks)
print(f"entropy stream -> {len(chunks)} notifications, {total} bytes in 3 s "
f"({total / 3:.0f} B/s), {len(set(chunks))} distinct")
if __name__ == "__main__":
asyncio.run(main())