Rebase private OTA libs on 2.7

This commit is contained in:
Pim van Pelt
2018-10-28 12:12:24 +01:00
parent 497c62b382
commit 9ee393bc8a
29 changed files with 3836 additions and 521 deletions

13
libs/ota-common/LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright 2018 Cesanta Software Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
bool cc3200_upd_init(void);
const char *cc3200_upd_get_fs_container_prefix(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
void esp32_updater_early_init();
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2014-2016 Cesanta Software Limited
* All rights reserved
*/
/*
* OTA API.
*
* See https://mongoose-os.com/docs/mos/userguide/ota.md for more details about
* Mongoose OS OTA mechanism.
*/
#ifndef CS_FW_SRC_MGOS_UPDATER_H_
#define CS_FW_SRC_MGOS_UPDATER_H_
#include <stdbool.h>
#include "frozen.h"
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
struct mgos_upd_file_info {
char name[50];
uint32_t size;
uint32_t processed;
};
struct mgos_upd_info {
/* Data from the manifest, available from BEGIN until END */
struct json_token name;
struct json_token platform;
struct json_token version;
struct json_token build_id;
struct json_token parts;
bool abort; /* If MGOS_EVENT_OTA_BEGIN handler sets this to true, abort OTA */
/* Current file, available in PROGRESS. */
struct mgos_upd_file_info current_file;
};
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* CS_FW_SRC_MGOS_UPDATER_H_ */

View File

@ -0,0 +1,132 @@
/*
* Copyright (c) 2014-2016 Cesanta Software Limited
* All rights reserved
*
* Common bits of code handling update process.
* Driven externaly by data source - mg_rpc or POST file upload.
*/
#ifndef CS_FW_SRC_MGOS_UPDATER_COMMON_H_
#define CS_FW_SRC_MGOS_UPDATER_COMMON_H_
#include <stdbool.h>
#include <stdint.h>
#include "frozen.h"
#include "mgos_event.h"
#include "mgos_timers.h"
#include "mgos_updater.h"
#include "mgos_updater_hal.h"
#include "mongoose.h"
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
struct update_context;
typedef void (*mgos_updater_result_cb)(struct update_context *ctx);
struct mgos_upd_hal_ctx; /* This struct is defined by HAL and is opaque to us */
#define MGOS_EVENT_OTA_BASE MGOS_EVENT_BASE('O', 'T', 'A')
enum mgos_event_ota {
MGOS_EVENT_OTA_BEGIN =
MGOS_EVENT_OTA_BASE, /* ev_data: struct mgos_upd_info */
MGOS_EVENT_OTA_STATUS, /* ev_data: struct mgos_ota_status */
MGOS_EVENT_OTA_REQUEST, /* ev_data: struct ota_request_param */
};
enum mgos_ota_state {
MGOS_OTA_STATE_IDLE = 0, /* idle */
MGOS_OTA_STATE_PROGRESS, /* "progress" */
MGOS_OTA_STATE_ERROR, /* "error" */
MGOS_OTA_STATE_SUCCESS, /* "success" */
};
struct mgos_ota_status {
bool is_committed;
int commit_timeout;
int partition;
enum mgos_ota_state state;
const char *msg; /* stringified state */
int progress_percent; /* valid only for "progress" state */
};
const char *mgos_ota_state_str(enum mgos_ota_state state);
struct update_context {
int update_state; /* Internal state machine - parsing zip, etc */
enum mgos_ota_state ota_state; /* Externally visible */
const char *status_msg;
char *zip_file_url;
size_t zip_file_size;
size_t bytes_already_downloaded;
size_t last_reported_bytes;
double last_reported_time;
const char *data;
size_t data_len;
struct mbuf unprocessed;
struct mgos_upd_info info;
uint32_t current_file_crc;
uint32_t current_file_crc_calc;
bool current_file_has_descriptor;
bool ignore_same_version;
bool need_reboot;
int result;
mgos_updater_result_cb result_cb;
char *manifest_data;
char file_name[50];
struct mgos_upd_hal_ctx *dev_ctx;
mgos_timer_id wdt;
int wdt_timeout_ms;
/* Network connection associated with this update, if any.
* It is only used in case update times out - it is closed. */
struct mg_connection *nc;
/*
* At the end of update this struct is written to a file
* and then restored after reboot.
*/
struct update_file_context {
int commit_timeout;
} fctx __attribute__((packed));
};
struct update_context *updater_context_create(int timeout);
/*
* Returns updater context of the update in progress. If no update is in
* progress, returns NULL.
*/
struct update_context *updater_context_get_current(void);
int updater_process(struct update_context *ctx, const char *data, size_t len);
void updater_finish(struct update_context *ctx);
void updater_context_free(struct update_context *ctx);
int updater_finalize(struct update_context *ctx);
int is_write_finished(struct update_context *ctx);
int is_update_finished(struct update_context *ctx);
int is_reboot_required(struct update_context *ctx);
void mgos_upd_boot_finish(bool is_successful, bool is_first);
bool mgos_upd_commit();
bool mgos_upd_is_committed();
bool mgos_upd_revert(bool reboot);
bool mgos_upd_get_status(struct mgos_ota_status *);
int mgos_upd_get_commit_timeout();
bool mgos_upd_set_commit_timeout(int commit_timeout);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* CS_FW_SRC_MGOS_UPDATER_COMMON_H_ */

View File

@ -0,0 +1,108 @@
/*
* Copyright (c) 2014-2016 Cesanta Software Limited
* All rights reserved
*
* Updater HAL. Devices need to implement this interface.
*/
#ifndef CS_FW_SRC_MGOS_UPDATER_HAL_H_
#define CS_FW_SRC_MGOS_UPDATER_HAL_H_
#include <inttypes.h>
#include "common/mbuf.h"
#include "common/mg_str.h"
#include "frozen.h"
#include "mgos_updater.h"
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void);
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx);
/*
* Process the firmware manifest. Parsed manifest is available in ctx->manifest,
* and for convenience the "parts" key within it is in ctx->parts.
* Return >= 0 if ok, < 0 + ctx->status_msg on error.
*/
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts);
/*
* Decide what to do with the next file.
* In case of abort, message should be provided in status_msg.
*/
enum mgos_upd_file_action {
MGOS_UPDATER_ABORT,
MGOS_UPDATER_PROCESS_FILE,
MGOS_UPDATER_SKIP_FILE,
};
enum mgos_upd_file_action mgos_upd_file_begin(
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi);
#ifndef MGOS_UPDATER_DATA_CHUNK_SIZE
#define MGOS_UPDATER_DATA_CHUNK_SIZE 512
#endif
/*
* Process a chunk of file data. Data will be delivered to this function in
* MGOS_UPDATER_DATA_CHUNK_SIZE chunks.
* Return number of bytes processed (0 .. data.len)
* or < 0 for error. In case of error, message should be provided in status_msg.
*/
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi, struct mg_str data);
/*
* Finalize a file. Remainder of the data (if any) is passed,
* number of bytes of that data processed should be returned. The amount of data
* will be less than MGOS_UPDATER_DATA_CHUNK_SIZE.
* Value equal to data.len is an indication of success,
* < 0 + ctx->status_msg on error.
*/
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi, struct mg_str data);
/*
* Finalize the update.
* Return >= 0 if ok, < 0 + ctx->status_msg on error.
*/
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx);
bool mgos_upd_is_first_boot(void);
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx);
/* Apply update on first boot, usually involves merging filesystem. */
int mgos_upd_apply_update(void);
/*
* Create a snapshot of currently running firmware (including FS) in
* a currently inactive slot. There must be no uncommitted update
* in progress.
* Returns slot id used for snapshot or < 0 in case of error.
*/
int mgos_upd_create_snapshot(void);
struct mgos_upd_boot_state {
/* Slot that will be used to load firmware during next boot. */
int active_slot;
/* Whether the boot configuration is committed or not.
* Reboot with uncommitted configration reverts to revert_slot. */
bool is_committed;
/* Slot that will be used in case of revert, explicit or implicit. */
int revert_slot;
};
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs);
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs);
/* Shortcuts for get and set */
void mgos_upd_boot_commit(void);
void mgos_upd_boot_revert(void);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* CS_FW_SRC_MGOS_UPDATER_HAL_H_ */

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) 2014-2016 Cesanta Software Limited
* All rights reserved
*/
#ifndef CS_FW_SRC_MGOS_UPDATER_UTIL_H_
#define CS_FW_SRC_MGOS_UPDATER_UTIL_H_
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/* Function to merge filesystems. */
bool mgos_upd_merge_fs(const char *old_fs_path, const char *new_fs_path);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* CS_FW_SRC_MGOS_UPDATER_UTIL_H_ */

34
libs/ota-common/mos.yml Normal file
View File

@ -0,0 +1,34 @@
author: mongoose-os
description: OTA common bits
type: lib
version: 1.0
platforms: [ cc3200, esp32, esp8266, stm32 ]
sources:
- src
- src/${platform}
includes:
- include
- include/${platform}
libs:
- origin: https://github.com/mongoose-os-libs/mongoose
no_implicit_init_deps: true
init_deps:
- mongoose
config_schema:
- ["update", "o", {title: "Firmware updater"}]
- ["update.timeout", "i", 600, {title : "Update will be aborted if it does not finish within this time"}]
- ["update.commit_timeout", "i", {title : "After applying update, wait for commit up to this long"}]
tags:
- c
- core
- ota
- docs:net:OTA
manifest_version: 2018-06-20

View File

@ -0,0 +1,2 @@
libs:
- origin: https://github.com/mongoose-os-libs/bootloader

View File

@ -0,0 +1,505 @@
/*
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cc3200_updater.h"
#include <inttypes.h>
#include <strings.h>
#include "common/cs_dbg.h"
#include "common/platform.h"
#include "frozen.h"
#include "mongoose.h"
#include "mgos_hal.h"
#include "mgos_sys_config.h"
#include "mgos_updater_hal.h"
#include "mgos_updater_util.h"
#include "mgos_utils.h"
#include "cc32xx_hash.h"
#include "cc3200_vfs_dev_slfs_container.h"
#include "cc3200_vfs_dev_slfs_container_meta.h"
#include "fw/platforms/cc3200/boot/lib/boot.h"
static int s_boot_cfg_idx;
static struct boot_cfg s_boot_cfg;
struct mgos_upd_hal_ctx {
struct json_token *parts;
int cur_boot_cfg_idx;
int new_boot_cfg_idx;
char app_image_file[MAX_APP_IMAGE_FILE_LEN];
uint32_t app_load_addr;
char fs_container_file[MAX_FS_CONTAINER_FNAME_LEN + 3];
uint32_t fs_size, fs_block_size, fs_page_size, fs_erase_size;
struct json_token cur_part;
const _u8 *cur_fn;
_i32 cur_fh;
const char *status_msg;
};
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void) {
struct mgos_upd_hal_ctx *ctx =
(struct mgos_upd_hal_ctx *) calloc(1, sizeof(*ctx));
ctx->cur_fh = -1;
return ctx;
}
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx) {
return ctx->status_msg;
}
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts) {
ctx->parts = parts;
/* We want to make sure device uses boto loader. */
ctx->cur_boot_cfg_idx = get_active_boot_cfg_idx();
if (ctx->cur_boot_cfg_idx < 0) {
ctx->status_msg = "Could not read current boot cfg";
return -1;
}
ctx->new_boot_cfg_idx = get_inactive_boot_cfg_idx();
return 1;
}
typedef int (*read_file_cb_t)(_u8 *data, int len, void *arg);
static int read_file(const char *fn, int offset, int len, read_file_cb_t cb,
void *arg) {
_i32 fh;
int r = sl_FsOpen((const _u8 *) fn, FS_MODE_OPEN_READ, NULL, &fh);
if (r < 0) return r;
while (len > 0) {
_u8 buf[512];
int to_read = MIN(len, sizeof(buf));
r = sl_FsRead(fh, offset, buf, to_read);
if (r != to_read) break;
if (cb(buf, to_read, arg) != to_read) break;
offset += to_read;
len -= to_read;
}
sl_FsClose(fh, NULL, NULL, 0);
return (len == 0 ? 0 : -1);
}
static int sha1_update_cb(_u8 *data, int len, void *arg) {
cc32xx_hash_update((struct cc32xx_hash_ctx *) arg, data, len);
return len;
}
void bin2hex(const uint8_t *src, int src_len, char *dst);
static bool verify_checksum(const char *fn, int fs,
const struct json_token *expected) {
_u8 digest[20];
char digest_str[41];
if (expected->len != 40) {
return false;
}
struct cc32xx_hash_ctx ctx;
cc32xx_hash_init(&ctx);
cc32xx_hash_start(&ctx, CC32XX_HASH_ALGO_SHA1);
if (read_file(fn, 0, fs, sha1_update_cb, &ctx) < 0) {
return false;
}
cc32xx_hash_finish(&ctx, digest);
bin2hex(digest, 20, digest_str);
LOG(LL_INFO,
("%s: have %.*s, want %.*s", fn, 40, digest_str, 40, expected->ptr));
return (strncasecmp(expected->ptr, digest_str, 40) == 0);
}
/* Create file name by appending ".$idx" to prefix. */
static void create_fname(struct mg_str pfx, int idx, char *fn, int len) {
int l = MIN(len - 3, pfx.len);
memcpy(fn, pfx.p, l);
fn[l++] = '.';
fn[l++] = ('0' + idx);
fn[l] = '\0';
}
static int prepare_to_write(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi,
const char *fname, uint32_t falloc,
struct json_token *part) {
struct json_token expected_sha1 = JSON_INVALID_TOKEN;
json_scanf(part->ptr, part->len, "{cs_sha1: %T}", &expected_sha1);
if (verify_checksum(fname, fi->size, &expected_sha1)) {
LOG(LL_INFO,
("Digest matched for %s %u (%.*s)", fname, (unsigned int) fi->size,
(int) expected_sha1.len, expected_sha1.ptr));
return 0;
}
LOG(LL_INFO, ("Storing %s %u -> %s %u (%.*s)", fi->name,
(unsigned int) fi->size, fname, (unsigned int) falloc,
(int) expected_sha1.len, expected_sha1.ptr));
ctx->cur_fn = (const _u8 *) fname;
sl_FsDel(ctx->cur_fn, 0);
_i32 r = sl_FsOpen(ctx->cur_fn, FS_MODE_OPEN_CREATE(falloc, 0), NULL,
&ctx->cur_fh);
if (r < 0) {
ctx->status_msg = "Failed to create file";
return r;
}
return 1;
}
struct find_part_info {
const char *src;
struct mg_str *key;
struct json_token *value;
char buf[50];
};
static void find_part(void *data, const char *name, size_t name_len,
const char *path, const struct json_token *tok) {
struct find_part_info *info = (struct find_part_info *) data;
size_t path_len = strlen(path), src_len = strlen(info->src);
(void) name;
(void) name_len;
if (tok->ptr == NULL) {
/*
* We're not interested here in the events for which we have no value;
* namely, JSON_TYPE_OBJECT_START and JSON_TYPE_ARRAY_START
*/
return;
}
/* For matched 'src' attribute, remember parent object path. */
if (src_len == tok->len && strncmp(info->src, tok->ptr, tok->len) == 0) {
const char *p = path + path_len;
while (--p > path + 1) {
if (*p == '.') break;
}
info->key->len = snprintf(info->buf, sizeof(info->buf), "%.*s",
(p - path) - 1, path + 1);
info->key->p = info->buf;
}
/*
* And store parent's object token. These conditionals are triggered
* in separate callback invocations.
*/
if (info->value->len == 0 && info->key->len > 0 &&
info->key->len + 1 == path_len &&
strncmp(path + 1, info->key->p, info->key->len) == 0) {
*info->value = *tok;
}
}
static int tcmp(const struct json_token *tok, const char *str) {
struct mg_str s = {.p = tok->ptr, .len = tok->len};
return mg_vcmp(&s, str);
}
enum mgos_upd_file_action mgos_upd_file_begin(
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi) {
struct mg_str part_name = MG_MK_STR("");
enum mgos_upd_file_action ret = MGOS_UPDATER_SKIP_FILE;
struct find_part_info find_part_info = {fi->name, &part_name, &ctx->cur_part};
ctx->cur_part.len = part_name.len = 0;
json_walk(ctx->parts->ptr, ctx->parts->len, find_part, &find_part_info);
if (ctx->cur_part.len == 0) return ret;
/* Drop any indexes from part name, we'll add our own. */
while (1) {
char c = part_name.p[part_name.len - 1];
if (c != '.' && !(c >= '0' && c <= '9')) break;
part_name.len--;
}
struct json_token type = JSON_INVALID_TOKEN;
const char *fname = NULL;
uint32_t falloc = 0;
json_scanf(ctx->cur_part.ptr, ctx->cur_part.len,
"{load_addr:%u, falloc:%u, type: %T}", &ctx->app_load_addr,
&falloc, &type);
if (falloc == 0) falloc = fi->size;
if (tcmp(&type, "app") == 0) {
struct boot_cfg cur_cfg;
int r = read_boot_cfg(ctx->cur_boot_cfg_idx, &cur_cfg);
if (r < 0) {
ctx->status_msg = "Could not read current boot cfg";
return MGOS_UPDATER_ABORT;
}
#if CC3200_SAFE_CODE_UPDATE
/*
* When safe code update is enabled, we write code to a new file.
* Otherwise we write to the same slot we're using currently, which is
* unsafe, makes reverting code update not possible, but saves space.
*/
create_fname(
mg_mk_str_n(cur_cfg.app_image_file, strlen(cur_cfg.app_image_file) - 2),
ctx->new_boot_cfg_idx, ctx->app_image_file,
sizeof(ctx->app_image_file));
#else
{
strncpy(ctx->app_image_file, cur_cfg.app_image_file,
sizeof(ctx->app_image_file));
}
#endif
if (ctx->app_load_addr >= 0x20000000) {
fname = ctx->app_image_file;
} else {
ctx->status_msg = "Bad/missing app load_addr";
ret = MGOS_UPDATER_ABORT;
}
} else if (tcmp(&type, "fs") == 0) {
json_scanf(
ctx->cur_part.ptr, ctx->cur_part.len,
"{fs_size: %u, fs_block_size: %u, fs_page_size: %u, fs_erase_size: %u}",
&ctx->fs_size, &ctx->fs_block_size, &ctx->fs_page_size,
&ctx->fs_erase_size);
if (ctx->fs_size > 0 && ctx->fs_block_size > 0 && ctx->fs_page_size > 0 &&
ctx->fs_erase_size > 0) {
char fs_container_prefix[MAX_FS_CONTAINER_PREFIX_LEN];
create_fname(part_name, ctx->new_boot_cfg_idx, fs_container_prefix,
sizeof(fs_container_prefix));
/* Delete container 1 (if any) so that 0 is the only one. */
cc3200_vfs_dev_slfs_container_delete_container(fs_container_prefix, 1);
cc3200_vfs_dev_slfs_container_fname(fs_container_prefix, 0,
(_u8 *) ctx->fs_container_file);
fname = ctx->fs_container_file;
if (fi->size > ctx->fs_size) {
/* Assume meta has already been added. */
falloc = fi->size;
} else {
falloc = FS_CONTAINER_SIZE(fi->size);
}
} else {
ctx->status_msg = "Missing FS parameters";
ret = MGOS_UPDATER_ABORT;
}
}
if (fname != NULL) {
int r = prepare_to_write(ctx, fi, fname, falloc, &ctx->cur_part);
if (r < 0) {
LOG(LL_ERROR, ("err = %d", r));
ret = MGOS_UPDATER_ABORT;
} else {
ret = (r > 0 ? MGOS_UPDATER_PROCESS_FILE : MGOS_UPDATER_SKIP_FILE);
}
}
if (ret == MGOS_UPDATER_SKIP_FILE) {
DBG(("Skipping %s %.*s", fi->name, (int) part_name.len, part_name.p));
}
return ret;
}
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi,
struct mg_str data) {
_i32 r = sl_FsWrite(ctx->cur_fh, fi->processed, (_u8 *) data.p, data.len);
if (r != data.len) {
ctx->status_msg = "Write failed";
r = -1;
}
return r;
}
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi, struct mg_str tail) {
int r = tail.len;
assert(tail.len == 0);
if (ctx->cur_fn == (_u8 *) ctx->fs_container_file) {
if (!cc3200_vfs_dev_slfs_container_write_meta(
ctx->cur_fh, FS_INITIAL_SEQ, ctx->fs_size, ctx->fs_block_size,
ctx->fs_page_size, ctx->fs_erase_size)) {
ctx->status_msg = "Failed to write fs meta";
r = -1;
}
}
if (sl_FsClose(ctx->cur_fh, NULL, NULL, 0) != 0) {
ctx->status_msg = "Close failed";
r = -1;
} else {
struct json_token sha1 = JSON_INVALID_TOKEN;
json_scanf(ctx->cur_part.ptr, ctx->cur_part.len, "{cs_sha1: %T}", &sha1);
if (!verify_checksum((const char *) ctx->cur_fn, fi->size, &sha1)) {
ctx->status_msg = "Checksum mismatch";
r = -1;
}
}
ctx->cur_fh = -1;
ctx->cur_fn = NULL;
return r;
}
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx) {
struct boot_cfg cur_cfg, new_cfg;
int r = read_boot_cfg(ctx->cur_boot_cfg_idx, &cur_cfg);
if (r < 0) {
ctx->status_msg = "Could not read current boot cfg";
return r;
}
LOG(LL_INFO,
("Boot cfg %d: 0x%llx, 0x%x, %s @ 0x%08x, %s", ctx->cur_boot_cfg_idx,
cur_cfg.seq, (unsigned int) cur_cfg.flags, cur_cfg.app_image_file,
(unsigned int) cur_cfg.app_load_addr, cur_cfg.fs_container_prefix));
memset(&new_cfg, 0, sizeof(new_cfg));
new_cfg.seq = cur_cfg.seq - 1;
new_cfg.flags |= BOOT_F_FIRST_BOOT;
if (ctx->app_image_file[0] != '\0') {
strncpy(new_cfg.app_image_file, ctx->app_image_file,
sizeof(new_cfg.app_image_file));
new_cfg.app_load_addr = ctx->app_load_addr;
} else {
strcpy(new_cfg.app_image_file, cur_cfg.app_image_file);
new_cfg.app_load_addr = cur_cfg.app_load_addr;
}
if (ctx->fs_container_file[0] != '\0') {
int n = strlen(ctx->fs_container_file);
do {
n--;
} while (ctx->fs_container_file[n] != '.');
strncpy(new_cfg.fs_container_prefix, ctx->fs_container_file, n);
new_cfg.flags |= BOOT_F_MERGE_SPIFFS;
} else {
strcpy(new_cfg.fs_container_prefix, cur_cfg.fs_container_prefix);
}
LOG(LL_INFO,
("Boot cfg %d: 0x%llx, 0x%x, %s @ 0x%08x, %s", ctx->new_boot_cfg_idx,
new_cfg.seq, (unsigned int) new_cfg.flags, new_cfg.app_image_file,
(unsigned int) new_cfg.app_load_addr, new_cfg.fs_container_prefix));
r = write_boot_cfg(&new_cfg, ctx->new_boot_cfg_idx);
if (r < 0) {
ctx->status_msg = "Could not write new boot cfg";
return r;
}
return 1;
}
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx) {
if (ctx == NULL) return;
if (ctx->cur_fh >= 0) sl_FsClose(ctx->cur_fh, NULL, NULL, 0);
if (ctx->cur_fn != NULL) sl_FsDel(ctx->cur_fn, 0);
memset(ctx, 0, sizeof(*ctx));
free(ctx);
}
int mgos_upd_create_snapshot() {
/* TODO(rojer): Implement. */
return -1;
}
bool cc3200_upd_init(void) {
s_boot_cfg_idx = get_active_boot_cfg_idx();
if (s_boot_cfg_idx < 0 || read_boot_cfg(s_boot_cfg_idx, &s_boot_cfg) < 0) {
return false;
}
LOG(LL_INFO,
("Boot cfg %d: 0x%llx, 0x%u, %s @ 0x%08x, %s", s_boot_cfg_idx,
s_boot_cfg.seq, (unsigned int) s_boot_cfg.flags,
s_boot_cfg.app_image_file, (unsigned int) s_boot_cfg.app_load_addr,
s_boot_cfg.fs_container_prefix));
if (mgos_upd_is_first_boot()) {
/* Tombstone the current config. If anything goes wrong between now and
* commit, next boot will use the old one. */
uint64_t saved_seq = s_boot_cfg.seq;
s_boot_cfg.seq = BOOT_CFG_TOMBSTONE_SEQ;
write_boot_cfg(&s_boot_cfg, s_boot_cfg_idx);
s_boot_cfg.seq = saved_seq;
}
/*
* We aim to maintain at most 3 FS containers at all times.
* Delete inactive FS container in the inactive boot configuration.
*/
struct boot_cfg cfg;
int inactive_idx = (s_boot_cfg_idx == 0 ? 1 : 0);
if (read_boot_cfg(inactive_idx, &cfg) >= 0) {
cc3200_vfs_dev_slfs_container_delete_inactive_container(
cfg.fs_container_prefix);
}
return true;
}
const char *cc3200_upd_get_fs_container_prefix(void) {
return s_boot_cfg.fs_container_prefix;
}
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs) {
struct boot_cfg *cfg = &s_boot_cfg;
memset(bs, 0, sizeof(*bs));
const char *p = strrchr(cfg->app_image_file, '.');
bs->active_slot = (*(p + 1) == '0' ? 0 : 1);
bs->revert_slot = (bs->active_slot == 0 ? 1 : 0);
bs->is_committed = !(cfg->flags & BOOT_F_FIRST_BOOT);
return true;
}
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs) {
/* TODO(rojer): Implement. */
(void) bs;
return false;
}
void mgos_upd_boot_revert(void) {
int boot_cfg_idx = s_boot_cfg_idx;
struct boot_cfg *cfg = &s_boot_cfg;
if (!cfg->flags & BOOT_F_FIRST_BOOT) return;
LOG(LL_ERROR, ("Config %d is bad, reverting", boot_cfg_idx));
/* Tombstone the current config. */
cfg->seq = BOOT_CFG_TOMBSTONE_SEQ;
write_boot_cfg(cfg, boot_cfg_idx);
}
bool mgos_upd_is_first_boot(void) {
return (s_boot_cfg.flags & BOOT_F_FIRST_BOOT) != 0;
}
void mgos_upd_boot_commit(void) {
int boot_cfg_idx = s_boot_cfg_idx;
struct boot_cfg *cfg = &s_boot_cfg;
if (!cfg->flags & BOOT_F_FIRST_BOOT) return;
cfg->flags &= ~(BOOT_F_FIRST_BOOT);
int r = write_boot_cfg(cfg, boot_cfg_idx);
if (r < 0) mgos_upd_boot_revert();
LOG(LL_INFO, ("Committed cfg %d, seq 0x%llx", boot_cfg_idx, cfg->seq));
}
int mgos_upd_apply_update(void) {
int boot_cfg_idx = s_boot_cfg_idx;
struct boot_cfg *cfg = &s_boot_cfg;
if (cfg->flags & BOOT_F_MERGE_SPIFFS) {
int old_boot_cfg_idx = (boot_cfg_idx == 0 ? 1 : 0);
struct boot_cfg old_boot_cfg;
int r = read_boot_cfg(old_boot_cfg_idx, &old_boot_cfg);
if (r < 0) return r;
if (!cc3200_fs_container_mount("/old", old_boot_cfg.fs_container_prefix)) {
return -123;
}
if (mgos_upd_merge_fs("/old", "/")) {
r = 0;
cfg->flags &= ~(BOOT_F_MERGE_SPIFFS);
} else {
r = -124;
}
mgos_vfs_umount("/old");
}
return 0;
}

