Troubleshooting

Match your symptom against the headings below. Each entry shows the exact log line you'd see, the actual cause, and the fix.

Build fails: "scadable_generated.h: No such file"

fatal error: scadable_generated.h: No such file or directory
   30 |   include "scadable_generated.h"

Cause: You're building outside the SCADABLE pipeline, but the header expects codegen output that only the pipeline produces.

Fix: Either build via the pipeline (push a tag and let the cloud build) or define SCADABLE_NO_GENERATED before the include for local-only builds:

#define SCADABLE_NO_GENERATED
#include "scadable.h"

In standalone mode you pass raw uint32_t IDs instead of typed enums.

scadable_init returns SCADABLE_ERR_NO_CERT

E (1234) hello-world: scadable_init failed: no cert

Cause: NVS namespace scadable_certs is empty. The device was never provisioned through the SCADABLE web flasher.

Fix: Re-flash through the dashboard's "Flash a device" flow. The web flasher writes the SCADABLE provisioner image, which (after captive-portal WiFi setup + activation code) enrolls a per-device cert via EST and stores it in scadable_certs. After provisioning succeeds, the customer firmware can scadable_init(NULL) and read the cert from NVS.

If you're testing without a real device, define SCADABLE_NO_GENERATED and use the hello-world example, which is meant for unprovisioned boards.

Connect hangs in CONNECTING

I (5012) scadable: state: CONNECTING
I (35012) scadable: state: CONNECTING
I (65012) scadable: state: CONNECTING

Cause: Network reachability or TLS handshake. The library retries with exponential backoff, so it will sit in CONNECTING indefinitely without giving up.

Diagnosis: Register an event callback and inspect the error payload on SCADABLE_EVT_ERROR:

case SCADABLE_EVT_ERROR:
    ESP_LOGE(TAG, "scadable error: code=%d esp_tls=%d mbedtls=%d retriable=%d",
             e.error.code,
             e.error.esp_tls_err,
             e.error.mbedtls_err,
             e.error.retriable);
    break;

