Skip to main content

Code basics

A RainMaker Neo application is a single top-to-bottom startup function. Typically you,

  • bring up your device drivers
  • bring up the hardware and the network,
  • describe your device's parameters/structures to the SDK
  • enable any optional services you want
  • start the agent

After that the SDK manages the MQTT connection and makes a call to your callbacks when something changes.

This page walks through a 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 enable call; see Schedules and automations.
  • Automation triggers — parameter-threshold triggers, evaluated on every esp_rmaker_param_update().
  • Parameter persistence — any parameter with PROP_FLAG_PERSIST is restored from NVS on boot and replayed through your write callback with ESP_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.