Code basics
A RainMaker Neo application is a single top-to-bottom startup function. You bring up hardware and the network, describe your device to the SDK, enable the services you want, and start the agent. After that the SDK owns the MQTT connection and calls back into your code when something changes.
This page walks the reference implementation — examples/light/main/app_main.c — in order. Everything shown here is identical on ESP-IDF and POSIX.
The shape of an application
#include <esp_rmaker_core.h> /* pulls in node, data model, standard types, events */
#include <esp_rmaker_ota.h> /* optional: OTA */
osal_err_t app_run(void)
{
/* 1. Serial console — do this first so you can talk to a node that fails later */
esp_rmaker_console_init();
/* 2. Your hardware */
app_driver_init();
/* 3. Storage, then the network stack */
osal_storage_init(NULL);
app_network_init();
/* 4. Provisioning — must complete before node init */
app_network_provision(RMNG_MFG_DATA_DEVICE_TYPE_LIGHT,
RMNG_MFG_DATA_DEVICE_SUBTYPE_LIGHT);
/* 5. Event handler — register before init so you catch RMAKER_EVENT_INIT_DONE */
app_event_loop_register_default_handler();
/* 6. Initialise the node */
esp_rmaker_config_t cfg = { .enable_time_sync = true };
esp_rmaker_node_t *node = esp_rmaker_node_init(&cfg, "Light", "light");
if (!node) {
OSAL_LOGE(TAG, "Could not initialise node. Aborting!!!");
return OSAL_ERR_FAIL;
}
/* 7. Describe your device */
light_device = esp_rmaker_lightbulb_device_create("Light", NULL, app_driver_get_power());
esp_rmaker_device_add_bulk_cb(light_device, bulk_write_cb, NULL);
esp_rmaker_device_add_param(light_device,
esp_rmaker_brightness_param_create(ESP_RMAKER_DEF_BRIGHTNESS_ID, app_driver_get_brightness()));
/* … more params … */
esp_rmaker_node_add_device(node, light_device);
/* 8. Optional services */
esp_rmaker_local_ctrl_service_enable();
esp_rmaker_timezone_service_enable();
esp_rmaker_system_service_enable(&system_serv_config);
/* 9. OTA — before starting the agent */
esp_rmaker_ota_config_t ota_config = { .ota_diag = ota_diag_fn };
esp_rmaker_ota_enable(&ota_config);
/* 10. Connect */
esp_rmaker_start();
return OSAL_ERR_OK;
}
app_run() returns as soon as the agent is started. The SDK keeps working on its own tasks — the MQTT agent, the work queue, and the state-report timer.
Why the order matters
The sequence is not arbitrary. Four constraints drive it:
Provisioning before node init. Provisioning is the most memory-hungry thing the firmware ever does — a BLE stack, RSA signing, protocomm sessions. The SDK therefore splits initialisation in two: esp_rmaker_pre_prov_init() brings up only what provisioning needs, and esp_rmaker_pre_prov_deinit() frees it again before the rest of the SDK comes up. app_network_provision() collapses that whole sequence into one call:
If you skip provisioning entirely — a POSIX build, or a device whose credentials are pre-flashed — you can call esp_rmaker_node_init() straight away. It runs the pre-provisioning init internally if it hasn't happened yet.
Event handler before node init. RMAKER_EVENT_INIT_DONE fires inside esp_rmaker_node_init(). Register your handler first or you will miss it.
Everything described before esp_rmaker_start(). Devices, parameters, and services are what the node reports as its configuration. esp_rmaker_start() connects to MQTT, subscribes, fetches group and integration data from the cloud, and publishes the node configuration if its checksum has changed. Adding a device after that point means the cloud's view of the node is stale until the next config report.
OTA before esp_rmaker_start(). esp_rmaker_ota_enable() also runs the first-boot rollback diagnostics, which must happen before the agent starts. See OTA.
Node, devices, parameters
Three levels, and one handle type for each:
Node (esp_rmaker_node_t) — the physical thing; one per firmware image
└── Device (esp_rmaker_device_t) — a controllable unit: a light, a fan, a sensor
└── Parameter (esp_rmaker_param_t) — one value: power, brightness, temperature
esp_rmaker_node_init(config, name, type) creates the node. name and type are what the phone app and dashboard display. The model and firmware version are filled in automatically from PROJECT_NAME and PROJECT_VER — the recommended behaviour; overriding them is possible but discouraged.
Services are devices with a different role. esp_rmaker_service_create() produces the same handle type as esp_rmaker_device_create(); the distinction is semantic — a service exposes control surface rather than a user-facing appliance. The SDK's own timezone, system, and local-control services are built this way, and so is anything custom you add.
For the full data-model surface — standard types, custom types, property flags, bounds, UI hints, attributes and tags — see Data model.
Reporting state, and being told to change it
Two directions, two mechanisms.
Cloud → device. Register a write callback. The SDK invokes it whenever a parameter write arrives, whatever the source — the phone app, a schedule firing, a scene activating, a local-control client, or a console command:
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)
{
for (uint8_t i = 0; i < count; i++) {
const char *type = esp_rmaker_param_get_type(write_req[i].param);
if (strcmp(type, ESP_RMAKER_PARAM_POWER) == 0) {
if (app_driver_set_power(write_req[i].val.val.b) == OSAL_ERR_OK) {
esp_rmaker_param_update(write_req[i].param, write_req[i].val);
}
}
/* … */
}
return ESP_RMAKER_OK;
}
Note the pattern: drive the hardware first, then call esp_rmaker_param_update(). The SDK does not assume a write succeeded. If your driver rejects the value, don't update the parameter, and the cloud's view stays at the old value.
Device → cloud. Call esp_rmaker_param_update() when local state changes — a button press, a sensor reading. Reports are coalesced: the first call arms a timer (CONFIG_RMAKER_STATE_REPORT_DELAY_MS, 500 ms by default) and every update inside that window ships in one MQTT publish. Use esp_rmaker_param_update_and_report() when you need the report to go out immediately.
Callbacks and events covers bulk vs per-parameter callbacks, read callbacks, request sources, and the event loop.
What you get for free
Some things need no code at all. esp_rmaker_node_init() brings up:
- Schedules — the whole schedule service, backed by NVS so schedules survive reboots. There is no
enablecall; see Schedules and automations. - Automation triggers — parameter-threshold triggers, evaluated on every
esp_rmaker_param_update(). - Parameter persistence — any parameter with
PROP_FLAG_PERSISTis restored from NVS on boot and replayed through your write callback withESP_RMAKER_REQ_SRC_INIT. - State reporting, shadow management, reconnect and retry — see Firmware specifications → State Management and Error Handling.
Everything else is opt-in. Feature overview lists what to call for what.
Error handling
SDK functions return esp_rmaker_error_t; the platform layer returns osal_err_t. Handle constructors (esp_rmaker_node_init, esp_rmaker_device_create, esp_rmaker_*_param_create) return NULL on failure.
The examples use APP_RETURN_ON_ERR from app_entry.h to keep app_run() readable — it logs and returns early on any non-OSAL_ERR_OK value. A startup failure is not fatal on ESP-IDF (app_main simply returns and the node sits there) but does set a non-zero exit code on POSIX, which is what CI keys off.
Related
- Data model
- Callbacks and events
- Serial console — inspect a running node
- Firmware specifications → Initialization — component init order, prerequisites, and failure points