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);What happens internally?
- The overload is only allowed for actionable properties (
static_assert((PropTag & TAG_ACTIONABLE) != 0)). - Internally, a full
PropCallBackActionableis registered that:- reads the value from
Property& p, - checks
min/max, - clamps the value into
[min, max]if necessary, - calls your
cb(clampedValue), - returns
E_OKorE_FAILED.
- reads the value from
Your callback is therefore considerably simpler:
- Signature:
void cb(T clampedValue) - No access to
PropArray& tx - No access to
internal - No explicit error code
Use this overload when:
- you only want to enforce a valid range,
- you do not need access to the transaction or the
internalflag, - a simple setter with a guaranteed valid value is enough.
Summary: Which on() variant for what?
| Goal | Property type | on() variant | Callback signature |
|---|---|---|---|
| Simple configuration, status mirror | non‑actionable | service->on<PropTag>(cb) | void cb(T value) |
| Commands with context / access control | actionable | service->on<PropTag>(cb) | int cb(Property& p, const PropArray& tx, bool internal) |
| Setpoints with min/max range | actionable | service->on<PropTag>(cb, min, max) | void cb(T clampedValue) |
Important rules
- Never call
service->set<>()orservice->set(PropArray&)from inside a property callback registered withon(). In that caseset()returnsE_REFUSED. - For actionable properties:
- Use the full callback when you need logic, checks and error codes.
- Use the clamping overload when you only want a validated value.
- For non‑actionable properties:
- Use the simple setter to mirror local state or hardware.
Example from the reference application
Excerpt from the reference application (simplified):
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;
}This pattern shows:
- SCRIPT_ENABLED is treated as a simple non‑actionable setter.
- DO1 / DO2 / STATE are actionable properties with full context, where:
- the script flag is taken into account,
- external writes can be rejected,
- hardware is switched accordingly.