Skip to main content

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.

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:

  1. Push the value to your hardware.
  2. If that succeeded, call esp_rmaker_param_update() (or esp_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:

SourceMeaning
ESP_RMAKER_REQ_SRC_INITBoot-time replay of a PROP_FLAG_PERSIST value from NVS.
ESP_RMAKER_REQ_SRC_CLOUDA cloud write — phone app, API, or voice assistant.
ESP_RMAKER_REQ_SRC_SCHEDULEA schedule fired.
ESP_RMAKER_REQ_SRC_SCENE_ACTIVATEReserved — scenes are not implemented on the node yet.
ESP_RMAKER_REQ_SRC_SCENE_DEACTIVATEReserved — scenes are not implemented on the node yet.
ESP_RMAKER_REQ_SRC_LOCALA local-control client on the same network.
ESP_RMAKER_REQ_SRC_FIRMWAREYour 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.

Watch for INIT

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

APIBehaviour
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

EventData
RMAKER_EVENT_INIT_DONE
RMAKER_EVENT_CORE_STARTED
RMAKER_EVENT_CLAIM_STARTED
RMAKER_EVENT_CLAIM_SUCCESSFUL
RMAKER_EVENT_CLAIM_FAILED
RMAKER_EVENT_LOCAL_CTRL_STARTEDService name (string)
RMAKER_EVENT_LOCAL_CTRL_STOPPED

RMAKER_COMMON_EVENT — system, MQTT, timezone

EventData
RMAKER_EVENT_REBOOTSeconds until reboot
RMAKER_EVENT_NETWORK_RESET
RMAKER_EVENT_FACTORY_RESET
RMAKER_MQTT_EVENT_CONNECTED
RMAKER_MQTT_EVENT_DISCONNECTED
RMAKER_MQTT_EVENT_PUBLISHEDosal_mqtt_event_loop_data_on_complete_t *
RMAKER_MQTT_EVENT_SUBSCRIBEDosal_mqtt_event_loop_data_on_complete_t *
RMAKER_MQTT_EVENT_UNSUBSCRIBEDosal_mqtt_event_loop_data_on_complete_t *
RMAKER_EVENT_TZ_POSIX_CHANGEDPOSIX TZ string, e.g. "PST8PDT,M3.2.0,M11.1.0"
RMAKER_EVENT_TZ_CHANGEDLocation 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.