diff --git a/README.md b/README.md index 092f2c1..b82ecd3 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,6 @@ Every command lives inside a group, so it is always `hubblenetwork str: _GROUP_BLURB = { "org": "your devices in the Hubble Cloud", "ble": "nearby devices over Bluetooth", - "ready": "provision a device over GATT", + "ready": "provision a device over GATT (in development)", "sat": "satellite packets via PlutoSDR", "metrics": "fleet counts", } @@ -2539,7 +2540,20 @@ def ble_validate(key: str, device_id: str, org_id: str, token: str, timeout: int @cli.group(cls=HubbleGroup) def ready() -> None: - """Provision a Hubble Ready device over a GATT connection.""" + """Provision a Hubble Ready device over a GATT connection. + + IN DEVELOPMENT. The provisioning flow is not finished: these commands may + change, may not work against current firmware, and are not covered by the + stability promise the other groups get. Every run prints a reminder. + """ + # The library says the same thing through ReadyInDevelopmentWarning, but a + # raw UserWarning traceback line is not what a CLI user should be reading. + warnings.filterwarnings("ignore", category=ready_mod.ReadyInDevelopmentWarning) + click.secho( + "[WARN] `ready` is in development -- commands may change or fail.", + fg="yellow", + err=True, + ) @ready.command("scan", short_help="Find Hubble Ready devices to provision") diff --git a/src/hubblenetwork/ready.py b/src/hubblenetwork/ready.py index 5809ee6..3bab5c3 100644 --- a/src/hubblenetwork/ready.py +++ b/src/hubblenetwork/ready.py @@ -1,14 +1,28 @@ # hubblenetwork/ready.py """ -Hubble Ready device provisioning module. +Hubble Ready device provisioning module -- IN DEVELOPMENT. This module handles provisioning of devices advertising the Hubble Provisioning Service (0xFCA7). Unlike beacon scanning (0xFCA6) which is passive, provisioning involves active GATT connections and characteristic writes. + +**This module is not finished.** The provisioning flow is still being built out: +its API may change without a major version bump, it may not work against current +firmware, and it is not covered by the stability promise the rest of the package +gets. Every public entry point raises `ReadyInDevelopmentWarning` when called. +Silence it with:: + + import warnings + from hubblenetwork.ready import ReadyInDevelopmentWarning + + warnings.filterwarnings("ignore", category=ReadyInDevelopmentWarning) """ from __future__ import annotations import asyncio +import functools +import inspect +import warnings from collections.abc import Callable from dataclasses import dataclass from typing import TYPE_CHECKING @@ -39,6 +53,41 @@ } +class ReadyInDevelopmentWarning(UserWarning): + """The `ready` provisioning API is unfinished and may change or fail.""" + + +_IN_DEVELOPMENT_NOTE = ( + "hubblenetwork.ready is in development: the provisioning flow is not " + "finished, its API may change without a major version bump, and it may not " + "work against current firmware. Silence with warnings.filterwarnings(" + '"ignore", category=hubblenetwork.ready.ReadyInDevelopmentWarning).' +) + + +def _in_development(fn): + """Warn, at every public entry point, that this module is not finished. + + A docstring nobody reads is not a warning. Async entry points get an async + wrapper so `inspect.iscoroutinefunction` keeps answering truthfully. + """ + if inspect.iscoroutinefunction(fn): + + @functools.wraps(fn) + async def async_wrapper(*args, **kwargs): + warnings.warn(_IN_DEVELOPMENT_NOTE, ReadyInDevelopmentWarning, stacklevel=2) + return await fn(*args, **kwargs) + + return async_wrapper + + @functools.wraps(fn) + def wrapper(*args, **kwargs): + warnings.warn(_IN_DEVELOPMENT_NOTE, ReadyInDevelopmentWarning, stacklevel=2) + return fn(*args, **kwargs) + + return wrapper + + @dataclass(frozen=True) class HubbleReadyDevice: """A device advertising the Hubble Provisioning Service (0xFCA7).""" @@ -87,6 +136,7 @@ def on_detect(device, adv_data) -> None: return devices +@_in_development def scan_ready_devices(timeout: float = 10.0) -> list[HubbleReadyDevice]: """ Scan for BLE devices advertising the Hubble Provisioning Service (0xFCA7). @@ -115,6 +165,7 @@ def scan_ready_devices(timeout: float = 10.0) -> list[HubbleReadyDevice]: ) +@_in_development async def scan_ready_devices_async(timeout: float = 10.0) -> list[HubbleReadyDevice]: """ Async version of scan_ready_devices() for use in async environments. @@ -163,6 +214,7 @@ def on_detect(device, adv_data) -> None: return devices +@_in_development def scan_ready_devices_streaming( timeout: float, on_device: Callable[[HubbleReadyDevice], None], @@ -262,6 +314,7 @@ async def _read_status_async(address: str, timeout: float = 30.0) -> StatusChara return StatusCharacteristic.from_bytes(bytes(data)) +@_in_development def read_status(address: str, timeout: float = 30.0) -> StatusCharacteristic: """ Read the Status characteristic from a Hubble Ready device. @@ -332,6 +385,7 @@ async def _read_key_info_async(address: str, timeout: float = 30.0) -> DeviceKey return DeviceKeyInfo.from_bytes(bytes(data)) +@_in_development def read_key_info(address: str, timeout: float = 30.0) -> DeviceKeyInfo: """ Read the Device Key characteristic from a Hubble Ready device. @@ -370,6 +424,7 @@ async def _read_config_async(address: str, timeout: float = 30.0) -> DeviceConfi return DeviceConfig.from_bytes(bytes(data)) +@_in_development def read_config(address: str, timeout: float = 30.0) -> DeviceConfig: """ Read the Device Configuration characteristic from a Hubble Ready device. @@ -408,6 +463,7 @@ async def _read_time_async(address: str, timeout: float = 30.0) -> int: return int.from_bytes(bytes(data), byteorder="little") +@_in_development def read_time(address: str, timeout: float = 30.0) -> int: """ Read the Epoch Time characteristic from a Hubble Ready device. @@ -484,6 +540,7 @@ async def _write_key_async(address: str, key: bytes, timeout: float = 30.0) -> W raise BleError(str(e), att_error_code=att_error_code) from e +@_in_development def write_key(address: str, key: bytes, timeout: float = 30.0) -> WriteResult: """ Write an encryption key to the Device Key characteristic. @@ -591,6 +648,7 @@ async def _write_config_async( raise BleError(str(e), att_error_code=att_error_code) from e +@_in_development def write_config( address: str, eid_type: str, @@ -670,6 +728,7 @@ async def _write_time_async( raise BleError(str(e), att_error_code=att_error_code) from e +@_in_development def write_time( address: str, timestamp: int | None = None, @@ -945,6 +1004,7 @@ async def _connect_and_read_characteristics_async( return results +@_in_development def connect_and_read_characteristics( address: str, timeout: float = 30.0 ) -> list[CharacteristicInfo]: @@ -1193,6 +1253,7 @@ def log(msg: str) -> None: ) +@_in_development def provision_device( address: str, org: Organization,