Config schema
Each diagnostic is one YAML file in .scadable/diagnostics/. The file name is cosmetic — only the id: field is wired into codegen and cloud ingestion.
Minimal example
# .scadable/diagnostics/motor-health.yaml
id: motor_health
type: function
name: "Motor Bearing Health"
description: "Reads vibration RMS + bearing temp; fails if either exceeds baseline."
timeout_secs: 10
run_after_ota: true
function:
symbol: motor_healthFive fields are mandatory: id, type, name, description, and the per-type config block matching type: (here function: with a symbol:). Everything else has a default.
Field reference
| Field | Required | Type | Default | Description |
|---|---|---|---|---|
id | Yes | string | — | Stable identifier. Regex ^[a-z][a-z0-9_]{1,62}$. Used as the dashboard ID and as the correlation key from cloud trigger to chip-side dispatch. Renaming this is a breaking change — see "Rename semantics" below. |
type | Yes | enum | — | function is the only value v1 chips can run. Future types (api_call, sensor_read, mqtt_check, etc.) are reserved and accepted by the cloud, but a v1 chip returns TEST_RESULT_TYPE_NOT_SUPPORTED if asked to run them. |
name | Yes | string | — | Operator-facing label. Truncated to 60 chars in dashboard rows. Hard cap 80 chars. |
description | Yes | string | — | Multi-line OK. Rendered as Markdown in the dashboard drawer. Cap 500 chars. |
timeout_secs | No | int | 30 | Wall-clock cap on the test function. Library returns TEST_RESULT_TIMEOUT if exceeded. Range 1–300. |
run_after_ota | No | bool | false | When true, the cloud auto-runs this diagnostic on every device after every successful OTA apply. The release status flips to verifying while it runs. Auto-rollback fires if it fails. See Post-OTA verification. |
required_sensors | No | list[string] | [] | Cosmetic in v1. List of register names from .scadable/digital-twin/ this test reads. Future "this test cannot run on this gateway class" UI will use this. |
<type>: | Yes | mapping | — | Per-type config nested under a key matching type:. For function, the only required field is symbol: (the C identifier the customer registers). |
Three realistic diagnostics
# .scadable/diagnostics/motor-health.yaml
id: motor_health
type: function
name: "Motor Bearing Health"
description: |
Reads vibration RMS + bearing temperature, compares against the baseline
learned during commissioning, and fails if either exceeds the configured
threshold.
timeout_secs: 10
run_after_ota: true
required_sensors:
- vibration_rms_g
- bearing_temp_c
function:
symbol: motor_health# .scadable/diagnostics/thermal-sweep.yaml
id: thermal_sweep
type: function
name: "Thermal Sweep (slow)"
description: |
Steps the heater through 25 to 80 C in 5 C increments and validates the
thermistor tracks within 2 C. Takes about 45 s — do NOT enable
run_after_ota for this one, or every deploy stalls a minute.
timeout_secs: 120
run_after_ota: false
function:
symbol: thermal_sweep# .scadable/diagnostics/network-roundtrip.yaml
id: network_roundtrip
type: function
name: "Network Round-trip"
description: "Pings the broker over MQTT and validates RTT under 500 ms."
timeout_secs: 5
run_after_ota: true
function:
symbol: network_roundtripValidation rules
The build pipeline validates every YAML at codegen time and the cloud re-validates on ingestion. Failures are surfaced in the release detail page in the dashboard.
| Rule | Failure code |
|---|---|
id matches ^[a-z][a-z0-9_]{1,62}$ | SCAD_E1001: invalid diagnostic id |
name length 1–80 | SCAD_E1002: name too long |
description length 1–500, UTF-8 | SCAD_E1003: description too long |
timeout_secs integer in 1–300 | SCAD_E1004: timeout_secs out of range |
required_sensors[N] matches a register declared in .scadable/digital-twin/ (warning only if folder absent) | SCAD_E1005: required_sensors[N] references unknown register "X" |
type is a value v1 chips can run | SCAD_E1006: type not supported in this libscadable version, requires v0.X.Y+ |
SCAD_E1006 is the one to watch as you adopt future diagnostic types — see "Future types" below.
How changes propagate
The flow from git commit to "the cloud knows about your diagnostic":
git push --tags
│
▼
GitHub webhook fires (push event with vX.Y.Z tag)
│
▼
SCADABLE GitHub App clones at the tag SHA
│
▼
Build pipeline parses .scadable/diagnostics/*.yaml
│
▼
Codegen emits scadable_generated.c (the registration calls) and links into firmware
│
▼
Same parsed catalog is POSTed to the cloud's diagnostic-catalog ingestion endpoint
│
▼
service-app upserts diagnostic_definitions for this release
│
▼
Cloud now knows your diagnostics; dashboard shows them on the next gateway view
The ingestion is keyed on (code_project_id, id). New entries insert. Renames or deletions soft-delete the old row but preserve its run history forever. The release detail page shows orphaned definitions with a "(removed)" pill so you can still inspect old failures.
Rename semantics
Changing id: foo to id: bar is an orphan-and-new operation, not a rename. The cloud cannot disambiguate "I renamed foo to bar" from "I deleted foo and added a brand-new bar that happens to read the same sensors."
When you push the rename:
- The new
barrow inserts. Its run history starts from this release. - The old
foorow is soft-deleted (itsdeleted_atis set). Its history is preserved indefinitely. - The dashboard shows
fooruns in history with a "(removed)" pill;barshows no history yet.
If you need a true rename later, file an issue — a replaces: foo field is on the roadmap. Until then, only rename when you're OK losing the per-test history continuity.
Future types
The type: field exists so the framework can grow without breaking existing chips. v1 only ships type: function, but the cloud already accepts other type values:
# .scadable/diagnostics/weather-api-check.yaml — NOT runnable on v1 chips
id: weather_api_check
type: api_call
name: "Weather API reachable"
description: "Hits the upstream weather API; fails if status != 200."
timeout_secs: 5
run_after_ota: false
api_call:
url: "https://api.openweathermap.org/data/2.5/weather"
method: GET
expect_status: 200
expect_body_contains: "main"If you add this to your repo today, the cloud accepts the row and the dashboard shows the diagnostic in your gateway's tab — but with a badge that says "This diagnostic type requires a newer firmware (v0.X.Y+). Upgrade your gateway to enable." Any cloud-triggered run yields immediate TEST_RESULT_TYPE_NOT_SUPPORTED with no firmware crash.
This forward-compatibility means you can declare api_call diagnostics now and have them spring to life the moment you bump libscadable past the version that adds the type.
Where this lives in the customer repo
.scadable/
├── config.toml
├── digital-twin/
│ └── *.toml
├── diagnostics/
│ ├── motor-health.yaml
│ ├── thermal-sweep.yaml
│ └── network-roundtrip.yaml
└── .schema/ (auto-generated by `scadable init`; .gitignored)
The .schema/ folder gets you autocomplete in editors that respect # yaml-language-server: $schema=... comments — handy but not required.
See also
- C API: the firmware side. The functions you bind to each
id. - Post-OTA verification: how
run_after_otaactually works end-to-end. - .scadable/ folder spec: the rest of the per-repo configuration.
Updated 1 day ago
