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
pyhubblenetworkpackage. 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>:
| Group | What it covers |
|---|---|
ble | Nearby devices — scanning, decrypting, validating over Bluetooth |
org | The Hubble Cloud — registering, listing, and reading device data |
sat | Satellite 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?
| Goal | Command |
|---|---|
| Check my setup is working | doctor |
| Watch nearby devices report | ble scan |
| Prove one device works end to end | ble validate |
| Register a new device and get its key | org register-device |
| See what's registered to my org | org list-devices |
| Read a device's history from the cloud | org get-packets |
| Receive packets from satellite | sat scan |
| Capture raw RF for offline analysis | sat 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
| Option | Description |
|---|---|
--timeout, -t | Stop after this many seconds (default: run until Ctrl+C). |
--count, -n | Stop after receiving N packets. |
--key, -k | Key to decrypt packets. Hex or base64, 16 or 32 bytes. |
--counter-mode | EID counter mode for AES-CTR packets: UNIX_TIME (default) or DEVICE_UPTIME. |
--days, -d | Days to search back when decrypting with the UNIX_TIME counter (default: 2). |
--period-exponent, -e | EID rotation period for AES-EAX packets as 2^n seconds, 0–15. Matches rot_exp in the device config. |
--show-failed-decryption | Also show packets the key can't decrypt, adding an ok/fail mark to each row. |
--network-id | Only show devices on one network (unencrypted protocol only). |
--ingest | Relay received packets to the Hubble Cloud. Requires --key and credentials. |
--org-id, --token | Credentials for --ingest (default to the HUBBLE_ORG_ID / HUBBLE_API_TOKEN env vars). |
--format, -o | tabular (default) or json. |
--payload-format | How payloads are rendered — see Payload format. |
--debug | Add 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:
- Validates input formats — the device key (hex or base64, 16- or 32-byte) and the device ID (standard 8-4-4-4-12 UUID).
- Loads credentials — from
--org-id/--tokenor theHUBBLE_ORG_IDandHUBBLE_API_TOKENenvironment variables. - Validates the organization credentials against the backend.
- Confirms the device is registered in your organization.
- Scans for BLE advertisements from Hubble-compatible devices.
- Decrypts a received packet with the provided key and reports the detected EID type (
UNIX_TIMEorDEVICE_UPTIME). - Ingests the packet into the backend and reads it back to confirm the full round trip succeeded.
| Option | Description |
|---|---|
--key, -k | Device key, used to test packet encryption (required). Accepts hex or base64, 16- or 32-byte. |
--device-id, -d | Device UUID, used to test the backend (required). |
--org-id | Organization ID (defaults to the HUBBLE_ORG_ID env var). |
--token | API token (defaults to the HUBBLE_API_TOKEN env var). |
--timeout, -t | BLE 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
| Option | Description |
|---|---|
--encryption, -e | AES-256-CTR, AES-128-CTR, AES-128-EAX, or NONE. |
--counter-source, -c | UNIX_TIME or DEVICE_UPTIME. |
--period-seconds | EID rotation period in seconds (AES-128-EAX + DEVICE_UPTIME only). |
--period-exponent | EID 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:
| Option | Description |
|---|---|
--timeout, -t | Stop after this many seconds (default: run until Ctrl+C). |
--count, -n | Stop after receiving N packets. |
--key, -k | Key to decrypt payloads. Hex or base64, 16 or 32 bytes. |
--counter-mode | EID counter source. Omit to auto-detect from the packets. |
--days, -d | Days searched around each packet's timestamp for the UNIX_TIME counter (default: 2). |
--show-failed-decryption | Also show packets the key can't decrypt, adding a DECRYPT column. |
--poll-interval | Seconds between polls of the receiver API (default: 2.0). |
--format, -o | tabular (default) or json. |
--payload-format | How payloads are rendered — see Payload format. |
--pluto-uri | Address of the Pluto SDR, if it isn't the default. |
--debug | Add 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:
| Option | Description |
|---|---|
--output | Where to write the file (default: an auto-generated timestamped name). |
--mock | Use the simulated receiver — Docker still required, but no Pluto SDR. |
--pluto-uri | Address of the Pluto SDR, if it isn't the default. |
--debug | Enable debug logging to stderr. |
recordcaptures the raw radio signal only — no decoding. The output is a NumPy.npyfile of IQ samples.signal-reportrecords 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, usesat scan --key.
Reading the output
Payload format
Commands that print packet data (ble scan, sat scan, org get-packets) take --payload-format:
| Value | Behavior |
|---|---|
auto | Printable ASCII shows as text, anything else as uppercase hex |
base64 | Encode payloads as base64 |
hex | Display payloads as hexadecimal |
string | Decode 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
blecommands:- 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
bluetoothgroup (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
satcommands: Docker Desktop (macOS/Windows) or Docker Engine (Linux) installed and running, and an ADALM-PLUTO connected over USB.sat mock-scan,sat record --mockandsat signal-report --mockneed 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
pyhubblenetworkREADME.
Troubleshooting
- macOS:
ble scancrashes instead of prompting for Bluetooth. You'll seeTermination Reason: Namespace TCCand a message about a missingNSBluetoothAlwaysUsageDescriptionkey. macOS refuses CoreBluetooth to any executable without that key in an Info.plist, and Homebrew'spython3binary 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. ble scanfinds 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.- Auth errors. Run
hubblenetwork doctor.validate-credentialsreports which environment accepted them and exits1if neither did, so it's safe in a script. Check thatHUBBLE_ORG_IDandHUBBLE_API_TOKENare exported in your current shell and come from the same environment. - Import errors. Make sure you installed into the same Python you're running (
python -m pip install pyhubblenetwork). Usingpipxavoids this entirely for CLI-only use. DockerError: Docker is not available. The Docker daemon isn't running. Start Docker Desktop (macOS/Windows) orsudo systemctl start docker(Linux).SatelliteError: No PlutoSDR detected. The container started but the SDR never connected. Ensure the ADALM-PLUTO is connected before runningsat scan, and that no other process is using it.sat scanhangs pulling the image. The first run fetchesghcr.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
pyhubblenetworkREADME.