ESP-IDF Integration Guide

This guide walks through integrating the Hubble Network dual-stack Satellite and BLE application with Espressif’s IoT Development Framework (ESP-IDF).

By the end of this guide you will know how to:

  • Integrate the Hubble Satellite and Terrestrial (BLE) network stacks into an ESP-IDF application.

  • Obtain ephemeris data and use pass prediction to schedule satellite transmissions.

  • Transmit data to the Hubble satellite network from an ESP32 device.

Supported Devices and SDK Version

ESP-IDF Compatibility

The following ESP-IDF versions have been validated with each Hubble Device SDK release. If you require support for a different version, contact us.

Hubble Device SDK

ESP-IDF

latest (main)

v6.1

v3.1.0

v6.0, v6.1

v3.0.0

v6.0

Supported Devices

SoC

IDF_TARGET

Notes

ESP32-C6

esp32c6

RISC-V, 20 dBm integrated PA

ESP32-S31

esp32s31

Dual-core RISC-V, 20 dBm integrated PA

Prerequisites

Before starting, ensure you have the following:

  • A supported development kit or custom board with a power amplifier (PA) or front-end module (FEM) capable of at least +20 dBm transmit output power. The Hubble satellite link budget requires this minimum output to reach the network reliably.

  • An antenna tuned to the Hubble satellite frequency band, connected to the RF output of the PA or FEM.

  • A Hubble account and API key to fetch ephemeris data for pass prediction.

  • ADALM-PLUTO SDR (optional, recommended for custom board bring-up): used to verify RF output at the physical layer. Available from common distributors such as DigiKey and Mouser. See the ADALM-PLUTO product page for details.

Create your Hubble Account

A Hubble account is required to access the Hubble Dashboard and generate an API key for fetching ephemeris data used in pass prediction.

  1. Create an account at the Hubble Dashboard.

  2. Once logged in, follow the Hubble Platform API documentation to authenticate and generate your API key.

Keep your API key accessible. It is used later in this guide when fetching orbital parameters for pass prediction.

SDK Setup

Install ESP-IDF

Follow the ESP-IDF Getting Started guide to install a supported ESP-IDF version (see ESP-IDF Compatibility) and all required dependencies. Once installed, source the export script to set up the environment:

. $IDF_PATH/export.sh

Clone the Hubble Device SDK

Clone the SDK alongside your application:

git clone https://github.com/HubbleNetwork/hubble-device-sdk.git

Add the Hubble Port as an ESP-IDF Component

Register the ESP-IDF port as a component by setting EXTRA_COMPONENT_DIRS in your application’s CMakeLists.txt:

set(EXTRA_COMPONENT_DIRS /path/to/hubble-device-sdk/port/esp-idf/)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(your-app LANGUAGES C)

See samples/esp-idf/sat-dual-stack/CMakeLists.txt for a complete reference.

Fetch the Satellite PHY Blob

Depending on the SoC and ESP-IDF version, the shipped libphy may not include the PHY API required by the Satellite Network module. In that case, swap in the Early Access (EA) blob from Espressif before building:

SoC

PHY blob

Required for version

IDF_TARGET

ESP32-C6

libphy_C6_20260317_c83212e.zip

ESP-IDF < v6.1

esp32c6

ESP32-S31

libphy_S31_20260728_e963440.zip

All ESP-IDF

esp32s31

  1. Download the PHY blob for your SoC from the table above.

  2. Unzip it and copy the extracted *.a files into your ESP-IDF installation, replacing <IDF_TARGET> with your target:

    unzip "<blob>.zip"
    cp *.a $IDF_PATH/components/esp_phy/lib/<IDF_TARGET>/
    

Project Configuration

sdkconfig.defaults

Enable the Hubble dual-stack by adding the following to your sdkconfig.defaults:

# Hubble Network
CONFIG_HUBBLE_BLE_NETWORK=y
CONFIG_HUBBLE_SAT_NETWORK=y

# Set to your oscillator's PPM rating (check your crystal datasheet)
CONFIG_HUBBLE_SAT_NETWORK_DEVICE_TDR=10

