Appearance
Handling Writable Properties (Commands & Configuration)
Writable properties are used for commands, configuration values and outputs that can be changed from outside the device (Coldwave Backend, local router, other Coldwave clients) or from internal subsystems (e.g. script VM).
On the device you handle writable properties via callbacks registered on your flake::Service instance.
There are three relevant callback forms for writable properties:
- A simple setter callback that only sees the final value
- A full actionable callback that sees the whole transaction (property, transaction array,
internalflag) - A clamping setter callback for actionable properties with automatic min/max handling
This chapter explains when each variant is used and how their signatures differ.
Overview: Property Types and Callback Variants
Every property in your Flake data model has a tag (PropTag). Some tags are marked as actionable (internally via the TAG_ACTIONABLE bit). Actionable properties are used for commands or setpoints where:
- you may need to reject a write,
- you may need additional context (other properties in the same transaction),
- you may want to distinguish internal vs external writes.
The callback type depends on whether the property is actionable:
| Property type | Callback registration | Callback signature | Typical use |
|---|---|---|---|
| Non‑actionable | service->on<PropTag>(cb); | void cb(PropType<PropTag> value) | Config/watchers, status mirrors |
| Actionable (raw) | service->on<PropTag>(cb); | int cb(Property& p, const PropArray& tx, bool internal) | Commands, DO/relays, guarded setpoints |
| Actionable (clamp) | service->on<PropTag>(cb, min, max); | void cb(PropType<PropTag> clampedValue) | Simple setpoints with min/max range |
All three ultimately plug into the same internal mechanism in flake::Service, but with different levels of abstraction.
Simple Setter Callbacks (Non‑Actionable Properties)
For non‑actionable properties (the default case), PropCallback<PropTag> resolves to a plain callback that only receives the property value. When you write:
cpp
void onScriptEnabled(bool enabled)
{
script_enabled = enabled; // mirror into RTC RAM, etc.
if (enabled) {
scripting_run();
} else {
scripting_stop();
}
}
service->on<SCRIPT_ENABLED>(onScriptEnabled);the template instantiation is:
PropTag=SCRIPT_ENABLED(non‑actionable)PropCallback<PropTag>=std::function<void(T)>T= the property type (here:bool)
Internally, Service::on<PropTag>(cb) for non‑actionable properties wraps your function in a lambda that extracts the typed value from the Property object and then calls cb(value).
When is the callback called?
The simple setter callback is invoked whenever the property is written, for example:
- from the Coldwave Backend,
- from a local router / client,
- from an internal component that sets the property through the service.
You do not see the PropArray transaction or the internal flag here – you just get the final value. Use this form when:
- you only care about what the new value is,
- you do not need to reject writes or inspect other properties in the same transaction.
Full Actionable Setter Callbacks
For actionable properties (tags with TAG_ACTIONABLE), PropCallback<PropTag> is defined as:
cpp
using PropCallBackActionable = std::function<int (Property&, const PropArray&, bool)>;You register them with the same on<PropTag>() API, but with a different signature:
cpp
int onDO1(Property& p, const PropArray& tx, bool internal)
{
auto se = tx.get(SCRIPT_ENABLED).value<bool>();
// reject if scripting owns the output or the write is external
if ((se && !*se) || (script_enabled && !internal)) {
return E_READ_ONLY;
}
uint8_t val = p.value<uint8_t>().value();
d0 = val; // mirror into local state / RTC RAM
if (val == 1U) {
set_do1_on();
} else {
set_do1_off();
}
return E_OK;
}
service->on<DO1>(onDO1);Parameters
Property& p
Contains the property value and the tag. You typically callp.value<T>()to get the typed value.const PropArray& tx
The transaction: all properties that are written together in this operation. This allows you to look at other values that were set alongsidep, e.g.SCRIPT_ENABLEDor further parameters.bool internaltrueif the write originated from an internal source (e.g. script VM);falseif it came from outside (backend, remote client). You can use this to implement ownership models (“only scripts may write this property”, “backend writes only allowed when not in local override mode”, etc.).
Return value
Your callback must return an int:
E_OK– write accepted and appliedE_READ_ONLY– write rejected due to access rulesE_FAILED– generic failure (invalid value, hardware error, etc.)
The return code is propagated back to the caller (e.g. the backend) as the result of the write.
When to use the full actionable callback
Use this form when:
- the property represents a command or critical setpoint,
- you need to inspect other properties in the same transaction (
tx), - you need to reject writes depending on context,
- you want to distinguish internal vs external writes (
internal).
Examples: digital outputs, mode/state machines, safety‑critical thresholds.
Actionable Callbacks with Clamping (min/max)
For actionable properties with a simple numeric range (e.g. 0–100%, 10–30 °C), you can use the clamping overload:
cpp
template<uint32_t PropTag>
int on (PropCallback<PropTag & ~TAG_ACTIONABLE> cb,
PropType<PropTag> min,
PropType<PropTag> max);Typical usage:
cpp
void onBrightness(uint8_t level)
{
// level is guaranteed to be in [10, 90]
pwm_set_brightness(level);
}
// ACTIONABLE brightness property, clamped between 10 and 90
service->on<BRIGHTNESS>(onBrightness, 10, 90);Was passiert intern?
- Der Overload ist nur für actionable Properties erlaubt (
static_assert((PropTag & TAG_ACTIONABLE) != 0)). - Intern wird ein vollständiger
PropCallBackActionableregistriert, der:- den Wert aus
Property& pliest, min/maxüberprüft,- den Wert bei Bedarf auf
[min, max]clamped, - dein
cb(clampedValue)aufruft, E_OKoderE_FAILEDzurückgibt.
- den Wert aus
Dein Callback ist dadurch deutlich einfacher:
- Signatur:
void cb(T clampedValue) - Kein Zugriff auf
PropArray& tx - Kein Zugriff auf
internal - Kein expliziter Errorcode
Verwende diesen Overload, wenn:
- du nur einen gültigen Bereich erzwingen willst,
- du keinen Zugang zu Transaktion oder
internalbrauchst, - ein einfacher Setter mit garantiert gültigem Wert ausreicht.
Zusammenfassung: Welche on()‑Variante wofür?
| Ziel | Property-Typ | on()‑Variante | Callback-Signatur |
|---|---|---|---|
| Einfache Konfiguration, Status | non‑actionable | service->on<PropTag>(cb) | void cb(T value) |
| Kommandos mit Kontext / Zugriffskontrolle | actionable | service->on<PropTag>(cb) | int cb(Property& p, const PropArray& tx, bool internal) |
| Setpoints mit Min/Max‑Range | actionable | service->on<PropTag>(cb, min, max) | void cb(T clampedValue) |
Wichtige Regeln
- Nie
service->set<>()oderservice->set(PropArray&)aus einem Property‑Callback aufrufen, das miton()registriert wurde. In diesem Fall liefertset()E_REFUSED. - Für actionable Properties:
- Nutze den vollen Callback, wenn du Logik, Checks und Fehlercodes brauchst.
- Nutze die Clamping-Variante, wenn du nur einen validierten Wert willst.
- Für non‑actionable Properties:
- Nutze den einfachen Setter, um lokalen Zustand oder Hardware zu spiegeln.
Beispiel aus der Referenz‑Applikation
Auszug aus der Referenz‑Applikation (vereinfacht):
cpp
// Declaration
int onDO1(const Property& p, const PropArray& tx, bool internal);
int onDO2(const Property& p, const PropArray& tx, bool internal);
int onState(const Property& p, const PropArray& tx, bool internal);
void onScriptEnabled(bool enabled);
// Registration
service->on<SCRIPT_ENABLED>(onScriptEnabled); // non-actionable, simple setter
service->on<DO1>(onDO1); // actionable, full callback
service->on<DO2>(onDO2); // actionable, full callback
service->on<STATE>(onState); // actionable, full callback
// SCRIPT_ENABLED: simple non-actionable setter
void onScriptEnabled(bool enabled)
{
script_enabled = enabled;
if (enabled) {
scripting_run();
} else {
scripting_stop();
}
}
// DO1: actionable setter with full context
int onDO1(const Property& p, const PropArray& tx, bool internal)
{
auto se = tx.get(SCRIPT_ENABLED).value<bool>();
// reject if scripting owns the output or the write is external
if ((se && !*se) || (script_enabled && !internal)) {
return E_READ_ONLY;
}
uint8_t val = p.value<uint8_t>().value();
d0 = val;
if (val == 1U) {
// switch hardware on
} else {
// switch hardware off
}
return E_OK;
}Dieses Muster zeigt:
- SCRIPT_ENABLED wird als einfacher non‑actionable Setter behandelt.
- DO1 / DO2 / STATE sind actionable Properties mit vollem Kontext, bei denen:
- das Skript‑Flag berücksichtigt wird,
- externe Zugriffe ggf. abgelehnt werden,
- Hardware entsprechend geschaltet wird.