API Reference

The complete public surface of libscadable v0.1.0: three functions and one enum. The library is intentionally minimal — v0.1.0 ships the smallest demonstrable thing. Convenience layers (typed event publish, log forwarding, command handlers) accrete in v0.2.0+.

#include "scadable.h"

scadable_user_main

void scadable_user_main(void);

The library's entry hook into your code. The library calls this once after NVS + log initialization, before the network is up.

You define this; the library provides a weak no-op default. If you don't define it, the device boots into headless mode and idles (the library logs a warning).

Inside scadable_user_main:

  • You're free to block forever, return, or spawn your own tasks.
  • esp_netif_init() and esp_event_loop_create_default() have already been called.
  • The library's bootstrap is dormant until something produces an IP. Bringing up Wi-Fi (or Ethernet, or cellular) from inside scadable_user_main is what triggers the rest of the bootstrap.

Example

void scadable_user_main(void) {
    my_wifi_connect();      // your responsibility

    while (1) {
        do_application_work();
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Advanced: owning app_main

If you absolutely need lower-level control (e.g. peripheral init that must run before everything else), define your own app_main strongly:

void app_main(void) {
    my_pre_bootstrap_init();
    // ... the library's weak app_main is overridden ...
}

You're then responsible for any SCADABLE bootstrap; the library no longer does it for you. Rarely needed.


scd_qos_t

typedef enum {
    SCD_QOS_0 = 0,   /* at most once  — telemetry default */
    SCD_QOS_1 = 1,   /* at least once — important events */
    SCD_QOS_2 = 2,   /* exactly once  — rarely needed */
} scd_qos_t;

Standard MQTT QoS levels. The library's own heartbeat uses SCD_QOS_0; OTA status uses SCD_QOS_1.


scadable_mqtt_publish

esp_err_t scadable_mqtt_publish(
    const char *topic,
    const void *payload,
    int         len,
    scd_qos_t   qos,
    bool        retain
);

Publish raw bytes to a MQTT topic.

Parameters

ParamTypeDescription
topicconst char *Topic string. v0.1.0 has no generated topic constants yet — use literals.
payloadconst void *Pointer to payload bytes (typically JSON).
lenintLength of payload in bytes. Pass -1 for null-terminated strings.
qosscd_qos_tQoS level.
retainboolWhether the broker should retain this message.

Return values

ValueMeaning
ESP_OKSuccessfully enqueued for transmission.
ESP_ERR_INVALID_STATEMQTT not connected. Gate with scadable_mqtt_connected().
ESP_ERR_INVALID_ARGtopic or payload is NULL.
ESP_ERR_NO_MEMMQTT client outbox full.

Properties

  • Thread-safe — call from any task.
  • Non-blocking — returns immediately; transmission happens asynchronously.
  • Zero heap allocation in the call path — payload is handed straight to esp_mqtt_client_publish, which copies once into its outbox.

Example

char buf[64];
int n = snprintf(buf, sizeof(buf), "{\"v\":%.2f}", reading);
if (scadable_mqtt_connected()) {
    scadable_mqtt_publish("my/custom/topic", buf, n, SCD_QOS_0, false);
}

In v0.1.0 the library uses this function internally for its own heartbeat. You can call it for ad-hoc publishes; v0.2.0 adds typed helpers (scadable_publish_event) that wrap it with a metadata envelope and topic conventions.


scadable_mqtt_connected

bool scadable_mqtt_connected(void);

Returns true once the MQTT session is up.

Use this to gate scadable_mqtt_publish calls if you want to avoid the ESP_ERR_INVALID_STATE return on disconnect, or to defer some domain work until the platform link is live.

Example

while (!scadable_mqtt_connected()) {
    ESP_LOGI("app", "waiting for SCADABLE link");
    vTaskDelay(pdMS_TO_TICKS(1000));
}
ESP_LOGI("app", "online; starting sensor loop");

Topic conventions (v0.1.0)

The library uses three topics internally. You normally don't publish to these — they're the library's own channels — but knowing the convention helps when debugging with mosquitto_sub or reading dashboard data.

TopicDirectionPurposeQoS
scadable/<cn>/heartbeatdevice → backendPeriodic device-health publish0
scadable/<cn>/ota/commandbackend → deviceOTA dispatch (subscribed by library)(broker)
scadable/<cn>/ota/statusdevice → backendOTA progress + result1

<cn> is your device certificate's Common Name, format SC-<device_id>. The library assembles these topics at boot once the cert is loaded.

For ad-hoc publishes, pick any topic structure that makes sense for your data; the backend currently consumes the reserved topics above plus anything else routed through the broker's general subscription rules.


Heartbeat payload shape

Library publishes this every CONFIG_SCD_HEARTBEAT_INTERVAL_S seconds (default 30):

{
  "uptime_s":  1234,
  "free_heap": 187392,
  "boot":      1,
  "fw":        "0.1.0"
}

Fields are stable in v0.1.0; v0.2.0 may add more (RSSI, region, etc.) but won't remove existing ones.


OTA command payload (incoming)

Dashboard "Deploy" sends:

{
  "version": "0.2.3",
  "url":     "https://app.scadable.com/api/firmware/<artifact>.bin"
}

The library downloads via esp_https_ota, writes to the inactive OTA slot, restarts, and marks the new image valid on the first successful boot.


OTA status payload (outgoing)

Library publishes progress + result on scadable/<cn>/ota/status:

{
  "version": "0.2.3",
  "state":   "downloading|progress|success|failed",
  "details": "42% (256000/608000)"
}

details is omitted for the initial downloading event.


Standalone build

The library has no codegen step in v0.1.0; everything is hand-written C with no preprocessor magic. No SCADABLE_NO_GENERATED flag exists yet. The library compiles cleanly into any ESP-IDF project ≥ 5.1.

Stability

The v0.1.0 surface is intentionally tiny and stable. v0.2.0 will add functions (scadable_publish_event, log forwarding helpers, command registration) without breaking v0.1.0 callers.