CONFIG_HUBBLE_SAT_NETWORK_DEVICE_TDR sets the clock drift rate in parts per million (PPM). See Clock Drift Compensation for details.

For the full set of available options, see Configuration Options.

Other common options for a dual-stack application:

# Bluetooth
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y

# Hubble uses legacy advertising, not extended advertising
CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=n

# Optional: enable logging
CONFIG_LOG_VERSION_2=y

Register Application Components

In your main/CMakeLists.txt, declare the required component dependencies:

idf_component_register(SRCS ${YOUR_APP_SOURCES}
                       PRIV_REQUIRES bt mbedtls nvs_flash esp_timer hubblenetwork-sdk
                       INCLUDE_DIRS ".")

bt and hubblenetwork-sdk are required for the dual-stack. nvs_flash is required by NimBLE. esp_timer is used in the reference sample for pass scheduling, however, any timer peripheral works. mbedtls is only required if your application decodes a base64-encoded key at runtime, as described in Handling the Hubble Device Key.

Preparing for Satellite Transmission

The satellite stack requires three inputs before it can schedule and transmit:

  • Unix time: used for pass prediction and key derivation

  • Device location: latitude and longitude, used to compute satellite passes

  • Orbital parameters: satellite ephemeris data describing each satellite’s orbit

How these are provisioned is up to the application.

Time

Many approaches work: BLE provisioning from a phone, NTP over Wi-Fi, GPS, an RTC, or reading from persistent storage across reboots. See Time Management for best practices and trade-offs.

Location

If the device is deployed at a fixed location, latitude and longitude can be hard-coded directly in firmware:

struct hubble_sat_device_pos device_pos = {
    .lat = 47.6,
    .lon = -122.3,
};

For mobile devices, location can be obtained from an onboard GPS module if present, or provisioned at runtime from a companion app. For example, delivered over BLE from a phone/gateway that has GPS access. The Dual-Stack Companion Tool is a host-side reference for this flow: it provisions time, location and orbital parameters over BLE in one step.

Orbital Parameters

Orbital parameters describe each satellite’s orbit and are used by hubble_sat_next_pass_get() to predict when a satellite will be visible from the device location.

Since Hubble satellites are station-keeping, orbital parameters are stable enough to be baked into firmware at build time. Use the tools/orbital_params_fetch.py helper to fetch current parameters from the Hubble API and generate a sat_params.c file ready to compile into your application:

export HUBBLE_API_TOKEN=<your-api-token>
python tools/orbital_params_fetch.py path/to/output

See Orbital Parameters (Satellites information) for details on the generated format and how to register the array with the SDK.

Handling the Hubble Device Key

Every device authenticates to the Hubble Network with a cryptographic key issued by the Hubble Platform API. The key is either AES-128 or AES-256, and its size must match the size the SDK was built for (CONFIG_HUBBLE_NETWORK_KEY_128 or CONFIG_HUBBLE_NETWORK_KEY_256, exposed in bytes as CONFIG_HUBBLE_KEY_SIZE). Requesting a key of one size and building for the other fails at initialization.

The key reaches the SDK through hubble_init():

int hubble_init(uint64_t initial_time, const void *key);

Important

The SDK stores the pointer you hand it and does not copy the key material. The buffer must stay valid for as long as the SDK is in use, so it cannot be a stack or otherwise temporary buffer. Give it static storage duration.

Passing NULL defers the key; supply it later with hubble_key_set() before requesting any advertisement.

There are two common ways to get a key onto a device. Not every platform supports both, and some offer further options; see the platform-specific notes that follow.

Embedding the Key at Build Time

This is the recommended method for production applications. It is the simplest option to automate in a build, and the only one that provisions the initial time in the same step. The key material still lives in the firmware image, so treat the image as a secret.

To embed the key at build time, point embed_key_time.py at <your_src_dir>/:

python tools/embed_key_time.py -b /path/to/key -o <your_src_dir>/