View File

@ -0,0 +1,645 @@
/*
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "esp32_updater.h"
#include <stdint.h>
#include <strings.h>
#include "esp_flash_encrypt.h"
#include "esp_flash_partitions.h"
#include "esp_ota_ops.h"
#include "esp_partition.h"
#include "esp_spi_flash.h"
#include "nvs.h"
#include "mbedtls/sha1.h"
#include "common/cs_dbg.h"
#include "common/mg_str.h"
#include "common/platform.h"
#include "frozen.h"
#include "mongoose.h"
#include "mgos_hal.h"
#include "mgos_sys_config.h"
#include "mgos_updater_hal.h"
#include "mgos_updater_util.h"
#include "mgos_utils.h"
#include "mgos_vfs.h"
#include "mgos_vfs_internal.h"
#include "esp32_fs.h"
/*
* Since boot loader does not provide a way to store flags, we use NVS.
* We store a 32-bit uint with old slot, new slot and first boot flag.
*/
#define MGOS_UPDATE_NVS_NAMESPACE "mgos"
#define MGOS_UPDATE_NVS_KEY_FLAGS "update"
#define MGOS_UPDATE_MERGE_FS 0x200
#define MGOS_UPDATE_FIRST_BOOT 0x100
#define MGOS_UPDATE_NVS_VAL(old_slot, new_slot, first_boot, merge_fs) \
(((merge_fs) ? MGOS_UPDATE_MERGE_FS : 0) | \
((first_boot) ? MGOS_UPDATE_FIRST_BOOT : 0) | (((new_slot) &0xf) << 4) | \
((old_slot) &0xf))
#define MGOS_UPDATE_OLD_SLOT(v) ((v) &0x0f)
#define MGOS_UPDATE_NEW_SLOT(v) (((v) >> 4) & 0x0f)
/* In this key we store type and opts of the old FS. */
#define MGOS_UPDATE_NVS_KEY_OLD_FS "update_old_fs"
#define CS_LEN 20 /* SHA1 */
#define CS_HEX_LEN (CS_LEN * 2)
#define CS_HEX_BUF_SIZE (CS_HEX_LEN + 1)
#define FLASH_PARAMS_ADDR 0x1000
#define FLASH_PARAMS_LEN 4
#define LABEL_OFFSET 8
#if (FLASH_PARAMS_ADDR % MGOS_UPDATER_DATA_CHUNK_SIZE != 0) || \
MGOS_UPDATER_DATA_CHUNK_SIZE < FLASH_PARAMS_LEN
#error "Don't do that"
#endif
uint32_t g_boot_status = 0;
struct mgos_upd_hal_ctx {
const char *status_msg;
struct json_token boot_file_name, boot_cs_sha1;
uint32_t boot_addr;
bool update_bootloader;
/*
* Note: Latter 8 bytes of the label are used to store flash params:
* 4 bytes are the current device's flash params,
* 4 bytes are the ones in the image. We substitute device's params
* during writing but use bytes fro flash image when computing the checksum.
*/
esp_partition_t boot_partition;
const esp_partition_t *cur_app_partition;
struct json_token app_file_name, app_cs_sha1;
const esp_partition_t *app_partition;
esp_ota_handle_t app_ota_handle;
struct json_token fs_file_name, fs_cs_sha1;
const esp_partition_t *fs_partition;
size_t write_offset;
struct json_token *cs_sha1;
const esp_partition_t *write_partition;
};
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void) {
struct mgos_upd_hal_ctx *ctx =
(struct mgos_upd_hal_ctx *) calloc(1, sizeof(*ctx));
return ctx;
}
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx) {
return ctx->status_msg;
}
static int find_inactive_slot(const esp_partition_t **cur_app_partition,
const esp_partition_t **cur_fs_partition,
const esp_partition_t **new_app_partition,
const esp_partition_t **new_fs_partition,
const char **status_msg) {
*cur_app_partition = esp_ota_get_boot_partition();
if (*cur_app_partition == NULL) {
*status_msg = "Not in OTA boot mode";
return -1;
}
int slot = SUBTYPE_TO_SLOT((*cur_app_partition)->subtype);
*cur_fs_partition = esp32_find_fs_for_app_slot(slot);
/* Find next OTA slot */
do {
slot = (slot + 1) & (NUM_OTA_SLOTS - 1);
esp_partition_subtype_t subtype = ESP_PARTITION_SUBTYPE_OTA(slot);
if (subtype == (*cur_app_partition)->subtype) break;
*new_app_partition =
esp_partition_find_first(ESP_PARTITION_TYPE_APP, subtype, NULL);
} while (*new_app_partition == NULL);
if (*new_app_partition == NULL) {
*status_msg = "No app slots";
return -2;
}
*new_fs_partition = esp32_find_fs_for_app_slot(slot);
if (*new_fs_partition == NULL) {
*status_msg = "No fs slots";
return -3;
}
return slot;
}
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts) {
const esp_partition_t *cur_fs_partition;
int slot = find_inactive_slot(&ctx->cur_app_partition, &cur_fs_partition,
&ctx->app_partition, &ctx->fs_partition,
&ctx->status_msg);
if (slot < 0) {
return slot;
}
uint32_t boot_addr = 0;
int update_bootloader = false;
json_scanf(parts->ptr, parts->len,
"{boot: {src: %T, addr: %u, cs_sha1: %T, update: %B}, "
"app: {src: %T, cs_sha1: %T}, "
"fs: {src: %T, cs_sha1: %T}}",
&ctx->boot_file_name, &boot_addr, &ctx->boot_cs_sha1,
&update_bootloader, &ctx->app_file_name, &ctx->app_cs_sha1,
&ctx->fs_file_name, &ctx->fs_cs_sha1);
if (ctx->app_file_name.len == 0 || ctx->app_cs_sha1.len == 0 ||
ctx->fs_file_name.len == 0 || ctx->fs_cs_sha1.len == 0 ||
(ctx->update_bootloader &&
(ctx->boot_file_name.len == 0 || ctx->boot_cs_sha1.len == 0))) {
ctx->status_msg = "Incomplete update package";
return -3;
}
ctx->boot_addr = boot_addr;
ctx->update_bootloader = update_bootloader;
if (ctx->update_bootloader) {
/* Create a bootloader "partition", so esp_partition_* API can be used. */
ctx->boot_partition.address = 0x1000;
ctx->boot_partition.size = (CONFIG_PARTITION_TABLE_OFFSET - 0x1000);
/* If encryption is enabled, boot loader must be encrypted. */
ctx->boot_partition.encrypted = esp_flash_encryption_enabled();
if (boot_addr > ctx->boot_partition.size) {
ctx->status_msg = "Invalid boot write addr";
return -4;
}
strcpy(ctx->boot_partition.label, "boot");
if (esp_partition_read(&ctx->boot_partition, FLASH_PARAMS_ADDR,
ctx->boot_partition.label + LABEL_OFFSET,
FLASH_PARAMS_LEN) != 0) {
ctx->status_msg = "Failed to read flash params";
return -5;
}
LOG(LL_INFO, ("Boot: %.*s -> 0x%x, current flash params: 0x%02x%02x",
(int) ctx->boot_file_name.len, ctx->boot_file_name.ptr,
ctx->boot_addr, ctx->boot_partition.label[LABEL_OFFSET + 2],
ctx->boot_partition.label[LABEL_OFFSET + 3]));
}
LOG(LL_INFO, ("App: %.*s -> %s, FS: %.*s -> %s", (int) ctx->app_file_name.len,
ctx->app_file_name.ptr, ctx->app_partition->label,
(int) ctx->fs_file_name.len, ctx->fs_file_name.ptr,
ctx->fs_partition->label));
return 1;
}
void bin2hex(const uint8_t *src, int src_len, char *dst);
static bool compute_checksum(const esp_partition_t *p, size_t len,
char *cs_hex) {
bool ret = false;
size_t offset = 0;
mbedtls_sha1_context sha1_ctx;
unsigned char digest[CS_LEN];
mbedtls_sha1_init(&sha1_ctx);
mbedtls_sha1_starts(&sha1_ctx);
while (offset < len) {
uint8_t tmp[MGOS_UPDATER_DATA_CHUNK_SIZE];
size_t block_len = len - offset;
if (block_len > sizeof(tmp)) block_len = sizeof(tmp);
esp_err_t err = esp_partition_read(p, offset, tmp, block_len);
if (err != ESP_OK) {
LOG(LL_ERROR,
("Error reading %s at offset %u: %d", p->label, offset, err));
goto cleanup;
}
/* Special handling of flash params. It's a bit simpler than when writing
* because we know that FLASH_PARAMS_ADDR divides WRITE_CHUNK_SIZE and
* WRITE_CHUNK_SIZE is > FLASH_PARAMS_LEN. */
if (p->address + offset == FLASH_PARAMS_ADDR) {
LOG(LL_DEBUG,
("Swapping %d @ 0x%x", FLASH_PARAMS_LEN, p->address + offset));
memcpy(tmp, p->label + LABEL_OFFSET + FLASH_PARAMS_LEN, FLASH_PARAMS_LEN);
}
mbedtls_sha1_update(&sha1_ctx, tmp, block_len);
offset += block_len;
}
mbedtls_sha1_finish(&sha1_ctx, digest);
bin2hex(digest, CS_LEN, cs_hex);
cs_hex[CS_HEX_LEN] = '\0';
ret = true;
cleanup:
mbedtls_sha1_free(&sha1_ctx);
return ret;
}
static bool verify_checksum(const esp_partition_t *p, size_t len,
const char *exp_sha1, bool critical) {
char cs_hex[CS_HEX_BUF_SIZE];
bool ret = compute_checksum(p, len, cs_hex) &&
(strncmp(cs_hex, exp_sha1, CS_HEX_LEN) == 0);
LOG((ret || !critical ? LL_DEBUG : LL_ERROR),
("%s: %u @ 0x%x, cs_sha1 %s, expected %.*s", p->label, len, p->address,
cs_hex, CS_HEX_LEN, exp_sha1));
return ret;
}
enum mgos_upd_file_action mgos_upd_file_begin(
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi) {
esp_err_t err;
ctx->write_offset = 0;
ctx->write_partition = NULL;
if (strncmp(fi->name, ctx->app_file_name.ptr, ctx->app_file_name.len) == 0) {
if (verify_checksum(ctx->app_partition, fi->size, ctx->app_cs_sha1.ptr,
false /* critical */)) {
LOG(LL_INFO, ("Skip writing app (digest matches)"));
return MGOS_UPDATER_SKIP_FILE;
}
LOG(LL_INFO, ("Writing app image @ 0x%x", ctx->app_partition->address));
if (esp_ota_begin(ctx->app_partition, 0, &ctx->app_ota_handle) != ESP_OK) {
ctx->status_msg = "Failed to start app write";
return MGOS_UPDATER_ABORT;
}
ctx->cs_sha1 = &ctx->app_cs_sha1;
ctx->write_partition = ctx->app_partition;
return MGOS_UPDATER_PROCESS_FILE;
}
if (ctx->update_bootloader &&
strncmp(fi->name, ctx->boot_file_name.ptr, ctx->boot_file_name.len) ==
0) {
LOG(LL_INFO, ("Writing boot loader @ 0x%x", ctx->boot_addr));
ctx->write_partition = &ctx->boot_partition;
ctx->write_offset = ctx->boot_addr;
ctx->cs_sha1 = &ctx->boot_cs_sha1;
} else if (strncmp(fi->name, ctx->fs_file_name.ptr, ctx->fs_file_name.len) ==
0) {
if (verify_checksum(ctx->fs_partition, fi->size, ctx->app_cs_sha1.ptr,
false /* critical */)) {
LOG(LL_INFO, ("Skip writing FS (digest matches)"));
return MGOS_UPDATER_SKIP_FILE;
}
LOG(LL_INFO, ("Writing FS image @ 0x%x", ctx->fs_partition->address));
ctx->write_partition = ctx->fs_partition;
ctx->write_offset = 0;
ctx->cs_sha1 = &ctx->fs_cs_sha1;
}
if (ctx->write_partition != NULL) {
err = esp_partition_erase_range(
ctx->write_partition, ctx->write_offset,
ctx->write_partition->size - ctx->write_offset);
if (err != ESP_OK) {
ctx->status_msg = "Failed to start write";
LOG(LL_ERROR, ("%s: %d", ctx->status_msg, err));
return MGOS_UPDATER_ABORT;
}
return MGOS_UPDATER_PROCESS_FILE;
}
LOG(LL_DEBUG, ("Not interesting: %s", fi->name));
return MGOS_UPDATER_SKIP_FILE;
}
static void swap_flash_params(struct mgos_upd_hal_ctx *ctx, struct mg_str data,
bool save) {
if (ctx->write_partition != &ctx->boot_partition) return;
int off_s = MAX(0, FLASH_PARAMS_ADDR - (int) ctx->write_offset);
int off_d = MAX(0, (int) ctx->write_offset - FLASH_PARAMS_ADDR);
if (off_s >= data.len || off_d >= FLASH_PARAMS_LEN) return;
int len = MIN(FLASH_PARAMS_LEN - off_d, data.len - off_s);
if (save) {
LOG(LL_DEBUG,
("Swapping %d @ 0x%x %d", len, (ctx->write_offset + off_s), off_d));
memcpy(ctx->boot_partition.label + LABEL_OFFSET + FLASH_PARAMS_LEN + off_d,
data.p + off_s, len);
memcpy((char *) data.p + off_s,
ctx->boot_partition.label + LABEL_OFFSET + off_d, len);
} else {
memcpy((char *) data.p + off_s,
ctx->boot_partition.label + LABEL_OFFSET + FLASH_PARAMS_LEN + off_d,
len);
}
}
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi,
struct mg_str data) {
esp_err_t err = ESP_FAIL;
int to_process = (int) data.len;
if (strncmp(fi->name, ctx->app_file_name.ptr, ctx->app_file_name.len) == 0) {
err = esp_ota_write(ctx->app_ota_handle, data.p, to_process);
} else {
swap_flash_params(ctx, data, true /* save */);
err = esp_partition_write(ctx->write_partition, ctx->write_offset, data.p,
to_process);
swap_flash_params(ctx, data, false /* save */);
}
if (err != ESP_OK) {
LOG(LL_ERROR,
("Write %d @ %d failed: %d", (int) data.len, ctx->write_offset, err));
ctx->status_msg = "Failed to write data";
return -1;
}
ctx->write_offset += to_process;
return to_process;
}
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi, struct mg_str tail) {
int ret = -1;
if (tail.len > 0) {
char tmp[MGOS_UPDATER_DATA_CHUNK_SIZE];
memset(tmp, 0xff, sizeof(tmp));
memcpy(tmp, tail.p, tail.len);
ret = mgos_upd_file_data(ctx, fi, mg_mk_str_n(tmp, sizeof(tmp)));
if (ret != sizeof(tmp)) {
ctx->status_msg = "Failed to write tail";
return -1;
}
}
if (strncmp(fi->name, ctx->app_file_name.ptr, ctx->app_file_name.len) == 0) {
esp_ota_handle_t oh = ctx->app_ota_handle;
ctx->app_ota_handle = 0;
if (esp_ota_end(oh) != ESP_OK) {
ctx->app_ota_handle = 0;
ctx->status_msg = "Failed to finalize app write";
return -2;
}
} else if (ctx->write_partition == &ctx->boot_partition) {
ctx->boot_partition.address = ctx->boot_addr;
}
if (!verify_checksum(ctx->write_partition, fi->size, ctx->cs_sha1->ptr,
true /* critical */)) {
ctx->status_msg = "Digest mismatch";
return -10;
} else {
LOG(LL_INFO, ("%s verified (%.*s)", ctx->write_partition->label,
(int) ctx->cs_sha1->len, ctx->cs_sha1->ptr));
}
return tail.len;
}
static bool set_update_status(int old_slot, int new_slot, bool first_boot,
bool merge_fs) {
bool ret = false;
nvs_handle h;
esp_err_t err = nvs_open(MGOS_UPDATE_NVS_NAMESPACE, NVS_READWRITE, &h);
if (err != ESP_OK) {
LOG(LL_ERROR, ("Failed to open NVS: %d", err));
return false;
}
const uint32_t val =
MGOS_UPDATE_NVS_VAL(old_slot, new_slot, first_boot, merge_fs);
err = nvs_set_u32(h, MGOS_UPDATE_NVS_KEY_FLAGS, val);
if (err != ESP_OK) {
LOG(LL_ERROR, ("Failed to set: %d", err));
goto cleanup;
}
LOG(LL_DEBUG, ("New status: %08x", val));
if (first_boot && merge_fs) {
char *old_fs_val;
mg_asprintf(&old_fs_val, 0, "%s %s", mgos_vfs_get_root_fs_type(),
mgos_vfs_get_root_fs_opts());
err = nvs_set_str(h, MGOS_UPDATE_NVS_KEY_OLD_FS, old_fs_val);
free(old_fs_val);
}
nvs_commit(h);
g_boot_status = val;
ret = true;
cleanup:
nvs_close(h);
return ret;
}
static uint32_t get_update_status() {
uint32_t val = 0;
nvs_handle h;
esp_err_t err = nvs_open(MGOS_UPDATE_NVS_NAMESPACE, NVS_READONLY, &h);
if (err != ESP_OK) {
/* This is normal, meaning no update has taken place yet. */
if (err != ESP_ERR_NVS_NOT_FOUND) {
LOG(LL_ERROR, ("Failed to open NVS: %d", err));
}
return val;
}
err = nvs_get_u32(h, MGOS_UPDATE_NVS_KEY_FLAGS, &val);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) {
LOG(LL_ERROR, ("Failed to get: %d", err));
goto cleanup;
}
LOG(LL_DEBUG, ("Update status: %08x", val));
cleanup:
nvs_close(h);
return val;
}
static bool esp32_set_boot_slot(int slot) {
const esp_partition_t *p = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_OTA(slot), NULL);
if (p == NULL) return false;
LOG(LL_INFO, ("Setting boot partition to %s", p->label));
return (esp_ota_set_boot_partition(p) == ESP_OK);
}
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx) {
if (!set_update_status(SUBTYPE_TO_SLOT(ctx->cur_app_partition->subtype),
SUBTYPE_TO_SLOT(ctx->app_partition->subtype),
true /* first_boot */, true /* merge_fs */)) {
ctx->status_msg = "Failed to set update status";
return -1;
}
if (!esp32_set_boot_slot(SUBTYPE_TO_SLOT(ctx->app_partition->subtype))) {
ctx->status_msg = "Failed to set boot partition";
return -1;
}
return 1;
}
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx) {
if (ctx == NULL) return;
if (ctx->app_ota_handle != 0) {
esp_ota_end(ctx->app_ota_handle);
}
memset(ctx, 0, sizeof(*ctx));
free(ctx);
}
static bool copy_partition(const esp_partition_t *src,
const esp_partition_t *dst) {
esp_err_t err;
if (src->size > dst->size) return false;
char cs_hex[CS_HEX_BUF_SIZE];
if (!compute_checksum(src, src->size, cs_hex)) {
return false;
}
if (verify_checksum(dst, src->size, cs_hex, false /* critical */)) {
LOG(LL_INFO,
("%s -> %s: digest matched (%s)", src->label, dst->label, cs_hex));
return true;
}
if ((err = esp_partition_erase_range(dst, 0, src->size)) != ESP_OK) {
LOG(LL_ERROR, ("%s: erase %u failed: %d", dst->label, src->size, err));
return false;
}
uint32_t offset = 0;
while (offset < src->size) {
uint32_t buf[128];
uint32_t n = sizeof(buf);
if (n > src->size - offset) n = src->size - offset;
if ((err = esp_partition_read(src, offset, buf, n)) != ESP_OK) {
LOG(LL_ERROR, ("%s: read @ %u failed: %d", src->label, offset, err));
return false;
}
if ((err = esp_partition_write(dst, offset, buf, n)) != ESP_OK) {
LOG(LL_ERROR, ("%s: write @ %u failed: %d", dst->label, offset, err));
return false;
}
offset += n;
}
if (!verify_checksum(dst, src->size, cs_hex, true /* critical */)) {
return false;
}
LOG(LL_INFO, ("%s -> %s: copied %u bytes, SHA1 %s", src->label, dst->label,
offset, cs_hex));
return true;
}
int mgos_upd_create_snapshot() {
const esp_partition_t *cur_app_partition, *cur_fs_partition;
const esp_partition_t *new_app_partition, *new_fs_partition;
const char *status_msg = NULL;
int slot =
find_inactive_slot(&cur_app_partition, &cur_fs_partition,
&new_app_partition, &new_fs_partition, &status_msg);
if (slot < 0) {
LOG(LL_ERROR, ("%s", status_msg));
return slot;
}
LOG(LL_INFO, ("Snapshot: %s -> %s, %s -> %s", cur_app_partition->label,
new_app_partition->label, cur_fs_partition->label,
new_fs_partition->label));
if (!copy_partition(cur_app_partition, new_app_partition)) return -2;
if (!copy_partition(cur_fs_partition, new_fs_partition)) return -3;
LOG(LL_INFO, ("Snapshot created"));
return slot;
}
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs) {
memset(bs, 0, sizeof(*bs));
bs->active_slot = MGOS_UPDATE_NEW_SLOT(g_boot_status);
bs->revert_slot = MGOS_UPDATE_OLD_SLOT(g_boot_status);
bs->is_committed = (g_boot_status & MGOS_UPDATE_FIRST_BOOT) == 0;
return true;
}
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs) {
return (set_update_status(bs->revert_slot, bs->active_slot,
!bs->is_committed /* first_boot */,
false /* merge_fs */) &&
esp32_set_boot_slot(bs->active_slot));
}
void mgos_upd_boot_revert(void) {
int slot = MGOS_UPDATE_OLD_SLOT(g_boot_status);
if (slot == MGOS_UPDATE_NEW_SLOT(g_boot_status)) return;
LOG(LL_ERROR, ("Reverting to slot %d", slot));
set_update_status(slot, slot, false /* first_boot */, false /* merge_fs */);
esp32_set_boot_slot(slot);
}
void mgos_upd_boot_commit(void) {
int slot = MGOS_UPDATE_NEW_SLOT(g_boot_status);
if (set_update_status(MGOS_UPDATE_OLD_SLOT(g_boot_status), slot,
false /* first_boot */, false /* merge_fs */) &&
esp32_set_boot_slot(slot)) {
LOG(LL_INFO, ("Committed slot %d", slot));
} else {
LOG(LL_ERROR, ("Failed to commit update"));
}
}
int mgos_upd_apply_update(void) {
int ret = -1;
nvs_handle h = 0;
char *old_fs_val = NULL;
int old_slot = MGOS_UPDATE_OLD_SLOT(g_boot_status);
if (MGOS_UPDATE_NEW_SLOT(g_boot_status) == old_slot ||
!(g_boot_status & MGOS_UPDATE_MERGE_FS)) {
return 0;
}
const esp_partition_t *old_fs_part = esp32_find_fs_for_app_slot(old_slot);
if (old_fs_part == NULL) {
LOG(LL_ERROR, ("No old FS partition"));
goto out;
}
const char *old_fs_type = mgos_vfs_get_root_fs_type();
const char *old_fs_opts = mgos_vfs_get_root_fs_opts();
esp_err_t err = nvs_open(MGOS_UPDATE_NVS_NAMESPACE, NVS_READONLY, &h);
if (err == ESP_OK) {
size_t l = 0;
if ((err = nvs_get_str(h, MGOS_UPDATE_NVS_KEY_OLD_FS, NULL, &l)) ==
ESP_OK) {
old_fs_val = malloc(l);
if (old_fs_val != NULL &&
(err = nvs_get_str(h, MGOS_UPDATE_NVS_KEY_OLD_FS, old_fs_val, &l)) ==
ESP_OK) {
old_fs_type = old_fs_val;
char *sp = strchr(old_fs_val, ' ');
*sp = '\0';
old_fs_opts = sp + 1;
}
}
}
if (!esp32_fs_mount_part(old_fs_part->label, "/old", old_fs_type,
old_fs_opts)) {
LOG(LL_ERROR, ("Failed to mount old file system"));
return -1;
}
ret = (mgos_upd_merge_fs("/old", "/") ? 0 : -2);
mgos_vfs_umount("/old");
out:
free(old_fs_val);
if (h != 0) nvs_close(h);
return ret;
}
void esp32_updater_early_init(void) {
g_boot_status = get_update_status();
if (!mgos_upd_is_first_boot()) return;
/*
* Tombstone the current config. If anything goes wrong between now and
* commit, next boot will use the old slot.
*/
uint32_t bs = g_boot_status;
set_update_status(MGOS_UPDATE_OLD_SLOT(g_boot_status),
MGOS_UPDATE_OLD_SLOT(g_boot_status), false /* first_boot */,
false /* merge_fs */);
g_boot_status = bs;
esp32_set_boot_slot(MGOS_UPDATE_OLD_SLOT(g_boot_status));
}
bool mgos_upd_is_first_boot(void) {
return (g_boot_status & MGOS_UPDATE_FIRST_BOOT) != 0;
}

