From ca89bff073adc83927487a61aadfbf2d2ede7c88 Mon Sep 17 00:00:00 2001 From: Johny Mattsson Date: Sat, 6 Jul 2019 22:21:08 +1000 Subject: [PATCH] OTA support for ESP32 (#2812) * Implemented otaupgrade module. * Added partition table example for otaupgrade. * Copy-paste omission. Whoops. * Updated otaupgrade docs after review. --- components/modules/Kconfig | 10 +- components/modules/otaupgrade.c | 253 ++++++++++++++++++++++++++++++++ docs/modules/otaupgrade.md | 232 +++++++++++++++++++++++++++++ 3 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 components/modules/otaupgrade.c create mode 100644 docs/modules/otaupgrade.md diff --git a/components/modules/Kconfig b/components/modules/Kconfig index d8c2ff7b..44461973 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -159,6 +159,14 @@ config LUA_MODULE_OW help Includes the 1-Wire (ow) module (recommended). +config LUA_MODULE_OTAUPGRADE + bool "Over-The-Air upgrade module" + default "n" + help + Includes the over-the-air firmware upgrade module. Use of this requires + a partition table with at least two OTA partitions, plus the OTA data + partition. See the IDF documentation for details. + config LUA_MODULE_QRCODEGEN bool "QR Code Generator module" default "n" @@ -240,7 +248,7 @@ config LUA_MODULE_WS2812 Includes the ws2812 module. config LUA_MODULE_TIME - bool "time module" + bool "Time module" default "n" help Includes the time module. diff --git a/components/modules/otaupgrade.c b/components/modules/otaupgrade.c new file mode 100644 index 00000000..92546b54 --- /dev/null +++ b/components/modules/otaupgrade.c @@ -0,0 +1,253 @@ +/* + * Copyright 2019 Dius Computing Pty Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * - Neither the name of the copyright holders nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Johny Mattsson + */ + +#include "module.h" +#include "lauxlib.h" + +#include "esp_system.h" +#include "esp_ota_ops.h" +#include "esp_partition.h" + +static esp_ota_handle_t ota_handle; +static const esp_partition_t *next; + +// ----------otaupgrade Lua API ------------------------------------------ + + +// Lua: otaupgrade.commence() -- wipes an inactive slot and enables .write() +static int lotaupgrade_commence (lua_State* L) +{ + next = esp_ota_get_next_update_partition (NULL); + if (!next) + return luaL_error (L, "no OTA partition available"); + + esp_err_t err = esp_ota_begin (next, OTA_SIZE_UNKNOWN, &ota_handle); + const char *msg = NULL; + switch (err) { + case ESP_OK: break; + case ESP_ERR_NO_MEM: msg = "out of memory"; break; + case ESP_ERR_OTA_PARTITION_CONFLICT: // I don't think we can get this? + msg = "can't overrite running firmware"; break; + case ESP_ERR_OTA_SELECT_INFO_INVALID: + msg = "ota data partition invalid"; break; + case ESP_ERR_OTA_ROLLBACK_INVALID_STATE: + msg = "can't upgrade from unconfirmed firmware"; break; + default: msg = "ota error"; break; + } + if (msg) + return luaL_error (L, msg); + else + return 0; +} + +// Lua: otaupgrade.write(data) -- writes the data block to flash +static int lotaupgrade_write (lua_State *L) +{ + const char *bytes = luaL_checkstring (L, 1); + size_t len = lua_objlen (L, 1); + + esp_err_t err = esp_ota_write (ota_handle, bytes, len); + const char *msg = NULL; + switch (err) { + case ESP_OK: break; + case ESP_ERR_INVALID_ARG: + msg = "write not possible, use otaupgrade.commence() first"; break; + case ESP_ERR_OTA_VALIDATE_FAILED: msg = "not a valid ota image"; break; + default: msg = "ota error"; break; + } + if (msg) + return luaL_error (L, msg); + else + return 0; +} + + +// Lua: otaupgrade.complete(optional_reboot) +static int lotaupgrade_complete (lua_State *L) +{ + if (!next) + return luaL_error (L, "no upgrade in progress"); + + esp_err_t err = esp_ota_end(ota_handle); + const char *msg = NULL; + switch (err) { + case ESP_OK: break; + case ESP_ERR_INVALID_ARG: msg = "empty firmware image"; break; + case ESP_ERR_OTA_VALIDATE_FAILED: msg = "validation failed"; break; + default: msg = "ota error"; break; + } + if (msg) + return luaL_error (L, msg); + + err = esp_ota_set_boot_partition (next); + next = NULL; + + if (luaL_optint (L, 1, 0)) + esp_restart (); + + return 0; +} + +// Lua: otaupgrade.accept() +static int lotaupgrade_accept (lua_State *L) +{ + esp_err_t err = esp_ota_mark_app_valid_cancel_rollback(); + if (err != ESP_OK) // only ESP_OK defined as expected return value + return luaL_error(L, "firmware accept failed"); + else + return 0; +} + +// Lua: otaupgrade.rollback() +static int lotaupgrade_rollback (lua_State *L) +{ + esp_err_t err = esp_ota_mark_app_invalid_rollback_and_reboot(); + const char *msg = NULL; + switch (err) { + case ESP_OK: break; + case ESP_ERR_OTA_ROLLBACK_FAILED: + msg = "no other firmware to roll back to"; break; + default: + msg = "ota error"; break; + } + if (msg) + return luaL_error(L, msg); + else + return 0; // actually, we never get here as on success the chip reboots +} + + +/* Lua: t = otaupgrade.info () + * -- running_partition, nextboot_partition, { + * .X = { name, version, secure_version, date, time, idf_version, state }, + * .Y = { name, version, secure_version, date, time, idf_version, state } + * } + */ +static int lotaupgrade_info (lua_State *L) +{ + const esp_partition_t *running = esp_ota_get_running_partition (); + if (running) + lua_pushstring (L, running->label); + else + lua_pushnil (L); + + const esp_partition_t *boot = esp_ota_get_boot_partition (); + if (boot) + lua_pushstring (L, boot->label); + else + lua_pushnil (L); + + lua_createtable (L, 0, 2); + esp_partition_iterator_t iter = esp_partition_find ( + ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, NULL); + while (iter) { + const esp_partition_t *part = esp_partition_get (iter); + if (part->subtype >= ESP_PARTITION_SUBTYPE_APP_OTA_MIN && + part->subtype <= ESP_PARTITION_SUBTYPE_APP_OTA_MAX) + { + lua_pushstring (L, part->label); + lua_createtable (L, 0, 6); + + esp_ota_img_states_t state; + esp_err_t err = esp_ota_get_state_partition (part, &state); + + if (err == ESP_OK) + { + lua_pushliteral (L, "state"); + const char *msg = ""; + switch (state) { + case ESP_OTA_IMG_NEW: msg = "new"; break; + case ESP_OTA_IMG_PENDING_VERIFY: msg = "testing"; break; + case ESP_OTA_IMG_VALID: msg = "valid"; break; + case ESP_OTA_IMG_INVALID: msg = "invalid"; break; + case ESP_OTA_IMG_ABORTED: msg = "aborted"; break; + case ESP_OTA_IMG_UNDEFINED: // fall-through + default: msg = "undefined"; break; + } + lua_pushstring (L, msg); + lua_settable (L, -3); + } + else + goto next; // just add an empty table for this slot + + esp_app_desc_t desc; + err = esp_ota_get_partition_description(part, &desc); + + if (err == ESP_OK) + { + lua_pushliteral (L, "name"); + lua_pushstring (L, desc.project_name); + lua_settable (L, -3); + + lua_pushliteral (L, "version"); + lua_pushstring (L, desc.version); + lua_settable (L, -3); + + lua_pushliteral (L, "secure_version"); + lua_pushinteger (L, desc.secure_version); + lua_settable (L, -3); + + lua_pushliteral (L, "date"); + lua_pushstring (L, desc.date); + lua_settable (L, -3); + + lua_pushliteral (L, "time"); + lua_pushstring (L, desc.time); + lua_settable (L, -3); + + lua_pushliteral (L, "idf_version"); + lua_pushstring (L, desc.idf_ver); + lua_settable (L, -3); + } + +next: + lua_settable (L, -3); // info table into return arg #3 table + } + iter = esp_partition_next (iter); + } + return 3; +} + + +static const LUA_REG_TYPE otaupgrade_map[] = +{ + { LSTRKEY( "commence" ), LFUNCVAL( lotaupgrade_commence ) }, + { LSTRKEY( "write" ), LFUNCVAL( lotaupgrade_write) }, + { LSTRKEY( "complete" ), LFUNCVAL( lotaupgrade_complete) }, + { LSTRKEY( "accept" ), LFUNCVAL( lotaupgrade_accept) }, + { LSTRKEY( "rollback" ), LFUNCVAL( lotaupgrade_rollback) }, + { LSTRKEY( "info" ), LFUNCVAL( lotaupgrade_info) }, + { LNILKEY, LNILVAL } +}; + +NODEMCU_MODULE(OTAUPGRADE, "otaupgrade", otaupgrade_map, NULL); diff --git a/docs/modules/otaupgrade.md b/docs/modules/otaupgrade.md new file mode 100644 index 00000000..9a7c5a98 --- /dev/null +++ b/docs/modules/otaupgrade.md @@ -0,0 +1,232 @@ +# OTA Upgrade module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2019-06-24 | [DiUS](https://github.com/DiUS), [Johny Mattsson](https://github.com/jmattsson) | [Johny Mattsson](https://github.com/jmattsson) | [otaupgrade.c](../../components/modules/otaupgrade.c)| + +The OTA Upgrade module provides access to the IDF Over-The-Air Upgrade +support, enabling new application firmware to be applied and booted into. + +This module is not concerned with where the new application comes from. +The choice of download source and method (e.g. https, tftp) is left to +the user, as is the trigger to start an upgrade. A common approach is +to have the device periodically check in with a central server and +compare a provided version number with the currently running version, +and if necessary kick off an upgrade. + +In order to use the `otaupgrade` module, there must exist at least two +OTA partitions (type `app`, subtype `ota_0` / `ota_1`), as well as the +"otadata" partition (type `data`, subtype `ota`). The IDF implements +the typical "flip-flop" approach to upgrades, in that one of the +partitions hosts the running application, and the upgrade is downloaded +into the inactive partition and only when fully downloaded and verified +is it marked as bootable. This makes the system resilient to incomplete +upgrades, be it due to power-loss, interrupted downloads, or other such +things. + +An example partition table for OTA might look like: +``` +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0x9000, 0x5000 +otadata, data, ota, 0xe000, 0x2000 +ota_0, app, ota_0, 0x10000,0x130000 +ota_1, app, ota_1, 0x140000,0x130000 +``` + +Depending on whether the installed boot loader has been built with or +without rollback support, the upgrade process itself has four or three +steps. Without rollback support, the steps are: + +- `otaupgrade.commence()` +- feed the new application image into `otaupgrade.write(data)` in chunks +- `otaupgrade.complete(1)` to finalise and reboot into the new application + +If the boot loader is built with rollback support, an extra step is needed +after the new application has booted (and been tested to be "good", by +whatever metric(s) the user chooses): + +- `otaupgrade.accept()` to mark this image as valid, and allow it to be + booted into again. + +If a new firmware is not `accept()`ed before the device reboots, the +boot loader will switch back to the previous firmware version (provided +said boot loader is built with rollback support). A common test before +marking a new firmware as valid is to ensure the upgrade server can be +reached, on the basis that as long as the firmware can be remotely +upgraded, it's "good enough" to accept. + +# otaupgrade.info() + +The boot info and application state and version info can be queried with +this function. Typically it will be used to check the version of the +running application, to compare against a "desired" version in order +to decide whether an upgrade is required. + +#### Parameters +None. + +#### Returns +A list of three values: +- the name of the partition of the running application +- the name of the partition currently marked for boot next (typically the + same as the running application, but after `otaupgrade.complete()` it + may point to a new application partition. +- a table whose keys are the names of OTA partitions and corresponding + values are tables containing: + - `state` one of `new`, `testing`, `valid`, `invalid`, `aborted` or + possibly `undefined`. The values `invalid` and `aborted` largely + mean the same things. See the IDF documentation for specifics. + A partition in `testing` state needs to call `otaupgrade.accept()` + if it wishes to become `valid`. + - `name` the application name, typically "NodeMCU" + - `date` the build date + - `time` the build time + - `version` the build version, as set by the *PROJECT_VER* variable + during build + - `secure_version` the secure version number, if secure boot is enabled + - `idf_version` the IDF version + +#### Example +```lua +boot_part, next_part, info = otaupgrade.info() +print("Booted: "..boot_part) +print(" Next: "..next_part) +for p,t in pairs(info) do + print("@ "..p..":") + for k,v in pairs(t) do + print(" "..k..": "..v) + end +end +print("Running version: "..info[boot_part].version) +``` + +# otaupgrade.commence() + +Wipes the spare application partition and prepares to receive the new +application firmware. + +If rollback support is enabled, note that the running application must +first be marked valid/accepted before it is possible to commence a +new OTA upgrade. + +#### Syntax +`otaupgrade.commence()` + +#### Parameters +None. + +#### Returns +`nil` + +A Lua error may be raised if the OTA upgrade cannot be commenced for some +reason (such as due to incorrect partition setup). + + +# otaupgrade.write(data) + +Write a chunk of application firmware data to the correct partition and +location. Data must be streamed sequentially, the IDF does not support +out-of-order data as would be the case from e.g. bittorrent. + +#### Syntax +`otaupgrade.write(data)` + +#### Parameters +- `data` a string of binary data + +#### Returns +`nil` + +A Lua error may be raised if the data can not be written, e.g. due to the +data not being a valid OTA image (the IDF performs some checks in this +regard). + + +# otaupgrade.complete(reboot) + +Finalises the upgrade, and optionally reboots into the new application +firmware right away. + +#### Syntax +`otaupgrade.complete(reboot)` + +#### Parameters +- `reboot` 1 to reboot into the new firmware immediately, nil to keep running + +#### Returns +`nil` + +A Lua error may be raised if the image does not pass validation, or no data +has been written to the image at all. + +#### Example +```lua +-- Quick, dirty and totally insecure "push upgrade" for development use. +-- Use netcat to push a new firmware to a device: +-- nc -q 1 your-device-ip 9999 < build/NodeMCU.bin +-- +osv = net.createServer() +osv:listen(9999, function(conn) + print('Commencing OTA upgrade') + local status, err = pcall(otaupgrade.commence) + if err then + print(err) + conn:send(err) + conn:close() + end + conn:on('receive', function(sck, data) + status, err = pcall(function() otaupgrade.write(data) end) + if err then + print(err) + conn:send(err) + conn:close() + end + end) + conn:on('disconnection', function() + print('EOF, completing OTA') + status, err = pcall(function() otaupgrade.complete(1) end) + if err then + print(err) + end + end) +end) + +``` + +# otaupgrade.accept() + +When the installed boot loader is built with rollback support, a new +application image is by default only booted once. During this "test run" +it can perform whatever checks is appropriate (like testing whether it +can still reach the update server), and if satisfied can mark itself +as valid. Without being marked valid, upon the next reboot the system +would "roll back" to the previous version instead. + +#### Syntax +`otaupgrade.accept()` + +#### Parameters +None. + +#### Returns +`nil` + + +# otaupgrade.rollback() + +A new firmware may decide that it is not performing as expected, and +request an explicit rollback to the previous version. If the call to this +function succeeds, the system will reboot without returning from the +call. + +Note that it is also possible to roll back to a previous firmware +version even after the new version has called `otaupgrade.accept()`. + +#### Syntax +`otaupgrade.rollback()` + +#### Parameters +None. + +#### Returns +Never. Either the system is rebooted, or a Lua error is raised (e.g. due +to there being no other firmware to roll back to).