State Machine

The state machine library provides a generic interface for initializing, updating, and querying the flight state. As almost everything else in AURORA, it features a dynamic selection of state machine types via Kconfig CONFIG_AURORA_STATE_MACHINE_TYPE. Currently only the simple state machine is implemented and it uses the following flight sequence:

Simple State Machine

The simple state machine implementation defines a 9-state flight sequence driven by sensor thresholds.

states
Signals

Signal

Comment

ARM

ARM signal from extern. Arms the pyro channels as well

DISARM

DISARM signal from extern. Disarms the pyro channels as well

Sensor Readings

Sensor Reading

Comment

TAB

Acceleration needed to go from ARMED to BOOST

TH

Altitude needed to go from ARMED to BOOST

TBB

Acceleration needed to go from BOOST to BURNOUT

TM

Altitude needed to go from APOGEE to MAIN

TL

Velocity needed to signal LANDED

Timers

Timer

Comment

DTAB

Time that T_AB and T_H shall be asserted for

DTL

Time that T_L shall be asserted

Timeouts

Timeout

Comment

TOA

Timeout for APOGEE state

TOR

Timeout for REDUNDANT state

State transitions are also driven by sensor thresholds configured via Kconfig (boost acceleration, main descent height, apogee timeout, etc.).

Remove Before Flight

The ARM/DISARM signal normally comes from the application, through sm_inputs.armed. Setting CONFIG_AURORA_STATE_MACHINE_RBF takes it from hardware instead: the state machine reads the mechanical safety lock (the key or shorting plug that is pulled off the rocket on the pad) and substitutes the pin for armed on every update. Whatever the application writes into that field is ignored, so software arming and the physical “is the streamer still in?” check can never disagree.

The GPIO is selected by the auxspace,rbf chosen node and follows the usual “safe when made” convention:

/ {
   chosen {
      auxspace,rbf = &rbf_in;
   };

   buttons {
      compatible = "gpio-keys";

      rbf_in: rbf_in {
         gpios = <&gpio1 6 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
         label = "RBF Button";
      };
   };
}

“Remove Before Flight”-Plug

Line

Vehicle

installed

asserted

SAFE: machine held in IDLE

removed

deasserted

ARMED: machine free to leave IDLE

Every edge restarts a debounce window (CONFIG_AURORA_STATE_MACHINE_RBF_DEBOUNCE_MS, default 50 ms) and the level is only sampled once the contact has been quiet for a full window, so a chattering plug cannot arm and disarm the machine repeatedly. Changes are recorded in the audit log.

The level is also not latched during sm_init(), before the interrupt is enabled: a board that boots with the interlock already pulled never produces an edge and will sit disarmed forever waiting for one. If the pin cannot be brought up at all the interlock reports safe, holding the machine in IDLE rather than arming on an input it cannot read.

Note

This used to be done by pointing the powerfail subsystem at the RBF pin and treating “power failing” as “disarmed”. That is no longer the case: powerfail watches the battery, the state machine watches the interlock, and the two are independent.

Flight Thresholds

The thresholds that drive the transitions are per-vehicle data, not a firmware constant: a 200 m model and a 1 km vehicle disagree on every altitude and timeout in the set. The Kconfig options (see the sensor_board tables) are the factory defaults; the running values are edited from the shell and persisted, so a board flashed with one firmware image can fly either rocket.

The store is a single flash erase page selected by the auxspace,sm-config chosen node, holding one record: a magic, a layout version, a CRC and the threshold struct. Every save erases and rewrites the whole page. A blank page, a firmware update that reshuffled struct sm_thresholds, or a write cut short by a power loss all fail their check and fall back to the defaults, so a half-written set is never flown.

Reserve the page in the board devicetree, e.g. for micrometer rev.2:

/ {
	chosen {
		auxspace,sm-config = &sm_config_partition;
	};
};

&flash0 {
	partitions {
		sm_config_partition: partition@3df000 {
			compatible = "zephyr,mapped-partition";
			label = "sm-config";
			reg = <0x3df000 DT_SIZE_K(4)>;
		};
	};
};

Without the chosen node (or with CONFIG_AURORA_STATE_MACHINE_CONFIG_STORE disabled) the thresholds still change at runtime, but the shell warns that the change is lost on reboot.

