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_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).

ParameterTypeDescription
cfgconst 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_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_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_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

void 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_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.

ParameterTypeDescription
timeout_msuint32_tMaximum 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_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_err_t scadable_publish(scadable_channel_t channel,
                                const void *data, size_t len, uint8_t qos);

Publishes a payload to a channel.

ParameterTypeDescription
channelscadable_channel_tGenerated SCADABLE_CH_* enum from .scadable/config.toml.
dataconst void *Payload bytes. Library copies into its own queue; you can free immediately after the call.
lensize_tPayload length in bytes. Hard cap at 64 KiB.
qosuint8_t0 (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_err_t scadable_metric_set_u32(scadable_metric_t metric, uint32_t value);

Sets an integer metric.

scadable_metric_set_f64

scadable_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(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(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-wide

Each 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_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

extern 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:

  1. Receives OTA-available notifications over MQTT.
  2. Fires SCADABLE_EVT_OTA_AVAILABLE with the new version string.
  3. Downloads the artifact to the inactive OTA slot.
  4. Validates the signature.
  5. Reboots into the new slot.
  6. 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.