How to implement a feature that allows both the device and the mobile app to proactively check for OTA updates?
This describes the workflow and implementation for User Approval OTA in ESP RainMaker. Unlike the "User Approval" option in the dashboard, this approach gives nodes the capability to decide whether to apply an update or wait for explicit user approval, providing better control and security over the update process.
Components
Node Side
- OTA Service (
esp.service.ota) with two parameters:Operation(esp.param.ota_operation): For receiving commandsInfo(esp.param.ota_info): For sharing update information
Client Application
- Monitors the
Infoparameter for available updates - Provides user interface for update approval
- Sends commands through the
Operationparameter
Workflow Steps
- Update Check Initiation — Can be triggered automatically (via
CONFIG_ESP_RMAKER_OTA_AUTOFETCH) or manually through thecheckoperation command. - Update Available — Node updates the
Infoparameter with update details and waits for user approval. Status reported asdelayedwith descriptionWaiting for Approval. - Update Approval — User reviews update information in app. App sends
startcommand upon user approval and caches the job ID for further querying via thenodes/ota_statusREST API. - Update Process — Node downloads firmware, validates it, applies the update, and reboots if successful.
- Update Monitoring — Node reports the update status as part of the regular OTA process. User can monitor via the
nodes/ota_statusREST API using the node ID and job ID.
JSON Structures
Node Configuration JSON
{
"node_id": "<node-id>",
"config_version": "2020-03-20",
"devices": [],
"services": [
{
"name": "OTA",
"type": "esp.service.ota",
"params": [
{
"name": "Operation",
"type": "esp.param.ota_operation",
"data_type": "string",
"properties": ["write"],
"valid_strs": ["check", "start"]
},
{
"name": "Info",
"type": "esp.param.ota_info",
"data_type": "object",
"properties": ["read"]
}
]
}
]
}
Node Parameters JSON
Initial State
{
"OTA": {
"Operation": "",
"Info": "{}"
}
}
When Update Available
{
"OTA": {
"Operation": "",
"Info": {
"job_id": "job_12345",
"fw_version": "2.0.0"
}
}
}
Parameter Update Messages
From App to Node (Operation)
{ "OTA": { "Operation": "check" } }
or
{ "OTA": { "Operation": "start" } }
From Node to App (Info)
{
"OTA": {
"Info": {
"job_id": "job_12345",
"fw_version": "2.0.0"
}
}
}
Implementation Example
Node Side Implementation
/* Create the OTA service */
esp_rmaker_device_t *ota_service = esp_rmaker_service_create("OTA", "esp.service.ota", NULL);
/* Add "Operation" string parameter with write property and valid values */
esp_rmaker_param_t *operation_param = esp_rmaker_param_create("Operation",
"esp.param.ota_operation",
esp_rmaker_str(""),
PROP_FLAG_WRITE);
const char *valid_strs[] = {"check", "start"};
esp_rmaker_param_add_valid_str_list(operation_param, valid_strs, 2);
esp_rmaker_device_add_param(ota_service, operation_param);
/* Add "Info" object parameter with read-only property */
esp_rmaker_param_t *info_param = esp_rmaker_param_create("Info",
"esp.param.ota_info",
esp_rmaker_obj("{}"),
PROP_FLAG_READ);
esp_rmaker_device_add_param(ota_service, info_param);
/* Register the write callback for the OTA service */
esp_rmaker_device_add_cb(ota_service, ota_service_write_cb, NULL);
/* Add the OTA service to the node */
esp_rmaker_node_add_device(node, ota_service);
/* Register custom OTA callback */
esp_rmaker_ota_config_t ota_config = {
.ota_cb = custom_ota_callback,
};
esp_rmaker_ota_enable(&ota_config, OTA_USING_TOPICS);
Write Callback Implementation
static esp_err_t ota_service_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 (strcmp(esp_rmaker_param_get_name(param), "Operation") == 0) {
if (strcmp(val.val.s, "start") == 0) {
ota_approved = true;
ESP_LOGI(TAG, "OTA approved.");
esp_rmaker_ota_fetch_with_delay(1);
} else if (strcmp(val.val.s, "check") == 0) {
ESP_LOGI(TAG, "Checking for OTA updates.");
esp_rmaker_ota_fetch_with_delay(1);
}
}
return ESP_OK;
}
Custom OTA Callback
static esp_err_t custom_ota_callback(esp_rmaker_ota_handle_t handle,
esp_rmaker_ota_data_t *ota_data)
{
esp_rmaker_param_t *info_param = esp_rmaker_device_get_param_by_name(ota_service, "Info");
/* If OTA is already approved, proceed with the update */
if (ota_approved) {
ota_approved = false; /* Approval valid only for one fetch */
esp_err_t err = esp_rmaker_ota_default_cb(handle, ota_data);
/* Clear the Info parameter after successful OTA update */
esp_rmaker_param_update_and_report(info_param, esp_rmaker_obj("{}"));
return err;
}
/* Update the Info parameter with job ID and firmware version */
if (info_param && (ota_data->ota_job_id || ota_data->fw_version)) {
json_gen_str_t jstr;
char json_str[128];
json_gen_str_start(&jstr, json_str, sizeof(json_str), NULL, NULL);
json_gen_start_object(&jstr);
if (ota_data->ota_job_id) {
json_gen_obj_set_string(&jstr, "job_id", ota_data->ota_job_id);
}
if (ota_data->fw_version) {
json_gen_obj_set_string(&jstr, "fw_version", ota_data->fw_version);
}
json_gen_end_object(&jstr);
json_gen_str_end(&jstr);
esp_rmaker_param_update_and_report(info_param, esp_rmaker_obj(json_str));
}
esp_rmaker_ota_report_status(handle, OTA_STATUS_DELAYED, "Waiting for Approval");
return ESP_OK;
}
Configuration Options
The following options are available under ESP RainMaker Config → ESP RainMaker OTA Config in menuconfig:
| Config | Default | Description |
|---|---|---|
CONFIG_ESP_RMAKER_OTA_AUTOFETCH | y | Automatically fetch OTA updates on boot |
CONFIG_ESP_RMAKER_OTA_AUTOFETCH_PERIOD | 0 (once on boot) | Periodic check interval in hours (0–168) |
Console Commands (for testing)
After initialising the console with esp_rmaker_console_init(), you can register an ota command:
static int ota_cmd_handler(int argc, char **argv)
{
if (argc < 2) {
ESP_LOGE(TAG, "Invalid arguments. Use 'ota start' or 'ota check'.");
return ESP_ERR_INVALID_ARG;
}
if (strcmp(argv[1], "start") == 0) {
ota_approved = true;
ESP_LOGI(TAG, "OTA approved via console command.");
esp_rmaker_ota_fetch_with_delay(1);
} else if (strcmp(argv[1], "check") == 0) {
ESP_LOGI(TAG, "Checking for OTA updates via console command.");
esp_rmaker_ota_fetch_with_delay(1);
} else {
ESP_LOGE(TAG, "Unknown argument: %s. Use 'start' or 'check'.", argv[1]);
return ESP_ERR_INVALID_ARG;
}
return ESP_OK;
}
void register_ota_command()
{
const esp_console_cmd_t ota_cmd = {
.command = "ota",
.help = "Manage OTA updates. Usage: ota <start|check>",
.hint = NULL,
.func = &ota_cmd_handler,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&ota_cmd));
}
Usage:
ota check # Check for updates
ota start # Start approved update