Thresholds can only be changed in IDLE. Swapping one mid-flight would compare fresh limits against timers already started under the old ones, so sm_set_thresholds() returns -EBUSY outside IDLE.

Flight State Recovery

With CONFIG_AURORA_STATE_MACHINE_RETAIN, the active flight state survives a watchdog reset. Without it, a board that reboots mid-flight comes back in IDLE — a silently disarmed vehicle, with the recovery charges no longer going to fire.

The record lives in RTC slow memory (.rtc_noinit), which the linker marks NOLOAD and the startup code does not clear, so it survives the warm reset a watchdog produces. It is written on every transition through sm_transition(), which is the only sanctioned way to change state and therefore the one place the record cannot drift out of sync.

Why not flash

Flash would also survive losing power, but the state machine writes on every transition, and the transitions that matter most (APOGEE, MAIN, REDUNDANT) are exactly the ones that must not stall. A page erase blocks for milliseconds with interrupts locked and contends with the flight recorder already writing that device. The write that matters is the one issued while the system is already misbehaving, which is the worst possible moment to start erasing pages.

RTC memory costs a few stores, has no wear and needs no erase. The trade-off is that it does not survive a power cycle or a brownout — see powerfail for that failure, which needs a different mitigation.

When recovery happens

All of the following must hold, or the machine starts in IDLE:

  • the reset cause latched at boot includes RESET_WATCHDOG,

  • the record carries the expected magic, version and payload size,

  • the CRC matches,

  • it was written by the same state-machine backend (sm_type), so swapping implementations cannot resurrect a state ID that now means something else,

  • the retained state is not IDLE, and

  • the RBF interlock reads removed when re-sampled.

A power-up, a reset button or a debugger attach therefore always lands in IDLE. That is deliberate: the operator needs a reliable way to reach a known-safe state, and holding reset must not resurrect a live flight.

The interlock has to be re-sampled

sm_rbf_init() leaves the vehicle reported safe at boot and waits for an edge, so that forgetting to install the plug cannot arm the machine. That is right for a cold boot and wrong here: in flight the plug was pulled long before the reset, no further edge is coming, and the machine would disarm itself on its first update — undoing the recovery a few milliseconds after making it.

The recovery path therefore calls sm_rbf_resync() to adopt the pin’s present level, but only once it knows the reset came from the watchdog and a valid in-flight record exists. Under those conditions the vehicle was demonstrably armed already, which makes the pin the authority rather than a guess. A plug that is still installed still reads safe, so resetting on the pad still disarms.

Attitude calibration comes back too

Calibration only runs while the machine is in IDLE (see the sensor board IMU handling), so a board that resumes into a flight state can never redo it. Losing it is not cosmetic: vertical acceleration would stay pinned at zero, leaving boost and apogee detection blind, and orientation would integrate from a zero reference — reading horizontal, which trips the elevation gate and disarms within N_OI samples.

The calibration is therefore snapshotted into the same record the moment it completes, while the vehicle is stationary and the result is known good, and handed back on recovery. The biases were measured on the pad and do not change across a reset, and there is no opportunity to measure them again mid-flight.

If the reset landed before calibration ever finished there is nothing to restore. The flight continues, but degraded, and says so in the log.

Flight timers

Timers restart from zero. There is no way to know how long the board was absent, so phase timeouts (TO_A, TO_R) are necessarily generous after a recovery rather than wrong in the unsafe direction.

Reading the audit log

A recovery leaves a recognisable trace:

transition   IDLE   ARMED
event        ARMED  recovered after watchdog reset

A refusal leaves recovery refused: interlock safe instead. If you see the resume followed by an ARMED -> IDLE transition with no event between them, that is the disarm check in the backend, not the orientation gate — the orientation path logs orientation below threshold and the log-offline path logs arm aborted: flight log offline, so an eventless transition means .armed went false.

Shell Commands

Enabling CONFIG_AURORA_STATE_MACHINE_SHELL registers the state_machine command group. Audit-log commands are only available when CONFIG_AURORA_STATE_MACHINE_AUDIT is also enabled.

Command

Description

state_machine status

Print the active state-machine implementation and its current state.

state_machine transition <STATE>

