Lightweight data components for HAPI and embedded systems.
HAPI Compatibility: Updated for new Check/Apply/ApplyPack API (2026-Q2)
Owned values, external references, compile-time constants, change tracking, value ranges, and default injection — all composable via DataDef<> with zero runtime overhead where the hardware allows it.
| Component | Description | RAM cost |
|---|---|---|
Data<T> |
Owned runtime value | sizeof(T) |
StaticData<T, v> |
Compile-time immutable constant | 0 |
DataRef<T*, &var> |
Reference to external variable or hardware register | 0 |
Watch<W> |
Change-detection modifier wrapping any data component | sizeof(T) |
NumRange<N> |
Dynamic value range with step/wrap | 3 × sizeof(N) + 1 |
StaticNumRange<N, low, high> |
Compile-time range, clamp on step | 0 |
Default<T, v> |
Default-value injection modifier | 0 |
DataFn<Src> |
External get()/set() functions as storage (pins, ISR-shared vars, sensors) |
0 |
Translated<W, Policy> |
Bidirectional raw↔display value conversion (e.g. ADC counts ↔ volts) | 0 |
ReadOnly<W> |
Erases set() at compile time — genuine read-only view, not a silent no-op |
0 |
Decimals<N, W> |
Fixed N-decimal-place print formatting, no libc float-printf needed | 0 |
String aliases: Text (Data<const char*>), Bool (Data<bool>), Int (Data<int>).
Pointer aliases: IntRef<p>, BoolRef<p>, CharRef<p>, StaticText<ref>.
#include "oneData.h"
using namespace oneData;
auto power = DataDef<Watch<Int>, Int>{60};
power.set(80);
if (power.changed()) {
// react to change
power.sync(); // clear flag, arm for next change
}inline volatile int fake_hw{};
using PinPort = DataRef<volatile int*, &fake_hw>;
DataDef<PinPort> led_pin;
led_pin.set(0xFF); // writes directly to fake_hwDataDef<StaticInt<42>> answer;
int v = answer.get(); // v == 42inline const char* label = "Status";
DataDef<StaticText<label>> status_label;
// status_label.get() returns "Status" with no local storage// Dynamic range
auto brightness = DataDef<NumRange<int>, Int>{};
// construct with: low, high, wraps, initial_value
auto vol = DataDef<NumRange<int>, Int>(0, 100, true, 50);
vol.up(); // 51
vol.down(); // 50
// Static range (compile-time, 0 bytes overhead)
DataDef<StaticNumRange<int, 0, 100>, Int> gain{50};
gain.up(10); // 60
gain.down(20); // 40DataDef<Default<int, 128>, Int> mid; // initialises to 128 without passing a valuestruct MyAdcPin { static int get(); static void set(int); }; // or any OnePin terminal — get()/set() already match
struct AdcToVolts {
static constexpr float toDisplay(int raw) { return raw * (5.0f/1023.0f); }
static constexpr int toRaw(float v) { return (int)(v * 1023.0f/5.0f); } // omit if read-only
};
DataDef<Decimals<2, Translated<Watch<DataFn<MyAdcPin>>, AdcToVolts>>> sensor;
sensor.get(); // "2.50" -style raw->volts conversion, formatted to 2 decimals when printed
sensor.changed(); // tracks the raw int underneath Translated, not the lossy floatA read-only monitor (no editing) just needs a Policy with toDisplay() — toRaw() is never instantiated unless set() is actually called. Wrap with ReadOnly<...> for a compile-time-enforced guarantee instead of relying on that:
DataDef<Translated<ReadOnly<DataFn<MyAdcPin>>, AdcToVolts>> sensor; // sensor.set(x) fails to compiletemplate<typename... OO>
using DataDef = DefaultDataDef<OO...>;Composes a chain of data components via APIOf. The first matching Part in the chain provides each method; components that do not provide a method fall through to the next layer. The base DataAPI<> supplies no-op defaults for changed(), sync(), and print().
Owns a value of type T.
const T& get() const noexcept;
void set(V&&) noexcept; // forwardingConstructor forwarding: the first argument is treated as the initial value when V is convertible to T; remaining arguments are forwarded to the base.
Note:
Data<const char*>copies the pointer, not the string. Ownership semantics for string literals are caller-managed.
Stores nothing; get() returns a constexpr reference to v.
static constexpr const T& get() noexcept;Zero-RAM reference to an externally owned variable. T must be a pointer type; address must be a valid non-null pointer.
static auto& get() noexcept; // returns *address (or address for char*)
static void set(T v) noexcept; // *address = vWraps another data component W and tracks whether get() has changed since the last sync().
bool changed() const noexcept; // get() != watched
void sync() noexcept; // watched = get()Watch delegates get(), set(), and constructors to W.
Adds a dynamic range [m_low, m_high] with optional wrap. Requires a backing Data<N> lower in the chain.
// Constructor arguments (prepended to the chain):
Part(NRP low, NRP high, bool wraps, ...rest);
bool valid(NRP v) const noexcept;
NRP clamp(NRP v) const noexcept;
void up (NRP step = 1) noexcept;
void down(NRP step = 1) noexcept;Known issue:
stepDownargument order is(s, o)wheresis the step andois the current value — the reverse ofstepUp(o, s). This is an internal inconsistency;up()anddown()are the correct public interface and are unaffected.
Compile-time variant. No stored state; up()/down() clamp via constexpr expressions.
Injects defaultValue as the first constructor argument to the layer below, so a DataDef can be constructed without explicitly providing an initial value.
Storage backed by external get()/set() static functions instead of an owned value. Src just needs static Type get() (and static void set(Type) if writable) — an OnePin terminal already exposes both (via Mask<>), so a pin works directly as Src with no adapter. A hand-written struct works the same way for volatile globals, hardware registers, sensor reads, etc.
static Type get() noexcept; // Src::get()
static void set(Type) noexcept; // Src::set(v)Bidirectional value conversion between an underlying raw storage W and a displayed/edited Type (e.g. a 0–1023 ADC reading shown/edited as 0.0–5.0 volts).
static Type toDisplay(RawType) noexcept; // required on Policy
static RawType toRaw(Type) noexcept; // required on Policy only if set() is ever calledtoRaw() is only required if the field is actually edited — a non-template member function of a class template is only instantiated when called, so a read-only Policy (no toRaw) is enough for a monitor-only field. Defines its own print()/printItem() (does not forward to Base's) — Base eventually reaches a Data/DataFn/DataRef terminal that would print the untranslated raw value too, double-printing the same logical value at two representations.
Erases set() from W via private inheritance, re-exposing only get()/print()/printItem(). Calling set() on a ReadOnly-wrapped chain is a genuine compile error ("is inaccessible within this context"), not a silent no-op. Does not re-expose changed()/sync() — ReadOnly<Watch<...>> would lose those too; compose the other way (Watch<ReadOnly<...>>) if you need both.
Fixed N-decimal-place print formatting for a floating-point W. Hijacks print()/printItem() the same way Translated does — it's replacing, not adding to, how the value is rendered. Digit extraction is done manually (no snprintf/%f), so it works on AVR without needing printf float-support linked in (-Wl,-u,vfprintf -lprintf_flt -lm), which isn't enabled by default.
Components are composed left-to-right in the DataDef<> template argument list. A modifier like Watch or Default must appear before the data component it wraps:
// Watch wrapping Int — correct
DataDef<Watch<Int>, Int> x{0};
// Default injecting into Int — correct
DataDef<Default<int, 0>, Int> y;print(out) is defined on every component and chains down the stack; out must supply a put() method compatible with the stored type.
- C++17 or later (required for
if constexpr, fold expressions) hapi/hapi.h— providesAPIOfandhapi::Nil- No dynamic allocation; no exceptions; no RTTI
- Tested on AVR (avr-gcc 7+) and x86-64
MIT — see LICENSE.