Actuate (write to device)
Status (2026-05-11). This page describes today's libscadable v0.3.0 surface and flags what's coming. The earlier Python-DSL platform had a
self.actuate(device, register, value)API for protocol-driver writes (Modbus FC6/FC16, GPIO, BACnet WriteProperty). That surface does not exist in libscadable today — protocol-driver writes are still owned bygateway-linux's Modbus driver and not yet exposed through the chip-side library. The roadmap below tracks the gap.
The platform's current device-write story has two halves: runtime configuration (env vars + secrets, live-pushed from the dashboard) and future RPC-style commands (not yet shipped).
What works today: env vars + secrets
The cloud can push configuration changes to a connected device without a firmware redeploy. Set a value in the dashboard, the cloud sends an MQTT nudge, the library refreshes the cache, and your firmware reads the new value on the next scadable_env_get / scadable_secret_get call. You can also subscribe to per-key change callbacks via scadable_on_env_change.
#include "scadable.h"
static void on_env_change(const char *key, const char *new_value, void *user) {
if (strcmp(key, "POLL_INTERVAL_S") == 0) {
int interval = scadable_env_get_int("POLL_INTERVAL_S", 30);
// tell your task to use the new interval
update_poll_interval(interval);
}
}
void app_main(void) {
wifi_up();
scadable_init(NULL);
scadable_on_env_change(on_env_change, NULL);
scadable_connect();
// ...
}Use this for setpoints, polling intervals, feature flags, API tokens, anything the operator wants to change without re-flashing. The change propagates within seconds of the dashboard save.
Trade-offs.
- Env vars cache in RAM only (NOT persisted to NVS — flash dump can't leak them).
- Secrets cache in a separate RAM-only table that's not visible to env-get callers.
- Secrets reset to "missing" on reboot until the next refresh succeeds. Don't put critical bootstrap credentials there; those belong in the device cert (which IS in NVS, encrypted at rest by flash encryption when enabled).
- Updates fire the change callback once per refresh whether the value actually changed or not (diff-only behavior is on the roadmap for v0.4).
What works today: diagnostics as a "write trigger" pattern
If you need an operator-triggered action that runs on the device, model it as a diagnostic test. The cloud "Run check" button dispatches the run; your firmware executes it and publishes the result. The result includes a free-form details field (1 KiB) where you can return whatever you like.
SCD_DIAG(reset_calibration, ctx) {
DIAG_LOG(ctx, "operator-triggered calibration reset");
if (!sensor_reset_calibration()) {
return DIAG_FAIL("sensor refused calibration reset");
}
return DIAG_PASS("calibration reset; new offset=%.4f", read_offset());
}Diagnostics are role-gated cloud-side. Operators with the right permissions trigger them from the dashboard; the dispatch is audited. This isn't a substitute for a real RPC channel, but it covers a useful subset (operator-triggered actions that the firmware acknowledges with a structured result).
Roadmap: RPC-style commands
The earlier platform's @on.message(command="X") decorator gave firmware an inbound RPC surface for cloud-issued commands. That surface is not in libscadable v0.3.0. When it lands (Phase 2 / v0.4 candidate), it will likely look like:
// Roadmap shape — subject to change before ship
SCADABLE_ON_COMMAND("set_setpoint", ctx) {
double v = scadable_command_arg_f64(ctx, "value");
update_setpoint(v);
return SCADABLE_CMD_OK;
}Cloud-side, the dashboard will auto-render a "Send Command" panel from declared commands in .scadable/config.toml. Role gating (requires_role: "operator") will gate who can issue. Audit will log every dispatch.
Until that ships, model operator-triggered actions as diagnostics; model device configuration changes as env vars.
Roadmap: protocol-driver writes (Modbus, GPIO, BACnet)
For protocol-driver writes (write a value to a Modbus register, set a GPIO pin high, BACnet WriteProperty), today's surface is:
gateway-linuxhas a Modbus driver that supports reads via the legacy declarative config; writes are not exposed to firmware-equivalent customer code.- ESP32 / ESP32-S3 don't have a chip-side protocol driver framework yet.
When this lands, the user-facing surface will likely mirror the older platform's self.actuate(device, register, value) shape but expressed in C. Authorization will be cloud-side per command (the role-gating model from the older platform was good and we'll keep it). Idempotency notes from the older API still apply: setpoints and absolute coil writes are safe to retry; counters and pulse outputs are not.
What about cloud-direct writes?
We deliberately don't support a "write directly to register from the dashboard without firmware code" affordance. Reason: every actuate should go through firmware code so the firmware can validate, rate-limit, log, and emit events around the write. A direct cloud-to-driver write bypasses all of that, and ops actions become indistinguishable from autonomous firmware actions in the audit log.
When the RPC and protocol-write surfaces land, the explicit-firmware-handler model will be the only path. If you want a thin actuate path with no firmware logic, write a one-handler command that takes the device + register + value as arguments — that makes the bypass explicit, auditable, and gateable per namespace.
Idempotency notes (preserved from the original architecture)
These guidelines remain true regardless of which API ships:
- Setpoints (write a holding register or env var to a value): idempotent. Safe to retry.
- Absolute coil / GPIO writes (set the pin/coil to an absolute new state, not a diff): idempotent.
- Counters / increments: NOT idempotent. The platform can't help here — the firmware handler must guard.
- Pulse outputs (open relay for 500ms then close): NOT idempotent. The "open" arrives; the "close" is best-effort.
When in doubt, log the request and the result, and let an ops dashboard show the divergence.
Where to go next
- Channels: the typed publish surface
- How SCADABLE Protects Your Data: the role-gating and audit model
- Library reference: every public function, macro, and enum
Updated 2 days ago
