API Reference
The complete public surface of libscadable v0.2: about 16 functions plus 4 macros. Mirrors include/scadable.h exactly. ABI is not yet stable; v1.0 will mark the API stable.
All functions return scadable_err_t (errno-style; SCADABLE_OK is 0) unless noted.
Error codes
typedef enum {
SCADABLE_OK = 0,
SCADABLE_ERR_NOT_INITIALIZED = -1,
SCADABLE_ERR_NOT_CONNECTED = -2,
SCADABLE_ERR_INVALID_ARG = -3,
SCADABLE_ERR_BACKPRESSURE = -4, // outbound queue full; retry after EVT_PUBLISHED
SCADABLE_ERR_TIMEOUT = -5,
SCADABLE_ERR_NO_CERT = -6,
SCADABLE_ERR_TLS = -7,
SCADABLE_ERR_NETWORK = -8,
SCADABLE_ERR_TEST_FAILED = -9,
SCADABLE_ERR_INTERNAL = -100,
} scadable_err_t;
const char *scadable_strerror(scadable_err_t err);scadable_strerror(err) returns a static string suitable for logging: "not initialized", "backpressure", etc.
Lifecycle
scadable_init
scadable_initscadable_err_t scadable_init(const scadable_config_t *cfg);Initializes the library. Reads the per-device cert, namespace ID, and baked-in broker URL from NVS (provisioned at flash time by the SCADABLE web flasher).
| Parameter | Type | Description |
|---|---|---|
cfg | const scadable_config_t * | Optional runtime overrides. Pass NULL to use everything from .scadable/config.toml. |
The config struct:
typedef struct {
const char *broker_url;
const char *device_id; // default = MAC-derived
uint16_t keepalive_secs; // default 30
uint16_t log_batch_secs; // default 5; 0 = realtime
} scadable_config_t;Idempotent. Calling twice is a no-op (returns SCADABLE_OK).
Returns: SCADABLE_OK, SCADABLE_ERR_NO_CERT (NVS missing cert; device was not provisioned), SCADABLE_ERR_INTERNAL.
scadable_err_t err = scadable_init(NULL);
if (err != SCADABLE_OK) {
ESP_LOGE(TAG, "scadable_init failed: %s", scadable_strerror(err));
return;
}scadable_connect
scadable_connectscadable_err_t scadable_connect(void);Begins connecting to the broker. Non-blocking. Transitions internal state from IDLE to CONNECTING. To know when the connection is ready, register an event callback and watch for SCADABLE_EVT_CONNECTED.
Auto-reconnects on transient failures (link drop, broker restart) with exponential backoff. Backoff caps at 60 seconds.
Returns: SCADABLE_OK, SCADABLE_ERR_NOT_INITIALIZED.
scadable_disconnect
scadable_disconnectscadable_err_t scadable_disconnect(void);Graceful disconnect. Sends LWT, flushes pending PUBACKs (up to 2 seconds), stops auto-reconnect. Call before turning WiFi off so the broker sees a clean disconnect rather than a stale session.
Returns: SCADABLE_OK.
scadable_state and scadable_is_connected
scadable_state and scadable_is_connectedscadable_state_t scadable_state(void);
bool scadable_is_connected(void);Synchronous, no IO. scadable_is_connected() is the convenience equivalent of scadable_state() == SCADABLE_STATE_CONNECTED.
typedef enum {
SCADABLE_STATE_UNINITIALIZED = 0,
SCADABLE_STATE_IDLE,
SCADABLE_STATE_CONNECTING,
SCADABLE_STATE_CONNECTED,
SCADABLE_STATE_DISCONNECTING,
SCADABLE_STATE_ERROR,
} scadable_state_t;scadable_on_event
scadable_on_eventvoid scadable_on_event(scadable_event_cb_t cb, void *user);Registers an event callback. Replaces any previous callback. Pass NULL to unregister.
typedef enum {
SCADABLE_EVT_CONNECTED,
SCADABLE_EVT_DISCONNECTED,
SCADABLE_EVT_PUBLISHED, // QoS1 PUBACK landed
SCADABLE_EVT_ERROR,
SCADABLE_EVT_OTA_AVAILABLE,
SCADABLE_EVT_ENV_CHANGED,
} scadable_event_type_t;
typedef struct {
scadable_event_type_t type;
union {
struct { uint32_t recovered_count; } connected;
struct { int32_t msg_id; } published;
struct {
scadable_err_t code;
int32_t esp_tls_err;
int32_t mbedtls_err;
uint32_t cert_verify_flags;
bool retriable;
} error;
struct { const char *new_version; } ota_available;
struct { const char *key; const char *new_value; } env_changed;
};
} scadable_event_t;
typedef void (*scadable_event_cb_t)(scadable_event_t event, void *user);Callbacks fire on the library's internal task. Do not block in the callback. Stash data and signal a worker if you need to do anything substantial.
static void on_event(scadable_event_t e, void *user) {
switch (e.type) {
case SCADABLE_EVT_CONNECTED:
xEventGroupSetBits(events, BIT_CONNECTED);
break;
case SCADABLE_EVT_OTA_AVAILABLE:
ESP_LOGI(TAG, "OTA available: %s", e.ota_available.new_version);
break;
default:
break;
}
}
scadable_on_event(on_event, NULL);Pre-sleep coordination
For scheduled-online or low-power devices that publish then deep-sleep.
scadable_flush
scadable_flushscadable_err_t scadable_flush(uint32_t timeout_ms);Blocks until all pending QoS1 publishes land or the timeout fires.
MUST be called before deep sleep if you've recently published QoS1. Otherwise the message can be lost when the socket goes down with the broker before PUBACK arrives.
| Parameter | Type | Description |
|---|---|---|
timeout_ms | uint32_t | Maximum time to wait. Recommended: 5000 to 10000. |
Returns: SCADABLE_OK (all PUBACKs landed), SCADABLE_ERR_TIMEOUT (some still pending), SCADABLE_ERR_NOT_CONNECTED.
scadable_publish(SCADABLE_CH_TELEMETRY, payload, len, 1);
scadable_flush(5000); // wait up to 5s
scadable_disconnect();
esp_deep_sleep_start();scadable_announce_offline
scadable_announce_offlinescadable_err_t scadable_announce_offline(uint32_t expected_offline_secs);Tells the cloud "I'm going dark for N seconds, don't alert."
For scheduled-online devices: a sensor that wakes once a day for 5 minutes calls scadable_announce_offline(24 * 3600) before sleeping. The cloud's offline-detection respects the announced window, so no alert fires until 24h plus the configured offline_grace elapses without the device coming back.
Returns: SCADABLE_OK, SCADABLE_ERR_NOT_CONNECTED.
Publishing
scadable_publish
scadable_publishscadable_err_t scadable_publish(scadable_channel_t channel,
const void *data, size_t len, uint8_t qos);Publishes a payload to a channel.
| Parameter | Type | Description |
|---|---|---|
channel | scadable_channel_t | Generated SCADABLE_CH_* enum from .scadable/config.toml. |
data | const void * | Payload bytes. Library copies into its own queue; you can free immediately after the call. |
len | size_t | Payload length in bytes. Hard cap at 64 KiB. |
qos | uint8_t | 0 (fire and forget) or 1 (at least once, with PUBACK). 2 is not supported. |
Returns: SCADABLE_OK, SCADABLE_ERR_NOT_CONNECTED, SCADABLE_ERR_INVALID_ARG, SCADABLE_ERR_BACKPRESSURE.
SCADABLE_ERR_BACKPRESSURE means the outbound queue is full. The library does not drop messages on its own; it returns the error and leaves it to you to decide whether to retry, sample, or buffer to flash. The recommended pattern is to wait for the next SCADABLE_EVT_PUBLISHED and retry then.
const char *body = "{\"event\":\"button_pressed\"}";
scadable_err_t err = scadable_publish(SCADABLE_CH_EVENTS,
body, strlen(body), 1);
if (err == SCADABLE_ERR_BACKPRESSURE) {
// try again after the next EVT_PUBLISHED
}Telemetry
Typed metric setters. Names come from SCADABLE_METRIC_* enums generated from .scadable/digital-twin/*.toml.
scadable_metric_set_u32
scadable_metric_set_u32scadable_err_t scadable_metric_set_u32(scadable_metric_t metric, uint32_t value);Sets an integer metric.
scadable_metric_set_f64
scadable_metric_set_f64scadable_err_t scadable_metric_set_f64(scadable_metric_t metric, double value);Sets a floating-point metric. Use this for f32 and f64 registers; the library handles the down-cast.
Returns (both): SCADABLE_OK, SCADABLE_ERR_NOT_CONNECTED, SCADABLE_ERR_INVALID_ARG (typed enum mismatch is a compile error, but raw uint32_t IDs in standalone mode are validated at runtime).
The library batches metric updates per the cadence declared on each register (rate = "30s") and publishes them on the dedicated telemetry channel. Setting a metric faster than its declared rate is fine; the library samples-down.
float temp = read_ds18b20();
scadable_metric_set_f64(SCADABLE_METRIC_TEMP_CELSIUS, temp);Logging
Macros, leveled, batched. The library captures __FILE__ and __LINE__ automatically.
SCADABLE_LOG_DEBUG / _INFO / _WARN / _ERROR
SCADABLE_LOG_DEBUG / _INFO / _WARN / _ERRORSCADABLE_LOG_DEBUG(fmt, ...);
SCADABLE_LOG_INFO(fmt, ...);
SCADABLE_LOG_WARN(fmt, ...);
SCADABLE_LOG_ERROR(fmt, ...);printf-style format string. __attribute__((format)) is set, so -Wformat warnings catch type mismatches at compile time.
Logs are batched per [logs].batch_interval in config.toml. Set batch_interval = "0s" to ship every line as it's emitted (more chatty, more responsive in dev).
SCADABLE_LOG_INFO("boot complete, %u ms", esp_log_timestamp());
SCADABLE_LOG_WARN("retry %d after %s", attempt, scadable_strerror(err));Underlying function
void scadable_log_(scadable_log_level_t lvl, const char *file, int line,
const char *fmt, ...);You normally use the macros, but the function is exposed for cases where you have a runtime-determined level (e.g. logging at the result level of an operation).
typedef enum {
SCADABLE_LOG_DEBUG_LEVEL,
SCADABLE_LOG_INFO_LEVEL,
SCADABLE_LOG_WARN_LEVEL,
SCADABLE_LOG_ERROR_LEVEL,
} scadable_log_level_t;Diagnostics
Remote-runnable tests. Operators dispatch from the dashboard; results stream back with timing and a structured message.
SCADABLE_TEST macro
SCADABLE_TEST macroSCADABLE_TEST(name, ctx_param) { ... }Defines a test handler. The name becomes both the function name in source and the test ID referenced from .scadable/diagnostics/*.yaml. The body returns one of TEST_PASS, TEST_PASS_WITH_WARN, or TEST_FAIL.
SCADABLE_TEST(check_sensor_health, ctx) {
TEST_LOG(ctx, "starting probe");
if (problem) return TEST_FAIL("sensor unreachable");
return TEST_PASS("temp=%.2f", t);
}Result macros
TEST_PASS(fmt, ...) // green check
TEST_PASS_WITH_WARN(fmt, ...) // yellow warning, fingerprinted
TEST_FAIL(fmt, ...) // red fail, fingerprinted fleet-wideEach takes a printf-style format string. The message is capped at 256 bytes; longer messages truncate. The library auto-measures duration_ms; you don't need to time it yourself.
TEST_LOG
TEST_LOGTEST_LOG(ctx, fmt, ...);Mid-test progress log. Visible in the dashboard's "Run check" results stream. Useful for long-running tests where the operator wants to see progress, not just the final verdict.
scadable_init_diagnostics
scadable_init_diagnosticsextern void scadable_init_diagnostics(void);Auto-generated. Walks every YAML in .scadable/diagnostics/ and registers the matching SCADABLE_TEST function. Customer calls it once after scadable_init:
scadable_init(NULL);
scadable_init_diagnostics();
scadable_connect();If you're building standalone (SCADABLE_NO_GENERATED), call scadable_register_test_(name, fn) for each test instead.
Underlying types
typedef enum {
TEST_RESULT_PASS = 0,
TEST_RESULT_PASS_WITH_WARN = 1,
TEST_RESULT_FAIL = 2,
} scadable_test_status_t;
typedef struct {
scadable_test_status_t status;
char message[256];
uint32_t duration_ms;
} scadable_test_result_t;Environment variables and secrets
Both ship from the dashboard at runtime. Env vars are NVS-cached so they survive an offline reboot. Secrets are RAM-only (a stolen flash dump leaks no secrets).
Env vars
const char *scadable_env_get(const char *key);
const char *scadable_env_get_or(const char *key, const char *fallback);
int32_t scadable_env_get_int(const char *key, int32_t fallback);
double scadable_env_get_double(const char *key, double fallback);
bool scadable_env_get_bool(const char *key, bool fallback);scadable_env_get(key) returns NULL if the key isn't set. scadable_env_get_or(key, fallback) is the safer variant that always returns a non-NULL pointer.
The pointer is owned by the library. Do not free. It remains valid until the next env-changed event for that key, at which point a new buffer is swapped in.
const char *api_url = scadable_env_get_or("DOWNSTREAM_API_URL",
"https://default.example.com");
int32_t timeout_ms = scadable_env_get_int("UPLOAD_TIMEOUT_MS", 5000);Secrets
const char *scadable_secret_get(const char *key);
const char *scadable_secret_get_or(const char *key, const char *fallback);Same semantics as env vars, different backing store. Secrets are encrypted at rest (per-namespace KMS key) and never written to flash on the device.
Caveat: secrets are only available after SCADABLE_EVT_CONNECTED. The initial fetch happens during the connect handshake. Reading a secret before connect returns NULL.
Watching for changes
typedef void (*scadable_env_change_cb_t)(const char *key,
const char *new_value,
void *user);
void scadable_on_env_change(scadable_env_change_cb_t cb, void *user);Callback fires when an env var or secret value changes. Replaces any previous callback. Same "do not block" rule as the event callback.
You can also watch the broader event stream and look for SCADABLE_EVT_ENV_CHANGED; this is the dedicated callback for code that only cares about env changes.
OTA
OTA is fully managed by the library. There is no scadable_ota_* API surface. The library:
- Receives OTA-available notifications over MQTT.
- Fires
SCADABLE_EVT_OTA_AVAILABLEwith the new version string. - Downloads the artifact to the inactive OTA slot.
- Validates the signature.
- Reboots into the new slot.
- Validates boot via the ESP-IDF rollback mechanism. If validation fails, bootloader rolls back to the previous slot automatically.
Customers do not need to call anything. The event is informational; you can use it to log or notify a UI, but you don't gate or trigger the apply.
Standalone build
If you build outside the SCADABLE pipeline (no codegen header), define SCADABLE_NO_GENERATED before including scadable.h. You then pass raw uint32_t IDs instead of typed enums:
#define SCADABLE_NO_GENERATED
#include "scadable.h"
void app_main(void) {
scadable_init(NULL);
scadable_connect();
scadable_publish(0, "world", 5, 1); // SCADABLE_CH_HELLO == 0
}Codegen-driven typed enums are recommended for production. Standalone is for the hello-world example and quick local experiments.
Updated 2 days ago
