From 9ee393bc8a2489999df49df33f1d1200960644e8 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Sun, 28 Oct 2018 12:12:24 +0100 Subject: [PATCH] Rebase private OTA libs on 2.7 --- libs/ota-common/LICENSE | 13 + .../include/cc3200/cc3200_updater.h | 31 + libs/ota-common/include/esp32/esp32_updater.h | 30 + libs/ota-common/include/mgos_updater.h | 47 + libs/ota-common/include/mgos_updater_common.h | 132 +++ libs/ota-common/include/mgos_updater_hal.h | 108 +++ libs/ota-common/include/mgos_updater_util.h | 22 + libs/ota-common/mos.yml | 34 + libs/ota-common/mos_stm32.yml | 2 + libs/ota-common/src/cc3200/cc3200_updater.c | 505 ++++++++++ libs/ota-common/src/esp32/esp32_updater.c | 645 +++++++++++++ libs/ota-common/src/esp8266/esp_updater.c | 481 ++++++++++ libs/ota-common/src/mgos_updater_common.c | 887 ++++++++++++++++++ libs/ota-common/src/stm32/stm32_updater.c | 370 ++++++++ libs/ota-http-client/LICENSE | 14 + libs/ota-http-client/README.md | 26 + .../include/mgos_ota_http_client.h | 20 +- libs/ota-http-client/mjs_fs/api_ota.js | 6 + libs/ota-http-client/mos.yml | 11 +- .../src/mgos_ota_http_client.c | 343 ++++--- .../include/mgos_ota_http_server.h | 24 - libs/ota-http-server/mos.yml | 22 - .../src/mgos_ota_http_server.c | 258 ----- libs/rpc-service-ota/LICENSE | 14 + libs/rpc-service-ota/README.md | 89 ++ .../include/mgos_rpc_service_ota.h | 14 +- libs/rpc-service-ota/mos.yml | 6 +- .../src/mgos_rpc_service_ota.c | 198 ++-- mos.yml | 5 +- 29 files changed, 3836 insertions(+), 521 deletions(-) create mode 100644 libs/ota-common/LICENSE create mode 100644 libs/ota-common/include/cc3200/cc3200_updater.h create mode 100644 libs/ota-common/include/esp32/esp32_updater.h create mode 100644 libs/ota-common/include/mgos_updater.h create mode 100644 libs/ota-common/include/mgos_updater_common.h create mode 100644 libs/ota-common/include/mgos_updater_hal.h create mode 100644 libs/ota-common/include/mgos_updater_util.h create mode 100644 libs/ota-common/mos.yml create mode 100644 libs/ota-common/mos_stm32.yml create mode 100644 libs/ota-common/src/cc3200/cc3200_updater.c create mode 100644 libs/ota-common/src/esp32/esp32_updater.c create mode 100644 libs/ota-common/src/esp8266/esp_updater.c create mode 100644 libs/ota-common/src/mgos_updater_common.c create mode 100644 libs/ota-common/src/stm32/stm32_updater.c create mode 100644 libs/ota-http-client/LICENSE create mode 100644 libs/ota-http-client/README.md create mode 100644 libs/ota-http-client/mjs_fs/api_ota.js delete mode 100644 libs/ota-http-server/include/mgos_ota_http_server.h delete mode 100644 libs/ota-http-server/mos.yml delete mode 100644 libs/ota-http-server/src/mgos_ota_http_server.c create mode 100644 libs/rpc-service-ota/LICENSE create mode 100644 libs/rpc-service-ota/README.md diff --git a/libs/ota-common/LICENSE b/libs/ota-common/LICENSE new file mode 100644 index 0000000..66bb85a --- /dev/null +++ b/libs/ota-common/LICENSE @@ -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. diff --git a/libs/ota-common/include/cc3200/cc3200_updater.h b/libs/ota-common/include/cc3200/cc3200_updater.h new file mode 100644 index 0000000..b842055 --- /dev/null +++ b/libs/ota-common/include/cc3200/cc3200_updater.h @@ -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 + +#ifdef __cplusplus +extern "C" { +#endif + +bool cc3200_upd_init(void); +const char *cc3200_upd_get_fs_container_prefix(void); + +#ifdef __cplusplus +} +#endif diff --git a/libs/ota-common/include/esp32/esp32_updater.h b/libs/ota-common/include/esp32/esp32_updater.h new file mode 100644 index 0000000..9c069cd --- /dev/null +++ b/libs/ota-common/include/esp32/esp32_updater.h @@ -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 + +#ifdef __cplusplus +extern "C" { +#endif + +void esp32_updater_early_init(); + +#ifdef __cplusplus +} +#endif diff --git a/libs/ota-common/include/mgos_updater.h b/libs/ota-common/include/mgos_updater.h new file mode 100644 index 0000000..171fec7 --- /dev/null +++ b/libs/ota-common/include/mgos_updater.h @@ -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 + +#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_ */ diff --git a/libs/ota-common/include/mgos_updater_common.h b/libs/ota-common/include/mgos_updater_common.h new file mode 100644 index 0000000..35da50a --- /dev/null +++ b/libs/ota-common/include/mgos_updater_common.h @@ -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 +#include + +#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_ */ diff --git a/libs/ota-common/include/mgos_updater_hal.h b/libs/ota-common/include/mgos_updater_hal.h new file mode 100644 index 0000000..dbf4b23 --- /dev/null +++ b/libs/ota-common/include/mgos_updater_hal.h @@ -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 + +#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_ */ diff --git a/libs/ota-common/include/mgos_updater_util.h b/libs/ota-common/include/mgos_updater_util.h new file mode 100644 index 0000000..741b61e --- /dev/null +++ b/libs/ota-common/include/mgos_updater_util.h @@ -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 + +#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_ */ diff --git a/libs/ota-common/mos.yml b/libs/ota-common/mos.yml new file mode 100644 index 0000000..bd198dd --- /dev/null +++ b/libs/ota-common/mos.yml @@ -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 diff --git a/libs/ota-common/mos_stm32.yml b/libs/ota-common/mos_stm32.yml new file mode 100644 index 0000000..e8d593e --- /dev/null +++ b/libs/ota-common/mos_stm32.yml @@ -0,0 +1,2 @@ +libs: + - origin: https://github.com/mongoose-os-libs/bootloader diff --git a/libs/ota-common/src/cc3200/cc3200_updater.c b/libs/ota-common/src/cc3200/cc3200_updater.c new file mode 100644 index 0000000..fbdad99 --- /dev/null +++ b/libs/ota-common/src/cc3200/cc3200_updater.c @@ -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 +#include + +#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; +} diff --git a/libs/ota-common/src/esp32/esp32_updater.c b/libs/ota-common/src/esp32/esp32_updater.c new file mode 100644 index 0000000..8e2e390 --- /dev/null +++ b/libs/ota-common/src/esp32/esp32_updater.c @@ -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 +#include + +#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; +} diff --git a/libs/ota-common/src/esp8266/esp_updater.c b/libs/ota-common/src/esp8266/esp_updater.c new file mode 100644 index 0000000..2624381 --- /dev/null +++ b/libs/ota-common/src/esp8266/esp_updater.c @@ -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 +#include + +#include +#include + +#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; +} diff --git a/libs/ota-common/src/mgos_updater_common.c b/libs/ota-common/src/mgos_updater_common.c new file mode 100644 index 0000000..d0d3194 --- /dev/null +++ b/libs/ota-common/src/mgos_updater_common.c @@ -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 +#include + +#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; +} diff --git a/libs/ota-common/src/stm32/stm32_updater.c b/libs/ota-common/src/stm32/stm32_updater.c new file mode 100644 index 0000000..3cd4f2e --- /dev/null +++ b/libs/ota-common/src/stm32/stm32_updater.c @@ -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 + +#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; +} diff --git a/libs/ota-http-client/LICENSE b/libs/ota-http-client/LICENSE new file mode 100644 index 0000000..19bf2e3 --- /dev/null +++ b/libs/ota-http-client/LICENSE @@ -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. diff --git a/libs/ota-http-client/README.md b/libs/ota-http-client/README.md new file mode 100644 index 0000000..d88cdb5 --- /dev/null +++ b/libs/ota-http-client/README.md @@ -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 + } +``` + diff --git a/libs/ota-http-client/include/mgos_ota_http_client.h b/libs/ota-http-client/include/mgos_ota_http_client.h index 0ab57d0..81095f7 100644 --- a/libs/ota-http-client/include/mgos_ota_http_client.h +++ b/libs/ota-http-client/include/mgos_ota_http_client.h @@ -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 */ diff --git a/libs/ota-http-client/mjs_fs/api_ota.js b/libs/ota-http-client/mjs_fs/api_ota.js new file mode 100644 index 0000000..98c6025 --- /dev/null +++ b/libs/ota-http-client/mjs_fs/api_ota.js @@ -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 *)'), +}; diff --git a/libs/ota-http-client/mos.yml b/libs/ota-http-client/mos.yml index af39858..a601b6f 100644 --- a/libs/ota-http-client/mos.yml +++ b/libs/ota-http-client/mos.yml @@ -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 diff --git a/libs/ota-http-client/src/mgos_ota_http_client.c b/libs/ota-http-client/src/mgos_ota_http_client.c index 64974d9..f3b18e2 100644 --- a/libs/ota-http-client/src/mgos_ota_http_client.c +++ b/libs/ota-http-client/src/mgos_ota_http_client.c @@ -1,140 +1,182 @@ /* - * 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; - int res = 0; + 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); - if (result != 0) { - LOG(LL_ERROR, ("connect error: %d", result)); - } - break; - } - - case MG_EV_RECV: { - if (ctx->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); - if (parsed <= 0) { - return; + case MG_EV_CONNECT: { + int result = *((int *) p); + if (result != 0) { + LOG(LL_ERROR, ("Connect error: %d", result)); + ctx->status_msg = "Failed to connect"; + ctx->result = -10; } - if (hm.resp_code != 200) { - if (hm.resp_code == 304) { - ctx->result = 1; - ctx->need_reboot = false; - ctx->status_msg = "Not Modified"; - updater_finish(ctx); - } else if ((hm.resp_code == 301 || hm.resp_code == 302) && - (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'; - - /* 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); - c->user_data = NULL; - } else { - ctx->result = -hm.resp_code; - ctx->need_reboot = false; - ctx->status_msg = "Invalid HTTP response 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.body.len == (size_t) ~0) { - LOG(LL_ERROR, ("Invalid content-length, perhaps chunked-encoding")); - ctx->status_msg = - "Invalid content-length, perhaps chunked-encoding"; - c->flags |= MG_F_CLOSE_IMMEDIATELY; - break; - } else { - ctx->file_size = hm.body.len; - } - - mbuf_remove(io, parsed); - } - } - - if (io->len != 0) { - res = updater_process(ctx, io->buf, io->len); - mbuf_remove(io, io->len); - - if (res == 0) { - if (is_write_finished(ctx)) { - res = updater_finalize(ctx); - } - if (res == 0) { - /* Need more data, everything is OK */ - break; - } - } - - if (res < 0) { - /* Error */ - LOG(LL_ERROR, ("Update error: %d %s", ctx->result, ctx->status_msg)); - } - c->flags |= MG_F_CLOSE_IMMEDIATELY; - } - break; - } - - case MG_EV_CLOSE: { - if (ctx == NULL) { break; } + case MG_EV_RECV: { + 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); + if (parsed <= 0) { + return; + } + if (hm.resp_code != 200) { + if (hm.resp_code == 304) { + ctx->result = 1; + ctx->need_reboot = false; + ctx->status_msg = "Not Modified"; + updater_finish(ctx); + } else if ((hm.resp_code == 301 || hm.resp_code == 302) && + (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'; + /* We were told to look elsewhere. Detach update context from this + * connection so that it doesn't get finalized when it's closed. */ + 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.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) { + ctx->status_msg = + "Invalid content-length, perhaps chunked-encoding"; + LOG(LL_ERROR, (ctx->status_msg)); + c->flags |= MG_F_CLOSE_IMMEDIATELY; + break; + } else { + ctx->zip_file_size = hm.body.len; + } - 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"; + mbuf_remove(io, parsed); + } } - ctx->result = -1; - } else if (is_reboot_required(ctx)) { - LOG(LL_INFO, ("Rebooting device")); - mgos_system_restart_after(100); + + if (io->len != 0) { + res = updater_process(ctx, io->buf, io->len); + mbuf_remove(io, io->len); + + if (res == 0) { + if (is_write_finished(ctx)) res = updater_finalize(ctx); + if (res == 0) { + /* Need more data, everything is OK */ + break; + } + } + + if (res < 0) { + /* Error */ + LOG(LL_ERROR, ("Update error: %d %s", ctx->result, ctx->status_msg)); + } + c->flags |= MG_F_CLOSE_IMMEDIATELY; + } + break; + } + case MG_EV_CLOSE: { + if (ctx == NULL) break; + + 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 = -5; + } else if (is_reboot_required(ctx)) { + 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); + c->user_data = NULL; + break; } - updater_finish(ctx); - updater_context_free(ctx); - c->user_data = NULL; - break; - } } } 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)); @@ -142,35 +184,34 @@ void mgos_ota_http_start(struct update_context *ctx, const char *url) { #if MG_ENABLE_SSL if (strlen(url) > 8 && strncmp(url, "https://", 8) == 0) { opts.ssl_server_name = mgos_sys_config_get_update_ssl_server_name(); - opts.ssl_ca_cert = mgos_sys_config_get_update_ssl_ca_file(); - opts.ssl_cert = mgos_sys_config_get_update_ssl_client_cert_file(); + opts.ssl_ca_cert = mgos_sys_config_get_update_ssl_ca_file(); + opts.ssl_cert = mgos_sys_config_get_update_ssl_client_cert_file(); } #endif - char ehb[150]; + char ehb[150]; char *extra_headers = ehb; - mg_asprintf(&extra_headers, sizeof(ehb), - "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()); + 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()); struct mg_connection *c = mg_connect_http_opt( - mgos_get_mgr(), fw_download_handler, ctx, opts, url, extra_headers, NULL); + 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"; + 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 */ diff --git a/libs/ota-http-server/include/mgos_ota_http_server.h b/libs/ota-http-server/include/mgos_ota_http_server.h deleted file mode 100644 index 6199581..0000000 --- a/libs/ota-http-server/include/mgos_ota_http_server.h +++ /dev/null @@ -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 - -#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_ */ diff --git a/libs/ota-http-server/mos.yml b/libs/ota-http-server/mos.yml deleted file mode 100644 index a512293..0000000 --- a/libs/ota-http-server/mos.yml +++ /dev/null @@ -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 diff --git a/libs/ota-http-server/src/mgos_ota_http_server.c b/libs/ota-http-server/src/mgos_ota_http_server.c deleted file mode 100644 index 219a6ec..0000000 --- a/libs/ota-http-server/src/mgos_ota_http_server.c +++ /dev/null @@ -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(¶ms, "url", p, buf_len); - if (len > 0) { - url = p; - p += len + 1; - buf_len -= len + 1; - } - len = mg_get_http_var(¶ms, "commit_timeout", p, buf_len); - if (len > 0) { - commit_timeout = atoi(p); - } - len = mg_get_http_var(¶ms, "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; -} diff --git a/libs/rpc-service-ota/LICENSE b/libs/rpc-service-ota/LICENSE new file mode 100644 index 0000000..19bf2e3 --- /dev/null +++ b/libs/rpc-service-ota/LICENSE @@ -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. diff --git a/libs/rpc-service-ota/README.md b/libs/rpc-service-ota/README.md new file mode 100644 index 0000000..7b23e17 --- /dev/null +++ b/libs/rpc-service-ota/README.md @@ -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: + + + +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: + +
mos call OTA.Update '{"url": "http://1.2.3.4/fw.zip", "commit_timeout": 300}'
+ + +## OTA.Commit +Commit current firmware. Arguments: none. + +Example usage: + +
mos call OTA.Commit
+ + +## OTA.Revert +Rolls back to the previous firmware. Arguments: none. + +Example usage: +
mos call OTA.Revert
+ + +## 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: +
mos call OTA.CreateSnapshot
+ + +## OTA.GetBootState +Get current boot state. Arguments: none. + +Example usage: +
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.
+}
+ +## OTA.SetBootState +Get current boot state. Arguments: see `OTA.GetBootState` reply section. + +Example usage: +
mos call OTA.SetBootState '{"revert_slot": 1}'
diff --git a/libs/rpc-service-ota/include/mgos_rpc_service_ota.h b/libs/rpc-service-ota/include/mgos_rpc_service_ota.h index dbf4530..c8865ee 100644 --- a/libs/rpc-service-ota/include/mgos_rpc_service_ota.h +++ b/libs/rpc-service-ota/include/mgos_rpc_service_ota.h @@ -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_ diff --git a/libs/rpc-service-ota/mos.yml b/libs/rpc-service-ota/mos.yml index 9e2e577..192a249 100644 --- a/libs/rpc-service-ota/mos.yml +++ b/libs/rpc-service-ota/mos.yml @@ -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 diff --git a/libs/rpc-service-ota/src/mgos_rpc_service_ota.c b/libs/rpc-service-ota/src/mgos_rpc_service_ota.c index d272ec2..ab6492e 100644 --- a/libs/rpc-service-ota/src/mgos_rpc_service_ota.c +++ b/libs/rpc-service-ota/src/mgos_rpc_service_ota.c @@ -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; +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; } - mg_rpc_send_errorf(s_update_req, (ctx->result > 0 ? 0 : -1), ctx->status_msg); - 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; - struct json_token url_tok = JSON_INVALID_TOKEN; - int commit_timeout = 0; + char *blob_url = NULL; + struct json_token url_tok = JSON_INVALID_TOKEN; + 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,28 +80,25 @@ 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; + s_update_req = ri; mgos_ota_http_start(ctx, blob_url); free(blob_url); 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, @@ -121,32 +135,31 @@ static void handle_create_snapshot_req(struct mg_rpc_request_info *ri, struct mg_rpc_frame_info *fi, struct mg_str args) { const char *err_msg = NULL; - int ret = -1; - + int ret = -1; if (mgos_upd_is_committed()) { ret = mgos_upd_create_snapshot(); if (ret >= 0) { - bool set_as_revert = false; - int commit_timeout = -1; + bool set_as_revert = false; + int commit_timeout = -1; json_scanf(args.p, args.len, ri->args_fmt, &set_as_revert, &commit_timeout); if (set_as_revert) { struct mgos_upd_boot_state bs; if (mgos_upd_boot_get_state(&bs)) { bs.is_committed = false; - bs.revert_slot = ret; + bs.revert_slot = ret; if (mgos_upd_boot_set_state(&bs)) { if (commit_timeout >= 0 && !mgos_upd_set_commit_timeout(commit_timeout)) { - ret = -4; + ret = -4; err_msg = "Failed to set commit timeout"; } } else { - ret = -3; + ret = -3; err_msg = "Failed to set boot state"; } } else { - ret = -2; + ret = -2; err_msg = "Failed to get boot state"; } } @@ -154,7 +167,7 @@ static void handle_create_snapshot_req(struct mg_rpc_request_info *ri, err_msg = "Failed to create snapshot"; } } else { - ret = -1; + ret = -1; err_msg = "Cannot create snapshots in uncommitted state"; } 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; } diff --git a/mos.yml b/mos.yml index cbc64e0..840fed8 100644 --- a/mos.yml +++ b/mos.yml @@ -1,6 +1,6 @@ author: Pim van Pelt 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