Skip to main content
For the complete documentation index, see llms.txt.

Hubble Network developer documentation: integrate Bluetooth devices with terrestrial and satellite connectivity.

Hubble CLI

Watch nearby devices report over Bluetooth, receive their packets from satellite, decrypt payloads locally with a device key, and manage your fleet using the Hubble CLI — no embedded toolchain required.


Overview​

hubblenetwork is the command-line tool for Hubble Network devices. It runs on your laptop and pairs with whatever tooling you already use to flash firmware, which makes it the fastest way to answer the question that comes up on every integration: is my device actually working?

Go Further: The CLI ships in the pyhubblenetwork package. For the exhaustive flag reference, see the repository on GitHub.

Install and check your setup​

pip install pyhubblenetwork

For CLI-only use, pipx keeps it isolated from your other Python environments:

pipx install pyhubblenetwork

Requires Python 3.10 or later. To upgrade an existing install:

pip install --upgrade pyhubblenetwork

Set your credentials​

Export your Hubble organization ID and API token so you don't have to pass them on every command:

export HUBBLE_ORG_ID=<your org id>
export HUBBLE_API_TOKEN=<your api token>

Find both in your Hubble Dashboard under Developer Tools > API Tokens; see Authorization Scopes for the scope reference. Both values must come from the same environment — a Production org ID paired with a Sandbox token will be rejected.

Run doctor​

doctor is the setup step. It checks credentials, Bluetooth and Docker, and names the fix for anything broken:

hubblenetwork doctor
  ✓ Credentials    valid, PROD
✓ Bluetooth usage description present
✓ Docker reachable
- Receiver receiver image not pulled yet
First `sat scan` will pull it, which takes a minute.

Ready to go. · 3 ok · 1 skipped

When something is wrong, it says so and tells you what to do about it:

  ✗ Credentials    not set
Set both, or pass --org-id/--token:
export HUBBLE_ORG_ID=<your org id>
export HUBBLE_API_TOKEN=<your api token>
✓ Bluetooth usage description present
✗ Docker Docker is not available
Only `sat` commands need Docker.

Not ready. · 1 ok · 2 failed

doctor exits 1 when something needed is broken, so a script can gate on it. A skipped check is not a failure: it means the check doesn't apply on your platform, or couldn't be answered without doing real work — like pulling the satellite receiver image.

How commands are organized​

Every command lives inside a group, so it is always hubblenetwork <group> <command>:

GroupWhat it covers
bleNearby devices — scanning, decrypting, validating over Bluetooth
orgThe Hubble Cloud — registering, listing, and reading device data
satSatellite reception through a Pluto SDR

You don't have to remember which group a command is in. If you type one at the wrong level, the CLI finds it for you:

$ hubblenetwork list-devices

Usage: hubblenetwork [OPTIONS] COMMAND [ARGS]...
Try 'hubblenetwork --help' for help.

Error: No such command 'list-devices'.

Did you mean: hubblenetwork org list-devices

The same applies to missing arguments (they say how to find the value), unknown options (they list what the command accepts), and missing credentials (they name the environment variables and the flags). hubblenetwork --help prints every command with a one-line description, and every command takes --help for its own options.


What do you want to do?​

GoalCommand
Check my setup is workingdoctor
Watch nearby devices reportble scan
Prove one device works end to endble validate
Register a new device and get its keyorg register-device
See what's registered to my orgorg list-devices
Read a device's history from the cloudorg get-packets
Receive packets from satellitesat scan
Capture raw RF for offline analysissat record

Watch nearby devices report​

ble scan uses your laptop's built-in Bluetooth hardware to listen for Hubble beacon advertisements (service UUID 0xFCA6) and prints one line per packet as it arrives. No external hardware and no credentials needed — this is purely local.

hubblenetwork ble scan

It runs until you press Ctrl+C. This is the quickest way to confirm your firmware is beaconing at all.

Decrypt as you scan​

If you have the device key, pass it and payloads are decrypted locally as they arrive — no backend round trip. The key is hex or base64, 16 or 32 bytes:

hubblenetwork ble scan --key "a562a2f7e4c62bed52ab09633878f62b"

--counter-mode accepts UNIX_TIME (default) or DEVICE_UPTIME for devices using a counter-based EID rather than UTC time. For AES-128-EAX devices, --period-exponent sets the EID rotation period as 2^n seconds, matching rot_exp in your device configuration.

Relay what you receive​

Add --ingest to push the packets you scan into the Hubble Cloud, turning your laptop into a temporary gateway. This needs both a --key and credentials:

hubblenetwork ble scan --key "<key>" --ingest

Useful when you're testing away from network coverage and want the data to land in your organization anyway.

ble scan options​