View File

@ -0,0 +1,481 @@
/*
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <stdint.h>
#include <strings.h>
#include <c_types.h>
#include <spi_flash.h>
#include "common/cs_dbg.h"
#include "common/cs_sha1.h"
#include "common/platforms/esp8266/esp_missing_includes.h"
#include "common/platforms/esp8266/rboot/rboot/appcode/rboot-api.h"
#include "common/queue.h"
#include "mgos_hal.h"
#include "mgos_sys_config.h"
#include "mgos_updater_hal.h"
#include "mgos_updater_util.h"
#include "mgos_vfs.h"
#include "esp_flash_writer.h"
#include "esp_fs.h"
#include "esp_rboot.h"
#define CS_LEN 20 /* SHA1 */
#define CS_HEX_LEN (CS_LEN * 2)
#define CS_HEX_BUF_SIZE (CS_HEX_LEN + 1)
#define BOOT_F_MERGE_FS (1U << 0)
#define FW_SLOT_SIZE 0x100000
#define FLASH_PARAMS_ADDR 0
#define WRITE_CHUNK_SIZE 4
struct slot_info {
int id;
uint32_t fw_addr;
uint32_t fw_size;
uint32_t fw_slot_size;
uint32_t fs_addr;
uint32_t fs_size;
uint32_t fs_slot_size;
};
struct mgos_upd_hal_ctx {
const char *status_msg;
struct slot_info write_slot;
struct json_token boot_file_name, boot_cs_sha1;
struct json_token fw_file_name, fw_cs_sha1;
struct json_token fs_file_name, fs_cs_sha1;
uint32_t boot_addr, boot_size, fw_size, fs_size;
bool update_bootloader;
union {
uint8_t bytes[4];
uint32_t align4;
} flash_params;
struct esp_flash_write_ctx wctx;
const struct json_token *wcs;
};
static void get_slot_info(int id, struct slot_info *si) {
memset(si, 0, sizeof(*si));
si->id = id;
if (id == 0) {
si->fw_addr = FW1_ADDR;
si->fs_addr = FW1_FS_ADDR;
} else {
si->fw_addr = FW2_ADDR;
si->fs_addr = FW2_FS_ADDR;
}
rboot_config *cfg = get_rboot_config();
si->fw_size = cfg->roms_sizes[id];
si->fw_slot_size = FW_SIZE;
si->fs_size = cfg->fs_sizes[id];
si->fs_slot_size = FS_SIZE;
}
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void) {
return calloc(1, sizeof(struct mgos_upd_hal_ctx));
}
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx) {
return ctx->status_msg;
}
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts) {
struct json_token fs = JSON_INVALID_TOKEN, fw = JSON_INVALID_TOKEN;
if (json_scanf(parts->ptr, parts->len, "{fw: %T, fs: %T}", &fw, &fs) != 2) {
ctx->status_msg = "Invalid manifest";
return -1;
}
uint32_t boot_addr = 0, fw_addr = 0, fs_addr = 0;
int update_bootloader = false;
json_scanf(parts->ptr, parts->len,
"{boot: {src: %T, addr: %u, cs_sha1: %T, update: %B}, "
"fw: {src: %T, addr: %u, cs_sha1: %T}, "
"fs: {src: %T, addr: %u, cs_sha1: %T}}",
&ctx->boot_file_name, &boot_addr, &ctx->boot_cs_sha1,
&update_bootloader, &ctx->fw_file_name, &fw_addr, &ctx->fw_cs_sha1,
&ctx->fs_file_name, &fs_addr, &ctx->fs_cs_sha1);
if (ctx->fw_file_name.len == 0 || ctx->fw_cs_sha1.len == 0 ||
ctx->fs_file_name.len == 0 || ctx->fs_cs_sha1.len == 0 || fs_addr == 0 ||
(ctx->update_bootloader &&
(ctx->boot_file_name.len == 0 || ctx->boot_cs_sha1.len == 0))) {
ctx->status_msg = "Incomplete update package";
return -3;
}
if (ctx->fw_cs_sha1.len != CS_HEX_LEN || ctx->fs_cs_sha1.len != CS_HEX_LEN ||
(ctx->update_bootloader && ctx->boot_cs_sha1.len != CS_HEX_LEN)) {
ctx->status_msg = "Invalid checksum format";
return -4;
}
struct mgos_upd_boot_state bs;
if (!mgos_upd_boot_get_state(&bs)) return -5;
int inactive_slot = (bs.active_slot == 0 ? 1 : 0);
get_slot_info(inactive_slot, &ctx->write_slot);
if (ctx->write_slot.fw_addr == 0) {
ctx->status_msg = "OTA is not supported in this build";
return -5;
}
ctx->boot_addr = boot_addr;
ctx->update_bootloader = update_bootloader;
if (ctx->update_bootloader) {
/*
* Preserve old flash params.
* We need bytes 2 and 3, but the first 2 bytes are constant anyway, so we
* read and write 4 for simplicity.
*/
if (spi_flash_read(FLASH_PARAMS_ADDR, &ctx->flash_params.align4, 4) != 0) {
ctx->status_msg = "Failed to read flash params";
return -6;
}
LOG(LL_INFO,
("Boot: %.*s -> 0x%x, current flash params: 0x%02x%02x",
(int) ctx->boot_file_name.len, ctx->boot_file_name.ptr, ctx->boot_addr,
ctx->flash_params.bytes[2], ctx->flash_params.bytes[3]));
}
LOG(LL_INFO,
("Slot %d, FW: %.*s -> 0x%x, FS %.*s -> 0x%x", ctx->write_slot.id,
(int) ctx->fw_file_name.len, ctx->fw_file_name.ptr,
ctx->write_slot.fw_addr, (int) ctx->fs_file_name.len,
ctx->fs_file_name.ptr, ctx->write_slot.fs_addr));
return 1;
}
void bin2hex(const uint8_t *src, int src_len, char *dst);
static bool compute_checksum(uint32_t addr, size_t len, char *cs_hex) {
cs_sha1_ctx ctx;
cs_sha1_init(&ctx);
while (len != 0) {
uint32_t read_buf[16];
uint32_t to_read = sizeof(read_buf);
if (to_read > len) to_read = len;
if (spi_flash_read(addr, read_buf, to_read) != 0) {
LOG(LL_ERROR, ("Failed to read %d bytes from %X", to_read, addr));
return false;
}
cs_sha1_update(&ctx, (uint8_t *) read_buf, to_read);
mgos_wdt_feed();
addr += to_read;
len -= to_read;
}
uint8_t cs_buf[CS_LEN];
cs_sha1_final(cs_buf, &ctx);
bin2hex(cs_buf, CS_LEN, cs_hex);
return true;
}
static bool verify_checksum(uint32_t addr, size_t len, const char *exp_cs_hex,
bool critical) {
char cs_hex[CS_HEX_LEN + 1];
if (!compute_checksum(addr, len, cs_hex)) return false;
bool ret = (strncasecmp(cs_hex, exp_cs_hex, CS_HEX_LEN) == 0);
LOG((ret || !critical ? LL_DEBUG : LL_ERROR),
("SHA1 %u @ 0x%x = %.*s, want %.*s", len, addr, CS_HEX_LEN, cs_hex,
CS_HEX_LEN, exp_cs_hex));
return ret;
}
enum mgos_upd_file_action mgos_upd_file_begin(
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi) {
bool res = false;
struct esp_flash_write_ctx *wctx = &ctx->wctx;
if (ctx->update_bootloader &&
strncmp(fi->name, ctx->boot_file_name.ptr, ctx->boot_file_name.len) ==
0) {
if (fi->size <= BOOT_CONFIG_ADDR) {
res = esp_init_flash_write_ctx(wctx, ctx->boot_addr, BOOT_CONFIG_ADDR);
ctx->wcs = &ctx->boot_cs_sha1;
ctx->boot_size = fi->size;
} else {
LOG(LL_ERROR, ("Boot loader too big."));
res = false;
}
} else if (strncmp(fi->name, ctx->fw_file_name.ptr, ctx->fw_file_name.len) ==
0) {
res = esp_init_flash_write_ctx(wctx, ctx->write_slot.fw_addr,
ctx->write_slot.fw_slot_size);
ctx->wcs = &ctx->fw_cs_sha1;
ctx->fw_size = fi->size;
} else if (strncmp(fi->name, ctx->fs_file_name.ptr, ctx->fs_file_name.len) ==
0) {
res = esp_init_flash_write_ctx(wctx, ctx->write_slot.fs_addr,
ctx->write_slot.fs_slot_size);
ctx->wcs = &ctx->fs_cs_sha1;
ctx->fs_size = fi->size;
} else {
LOG(LL_DEBUG, ("Not interesting: %s", fi->name));
return MGOS_UPDATER_SKIP_FILE;
}
if (!res) {
ctx->status_msg = "Failed to start write";
return MGOS_UPDATER_ABORT;
}
if (fi->size > wctx->max_size) {
LOG(LL_ERROR, ("Cannot write %s (%u) @ 0x%x: max size %u", fi->name,
fi->size, wctx->addr, wctx->max_size));
ctx->status_msg = "Image too big";
return MGOS_UPDATER_ABORT;
}
wctx->max_size = fi->size;
if (verify_checksum(wctx->addr, fi->size, ctx->wcs->ptr, false)) {
LOG(LL_INFO, ("Skip writing %s (%u) @ 0x%x (digest matches)", fi->name,
fi->size, wctx->addr));
return MGOS_UPDATER_SKIP_FILE;
}
LOG(LL_INFO,
("Start writing %s (%u) @ 0x%x", fi->name, fi->size, wctx->addr));
return MGOS_UPDATER_PROCESS_FILE;
}
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi,
struct mg_str data) {
int to_process = (data.len / WRITE_CHUNK_SIZE) * WRITE_CHUNK_SIZE;
if (to_process == 0) {
return 0;
}
int num_written = esp_flash_write(&ctx->wctx, data);
if (num_written < 0) {
ctx->status_msg = "Write failed";
}
(void) fi;
return num_written;
}
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi, struct mg_str tail) {
assert(tail.len < WRITE_CHUNK_SIZE);
if (tail.len > 0 && esp_flash_write(&ctx->wctx, tail) != (int) tail.len) {
ctx->status_msg = "Tail write failed";
return -1;
}
if (!verify_checksum(ctx->wctx.addr, fi->size, ctx->wcs->ptr, true)) {
ctx->status_msg = "Invalid checksum";
return -2;
} else {
LOG(LL_INFO, ("Write finished, checksum ok"));
}
if (ctx->update_bootloader &&
strncmp(fi->name, ctx->boot_file_name.ptr, ctx->boot_file_name.len) ==
0) {
LOG(LL_INFO, ("Restoring flash params"));
if (spi_flash_write(FLASH_PARAMS_ADDR, &ctx->flash_params.align4, 4) != 0) {
ctx->status_msg = "Failed to write flash params";
return -3;
}
}
memset(&ctx->wctx, 0, sizeof(ctx->wctx));
return tail.len;
}
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx) {
if (ctx->fw_size == 0) {
ctx->status_msg = "Missing fw part";
return -1;
}
if (ctx->fs_size == 0) {
ctx->status_msg = "Missing fs part";
return -2;
}
int slot = ctx->write_slot.id;
rboot_config *cfg = get_rboot_config();
cfg->current_rom = slot;
cfg->previous_rom = (slot == 0 ? 1 : 0);
cfg->roms[slot] = ctx->write_slot.fw_addr;
cfg->roms_sizes[slot] = ctx->fw_size;
cfg->fs_addresses[slot] = ctx->write_slot.fs_addr;
cfg->fs_sizes[slot] = ctx->fs_size;
cfg->is_first_boot = cfg->fw_updated = true;
cfg->boot_attempts = 0;
cfg->user_flags |= BOOT_F_MERGE_FS;
if (!rboot_set_config(cfg)) {
ctx->status_msg = "Failed to set boot config";
return -3;
}
LOG(LL_INFO,
("New rboot config: "
"prev_rom: %d, current_rom: %d current_rom addr: 0x%x, "
"current_rom size: %d, current_fs addr: 0x%0x, current_fs size: %d",
(int) cfg->previous_rom, (int) cfg->current_rom,
cfg->roms[cfg->current_rom], cfg->roms_sizes[cfg->current_rom],
cfg->fs_addresses[cfg->current_rom], cfg->fs_sizes[cfg->current_rom]));
return 1;
}
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx) {
memset(ctx, 0, sizeof(*ctx));
free(ctx);
}
int mgos_upd_apply_update(void) {
rboot_config *cfg = get_rboot_config();
if (!cfg->user_flags & BOOT_F_MERGE_FS) return 0;
uint32_t old_fs_addr = cfg->fs_addresses[cfg->previous_rom];
uint32_t old_fs_size = cfg->fs_sizes[cfg->previous_rom];
LOG(LL_INFO, ("Mounting old FS: %d @ 0x%x", old_fs_size, old_fs_addr));
if (!esp_fs_mount(old_fs_addr, old_fs_size, "oldroot", "/old")) {
LOG(LL_ERROR, ("Update failed: cannot mount previous file system"));
return -1;
}
int ret = (mgos_upd_merge_fs("/old", "/") ? 0 : -2);
mgos_vfs_umount("/old");
mgos_vfs_dev_unregister("oldroot");
if (ret == 0) {
cfg->user_flags &= ~BOOT_F_MERGE_FS;
rboot_set_config(cfg);
}
return ret;
}
static bool copy_region(uint32_t src_addr, uint32_t dst_addr, size_t len) {
char cs_hex[CS_HEX_LEN + 1];
if (!compute_checksum(src_addr, len, cs_hex)) return false;
if (verify_checksum(dst_addr, len, cs_hex, false)) {
LOG(LL_DEBUG, ("Skip copying %u @ 0x%x -> 0x%x (digest matches)", len,
src_addr, dst_addr));
return true;
}
LOG(LL_DEBUG,
("Copy %u @ 0x%x -> 0x%x (%s)", len, src_addr, dst_addr, cs_hex));
struct esp_flash_write_ctx wctx;
if (!esp_init_flash_write_ctx(&wctx, dst_addr, len)) {
return false;
}
uint32_t offset = 0;
while (offset < len) {
uint32_t read_buf[128];
int to_read = sizeof(read_buf);
if (offset + to_read > len) to_read = len - offset;
if (spi_flash_read(src_addr + offset, read_buf, to_read) != 0) {
LOG(LL_ERROR, ("Failed to read %d @ 0x%x", to_read, src_addr + offset));
return false;
}
int num_written =
esp_flash_write(&wctx, mg_mk_str_n((const char *) read_buf, to_read));
if (num_written < 0) return false;
if (num_written != to_read) {
/* Flush last chunk */
int to_write = to_read - num_written;
num_written = esp_flash_write(
&wctx,
mg_mk_str_n(((const char *) read_buf) + num_written, to_write));
if (num_written != to_write) return false;
}
offset += to_read;
mgos_wdt_feed();
}
if (!verify_checksum(dst_addr, len, cs_hex, true)) {
return false;
}
return true;
}
int mgos_upd_create_snapshot() {
struct slot_info rsi, wsi;
struct mgos_upd_boot_state bs;
if (!mgos_upd_boot_get_state(&bs)) return -1;
int inactive_slot = (bs.active_slot == 0 ? 1 : 0);
get_slot_info(bs.active_slot, &rsi);
get_slot_info(inactive_slot, &wsi);
LOG(LL_INFO, ("Snapshot: %d -> %d, "
"FW: 0x%x (%u) -> 0x%x, FS: 0x%x (%u) -> 0x%x",
rsi.id, wsi.id, rsi.fw_addr, rsi.fw_size, wsi.fw_addr,
rsi.fs_addr, rsi.fs_size, wsi.fs_addr));
if (!copy_region(rsi.fw_addr, wsi.fw_addr, rsi.fw_size)) return -2;
if (!copy_region(rsi.fs_addr, wsi.fs_addr, rsi.fs_size)) return -3;
int slot = wsi.id;
rboot_config *cfg = get_rboot_config();
cfg->roms[slot] = wsi.fw_addr;
cfg->roms_sizes[slot] = rsi.fw_size;
cfg->fs_addresses[slot] = wsi.fs_addr;
cfg->fs_sizes[slot] = rsi.fs_size;
if (!rboot_set_config(cfg)) return -4;
LOG(LL_INFO, ("Snapshot created"));
return slot;
}
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs) {
rboot_config *cfg = get_rboot_config();
if (cfg == NULL) return false;
LOG(LL_DEBUG, ("cur %d prev %d fwu %d", cfg->current_rom, cfg->previous_rom,
cfg->fw_updated));
memset(bs, 0, sizeof(*bs));
bs->active_slot = cfg->current_rom;
bs->revert_slot = cfg->previous_rom;
bs->is_committed = !cfg->fw_updated;
return true;
}
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs) {
rboot_config *cfg = get_rboot_config();
if (cfg == NULL) return false;
if (bs->active_slot < 0 || bs->active_slot > 1 || bs->revert_slot < 0 ||
bs->revert_slot > 1) {
return false;
}
cfg->current_rom = bs->active_slot;
cfg->previous_rom = bs->revert_slot;
cfg->fw_updated = cfg->is_first_boot = (!bs->is_committed);
cfg->boot_attempts = 0;
cfg->user_flags = 0;
LOG(LL_INFO, ("cur %d prev %d fwu %d", cfg->current_rom, cfg->previous_rom,
cfg->fw_updated));
return rboot_set_config(cfg);
}
void mgos_upd_boot_commit() {
struct mgos_upd_boot_state s;
if (!mgos_upd_boot_get_state(&s)) return;
if (s.is_committed) return;
LOG(LL_INFO, ("Committing ROM %d", s.active_slot));
s.is_committed = true;
mgos_upd_boot_set_state(&s);
}
void mgos_upd_boot_revert(void) {
struct mgos_upd_boot_state s;
if (!mgos_upd_boot_get_state(&s)) return;
if (s.is_committed) return;
s.active_slot = (s.active_slot == 0 ? 1 : 0);
LOG(LL_INFO, ("Update failed, reverting to ROM %d", s.active_slot));
s.is_committed = true;
mgos_upd_boot_set_state(&s);
}
bool mgos_upd_is_first_boot(void) {
return get_rboot_config()->is_first_boot;
}

