Pad Link¶
The pad-link library turns the rocket into a small Bluetooth Low Energy (BLE) peripheral so a nearby ground station (usually a launchrail computer or a laptop running a BLE app) can read the live status while the rocket is sitting on the launch pad.
It is not a flight downlink. The BLE radio range is tens of metres at best, and once the motor lights, the rocket will leave that bubble within a second or two. The link is meant for the minutes between firmware booted and launch button pressed.
If you have used a fitness tracker or a smart watch, the design is the same: the rocket advertises (broadcasts “I’m here”), the ground station scans and connects, and then it reads or subscribes to a handful of characteristics (think: named variables on the device).
When to use it¶
Use the pad link when you want any of the following:
Watch the state machine on the pad (“did it really go IDLE -> ARMED when I flipped the arm switch?”) without a USB cable.
Sanity-check the last raw IMU and baro readings before flight.
Display computed altitude, velocity, and orientation on a phone or laptop during setup.
Identify which board is on the pad (e.g.
sensor_board_v2/rp2040vs. an ESP32-S3 variant) when you have several flight computers in a backpack.
Do not use it for:
In-flight telemetry. Use the telemetry library (HC-12 today, more long-range backends later). See Telemetry.
Sending commands to the rocket. The pad-link is read-only by design. The only way to influence the rocket from the ground is the arm switch and the launch wire.
Connection lifecycle¶
The rocket is the peripheral; the ground station is the central. The lifecycle is symmetric and forgiving:
pad_link_init()brings up the BLE host stack and starts connectable advertising.Any central that scans for the advertised name (default
AURORA-Rocket) sees the rocket and may connect.While connected, the central reads characteristics directly or subscribes to notifications and is pushed every new value.
When the central disconnects, deliberately, or because the rocket just travelled 200 m up in two seconds, the disconnect callback re-starts advertising. No state on the rocket cares whether anyone is listening.
The flight code never waits on the link. A central showing up an hour after boot, or never showing up at all, is the same code path for the rocket.
GATT service¶
All data is grouped under one custom primary service. A service is just a container. The interesting parts are its characteristics. You can think of each characteristic as a named, typed register the central can read.
128-bit UUIDs are used throughout. The service UUID lets a central filter scan results to “AURORA rockets only”:
Name |
UUID |
Access |
|---|---|---|
Service |
|
- |
Board identifier |
|
read |
SM state |
|
read, notify |
Raw sensors |
|
read, notify |
Computed kinematics |
|
read, notify |
SM type |
|
read |
Read vs. notify¶
Every characteristic supports read: the central asks once and gets the
current value. The three live ones (state, raw sensors, computed) also
support notify: the central writes “1” to the characteristic’s CCC
descriptor and then receives a push every time the rocket calls
pad_link_publish_sm(). Notifications are cheaper than polling at the
same rate and arrive immediately on each update.
Characteristics in detail¶
Board identifier: UTF-8 string, length-bounded by
CONFIG_AURORA_PAD_LINK_BOARD_ID. Defaults to the Zephyr board name
(e.g. sensor_board_v2_rp2040). Read once; it doesn’t change.
SM type: uint8_t. Identifies which state machine
implementation the firmware is running, so the central knows how to
decode the SM-state byte. 0 is the simple 9-state SM
(see State Machine); future implementations append new values. Read
once after connecting.
SM state: uint8_t. Current flight state, as defined by the
active SM implementation. With SM type = 0 (simple), the byte maps
to IDLE, ARMED, BOOST, BURNOUT, APOGEE, MAIN, REDUNDANT, LANDED,
ERROR.
Raw sensors: packed, little-endian struct. Most recent samples
from the IMU and barometer in their native Zephyr sensor_value
format (val1 = whole part, val2 = micro-fraction).
Offset |
Size |
Type |
Field |
|---|---|---|---|
0 |
4 |
|
|
4 |
12 |
|
|
16 |
12 |
|
|
28 |
12 |
|
|
40 |
12 |
|
|
52 |
4 |
|
|
56 |
4 |
|
|
60 |
4 |
|
|
64 |
4 |
|
|
To recover a physical value, combine the two halves:
value = val1 + val2 * 1e-6.
Computed kinematics: packed, little-endian struct. The outputs of the state-machine input pipeline (after Kalman filtering, if enabled). 32-bit floats keep the payload small; the precision is far more than the central needs for a status screen.
Offset |
Size |
Type |
Field |
|---|---|---|---|
0 |
4 |
|
|
4 |
4 |
|
|
8 |
4 |
|
|
12 |
4 |
|
|
16 |
4 |
|
|
20 |
4 |
|
|
24 |
4 |
|
|
Rocket-side integration¶
For the typical sensor-board application, the integration is two calls
in main.c and one Kconfig:
#include <aurora/lib/pad_link.h>
#include <aurora/lib/state/state.h>
int main(void)
{
/* ... other init ... */
(void)pad_link_init(); /* non-fatal if it fails */
return 0;
}
/* In the state-machine loop, right after sm_update(): */
struct sm_inputs sm_in;
sm_get_inputs(&sm_in);
pad_link_publish_sm(sm_get_state(), sm_get_type(), &sm_in);
That is the whole rocket-side contract. The raw-sensor characteristic fills itself in: the library subscribes to the IMU and baro zbus channels internally and snapshots every published sample.
Kconfig¶
Symbol |
Default |
Purpose |
|---|---|---|
|
n |
Enable the library. Pulls in |
|
|
Name in the advertising payload: what the central displays when scanning. |
|
|
Value of the board-identifier characteristic. Override per hardware revision. |
A board configuration also has to enable a working BLE controller for
the chip in question (Bluetooth HCI driver, controller stack, …). Those
configs live in the board .conf files because they vary per SoC.
Ground-station example¶
The central side is whatever can speak BLE: a phone running nRF
Connect, a laptop with bluetoothctl, the launchrail’s own
firmware, or a small Python script. The shape of the code is always
the same: scan, connect, discover, then read or subscribe.
A runnable Python reference using Bleak ships in-tree as pad_link_central_example.py. It scans by service UUID, reads the board id and SM type, then either subscribes to notifications or polls reads on a timer:
$ pip install bleak
$ python3 aurora/tools/pad_link_central_example.py
connected to sensor_board_v2_rp2040 (sm_type=0)
state: IDLE
t=12345 alt=+0.1 v=+0.0 ypr=+0.3/-0.1/+0.0
...
The mode is selectable from the command line:
--mode notify(default): subscribe to the SM-state and computed characteristics. The rocket pushes a notification on every SM tick. Lowest latency, highest radio traffic.--mode poll --interval 1.0: skip notifications andreadthe characteristics on a timer. The central drives the cadence; the rocket never pushes. Use this for a low-rate status display where every SM-tick update would be wasted.--durationcaps how long the script stays connected (default 60 s).
Read the file itself for the wire-format decoding. The header docstring lists prerequisites, gotchas, and pointers on extending it into a fuller dashboard.
A few notes for the first time you wire this up:
BLE characteristic reads are limited by the negotiated MTU. With the default 23-byte ATT MTU the raw-sensor (68 B) and computed (28 B) payloads still arrive correctly, because the central transparently issues long reads. Negotiating a larger MTU (
BleakClient(..., mtu=247)) makes notifications and reads a single round-trip and is worth doing.The first read of
sm_statebefore the state-machine has run even once returns 0 (IDLE). The values stabilise within milliseconds of boot.Reconnecting after a disconnect just works: the rocket re-advertises immediately. Centrals typically remember the address and reconnect on the next scan.
Failure modes¶
The library is designed so nothing it does can break the flight code:
bt_enable()returns an error (no controller, bad config, …) ->pad_link_init()logs and returns the error.main()ignores it. The flight loop runs normally; you simply won’t see any advertising.Notification send fails (queue full, central too slow) -> the failure is silently dropped. The next call to
pad_link_publish_sm()will try again with fresher data.Central disconnects mid-flight ->
disconnectedcallback re-arms advertising. The next time a central is in range, it can reconnect. The flight loop is unaffected.
There is intentionally no retry logic, no buffering for offline centrals, and no fail-safe mode. The pad link is allowed to be missing, reconnecting and imperfect. That is what the SD-card log and the HC-12 downlink are for.
API Reference¶
-
int pad_link_init(void)¶
Bring up the BLE stack and start advertising.
Idempotent within a boot: returns the
bt_enableerror on failure and does not retry. The caller should treat a failure as “no pad link this boot” and continue.- Return values:
0 – on success.
<0 – propagated from
bt_enable.
-
void pad_link_publish_sm(enum sm_state state, enum sm_type type, const struct sm_inputs *inputs)¶
Publish the latest SM state and computed kinematics.
Updates the read snapshot and, for any subscribed central, fires a GATT notification. Safe to call from the SM thread; never blocks.
typelets the central pick the rightsm_stateenum mapping- Parameters:
state – Current flight state.
type – Active state machine implementation ID (see sm_get_type). Constant across a boot.
inputs – Snapshot returned by
sm_get_inputs.