OptionDescription
--timeout, -tStop after this many seconds (default: run until Ctrl+C).
--count, -nStop after receiving N packets.
--key, -kKey to decrypt packets. Hex or base64, 16 or 32 bytes.
--counter-modeEID counter mode for AES-CTR packets: UNIX_TIME (default) or DEVICE_UPTIME.
--days, -dDays to search back when decrypting with the UNIX_TIME counter (default: 2).
--period-exponent, -eEID rotation period for AES-EAX packets as 2^n seconds, 0–15. Matches rot_exp in the device config.
--show-failed-decryptionAlso show packets the key can't decrypt, adding an ok/fail mark to each row.
--network-idOnly show devices on one network (unencrypted protocol only).
--ingestRelay received packets to the Hubble Cloud. Requires --key and credentials.
--org-id, --tokenCredentials for --ingest (default to the HUBBLE_ORG_ID / HUBBLE_API_TOKEN env vars).
--format, -otabular (default) or json.
--payload-formatHow payloads are rendered — see Payload format.
--debugAdd the forensic columns EPOCH, TAG and SALT.

Test in Sandbox: For Sandbox access, the Hubble Connect mobile app is the supported way to relay device data.


Prove one device works end to end​

ble validate is the quickest way to answer "is my device working?". It walks the whole chain — your credentials, the device's registration, its advertisements, the key, and the cloud round trip — and stops at the first failure.

hubblenetwork ble validate \
--key "a562a2f7e4c62bed52ab09633878f62b" \
--device-id "3f4b2c0c-2d43-4cbe-9c1f-0a4c2d59e2a1"

The steps, in order:

  1. Validates input formats — the device key (hex or base64, 16- or 32-byte) and the device ID (standard 8-4-4-4-12 UUID).
  2. Loads credentials — from --org-id/--token or the HUBBLE_ORG_ID and HUBBLE_API_TOKEN environment variables.
  3. Validates the organization credentials against the backend.
  4. Confirms the device is registered in your organization.
  5. Scans for BLE advertisements from Hubble-compatible devices.
  6. Decrypts a received packet with the provided key and reports the detected EID type (UNIX_TIME or DEVICE_UPTIME).
  7. Ingests the packet into the backend and reads it back to confirm the full round trip succeeded.
OptionDescription
--key, -kDevice key, used to test packet encryption (required). Accepts hex or base64, 16- or 32-byte.
--device-id, -dDevice UUID, used to test the backend (required).
--org-idOrganization ID (defaults to the HUBBLE_ORG_ID env var).
--tokenAPI token (defaults to the HUBBLE_API_TOKEN env var).
--timeout, -tBLE scan timeout in seconds (default: 30).

If a step fails, the command prints targeted debugging tips. A common cause of a failed scan is a slow advertising interval combined with OS-level BLE scan optimizations — simply running the command again often resolves it.


Manage your fleet in the cloud​

The org group talks to the Hubble Cloud, so it needs credentials.

hubblenetwork org info                      # which org and environment am I on?
hubblenetwork org list-devices # everything registered
hubblenetwork org list-devices -n 20
hubblenetwork org list-devices -f json # machine-readable

hubblenetwork org get-packets <id> # last 7 days by default
hubblenetwork org get-packets <id> --days 30 --format csv
hubblenetwork org get-packets <id> -n 50 --debug

hubblenetwork org register-device # returns the new device's key
hubblenetwork org set-device-name <id> <name>
hubblenetwork org delete-device <id>

list-devices and get-packets stream rows as pages arrive, so the first rows appear in about a second rather than after the whole window downloads. A busy device can hold tens of thousands of packets; Ctrl+C stops early and still prints a summary, and --limit/-n caps the run. It always says how it stopped, never silently.

list-devices uses --format/-f, while get-packets uses --format/-o. Both accept the long form.

Registering devices​

org register-device returns a new device ID and its secret key. Keep the key — it is shown once, and your firmware needs it to encrypt.

hubblenetwork org register-device
hubblenetwork org register-device --encryption AES-128-EAX --counter-source DEVICE_UPTIME
OptionDescription
--encryption, -eAES-256-CTR, AES-128-CTR, AES-128-EAX, or NONE.
--counter-source, -cUNIX_TIME or DEVICE_UPTIME.
--period-secondsEID rotation period in seconds (AES-128-EAX + DEVICE_UPTIME only).
--period-exponentEID rotation period as 2^n seconds. The cloud accepts 10–15 (default 15, ≈9h).

--period-seconds and --period-exponent are mutually exclusive.

Go Further: Registering devices in bulk, or through the dashboard and API instead, is covered in Register Devices.


Receive satellite packets​

The sat group receives packets through a Pluto SDR. It runs a Docker container (ghcr.io/hubblenetwork/sdr-docker) that handles RF reception and decoding, polls that container's HTTP API, and streams decoded packets to stdout.

