Skip to content
Merged
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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ Every command lives inside a group, so it is always `hubblenetwork <group> <comm

* **`org`** — your devices in the Hubble Cloud
* **`ble`** — nearby devices over Bluetooth
* **`ready`** — provision a device over GATT
* **`sat`** — satellite packets via PlutoSDR
* **`metrics`** — fleet counts

Expand Down Expand Up @@ -361,7 +360,7 @@ Check whichever route you used with `hubblenetwork validate-credentials` or
## Requirements

- Python **3.10+** (3.11/3.12 recommended)
- **Bluetooth**, for the `ble` and `ready` groups:
- **Bluetooth**, for the `ble` group:
- **macOS**: CoreBluetooth. Run from a real terminal app and grant it Bluetooth
access when prompted. macOS kills any process whose executable has no
`NSBluetoothAlwaysUsageDescription` in an Info.plist, and a bare Python binary
Expand All @@ -386,7 +385,7 @@ for a stable surface:

```python
from hubblenetwork import (
ble, cloud, ready, sat,
ble, cloud, sat,
Organization, Device, Credentials, Environment,
EncryptedPacket, UnencryptedPacket, AesEaxPacket, UnknownPacket,
DecryptedPacket, SatellitePacket, Location,
Expand Down
18 changes: 16 additions & 2 deletions src/hubblenetwork/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import sys
import time
import uuid
import warnings
from dataclasses import replace
from datetime import datetime, timezone
from functools import partial
Expand Down Expand Up @@ -318,7 +319,7 @@ def _fit(text: str, width: int, *, nbytes: int | None = None) -> 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",
}
Expand Down Expand Up @@ -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")
Expand Down
63 changes: 62 additions & 1 deletion src/hubblenetwork/ready.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)."""
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -1193,6 +1253,7 @@ def log(msg: str) -> None:
)


@_in_development
def provision_device(
address: str,
org: Organization,
Expand Down