Force a transition. The state name completes via tab. Because the state machine exposes no arbitrary setter, this deinitializes and reinitializes the machine, landing it in IDLE; a warning is printed when the requested target is not IDLE. Ground testing only.

state_machine config

List the running thresholds next to the compiled-in defaults.

state_machine config set <NAME> <VALUE>

Set one threshold, apply it and save the whole set. The name completes via tab and is case-insensitive; out-of-range values are rejected.

state_machine config default

Restore the factory (Kconfig) thresholds and save them.

state_machine config save

Save the running thresholds as they are.

state_machine audit

Dump the audit log (timestamped transitions and events). Requires CONFIG_AURORA_STATE_MACHINE_AUDIT.

state_machine audit_clear

Clear the audit log. Requires CONFIG_AURORA_STATE_MACHINE_AUDIT.

Setting the main deployment altitude to 400 m and keeping it:

uart:~$ state_machine config set T_M 400
T_M = 400 m
Applied and saved

Valid state names for transition are IDLE, ARMED, BOOST, BURNOUT, APOGEE, MAIN, REDUNDANT, LANDED and ERROR.

Warning

state_machine transition bypasses normal flight logic and resets the machine. Do not use in flight.

API Reference

enum sm_type

Identifier of the active state machine implementation.

Each implementation defines its own sm_state enum, so any external consumer (ground station, post-flight tooling) needs to know which implementation produced a given state value before it can decode it.

Values:

enumerator SM_TYPE_SIMPLE

Simple backend (lib/state/simple.c).

enum sm_error_reason

Reasons the state machine can enter SM_ERROR.

Passed to the error callback so the application can decide per cause whether to recover (return 0 → back to IDLE) or hold the error state (non-zero, e.g. a pre-flight interlock the operator must acknowledge by disarming) and how to signal the operator (LED, buzzer, …).

Values:

enumerator SM_ERR_UNKNOWN

Unspecified error.

enumerator SM_ERR_LOG_OFFLINE

Flight recorder unavailable while arming or armed.

enumerator SM_ERR_APOGEE_TIMEOUT

No descent detected within TO_A.

enumerator SM_ERR_REDUNDANT_TIMEOUT

No landing detected within TO_R.

typedef int (*sm_error_cb_t)(enum sm_error_reason reason, void *args)

Callback invoked when the state machine encounters an error.

The implementation can define specific recovery logic.

Param reason:

Why the state machine entered SM_ERROR.

Param args:

Pointer to an implementation-specific config structure.

Return:

0 if the error was mitigated (state machine returns to IDLE), negative errno to hold SM_ERROR (the callback is re-invoked on every update until it mitigates or the system is disarmed).

const char *sm_state_str(enum sm_state state)

Return a human-readable name for the given state.

Parameters:

state – State value.

Returns:

Pointer to a static string, or “UNKNOWN” if invalid.

const char *sm_error_reason_str(enum sm_error_reason reason)

Return a human-readable name for the given error reason.

Parameters:

reason – Error reason value.

Returns:

Pointer to a static string, or “UNKNOWN” if invalid.

void sm_init(const struct sm_thresholds *cfg, struct sm_error_handling_args *err_hdl)

Initialize the rocket state machine.

This function prepares the state machine, loads the threshold configuration, initializes internal timers, and sets the initial state to SM_IDLE.

Parameters:
  • cfg – Pointer to a threshold configuration structure.

  • err_hdl – Pointer to an error handling configuration (callback + args), or NULL.

void sm_deinit(void)

Deinitialize the rocket state machine.

This function resets the state machine, unloads the threshold configuration, stops internal timers, and sets the initial state.

void sm_update(const struct sm_inputs *inputs)

Update the state machine using current sensor readings.

Function evaluates sensor data and executes state transitions according to the flight logic diagram. Must be called regularly (e.g. at sensor update rate).

Parameters:

inputs – Pointer to populated sensor readings.

enum sm_state sm_get_state(void)

Retrieve the current state of the state machine.

Returns:

Current state (usually an enum implementation in state implementation).

bool sm_inflight(void)

Return if the state machine is in flight.

True from liftoff detection until touchdown. The pre-liftoff states, the landed state and SM_ERROR all report false.

Returns:

True if it is in flight, false if it’s on the pad.

