dynamic-config-embedded 0.5.0

Hot-reloadable configuration for no_std targets: no filesystem, no allocator, no runtime.
Documentation

dynamic-config-embedded

Hot-reloadable configuration for no_std targets: no filesystem, no allocator, no runtime.

[dependencies]
dynamic-config-embedded = { version = "0.5.0", default-features = false, features = ["json"] }
use dynamic_config_embedded::{ConfigCell, Format, Validate};
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
struct Settings {
    interval_ms: u32,
    verbose: bool,
}

impl Validate for Settings {}

static SETTINGS: ConfigCell<Settings> = ConfigCell::new();

// Compiled-in defaults, so the device is configured before anything arrives.
SETTINGS.store(Settings { interval_ms: 1000, verbose: false });

// A document from wherever this device gets one: a serial link, an MQTT
// message, a page of flash.
SETTINGS.apply(br#"{"interval_ms": 250, "verbose": true}"#, Format::Json)?;

Why this is a separate crate

dynamic-config reads files, searches directories and merges layers with figment. A microcontroller has no files, no directories and no allocator, and figment is std — so this is not that crate with a feature switched off. It is the same shape, built from what a device actually has.

What it keeps

  • A snapshot in a static, replaced whole. A reader never sees a half-applied configuration.
  • A bad document cannot take the device down. Parsing and validation happen before anything is installed; a failure leaves the previous configuration serving.
  • changes(), a Future that resolves on the next configuration — the same generation-counter-and-wakers design as the std crate, which is why it drives on Embassy, RTIC, or a hand-written executor.
  • Validation, through the same Validate shape.

What it cannot keep, and why

Files, directory search, profiles there is no filesystem
Environment variables there is no environment
Layered merging figment is std, and merging needs a value tree that allocates
Arc snapshots no allocator; readers clone the value out
Provenance (source_of) there is one source, so the question does not arise

A device gets one document at a time and replaces the whole configuration. That is not a reduced version of layering — it is what configuring a device looks like.

No allocator, and no lock a reader can block on

Storage is a critical-section around a plain slot: a handful of instructions with interrupts masked, which is the primitive every embedded HAL provides and the only one this crate needs. The section is held for a clone of the value and nothing else.

That makes T: Clone the price of admission. For a configuration struct of scalars — which is what a device's configuration is — the clone is a memcpy.

Awaiting a change, with no allocator

changes() needs somewhere to keep a waker, and a Vec<Waker> needs an allocator. So there are four fixed slots, chosen for the shape of the problem: a device has a handful of tasks that care about configuration, not thousands. A fifth waiter replaces the oldest rather than being dropped — a task that is never woken is a hang, and one woken early merely polls again.

Features

Feature Default Effect
json Format::Json, via serde-json-core, which allocates nothing
async changes()
std the critical-section implementation a device gets from its HAL — for tests and host simulators

A field the firmware does not know is ignored

serde skips it, which on a device is the difference between a rolling upgrade and a fleet that stops taking configuration. A firmware that wants the opposite says so with #[serde(deny_unknown_fields)].

Testing

Unit and integration tests run on a host with the std feature, which supplies the critical-section implementation a device gets from its HAL — the same code otherwise. CI also builds for thumbv7em-none-eabihf, because "it is no_std" is a claim a host build cannot check.

cargo test -p dynamic-config-embedded --features std,async
cargo check -p dynamic-config-embedded --target thumbv7em-none-eabihf \
    --no-default-features --features json,async

The features matter: without std the integration tests compile to zero tests, silently, and the run still reports green.

MSRV

1.83 — higher than the core crate's 1.71 because of language features, not dependencies: core::error::Error in no_std needs 1.81 and inline const blocks need 1.79. 1.83 is the floor CI verifies. Still far below anything a device toolchain would struggle with.

License

MIT