View File

@ -0,0 +1,887 @@
/*
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "mgos_updater_common.h"
#include <stdio.h>
#include <strings.h>
#include "common/cs_crc32.h"
#include "common/cs_dbg.h"
#include "common/cs_file.h"
#include "common/str_util.h"
#include "mgos_event.h"
#include "mgos_hal.h"
#include "mgos_sys_config.h"
#include "mgos_timers.h"
#include "mgos_updater_hal.h"
#include "mgos_vfs.h"
/*
* Using static variable (not only c->user_data), it allows to check if update
* already in progress when another request arrives
*/
struct update_context *s_ctx = NULL;
/* Must be provided externally, usually auto-generated. */
extern const char *build_id;
extern const char *build_version;
#define UPDATER_CTX_FILE_NAME "updater.dat"
#define MANIFEST_FILENAME "manifest.json"
#define SHA1SUM_LEN 40
#define PROGRESS_REPORT_BYTES 50000
#define PROGRESS_REPORT_SECONDS 5
/*
* --- Zip file local header structure ---
* size offset
* local file header signature (0x04034b50) 4 0
* version needed to extract 2 4
* general purpose bit flag 2 6
* compression method 2 8
* last mod file time 2 10
* last mod file date 2 12
* crc-32 4 14
* compressed size 4 18
* uncompressed size 4 22
* file name length 2 26
* extra field length 2 28
* file name (variable size) v 30
* extra field (variable size) v
*/
#define ZIP_LOCAL_HDR_SIZE 30U
#define ZIP_GENFLAG_OFFSET 6U
#define ZIP_COMPRESSION_METHOD_OFFSET 8U
#define ZIP_CRC32_OFFSET 14U
#define ZIP_COMPRESSED_SIZE_OFFSET 18U
#define ZIP_UNCOMPRESSED_SIZE_OFFSET 22U
#define ZIP_FILENAME_LEN_OFFSET 26U
#define ZIP_EXTRAS_LEN_OFFSET 28U
#define ZIP_FILENAME_OFFSET 30U
#define ZIP_FILE_DESCRIPTOR_SIZE 12U
const uint32_t c_zip_file_header_magic = 0x04034b50;
const uint32_t c_zip_cdir_magic = 0x02014b50;
enum update_state {
US_INITED = 0,
US_WAITING_MANIFEST_HEADER,
US_WAITING_MANIFEST,
US_WAITING_FILE_HEADER,
US_WAITING_FILE,
US_SKIPPING_DATA,
US_SKIPPING_DESCRIPTOR,
US_WRITE_FINISHED,
US_FINALIZE,
US_FINISHED,
};
#define MIN(a, b) ((a) < (b) ? (a) : (b))
static void mgos_upd_trigger_ota_event(void) {
struct mgos_ota_status s;
mgos_upd_get_status(&s);
mgos_event_trigger(MGOS_EVENT_OTA_STATUS, &s);
}
static void updater_abort(void *arg) {
struct update_context *ctx = (struct update_context *) arg;
if (s_ctx != ctx) return;
LOG(LL_ERROR, ("Update timed out"));
/* Note that we do not free the context here, because whatever process
* is stuck may still be referring to it. We close the network connection,
* if there is one, to hopefully get things to wind down cleanly. */
if (ctx->nc) ctx->nc->flags |= MG_F_CLOSE_IMMEDIATELY;
ctx->wdt = MGOS_INVALID_TIMER_ID;
s_ctx = NULL;
}
struct update_context *updater_context_create(int timeout) {
if (s_ctx != NULL) {
LOG(LL_ERROR, ("%s", "Update already in progress"));
return NULL;
}
struct mgos_upd_boot_state st;
if (!mgos_upd_boot_get_state(&st)) {
LOG(LL_ERROR, ("%s", "OTA is not supported"));
return NULL;
}
if (!mgos_upd_is_committed()) {
LOG(LL_ERROR, ("%s", "Previous update has not been committed yet"));
return NULL;
}
s_ctx = calloc(1, sizeof(*s_ctx));
if (s_ctx == NULL) {
LOG(LL_ERROR, ("Out of memory"));
return NULL;
}
s_ctx->dev_ctx = mgos_upd_hal_ctx_create();
if (timeout <= 0) timeout = mgos_sys_config_get_update_timeout();
s_ctx->wdt_timeout_ms = timeout * 1000;
s_ctx->wdt = mgos_set_timer(s_ctx->wdt_timeout_ms, 0, updater_abort, s_ctx);
LOG(LL_INFO, ("starting, timeout %d", timeout));
s_ctx->ota_state = MGOS_OTA_STATE_PROGRESS;
return s_ctx;
}
struct update_context *updater_context_get_current(void) {
return s_ctx;
}
void updater_set_status(struct update_context *ctx, enum update_state st) {
LOG(LL_DEBUG, ("Update state %d -> %d", (int) ctx->update_state, (int) st));
ctx->update_state = st;
}
/*
* During its work, updater requires requires to store some data.
* For example, manifest file, zip header - must be received fully, while
* content FW/FS files can be flashed directly from recv_mbuf
* To avoid extra memory usage, context contains plain pointer (*data)
* and mbuf (unprocessed); data is storing in memory only if where is no way
* to process it right now.
*/
static void context_update(struct update_context *ctx, const char *data,
size_t len) {
if (ctx->unprocessed.len != 0) {
/* We have unprocessed data, concatenate them with arrived */
mbuf_append(&ctx->unprocessed, data, len);
ctx->data = ctx->unprocessed.buf;
ctx->data_len = ctx->unprocessed.len;
} else {
/* No unprocessed, trying to process directly received data */
ctx->data = data;
ctx->data_len = len;
}
}
static void context_save_unprocessed(struct update_context *ctx) {
if (ctx->unprocessed.len == 0) {
mbuf_append(&ctx->unprocessed, ctx->data, ctx->data_len);
ctx->data = ctx->unprocessed.buf;
ctx->data_len = ctx->unprocessed.len;
}
}
void context_remove_data(struct update_context *ctx, size_t len) {
if (ctx->unprocessed.len != 0) {
/* Consumed data from unprocessed*/
mbuf_remove(&ctx->unprocessed, len);
ctx->data = ctx->unprocessed.buf;
ctx->data_len = ctx->unprocessed.len;
} else {
/* Consumed received data */
ctx->data = ctx->data + len;
ctx->data_len -= len;
}
}
static void context_clear_current_file(struct update_context *ctx) {
memset(&ctx->info.current_file, 0, sizeof(ctx->info.current_file));
ctx->current_file_crc = ctx->current_file_crc_calc = 0;
ctx->current_file_has_descriptor = false;
}
int is_write_finished(struct update_context *ctx) {
return ctx->update_state == US_WRITE_FINISHED;
}
int is_update_finished(struct update_context *ctx) {
return ctx->update_state == US_FINISHED;
}
int is_reboot_required(struct update_context *ctx) {
return ctx->need_reboot;
}
static int parse_zip_file_header(struct update_context *ctx) {
if (ctx->data_len < ZIP_LOCAL_HDR_SIZE) {
LOG(LL_DEBUG, ("Zip header is incomplete"));
/* Need more data*/
return 0;
}
if (memcmp(ctx->data, &c_zip_file_header_magic, 4) != 0) {
ctx->status_msg = "Malformed archive (invalid file header)";
return -1;
}
uint16_t file_name_len, extras_len;
memcpy(&file_name_len, ctx->data + ZIP_FILENAME_LEN_OFFSET,
sizeof(file_name_len));
memcpy(&extras_len, ctx->data + ZIP_EXTRAS_LEN_OFFSET, sizeof(extras_len));
LOG(LL_DEBUG, ("Filename len = %d bytes, extras len = %d bytes",
(int) file_name_len, (int) extras_len));
if (ctx->data_len < ZIP_LOCAL_HDR_SIZE + file_name_len + extras_len) {
/* Still need mode data */
return 0;
}
uint16_t compression_method;
memcpy(&compression_method, ctx->data + ZIP_COMPRESSION_METHOD_OFFSET,
sizeof(compression_method));
LOG(LL_DEBUG, ("Compression method=%d", (int) compression_method));
if (compression_method != 0) {
/* Do not support compressed archives */
ctx->status_msg = "Cannot handle compressed .zip";
LOG(LL_ERROR, ("%s", ctx->status_msg));
return -1;
}
int i;
char *nodir_file_name = (char *) ctx->data + ZIP_FILENAME_OFFSET;
uint16_t nodir_file_name_len = file_name_len;
LOG(LL_DEBUG,
("File name: %.*s", (int) nodir_file_name_len, nodir_file_name));
for (i = 0; i < file_name_len; i++) {
/* archive may contain folder, but we skip it, using filenames only */
if (*(ctx->data + ZIP_FILENAME_OFFSET + i) == '/') {
nodir_file_name = (char *) ctx->data + ZIP_FILENAME_OFFSET + i + 1;
nodir_file_name_len -= (i + 1);
break;
}
}
LOG(LL_DEBUG,
("File name to use: %.*s", (int) nodir_file_name_len, nodir_file_name));
if (nodir_file_name_len >= sizeof(ctx->info.current_file.name)) {
/* We are in charge of file names, right? */
ctx->status_msg = "Too long file name";
LOG(LL_ERROR, ("%s", ctx->status_msg));
return -1;
}
memcpy(ctx->info.current_file.name, nodir_file_name, nodir_file_name_len);
memcpy(&ctx->info.current_file.size, ctx->data + ZIP_COMPRESSED_SIZE_OFFSET,
sizeof(ctx->info.current_file.size));
uint32_t uncompressed_size;
memcpy(&uncompressed_size, ctx->data + ZIP_UNCOMPRESSED_SIZE_OFFSET,
sizeof(uncompressed_size));
if (ctx->info.current_file.size != uncompressed_size) {
/* Probably malformed archive*/
LOG(LL_ERROR, ("Malformed archive"));
ctx->status_msg = "Malformed archive";
return -1;
}
LOG(LL_DEBUG, ("File size: %u", (unsigned int) ctx->info.current_file.size));
uint16_t gen_flag;
memcpy(&gen_flag, ctx->data + ZIP_GENFLAG_OFFSET, sizeof(gen_flag));
ctx->current_file_has_descriptor = ((gen_flag & (1 << 3)) != 0);
LOG(LL_DEBUG, ("General flag=%d", (int) gen_flag));
memcpy(&ctx->current_file_crc, ctx->data + ZIP_CRC32_OFFSET,
sizeof(ctx->current_file_crc));
LOG(LL_DEBUG, ("CRC32: 0x%08x", (unsigned int) ctx->current_file_crc));
context_remove_data(ctx, ZIP_LOCAL_HDR_SIZE + file_name_len + extras_len);
return 1;
}
static int parse_manifest(struct update_context *ctx) {
struct mgos_upd_info *info = &ctx->info;
ctx->manifest_data = calloc(1, info->current_file.size);
if (ctx->manifest_data == NULL) {
ctx->status_msg = "Out of memory";
return -1;
}
memcpy(ctx->manifest_data, ctx->data, info->current_file.size);
if (json_scanf(
ctx->manifest_data, info->current_file.size,
"{name: %T, platform: %T, version: %T, build_id: %T, parts: %T}",
&info->name, &info->platform, &info->version, &info->build_id,
&info->parts) <= 0) {
ctx->status_msg = "Failed to parse manifest";
return -1;
}
if (info->platform.len == 0 || info->version.len == 0 ||
info->build_id.len == 0 || info->parts.len == 0) {
ctx->status_msg = "Required manifest field missing";
return -1;
}
LOG(LL_INFO,
("FW: %.*s %.*s %s %s -> %.*s %.*s", (int) info->name.len, info->name.ptr,
(int) info->platform.len, info->platform.ptr, build_version, build_id,
(int) info->version.len, info->version.ptr, (int) info->build_id.len,
info->build_id.ptr));
context_remove_data(ctx, info->current_file.size);
return 1;
}
static int finalize_write(struct update_context *ctx, struct mg_str tail) {
/* We have to add the tail to CRC now to be able to verify it. */
if (tail.len > 0) {
ctx->current_file_crc_calc = cs_crc32(ctx->current_file_crc_calc,
(const uint8_t *) tail.p, tail.len);
}
if (ctx->current_file_crc != 0 &&
ctx->current_file_crc != ctx->current_file_crc_calc) {
LOG(LL_ERROR, ("Invalid CRC, want 0x%x, got 0x%x",
(unsigned int) ctx->current_file_crc,
(unsigned int) ctx->current_file_crc_calc));
ctx->status_msg = "Invalid CRC";
return -1;
}
int ret = mgos_upd_file_end(ctx->dev_ctx, &ctx->info.current_file, tail);
if (ret != (int) tail.len) {
if (ret < 0) {
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
} else {
ctx->status_msg = "Not all data was processed";
ret = -1;
}
return ret;
}
context_remove_data(ctx, tail.len);
return 1;
}
static void mgos_updater_progress(struct update_context *ctx) {
if (ctx->last_reported_bytes == 0 ||
ctx->bytes_already_downloaded - ctx->last_reported_bytes >=
PROGRESS_REPORT_BYTES ||
mg_time() - ctx->last_reported_time > PROGRESS_REPORT_SECONDS) {
if (ctx->zip_file_size > 0) {
float ratio =
(float) ctx->bytes_already_downloaded * 100.0f / ctx->zip_file_size;
LOG(LL_INFO,
("%.2f%% total, %s %d of %d", ratio, ctx->info.current_file.name,
(int) ctx->info.current_file.processed,
(int) ctx->info.current_file.size));
} else {
LOG(LL_INFO, ("%s %d of %d", ctx->info.current_file.name,
(int) ctx->info.current_file.processed,
(int) ctx->info.current_file.size));
}
/* If progress has been made, reset the update WDT. */
if (ctx->last_reported_bytes != ctx->bytes_already_downloaded) {
mgos_clear_timer(ctx->wdt);
ctx->wdt = mgos_set_timer(ctx->wdt_timeout_ms, 0, updater_abort, ctx);
}
ctx->last_reported_bytes = ctx->bytes_already_downloaded;
ctx->last_reported_time = mg_time();
mgos_upd_trigger_ota_event();
}
}
static int updater_process_int(struct update_context *ctx, const char *data,
size_t len) {
int ret;
if (len != 0) {
context_update(ctx, data, len);
}
while (true) {
switch (ctx->update_state) {
case US_INITED: {
updater_set_status(ctx, US_WAITING_MANIFEST_HEADER);
} /* fall through */
case US_WAITING_MANIFEST_HEADER: {
if ((ret = parse_zip_file_header(ctx)) <= 0) {
if (ret == 0) {
context_save_unprocessed(ctx);
}
return ret;
}
if (strncmp(ctx->info.current_file.name, MANIFEST_FILENAME,
sizeof(MANIFEST_FILENAME)) != 0) {
/* We've got file header, but it isn't not metadata */
LOG(LL_ERROR, ("Get %s instead of %s", ctx->info.current_file.name,
MANIFEST_FILENAME));
return -1;
}
updater_set_status(ctx, US_WAITING_MANIFEST);
} /* fall through */
case US_WAITING_MANIFEST: {
/*
* Assume metadata isn't too big and might be cached
* otherwise we need streaming json-parser
*/
if (ctx->data_len < ctx->info.current_file.size) {
context_save_unprocessed(ctx);
return 0;
}
if (ctx->current_file_crc != 0 &&
cs_crc32(0, (const uint8_t *) ctx->data,
ctx->info.current_file.size) != ctx->current_file_crc) {
ctx->status_msg = "Invalid CRC";
return -1;
}
if ((ret = parse_manifest(ctx)) < 0) return ret;
if (strncasecmp(ctx->info.platform.ptr,
CS_STRINGIFY_MACRO(FW_ARCHITECTURE),
strlen(CS_STRINGIFY_MACRO(FW_ARCHITECTURE))) != 0) {
LOG(LL_ERROR,
("Wrong platform: want \"%s\", got \"%s\"",
CS_STRINGIFY_MACRO(FW_ARCHITECTURE), ctx->info.platform.ptr));
ctx->status_msg = "Wrong platform";
ctx->ota_state = MGOS_OTA_STATE_ERROR;
return -1;
}
if (ctx->ignore_same_version &&
strncmp(ctx->info.version.ptr, build_version,
ctx->info.version.len) == 0 &&
strncmp(ctx->info.build_id.ptr, build_id, ctx->info.build_id.len) ==
0) {
ctx->status_msg = "Version is the same as current";
return 1;
}
if ((ret = mgos_upd_begin(ctx->dev_ctx, &ctx->info.parts)) < 0) {
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
LOG(LL_ERROR, ("Bad manifest: %d %s", ret, ctx->status_msg));
return ret;
}
ctx->info.abort = false;
mgos_event_trigger(MGOS_EVENT_OTA_BEGIN, &ctx->info);
if (ctx->info.abort) {
ctx->status_msg = "OTA aborted by the MGOS_EVENT_OTA_BEGIN handler";
return -1;
}
context_clear_current_file(ctx);
updater_set_status(ctx, US_WAITING_FILE_HEADER);
} /* fall through */
case US_WAITING_FILE_HEADER: {
if (ctx->data_len < 4) {
context_save_unprocessed(ctx);
return 0;
}
if (memcmp(ctx->data, &c_zip_cdir_magic, 4) == 0) {
LOG(LL_INFO, ("Reached the end of archive"));
updater_set_status(ctx, US_WRITE_FINISHED);
break;
}
if ((ret = parse_zip_file_header(ctx)) <= 0) {
if (ret == 0) context_save_unprocessed(ctx);
return ret;
}
enum mgos_upd_file_action r =
mgos_upd_file_begin(ctx->dev_ctx, &ctx->info.current_file);
if (r == MGOS_UPDATER_ABORT) {
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
return -1;
} else if (r == MGOS_UPDATER_SKIP_FILE) {
updater_set_status(ctx, US_SKIPPING_DATA);
break;
}
updater_set_status(ctx, US_WAITING_FILE);
ctx->current_file_crc_calc = 0;
ctx->last_reported_bytes = 0;
} /* fall through */
case US_WAITING_FILE: {
while (true) {
struct mg_str to_process = {
.p = ctx->data,
.len = MIN(MIN(ctx->info.current_file.size -
ctx->info.current_file.processed,
ctx->data_len),
MGOS_UPDATER_DATA_CHUNK_SIZE),
};
if (to_process.len < MGOS_UPDATER_DATA_CHUNK_SIZE) break;
int num_processed = mgos_upd_file_data(
ctx->dev_ctx, &ctx->info.current_file, to_process);
if (num_processed < 0) {
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
return num_processed;
} else if (num_processed > 0) {
ctx->current_file_crc_calc =
cs_crc32(ctx->current_file_crc_calc,
(const uint8_t *) to_process.p, num_processed);
context_remove_data(ctx, num_processed);
ctx->info.current_file.processed += num_processed;
} else {
break;
}
}
mgos_updater_progress(ctx);
uint32_t bytes_left =
ctx->info.current_file.size - ctx->info.current_file.processed;
if (bytes_left > ctx->data_len) {
context_save_unprocessed(ctx);
return 0;
}
struct mg_str tail = {.p = ctx->data, .len = bytes_left};
assert(tail.len < MGOS_UPDATER_DATA_CHUNK_SIZE);
if (finalize_write(ctx, tail) < 0) {
return -1;
}
context_clear_current_file(ctx);
updater_set_status(ctx, US_WAITING_FILE_HEADER);
break;
}
case US_SKIPPING_DATA: {
uint32_t to_skip =
MIN(ctx->data_len,
ctx->info.current_file.size - ctx->info.current_file.processed);
ctx->info.current_file.processed += to_skip;
context_remove_data(ctx, to_skip);
mgos_updater_progress(ctx);
if (ctx->info.current_file.processed < ctx->info.current_file.size) {
context_save_unprocessed(ctx);
return 0;
}
context_clear_current_file(ctx);
updater_set_status(ctx, US_SKIPPING_DESCRIPTOR);
} /* fall through */
case US_SKIPPING_DESCRIPTOR: {
bool has_descriptor = ctx->current_file_has_descriptor;
LOG(LL_DEBUG, ("Has descriptor : %d", has_descriptor));
context_clear_current_file(ctx);
ctx->current_file_has_descriptor = false;
if (has_descriptor) {
/* If file has descriptor we have to skip 12 bytes after its body */
ctx->info.current_file.size = ZIP_FILE_DESCRIPTOR_SIZE;
updater_set_status(ctx, US_SKIPPING_DATA);
} else {
updater_set_status(ctx, US_WAITING_FILE_HEADER);
}
context_save_unprocessed(ctx);
break;
}
case US_WRITE_FINISHED: {
/* We will stay in this state until explicitly finalized. */
return 0;
}
case US_FINALIZE: {
ret = 1;
ctx->status_msg = "Update applied, finalizing";
if (ctx->fctx.commit_timeout > 0) {
if (!mgos_upd_set_commit_timeout(ctx->fctx.commit_timeout)) {
ctx->status_msg = "Cannot save update status";
return -1;
}
}
if ((ret = mgos_upd_finalize(ctx->dev_ctx)) < 0) {
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
return ret;
}
ctx->result = 1;
ctx->need_reboot = 1;
updater_finish(ctx);
break;
}
case US_FINISHED: {
/* After receiving manifest, fw & fs just skipping all data */
context_remove_data(ctx, ctx->data_len);
if (ctx->result_cb != NULL) {
ctx->result_cb(ctx);
ctx->result_cb = NULL;
}
return ctx->result;
}
}
}
}
int updater_process(struct update_context *ctx, const char *data, size_t len) {
ctx->bytes_already_downloaded += len;
ctx->result = updater_process_int(ctx, data, len);
if (ctx->result != 0) {
updater_finish(ctx);
}
return ctx->result;
}
int updater_finalize(struct update_context *ctx) {
if (ctx->update_state == US_FINISHED) {
return -1;
} else if (ctx->update_state != US_WRITE_FINISHED) {
if (ctx->status_msg == NULL) ctx->status_msg = "Wrong state for finalize";
return -1;
}
updater_set_status(ctx, US_FINALIZE);
return updater_process(ctx, NULL, 0);
}
void updater_finish(struct update_context *ctx) {
updater_set_status(ctx, US_FINISHED);
ctx->ota_state =
ctx->result == 1 ? MGOS_OTA_STATE_SUCCESS : MGOS_OTA_STATE_ERROR;
mgos_upd_trigger_ota_event();
if (ctx->update_state == US_FINISHED) return;
updater_process_int(ctx, NULL, 0);
}
void updater_context_free(struct update_context *ctx) {
if (!is_update_finished(ctx)) {
LOG(LL_ERROR, ("Update terminated unexpectedly"));
}
if (ctx == s_ctx) s_ctx = NULL;
mgos_clear_timer(ctx->wdt);
mgos_upd_hal_ctx_free(ctx->dev_ctx);
mbuf_free(&ctx->unprocessed);
free(ctx->manifest_data);
free(ctx);
}
void bin2hex(const uint8_t *src, int src_len, char *dst) {
int i = 0;
for (i = 0; i < src_len; i++) {
sprintf(dst, "%02x", (int) *src);
dst += 2;
src += 1;
}
}
static bool file_copy(const char *old_path, const char *new_path,
const char *name, char tmp_name[MG_MAX_PATH]) {
bool ret = false;
FILE *old_f = NULL, *new_f = NULL;
struct stat st;
int readen, to_read = 0, total = 0;
LOG(LL_INFO, ("Copying %s", name));
sprintf(tmp_name, "%s/%s", old_path, name);
old_f = fopen(tmp_name, "r");
if (old_f == NULL) {
LOG(LL_ERROR, ("Failed to open %s for reading", tmp_name));
goto out;
}
if (stat(tmp_name, &st) != 0) {
LOG(LL_ERROR, ("Cannot get previous %s size", tmp_name));
goto out;
}
sprintf(tmp_name, "%s/%s", new_path, name);
new_f = fopen(tmp_name, "w");
if (new_f == NULL) {
LOG(LL_ERROR, ("Failed to open %s for writing", tmp_name));
goto out;
}
char buf[128];
to_read = MIN(sizeof(buf), (size_t) st.st_size);
while (to_read != 0) {
if ((readen = fread(buf, 1, to_read, old_f)) < 0) {
LOG(LL_ERROR, ("Failed to read %d bytes from %s", to_read, name));
goto out;
}
if (fwrite(buf, 1, readen, new_f) != (size_t) readen) {
LOG(LL_ERROR, ("Failed to write %d bytes to %s", readen, name));
goto out;
}
total += readen;
to_read = MIN(sizeof(buf), (size_t)(st.st_size - total));
}
LOG(LL_DEBUG, ("Wrote %d to %s", total, tmp_name));
ret = true;
out:
if (old_f != NULL) fclose(old_f);
if (new_f != NULL) {
fclose(new_f);
if (!ret) remove(tmp_name);
}
return ret;
}
bool mgos_upd_merge_fs(const char *old_fs_path, const char *new_fs_path) {
bool ret = false;
DIR *dir = opendir(old_fs_path);
if (dir == NULL) {
LOG(LL_ERROR, ("Failed to open root directory"));
goto out;
}
struct dirent *de;
while ((de = readdir(dir)) != NULL) {
struct stat st;
char tmp_name[MG_MAX_PATH + 1];
sprintf(tmp_name, "%s/%s", new_fs_path, de->d_name);
if (stat(tmp_name, &st) != 0) {
/* File not found on the new fs, copy. */
if (!file_copy(old_fs_path, new_fs_path, de->d_name, tmp_name)) {
LOG(LL_ERROR, ("Failed to copy %s", de->d_name));
goto out;
}
}
mgos_wdt_feed();
}
ret = true;
out:
if (dir != NULL) closedir(dir);
return ret;
}
bool mgos_upd_commit() {
if (mgos_upd_is_committed()) return false;
mgos_upd_boot_commit();
remove(UPDATER_CTX_FILE_NAME);
mgos_upd_trigger_ota_event();
return true;
}
bool mgos_upd_is_committed() {
struct mgos_upd_boot_state s;
if (!mgos_upd_boot_get_state(&s)) return false;
return s.is_committed;
}
bool mgos_upd_revert(bool reboot) {
if (mgos_upd_is_committed()) return false;
mgos_upd_boot_revert();
if (reboot) mgos_system_restart();
return true;
}
void mgos_upd_watchdog_cb(void *arg) {
if (!mgos_upd_is_committed()) {
/* Timer fired and update has not been committed. Revert! */
LOG(LL_ERROR, ("Update commit timeout expired"));
mgos_upd_revert(true /* reboot */);
}
(void) arg;
}
int mgos_upd_get_commit_timeout() {
size_t len;
char *data = cs_read_file(UPDATER_CTX_FILE_NAME, &len);
if (data == NULL) return 0;
struct update_file_context *fctx = (struct update_file_context *) data;
LOG(LL_INFO, ("Update state: %d", fctx->commit_timeout));
int res = fctx->commit_timeout;
free(data);
return res;
}
bool mgos_upd_set_commit_timeout(int commit_timeout) {
bool ret = false;
LOG(LL_DEBUG, ("Writing update state to %s", UPDATER_CTX_FILE_NAME));
FILE *fp = fopen(UPDATER_CTX_FILE_NAME, "w");
if (fp == NULL) return false;
struct update_file_context fctx;
fctx.commit_timeout = commit_timeout;
if (fwrite(&fctx, sizeof(fctx), 1, fp) == 1) {
ret = true;
}
fclose(fp);
return ret;
}
void mgos_upd_boot_finish(bool is_successful, bool is_first) {
/*
* If boot is not successful, there's only one thing to do:
* revert update (if any) and reboot.
* If this was the first boot after an update, this will revert it.
*/
LOG(LL_DEBUG, ("%d %d", is_successful, is_first));
if (!is_first) return;
if (!is_successful) {
mgos_upd_revert(true /* reboot */);
/* Not reached */
return;
}
/* We booted. Now see if we have any special instructions. */
int commit_timeout = mgos_upd_get_commit_timeout();
if (commit_timeout > 0) {
LOG(LL_INFO, ("Arming commit watchdog for %d seconds", commit_timeout));
mgos_set_timer(commit_timeout * 1000, 0 /* repeat */, mgos_upd_watchdog_cb,
NULL);
} else {
mgos_upd_commit();
}
mgos_upd_trigger_ota_event();
}
const char *mgos_ota_state_str(enum mgos_ota_state state) {
switch (state) {
case MGOS_OTA_STATE_IDLE:
return "idle";
case MGOS_OTA_STATE_PROGRESS:
return "progress";
case MGOS_OTA_STATE_ERROR:
return "error";
case MGOS_OTA_STATE_SUCCESS:
return "success";
}
return "";
}
/* For FFI */
const char *mgos_ota_status_get_msg(struct mgos_ota_status *s) {
return s->msg;
}
bool mgos_upd_get_status(struct mgos_ota_status *s) {
struct mgos_upd_boot_state bs;
memset(s, 0, sizeof(*s));
if (s_ctx != NULL) {
s->state = s_ctx->ota_state;
s->msg = s_ctx->status_msg;
if (s_ctx->zip_file_size > 0) {
s->progress_percent =
s_ctx->bytes_already_downloaded * 100.0 / s_ctx->zip_file_size;
}
}
if (s->msg == NULL) s->msg = mgos_ota_state_str(s->state);
if (!mgos_upd_boot_get_state(&bs)) return false;
s->is_committed = bs.is_committed;
s->partition = bs.active_slot;
s->commit_timeout = mgos_upd_get_commit_timeout();
return true;
}
bool mgos_ota_common_init(void) {
return true;
}

