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:
parent
e11087bfdf
commit
ca89bff073
|
@ -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.
|
||||
|
|
|
@ -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);
|
|
@ -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).
|
Loading…
Reference in New Issue