This writes <your_src_dir>/key.c and <your_src_dir>/time.c, defining master_key and unix_time. Both symbols are declared static, so the generated .c files are meant to be included into the translation unit that uses them rather than compiled separately. Include them into the translation unit that calls hubble_init():

#include "key.c"
#include "time.c"

/* ... */

err = hubble_init(unix_time, master_key);

See any of the ble-beacon samples for a complete example.

Supplying the Key from Project Configuration

Use this for debugging and testing only. Production applications should embed the key at build time instead.

Rather than generating source, the application can carry the key as a base64-encoded string in its own build configuration and decode it at runtime before calling hubble_init(). Decode into a buffer with static storage duration, and verify that the decoded length matches CONFIG_HUBBLE_KEY_SIZE before using it.

This supplies only the key. The initial time passed to hubble_init() has to be obtained separately, by whatever means suits the device: a real-time clock, a network time source, or provisioning from a companion application.

Note

The SDK does not define a configuration option for the key. Declare one in your own application and name it to suit your project; the option names used in the platform notes that follow are illustrative.

To supply the key from configuration, declare a string option in your component’s Kconfig.projbuild and set it in sdkconfig.defaults or through idf.py menuconfig:

# Hubble Device Key
CONFIG_HUBBLE_DEVICE_KEY="your-device-key"

Decode it before handing it to hubble_init(). This is the case that requires mbedtls in your component’s PRIV_REQUIRES:

#include "mbedtls/base64.h"

static uint8_t _hubble_key[CONFIG_HUBBLE_KEY_SIZE];

/* ... */

if (strlen(CONFIG_HUBBLE_DEVICE_KEY) != 0) {
    size_t outlen = 0;

    ret = mbedtls_base64_decode(
        _hubble_key, sizeof(_hubble_key), &outlen,
        (const unsigned char *)CONFIG_HUBBLE_DEVICE_KEY,
        strlen(CONFIG_HUBBLE_DEVICE_KEY));

    if (ret != 0) {
        ESP_LOGE(APP_TAG, "Invalid key provided!");
        return;
    }

    if (outlen != sizeof(_hubble_key)) {
        ESP_LOGE(APP_TAG, "Invalid key length provided!");
        return;
    }
}

/* unix_time_ms is obtained separately, as described above. */
ret = hubble_init(unix_time_ms, _hubble_key);

See samples/esp-idf/sat-dual-stack for a complete example.

Initializing the Hubble Device SDK

Once time, location, and orbital parameters are available, initialize the SDK before calling any other Hubble API:

/*
 * At this point unix_time_ms, device_pos, and orb_params are assumed to be
 * valid. Either baked into firmware or received via BLE provisioning.
 */
err = hubble_init(unix_time_ms, master_key);
if (err != 0) {
    LOG_ERR("Failed to initialize Hubble Device SDK (err %d)", err);
    return err;
}

err = hubble_sat_satellites_set(orb_params, orb_params_count);
if (err != 0) {
    LOG_ERR("Failed to set orbital parameters (err %d)", err);
    return err;
}

hubble_init() takes the current Unix time in milliseconds and a pointer to the master key. hubble_sat_satellites_set() registers the orbital parameters array with the SDK.

Warning

  • The key buffer MUST remain valid for the lifetime of SDK usage. The SDK stores the pointer directly and does not copy the key. Do not use a stack or temporary buffer.

  • Unix time must be non-zero. Passing 0 in CONFIG_HUBBLE_COUNTER_SOURCE_UNIX_TIME mode returns an error.

  • The orbital parameters array MUST remain valid for as long as pass prediction is used. The SDK stores a pointer and does not copy the array.

On ESP-IDF, nvs_flash_init() must be called before initializing the NimBLE stack and registering GATT characteristics and services:

/* NVS flash init, dependency of NimBLE stack */
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
    ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());
    ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

ret = nimble_port_init();
if (ret != ESP_OK) {
    ESP_LOGE(BLE_TAG, "Failed to init NimBLE (rc=%d)", ret);
    return ret;
}