View File

@ -0,0 +1,370 @@
/*
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <stdlib.h>
#include "common/cs_crc32.h"
#include "common/cs_dbg.h"
#include "common/str_util.h"
#include "frozen.h"
#include "mgos_boot_cfg.h"
#include "mgos_vfs.h"
#include "mgos_vfs_dev.h"
#include "mgos_updater_hal.h"
#include "mgos_updater_util.h"
#include "stm32_vfs_dev_flash.h"
struct mgos_upd_hal_ctx {
const char *status_msg;
struct mgos_vfs_dev *app_dev, *fs_dev, *bl_dev;
struct mg_str app_file_name, app_cs_sha1;
unsigned int app_bl_size, app_bl_cfg_size, app_fs_size, update_bl;
struct mg_str fs_file_name, fs_cs_sha1;
uintptr_t app_org;
size_t file_offset, app_bl_cfg_offset, app_fs_offset, app_app_offset;
uint32_t app_len, app_crc32;
struct mgos_vfs_dev *cur_dev;
size_t cur_dev_num_erased;
int8_t dst_slot;
};
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void) {
struct mgos_upd_hal_ctx *ctx =
(struct mgos_upd_hal_ctx *) calloc(1, sizeof(*ctx));
return ctx;
}
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx) {
return ctx->status_msg;
}
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts) {
const struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
if (bcfg == NULL) return -1;
struct json_token app_file_name = JSON_INVALID_TOKEN,
app_cs_sha1 = JSON_INVALID_TOKEN;
struct json_token fs_file_name = JSON_INVALID_TOKEN,
fs_cs_sha1 = JSON_INVALID_TOKEN;
json_scanf(parts->ptr, parts->len,
"{app: {src: %T, cs_sha1: %T, bl_size: %u, bl_cfg_size: %u, "
" fs_size: %u, update_bl: %B}, "
"fs: {src: %T, cs_sha1: %T}}",
&app_file_name, &app_cs_sha1, &ctx->app_bl_size,
&ctx->app_bl_cfg_size, &ctx->app_fs_size, &ctx->update_bl,
&fs_file_name, &fs_cs_sha1);
if (app_file_name.len == 0 || app_cs_sha1.len == 0 ||
(ctx->update_bl && ctx->app_bl_size == 0)) {
ctx->status_msg = "Incomplete update package";
return -2;
}
ctx->app_file_name = mg_mk_str_n(app_file_name.ptr, app_file_name.len);
ctx->app_cs_sha1 = mg_mk_str_n(app_cs_sha1.ptr, app_cs_sha1.len);
ctx->fs_file_name = mg_mk_str_n(fs_file_name.ptr, fs_file_name.len);
ctx->fs_cs_sha1 = mg_mk_str_n(fs_cs_sha1.ptr, fs_cs_sha1.len);
ctx->app_bl_cfg_offset = ctx->app_bl_size;
ctx->app_fs_offset = ctx->app_bl_cfg_offset + ctx->app_bl_cfg_size;
ctx->app_app_offset = ctx->app_fs_offset + ctx->app_fs_size;
ctx->app_org = FLASH_BASE + ctx->app_app_offset;
/* Try to put into a directly bootable slot, if possible. */
ctx->dst_slot =
mgos_boot_cfg_find_slot(bcfg, ctx->app_org, true /* want_fs */, -1, -1);
if (ctx->dst_slot < 0) {
/* Ok, try any available slot, boot loader will perform a swap. */
ctx->dst_slot = mgos_boot_cfg_find_slot(bcfg, 0 /* map_addr */,
true /* want_fs */, -1, -1);
}
if (ctx->dst_slot < 0) {
ctx->status_msg = "No slots available for update";
return -3;
}
const struct mgos_boot_slot *sl = &bcfg->slots[ctx->dst_slot];
LOG(LL_INFO, ("Picked slot %d, app -> %s, FS -> %s", ctx->dst_slot,
sl->cfg.app_dev, sl->cfg.fs_dev));
ctx->app_dev = mgos_vfs_dev_open(sl->cfg.app_dev);
if (ctx->app_dev == NULL) {
ctx->status_msg = "Failed to open app_dev";
return -4;
}
ctx->fs_dev = mgos_vfs_dev_open(sl->cfg.fs_dev);
if (ctx->fs_dev == NULL) {
ctx->status_msg = "Failed to open fs_dev";
return -5;
}
if (ctx->update_bl) {
ctx->bl_dev = mgos_vfs_dev_create(MGOS_VFS_DEV_TYPE_STM32_FLASH, NULL);
if (ctx->bl_dev == NULL ||
!stm32_flash_dev_init(ctx->bl_dev, 0, ctx->app_bl_size,
false /* ese */)) {
ctx->status_msg = "Failed to open bl_dev";
return -6;
}
}
LOG(LL_INFO, ("BL size %u (update? %d) + %u cfg; FS size %u; app org 0x%lx",
ctx->app_bl_size, ctx->update_bl, ctx->app_bl_cfg_size,
ctx->app_fs_size, (unsigned long) ctx->app_org));
/* To simplify logic while writing. */
if (ctx->app_bl_size % MGOS_UPDATER_DATA_CHUNK_SIZE != 0 ||
ctx->app_bl_cfg_size % MGOS_UPDATER_DATA_CHUNK_SIZE != 0 ||
ctx->app_fs_size % MGOS_UPDATER_DATA_CHUNK_SIZE != 0) {
ctx->status_msg = "Invalid size";
return -7;
}
return 1;
}
enum mgos_upd_file_action mgos_upd_file_begin(
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi) {
enum mgos_upd_file_action res = MGOS_UPDATER_SKIP_FILE;
ctx->file_offset = 0;
if (mg_vcmp(&ctx->app_file_name, fi->name) == 0) {
if (fi->size < ctx->app_app_offset) {
ctx->status_msg = "App file too short";
res = MGOS_UPDATER_ABORT;
goto out;
}
ctx->app_len = fi->size - ctx->app_app_offset;
res = MGOS_UPDATER_PROCESS_FILE;
} else if (mg_vcmp(&ctx->fs_file_name, fi->name) == 0) {
res = MGOS_UPDATER_PROCESS_FILE;
}
out:
return res;
}
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi,
struct mg_str data) {
int res = -1;
size_t write_offset = 0;
struct mgos_vfs_dev *dev = NULL;
if (mg_vcmp(&ctx->app_file_name, fi->name) == 0) {
if (ctx->file_offset < ctx->app_bl_size) {
/* Boot loader. Write if instructed. */
if (ctx->update_bl) {
dev = ctx->bl_dev;
write_offset = ctx->file_offset;
}
} else if (ctx->file_offset < ctx->app_fs_offset) {
/* Boot loader config is never updated during OTA. */
} else if (ctx->file_offset < ctx->app_app_offset) {
/* FS image - write unless we have a separate file.
* Generally speaking it should be one or the other, not both. */
if (ctx->fs_file_name.len == 0) {
dev = ctx->fs_dev;
write_offset = ctx->file_offset - ctx->app_fs_offset;
}
} else {
/* This is app. */
dev = ctx->app_dev;
write_offset = ctx->file_offset - ctx->app_app_offset;
ctx->app_crc32 =
cs_crc32(ctx->app_crc32, (const uint8_t *) data.p, data.len);
}
} else if (mg_vcmp(&ctx->fs_file_name, fi->name) == 0) {
dev = ctx->fs_dev;
}
if (dev != ctx->cur_dev) {
ctx->cur_dev = dev;
ctx->cur_dev_num_erased = 0;
}
LOG(LL_DEBUG,
("fn %s ds %d fo %d | %d %d %d | dev %s wo %d", fi->name, (int) data.len,
(int) ctx->file_offset, (int) ctx->app_bl_size, (int) ctx->app_fs_offset,
(int) ctx->app_app_offset, (dev ? dev->name : "-"), (int) write_offset));
if (dev != NULL) {
/* See if we need to erase before writing. */
size_t write_end = write_offset + data.len;
if (write_end > ctx->cur_dev_num_erased) {
size_t erase_len = 0;
size_t erase_sizes[MGOS_VFS_DEV_NUM_ERASE_SIZES];
size_t dev_size = mgos_vfs_dev_get_size(dev);
size_t headroom = dev_size - ctx->cur_dev_num_erased;
if (mgos_vfs_dev_get_erase_sizes(dev, erase_sizes) == 0) {
erase_len = write_end - ctx->cur_dev_num_erased;
/* Use the largest erase size smaller than the remaining space ahead. */
for (int i = 0; i < (int) ARRAY_SIZE(erase_sizes); i++) {
if (erase_sizes[i] > 0 && erase_sizes[i] < headroom) {
erase_len = erase_sizes[i];
} else {
break;
}
}
} else {
/* Just nuke the whole thing */
erase_len = headroom;
}
LOG(LL_DEBUG, ("Erase %s %d @ 0x%lx", dev->name, (int) erase_len,
(unsigned long) ctx->cur_dev_num_erased));
enum mgos_vfs_dev_err eres =
mgos_vfs_dev_erase(dev, ctx->cur_dev_num_erased, erase_len);
if (eres != 0) {
ctx->status_msg = "Erase failed";
LOG(LL_INFO,
("%s: erase %d failed: %d", dev->name, (int) erase_len, eres));
goto out;
}
ctx->cur_dev_num_erased += erase_len;
}
enum mgos_vfs_dev_err wres =
mgos_vfs_dev_write(dev, write_offset, data.len, data.p);
if (wres < 0) {
LOG(LL_ERROR,
("%s @ %d => wr %s: %d @ %d = %d", fi->name, (int) ctx->file_offset,
dev->name, (int) data.len, (int) write_offset, wres));
res = wres;
} else {
res = (int) data.len;
}
} else {
res = (int) data.len; /* Skip */
}
out:
ctx->file_offset += data.len;
return res;
}
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
const struct mgos_upd_file_info *fi, struct mg_str tail) {
int res = -1;
if (tail.len > 0) {
int wres = mgos_upd_file_data(ctx, fi, tail);
if (wres != (int) tail.len) {
res = wres;
goto out;
}
}
/* TODO(rojer): Verify SHA1. */
res = (int) tail.len;
out:
return res;
}
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx) {
int res = -1;
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
struct mgos_boot_slot_state *ss;
if (bcfg == NULL) goto out;
bcfg->revert_slot = bcfg->active_slot;
bcfg->active_slot = ctx->dst_slot;
bcfg->flags &= ~(MGOS_BOOT_F_COMMITTED);
bcfg->flags |= (MGOS_BOOT_F_FIRST_BOOT_A | MGOS_BOOT_F_FIRST_BOOT_B);
bcfg->flags |= (MGOS_BOOT_F_MERGE_FS);
ss = &bcfg->slots[ctx->dst_slot].state;
ss->app_len = ctx->app_len;
ss->app_org = ctx->app_org;
ss->app_crc32 = ctx->app_crc32;
ss->app_flags = 0;
LOG(LL_INFO, ("Updating boot config"));
res = (mgos_boot_cfg_write(bcfg, true /* dump */) ? 1 : -1);
out:
return res;
}
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx) {
if (ctx == NULL) return;
mgos_vfs_dev_close(ctx->app_dev);
mgos_vfs_dev_close(ctx->fs_dev);
mgos_vfs_dev_close(ctx->bl_dev);
free(ctx);
}
int mgos_upd_create_snapshot() {
/* TODO */
return -1;
}
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs) {
const struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
if (bcfg == NULL) return false;
memset(bs, 0, sizeof(*bs));
bs->active_slot = bcfg->active_slot;
bs->revert_slot = bcfg->revert_slot;
bs->is_committed = !!(bcfg->flags & MGOS_BOOT_F_COMMITTED);
return true;
}
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs) {
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
if (bcfg == NULL) return false;
bcfg->active_slot = bs->active_slot;
bcfg->revert_slot = bs->revert_slot;
if (bs->is_committed) {
bcfg->flags |= MGOS_BOOT_F_COMMITTED;
} else {
bcfg->flags &= ~(MGOS_BOOT_F_COMMITTED);
}
return mgos_boot_cfg_write(bcfg, true /* dump */);
}
int mgos_upd_apply_update(void) {
int res = -1;
struct mgos_vfs_dev *old_fs_dev = NULL;
const struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
if (bcfg == NULL) goto out;
if (bcfg->revert_slot < 0) {
LOG(LL_ERROR, ("Revert slot not set!"));
goto out;
}
if (!mgos_vfs_mount_dev_name("/old",
bcfg->slots[bcfg->revert_slot].cfg.fs_dev,
CS_STRINGIFY_MACRO(MGOS_ROOT_FS_TYPE),
CS_STRINGIFY_MACRO(MGOS_ROOT_FS_OPTS))) {
LOG(LL_ERROR, ("Failed to mount old file system"));
goto out;
}
res = (mgos_upd_merge_fs("/old", "/") ? 0 : -2);
mgos_vfs_umount("/old");
out:
mgos_vfs_dev_close(old_fs_dev);
return res;
}
static bool mgos_boot_commit_slot(struct mgos_boot_cfg *bcfg, int8_t slot) {
bcfg->active_slot = slot;
bcfg->revert_slot = -1;
bcfg->flags |= MGOS_BOOT_F_COMMITTED;
bcfg->flags &= ~(MGOS_BOOT_F_FIRST_BOOT_A | MGOS_BOOT_F_MERGE_FS);
return (mgos_boot_cfg_write(bcfg, false /* dump */));
}
void mgos_upd_boot_commit(void) {
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
if (bcfg == NULL) return;
int8_t active_slot = bcfg->active_slot;
if (mgos_boot_commit_slot(bcfg, active_slot)) {
LOG(LL_INFO, ("Committed slot %d", active_slot));
}
}
void mgos_upd_boot_revert(void) {
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
if (bcfg == NULL) return;
int8_t revert_slot = bcfg->revert_slot;
if (mgos_boot_commit_slot(bcfg, revert_slot)) {
LOG(LL_INFO, ("Reverted to slot %d", revert_slot));
}
}
bool mgos_upd_is_first_boot(void) {
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
if (bcfg == NULL) return false;
return (bcfg->flags & MGOS_BOOT_F_FIRST_BOOT_A) != 0;
}

