Skip to content

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:

  1. A simple setter callback that only sees the final value
  2. A full actionable callback that sees the whole transaction (property, transaction array, internal flag)
  3. 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 typeCallback registrationCallback signatureTypical use
Non‑actionableservice->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 call p.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 alongside p, e.g. SCRIPT_ENABLED or further parameters.

  • bool internal
    true if the write originated from an internal source (e.g. script VM); false if 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 applied
  • E_READ_ONLY – write rejected due to access rules
  • E_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 PropCallBackActionable registriert, der:
    • den Wert aus Property& p liest,
    • min / max überprüft,
    • den Wert bei Bedarf auf [min, max] clamped,
    • dein cb(clampedValue) aufruft,
    • E_OK oder E_FAILED zurückgibt.

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 internal brauchst,
  • ein einfacher Setter mit garantiert gültigem Wert ausreicht.

Zusammenfassung: Welche on()‑Variante wofür?

ZielProperty-Typon()‑VarianteCallback-Signatur
Einfache Konfiguration, Statusnon‑actionableservice->on<PropTag>(cb)void cb(T value)
Kommandos mit Kontext / Zugriffskontrolleactionableservice->on<PropTag>(cb)int cb(Property& p, const PropArray& tx, bool internal)
Setpoints mit Min/Max‑Rangeactionableservice->on<PropTag>(cb, min, max)void cb(T clampedValue)

Wichtige Regeln

  • Nie service->set<>() oder service->set(PropArray&) aus einem Property‑Callback aufrufen, das mit on() registriert wurde. In diesem Fall liefert set() 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.