bool sm_on_pad(void)

Return whether the vehicle is still sitting on the pad.

True in the pre-liftoff states only (IDLE and, where the backend has one, ARMED). Everything from liftoff onward reports false, and so do the terminal states: after touchdown the flight is over, and SM_ERROR may have been entered mid-flight, so neither may be treated as “on the pad”.

Note this is deliberately not the complement of sm_inflight(): the landed and error states report false to both.

Consumers use it to decide whether a pad-referenced quantity may still be re-zeroed – the barometric ground reference does exactly that, and must stay frozen from liftoff onward so that altitude keeps meaning height above the pad.

Returns:

True while the vehicle is on the pad, false otherwise.

enum sm_type sm_get_type(void)

Identify which state machine implementation is active.

External consumers use it to pick the right sm_state enum mapping.

Returns:

Active state machine type ID.

void sm_get_inputs(struct sm_inputs *out)

Retrieve the most recent inputs the state machine evaluated.

When CONFIG_FILTER is enabled, altitude and velocity are the Kalman-filtered values; the remaining fields are passed through from the last sm_update() call. When CONFIG_FILTER is disabled, all fields are the raw sm_update() inputs (and velocity is whatever the caller set, typically 0).

Before the first sm_update() call the returned struct is zeroed.

Parameters:

out – Destination struct, must be non-NULL.

int sm_get_armed()

Get the state machine armed state.

With the RBF arm source (CONFIG_AURORA_STATE_MACHINE_RBF) this returns the mechanical interlock’s debounced state. With tilt arming (CONFIG_AURORA_STATE_MACHINE_ARM_TILT) there is no input-level interlock, so it always reports armed and the orientation gate in the backend is the real arm/disarm control.

Return values:

0 – if disarmed, 1 if armed.

void sm_backend_get_thresholds(struct sm_thresholds *out)

Copy the thresholds that the backend is currently running with.

Parameters:

out – Buffer for the active thresholds.

int sm_set_thresholds(const struct sm_thresholds *cfg)

Replace the running thresholds.

Refused outside SM_IDLE: swapping a threshold under a running flight would compare fresh limits against timers already started under the old ones. Persisting the new set is up to the caller (sm_config_save()).

Parameters:

cfg – New thresholds, must be non-NULL.

Return values:
  • 0 – on success.

  • -EBUSY – if the machine is not in SM_IDLE.

void sm_update_force(enum sm_state transition_to)

Update the state machine with force. No further checks are done.

Parameters:

transition_to – State to transition.

double sm_orientation_elevation_deg(const double orientation[3])

Elevation of the configured up axis from horizontal (degrees).

Exposed beyond the backend so the shell can report the arm/disarm angle gate against the same number the transition logic uses.

Parameters:

orientation – Orientation reading (yaw, pitch, roll) in degrees.

Returns:

Elevation in degrees, clamped to [-90, 90]. +90 = up axis points to the sky, 0 = horizontal, -90 = inverted.

AURORA_STATE_BACKEND_INTERNAL
struct sm_thresholds
#include <simple.h>

Threshold configuration for the rocket state machine.

Defines thresholds for state transitions based on orientation, altitude, acceleration, and timing.

struct sm_inputs
#include <simple.h>

Sensor input structure for the state machine.

These values must be filled each update cycle to evaluate state transitions.

struct sm_error_handling_args
#include <state.h>

Error handling configuration for the state machine.

void sm_config_defaults(struct sm_thresholds *out)

Fill out with the compile-time (Kconfig) defaults.

Parameters:

out – Destination, must be non-NULL.

int sm_config_load(struct sm_thresholds *out)

Load the persisted thresholds, falling back to the defaults.

out is always left with a usable threshold set, so the caller can pass it straight to sm_init and only treat the return value as diagnostics.

Parameters:

out – Destination, must be non-NULL.

Return values:
  • 0 – A stored record was found and used.

  • -ENOENT – No (or no valid) record stored, defaults used.

  • -ENOTSUP – No threshold store configured, defaults used.

  • -errno – Read failed, defaults used.

int sm_config_save(const struct sm_thresholds *cfg)

Persist cfg, replacing whatever was stored before.

Parameters:

cfg – Thresholds to store, must be non-NULL.