/*
 * Call your BLE setup function here. For example:
 * Register GATT services, configs, callbacks, NimBLE host task, etc.
 */

The Pass Prediction Loop

With the SDK initialized, the application enters the main dual-stack loop: compute the next satellite pass, beacon over BLE while waiting, stop BLE, transmit to the satellite, then repeat.

Compute the Next Pass

for (;;) {
    now_ms = hubble_time_get();

    err = hubble_sat_next_pass_get(now_ms, &device_pos, &pass_info);
    if (err != 0) {
        LOG_ERR("Failed to get next pass (err %d)", err);
        return err;
    }

    /* If the pass has already started, find the next one. */
    if (pass_info.start <= now_ms) {
        err = hubble_sat_next_pass_get(
            pass_info.start + pass_info.duration,
            &device_pos, &pass_info);
        if (err != 0) {
            LOG_ERR("Failed to get next pass (err %d)", err);
            return err;
        }
    }

hubble_sat_next_pass_get() returns the soonest pass visible from the device location. If pass_info.start is already in the past, a pass is in progress, skip it and compute the next one by searching from after its end (pass_info.start + pass_info.duration).

Beacon over BLE While Waiting

Schedule a one-shot timer for the pass window and start BLE advertising while the device waits. The example below uses esp_timer; any timer peripheral that can schedule a future callback works equally well.

sat_wait_us = (pass_info.start - now_ms) * US_PER_MS;
esp_timer_start_once(_sat_timer, sat_wait_us);

/*
 * Get the Hubble beacon payload using hubble_ble_advertise_get()
 * and start BLE advertising with ble_gap_adv_start().
 */
ble_adv_start();

/*
 * Block and wait until the pass timer fires. Common approaches are
 * a semaphore, task notification, or event flag.
 */
block_until_timer_expires();

The timer callback signals the waiting task when the pass window is overhead.

Transmit to the Satellite

Stop advertising, then build and send the packet:

    /* Stop advertising to release the radio. */
    ble_gap_adv_stop();

    err = hubble_sat_packet_get(&packet, NULL, 0);
    if (err != 0) {
        ESP_LOGE(APP_TAG, "Failed to build packet (err %d)", err);
        return;
    }

    /* Blocking call. Retries are handled internally by the SDK */
    err = hubble_sat_packet_send(&packet, HUBBLE_SAT_RELIABILITY_NORMAL);
    if (err != 0) {
        ESP_LOGE(APP_TAG, "Failed to send packet (err %d)", err);
        return;
    }
} /* end while loop, back to compute the next pass, re-enable bluetooth, and beacon */

hubble_sat_packet_send() is blocking. It returns only after the full transmission sequence completes, including all retries. See Reliability and Power Consumption for guidance on reliability modes and their effect on power consumption.

Building and Flashing

Set the target, build, flash, and open the serial monitor. Replace <IDF_TARGET> with the value for your SoC from Supported Devices:

idf.py set-target <IDF_TARGET>
idf.py build flash monitor

Verifying the Application

Expected Log Output

Enable logging by adding the following to your sdkconfig.defaults:

CONFIG_LOG_VERSION_2=y

After a successful hubble_init() call, the SDK logs:

I (xxx) hubblenetwork: Hubble Device SDK initialized (HDCV:1.0/E:256/CS:UT/RP:S86400/N:TS/TV:0/SV:0)

Note

If using ESP-IDF Log v1 (CONFIG_LOG_VERSION_1=y), the log level prefix and timestamp are omitted and only the plain message appears, e.g.:

Hubble Device SDK initialized (HDCV:1.0/E:256/CS:UT/RP:S86400/N:TS/TV:0/SV:0)

At debug level, once pass prediction runs and a transmission is scheduled:

D (xxx) hubblenetwork: Time drift since last sync: 20000 ms
D (xxx) hubblenetwork: Number of additional retries due TDR: 1
D (xxx) hubblenetwork: Number of retries: 9 - interval: 20 seconds

After hubble_sat_packet_send() completes:

