Callbacks and events
Two mechanisms carry information into your firmware: parameter callbacks, which fire when something wants to change a device's state, and the event loop, which fires on SDK lifecycle transitions.
Write callbacks
A write callback is where your firmware reacts to "turn the light on". It is invoked for every write, regardless of who sent it.
Bulk (recommended)
The bulk callback receives every writable parameter in one request together. This matters whenever parameters are related — a light receiving {hue: 200, saturation: 80} should make one decision about colour, not two:
static esp_rmaker_error_t bulk_write_cb(const esp_rmaker_device_t *device,
const esp_rmaker_param_write_req_t write_req[],
uint8_t count, void *priv_data,
esp_rmaker_write_ctx_t *ctx)
{
if (ctx) {
OSAL_LOGI(TAG, "Write request via: %s", esp_rmaker_req_src_to_string(ctx->src));
}
for (uint8_t i = 0; i < count; i++) {
const esp_rmaker_param_t *param = write_req[i].param;
const esp_rmaker_param_val_t val = write_req[i].val;
const char *type = esp_rmaker_param_get_type(param);
osal_err_t err = OSAL_ERR_FAIL;
if (strcmp(type, ESP_RMAKER_PARAM_NAME) == 0) {
err = OSAL_ERR_OK; /* see the caveat below */
} else if (strcmp(type, ESP_RMAKER_PARAM_POWER) == 0) {
err = app_driver_set_power(val.val.b);
} else if (strcmp(type, ESP_RMAKER_PARAM_BRIGHTNESS) == 0) {
err = app_driver_set_brightness(val.val.i);
}
if (err == OSAL_ERR_OK) {
esp_rmaker_param_update(param, val);
}
}
return ESP_RMAKER_OK;
}
esp_rmaker_device_add_bulk_cb(device, bulk_write_cb, NULL);
Register it with esp_rmaker_device_add_bulk_cb(device, write_cb, read_cb).
Per-parameter
If your parameters are genuinely independent, the simpler signature is available:
static esp_rmaker_error_t write_cb(const esp_rmaker_device_t *device,
const esp_rmaker_param_t *param,
const esp_rmaker_param_val_t val,
void *priv_data,
esp_rmaker_write_ctx_t *ctx)
{
if (app_driver_set(esp_rmaker_param_get_type(param), val) == OSAL_ERR_OK) {
esp_rmaker_param_update(param, val);
}
return ESP_RMAKER_OK;
}
esp_rmaker_device_add_cb(device, write_cb, read_cb);
Register it with esp_rmaker_device_add_cb(device, write_cb, read_cb).
Confirm, don't assume
The SDK does not update a parameter for you when a write arrives. The contract is:
- Push the value to your hardware.
- If that succeeded, call
esp_rmaker_param_update()(oresp_rmaker_param_update_and_report()).
Skip step 2 on failure and the reported value stays at the last known-good state, which is exactly what the app should show. Silently updating the parameter after a failed hardware write is the most common source of "the app says it's on but it isn't".
The Name parameter caveat
Standard device helpers add a Name parameter so users can rename the device. The SDK normally handles name changes internally — but registering a bulk callback takes over the whole device, including esp.param.name. If you don't handle it, renaming silently stops working:
if (strcmp(type, ESP_RMAKER_PARAM_NAME) == 0) {
esp_rmaker_param_update(param, val); /* accept and report the new name */
continue;
}
Per-parameter callbacks are not affected.
Request sources
The ctx->src field tells you where a write came from. Some devices need to behave differently depending on origin — suppress a chime for scheduled changes, ignore local writes while a calibration runs:
| Source | Meaning |
|---|---|
ESP_RMAKER_REQ_SRC_INIT | Boot-time replay of a PROP_FLAG_PERSIST value from NVS. |
ESP_RMAKER_REQ_SRC_CLOUD | A cloud write — phone app, API, or voice assistant. |
ESP_RMAKER_REQ_SRC_SCHEDULE | A schedule fired. |
ESP_RMAKER_REQ_SRC_SCENE_ACTIVATE | Reserved — scenes are not implemented on the node yet. |
ESP_RMAKER_REQ_SRC_SCENE_DEACTIVATE | Reserved — scenes are not implemented on the node yet. |
ESP_RMAKER_REQ_SRC_LOCAL | A local-control client on the same network. |
ESP_RMAKER_REQ_SRC_FIRMWARE | Your own firmware, or a console command such as set-param. |
esp_rmaker_req_src_to_string() gives you a printable name. ctx may be NULL — check before dereferencing.
ESP_RMAKER_REQ_SRC_INIT arrives during esp_rmaker_node_init(), before your hardware is necessarily in a state to accept it. Initialise drivers before node init — as the examples do — or handle INIT specially.
Reporting state upward
| API | Behaviour |
|---|---|
esp_rmaker_param_update(param, val) | Updates the value and arms a delayed report. Further updates inside the window (CONFIG_RMAKER_STATE_REPORT_DELAY_MS, default 500 ms) reset the timer and coalesce into one publish. |
esp_rmaker_param_update_and_report(param, val) | Same, then cancels the timer and publishes immediately. |
esp_rmaker_param_update_and_notify(param, val) | Behaves as esp_rmaker_param_update(), then raises a notification to all connected applications. |
Prefer esp_rmaker_param_update(). Batching is why a light that receives a five-parameter write costs one MQTT publish instead of five. Reach for _and_report() when latency is visible to a user, such as confirming a physical button press.
Both calls also drive persistence, timeseries queueing, bounds validation, and automation-trigger evaluation.
The event loop
The SDK posts lifecycle events to a platform event loop. Register a handler before esp_rmaker_node_init() so you catch RMAKER_EVENT_INIT_DONE. The app_event_loop example component does this and logs everything, which is a good starting point:
app_event_loop_register_default_handler(); /* before node init */
RMAKER_EVENT — core lifecycle
| Event | Data |
|---|---|
RMAKER_EVENT_INIT_DONE | — |
RMAKER_EVENT_CORE_STARTED | — |
RMAKER_EVENT_CLAIM_STARTED | — |
RMAKER_EVENT_CLAIM_SUCCESSFUL | — |
RMAKER_EVENT_CLAIM_FAILED | — |
RMAKER_EVENT_LOCAL_CTRL_STARTED | Service name (string) |
RMAKER_EVENT_LOCAL_CTRL_STOPPED | — |
RMAKER_COMMON_EVENT — system, MQTT, timezone
| Event | Data |
|---|---|
RMAKER_EVENT_REBOOT | Seconds until reboot |
RMAKER_EVENT_NETWORK_RESET | — |
RMAKER_EVENT_FACTORY_RESET | — |
RMAKER_MQTT_EVENT_CONNECTED | — |
RMAKER_MQTT_EVENT_DISCONNECTED | — |
RMAKER_MQTT_EVENT_PUBLISHED | osal_mqtt_event_loop_data_on_complete_t * |
RMAKER_MQTT_EVENT_SUBSCRIBED | osal_mqtt_event_loop_data_on_complete_t * |
RMAKER_MQTT_EVENT_UNSUBSCRIBED | osal_mqtt_event_loop_data_on_complete_t * |
RMAKER_EVENT_TZ_POSIX_CHANGED | POSIX TZ string, e.g. "PST8PDT,M3.2.0,M11.1.0" |
RMAKER_EVENT_TZ_CHANGED | Location string, e.g. "America/Los_Angeles" |
RMAKER_EVENT_TZ_CHANGED is always accompanied by RMAKER_EVENT_TZ_POSIX_CHANGED, but not the other way around — a client may set the POSIX string directly. See Time and timezone.
Two events deserve handlers in most products: RMAKER_MQTT_EVENT_CONNECTED and RMAKER_MQTT_EVENT_DISCONNECTED, so you can show connectivity on an LED. Add the RMAKER_EVENT_CLAIM_* trio if you ship with assisted claiming — key generation and the claim exchange can take a while at first setup, and a device that looks dead generates support calls.
Handler rules
Event handlers run on the event-loop task. Do not block in them — no long delays, no synchronous network calls. Post to your own queue and return.
The RMAKER_EVENT_REBOOT / NETWORK_RESET / FACTORY_RESET events are your chance to persist state before the node goes down; the delays configured on the system service exist precisely to give you that window. See System service.
Related
- Data model — defining the parameters these callbacks receive
- Serial console — driving your callbacks by hand with
set-param - Firmware specifications → State Management — the wire-level view of writes and reports
- Firmware specifications → Cloud Communication