View File

@ -0,0 +1,14 @@
Copyright (c) 2018 Cesanta Software Limited
All rights reserved
Licensed under the Apache License, Version 2.0 (the ""License"");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an ""AS IS"" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,26 @@
# Implementation of Mongoose OS OTA HTTP client
This library adds a device configuration section called `update`, where
a device could be configured to poll a specified HTTP URL for a new
app firmware.
Also, this library adds a C API to fetch a new firmware from the given
URL and update programmatically.
## Configuration section
The library adds the following object to the device configuration:
```javascript
"update": {
"commit_timeout": 0, // OTA commit timeout
"url": "", // HTTP URL to poll
"interval": 0, // Polling interval
"ssl_ca_file": "ca.pem", // TLS CA cert file
"ssl_client_cert_file": "", // TLS cert file
"ssl_server_name": "", // TLS server name
"enable_post": true
}
```

View File

@ -1,8 +1,18 @@
/*
* Copyright (c) 2014-2016 Cesanta Software Limited
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* An updater implementation that fetches FW from the given URL.
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CS_MOS_LIBS_OTA_HTTP_CLIENT_SRC_MGOS_OTA_HTTP_CLIENT_H_
@ -16,13 +26,9 @@
extern "C" {
#endif /* __cplusplus */
#if MGOS_ENABLE_UPDATER
bool mgos_ota_http_client_init(void);
/* Start OTA update by pulling the firmware from the given URL. */
void mgos_ota_http_start(struct update_context *ctx, const char *url);
#endif
#ifdef __cplusplus
}
#endif /* __cplusplus */