I (xxx) hubblenetwork: Hubble Satellite packet sent

If this line appears without any preceding error from the hubblenetwork tag, the device has successfully transmitted to the satellite network.

Verify BLE

Use the SDK’s scan script to confirm BLE advertising is working:

pip install -r tools/requirements-scan.txt
python tools/scan.py --key "<your-device-key>"

Verify Satellite RF

To verify the satellite RF output before a live pass, use an ADALM-PLUTO SDR and the pyhubblenetwork scan tool. See the RF Verification with an SDR section in Next Steps below for full instructions.

Troubleshooting

hubble_init returns an error

Symptom: <wrn> hubblenetwork: Failed to set Unix Epoch time

Cause: unix_time_ms passed to hubble_init() is 0. The SDK requires a valid non-zero Unix timestamp.

Fix: Ensure time is provisioned (over BLE, NTP, GPS, or RTC) before calling hubble_init(). See the Preparing for Satellite Transmission section in this guide.

—

Symptom: <wrn> hubblenetwork: Failed to set key

Cause: The key buffer is NULL, zero-length, or the wrong size for the configured key type.

Fix: Verify the key is correctly decoded and its length matches the configured key size.

Pass prediction returns an error

Symptom: <wrn> hubblenetwork: Hubble Satellite next pass get: no satellites configured

Cause: hubble_sat_satellites_set() was not called before hubble_sat_next_pass_get().

Fix: Call hubble_sat_satellites_set() with a valid orbital parameters array immediately after hubble_init().

—

Symptom: <wrn> hubblenetwork: Hubble Satellite next pass get: no pass found

Cause: No satellite pass is visible from the given location within the search window. Most commonly caused by incorrect device coordinates or a stale Unix timestamp.

Fix: Verify that device_pos.lat and device_pos.lon are correct and that unix_time_ms reflects current wall-clock time.

—

Symptom: Firmware appears to hang or stall inside hubble_sat_next_pass_get().

Cause: The pass prediction algorithm iterates forward orbit-by-orbit until it finds a pass. If the input time is near zero (e.g. Unix time was passed in seconds instead of milliseconds), or if device_pos.lat and device_pos.lon are invalid, the loop can spin through thousands of orbits before returning.

Fix: Confirm unix_time_ms is in milliseconds and that device_pos.lat and device_pos.lon hold the actual device coordinates.

Build fails with missing PHY symbols

Symptom: Linker error referencing undefined symbols.

Cause: The EA PHY blob required for your SoC and ESP-IDF version was not swapped into the ESP-IDF installation.

Fix: Follow the Fetch the Satellite PHY Blob steps in the SDK Setup section.

NimBLE fails to initialize

Symptom: nimble_port_init() returns an error.

Cause: nvs_flash_init() was not called before initializing the NimBLE stack.

Fix: Ensure nvs_flash_init() is called before nimble_port_init().

Next Steps

RF Verification with an SDR

Before waiting for a live satellite pass, you can verify that your device is transmitting a valid Hubble packet at the physical layer using an ADALM-PLUTO product page and the pyhubblenetwork Python library.

Install the library:

pip install pyhubblenetwork

Connect the ADALM-PLUTO near the device antenna and run the scanner with your device key to decode captured packets in real time:

hubblenetwork sat scan --key "<your-device-key>"

A successfully decoded packet confirms the RF output, packet framing, channel hopping sequence, and PA/FEM sequencing are all correct. For the full list of available commands, run:

hubblenetwork --help

See the pyhubblenetwork repository for detailed usage and setup instructions.

Viewing Upcoming Passes

Use the Hubble Pass Explorer to see when the next satellite pass is predicted for your location. This is useful to cross-check pass prediction results from the device and to plan test windows.

Dashboard Verification

Once a live satellite pass has occurred and hubble_sat_packet_send() returned without error, after the downlink data is successful, log into the Hubble Dashboard Devices Page to confirm the packet was received by the network. A packet appearing on the dashboard is end-to-end proof that the device is operational on the Hubble satellite network.

Further Reading