See the Test with Pluto SDR guide for step-by-step instructions to test your device beaconing on Satellite Network.

sat needs Docker running and an Analog Devices ADALM-PLUTO connected over USB. hubblenetwork doctor checks the Docker half.

hubblenetwork sat scan

Pass a device key to decrypt payloads locally as they arrive:

hubblenetwork sat scan --key "a562a2f7e4c62bed52ab09633878f62b"

Decryption uses the same AES-CTR scheme as BLE and supports both counter sources. The source is auto-detected from the packets and announced, unless --counter-mode is given. For UNIX_TIME, --days controls how many days around each packet's timestamp are searched. Packets the key cannot decrypt are hidden unless --show-failed-decryption is given.

sat scan handles the container for you: it verifies Docker, pulls the image if it isn't cached, starts the container privileged so it can reach USB, waits for the receiver API and for the SDR to connect, deduplicates packets by device ID and sequence number, then stops and removes the container on exit.

No hardware to hand? sat mock-scan streams fake packets and takes the same options:

hubblenetwork sat mock-scan

sat scan options​

sat scan and sat mock-scan share these:

OptionDescription
--timeout, -tStop after this many seconds (default: run until Ctrl+C).
--count, -nStop after receiving N packets.
--key, -kKey to decrypt payloads. Hex or base64, 16 or 32 bytes.
--counter-modeEID counter source. Omit to auto-detect from the packets.
--days, -dDays searched around each packet's timestamp for the UNIX_TIME counter (default: 2).
--show-failed-decryptionAlso show packets the key can't decrypt, adding a DECRYPT column.
--poll-intervalSeconds between polls of the receiver API (default: 2.0).
--format, -otabular (default) or json.
--payload-formatHow payloads are rendered — see Payload format.
--pluto-uriAddress of the Pluto SDR, if it isn't the default.
--debugAdd the forensic columns RS_CORR, SYM_MS and GAP_MS.

One-shot capture​

Alongside the live stream, two commands record for a fixed duration, save a single file, and exit. Both take a duration in seconds as their only argument.

# Capture 10 s of raw IQ samples to a .npy file
hubblenetwork sat record 10

# Record 10 s and save an RF signal-diagnostic report to a .txt file
hubblenetwork sat signal-report 10

Both accept:

OptionDescription
--outputWhere to write the file (default: an auto-generated timestamped name).
--mockUse the simulated receiver — Docker still required, but no Pluto SDR.
--pluto-uriAddress of the Pluto SDR, if it isn't the default.
--debugEnable debug logging to stderr.
  • record captures the raw radio signal only — no decoding. The output is a NumPy .npy file of IQ samples.
  • signal-report records IQ, then re-analyzes it offline into a plain-text link-health diagnostic: per-symbol timing/drift, channel-hopping validation, amplitude/SNR, and chipset metrics. It reports on signal quality and does not contain decoded packet payloads — to receive payloads, use sat scan --key.

Reading the output​

Payload format​

Commands that print packet data (ble scan, sat scan, org get-packets) take --payload-format:

ValueBehavior
autoPrintable ASCII shows as text, anything else as uppercase hex
base64Encode payloads as base64
hexDisplay payloads as hexadecimal
stringDecode payloads as UTF-8 (falls back to <invalid UTF-8>)

All four work with every output format, but the default differs, because a person and a program want different things. Tabular output defaults to auto, so a decrypted payload reads as T=21.4 rather than VD0yMS40. JSON and CSV default to base64 so the machine contract stays stable. An explicit --payload-format always wins.

Scan layout​

ble scan and sat scan print one line per packet with a signal bar, and close with a summary:

    TIME     RSSI       V EID              CTR/SEQ PAYLOAD
─────────────────────────────────────────────────────────────────────────────
✓ 00:06:40 -62 ███▏ 2 9c4e2ab77d3f0e1a 20320 T=21.4,B=87
✓ 00:06:43 -66 ██▉ 2 9c4e2ab77d3f0e1b 20321 T=21.4,B=87
✗ 00:06:49 -74 ██▏ 2 9c4e2ab77d3f0e1d - D307912C66BA4018E5
─────────────────────────────────────────────────────────────────────────────

4 packets · 3 decrypted, 1 failed · RSSI -62 to -74 dBm · 12s

The bar next to RSSI is signal strength: length is the magnitude, so you can watch it shrink as you walk away from a device. The ✓/✗ mark only appears with --show-failed-decryption, and it carries the state on its own, so the output still reads correctly without color.

Piping to a file​

Packet rows go to stdout and everything else — the scanning notice, detection lines, the summary — goes to stderr, so this captures data only:

hubblenetwork ble scan > packets.txt