Common patterns:

  • code=SCADABLE_ERR_NETWORK, retriable=true: WiFi up, broker unreachable. Check that the device's WiFi SSID can reach the broker URL baked into your config.toml (default mqtts://io.scadable.com:8883).
  • code=SCADABLE_ERR_TLS, mbedtls_err != 0: TLS handshake failed. Most often a clock-skew issue (the device's clock is way off, so cert validity windows fail). The provisioner ships SNTP, but if you're booting on a network with no NTP reachability the clock stays at 1970.
  • cert_verify_flags != 0: cert chain verification failed. The CA bundle in NVS is stale; re-provision.

scadable_publish returns SCADABLE_ERR_BACKPRESSURE

E (45123) app: scadable_publish: backpressure

Cause: The outbound queue is full. Either you're publishing faster than the network can drain, or the network is down and messages are stacking up.

Fix: Two patterns, depending on what you want.

  • Drop on backpressure (recommended for high-frequency telemetry): check the return value, drop the sample, move on. Don't retry inline; the queue is full because the link is slow.
  • Retry on the next published event: subscribe to SCADABLE_EVT_PUBLISHED and retry the most recent failed publish from there. Watch out for unbounded retry loops.

If you're seeing this consistently in steady state, your declared default_rate in config.toml is too aggressive for the link. Consider switching to QoS 0 for high-frequency channels (telemetry that can tolerate loss) and reserving QoS 1 for events.

Deep sleep loses messages

[device] scadable_publish OK
[device] esp_deep_sleep_start
[dashboard] (message never arrives)

Cause: You published QoS 1 then deep-slept before the PUBACK arrived. The MQTT broker had no chance to confirm; the socket dropped before the response.

Fix: Always call scadable_flush(timeout_ms) between the last QoS 1 publish and esp_deep_sleep_start:

scadable_publish(SCADABLE_CH_TELEMETRY, payload, len, 1);
scadable_flush(5000);                 // wait up to 5s for PUBACK
scadable_disconnect();
esp_deep_sleep_start();

scadable_flush returns SCADABLE_OK if everything landed, SCADABLE_ERR_TIMEOUT if some are still pending after the timeout.

OTA event fires but device doesn't update

I (32145) scadable: OTA available: v0.1.2
(no further activity)

Cause: The library schedules the OTA apply in the background; if the device is doing something heavy (a long-running telemetry burst, a busy main loop) the OTA apply may queue behind it.

Diagnosis: Watch for SCADABLE_LOG_INFO("ota: applying %s", new_version) and SCADABLE_LOG_INFO("ota: validated, rebooting"). If you see "applying" but no reboot, the artifact failed signature validation; check the dashboard's Releases view for the build's signing status.

If you see neither, the library is starved on the network. Reduce telemetry burst size or wait for an idle window.

Fix nothing on the firmware side. The library handles apply automatically. If apply genuinely fails (signature mismatch, partition out of space), the next reboot keeps the existing slot and the dashboard records the failure. There is no customer-facing apply API.

Diagnostic test never appears in dashboard

I (5123) scadable: init_diagnostics registered 0 tests

Cause: Codegen ran but found no SCADABLE_TEST(name) declarations matching the YAML files in .scadable/diagnostics/.

Diagnosis: Run the build pipeline, then check the build log for SCAD_E0021 errors. The pattern is:

SCAD_E0021: YAML diagnostics/sensor-health.yaml line 3 references
`check_sensor_health` but no `SCADABLE_TEST(check_sensor_health)` found.
Did you mean `check_sensor_healht` (main.c:42)?

Fix: The test name in YAML must exactly match the function name in source (case-sensitive, snake_case). The error message includes did-you-mean suggestions for typos.

Also confirm you're calling scadable_init_diagnostics() exactly once after scadable_init. Forgetting this is the #2 reason tests don't appear.

Env var read returns NULL right after connect

const char *url = scadable_env_get("DOWNSTREAM_API_URL");
// url is NULL even though the dashboard has the value set

Cause: You're reading before SCADABLE_EVT_CONNECTED fires. The initial env-var fetch happens during the connect handshake; the cache isn't populated until connect completes.

Fix: Either wait for SCADABLE_EVT_CONNECTED before reading, or use scadable_env_get_or with a sensible fallback.

// Pattern 1: wait for connect
xEventGroupWaitBits(events, BIT_CONNECTED, pdFALSE, pdTRUE, portMAX_DELAY);
const char *url = scadable_env_get("DOWNSTREAM_API_URL");

// Pattern 2: fallback
const char *url = scadable_env_get_or("DOWNSTREAM_API_URL",
                                      "https://default.example.com");

Secrets behave the same way (RAM-only, fetched during connect).

Build pipeline rejects config.toml

SCAD_E0007: .scadable/config.toml: profile = "custom" requires
expected_interval, offline_grace, default_rate, batch_interval

Cause: You set profile = "custom" but didn't fill in all the fields the preset would have provided.

Fix: Either pick a named preset (always_on, scheduled, low_power) or fill in every required field for custom. The error message lists which fields are missing.

Logs show up out of order

Cause: Log batching. The library bundles logs by [logs].batch_interval (default 5s) before pushing. Within a batch, ordering is preserved; across batches under network reorder, ordering may shuffle.

Fix: For development, set batch_interval = "0s" in config.toml to ship every line as it's emitted. For production, the order across batches is approximate; use timestamps in the message itself if exact ordering matters.

Where to find the broker URL

The broker URL is baked in at build time from .scadable/config.toml (default mqtts://io.scadable.com:8883). To override at runtime (e.g. point at a staging broker), pass a scadable_config_t to scadable_init:

scadable_config_t cfg = {
    .broker_url = "mqtts://staging.io.scadable.com:8883",
    .device_id = NULL,
    .keepalive_secs = 30,
    .log_batch_secs = 0,
};
scadable_init(&cfg);

Still stuck

  1. Check the dashboard's Releases view for build errors first. Most "device isn't behaving" issues turn out to be a build that didn't ship.
  2. Capture the device's serial output with idf.py monitor and grep for scadable:. The library logs every state transition.
  3. File an issue on scadable/libscadable with the serial log + your .scadable/config.toml.