View File

@ -0,0 +1,6 @@
let OTA = {
// ## **`OTA.evdataOtaStatusMsg(evdata)`**
// Getter function for the `evdata` given to the event callback for the event
// `Event.OTA_STATUS`, see `Event.addHandler()` in `api_events.js`.
evdataOtaStatusMsg: ffi('char *mgos_ota_status_get_msg(void *)'),
};

View File

@ -1,13 +1,17 @@
author: mongoose-os
description: Implements Mongoose OS OTA HTTP client
type: lib
version: 1.18
version: 1.0
sources:
- src
includes:
- include
libs:
- origin: https://github.com/mongoose-os-libs/ota-common
config_schema:
- ["update.url", "s", {title : "Fetch updates form here"}]
- ["update.interval", "i", {title : "Check for updates this often"}]
@ -21,8 +25,7 @@ tags:
- c
- ota
- http
build_vars:
MGOS_ENABLE_UPDATER: 1
- rpc
- docs:net:OTA via HTTP GET
manifest_version: 2017-09-29

View File

@ -1,40 +1,54 @@
/*
* Copyright (c) 2014-2016 Cesanta Software Limited
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "mgos_ota_http_client.h"
#include "common/cs_dbg.h"
#include "mgos_event.h"
#include "mgos_hal.h"
#include "mgos_mongoose.h"
#include "mgos_config.h"
#include "mgos_ro_vars.h"
#include "mgos_sys_config.h"
#include "mgos_timers.h"
#include "mgos_utils.h"
#if MGOS_ENABLE_UPDATER
static void mgos_ota_http_start_internal(struct update_context *ctx,
const char *url, bool restrict_url);
static void fw_download_handler(struct mg_connection *c, int ev, void *p,
void *user_data) {
struct mbuf * io = &c->recv_mbuf;
struct update_context *ctx = (struct update_context *)user_data;
struct mbuf *io = &c->recv_mbuf;
struct update_context *ctx = (struct update_context *) user_data;
int res = 0;
struct mg_str *loc;
(void)p;
(void) p;
switch (ev) {
case MG_EV_CONNECT: {
int result = *((int *)p);
int result = *((int *) p);
if (result != 0) {
LOG(LL_ERROR, ("connect error: %d", result));
LOG(LL_ERROR, ("Connect error: %d", result));
ctx->status_msg = "Failed to connect";
ctx->result = -10;
}
break;
}
case MG_EV_RECV: {
if (ctx->file_size == 0) {
if (ctx->zip_file_size == 0) {
LOG(LL_DEBUG, ("Looking for HTTP header"));
struct http_message hm;
int parsed = mg_parse_http(io->buf, io->len, &hm, 0);
@ -51,31 +65,33 @@ static void fw_download_handler(struct mg_connection *c, int ev, void *p,
(loc = mg_get_http_header(&hm, "Location")) != NULL) {
/* NUL-terminate the URL. Every header must be followed by \r\n,
* so there is deifnitely space there. */
((char *)loc->p)[loc->len] = '\0';
((char *) loc->p)[loc->len] = '\0';
/* We were told to look elsewhere. Detach update context from this
* connection so that it doesn't get finalized when it's closed. */
mgos_ota_http_start(ctx, loc->p);
LOG(LL_INFO, ("Got redirect: [%s]", loc->p));
/* Do not restrict redirected URLs, cause Github redirects */
mgos_ota_http_start_internal(ctx, loc->p, false);
c->user_data = NULL;
} else {
ctx->result = -hm.resp_code;
ctx->need_reboot = false;
ctx->status_msg = "Invalid HTTP response code";
LOG(LL_ERROR, ("%s: %d", ctx->status_msg, hm.resp_code));
updater_finish(ctx);
}
c->flags |= MG_F_CLOSE_IMMEDIATELY;
return;
}
if (hm.body.len != 0) {
LOG(LL_DEBUG, ("HTTP header: file size: %d", (int)hm.body.len));
if (hm.resp_code == 200 && hm.body.len != 0) {
LOG(LL_DEBUG, ("HTTP header: file size: %d", (int) hm.body.len));
if (hm.body.len == (size_t) ~0) {
LOG(LL_ERROR, ("Invalid content-length, perhaps chunked-encoding"));
ctx->status_msg =
"Invalid content-length, perhaps chunked-encoding";
LOG(LL_ERROR, (ctx->status_msg));
c->flags |= MG_F_CLOSE_IMMEDIATELY;
break;
} else {
ctx->file_size = hm.body.len;
ctx->zip_file_size = hm.body.len;
}
mbuf_remove(io, parsed);
@ -87,9 +103,7 @@ static void fw_download_handler(struct mg_connection *c, int ev, void *p,
mbuf_remove(io, io->len);
if (res == 0) {
if (is_write_finished(ctx)) {
res = updater_finalize(ctx);
}
if (is_write_finished(ctx)) res = updater_finalize(ctx);
if (res == 0) {
/* Need more data, everything is OK */
break;
@ -104,25 +118,19 @@ static void fw_download_handler(struct mg_connection *c, int ev, void *p,
}
break;
}
case MG_EV_CLOSE: {
if (ctx == NULL) {
break;
}
if (ctx == NULL) break;
if (is_write_finished(ctx)) {
updater_finalize(ctx);
}
if (is_write_finished(ctx)) updater_finalize(ctx);
if (!is_update_finished(ctx)) {
/* Update failed or connection was terminated by server */
if (ctx->status_msg == NULL) {
ctx->status_msg = "Update failed";
}
ctx->result = -1;
if (ctx->status_msg == NULL) ctx->status_msg = "Update failed";
ctx->result = -5;
} else if (is_reboot_required(ctx)) {
LOG(LL_INFO, ("Rebooting device"));
mgos_system_restart_after(100);
LOG(LL_INFO, ("OTA finished, rebooting device"));
/* Give it 3 seconds to drain any possible pending RPC calls */
mgos_system_restart_after(3000);
}
updater_finish(ctx);
updater_context_free(ctx);
@ -133,8 +141,42 @@ static void fw_download_handler(struct mg_connection *c, int ev, void *p,
}
void mgos_ota_http_start(struct update_context *ctx, const char *url) {
LOG(LL_INFO, ("Update URL: %s, ct: %d, isv? %d", url,
ctx->fctx.commit_timeout, ctx->ignore_same_version));
mgos_ota_http_start_internal(ctx, url, true);
}
static void mgos_ota_http_start_internal(struct update_context *ctx,
const char *url, bool restrict_url) {
LOG(LL_INFO,
("Update URL: %s, ct: %d, isv? %d %d", url, ctx->fctx.commit_timeout,
ctx->ignore_same_version, restrict_url));
#if defined(MGOS_FREE_BUILD)
if (restrict_url) {
const char *allowed_url_prefixes[] = {
"https://github.com/mongoose-os-apps/", "https://mongoose-os.com/",
"http://mongoose-os.com/", "https://dash.mongoose-os.com/",
"http://dash.mongoose-os.com/", NULL,
};
int i;
for (i = 0; allowed_url_prefixes[i] != NULL; i++) {
const char *s = allowed_url_prefixes[i];
if (strncmp(s, url, strlen(s)) == 0) break;
}
if (allowed_url_prefixes[i] == NULL) {
ctx->result = -10;
ctx->need_reboot = false;
ctx->status_msg =
"Free version of OTA library can only perform OTA from "
"github.com/mongoose-os-apps/. For "
"commercial version, please contact "
"https://mongoose-os.com/contact.html";
LOG(LL_ERROR, ("%s", ctx->status_msg));
updater_finish(ctx);
updater_context_free(ctx);
return;
}
}
#endif
struct mg_connect_opts opts;
memset(&opts, 0, sizeof(opts));
@ -149,28 +191,27 @@ void mgos_ota_http_start(struct update_context *ctx, const char *url) {
char ehb[150];
char *extra_headers = ehb;
mg_asprintf(&extra_headers, sizeof(ehb),
mg_asprintf(
&extra_headers, sizeof(ehb),
"Connection: close\r\n"
"X-MGOS-Device-ID: %s %s\r\n"
"X-MGOS-FW-Version: %s %s %s\r\n",
(mgos_sys_config_get_device_id() ? mgos_sys_config_get_device_id() : "-"),
mgos_sys_ro_vars_get_mac_address(),
mgos_sys_ro_vars_get_arch(),
mgos_sys_ro_vars_get_fw_version(),
mgos_sys_ro_vars_get_fw_id());
mgos_sys_ro_vars_get_mac_address(), mgos_sys_ro_vars_get_arch(),
mgos_sys_ro_vars_get_fw_version(), mgos_sys_ro_vars_get_fw_id());
struct mg_connection *c = mg_connect_http_opt(
mgos_get_mgr(), fw_download_handler, ctx, opts, url, extra_headers, NULL);
if (extra_headers != ehb) {
free(extra_headers);
}
if (extra_headers != ehb) free(extra_headers);
if (c == NULL) {
LOG(LL_ERROR, ("Failed to connect to %s", url));
ctx->result = -10;
ctx->result = -11;
ctx->need_reboot = false;
ctx->status_msg = "Failed to connect";
updater_finish(ctx);
updater_context_free(ctx);
return;
}
@ -178,32 +219,42 @@ void mgos_ota_http_start(struct update_context *ctx, const char *url) {
}
static void mgos_ota_timer_cb(void *arg) {
const struct mgos_config_update *mcu = mgos_sys_config_get_update();
if (mcu->url == NULL) {
return;
}
struct update_context *ctx = updater_context_create();
if (ctx == NULL) {
return;
}
if (mgos_sys_config_get_update_url() == NULL) return;
struct update_context *ctx = updater_context_create(0);
if (ctx == NULL) return;
ctx->ignore_same_version = true;
ctx->fctx.commit_timeout = mcu->commit_timeout;
mgos_ota_http_start(ctx, mcu->url);
ctx->fctx.commit_timeout = mgos_sys_config_get_update_commit_timeout();
mgos_ota_http_start(ctx, mgos_sys_config_get_update_url());
(void)arg;
(void) arg;
}
static void ota_request_cb(int ev, void *ev_data, void *userdata) {
struct ota_request_param *p = (struct ota_request_param *) ev_data;
mgos_ota_http_start(p->updater_context, p->location);
(void) ev;
(void) userdata;
}
bool mgos_ota_http_client_init(void) {
const struct mgos_config_update *mcu = mgos_sys_config_get_update();
if (mcu->url != NULL && mcu->interval > 0) {
LOG(LL_INFO,
("Updates from %s, every %d seconds", mcu->url, mcu->interval));
mgos_set_timer(mcu->interval * 1000, true /* repeat */, mgos_ota_timer_cb,
mcu->url);
const char *url = mgos_sys_config_get_update_url();
int interval = mgos_sys_config_get_update_interval();
if (url != NULL && interval > 0) {
LOG(LL_INFO, ("Updates from %s, every %d seconds", url, interval));
mgos_set_timer(interval * 1000, MGOS_TIMER_REPEAT, mgos_ota_timer_cb,
(void *) url);
}
const char *pref = mgos_sys_config_get_sys_pref_ota_lib();
const char *me = "ota-http-client";
bool ota_handler_set = false;
if ((pref == NULL || strcmp(pref, me) == 0) &&
mgos_event_register_base(MGOS_EVENT_OTA_REQUEST, me)) {
mgos_event_add_handler(MGOS_EVENT_OTA_REQUEST, ota_request_cb, NULL);
ota_handler_set = true;
}
LOG(LL_INFO, ("Init done, ota_lib=%s, ota handler %d",
pref == NULL ? "(null)" : pref, ota_handler_set));
(void) ota_handler_set;
return true;
}
#endif /* MGOS_ENABLE_UPDATER */

View File

@ -1,24 +0,0 @@
/*
* Copyright (c) 2014-2016 Cesanta Software Limited
* All rights reserved
*/
#ifndef CS_MOS_LIBS_OTA_HTTP_SERVER_SRC_MGOS_OTA_HTTP_SERVER_H_
#define CS_MOS_LIBS_OTA_HTTP_SERVER_SRC_MGOS_OTA_HTTP_SERVER_H_
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
#if MGOS_ENABLE_UPDATER
bool mgos_ota_http_server_init(void);
#endif
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* CS_MOS_LIBS_OTA_HTTP_SERVER_SRC_MGOS_OTA_HTTP_SERVER_H_ */

View File

@ -1,22 +0,0 @@
author: mongoose-os
description: Implements Mongoose OS OTA HTTP server
type: lib
version: 1.18
sources:
- src
includes:
- include
config_schema:
- ["update.enable_post", "b", true, {title : "Enable POST updates"}]
libs:
- origin: https://github.com/mongoose-os-libs/http-server
- origin: libs/ota-http-client
tags:
- c
- ota
- http
manifest_version: 2017-09-29

View File

@ -1,258 +0,0 @@
/*
* Copyright (c) 2014-2016 Cesanta Software Limited
* All rights reserved
*/
#include "mgos_ota_http_server.h"
#include "mgos_ota_http_client.h"
#include "mgos_http_server.h"
#include "common/cs_dbg.h"
#include "mgos_hal.h"
#include "mgos_mongoose.h"
#include "mgos_config.h"
#include "mgos_timers.h"
#include "mgos_utils.h"
static void handle_update_post(struct mg_connection *c, int ev, void *p) {
struct mg_http_multipart_part *mp = (struct mg_http_multipart_part *)p;
struct update_context * ctx = (struct update_context *)c->user_data;
if (ctx == NULL && ev != MG_EV_HTTP_MULTIPART_REQUEST) {
return;
}
switch (ev) {
case MG_EV_HTTP_MULTIPART_REQUEST: {
ctx = updater_context_create();
if (ctx != NULL) {
ctx->nc = c;
c->user_data = ctx;
} else {
c->flags |= MG_F_CLOSE_IMMEDIATELY;
}
break;
}
case MG_EV_HTTP_PART_BEGIN: {
LOG(LL_DEBUG, ("MG_EV_HTTP_PART_BEGIN: %p %s %s", ctx, mp->var_name,
mp->file_name));
/* We use ctx->file_name as a temp buffer for non-file variable values. */
if (mp->file_name[0] == '\0') {
ctx->file_name[0] = '\0';
}
break;
}
case MG_EV_HTTP_PART_DATA: {
LOG(LL_DEBUG, ("MG_EV_HTTP_PART_DATA: %p %s %s %d", ctx, mp->var_name,
mp->file_name, (int)mp->data.len));
if (mp->file_name[0] == '\0') {
/* It's a non-file form variable. */
size_t l = strlen(ctx->file_name);
size_t avail = sizeof(ctx->file_name) - l - 1;
strncat(ctx->file_name, mp->data.p, MIN(mp->data.len, avail));
break;
} else if (!is_update_finished(ctx)) {
updater_process(ctx, mp->data.p, mp->data.len);
LOG(LL_DEBUG, ("updater_process res: %d", ctx->result));
} else {
/* Don't close connection just yet, not all browsers like that. */
}
break;
}
case MG_EV_HTTP_PART_END: {
LOG(LL_DEBUG, ("MG_EV_HTTP_PART_END: %p %s %s %d", ctx, mp->var_name,
mp->file_name, mp->status));
/* Part finished with an error. REQUEST_END will follow. */
if (mp->status < 0) {
break;
}
if (mp->file_name[0] == '\0') {
/* It's a non-file form variable. Value is in ctx->file_name. */
LOG(LL_DEBUG, ("Got var: %s=%s", mp->var_name, ctx->file_name));
/* Commit timeout can be set after flashing. */
if (strcmp(mp->var_name, "commit_timeout") == 0) {
ctx->fctx.commit_timeout = atoi(ctx->file_name);
}
} else {
/* End of the fw part, but there may still be parts with vars to follow,
* which can modify settings (that can be applied post-flashing). */
}
break;
}
case MG_EV_HTTP_MULTIPART_REQUEST_END: {
LOG(LL_DEBUG,
("MG_EV_HTTP_MULTIPART_REQUEST_END: %p %d", ctx, mp->status));
/* Whatever happens, this is the last thing we do. */
c->flags |= MG_F_SEND_AND_CLOSE;
if (ctx == NULL) {
break;
}
if (is_write_finished(ctx)) {
updater_finalize(ctx);
}
if (!is_update_finished(ctx)) {
ctx->result = -1;
ctx->status_msg = "Update aborted";
updater_finish(ctx);
}
if (mp->status < 0) {
/* mp->status < 0 means connection is dead, do not send reply */
} else {
int code = (ctx->result > 0 ? 200 : 400);
mg_send_response_line(c, code,
"Content-Type: text/plain\r\n"
"Connection: close\r\n");
mg_printf(c, "%s\r\n",
ctx->status_msg ? ctx->status_msg : "Unknown error");
if (is_reboot_required(ctx)) {
LOG(LL_INFO, ("Rebooting device"));
mgos_system_restart_after(101);
}
c->flags |= MG_F_SEND_AND_CLOSE;
}
updater_context_free(ctx);
c->user_data = NULL;
break;
}
}
}
struct mg_connection *s_update_request_conn;
static void mgos_ota_result_cb(struct update_context *ctx) {
if (ctx != updater_context_get_current()) {
return;
}
if (s_update_request_conn != NULL) {
int code = (ctx->result > 0 ? 200 : 500);
mg_send_response_line(s_update_request_conn, code,
"Content-Type: text/plain\r\n"
"Connection: close\r\n");
mg_printf(s_update_request_conn, "(%d) %s\r\n", ctx->result,
ctx->status_msg);
s_update_request_conn->flags |= MG_F_SEND_AND_CLOSE;
s_update_request_conn = NULL;
}
}
static void update_handler(struct mg_connection *c, int ev, void *ev_data,
void *user_data) {
switch (ev) {
case MG_EV_HTTP_MULTIPART_REQUEST:
case MG_EV_HTTP_PART_BEGIN:
case MG_EV_HTTP_PART_DATA:
case MG_EV_HTTP_PART_END:
case MG_EV_HTTP_MULTIPART_REQUEST_END: {
if (mgos_sys_config_get_update_enable_post()) {
handle_update_post(c, ev, ev_data);
} else {
mg_send_response_line(c, 400,
"Content-Type: text/plain\r\n"
"Connection: close\r\n");
mg_printf(c, "POST updates are disabled.");
c->flags |= MG_F_SEND_AND_CLOSE;
}
return;
}
case MG_EV_HTTP_REQUEST: {
struct http_message *hm = (struct http_message *)ev_data;
if (updater_context_get_current() != NULL) {
mg_send_response_line(c, 409,
"Content-Type: text/plain\r\n"
"Connection: close\r\n");
mg_printf(c, "Another update is in progress.\r\n");
c->flags |= MG_F_SEND_AND_CLOSE;
return;
}
const struct mgos_config_update *mcu = mgos_sys_config_get_update();
char * url = mcu->url;
int commit_timeout = mcu->commit_timeout;
bool ignore_same_version = true;
struct mg_str params =
(mg_vcmp(&hm->method, "POST") == 0 ? hm->body : hm->query_string);
size_t buf_len = params.len;
char * buf = calloc(params.len, 1), *p = buf;
int len = mg_get_http_var(&params, "url", p, buf_len);
if (len > 0) {
url = p;
p += len + 1;
buf_len -= len + 1;
}
len = mg_get_http_var(&params, "commit_timeout", p, buf_len);
if (len > 0) {
commit_timeout = atoi(p);
}
len = mg_get_http_var(&params, "ignore_same_version", p, buf_len);
if (len > 0) {
ignore_same_version = (atoi(p) > 0);
}
if (url != NULL) {
s_update_request_conn = c;
struct update_context *ctx = updater_context_create();
if (ctx == NULL) {
mg_send_response_line(c, 409,
"Content-Type: text/plain\r\n"
"Connection: close\r\n");
mg_printf(c, "Failed to create updater context.\r\n");
c->flags |= MG_F_SEND_AND_CLOSE;
return;
}
ctx->ignore_same_version = ignore_same_version;
ctx->fctx.commit_timeout = commit_timeout;
ctx->result_cb = mgos_ota_result_cb;
mgos_ota_http_start(ctx, url);
} else {
mg_send_response_line(c, 400,
"Content-Type: text/plain\r\n"
"Connection: close\r\n");
mg_printf(c, "Update URL not specified and none is configured.\r\n");
c->flags |= MG_F_SEND_AND_CLOSE;
}
free(buf);
break;
}
case MG_EV_CLOSE: {
if (s_update_request_conn == c) {
/* Client went away while waiting for response. */
s_update_request_conn = NULL;
}
break;
}
}
(void)user_data;
}
static void update_action_handler(struct mg_connection *c, int ev, void *p,
void *user_data) {
if (ev != MG_EV_HTTP_REQUEST) {
return;
}
struct http_message *hm = (struct http_message *)p;
bool is_commit = (mg_vcmp(&hm->uri, "/update/commit") == 0);
bool ok =
(is_commit ? mgos_upd_commit() : mgos_upd_revert(false /* reboot */));
mg_send_response_line(c, (ok ? 200 : 400),
"Content-Type: text/html\r\n"
"Connection: close");
mg_printf(c, "\r\n%s\r\n", (ok ? "Ok" : "Error"));
c->flags |= MG_F_SEND_AND_CLOSE;
if (ok && !is_commit) {
mgos_system_restart_after(100);
}
(void)user_data;
}
bool mgos_ota_http_server_init(void) {
mgos_register_http_endpoint("/update/commit", update_action_handler, NULL);
mgos_register_http_endpoint("/update/revert", update_action_handler, NULL);
mgos_register_http_endpoint("/update", update_handler, NULL);
return true;
}

View File

@ -0,0 +1,14 @@
Copyright (c) 2018 Cesanta Software Limited
All rights reserved
Licensed under the Apache License, Version 2.0 (the ""License"");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an ""AS IS"" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,89 @@
# RPC Service - OTA (Over The Air updates)
This service provides an ability to manage OTA on devices remotely.
It is possible to call this service programmatically via serial, HTTP/RESTful,
Websocket, MQTT or other transports
(see [RPC section](/docs/mos/userguide/rpc.md)) or via the `mos` tool.
See in-depth description of our OTA mechanism at
[Updating firmware reliably - embedded.com](http://www.embedded.com/design/prototyping-and-development/4443082/Updating-firmware-reliably).
See OTA video tutorial:
<iframe width="560" src="https://www.youtube.com/embed/o05sBDfaFO8"
height="315" align="center" frameborder="0" allowfullscreen></iframe>
Below is a list of exported RPC methods and arguments:
## OTA.Update
Trigger OTA firmware update. Arguments:
```javascript
{
"url": "https://foo.com/fw123.zip", // Required. URL to the new firmware.
"commit_timeout": "300" // Optional. Time frame in seconds to do
// OTA.Commit after reboot. If commit is
// not done during the timeout, OTA rolls back.
}
```
A new firmware gets downloaded to the separate flash partition,
and is marked dirty. When the download is complete, device is rebooted.
After reboot, a firmware partition could become committed by calling
`OTA.Commit` - in which case, it is marked as "good". Otherwise, a device
reboots back into the old firmware after the `commit_timeout` seconds.
Example usage:
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.Update '{"url": "http://1.2.3.4/fw.zip", "commit_timeout": 300}'</code></pre>
## OTA.Commit
Commit current firmware. Arguments: none.
Example usage:
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.Commit</code></pre>
## OTA.Revert
Rolls back to the previous firmware. Arguments: none.
Example usage:
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.Revert</code></pre>
## OTA.CreateSnapshot
Create new firmware patition with the copy of currently running firmware. Arguments:
```javascript
{
// Optional. If true, then current firmware is uncommited, and needs to
// be explicitly commited after the first reboot. Otherwise, it'll reboot
// into the created snapshot. This option is useful if a dangerous, risky
// live update is to be done on the living device. Then, if the update
// fails and device bricks, it'll revert to the created good snapshot.
"set_as_revert": false,
// Optional. Same meaning as for OTA.Update
"commit_timeout": "300"
}
```
Example usage:
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.CreateSnapshot</code></pre>
## OTA.GetBootState
Get current boot state. Arguments: none.
Example usage:
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.GetBootState
{
"active_slot": 0, # Currently active flash partition.
"is_committed": true, # Current firmware is marked as "good" (committed).
"revert_slot": 0, # If uncommitted, slot to roll back to.
"commit_timeout": 0 # Commit timeout.
}</code></pre>
## OTA.SetBootState
Get current boot state. Arguments: see `OTA.GetBootState` reply section.
Example usage:
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.SetBootState '{"revert_slot": 1}'</code></pre>

View File

@ -1,6 +1,18 @@
/*
* Copyright (c) 2016 Cesanta Software Limited
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CS_FW_SRC_MGOS_UPDATER_MG_RPC_H_

View File

@ -1,17 +1,17 @@
author: mongoose-os
description: Support for Over-The-Air update via RPC
type: lib
version: 1.18
version: 1.0
manifest_version: 2017-09-29
sources:
- src
includes:
- include
libs:
- origin: https://github.com/mongoose-os-libs/ota-http-client
- origin: https://github.com/mongoose-os-libs/rpc-common
- origin: libs/ota-http-client
tags:
- c
- ota
- rpc
- updater
- docs:rpc:Service - OTA

View File

@ -1,13 +1,25 @@
/*
* Copyright (c) 2016 Cesanta Software Limited
* Copyright (c) 2014-2018 Cesanta Software Limited
* All rights reserved
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "mgos_rpc_service_ota.h"
#include "mg_rpc.h"
#include "mgos_rpc.h"
#include "mgos_ota_http_client.h"
#include "mgos_rpc.h"
#include "common/cs_dbg.h"
#include "common/mg_str.h"
@ -17,23 +29,29 @@
static struct mg_rpc_request_info *s_update_req;
static void mg_rpc_updater_result(struct update_context *ctx) {
if (s_update_req == NULL) {
return;
}
mg_rpc_send_errorf(s_update_req, (ctx->result > 0 ? 0 : -1), ctx->status_msg);
static void ota_status_cb(int ev, void *ev_data, void *userdata) {
struct mgos_ota_status *s = (struct mgos_ota_status *) ev_data;
if (s_update_req == NULL) return;
if (s->state == MGOS_OTA_STATE_ERROR) {
mg_rpc_send_errorf(s_update_req, -1, s->msg);
s_update_req = NULL;
} else if (s->state == MGOS_OTA_STATE_SUCCESS) {
mg_rpc_send_responsef(s_update_req, "true");
s_update_req = NULL;
}
(void) ev;
(void) userdata;
}
static void handle_update_req(struct mg_rpc_request_info *ri, void *cb_arg,
struct mg_rpc_frame_info *fi,
struct mg_str args) {
char * blob_url = NULL;
char *blob_url = NULL;
struct json_token url_tok = JSON_INVALID_TOKEN;
int commit_timeout = 0;
int timeout = 0, commit_timeout = 0;
struct update_context *ctx = NULL;
LOG(LL_DEBUG, ("Update request received: %.*s", (int)args.len, args.p));
LOG(LL_DEBUG, ("Update request received: %.*s", (int) args.len, args.p));
const char *reply = "Malformed request";
@ -41,13 +59,12 @@ static void handle_update_req(struct mg_rpc_request_info *ri, void *cb_arg,
goto clean;
}
json_scanf(args.p, args.len, ri->args_fmt, &url_tok, &commit_timeout);
json_scanf(args.p, args.len, ri->args_fmt, &url_tok, &timeout,
&commit_timeout);
if (url_tok.len == 0 || url_tok.type != JSON_TYPE_STRING) {
goto clean;
}
if (url_tok.len == 0 || url_tok.type != JSON_TYPE_STRING) goto clean;
LOG(LL_DEBUG, ("URL: %.*s commit_timeout: %d", url_tok.len, url_tok.ptr,
LOG(LL_DEBUG, ("URL: %.*s to: %d cto: %d", url_tok.len, url_tok.ptr, timeout,
commit_timeout));
/*
@ -63,13 +80,12 @@ static void handle_update_req(struct mg_rpc_request_info *ri, void *cb_arg,
memcpy(blob_url, url_tok.ptr, url_tok.len);
ctx = updater_context_create();
ctx = updater_context_create(timeout);
if (ctx == NULL) {
reply = "Failed to init updater";
goto clean;
}
ctx->fctx.commit_timeout = commit_timeout;
ctx->result_cb = mg_rpc_updater_result;
s_update_req = ri;
mgos_ota_http_start(ctx, blob_url);
@ -77,14 +93,12 @@ static void handle_update_req(struct mg_rpc_request_info *ri, void *cb_arg,
return;
clean:
if (blob_url != NULL) {
free(blob_url);
}
if (blob_url != NULL) free(blob_url);
LOG(LL_ERROR, ("Failed to start update: %s", reply));
mg_rpc_send_errorf(ri, -1, reply);
ri = NULL;
(void)cb_arg;
(void)fi;
(void) cb_arg;
(void) fi;
}
static void handle_commit_req(struct mg_rpc_request_info *ri, void *cb_arg,
@ -96,9 +110,9 @@ static void handle_commit_req(struct mg_rpc_request_info *ri, void *cb_arg,
mg_rpc_send_errorf(ri, -1, NULL);
}
ri = NULL;
(void)cb_arg;
(void)fi;
(void)args;
(void) cb_arg;
(void) fi;
(void) args;
}
static void handle_revert_req(struct mg_rpc_request_info *ri, void *cb_arg,
@ -106,14 +120,14 @@ static void handle_revert_req(struct mg_rpc_request_info *ri, void *cb_arg,
struct mg_str args) {
if (mgos_upd_revert(false /* reboot */)) {
mg_rpc_send_responsef(ri, NULL);
mgos_system_restart_after(100);
mgos_system_restart_after(300);
} else {
mg_rpc_send_errorf(ri, -1, NULL);
}
ri = NULL;
(void)cb_arg;
(void)fi;
(void)args;
(void) cb_arg;
(void) fi;
(void) args;
}
static void handle_create_snapshot_req(struct mg_rpc_request_info *ri,
@ -122,7 +136,6 @@ static void handle_create_snapshot_req(struct mg_rpc_request_info *ri,
struct mg_str args) {
const char *err_msg = NULL;
int ret = -1;
if (mgos_upd_is_committed()) {
ret = mgos_upd_create_snapshot();
if (ret >= 0) {
@ -162,8 +175,8 @@ static void handle_create_snapshot_req(struct mg_rpc_request_info *ri,
} else {
mg_rpc_send_errorf(ri, ret, err_msg);
}
(void)cb_arg;
(void)fi;
(void) cb_arg;
(void) fi;
}
static void handle_get_boot_state_req(struct mg_rpc_request_info *ri,
@ -171,7 +184,6 @@ static void handle_get_boot_state_req(struct mg_rpc_request_info *ri,
struct mg_rpc_frame_info *fi,
struct mg_str args) {
struct mgos_upd_boot_state bs;
if (!mgos_upd_boot_get_state(&bs)) {
mg_rpc_send_errorf(ri, -1, NULL);
} else {
@ -181,9 +193,9 @@ static void handle_get_boot_state_req(struct mg_rpc_request_info *ri,
bs.active_slot, bs.is_committed, bs.revert_slot,
mgos_upd_get_commit_timeout());
}
(void)cb_arg;
(void)fi;
(void)args;
(void) cb_arg;
(void) fi;
(void) args;
}
static void handle_set_boot_state_req(struct mg_rpc_request_info *ri,
@ -192,7 +204,6 @@ static void handle_set_boot_state_req(struct mg_rpc_request_info *ri,
struct mg_str args) {
int ret = 0;
struct mgos_upd_boot_state bs;
if (mgos_upd_boot_get_state(&bs)) {
int commit_timeout = -1;
if (json_scanf(args.p, args.len, ri->args_fmt, &bs.active_slot,
@ -212,28 +223,107 @@ static void handle_set_boot_state_req(struct mg_rpc_request_info *ri,
} else {
mg_rpc_send_errorf(ri, ret, NULL);
}
(void)cb_arg;
(void)fi;
(void) cb_arg;
(void) fi;
}
static void handle_status(struct mg_rpc_request_info *ri, void *cb_arg,
struct mg_rpc_frame_info *fi, struct mg_str args) {
struct mgos_ota_status s;
mgos_upd_get_status(&s);
mg_rpc_send_responsef(ri,
"{state: %d, message: %Q, is_committed: %B, "
"progress_percent: %d, commit_timeout: %d, partition: "
"%d}",
s.state, s.msg, s.is_committed, s.progress_percent,
s.commit_timeout, s.partition);
(void) args;
(void) cb_arg;
(void) fi;
}
static void handle_begin(struct mg_rpc_request_info *ri, void *cb_arg,
struct mg_rpc_frame_info *fi, struct mg_str args) {
int timeout = 0, commit_timeout = 0, size = 0;
json_scanf(args.p, args.len, ri->args_fmt, &timeout, &commit_timeout, &size);
struct update_context *ctx = updater_context_create(timeout);
if (ctx == NULL) {
mg_rpc_send_errorf(ri, -1, "Failed to init updater");
} else {
ctx->fctx.commit_timeout = commit_timeout;
ctx->zip_file_size = size;
mg_rpc_send_responsef(ri, NULL);
}
(void) cb_arg;
(void) fi;
}
static void handle_write(struct mg_rpc_request_info *ri, void *cb_arg,
struct mg_rpc_frame_info *fi, struct mg_str args) {
char *data = NULL;
int len;
struct update_context *ctx = updater_context_get_current();
if (ctx == NULL) {
mg_rpc_send_errorf(ri, -1, "Update not started");
return;
}
json_scanf(args.p, args.len, ri->args_fmt, &data, &len);
if (data == NULL) {
mg_rpc_send_errorf(ri, -1, "Data required");
} else if (updater_process(ctx, data, len) < 0) {
mg_rpc_send_errorf(ri, -1, "Write error: %s",
(ctx->status_msg ? ctx->status_msg : ""));
} else {
mg_rpc_send_responsef(ri, NULL);
}
free(data);
(void) cb_arg;
(void) fi;
}
static void handle_end(struct mg_rpc_request_info *ri, void *cb_arg,
struct mg_rpc_frame_info *fi, struct mg_str args) {
struct update_context *ctx = updater_context_get_current();
if (ctx == NULL) {
mg_rpc_send_errorf(ri, -1, "Update not started");
} else if (updater_finalize(ctx) < 0) {
mg_rpc_send_errorf(ri, -1, "Update finalize failed: %s", ctx->status_msg);
} else {
mgos_system_restart_after(500);
handle_status(ri, cb_arg, fi, args);
}
/* Finalized successfully or not, update is finished. */
if (ctx != NULL) {
updater_finish(ctx);
updater_context_free(ctx);
}
}
bool mgos_rpc_service_ota_init(void) {
struct mg_rpc *mg_rpc = mgos_rpc_get_global();
if (mg_rpc == NULL) {
return true;
}
mg_rpc_add_handler(mg_rpc, "OTA.Update", "{url: %T, commit_timeout: %d}",
if (mg_rpc == NULL) return true;
mg_rpc_add_handler(mg_rpc, "OTA.Update",
"{url: %T, timeout: %d, commit_timeout: %d}",
handle_update_req, NULL);
mg_rpc_add_handler(mg_rpc, "OTA.Commit", "", handle_commit_req, NULL);
mg_rpc_add_handler(mg_rpc, "OTA.Revert", "", handle_revert_req, NULL);
mg_rpc_add_handler(mg_rpc, "OTA.CreateSnapshot",
"{set_as_revert: %B, commit_timeout: %d}",
handle_create_snapshot_req, NULL);
mg_rpc_add_handler(mg_rpc, "OTA.Begin",
"{timeout: %d, commit_timeout: %d, size: %d}",
handle_begin, NULL);
mg_rpc_add_handler(mg_rpc, "OTA.Write", "{data: %V}", handle_write, NULL);
mg_rpc_add_handler(mg_rpc, "OTA.End", "", handle_end, NULL);
mg_rpc_add_handler(mg_rpc, "OTA.Status", "", handle_status, NULL);
mg_rpc_add_handler(mg_rpc, "OTA.GetBootState", "", handle_get_boot_state_req,
NULL);
mg_rpc_add_handler(mg_rpc, "OTA.SetBootState",
"{active_slot: %d, is_committed: %B, revert_slot: %d, "
"commit_timeout: %d}",
handle_set_boot_state_req, NULL);
mgos_event_add_handler(MGOS_EVENT_OTA_STATUS, ota_status_cb, NULL);
return true;
}

View File

@ -1,6 +1,6 @@
author: Pim van Pelt <pim@ipng.nl>
description: A Mongoose-OS Light Switch
version: 1.1
version: 1.2
platform: esp8266
libs_version: ${mos.version}
@ -59,8 +59,9 @@ libs:
- origin: https://github.com/mongoose-os-libs/rpc-mqtt
- origin: https://github.com/mongoose-os-libs/mqtt
- origin: https://github.com/mongoose-os-libs/dht
version: latest
- origin: /home/pim/src/prometheus-sensors
- origin: libs/ota-common
- origin: libs/ota-http-client
- origin: libs/rpc-service-ota
# Used by the mos tool to catch mos binaries incompatible with this file format