OTA support for ESP32 (#2812)

* Implemented otaupgrade module.

* Added partition table example for otaupgrade.

* Copy-paste omission. Whoops.

* Updated otaupgrade docs after review.
This commit is contained in:
Johny Mattsson 2019-07-06 22:21:08 +10:00 committed by Arnim Läuger
parent e11087bfdf
commit ca89bff073
3 changed files with 494 additions and 1 deletions

View File

@ -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.

View File

@ -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 <jmattsson@dius.com.au>
*/
#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);

232
docs/modules/otaupgrade.md Normal file
View File

@ -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).