Return values:
  • 0 – on success.

  • -ENOTSUP – No threshold store configured.

  • -errno – on erase/write failure.

int sm_config_erase(void)

Drop the persisted record, so the next boot uses the defaults.

Return values:

0 – on success, -ENOTSUP without a store, negative errno on failure.

const struct sm_config_field *sm_config_fields(size_t *count)

Table of editable thresholds.

Parameters:

count – Written with the number of entries, must be non-NULL.

Returns:

Pointer to a static table of count entries.

const struct sm_config_field *sm_config_field_find(const char *name)

Look up a threshold by name (case-insensitive).

Parameters:

name – Field name, e.g. “T_M”.

Returns:

Matching entry, or NULL if name is unknown.

int sm_config_field_get(const struct sm_thresholds *cfg, const struct sm_config_field *field)

Read a threshold through its descriptor.

Parameters:
  • cfg – Threshold set to read from.

  • field – Descriptor from sm_config_fields.

Returns:

The stored value.

int sm_config_field_set(struct sm_thresholds *cfg, const struct sm_config_field *field, int value)

Write a threshold through its descriptor, with range checking.

Parameters:
  • cfg – Threshold set to modify.

  • field – Descriptor from sm_config_fields.

  • value – New value.

Return values:

0 – on success, -ERANGE if value is outside the field’s range.

struct sm_config_field
#include <config.h>

One editable threshold, for generic (shell) access.

void sm_retain_save(enum sm_state state)

Persist state so a watchdog reset can resume from it.

Called from the state core on every transition. Writing to RTC RAM is a handful of stores, so this is cheap enough for the transition path and involves no flash erase.

Parameters:

state – State now active.

int sm_retain_restore(enum sm_state *out)

Recover the flight state latched before a watchdog reset.

Succeeds only when all of the following apply:

  • the reset cause latched at boot includes RESET_WATCHDOG,

  • the record carries the expected magic, version and payload size,

  • the CRC matches,

  • the record was written by the same state machine backend, and

  • the retained state is one a flight can actually be interrupted in (not SM_IDLE, which means nothing was in progress).

Parameters:

out – Receives the state to resume. Untouched on failure.

Return values:
  • 0 – on success.

  • -ENOTSUP – if the reset was not caused by the watchdog.

  • -ENOENT – if no valid record is present.

  • -EINVAL – if the record is stale, corrupt or from another backend.

void sm_retain_invalidate(void)

Discard the retained record.

Called when the machine reaches a state that must not be resumed into, so a later watchdog reset starts clean.

uint16_t sm_retain_recovery_count(void)

Number of watchdog recoveries since the last power cycle.

Survives in the same record. A climbing count means the board is resetting repeatedly rather than recovering, which is worth surfacing.

Returns:

Recovery count, or 0 if no valid record exists.

bool sm_retain_recovered(void)

Whether this boot resumed a flight.

Lets the application restore anything that lives outside the state machine, most importantly the attitude calibration.

Return values:

true – if sm_retain_restore recovered a state this boot.

int sm_retain_save_blob(const void *data, size_t len)

Store an opaque payload alongside the retained state.

Used for the attitude calibration. Losing it across a reset is not a cosmetic problem: calibration only runs in SM_IDLE, so a machine that resumes into a flight state would never recalibrate. Orientation would integrate from a zero reference (reading horizontal, which disarms on the elevation gate) and vertical acceleration would stay pinned at zero, leaving boost and apogee detection blind.

Parameters:
  • data – Payload to copy in.

  • len – Payload size, at most SM_RETAIN_BLOB_SIZE.

Return values:
  • 0 – on success.

  • -EINVAL – if data is NULL or len exceeds the capacity.

int sm_retain_load_blob(void *data, size_t len)

Retrieve the payload stored by sm_retain_save_blob.

Parameters:
  • data – Destination buffer.

  • len – Expected size

Return values:
  • 0 – on success.

  • -EINVAL – if data is NULL or len is not the stored size.

  • -ENOENT – if no valid record or no payload is present.

SM_RETAIN_BLOB_SIZE

Capacity of the opaque payload carried alongside the state.

Sized for the attitude calibration with room to spare; RTC slow memory is 8 KiB and this whole subsystem uses a fraction of it.