The same split applies to org list-devices and org get-packets.

Pass --debug for the forensic columns: EPOCH, TAG and SALT on ble scan, RS_CORR, SYM_MS and GAP_MS on sat scan, EPOCH, CTR and SEQ on org get-packets.

Terminals that can't do box-drawing​

Not every terminal can render ─ and █. Pass --ascii (or set HUBBLE_ASCII=1) for a pure-ASCII rendering with identical column widths:

  TIME     RSSI       V EID              CTR/SEQ PAYLOAD
---------------------------------------------------------------------------
15:50:13 -62 ###= 0 2030405 300 0A0B0C0D0E0F
---------------------------------------------------------------------------

1 packets | RSSI -62 to -62 dBm | 0s

Encoding problems are detected automatically; you only need the flag when a CJK terminal configuration renders the glyphs double-width and shears the columns. --no-ascii forces the Unicode rendering if the detection is wrong for you.

Color is a separate axis: --no-color, NO_COLOR=1, or a non-TTY stdout all disable it, and FORCE_COLOR=1 keeps it on where a pipe would otherwise strip it (useful in CI). Both flags work on any command, before or after the subcommand.


Requirements​

  • Python 3.10 or later (3.11/3.12 recommended).
  • Bluetooth, for the ble commands:
    • macOS: CoreBluetooth. Run from a real terminal app and grant it Bluetooth access when prompted.
    • Linux: BlueZ required; your user needs permission to access the BLE adapter — often membership of the bluetooth group (sudo usermod -a -G bluetooth $USER, then log out and back in).
    • Windows: a compatible BLE stack/adapter.
  • Docker and a Pluto SDR, for the sat commands: Docker Desktop (macOS/Windows) or Docker Engine (Linux) installed and running, and an ADALM-PLUTO connected over USB. sat mock-scan, sat record --mock and sat signal-report --mock need Docker but no SDR.

hubblenetwork doctor reports on all of this.


Also a Python library​

Everything the CLI does is available as an importable SDK, for when you need to script something the commands don't cover.

import os
from hubblenetwork import Organization, ble, decrypt

org = Organization(
org_id=os.environ["HUBBLE_ORG_ID"],
api_token=os.environ["HUBBLE_API_TOKEN"],
)

new_dev = org.register_device() # returns a Device, with its key
for d in org.iter_devices(): # streams as pages arrive
print(d.id, d.name)

for pkt in ble.scan(timeout=5.0):
plaintext = decrypt(new_dev.key, pkt)
if plaintext:
print(plaintext.payload)

Unlike the CLI, the SDK does not read the environment. Organization() requires its credentials explicitly.

iter_devices() and iter_packets() are generators that yield as each API page arrives; list_devices() and retrieve_packets() are list() wrappers over them. BLE functions have sync and async variants — ble.scan() / ble.scan_async(). The sat module exposes sat.scan(), sat.record() and sat.signal_report(), and manages the Docker container for you.

Go Further: The full library surface is documented in the pyhubblenetwork README.


Troubleshooting​

  1. macOS: ble scan crashes instead of prompting for Bluetooth. You'll see Termination Reason: Namespace TCC and a message about a missing NSBluetoothAlwaysUsageDescription key. macOS refuses CoreBluetooth to any executable without that key in an Info.plist, and Homebrew's python3 binary has no Info.plist at all. Run from a real terminal app (Terminal, iTerm) rather than an embedded IDE shell, and grant it Bluetooth under System Settings → Privacy & Security → Bluetooth. BLE scanning must run in a GUI session, not over SSH.
  2. ble scan finds nothing. Verify BLE permissions and adapter state, and try a longer --timeout. Slow advertising intervals plus OS-level scan optimizations mean a second attempt often succeeds.
  3. Auth errors. Run hubblenetwork doctor. validate-credentials reports which environment accepted them and exits 1 if neither did, so it's safe in a script. Check that HUBBLE_ORG_ID and HUBBLE_API_TOKEN are exported in your current shell and come from the same environment.
  4. Import errors. Make sure you installed into the same Python you're running (python -m pip install pyhubblenetwork). Using pipx avoids this entirely for CLI-only use.
  5. DockerError: Docker is not available. The Docker daemon isn't running. Start Docker Desktop (macOS/Windows) or sudo systemctl start docker (Linux).
  6. SatelliteError: No PlutoSDR detected. The container started but the SDR never connected. Ensure the ADALM-PLUTO is connected before running sat scan, and that no other process is using it.
  7. sat scan hangs pulling the image. The first run fetches ghcr.io/hubblenetwork/sdr-docker:latest, which may take a minute on a slow connection. Later runs use the cached image.

Go Further: For the full troubleshooting reference, see the pyhubblenetwork README.