Rebase private OTA libs on 2.7
This commit is contained in:
13
libs/ota-common/LICENSE
Normal file
13
libs/ota-common/LICENSE
Normal file
@ -0,0 +1,13 @@
|
||||
Copyright 2018 Cesanta Software Limited
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
31
libs/ota-common/include/cc3200/cc3200_updater.h
Normal file
31
libs/ota-common/include/cc3200/cc3200_updater.h
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
bool cc3200_upd_init(void);
|
||||
const char *cc3200_upd_get_fs_container_prefix(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
30
libs/ota-common/include/esp32/esp32_updater.h
Normal file
30
libs/ota-common/include/esp32/esp32_updater.h
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void esp32_updater_early_init();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
47
libs/ota-common/include/mgos_updater.h
Normal file
47
libs/ota-common/include/mgos_updater.h
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*/
|
||||
|
||||
/*
|
||||
* OTA API.
|
||||
*
|
||||
* See https://mongoose-os.com/docs/mos/userguide/ota.md for more details about
|
||||
* Mongoose OS OTA mechanism.
|
||||
*/
|
||||
|
||||
#ifndef CS_FW_SRC_MGOS_UPDATER_H_
|
||||
#define CS_FW_SRC_MGOS_UPDATER_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "frozen.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif /* __cplusplus */
|
||||
|
||||
struct mgos_upd_file_info {
|
||||
char name[50];
|
||||
uint32_t size;
|
||||
uint32_t processed;
|
||||
};
|
||||
|
||||
struct mgos_upd_info {
|
||||
/* Data from the manifest, available from BEGIN until END */
|
||||
struct json_token name;
|
||||
struct json_token platform;
|
||||
struct json_token version;
|
||||
struct json_token build_id;
|
||||
struct json_token parts;
|
||||
bool abort; /* If MGOS_EVENT_OTA_BEGIN handler sets this to true, abort OTA */
|
||||
|
||||
/* Current file, available in PROGRESS. */
|
||||
struct mgos_upd_file_info current_file;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif /* CS_FW_SRC_MGOS_UPDATER_H_ */
|
132
libs/ota-common/include/mgos_updater_common.h
Normal file
132
libs/ota-common/include/mgos_updater_common.h
Normal file
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Common bits of code handling update process.
|
||||
* Driven externaly by data source - mg_rpc or POST file upload.
|
||||
*/
|
||||
|
||||
#ifndef CS_FW_SRC_MGOS_UPDATER_COMMON_H_
|
||||
#define CS_FW_SRC_MGOS_UPDATER_COMMON_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "frozen.h"
|
||||
#include "mgos_event.h"
|
||||
#include "mgos_timers.h"
|
||||
#include "mgos_updater.h"
|
||||
#include "mgos_updater_hal.h"
|
||||
#include "mongoose.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif /* __cplusplus */
|
||||
|
||||
struct update_context;
|
||||
typedef void (*mgos_updater_result_cb)(struct update_context *ctx);
|
||||
|
||||
struct mgos_upd_hal_ctx; /* This struct is defined by HAL and is opaque to us */
|
||||
|
||||
#define MGOS_EVENT_OTA_BASE MGOS_EVENT_BASE('O', 'T', 'A')
|
||||
enum mgos_event_ota {
|
||||
MGOS_EVENT_OTA_BEGIN =
|
||||
MGOS_EVENT_OTA_BASE, /* ev_data: struct mgos_upd_info */
|
||||
MGOS_EVENT_OTA_STATUS, /* ev_data: struct mgos_ota_status */
|
||||
MGOS_EVENT_OTA_REQUEST, /* ev_data: struct ota_request_param */
|
||||
};
|
||||
|
||||
enum mgos_ota_state {
|
||||
MGOS_OTA_STATE_IDLE = 0, /* idle */
|
||||
MGOS_OTA_STATE_PROGRESS, /* "progress" */
|
||||
MGOS_OTA_STATE_ERROR, /* "error" */
|
||||
MGOS_OTA_STATE_SUCCESS, /* "success" */
|
||||
};
|
||||
|
||||
struct mgos_ota_status {
|
||||
bool is_committed;
|
||||
int commit_timeout;
|
||||
int partition;
|
||||
enum mgos_ota_state state;
|
||||
const char *msg; /* stringified state */
|
||||
int progress_percent; /* valid only for "progress" state */
|
||||
};
|
||||
|
||||
const char *mgos_ota_state_str(enum mgos_ota_state state);
|
||||
|
||||
struct update_context {
|
||||
int update_state; /* Internal state machine - parsing zip, etc */
|
||||
enum mgos_ota_state ota_state; /* Externally visible */
|
||||
const char *status_msg;
|
||||
|
||||
char *zip_file_url;
|
||||
size_t zip_file_size;
|
||||
size_t bytes_already_downloaded;
|
||||
size_t last_reported_bytes;
|
||||
double last_reported_time;
|
||||
|
||||
const char *data;
|
||||
size_t data_len;
|
||||
struct mbuf unprocessed;
|
||||
|
||||
struct mgos_upd_info info;
|
||||
uint32_t current_file_crc;
|
||||
uint32_t current_file_crc_calc;
|
||||
bool current_file_has_descriptor;
|
||||
|
||||
bool ignore_same_version;
|
||||
bool need_reboot;
|
||||
|
||||
int result;
|
||||
mgos_updater_result_cb result_cb;
|
||||
|
||||
char *manifest_data;
|
||||
char file_name[50];
|
||||
|
||||
struct mgos_upd_hal_ctx *dev_ctx;
|
||||
|
||||
mgos_timer_id wdt;
|
||||
int wdt_timeout_ms;
|
||||
|
||||
/* Network connection associated with this update, if any.
|
||||
* It is only used in case update times out - it is closed. */
|
||||
struct mg_connection *nc;
|
||||
|
||||
/*
|
||||
* At the end of update this struct is written to a file
|
||||
* and then restored after reboot.
|
||||
*/
|
||||
struct update_file_context {
|
||||
int commit_timeout;
|
||||
} fctx __attribute__((packed));
|
||||
};
|
||||
|
||||
struct update_context *updater_context_create(int timeout);
|
||||
|
||||
/*
|
||||
* Returns updater context of the update in progress. If no update is in
|
||||
* progress, returns NULL.
|
||||
*/
|
||||
struct update_context *updater_context_get_current(void);
|
||||
int updater_process(struct update_context *ctx, const char *data, size_t len);
|
||||
void updater_finish(struct update_context *ctx);
|
||||
void updater_context_free(struct update_context *ctx);
|
||||
int updater_finalize(struct update_context *ctx);
|
||||
int is_write_finished(struct update_context *ctx);
|
||||
int is_update_finished(struct update_context *ctx);
|
||||
int is_reboot_required(struct update_context *ctx);
|
||||
|
||||
void mgos_upd_boot_finish(bool is_successful, bool is_first);
|
||||
bool mgos_upd_commit();
|
||||
bool mgos_upd_is_committed();
|
||||
bool mgos_upd_revert(bool reboot);
|
||||
bool mgos_upd_get_status(struct mgos_ota_status *);
|
||||
|
||||
int mgos_upd_get_commit_timeout();
|
||||
bool mgos_upd_set_commit_timeout(int commit_timeout);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif /* CS_FW_SRC_MGOS_UPDATER_COMMON_H_ */
|
108
libs/ota-common/include/mgos_updater_hal.h
Normal file
108
libs/ota-common/include/mgos_updater_hal.h
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Updater HAL. Devices need to implement this interface.
|
||||
*/
|
||||
|
||||
#ifndef CS_FW_SRC_MGOS_UPDATER_HAL_H_
|
||||
#define CS_FW_SRC_MGOS_UPDATER_HAL_H_
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "common/mbuf.h"
|
||||
#include "common/mg_str.h"
|
||||
#include "frozen.h"
|
||||
|
||||
#include "mgos_updater.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif /* __cplusplus */
|
||||
|
||||
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void);
|
||||
|
||||
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx);
|
||||
|
||||
/*
|
||||
* Process the firmware manifest. Parsed manifest is available in ctx->manifest,
|
||||
* and for convenience the "parts" key within it is in ctx->parts.
|
||||
* Return >= 0 if ok, < 0 + ctx->status_msg on error.
|
||||
*/
|
||||
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts);
|
||||
|
||||
/*
|
||||
* Decide what to do with the next file.
|
||||
* In case of abort, message should be provided in status_msg.
|
||||
*/
|
||||
enum mgos_upd_file_action {
|
||||
MGOS_UPDATER_ABORT,
|
||||
MGOS_UPDATER_PROCESS_FILE,
|
||||
MGOS_UPDATER_SKIP_FILE,
|
||||
};
|
||||
enum mgos_upd_file_action mgos_upd_file_begin(
|
||||
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi);
|
||||
|
||||
#ifndef MGOS_UPDATER_DATA_CHUNK_SIZE
|
||||
#define MGOS_UPDATER_DATA_CHUNK_SIZE 512
|
||||
#endif
|
||||
/*
|
||||
* Process a chunk of file data. Data will be delivered to this function in
|
||||
* MGOS_UPDATER_DATA_CHUNK_SIZE chunks.
|
||||
* Return number of bytes processed (0 .. data.len)
|
||||
* or < 0 for error. In case of error, message should be provided in status_msg.
|
||||
*/
|
||||
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi, struct mg_str data);
|
||||
|
||||
/*
|
||||
* Finalize a file. Remainder of the data (if any) is passed,
|
||||
* number of bytes of that data processed should be returned. The amount of data
|
||||
* will be less than MGOS_UPDATER_DATA_CHUNK_SIZE.
|
||||
* Value equal to data.len is an indication of success,
|
||||
* < 0 + ctx->status_msg on error.
|
||||
*/
|
||||
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi, struct mg_str data);
|
||||
|
||||
/*
|
||||
* Finalize the update.
|
||||
* Return >= 0 if ok, < 0 + ctx->status_msg on error.
|
||||
*/
|
||||
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx);
|
||||
|
||||
bool mgos_upd_is_first_boot(void);
|
||||
|
||||
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx);
|
||||
|
||||
/* Apply update on first boot, usually involves merging filesystem. */
|
||||
int mgos_upd_apply_update(void);
|
||||
|
||||
/*
|
||||
* Create a snapshot of currently running firmware (including FS) in
|
||||
* a currently inactive slot. There must be no uncommitted update
|
||||
* in progress.
|
||||
* Returns slot id used for snapshot or < 0 in case of error.
|
||||
*/
|
||||
int mgos_upd_create_snapshot(void);
|
||||
|
||||
struct mgos_upd_boot_state {
|
||||
/* Slot that will be used to load firmware during next boot. */
|
||||
int active_slot;
|
||||
/* Whether the boot configuration is committed or not.
|
||||
* Reboot with uncommitted configration reverts to revert_slot. */
|
||||
bool is_committed;
|
||||
/* Slot that will be used in case of revert, explicit or implicit. */
|
||||
int revert_slot;
|
||||
};
|
||||
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs);
|
||||
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs);
|
||||
/* Shortcuts for get and set */
|
||||
void mgos_upd_boot_commit(void);
|
||||
void mgos_upd_boot_revert(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif /* CS_FW_SRC_MGOS_UPDATER_HAL_H_ */
|
22
libs/ota-common/include/mgos_updater_util.h
Normal file
22
libs/ota-common/include/mgos_updater_util.h
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*/
|
||||
|
||||
#ifndef CS_FW_SRC_MGOS_UPDATER_UTIL_H_
|
||||
#define CS_FW_SRC_MGOS_UPDATER_UTIL_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif /* __cplusplus */
|
||||
|
||||
/* Function to merge filesystems. */
|
||||
bool mgos_upd_merge_fs(const char *old_fs_path, const char *new_fs_path);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif /* CS_FW_SRC_MGOS_UPDATER_UTIL_H_ */
|
34
libs/ota-common/mos.yml
Normal file
34
libs/ota-common/mos.yml
Normal file
@ -0,0 +1,34 @@
|
||||
author: mongoose-os
|
||||
description: OTA common bits
|
||||
type: lib
|
||||
version: 1.0
|
||||
|
||||
platforms: [ cc3200, esp32, esp8266, stm32 ]
|
||||
|
||||
sources:
|
||||
- src
|
||||
- src/${platform}
|
||||
|
||||
includes:
|
||||
- include
|
||||
- include/${platform}
|
||||
|
||||
libs:
|
||||
- origin: https://github.com/mongoose-os-libs/mongoose
|
||||
|
||||
no_implicit_init_deps: true
|
||||
init_deps:
|
||||
- mongoose
|
||||
|
||||
config_schema:
|
||||
- ["update", "o", {title: "Firmware updater"}]
|
||||
- ["update.timeout", "i", 600, {title : "Update will be aborted if it does not finish within this time"}]
|
||||
- ["update.commit_timeout", "i", {title : "After applying update, wait for commit up to this long"}]
|
||||
|
||||
tags:
|
||||
- c
|
||||
- core
|
||||
- ota
|
||||
- docs:net:OTA
|
||||
|
||||
manifest_version: 2018-06-20
|
2
libs/ota-common/mos_stm32.yml
Normal file
2
libs/ota-common/mos_stm32.yml
Normal file
@ -0,0 +1,2 @@
|
||||
libs:
|
||||
- origin: https://github.com/mongoose-os-libs/bootloader
|
505
libs/ota-common/src/cc3200/cc3200_updater.c
Normal file
505
libs/ota-common/src/cc3200/cc3200_updater.c
Normal file
@ -0,0 +1,505 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "cc3200_updater.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "common/cs_dbg.h"
|
||||
#include "common/platform.h"
|
||||
|
||||
#include "frozen.h"
|
||||
#include "mongoose.h"
|
||||
|
||||
#include "mgos_hal.h"
|
||||
#include "mgos_sys_config.h"
|
||||
#include "mgos_updater_hal.h"
|
||||
#include "mgos_updater_util.h"
|
||||
#include "mgos_utils.h"
|
||||
|
||||
#include "cc32xx_hash.h"
|
||||
|
||||
#include "cc3200_vfs_dev_slfs_container.h"
|
||||
#include "cc3200_vfs_dev_slfs_container_meta.h"
|
||||
#include "fw/platforms/cc3200/boot/lib/boot.h"
|
||||
|
||||
static int s_boot_cfg_idx;
|
||||
static struct boot_cfg s_boot_cfg;
|
||||
|
||||
struct mgos_upd_hal_ctx {
|
||||
struct json_token *parts;
|
||||
int cur_boot_cfg_idx;
|
||||
int new_boot_cfg_idx;
|
||||
char app_image_file[MAX_APP_IMAGE_FILE_LEN];
|
||||
uint32_t app_load_addr;
|
||||
char fs_container_file[MAX_FS_CONTAINER_FNAME_LEN + 3];
|
||||
uint32_t fs_size, fs_block_size, fs_page_size, fs_erase_size;
|
||||
struct json_token cur_part;
|
||||
const _u8 *cur_fn;
|
||||
_i32 cur_fh;
|
||||
const char *status_msg;
|
||||
};
|
||||
|
||||
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void) {
|
||||
struct mgos_upd_hal_ctx *ctx =
|
||||
(struct mgos_upd_hal_ctx *) calloc(1, sizeof(*ctx));
|
||||
ctx->cur_fh = -1;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx) {
|
||||
return ctx->status_msg;
|
||||
}
|
||||
|
||||
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts) {
|
||||
ctx->parts = parts;
|
||||
/* We want to make sure device uses boto loader. */
|
||||
ctx->cur_boot_cfg_idx = get_active_boot_cfg_idx();
|
||||
if (ctx->cur_boot_cfg_idx < 0) {
|
||||
ctx->status_msg = "Could not read current boot cfg";
|
||||
return -1;
|
||||
}
|
||||
ctx->new_boot_cfg_idx = get_inactive_boot_cfg_idx();
|
||||
return 1;
|
||||
}
|
||||
|
||||
typedef int (*read_file_cb_t)(_u8 *data, int len, void *arg);
|
||||
static int read_file(const char *fn, int offset, int len, read_file_cb_t cb,
|
||||
void *arg) {
|
||||
_i32 fh;
|
||||
int r = sl_FsOpen((const _u8 *) fn, FS_MODE_OPEN_READ, NULL, &fh);
|
||||
if (r < 0) return r;
|
||||
while (len > 0) {
|
||||
_u8 buf[512];
|
||||
int to_read = MIN(len, sizeof(buf));
|
||||
r = sl_FsRead(fh, offset, buf, to_read);
|
||||
if (r != to_read) break;
|
||||
if (cb(buf, to_read, arg) != to_read) break;
|
||||
offset += to_read;
|
||||
len -= to_read;
|
||||
}
|
||||
sl_FsClose(fh, NULL, NULL, 0);
|
||||
return (len == 0 ? 0 : -1);
|
||||
}
|
||||
|
||||
static int sha1_update_cb(_u8 *data, int len, void *arg) {
|
||||
cc32xx_hash_update((struct cc32xx_hash_ctx *) arg, data, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
void bin2hex(const uint8_t *src, int src_len, char *dst);
|
||||
|
||||
static bool verify_checksum(const char *fn, int fs,
|
||||
const struct json_token *expected) {
|
||||
_u8 digest[20];
|
||||
char digest_str[41];
|
||||
|
||||
if (expected->len != 40) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct cc32xx_hash_ctx ctx;
|
||||
cc32xx_hash_init(&ctx);
|
||||
cc32xx_hash_start(&ctx, CC32XX_HASH_ALGO_SHA1);
|
||||
if (read_file(fn, 0, fs, sha1_update_cb, &ctx) < 0) {
|
||||
return false;
|
||||
}
|
||||
cc32xx_hash_finish(&ctx, digest);
|
||||
bin2hex(digest, 20, digest_str);
|
||||
|
||||
LOG(LL_INFO,
|
||||
("%s: have %.*s, want %.*s", fn, 40, digest_str, 40, expected->ptr));
|
||||
|
||||
return (strncasecmp(expected->ptr, digest_str, 40) == 0);
|
||||
}
|
||||
|
||||
/* Create file name by appending ".$idx" to prefix. */
|
||||
static void create_fname(struct mg_str pfx, int idx, char *fn, int len) {
|
||||
int l = MIN(len - 3, pfx.len);
|
||||
memcpy(fn, pfx.p, l);
|
||||
fn[l++] = '.';
|
||||
fn[l++] = ('0' + idx);
|
||||
fn[l] = '\0';
|
||||
}
|
||||
|
||||
static int prepare_to_write(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi,
|
||||
const char *fname, uint32_t falloc,
|
||||
struct json_token *part) {
|
||||
struct json_token expected_sha1 = JSON_INVALID_TOKEN;
|
||||
json_scanf(part->ptr, part->len, "{cs_sha1: %T}", &expected_sha1);
|
||||
if (verify_checksum(fname, fi->size, &expected_sha1)) {
|
||||
LOG(LL_INFO,
|
||||
("Digest matched for %s %u (%.*s)", fname, (unsigned int) fi->size,
|
||||
(int) expected_sha1.len, expected_sha1.ptr));
|
||||
return 0;
|
||||
}
|
||||
LOG(LL_INFO, ("Storing %s %u -> %s %u (%.*s)", fi->name,
|
||||
(unsigned int) fi->size, fname, (unsigned int) falloc,
|
||||
(int) expected_sha1.len, expected_sha1.ptr));
|
||||
ctx->cur_fn = (const _u8 *) fname;
|
||||
sl_FsDel(ctx->cur_fn, 0);
|
||||
_i32 r = sl_FsOpen(ctx->cur_fn, FS_MODE_OPEN_CREATE(falloc, 0), NULL,
|
||||
&ctx->cur_fh);
|
||||
if (r < 0) {
|
||||
ctx->status_msg = "Failed to create file";
|
||||
return r;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct find_part_info {
|
||||
const char *src;
|
||||
struct mg_str *key;
|
||||
struct json_token *value;
|
||||
char buf[50];
|
||||
};
|
||||
|
||||
static void find_part(void *data, const char *name, size_t name_len,
|
||||
const char *path, const struct json_token *tok) {
|
||||
struct find_part_info *info = (struct find_part_info *) data;
|
||||
size_t path_len = strlen(path), src_len = strlen(info->src);
|
||||
|
||||
(void) name;
|
||||
(void) name_len;
|
||||
|
||||
if (tok->ptr == NULL) {
|
||||
/*
|
||||
* We're not interested here in the events for which we have no value;
|
||||
* namely, JSON_TYPE_OBJECT_START and JSON_TYPE_ARRAY_START
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
/* For matched 'src' attribute, remember parent object path. */
|
||||
if (src_len == tok->len && strncmp(info->src, tok->ptr, tok->len) == 0) {
|
||||
const char *p = path + path_len;
|
||||
while (--p > path + 1) {
|
||||
if (*p == '.') break;
|
||||
}
|
||||
|
||||
info->key->len = snprintf(info->buf, sizeof(info->buf), "%.*s",
|
||||
(p - path) - 1, path + 1);
|
||||
info->key->p = info->buf;
|
||||
}
|
||||
|
||||
/*
|
||||
* And store parent's object token. These conditionals are triggered
|
||||
* in separate callback invocations.
|
||||
*/
|
||||
if (info->value->len == 0 && info->key->len > 0 &&
|
||||
info->key->len + 1 == path_len &&
|
||||
strncmp(path + 1, info->key->p, info->key->len) == 0) {
|
||||
*info->value = *tok;
|
||||
}
|
||||
}
|
||||
|
||||
static int tcmp(const struct json_token *tok, const char *str) {
|
||||
struct mg_str s = {.p = tok->ptr, .len = tok->len};
|
||||
return mg_vcmp(&s, str);
|
||||
}
|
||||
|
||||
enum mgos_upd_file_action mgos_upd_file_begin(
|
||||
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi) {
|
||||
struct mg_str part_name = MG_MK_STR("");
|
||||
enum mgos_upd_file_action ret = MGOS_UPDATER_SKIP_FILE;
|
||||
struct find_part_info find_part_info = {fi->name, &part_name, &ctx->cur_part};
|
||||
ctx->cur_part.len = part_name.len = 0;
|
||||
json_walk(ctx->parts->ptr, ctx->parts->len, find_part, &find_part_info);
|
||||
if (ctx->cur_part.len == 0) return ret;
|
||||
/* Drop any indexes from part name, we'll add our own. */
|
||||
while (1) {
|
||||
char c = part_name.p[part_name.len - 1];
|
||||
if (c != '.' && !(c >= '0' && c <= '9')) break;
|
||||
part_name.len--;
|
||||
}
|
||||
struct json_token type = JSON_INVALID_TOKEN;
|
||||
const char *fname = NULL;
|
||||
uint32_t falloc = 0;
|
||||
json_scanf(ctx->cur_part.ptr, ctx->cur_part.len,
|
||||
"{load_addr:%u, falloc:%u, type: %T}", &ctx->app_load_addr,
|
||||
&falloc, &type);
|
||||
|
||||
if (falloc == 0) falloc = fi->size;
|
||||
if (tcmp(&type, "app") == 0) {
|
||||
struct boot_cfg cur_cfg;
|
||||
int r = read_boot_cfg(ctx->cur_boot_cfg_idx, &cur_cfg);
|
||||
if (r < 0) {
|
||||
ctx->status_msg = "Could not read current boot cfg";
|
||||
return MGOS_UPDATER_ABORT;
|
||||
}
|
||||
#if CC3200_SAFE_CODE_UPDATE
|
||||
/*
|
||||
* When safe code update is enabled, we write code to a new file.
|
||||
* Otherwise we write to the same slot we're using currently, which is
|
||||
* unsafe, makes reverting code update not possible, but saves space.
|
||||
*/
|
||||
create_fname(
|
||||
mg_mk_str_n(cur_cfg.app_image_file, strlen(cur_cfg.app_image_file) - 2),
|
||||
ctx->new_boot_cfg_idx, ctx->app_image_file,
|
||||
sizeof(ctx->app_image_file));
|
||||
#else
|
||||
{
|
||||
strncpy(ctx->app_image_file, cur_cfg.app_image_file,
|
||||
sizeof(ctx->app_image_file));
|
||||
}
|
||||
#endif
|
||||
if (ctx->app_load_addr >= 0x20000000) {
|
||||
fname = ctx->app_image_file;
|
||||
} else {
|
||||
ctx->status_msg = "Bad/missing app load_addr";
|
||||
ret = MGOS_UPDATER_ABORT;
|
||||
}
|
||||
} else if (tcmp(&type, "fs") == 0) {
|
||||
json_scanf(
|
||||
ctx->cur_part.ptr, ctx->cur_part.len,
|
||||
"{fs_size: %u, fs_block_size: %u, fs_page_size: %u, fs_erase_size: %u}",
|
||||
&ctx->fs_size, &ctx->fs_block_size, &ctx->fs_page_size,
|
||||
&ctx->fs_erase_size);
|
||||
if (ctx->fs_size > 0 && ctx->fs_block_size > 0 && ctx->fs_page_size > 0 &&
|
||||
ctx->fs_erase_size > 0) {
|
||||
char fs_container_prefix[MAX_FS_CONTAINER_PREFIX_LEN];
|
||||
create_fname(part_name, ctx->new_boot_cfg_idx, fs_container_prefix,
|
||||
sizeof(fs_container_prefix));
|
||||
/* Delete container 1 (if any) so that 0 is the only one. */
|
||||
cc3200_vfs_dev_slfs_container_delete_container(fs_container_prefix, 1);
|
||||
cc3200_vfs_dev_slfs_container_fname(fs_container_prefix, 0,
|
||||
(_u8 *) ctx->fs_container_file);
|
||||
fname = ctx->fs_container_file;
|
||||
if (fi->size > ctx->fs_size) {
|
||||
/* Assume meta has already been added. */
|
||||
falloc = fi->size;
|
||||
} else {
|
||||
falloc = FS_CONTAINER_SIZE(fi->size);
|
||||
}
|
||||
} else {
|
||||
ctx->status_msg = "Missing FS parameters";
|
||||
ret = MGOS_UPDATER_ABORT;
|
||||
}
|
||||
}
|
||||
if (fname != NULL) {
|
||||
int r = prepare_to_write(ctx, fi, fname, falloc, &ctx->cur_part);
|
||||
if (r < 0) {
|
||||
LOG(LL_ERROR, ("err = %d", r));
|
||||
ret = MGOS_UPDATER_ABORT;
|
||||
} else {
|
||||
ret = (r > 0 ? MGOS_UPDATER_PROCESS_FILE : MGOS_UPDATER_SKIP_FILE);
|
||||
}
|
||||
}
|
||||
if (ret == MGOS_UPDATER_SKIP_FILE) {
|
||||
DBG(("Skipping %s %.*s", fi->name, (int) part_name.len, part_name.p));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi,
|
||||
struct mg_str data) {
|
||||
_i32 r = sl_FsWrite(ctx->cur_fh, fi->processed, (_u8 *) data.p, data.len);
|
||||
if (r != data.len) {
|
||||
ctx->status_msg = "Write failed";
|
||||
r = -1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi, struct mg_str tail) {
|
||||
int r = tail.len;
|
||||
assert(tail.len == 0);
|
||||
if (ctx->cur_fn == (_u8 *) ctx->fs_container_file) {
|
||||
if (!cc3200_vfs_dev_slfs_container_write_meta(
|
||||
ctx->cur_fh, FS_INITIAL_SEQ, ctx->fs_size, ctx->fs_block_size,
|
||||
ctx->fs_page_size, ctx->fs_erase_size)) {
|
||||
ctx->status_msg = "Failed to write fs meta";
|
||||
r = -1;
|
||||
}
|
||||
}
|
||||
if (sl_FsClose(ctx->cur_fh, NULL, NULL, 0) != 0) {
|
||||
ctx->status_msg = "Close failed";
|
||||
r = -1;
|
||||
} else {
|
||||
struct json_token sha1 = JSON_INVALID_TOKEN;
|
||||
json_scanf(ctx->cur_part.ptr, ctx->cur_part.len, "{cs_sha1: %T}", &sha1);
|
||||
if (!verify_checksum((const char *) ctx->cur_fn, fi->size, &sha1)) {
|
||||
ctx->status_msg = "Checksum mismatch";
|
||||
r = -1;
|
||||
}
|
||||
}
|
||||
ctx->cur_fh = -1;
|
||||
ctx->cur_fn = NULL;
|
||||
return r;
|
||||
}
|
||||
|
||||
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx) {
|
||||
struct boot_cfg cur_cfg, new_cfg;
|
||||
int r = read_boot_cfg(ctx->cur_boot_cfg_idx, &cur_cfg);
|
||||
if (r < 0) {
|
||||
ctx->status_msg = "Could not read current boot cfg";
|
||||
return r;
|
||||
}
|
||||
LOG(LL_INFO,
|
||||
("Boot cfg %d: 0x%llx, 0x%x, %s @ 0x%08x, %s", ctx->cur_boot_cfg_idx,
|
||||
cur_cfg.seq, (unsigned int) cur_cfg.flags, cur_cfg.app_image_file,
|
||||
(unsigned int) cur_cfg.app_load_addr, cur_cfg.fs_container_prefix));
|
||||
memset(&new_cfg, 0, sizeof(new_cfg));
|
||||
new_cfg.seq = cur_cfg.seq - 1;
|
||||
new_cfg.flags |= BOOT_F_FIRST_BOOT;
|
||||
if (ctx->app_image_file[0] != '\0') {
|
||||
strncpy(new_cfg.app_image_file, ctx->app_image_file,
|
||||
sizeof(new_cfg.app_image_file));
|
||||
new_cfg.app_load_addr = ctx->app_load_addr;
|
||||
} else {
|
||||
strcpy(new_cfg.app_image_file, cur_cfg.app_image_file);
|
||||
new_cfg.app_load_addr = cur_cfg.app_load_addr;
|
||||
}
|
||||
if (ctx->fs_container_file[0] != '\0') {
|
||||
int n = strlen(ctx->fs_container_file);
|
||||
do {
|
||||
n--;
|
||||
} while (ctx->fs_container_file[n] != '.');
|
||||
strncpy(new_cfg.fs_container_prefix, ctx->fs_container_file, n);
|
||||
new_cfg.flags |= BOOT_F_MERGE_SPIFFS;
|
||||
} else {
|
||||
strcpy(new_cfg.fs_container_prefix, cur_cfg.fs_container_prefix);
|
||||
}
|
||||
LOG(LL_INFO,
|
||||
("Boot cfg %d: 0x%llx, 0x%x, %s @ 0x%08x, %s", ctx->new_boot_cfg_idx,
|
||||
new_cfg.seq, (unsigned int) new_cfg.flags, new_cfg.app_image_file,
|
||||
(unsigned int) new_cfg.app_load_addr, new_cfg.fs_container_prefix));
|
||||
r = write_boot_cfg(&new_cfg, ctx->new_boot_cfg_idx);
|
||||
if (r < 0) {
|
||||
ctx->status_msg = "Could not write new boot cfg";
|
||||
return r;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx) {
|
||||
if (ctx == NULL) return;
|
||||
if (ctx->cur_fh >= 0) sl_FsClose(ctx->cur_fh, NULL, NULL, 0);
|
||||
if (ctx->cur_fn != NULL) sl_FsDel(ctx->cur_fn, 0);
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
int mgos_upd_create_snapshot() {
|
||||
/* TODO(rojer): Implement. */
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool cc3200_upd_init(void) {
|
||||
s_boot_cfg_idx = get_active_boot_cfg_idx();
|
||||
if (s_boot_cfg_idx < 0 || read_boot_cfg(s_boot_cfg_idx, &s_boot_cfg) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG(LL_INFO,
|
||||
("Boot cfg %d: 0x%llx, 0x%u, %s @ 0x%08x, %s", s_boot_cfg_idx,
|
||||
s_boot_cfg.seq, (unsigned int) s_boot_cfg.flags,
|
||||
s_boot_cfg.app_image_file, (unsigned int) s_boot_cfg.app_load_addr,
|
||||
s_boot_cfg.fs_container_prefix));
|
||||
|
||||
if (mgos_upd_is_first_boot()) {
|
||||
/* Tombstone the current config. If anything goes wrong between now and
|
||||
* commit, next boot will use the old one. */
|
||||
uint64_t saved_seq = s_boot_cfg.seq;
|
||||
s_boot_cfg.seq = BOOT_CFG_TOMBSTONE_SEQ;
|
||||
write_boot_cfg(&s_boot_cfg, s_boot_cfg_idx);
|
||||
s_boot_cfg.seq = saved_seq;
|
||||
}
|
||||
|
||||
/*
|
||||
* We aim to maintain at most 3 FS containers at all times.
|
||||
* Delete inactive FS container in the inactive boot configuration.
|
||||
*/
|
||||
struct boot_cfg cfg;
|
||||
int inactive_idx = (s_boot_cfg_idx == 0 ? 1 : 0);
|
||||
if (read_boot_cfg(inactive_idx, &cfg) >= 0) {
|
||||
cc3200_vfs_dev_slfs_container_delete_inactive_container(
|
||||
cfg.fs_container_prefix);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *cc3200_upd_get_fs_container_prefix(void) {
|
||||
return s_boot_cfg.fs_container_prefix;
|
||||
}
|
||||
|
||||
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs) {
|
||||
struct boot_cfg *cfg = &s_boot_cfg;
|
||||
memset(bs, 0, sizeof(*bs));
|
||||
const char *p = strrchr(cfg->app_image_file, '.');
|
||||
bs->active_slot = (*(p + 1) == '0' ? 0 : 1);
|
||||
bs->revert_slot = (bs->active_slot == 0 ? 1 : 0);
|
||||
bs->is_committed = !(cfg->flags & BOOT_F_FIRST_BOOT);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs) {
|
||||
/* TODO(rojer): Implement. */
|
||||
(void) bs;
|
||||
return false;
|
||||
}
|
||||
|
||||
void mgos_upd_boot_revert(void) {
|
||||
int boot_cfg_idx = s_boot_cfg_idx;
|
||||
struct boot_cfg *cfg = &s_boot_cfg;
|
||||
if (!cfg->flags & BOOT_F_FIRST_BOOT) return;
|
||||
LOG(LL_ERROR, ("Config %d is bad, reverting", boot_cfg_idx));
|
||||
/* Tombstone the current config. */
|
||||
cfg->seq = BOOT_CFG_TOMBSTONE_SEQ;
|
||||
write_boot_cfg(cfg, boot_cfg_idx);
|
||||
}
|
||||
|
||||
bool mgos_upd_is_first_boot(void) {
|
||||
return (s_boot_cfg.flags & BOOT_F_FIRST_BOOT) != 0;
|
||||
}
|
||||
|
||||
void mgos_upd_boot_commit(void) {
|
||||
int boot_cfg_idx = s_boot_cfg_idx;
|
||||
struct boot_cfg *cfg = &s_boot_cfg;
|
||||
if (!cfg->flags & BOOT_F_FIRST_BOOT) return;
|
||||
cfg->flags &= ~(BOOT_F_FIRST_BOOT);
|
||||
int r = write_boot_cfg(cfg, boot_cfg_idx);
|
||||
if (r < 0) mgos_upd_boot_revert();
|
||||
LOG(LL_INFO, ("Committed cfg %d, seq 0x%llx", boot_cfg_idx, cfg->seq));
|
||||
}
|
||||
|
||||
int mgos_upd_apply_update(void) {
|
||||
int boot_cfg_idx = s_boot_cfg_idx;
|
||||
struct boot_cfg *cfg = &s_boot_cfg;
|
||||
if (cfg->flags & BOOT_F_MERGE_SPIFFS) {
|
||||
int old_boot_cfg_idx = (boot_cfg_idx == 0 ? 1 : 0);
|
||||
struct boot_cfg old_boot_cfg;
|
||||
int r = read_boot_cfg(old_boot_cfg_idx, &old_boot_cfg);
|
||||
if (r < 0) return r;
|
||||
if (!cc3200_fs_container_mount("/old", old_boot_cfg.fs_container_prefix)) {
|
||||
return -123;
|
||||
}
|
||||
if (mgos_upd_merge_fs("/old", "/")) {
|
||||
r = 0;
|
||||
cfg->flags &= ~(BOOT_F_MERGE_SPIFFS);
|
||||
} else {
|
||||
r = -124;
|
||||
}
|
||||
mgos_vfs_umount("/old");
|
||||
}
|
||||
return 0;
|
||||
}
|
645
libs/ota-common/src/esp32/esp32_updater.c
Normal file
645
libs/ota-common/src/esp32/esp32_updater.c
Normal file
@ -0,0 +1,645 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "esp32_updater.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "esp_flash_encrypt.h"
|
||||
#include "esp_flash_partitions.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_partition.h"
|
||||
#include "esp_spi_flash.h"
|
||||
#include "nvs.h"
|
||||
|
||||
#include "mbedtls/sha1.h"
|
||||
|
||||
#include "common/cs_dbg.h"
|
||||
#include "common/mg_str.h"
|
||||
#include "common/platform.h"
|
||||
|
||||
#include "frozen.h"
|
||||
#include "mongoose.h"
|
||||
|
||||
#include "mgos_hal.h"
|
||||
#include "mgos_sys_config.h"
|
||||
#include "mgos_updater_hal.h"
|
||||
#include "mgos_updater_util.h"
|
||||
#include "mgos_utils.h"
|
||||
#include "mgos_vfs.h"
|
||||
#include "mgos_vfs_internal.h"
|
||||
|
||||
#include "esp32_fs.h"
|
||||
|
||||
/*
|
||||
* Since boot loader does not provide a way to store flags, we use NVS.
|
||||
* We store a 32-bit uint with old slot, new slot and first boot flag.
|
||||
*/
|
||||
#define MGOS_UPDATE_NVS_NAMESPACE "mgos"
|
||||
#define MGOS_UPDATE_NVS_KEY_FLAGS "update"
|
||||
#define MGOS_UPDATE_MERGE_FS 0x200
|
||||
#define MGOS_UPDATE_FIRST_BOOT 0x100
|
||||
#define MGOS_UPDATE_NVS_VAL(old_slot, new_slot, first_boot, merge_fs) \
|
||||
(((merge_fs) ? MGOS_UPDATE_MERGE_FS : 0) | \
|
||||
((first_boot) ? MGOS_UPDATE_FIRST_BOOT : 0) | (((new_slot) &0xf) << 4) | \
|
||||
((old_slot) &0xf))
|
||||
#define MGOS_UPDATE_OLD_SLOT(v) ((v) &0x0f)
|
||||
#define MGOS_UPDATE_NEW_SLOT(v) (((v) >> 4) & 0x0f)
|
||||
/* In this key we store type and opts of the old FS. */
|
||||
#define MGOS_UPDATE_NVS_KEY_OLD_FS "update_old_fs"
|
||||
|
||||
#define CS_LEN 20 /* SHA1 */
|
||||
#define CS_HEX_LEN (CS_LEN * 2)
|
||||
#define CS_HEX_BUF_SIZE (CS_HEX_LEN + 1)
|
||||
|
||||
#define FLASH_PARAMS_ADDR 0x1000
|
||||
#define FLASH_PARAMS_LEN 4
|
||||
#define LABEL_OFFSET 8
|
||||
|
||||
#if (FLASH_PARAMS_ADDR % MGOS_UPDATER_DATA_CHUNK_SIZE != 0) || \
|
||||
MGOS_UPDATER_DATA_CHUNK_SIZE < FLASH_PARAMS_LEN
|
||||
#error "Don't do that"
|
||||
#endif
|
||||
|
||||
uint32_t g_boot_status = 0;
|
||||
|
||||
struct mgos_upd_hal_ctx {
|
||||
const char *status_msg;
|
||||
|
||||
struct json_token boot_file_name, boot_cs_sha1;
|
||||
uint32_t boot_addr;
|
||||
bool update_bootloader;
|
||||
/*
|
||||
* Note: Latter 8 bytes of the label are used to store flash params:
|
||||
* 4 bytes are the current device's flash params,
|
||||
* 4 bytes are the ones in the image. We substitute device's params
|
||||
* during writing but use bytes fro flash image when computing the checksum.
|
||||
*/
|
||||
esp_partition_t boot_partition;
|
||||
|
||||
const esp_partition_t *cur_app_partition;
|
||||
|
||||
struct json_token app_file_name, app_cs_sha1;
|
||||
const esp_partition_t *app_partition;
|
||||
esp_ota_handle_t app_ota_handle;
|
||||
|
||||
struct json_token fs_file_name, fs_cs_sha1;
|
||||
const esp_partition_t *fs_partition;
|
||||
|
||||
size_t write_offset;
|
||||
struct json_token *cs_sha1;
|
||||
const esp_partition_t *write_partition;
|
||||
};
|
||||
|
||||
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void) {
|
||||
struct mgos_upd_hal_ctx *ctx =
|
||||
(struct mgos_upd_hal_ctx *) calloc(1, sizeof(*ctx));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx) {
|
||||
return ctx->status_msg;
|
||||
}
|
||||
|
||||
static int find_inactive_slot(const esp_partition_t **cur_app_partition,
|
||||
const esp_partition_t **cur_fs_partition,
|
||||
const esp_partition_t **new_app_partition,
|
||||
const esp_partition_t **new_fs_partition,
|
||||
const char **status_msg) {
|
||||
*cur_app_partition = esp_ota_get_boot_partition();
|
||||
if (*cur_app_partition == NULL) {
|
||||
*status_msg = "Not in OTA boot mode";
|
||||
return -1;
|
||||
}
|
||||
int slot = SUBTYPE_TO_SLOT((*cur_app_partition)->subtype);
|
||||
*cur_fs_partition = esp32_find_fs_for_app_slot(slot);
|
||||
/* Find next OTA slot */
|
||||
do {
|
||||
slot = (slot + 1) & (NUM_OTA_SLOTS - 1);
|
||||
esp_partition_subtype_t subtype = ESP_PARTITION_SUBTYPE_OTA(slot);
|
||||
if (subtype == (*cur_app_partition)->subtype) break;
|
||||
*new_app_partition =
|
||||
esp_partition_find_first(ESP_PARTITION_TYPE_APP, subtype, NULL);
|
||||
} while (*new_app_partition == NULL);
|
||||
if (*new_app_partition == NULL) {
|
||||
*status_msg = "No app slots";
|
||||
return -2;
|
||||
}
|
||||
*new_fs_partition = esp32_find_fs_for_app_slot(slot);
|
||||
if (*new_fs_partition == NULL) {
|
||||
*status_msg = "No fs slots";
|
||||
return -3;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts) {
|
||||
const esp_partition_t *cur_fs_partition;
|
||||
int slot = find_inactive_slot(&ctx->cur_app_partition, &cur_fs_partition,
|
||||
&ctx->app_partition, &ctx->fs_partition,
|
||||
&ctx->status_msg);
|
||||
if (slot < 0) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
uint32_t boot_addr = 0;
|
||||
int update_bootloader = false;
|
||||
json_scanf(parts->ptr, parts->len,
|
||||
"{boot: {src: %T, addr: %u, cs_sha1: %T, update: %B}, "
|
||||
"app: {src: %T, cs_sha1: %T}, "
|
||||
"fs: {src: %T, cs_sha1: %T}}",
|
||||
&ctx->boot_file_name, &boot_addr, &ctx->boot_cs_sha1,
|
||||
&update_bootloader, &ctx->app_file_name, &ctx->app_cs_sha1,
|
||||
&ctx->fs_file_name, &ctx->fs_cs_sha1);
|
||||
|
||||
if (ctx->app_file_name.len == 0 || ctx->app_cs_sha1.len == 0 ||
|
||||
ctx->fs_file_name.len == 0 || ctx->fs_cs_sha1.len == 0 ||
|
||||
(ctx->update_bootloader &&
|
||||
(ctx->boot_file_name.len == 0 || ctx->boot_cs_sha1.len == 0))) {
|
||||
ctx->status_msg = "Incomplete update package";
|
||||
return -3;
|
||||
}
|
||||
|
||||
ctx->boot_addr = boot_addr;
|
||||
ctx->update_bootloader = update_bootloader;
|
||||
if (ctx->update_bootloader) {
|
||||
/* Create a bootloader "partition", so esp_partition_* API can be used. */
|
||||
ctx->boot_partition.address = 0x1000;
|
||||
ctx->boot_partition.size = (CONFIG_PARTITION_TABLE_OFFSET - 0x1000);
|
||||
/* If encryption is enabled, boot loader must be encrypted. */
|
||||
ctx->boot_partition.encrypted = esp_flash_encryption_enabled();
|
||||
if (boot_addr > ctx->boot_partition.size) {
|
||||
ctx->status_msg = "Invalid boot write addr";
|
||||
return -4;
|
||||
}
|
||||
strcpy(ctx->boot_partition.label, "boot");
|
||||
if (esp_partition_read(&ctx->boot_partition, FLASH_PARAMS_ADDR,
|
||||
ctx->boot_partition.label + LABEL_OFFSET,
|
||||
FLASH_PARAMS_LEN) != 0) {
|
||||
ctx->status_msg = "Failed to read flash params";
|
||||
return -5;
|
||||
}
|
||||
LOG(LL_INFO, ("Boot: %.*s -> 0x%x, current flash params: 0x%02x%02x",
|
||||
(int) ctx->boot_file_name.len, ctx->boot_file_name.ptr,
|
||||
ctx->boot_addr, ctx->boot_partition.label[LABEL_OFFSET + 2],
|
||||
ctx->boot_partition.label[LABEL_OFFSET + 3]));
|
||||
}
|
||||
|
||||
LOG(LL_INFO, ("App: %.*s -> %s, FS: %.*s -> %s", (int) ctx->app_file_name.len,
|
||||
ctx->app_file_name.ptr, ctx->app_partition->label,
|
||||
(int) ctx->fs_file_name.len, ctx->fs_file_name.ptr,
|
||||
ctx->fs_partition->label));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void bin2hex(const uint8_t *src, int src_len, char *dst);
|
||||
|
||||
static bool compute_checksum(const esp_partition_t *p, size_t len,
|
||||
char *cs_hex) {
|
||||
bool ret = false;
|
||||
size_t offset = 0;
|
||||
mbedtls_sha1_context sha1_ctx;
|
||||
unsigned char digest[CS_LEN];
|
||||
mbedtls_sha1_init(&sha1_ctx);
|
||||
mbedtls_sha1_starts(&sha1_ctx);
|
||||
while (offset < len) {
|
||||
uint8_t tmp[MGOS_UPDATER_DATA_CHUNK_SIZE];
|
||||
size_t block_len = len - offset;
|
||||
if (block_len > sizeof(tmp)) block_len = sizeof(tmp);
|
||||
esp_err_t err = esp_partition_read(p, offset, tmp, block_len);
|
||||
if (err != ESP_OK) {
|
||||
LOG(LL_ERROR,
|
||||
("Error reading %s at offset %u: %d", p->label, offset, err));
|
||||
goto cleanup;
|
||||
}
|
||||
/* Special handling of flash params. It's a bit simpler than when writing
|
||||
* because we know that FLASH_PARAMS_ADDR divides WRITE_CHUNK_SIZE and
|
||||
* WRITE_CHUNK_SIZE is > FLASH_PARAMS_LEN. */
|
||||
if (p->address + offset == FLASH_PARAMS_ADDR) {
|
||||
LOG(LL_DEBUG,
|
||||
("Swapping %d @ 0x%x", FLASH_PARAMS_LEN, p->address + offset));
|
||||
memcpy(tmp, p->label + LABEL_OFFSET + FLASH_PARAMS_LEN, FLASH_PARAMS_LEN);
|
||||
}
|
||||
mbedtls_sha1_update(&sha1_ctx, tmp, block_len);
|
||||
offset += block_len;
|
||||
}
|
||||
mbedtls_sha1_finish(&sha1_ctx, digest);
|
||||
bin2hex(digest, CS_LEN, cs_hex);
|
||||
cs_hex[CS_HEX_LEN] = '\0';
|
||||
ret = true;
|
||||
|
||||
cleanup:
|
||||
mbedtls_sha1_free(&sha1_ctx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool verify_checksum(const esp_partition_t *p, size_t len,
|
||||
const char *exp_sha1, bool critical) {
|
||||
char cs_hex[CS_HEX_BUF_SIZE];
|
||||
bool ret = compute_checksum(p, len, cs_hex) &&
|
||||
(strncmp(cs_hex, exp_sha1, CS_HEX_LEN) == 0);
|
||||
LOG((ret || !critical ? LL_DEBUG : LL_ERROR),
|
||||
("%s: %u @ 0x%x, cs_sha1 %s, expected %.*s", p->label, len, p->address,
|
||||
cs_hex, CS_HEX_LEN, exp_sha1));
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum mgos_upd_file_action mgos_upd_file_begin(
|
||||
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi) {
|
||||
esp_err_t err;
|
||||
ctx->write_offset = 0;
|
||||
ctx->write_partition = NULL;
|
||||
if (strncmp(fi->name, ctx->app_file_name.ptr, ctx->app_file_name.len) == 0) {
|
||||
if (verify_checksum(ctx->app_partition, fi->size, ctx->app_cs_sha1.ptr,
|
||||
false /* critical */)) {
|
||||
LOG(LL_INFO, ("Skip writing app (digest matches)"));
|
||||
return MGOS_UPDATER_SKIP_FILE;
|
||||
}
|
||||
LOG(LL_INFO, ("Writing app image @ 0x%x", ctx->app_partition->address));
|
||||
if (esp_ota_begin(ctx->app_partition, 0, &ctx->app_ota_handle) != ESP_OK) {
|
||||
ctx->status_msg = "Failed to start app write";
|
||||
return MGOS_UPDATER_ABORT;
|
||||
}
|
||||
ctx->cs_sha1 = &ctx->app_cs_sha1;
|
||||
ctx->write_partition = ctx->app_partition;
|
||||
return MGOS_UPDATER_PROCESS_FILE;
|
||||
}
|
||||
if (ctx->update_bootloader &&
|
||||
strncmp(fi->name, ctx->boot_file_name.ptr, ctx->boot_file_name.len) ==
|
||||
0) {
|
||||
LOG(LL_INFO, ("Writing boot loader @ 0x%x", ctx->boot_addr));
|
||||
ctx->write_partition = &ctx->boot_partition;
|
||||
ctx->write_offset = ctx->boot_addr;
|
||||
ctx->cs_sha1 = &ctx->boot_cs_sha1;
|
||||
} else if (strncmp(fi->name, ctx->fs_file_name.ptr, ctx->fs_file_name.len) ==
|
||||
0) {
|
||||
if (verify_checksum(ctx->fs_partition, fi->size, ctx->app_cs_sha1.ptr,
|
||||
false /* critical */)) {
|
||||
LOG(LL_INFO, ("Skip writing FS (digest matches)"));
|
||||
return MGOS_UPDATER_SKIP_FILE;
|
||||
}
|
||||
LOG(LL_INFO, ("Writing FS image @ 0x%x", ctx->fs_partition->address));
|
||||
ctx->write_partition = ctx->fs_partition;
|
||||
ctx->write_offset = 0;
|
||||
ctx->cs_sha1 = &ctx->fs_cs_sha1;
|
||||
}
|
||||
if (ctx->write_partition != NULL) {
|
||||
err = esp_partition_erase_range(
|
||||
ctx->write_partition, ctx->write_offset,
|
||||
ctx->write_partition->size - ctx->write_offset);
|
||||
if (err != ESP_OK) {
|
||||
ctx->status_msg = "Failed to start write";
|
||||
LOG(LL_ERROR, ("%s: %d", ctx->status_msg, err));
|
||||
return MGOS_UPDATER_ABORT;
|
||||
}
|
||||
return MGOS_UPDATER_PROCESS_FILE;
|
||||
}
|
||||
LOG(LL_DEBUG, ("Not interesting: %s", fi->name));
|
||||
return MGOS_UPDATER_SKIP_FILE;
|
||||
}
|
||||
|
||||
static void swap_flash_params(struct mgos_upd_hal_ctx *ctx, struct mg_str data,
|
||||
bool save) {
|
||||
if (ctx->write_partition != &ctx->boot_partition) return;
|
||||
int off_s = MAX(0, FLASH_PARAMS_ADDR - (int) ctx->write_offset);
|
||||
int off_d = MAX(0, (int) ctx->write_offset - FLASH_PARAMS_ADDR);
|
||||
if (off_s >= data.len || off_d >= FLASH_PARAMS_LEN) return;
|
||||
int len = MIN(FLASH_PARAMS_LEN - off_d, data.len - off_s);
|
||||
if (save) {
|
||||
LOG(LL_DEBUG,
|
||||
("Swapping %d @ 0x%x %d", len, (ctx->write_offset + off_s), off_d));
|
||||
memcpy(ctx->boot_partition.label + LABEL_OFFSET + FLASH_PARAMS_LEN + off_d,
|
||||
data.p + off_s, len);
|
||||
memcpy((char *) data.p + off_s,
|
||||
ctx->boot_partition.label + LABEL_OFFSET + off_d, len);
|
||||
} else {
|
||||
memcpy((char *) data.p + off_s,
|
||||
ctx->boot_partition.label + LABEL_OFFSET + FLASH_PARAMS_LEN + off_d,
|
||||
len);
|
||||
}
|
||||
}
|
||||
|
||||
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi,
|
||||
struct mg_str data) {
|
||||
esp_err_t err = ESP_FAIL;
|
||||
int to_process = (int) data.len;
|
||||
if (strncmp(fi->name, ctx->app_file_name.ptr, ctx->app_file_name.len) == 0) {
|
||||
err = esp_ota_write(ctx->app_ota_handle, data.p, to_process);
|
||||
} else {
|
||||
swap_flash_params(ctx, data, true /* save */);
|
||||
err = esp_partition_write(ctx->write_partition, ctx->write_offset, data.p,
|
||||
to_process);
|
||||
swap_flash_params(ctx, data, false /* save */);
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
LOG(LL_ERROR,
|
||||
("Write %d @ %d failed: %d", (int) data.len, ctx->write_offset, err));
|
||||
ctx->status_msg = "Failed to write data";
|
||||
return -1;
|
||||
}
|
||||
ctx->write_offset += to_process;
|
||||
return to_process;
|
||||
}
|
||||
|
||||
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi, struct mg_str tail) {
|
||||
int ret = -1;
|
||||
if (tail.len > 0) {
|
||||
char tmp[MGOS_UPDATER_DATA_CHUNK_SIZE];
|
||||
memset(tmp, 0xff, sizeof(tmp));
|
||||
memcpy(tmp, tail.p, tail.len);
|
||||
ret = mgos_upd_file_data(ctx, fi, mg_mk_str_n(tmp, sizeof(tmp)));
|
||||
if (ret != sizeof(tmp)) {
|
||||
ctx->status_msg = "Failed to write tail";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (strncmp(fi->name, ctx->app_file_name.ptr, ctx->app_file_name.len) == 0) {
|
||||
esp_ota_handle_t oh = ctx->app_ota_handle;
|
||||
ctx->app_ota_handle = 0;
|
||||
if (esp_ota_end(oh) != ESP_OK) {
|
||||
ctx->app_ota_handle = 0;
|
||||
ctx->status_msg = "Failed to finalize app write";
|
||||
return -2;
|
||||
}
|
||||
} else if (ctx->write_partition == &ctx->boot_partition) {
|
||||
ctx->boot_partition.address = ctx->boot_addr;
|
||||
}
|
||||
if (!verify_checksum(ctx->write_partition, fi->size, ctx->cs_sha1->ptr,
|
||||
true /* critical */)) {
|
||||
ctx->status_msg = "Digest mismatch";
|
||||
return -10;
|
||||
} else {
|
||||
LOG(LL_INFO, ("%s verified (%.*s)", ctx->write_partition->label,
|
||||
(int) ctx->cs_sha1->len, ctx->cs_sha1->ptr));
|
||||
}
|
||||
return tail.len;
|
||||
}
|
||||
|
||||
static bool set_update_status(int old_slot, int new_slot, bool first_boot,
|
||||
bool merge_fs) {
|
||||
bool ret = false;
|
||||
nvs_handle h;
|
||||
esp_err_t err = nvs_open(MGOS_UPDATE_NVS_NAMESPACE, NVS_READWRITE, &h);
|
||||
if (err != ESP_OK) {
|
||||
LOG(LL_ERROR, ("Failed to open NVS: %d", err));
|
||||
return false;
|
||||
}
|
||||
const uint32_t val =
|
||||
MGOS_UPDATE_NVS_VAL(old_slot, new_slot, first_boot, merge_fs);
|
||||
err = nvs_set_u32(h, MGOS_UPDATE_NVS_KEY_FLAGS, val);
|
||||
if (err != ESP_OK) {
|
||||
LOG(LL_ERROR, ("Failed to set: %d", err));
|
||||
goto cleanup;
|
||||
}
|
||||
LOG(LL_DEBUG, ("New status: %08x", val));
|
||||
if (first_boot && merge_fs) {
|
||||
char *old_fs_val;
|
||||
mg_asprintf(&old_fs_val, 0, "%s %s", mgos_vfs_get_root_fs_type(),
|
||||
mgos_vfs_get_root_fs_opts());
|
||||
err = nvs_set_str(h, MGOS_UPDATE_NVS_KEY_OLD_FS, old_fs_val);
|
||||
free(old_fs_val);
|
||||
}
|
||||
nvs_commit(h);
|
||||
g_boot_status = val;
|
||||
ret = true;
|
||||
|
||||
cleanup:
|
||||
nvs_close(h);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static uint32_t get_update_status() {
|
||||
uint32_t val = 0;
|
||||
nvs_handle h;
|
||||
esp_err_t err = nvs_open(MGOS_UPDATE_NVS_NAMESPACE, NVS_READONLY, &h);
|
||||
if (err != ESP_OK) {
|
||||
/* This is normal, meaning no update has taken place yet. */
|
||||
if (err != ESP_ERR_NVS_NOT_FOUND) {
|
||||
LOG(LL_ERROR, ("Failed to open NVS: %d", err));
|
||||
}
|
||||
return val;
|
||||
}
|
||||
err = nvs_get_u32(h, MGOS_UPDATE_NVS_KEY_FLAGS, &val);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) {
|
||||
LOG(LL_ERROR, ("Failed to get: %d", err));
|
||||
goto cleanup;
|
||||
}
|
||||
LOG(LL_DEBUG, ("Update status: %08x", val));
|
||||
|
||||
cleanup:
|
||||
nvs_close(h);
|
||||
return val;
|
||||
}
|
||||
|
||||
static bool esp32_set_boot_slot(int slot) {
|
||||
const esp_partition_t *p = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_OTA(slot), NULL);
|
||||
if (p == NULL) return false;
|
||||
LOG(LL_INFO, ("Setting boot partition to %s", p->label));
|
||||
return (esp_ota_set_boot_partition(p) == ESP_OK);
|
||||
}
|
||||
|
||||
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx) {
|
||||
if (!set_update_status(SUBTYPE_TO_SLOT(ctx->cur_app_partition->subtype),
|
||||
SUBTYPE_TO_SLOT(ctx->app_partition->subtype),
|
||||
true /* first_boot */, true /* merge_fs */)) {
|
||||
ctx->status_msg = "Failed to set update status";
|
||||
return -1;
|
||||
}
|
||||
if (!esp32_set_boot_slot(SUBTYPE_TO_SLOT(ctx->app_partition->subtype))) {
|
||||
ctx->status_msg = "Failed to set boot partition";
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx) {
|
||||
if (ctx == NULL) return;
|
||||
if (ctx->app_ota_handle != 0) {
|
||||
esp_ota_end(ctx->app_ota_handle);
|
||||
}
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
static bool copy_partition(const esp_partition_t *src,
|
||||
const esp_partition_t *dst) {
|
||||
esp_err_t err;
|
||||
if (src->size > dst->size) return false;
|
||||
char cs_hex[CS_HEX_BUF_SIZE];
|
||||
if (!compute_checksum(src, src->size, cs_hex)) {
|
||||
return false;
|
||||
}
|
||||
if (verify_checksum(dst, src->size, cs_hex, false /* critical */)) {
|
||||
LOG(LL_INFO,
|
||||
("%s -> %s: digest matched (%s)", src->label, dst->label, cs_hex));
|
||||
return true;
|
||||
}
|
||||
if ((err = esp_partition_erase_range(dst, 0, src->size)) != ESP_OK) {
|
||||
LOG(LL_ERROR, ("%s: erase %u failed: %d", dst->label, src->size, err));
|
||||
return false;
|
||||
}
|
||||
uint32_t offset = 0;
|
||||
while (offset < src->size) {
|
||||
uint32_t buf[128];
|
||||
uint32_t n = sizeof(buf);
|
||||
if (n > src->size - offset) n = src->size - offset;
|
||||
if ((err = esp_partition_read(src, offset, buf, n)) != ESP_OK) {
|
||||
LOG(LL_ERROR, ("%s: read @ %u failed: %d", src->label, offset, err));
|
||||
return false;
|
||||
}
|
||||
if ((err = esp_partition_write(dst, offset, buf, n)) != ESP_OK) {
|
||||
LOG(LL_ERROR, ("%s: write @ %u failed: %d", dst->label, offset, err));
|
||||
return false;
|
||||
}
|
||||
offset += n;
|
||||
}
|
||||
if (!verify_checksum(dst, src->size, cs_hex, true /* critical */)) {
|
||||
return false;
|
||||
}
|
||||
LOG(LL_INFO, ("%s -> %s: copied %u bytes, SHA1 %s", src->label, dst->label,
|
||||
offset, cs_hex));
|
||||
return true;
|
||||
}
|
||||
|
||||
int mgos_upd_create_snapshot() {
|
||||
const esp_partition_t *cur_app_partition, *cur_fs_partition;
|
||||
const esp_partition_t *new_app_partition, *new_fs_partition;
|
||||
const char *status_msg = NULL;
|
||||
int slot =
|
||||
find_inactive_slot(&cur_app_partition, &cur_fs_partition,
|
||||
&new_app_partition, &new_fs_partition, &status_msg);
|
||||
if (slot < 0) {
|
||||
LOG(LL_ERROR, ("%s", status_msg));
|
||||
return slot;
|
||||
}
|
||||
LOG(LL_INFO, ("Snapshot: %s -> %s, %s -> %s", cur_app_partition->label,
|
||||
new_app_partition->label, cur_fs_partition->label,
|
||||
new_fs_partition->label));
|
||||
if (!copy_partition(cur_app_partition, new_app_partition)) return -2;
|
||||
if (!copy_partition(cur_fs_partition, new_fs_partition)) return -3;
|
||||
LOG(LL_INFO, ("Snapshot created"));
|
||||
return slot;
|
||||
}
|
||||
|
||||
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs) {
|
||||
memset(bs, 0, sizeof(*bs));
|
||||
bs->active_slot = MGOS_UPDATE_NEW_SLOT(g_boot_status);
|
||||
bs->revert_slot = MGOS_UPDATE_OLD_SLOT(g_boot_status);
|
||||
bs->is_committed = (g_boot_status & MGOS_UPDATE_FIRST_BOOT) == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs) {
|
||||
return (set_update_status(bs->revert_slot, bs->active_slot,
|
||||
!bs->is_committed /* first_boot */,
|
||||
false /* merge_fs */) &&
|
||||
esp32_set_boot_slot(bs->active_slot));
|
||||
}
|
||||
|
||||
void mgos_upd_boot_revert(void) {
|
||||
int slot = MGOS_UPDATE_OLD_SLOT(g_boot_status);
|
||||
if (slot == MGOS_UPDATE_NEW_SLOT(g_boot_status)) return;
|
||||
LOG(LL_ERROR, ("Reverting to slot %d", slot));
|
||||
set_update_status(slot, slot, false /* first_boot */, false /* merge_fs */);
|
||||
esp32_set_boot_slot(slot);
|
||||
}
|
||||
|
||||
void mgos_upd_boot_commit(void) {
|
||||
int slot = MGOS_UPDATE_NEW_SLOT(g_boot_status);
|
||||
if (set_update_status(MGOS_UPDATE_OLD_SLOT(g_boot_status), slot,
|
||||
false /* first_boot */, false /* merge_fs */) &&
|
||||
esp32_set_boot_slot(slot)) {
|
||||
LOG(LL_INFO, ("Committed slot %d", slot));
|
||||
} else {
|
||||
LOG(LL_ERROR, ("Failed to commit update"));
|
||||
}
|
||||
}
|
||||
|
||||
int mgos_upd_apply_update(void) {
|
||||
int ret = -1;
|
||||
nvs_handle h = 0;
|
||||
char *old_fs_val = NULL;
|
||||
int old_slot = MGOS_UPDATE_OLD_SLOT(g_boot_status);
|
||||
if (MGOS_UPDATE_NEW_SLOT(g_boot_status) == old_slot ||
|
||||
!(g_boot_status & MGOS_UPDATE_MERGE_FS)) {
|
||||
return 0;
|
||||
}
|
||||
const esp_partition_t *old_fs_part = esp32_find_fs_for_app_slot(old_slot);
|
||||
if (old_fs_part == NULL) {
|
||||
LOG(LL_ERROR, ("No old FS partition"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
const char *old_fs_type = mgos_vfs_get_root_fs_type();
|
||||
const char *old_fs_opts = mgos_vfs_get_root_fs_opts();
|
||||
|
||||
esp_err_t err = nvs_open(MGOS_UPDATE_NVS_NAMESPACE, NVS_READONLY, &h);
|
||||
if (err == ESP_OK) {
|
||||
size_t l = 0;
|
||||
if ((err = nvs_get_str(h, MGOS_UPDATE_NVS_KEY_OLD_FS, NULL, &l)) ==
|
||||
ESP_OK) {
|
||||
old_fs_val = malloc(l);
|
||||
if (old_fs_val != NULL &&
|
||||
(err = nvs_get_str(h, MGOS_UPDATE_NVS_KEY_OLD_FS, old_fs_val, &l)) ==
|
||||
ESP_OK) {
|
||||
old_fs_type = old_fs_val;
|
||||
char *sp = strchr(old_fs_val, ' ');
|
||||
*sp = '\0';
|
||||
old_fs_opts = sp + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!esp32_fs_mount_part(old_fs_part->label, "/old", old_fs_type,
|
||||
old_fs_opts)) {
|
||||
LOG(LL_ERROR, ("Failed to mount old file system"));
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = (mgos_upd_merge_fs("/old", "/") ? 0 : -2);
|
||||
|
||||
mgos_vfs_umount("/old");
|
||||
|
||||
out:
|
||||
free(old_fs_val);
|
||||
if (h != 0) nvs_close(h);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void esp32_updater_early_init(void) {
|
||||
g_boot_status = get_update_status();
|
||||
if (!mgos_upd_is_first_boot()) return;
|
||||
/*
|
||||
* Tombstone the current config. If anything goes wrong between now and
|
||||
* commit, next boot will use the old slot.
|
||||
*/
|
||||
uint32_t bs = g_boot_status;
|
||||
set_update_status(MGOS_UPDATE_OLD_SLOT(g_boot_status),
|
||||
MGOS_UPDATE_OLD_SLOT(g_boot_status), false /* first_boot */,
|
||||
false /* merge_fs */);
|
||||
g_boot_status = bs;
|
||||
esp32_set_boot_slot(MGOS_UPDATE_OLD_SLOT(g_boot_status));
|
||||
}
|
||||
|
||||
bool mgos_upd_is_first_boot(void) {
|
||||
return (g_boot_status & MGOS_UPDATE_FIRST_BOOT) != 0;
|
||||
}
|
481
libs/ota-common/src/esp8266/esp_updater.c
Normal file
481
libs/ota-common/src/esp8266/esp_updater.c
Normal file
@ -0,0 +1,481 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include <c_types.h>
|
||||
#include <spi_flash.h>
|
||||
|
||||
#include "common/cs_dbg.h"
|
||||
#include "common/cs_sha1.h"
|
||||
#include "common/platforms/esp8266/esp_missing_includes.h"
|
||||
#include "common/platforms/esp8266/rboot/rboot/appcode/rboot-api.h"
|
||||
#include "common/queue.h"
|
||||
|
||||
#include "mgos_hal.h"
|
||||
#include "mgos_sys_config.h"
|
||||
#include "mgos_updater_hal.h"
|
||||
#include "mgos_updater_util.h"
|
||||
#include "mgos_vfs.h"
|
||||
|
||||
#include "esp_flash_writer.h"
|
||||
#include "esp_fs.h"
|
||||
#include "esp_rboot.h"
|
||||
|
||||
#define CS_LEN 20 /* SHA1 */
|
||||
#define CS_HEX_LEN (CS_LEN * 2)
|
||||
#define CS_HEX_BUF_SIZE (CS_HEX_LEN + 1)
|
||||
|
||||
#define BOOT_F_MERGE_FS (1U << 0)
|
||||
|
||||
#define FW_SLOT_SIZE 0x100000
|
||||
|
||||
#define FLASH_PARAMS_ADDR 0
|
||||
|
||||
#define WRITE_CHUNK_SIZE 4
|
||||
|
||||
struct slot_info {
|
||||
int id;
|
||||
uint32_t fw_addr;
|
||||
uint32_t fw_size;
|
||||
uint32_t fw_slot_size;
|
||||
uint32_t fs_addr;
|
||||
uint32_t fs_size;
|
||||
uint32_t fs_slot_size;
|
||||
};
|
||||
|
||||
struct mgos_upd_hal_ctx {
|
||||
const char *status_msg;
|
||||
struct slot_info write_slot;
|
||||
struct json_token boot_file_name, boot_cs_sha1;
|
||||
struct json_token fw_file_name, fw_cs_sha1;
|
||||
struct json_token fs_file_name, fs_cs_sha1;
|
||||
uint32_t boot_addr, boot_size, fw_size, fs_size;
|
||||
bool update_bootloader;
|
||||
union {
|
||||
uint8_t bytes[4];
|
||||
uint32_t align4;
|
||||
} flash_params;
|
||||
|
||||
struct esp_flash_write_ctx wctx;
|
||||
const struct json_token *wcs;
|
||||
};
|
||||
|
||||
static void get_slot_info(int id, struct slot_info *si) {
|
||||
memset(si, 0, sizeof(*si));
|
||||
si->id = id;
|
||||
if (id == 0) {
|
||||
si->fw_addr = FW1_ADDR;
|
||||
si->fs_addr = FW1_FS_ADDR;
|
||||
} else {
|
||||
si->fw_addr = FW2_ADDR;
|
||||
si->fs_addr = FW2_FS_ADDR;
|
||||
}
|
||||
rboot_config *cfg = get_rboot_config();
|
||||
si->fw_size = cfg->roms_sizes[id];
|
||||
si->fw_slot_size = FW_SIZE;
|
||||
si->fs_size = cfg->fs_sizes[id];
|
||||
si->fs_slot_size = FS_SIZE;
|
||||
}
|
||||
|
||||
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void) {
|
||||
return calloc(1, sizeof(struct mgos_upd_hal_ctx));
|
||||
}
|
||||
|
||||
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx) {
|
||||
return ctx->status_msg;
|
||||
}
|
||||
|
||||
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts) {
|
||||
struct json_token fs = JSON_INVALID_TOKEN, fw = JSON_INVALID_TOKEN;
|
||||
if (json_scanf(parts->ptr, parts->len, "{fw: %T, fs: %T}", &fw, &fs) != 2) {
|
||||
ctx->status_msg = "Invalid manifest";
|
||||
return -1;
|
||||
}
|
||||
uint32_t boot_addr = 0, fw_addr = 0, fs_addr = 0;
|
||||
int update_bootloader = false;
|
||||
json_scanf(parts->ptr, parts->len,
|
||||
"{boot: {src: %T, addr: %u, cs_sha1: %T, update: %B}, "
|
||||
"fw: {src: %T, addr: %u, cs_sha1: %T}, "
|
||||
"fs: {src: %T, addr: %u, cs_sha1: %T}}",
|
||||
&ctx->boot_file_name, &boot_addr, &ctx->boot_cs_sha1,
|
||||
&update_bootloader, &ctx->fw_file_name, &fw_addr, &ctx->fw_cs_sha1,
|
||||
&ctx->fs_file_name, &fs_addr, &ctx->fs_cs_sha1);
|
||||
if (ctx->fw_file_name.len == 0 || ctx->fw_cs_sha1.len == 0 ||
|
||||
ctx->fs_file_name.len == 0 || ctx->fs_cs_sha1.len == 0 || fs_addr == 0 ||
|
||||
(ctx->update_bootloader &&
|
||||
(ctx->boot_file_name.len == 0 || ctx->boot_cs_sha1.len == 0))) {
|
||||
ctx->status_msg = "Incomplete update package";
|
||||
return -3;
|
||||
}
|
||||
|
||||
if (ctx->fw_cs_sha1.len != CS_HEX_LEN || ctx->fs_cs_sha1.len != CS_HEX_LEN ||
|
||||
(ctx->update_bootloader && ctx->boot_cs_sha1.len != CS_HEX_LEN)) {
|
||||
ctx->status_msg = "Invalid checksum format";
|
||||
return -4;
|
||||
}
|
||||
|
||||
struct mgos_upd_boot_state bs;
|
||||
if (!mgos_upd_boot_get_state(&bs)) return -5;
|
||||
int inactive_slot = (bs.active_slot == 0 ? 1 : 0);
|
||||
get_slot_info(inactive_slot, &ctx->write_slot);
|
||||
if (ctx->write_slot.fw_addr == 0) {
|
||||
ctx->status_msg = "OTA is not supported in this build";
|
||||
return -5;
|
||||
}
|
||||
|
||||
ctx->boot_addr = boot_addr;
|
||||
ctx->update_bootloader = update_bootloader;
|
||||
if (ctx->update_bootloader) {
|
||||
/*
|
||||
* Preserve old flash params.
|
||||
* We need bytes 2 and 3, but the first 2 bytes are constant anyway, so we
|
||||
* read and write 4 for simplicity.
|
||||
*/
|
||||
if (spi_flash_read(FLASH_PARAMS_ADDR, &ctx->flash_params.align4, 4) != 0) {
|
||||
ctx->status_msg = "Failed to read flash params";
|
||||
return -6;
|
||||
}
|
||||
LOG(LL_INFO,
|
||||
("Boot: %.*s -> 0x%x, current flash params: 0x%02x%02x",
|
||||
(int) ctx->boot_file_name.len, ctx->boot_file_name.ptr, ctx->boot_addr,
|
||||
ctx->flash_params.bytes[2], ctx->flash_params.bytes[3]));
|
||||
}
|
||||
|
||||
LOG(LL_INFO,
|
||||
("Slot %d, FW: %.*s -> 0x%x, FS %.*s -> 0x%x", ctx->write_slot.id,
|
||||
(int) ctx->fw_file_name.len, ctx->fw_file_name.ptr,
|
||||
ctx->write_slot.fw_addr, (int) ctx->fs_file_name.len,
|
||||
ctx->fs_file_name.ptr, ctx->write_slot.fs_addr));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void bin2hex(const uint8_t *src, int src_len, char *dst);
|
||||
|
||||
static bool compute_checksum(uint32_t addr, size_t len, char *cs_hex) {
|
||||
cs_sha1_ctx ctx;
|
||||
cs_sha1_init(&ctx);
|
||||
while (len != 0) {
|
||||
uint32_t read_buf[16];
|
||||
uint32_t to_read = sizeof(read_buf);
|
||||
if (to_read > len) to_read = len;
|
||||
if (spi_flash_read(addr, read_buf, to_read) != 0) {
|
||||
LOG(LL_ERROR, ("Failed to read %d bytes from %X", to_read, addr));
|
||||
return false;
|
||||
}
|
||||
cs_sha1_update(&ctx, (uint8_t *) read_buf, to_read);
|
||||
mgos_wdt_feed();
|
||||
addr += to_read;
|
||||
len -= to_read;
|
||||
}
|
||||
uint8_t cs_buf[CS_LEN];
|
||||
cs_sha1_final(cs_buf, &ctx);
|
||||
bin2hex(cs_buf, CS_LEN, cs_hex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool verify_checksum(uint32_t addr, size_t len, const char *exp_cs_hex,
|
||||
bool critical) {
|
||||
char cs_hex[CS_HEX_LEN + 1];
|
||||
if (!compute_checksum(addr, len, cs_hex)) return false;
|
||||
bool ret = (strncasecmp(cs_hex, exp_cs_hex, CS_HEX_LEN) == 0);
|
||||
LOG((ret || !critical ? LL_DEBUG : LL_ERROR),
|
||||
("SHA1 %u @ 0x%x = %.*s, want %.*s", len, addr, CS_HEX_LEN, cs_hex,
|
||||
CS_HEX_LEN, exp_cs_hex));
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum mgos_upd_file_action mgos_upd_file_begin(
|
||||
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi) {
|
||||
bool res = false;
|
||||
struct esp_flash_write_ctx *wctx = &ctx->wctx;
|
||||
if (ctx->update_bootloader &&
|
||||
strncmp(fi->name, ctx->boot_file_name.ptr, ctx->boot_file_name.len) ==
|
||||
0) {
|
||||
if (fi->size <= BOOT_CONFIG_ADDR) {
|
||||
res = esp_init_flash_write_ctx(wctx, ctx->boot_addr, BOOT_CONFIG_ADDR);
|
||||
ctx->wcs = &ctx->boot_cs_sha1;
|
||||
ctx->boot_size = fi->size;
|
||||
} else {
|
||||
LOG(LL_ERROR, ("Boot loader too big."));
|
||||
res = false;
|
||||
}
|
||||
} else if (strncmp(fi->name, ctx->fw_file_name.ptr, ctx->fw_file_name.len) ==
|
||||
0) {
|
||||
res = esp_init_flash_write_ctx(wctx, ctx->write_slot.fw_addr,
|
||||
ctx->write_slot.fw_slot_size);
|
||||
ctx->wcs = &ctx->fw_cs_sha1;
|
||||
ctx->fw_size = fi->size;
|
||||
} else if (strncmp(fi->name, ctx->fs_file_name.ptr, ctx->fs_file_name.len) ==
|
||||
0) {
|
||||
res = esp_init_flash_write_ctx(wctx, ctx->write_slot.fs_addr,
|
||||
ctx->write_slot.fs_slot_size);
|
||||
ctx->wcs = &ctx->fs_cs_sha1;
|
||||
ctx->fs_size = fi->size;
|
||||
} else {
|
||||
LOG(LL_DEBUG, ("Not interesting: %s", fi->name));
|
||||
return MGOS_UPDATER_SKIP_FILE;
|
||||
}
|
||||
if (!res) {
|
||||
ctx->status_msg = "Failed to start write";
|
||||
return MGOS_UPDATER_ABORT;
|
||||
}
|
||||
if (fi->size > wctx->max_size) {
|
||||
LOG(LL_ERROR, ("Cannot write %s (%u) @ 0x%x: max size %u", fi->name,
|
||||
fi->size, wctx->addr, wctx->max_size));
|
||||
ctx->status_msg = "Image too big";
|
||||
return MGOS_UPDATER_ABORT;
|
||||
}
|
||||
wctx->max_size = fi->size;
|
||||
if (verify_checksum(wctx->addr, fi->size, ctx->wcs->ptr, false)) {
|
||||
LOG(LL_INFO, ("Skip writing %s (%u) @ 0x%x (digest matches)", fi->name,
|
||||
fi->size, wctx->addr));
|
||||
return MGOS_UPDATER_SKIP_FILE;
|
||||
}
|
||||
LOG(LL_INFO,
|
||||
("Start writing %s (%u) @ 0x%x", fi->name, fi->size, wctx->addr));
|
||||
return MGOS_UPDATER_PROCESS_FILE;
|
||||
}
|
||||
|
||||
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi,
|
||||
struct mg_str data) {
|
||||
int to_process = (data.len / WRITE_CHUNK_SIZE) * WRITE_CHUNK_SIZE;
|
||||
if (to_process == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int num_written = esp_flash_write(&ctx->wctx, data);
|
||||
if (num_written < 0) {
|
||||
ctx->status_msg = "Write failed";
|
||||
}
|
||||
(void) fi;
|
||||
return num_written;
|
||||
}
|
||||
|
||||
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi, struct mg_str tail) {
|
||||
assert(tail.len < WRITE_CHUNK_SIZE);
|
||||
if (tail.len > 0 && esp_flash_write(&ctx->wctx, tail) != (int) tail.len) {
|
||||
ctx->status_msg = "Tail write failed";
|
||||
return -1;
|
||||
}
|
||||
if (!verify_checksum(ctx->wctx.addr, fi->size, ctx->wcs->ptr, true)) {
|
||||
ctx->status_msg = "Invalid checksum";
|
||||
return -2;
|
||||
} else {
|
||||
LOG(LL_INFO, ("Write finished, checksum ok"));
|
||||
}
|
||||
if (ctx->update_bootloader &&
|
||||
strncmp(fi->name, ctx->boot_file_name.ptr, ctx->boot_file_name.len) ==
|
||||
0) {
|
||||
LOG(LL_INFO, ("Restoring flash params"));
|
||||
if (spi_flash_write(FLASH_PARAMS_ADDR, &ctx->flash_params.align4, 4) != 0) {
|
||||
ctx->status_msg = "Failed to write flash params";
|
||||
return -3;
|
||||
}
|
||||
}
|
||||
memset(&ctx->wctx, 0, sizeof(ctx->wctx));
|
||||
return tail.len;
|
||||
}
|
||||
|
||||
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx) {
|
||||
if (ctx->fw_size == 0) {
|
||||
ctx->status_msg = "Missing fw part";
|
||||
return -1;
|
||||
}
|
||||
if (ctx->fs_size == 0) {
|
||||
ctx->status_msg = "Missing fs part";
|
||||
return -2;
|
||||
}
|
||||
|
||||
int slot = ctx->write_slot.id;
|
||||
rboot_config *cfg = get_rboot_config();
|
||||
cfg->current_rom = slot;
|
||||
cfg->previous_rom = (slot == 0 ? 1 : 0);
|
||||
cfg->roms[slot] = ctx->write_slot.fw_addr;
|
||||
cfg->roms_sizes[slot] = ctx->fw_size;
|
||||
cfg->fs_addresses[slot] = ctx->write_slot.fs_addr;
|
||||
cfg->fs_sizes[slot] = ctx->fs_size;
|
||||
cfg->is_first_boot = cfg->fw_updated = true;
|
||||
cfg->boot_attempts = 0;
|
||||
cfg->user_flags |= BOOT_F_MERGE_FS;
|
||||
if (!rboot_set_config(cfg)) {
|
||||
ctx->status_msg = "Failed to set boot config";
|
||||
return -3;
|
||||
}
|
||||
|
||||
LOG(LL_INFO,
|
||||
("New rboot config: "
|
||||
"prev_rom: %d, current_rom: %d current_rom addr: 0x%x, "
|
||||
"current_rom size: %d, current_fs addr: 0x%0x, current_fs size: %d",
|
||||
(int) cfg->previous_rom, (int) cfg->current_rom,
|
||||
cfg->roms[cfg->current_rom], cfg->roms_sizes[cfg->current_rom],
|
||||
cfg->fs_addresses[cfg->current_rom], cfg->fs_sizes[cfg->current_rom]));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx) {
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
int mgos_upd_apply_update(void) {
|
||||
rboot_config *cfg = get_rboot_config();
|
||||
if (!cfg->user_flags & BOOT_F_MERGE_FS) return 0;
|
||||
uint32_t old_fs_addr = cfg->fs_addresses[cfg->previous_rom];
|
||||
uint32_t old_fs_size = cfg->fs_sizes[cfg->previous_rom];
|
||||
LOG(LL_INFO, ("Mounting old FS: %d @ 0x%x", old_fs_size, old_fs_addr));
|
||||
if (!esp_fs_mount(old_fs_addr, old_fs_size, "oldroot", "/old")) {
|
||||
LOG(LL_ERROR, ("Update failed: cannot mount previous file system"));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = (mgos_upd_merge_fs("/old", "/") ? 0 : -2);
|
||||
|
||||
mgos_vfs_umount("/old");
|
||||
mgos_vfs_dev_unregister("oldroot");
|
||||
|
||||
if (ret == 0) {
|
||||
cfg->user_flags &= ~BOOT_F_MERGE_FS;
|
||||
rboot_set_config(cfg);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool copy_region(uint32_t src_addr, uint32_t dst_addr, size_t len) {
|
||||
char cs_hex[CS_HEX_LEN + 1];
|
||||
if (!compute_checksum(src_addr, len, cs_hex)) return false;
|
||||
if (verify_checksum(dst_addr, len, cs_hex, false)) {
|
||||
LOG(LL_DEBUG, ("Skip copying %u @ 0x%x -> 0x%x (digest matches)", len,
|
||||
src_addr, dst_addr));
|
||||
return true;
|
||||
}
|
||||
LOG(LL_DEBUG,
|
||||
("Copy %u @ 0x%x -> 0x%x (%s)", len, src_addr, dst_addr, cs_hex));
|
||||
struct esp_flash_write_ctx wctx;
|
||||
if (!esp_init_flash_write_ctx(&wctx, dst_addr, len)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t offset = 0;
|
||||
while (offset < len) {
|
||||
uint32_t read_buf[128];
|
||||
int to_read = sizeof(read_buf);
|
||||
if (offset + to_read > len) to_read = len - offset;
|
||||
if (spi_flash_read(src_addr + offset, read_buf, to_read) != 0) {
|
||||
LOG(LL_ERROR, ("Failed to read %d @ 0x%x", to_read, src_addr + offset));
|
||||
return false;
|
||||
}
|
||||
int num_written =
|
||||
esp_flash_write(&wctx, mg_mk_str_n((const char *) read_buf, to_read));
|
||||
if (num_written < 0) return false;
|
||||
if (num_written != to_read) {
|
||||
/* Flush last chunk */
|
||||
int to_write = to_read - num_written;
|
||||
num_written = esp_flash_write(
|
||||
&wctx,
|
||||
mg_mk_str_n(((const char *) read_buf) + num_written, to_write));
|
||||
if (num_written != to_write) return false;
|
||||
}
|
||||
offset += to_read;
|
||||
mgos_wdt_feed();
|
||||
}
|
||||
if (!verify_checksum(dst_addr, len, cs_hex, true)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int mgos_upd_create_snapshot() {
|
||||
struct slot_info rsi, wsi;
|
||||
struct mgos_upd_boot_state bs;
|
||||
if (!mgos_upd_boot_get_state(&bs)) return -1;
|
||||
int inactive_slot = (bs.active_slot == 0 ? 1 : 0);
|
||||
get_slot_info(bs.active_slot, &rsi);
|
||||
get_slot_info(inactive_slot, &wsi);
|
||||
LOG(LL_INFO, ("Snapshot: %d -> %d, "
|
||||
"FW: 0x%x (%u) -> 0x%x, FS: 0x%x (%u) -> 0x%x",
|
||||
rsi.id, wsi.id, rsi.fw_addr, rsi.fw_size, wsi.fw_addr,
|
||||
rsi.fs_addr, rsi.fs_size, wsi.fs_addr));
|
||||
if (!copy_region(rsi.fw_addr, wsi.fw_addr, rsi.fw_size)) return -2;
|
||||
if (!copy_region(rsi.fs_addr, wsi.fs_addr, rsi.fs_size)) return -3;
|
||||
int slot = wsi.id;
|
||||
rboot_config *cfg = get_rboot_config();
|
||||
cfg->roms[slot] = wsi.fw_addr;
|
||||
cfg->roms_sizes[slot] = rsi.fw_size;
|
||||
cfg->fs_addresses[slot] = wsi.fs_addr;
|
||||
cfg->fs_sizes[slot] = rsi.fs_size;
|
||||
if (!rboot_set_config(cfg)) return -4;
|
||||
LOG(LL_INFO, ("Snapshot created"));
|
||||
return slot;
|
||||
}
|
||||
|
||||
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs) {
|
||||
rboot_config *cfg = get_rboot_config();
|
||||
if (cfg == NULL) return false;
|
||||
LOG(LL_DEBUG, ("cur %d prev %d fwu %d", cfg->current_rom, cfg->previous_rom,
|
||||
cfg->fw_updated));
|
||||
memset(bs, 0, sizeof(*bs));
|
||||
bs->active_slot = cfg->current_rom;
|
||||
bs->revert_slot = cfg->previous_rom;
|
||||
bs->is_committed = !cfg->fw_updated;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs) {
|
||||
rboot_config *cfg = get_rboot_config();
|
||||
if (cfg == NULL) return false;
|
||||
if (bs->active_slot < 0 || bs->active_slot > 1 || bs->revert_slot < 0 ||
|
||||
bs->revert_slot > 1) {
|
||||
return false;
|
||||
}
|
||||
cfg->current_rom = bs->active_slot;
|
||||
cfg->previous_rom = bs->revert_slot;
|
||||
cfg->fw_updated = cfg->is_first_boot = (!bs->is_committed);
|
||||
cfg->boot_attempts = 0;
|
||||
cfg->user_flags = 0;
|
||||
LOG(LL_INFO, ("cur %d prev %d fwu %d", cfg->current_rom, cfg->previous_rom,
|
||||
cfg->fw_updated));
|
||||
return rboot_set_config(cfg);
|
||||
}
|
||||
|
||||
void mgos_upd_boot_commit() {
|
||||
struct mgos_upd_boot_state s;
|
||||
if (!mgos_upd_boot_get_state(&s)) return;
|
||||
if (s.is_committed) return;
|
||||
LOG(LL_INFO, ("Committing ROM %d", s.active_slot));
|
||||
s.is_committed = true;
|
||||
mgos_upd_boot_set_state(&s);
|
||||
}
|
||||
|
||||
void mgos_upd_boot_revert(void) {
|
||||
struct mgos_upd_boot_state s;
|
||||
if (!mgos_upd_boot_get_state(&s)) return;
|
||||
if (s.is_committed) return;
|
||||
s.active_slot = (s.active_slot == 0 ? 1 : 0);
|
||||
LOG(LL_INFO, ("Update failed, reverting to ROM %d", s.active_slot));
|
||||
s.is_committed = true;
|
||||
mgos_upd_boot_set_state(&s);
|
||||
}
|
||||
|
||||
bool mgos_upd_is_first_boot(void) {
|
||||
return get_rboot_config()->is_first_boot;
|
||||
}
|
887
libs/ota-common/src/mgos_updater_common.c
Normal file
887
libs/ota-common/src/mgos_updater_common.c
Normal file
@ -0,0 +1,887 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "mgos_updater_common.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "common/cs_crc32.h"
|
||||
#include "common/cs_dbg.h"
|
||||
#include "common/cs_file.h"
|
||||
#include "common/str_util.h"
|
||||
|
||||
#include "mgos_event.h"
|
||||
#include "mgos_hal.h"
|
||||
#include "mgos_sys_config.h"
|
||||
#include "mgos_timers.h"
|
||||
#include "mgos_updater_hal.h"
|
||||
#include "mgos_vfs.h"
|
||||
|
||||
/*
|
||||
* Using static variable (not only c->user_data), it allows to check if update
|
||||
* already in progress when another request arrives
|
||||
*/
|
||||
struct update_context *s_ctx = NULL;
|
||||
|
||||
/* Must be provided externally, usually auto-generated. */
|
||||
extern const char *build_id;
|
||||
extern const char *build_version;
|
||||
|
||||
#define UPDATER_CTX_FILE_NAME "updater.dat"
|
||||
#define MANIFEST_FILENAME "manifest.json"
|
||||
#define SHA1SUM_LEN 40
|
||||
#define PROGRESS_REPORT_BYTES 50000
|
||||
#define PROGRESS_REPORT_SECONDS 5
|
||||
|
||||
/*
|
||||
* --- Zip file local header structure ---
|
||||
* size offset
|
||||
* local file header signature (0x04034b50) 4 0
|
||||
* version needed to extract 2 4
|
||||
* general purpose bit flag 2 6
|
||||
* compression method 2 8
|
||||
* last mod file time 2 10
|
||||
* last mod file date 2 12
|
||||
* crc-32 4 14
|
||||
* compressed size 4 18
|
||||
* uncompressed size 4 22
|
||||
* file name length 2 26
|
||||
* extra field length 2 28
|
||||
* file name (variable size) v 30
|
||||
* extra field (variable size) v
|
||||
*/
|
||||
|
||||
#define ZIP_LOCAL_HDR_SIZE 30U
|
||||
#define ZIP_GENFLAG_OFFSET 6U
|
||||
#define ZIP_COMPRESSION_METHOD_OFFSET 8U
|
||||
#define ZIP_CRC32_OFFSET 14U
|
||||
#define ZIP_COMPRESSED_SIZE_OFFSET 18U
|
||||
#define ZIP_UNCOMPRESSED_SIZE_OFFSET 22U
|
||||
#define ZIP_FILENAME_LEN_OFFSET 26U
|
||||
#define ZIP_EXTRAS_LEN_OFFSET 28U
|
||||
#define ZIP_FILENAME_OFFSET 30U
|
||||
#define ZIP_FILE_DESCRIPTOR_SIZE 12U
|
||||
|
||||
const uint32_t c_zip_file_header_magic = 0x04034b50;
|
||||
const uint32_t c_zip_cdir_magic = 0x02014b50;
|
||||
|
||||
enum update_state {
|
||||
US_INITED = 0,
|
||||
US_WAITING_MANIFEST_HEADER,
|
||||
US_WAITING_MANIFEST,
|
||||
US_WAITING_FILE_HEADER,
|
||||
US_WAITING_FILE,
|
||||
US_SKIPPING_DATA,
|
||||
US_SKIPPING_DESCRIPTOR,
|
||||
US_WRITE_FINISHED,
|
||||
US_FINALIZE,
|
||||
US_FINISHED,
|
||||
};
|
||||
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
|
||||
static void mgos_upd_trigger_ota_event(void) {
|
||||
struct mgos_ota_status s;
|
||||
mgos_upd_get_status(&s);
|
||||
mgos_event_trigger(MGOS_EVENT_OTA_STATUS, &s);
|
||||
}
|
||||
|
||||
static void updater_abort(void *arg) {
|
||||
struct update_context *ctx = (struct update_context *) arg;
|
||||
if (s_ctx != ctx) return;
|
||||
LOG(LL_ERROR, ("Update timed out"));
|
||||
/* Note that we do not free the context here, because whatever process
|
||||
* is stuck may still be referring to it. We close the network connection,
|
||||
* if there is one, to hopefully get things to wind down cleanly. */
|
||||
if (ctx->nc) ctx->nc->flags |= MG_F_CLOSE_IMMEDIATELY;
|
||||
ctx->wdt = MGOS_INVALID_TIMER_ID;
|
||||
s_ctx = NULL;
|
||||
}
|
||||
|
||||
struct update_context *updater_context_create(int timeout) {
|
||||
if (s_ctx != NULL) {
|
||||
LOG(LL_ERROR, ("%s", "Update already in progress"));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct mgos_upd_boot_state st;
|
||||
if (!mgos_upd_boot_get_state(&st)) {
|
||||
LOG(LL_ERROR, ("%s", "OTA is not supported"));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!mgos_upd_is_committed()) {
|
||||
LOG(LL_ERROR, ("%s", "Previous update has not been committed yet"));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
s_ctx = calloc(1, sizeof(*s_ctx));
|
||||
if (s_ctx == NULL) {
|
||||
LOG(LL_ERROR, ("Out of memory"));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
s_ctx->dev_ctx = mgos_upd_hal_ctx_create();
|
||||
|
||||
if (timeout <= 0) timeout = mgos_sys_config_get_update_timeout();
|
||||
s_ctx->wdt_timeout_ms = timeout * 1000;
|
||||
s_ctx->wdt = mgos_set_timer(s_ctx->wdt_timeout_ms, 0, updater_abort, s_ctx);
|
||||
LOG(LL_INFO, ("starting, timeout %d", timeout));
|
||||
s_ctx->ota_state = MGOS_OTA_STATE_PROGRESS;
|
||||
return s_ctx;
|
||||
}
|
||||
|
||||
struct update_context *updater_context_get_current(void) {
|
||||
return s_ctx;
|
||||
}
|
||||
|
||||
void updater_set_status(struct update_context *ctx, enum update_state st) {
|
||||
LOG(LL_DEBUG, ("Update state %d -> %d", (int) ctx->update_state, (int) st));
|
||||
ctx->update_state = st;
|
||||
}
|
||||
|
||||
/*
|
||||
* During its work, updater requires requires to store some data.
|
||||
* For example, manifest file, zip header - must be received fully, while
|
||||
* content FW/FS files can be flashed directly from recv_mbuf
|
||||
* To avoid extra memory usage, context contains plain pointer (*data)
|
||||
* and mbuf (unprocessed); data is storing in memory only if where is no way
|
||||
* to process it right now.
|
||||
*/
|
||||
static void context_update(struct update_context *ctx, const char *data,
|
||||
size_t len) {
|
||||
if (ctx->unprocessed.len != 0) {
|
||||
/* We have unprocessed data, concatenate them with arrived */
|
||||
mbuf_append(&ctx->unprocessed, data, len);
|
||||
ctx->data = ctx->unprocessed.buf;
|
||||
ctx->data_len = ctx->unprocessed.len;
|
||||
} else {
|
||||
/* No unprocessed, trying to process directly received data */
|
||||
ctx->data = data;
|
||||
ctx->data_len = len;
|
||||
}
|
||||
}
|
||||
|
||||
static void context_save_unprocessed(struct update_context *ctx) {
|
||||
if (ctx->unprocessed.len == 0) {
|
||||
mbuf_append(&ctx->unprocessed, ctx->data, ctx->data_len);
|
||||
ctx->data = ctx->unprocessed.buf;
|
||||
ctx->data_len = ctx->unprocessed.len;
|
||||
}
|
||||
}
|
||||
|
||||
void context_remove_data(struct update_context *ctx, size_t len) {
|
||||
if (ctx->unprocessed.len != 0) {
|
||||
/* Consumed data from unprocessed*/
|
||||
mbuf_remove(&ctx->unprocessed, len);
|
||||
ctx->data = ctx->unprocessed.buf;
|
||||
ctx->data_len = ctx->unprocessed.len;
|
||||
} else {
|
||||
/* Consumed received data */
|
||||
ctx->data = ctx->data + len;
|
||||
ctx->data_len -= len;
|
||||
}
|
||||
}
|
||||
|
||||
static void context_clear_current_file(struct update_context *ctx) {
|
||||
memset(&ctx->info.current_file, 0, sizeof(ctx->info.current_file));
|
||||
ctx->current_file_crc = ctx->current_file_crc_calc = 0;
|
||||
ctx->current_file_has_descriptor = false;
|
||||
}
|
||||
|
||||
int is_write_finished(struct update_context *ctx) {
|
||||
return ctx->update_state == US_WRITE_FINISHED;
|
||||
}
|
||||
|
||||
int is_update_finished(struct update_context *ctx) {
|
||||
return ctx->update_state == US_FINISHED;
|
||||
}
|
||||
|
||||
int is_reboot_required(struct update_context *ctx) {
|
||||
return ctx->need_reboot;
|
||||
}
|
||||
|
||||
static int parse_zip_file_header(struct update_context *ctx) {
|
||||
if (ctx->data_len < ZIP_LOCAL_HDR_SIZE) {
|
||||
LOG(LL_DEBUG, ("Zip header is incomplete"));
|
||||
/* Need more data*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (memcmp(ctx->data, &c_zip_file_header_magic, 4) != 0) {
|
||||
ctx->status_msg = "Malformed archive (invalid file header)";
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint16_t file_name_len, extras_len;
|
||||
memcpy(&file_name_len, ctx->data + ZIP_FILENAME_LEN_OFFSET,
|
||||
sizeof(file_name_len));
|
||||
memcpy(&extras_len, ctx->data + ZIP_EXTRAS_LEN_OFFSET, sizeof(extras_len));
|
||||
|
||||
LOG(LL_DEBUG, ("Filename len = %d bytes, extras len = %d bytes",
|
||||
(int) file_name_len, (int) extras_len));
|
||||
if (ctx->data_len < ZIP_LOCAL_HDR_SIZE + file_name_len + extras_len) {
|
||||
/* Still need mode data */
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t compression_method;
|
||||
memcpy(&compression_method, ctx->data + ZIP_COMPRESSION_METHOD_OFFSET,
|
||||
sizeof(compression_method));
|
||||
|
||||
LOG(LL_DEBUG, ("Compression method=%d", (int) compression_method));
|
||||
if (compression_method != 0) {
|
||||
/* Do not support compressed archives */
|
||||
ctx->status_msg = "Cannot handle compressed .zip";
|
||||
LOG(LL_ERROR, ("%s", ctx->status_msg));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int i;
|
||||
char *nodir_file_name = (char *) ctx->data + ZIP_FILENAME_OFFSET;
|
||||
uint16_t nodir_file_name_len = file_name_len;
|
||||
LOG(LL_DEBUG,
|
||||
("File name: %.*s", (int) nodir_file_name_len, nodir_file_name));
|
||||
|
||||
for (i = 0; i < file_name_len; i++) {
|
||||
/* archive may contain folder, but we skip it, using filenames only */
|
||||
if (*(ctx->data + ZIP_FILENAME_OFFSET + i) == '/') {
|
||||
nodir_file_name = (char *) ctx->data + ZIP_FILENAME_OFFSET + i + 1;
|
||||
nodir_file_name_len -= (i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(LL_DEBUG,
|
||||
("File name to use: %.*s", (int) nodir_file_name_len, nodir_file_name));
|
||||
|
||||
if (nodir_file_name_len >= sizeof(ctx->info.current_file.name)) {
|
||||
/* We are in charge of file names, right? */
|
||||
ctx->status_msg = "Too long file name";
|
||||
LOG(LL_ERROR, ("%s", ctx->status_msg));
|
||||
return -1;
|
||||
}
|
||||
memcpy(ctx->info.current_file.name, nodir_file_name, nodir_file_name_len);
|
||||
|
||||
memcpy(&ctx->info.current_file.size, ctx->data + ZIP_COMPRESSED_SIZE_OFFSET,
|
||||
sizeof(ctx->info.current_file.size));
|
||||
|
||||
uint32_t uncompressed_size;
|
||||
memcpy(&uncompressed_size, ctx->data + ZIP_UNCOMPRESSED_SIZE_OFFSET,
|
||||
sizeof(uncompressed_size));
|
||||
|
||||
if (ctx->info.current_file.size != uncompressed_size) {
|
||||
/* Probably malformed archive*/
|
||||
LOG(LL_ERROR, ("Malformed archive"));
|
||||
ctx->status_msg = "Malformed archive";
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG(LL_DEBUG, ("File size: %u", (unsigned int) ctx->info.current_file.size));
|
||||
|
||||
uint16_t gen_flag;
|
||||
memcpy(&gen_flag, ctx->data + ZIP_GENFLAG_OFFSET, sizeof(gen_flag));
|
||||
ctx->current_file_has_descriptor = ((gen_flag & (1 << 3)) != 0);
|
||||
|
||||
LOG(LL_DEBUG, ("General flag=%d", (int) gen_flag));
|
||||
|
||||
memcpy(&ctx->current_file_crc, ctx->data + ZIP_CRC32_OFFSET,
|
||||
sizeof(ctx->current_file_crc));
|
||||
|
||||
LOG(LL_DEBUG, ("CRC32: 0x%08x", (unsigned int) ctx->current_file_crc));
|
||||
|
||||
context_remove_data(ctx, ZIP_LOCAL_HDR_SIZE + file_name_len + extras_len);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_manifest(struct update_context *ctx) {
|
||||
struct mgos_upd_info *info = &ctx->info;
|
||||
ctx->manifest_data = calloc(1, info->current_file.size);
|
||||
if (ctx->manifest_data == NULL) {
|
||||
ctx->status_msg = "Out of memory";
|
||||
return -1;
|
||||
}
|
||||
memcpy(ctx->manifest_data, ctx->data, info->current_file.size);
|
||||
|
||||
if (json_scanf(
|
||||
ctx->manifest_data, info->current_file.size,
|
||||
"{name: %T, platform: %T, version: %T, build_id: %T, parts: %T}",
|
||||
&info->name, &info->platform, &info->version, &info->build_id,
|
||||
&info->parts) <= 0) {
|
||||
ctx->status_msg = "Failed to parse manifest";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (info->platform.len == 0 || info->version.len == 0 ||
|
||||
info->build_id.len == 0 || info->parts.len == 0) {
|
||||
ctx->status_msg = "Required manifest field missing";
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG(LL_INFO,
|
||||
("FW: %.*s %.*s %s %s -> %.*s %.*s", (int) info->name.len, info->name.ptr,
|
||||
(int) info->platform.len, info->platform.ptr, build_version, build_id,
|
||||
(int) info->version.len, info->version.ptr, (int) info->build_id.len,
|
||||
info->build_id.ptr));
|
||||
|
||||
context_remove_data(ctx, info->current_file.size);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int finalize_write(struct update_context *ctx, struct mg_str tail) {
|
||||
/* We have to add the tail to CRC now to be able to verify it. */
|
||||
if (tail.len > 0) {
|
||||
ctx->current_file_crc_calc = cs_crc32(ctx->current_file_crc_calc,
|
||||
(const uint8_t *) tail.p, tail.len);
|
||||
}
|
||||
|
||||
if (ctx->current_file_crc != 0 &&
|
||||
ctx->current_file_crc != ctx->current_file_crc_calc) {
|
||||
LOG(LL_ERROR, ("Invalid CRC, want 0x%x, got 0x%x",
|
||||
(unsigned int) ctx->current_file_crc,
|
||||
(unsigned int) ctx->current_file_crc_calc));
|
||||
ctx->status_msg = "Invalid CRC";
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = mgos_upd_file_end(ctx->dev_ctx, &ctx->info.current_file, tail);
|
||||
if (ret != (int) tail.len) {
|
||||
if (ret < 0) {
|
||||
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
|
||||
} else {
|
||||
ctx->status_msg = "Not all data was processed";
|
||||
ret = -1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
context_remove_data(ctx, tail.len);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void mgos_updater_progress(struct update_context *ctx) {
|
||||
if (ctx->last_reported_bytes == 0 ||
|
||||
ctx->bytes_already_downloaded - ctx->last_reported_bytes >=
|
||||
PROGRESS_REPORT_BYTES ||
|
||||
mg_time() - ctx->last_reported_time > PROGRESS_REPORT_SECONDS) {
|
||||
if (ctx->zip_file_size > 0) {
|
||||
float ratio =
|
||||
(float) ctx->bytes_already_downloaded * 100.0f / ctx->zip_file_size;
|
||||
LOG(LL_INFO,
|
||||
("%.2f%% total, %s %d of %d", ratio, ctx->info.current_file.name,
|
||||
(int) ctx->info.current_file.processed,
|
||||
(int) ctx->info.current_file.size));
|
||||
} else {
|
||||
LOG(LL_INFO, ("%s %d of %d", ctx->info.current_file.name,
|
||||
(int) ctx->info.current_file.processed,
|
||||
(int) ctx->info.current_file.size));
|
||||
}
|
||||
/* If progress has been made, reset the update WDT. */
|
||||
if (ctx->last_reported_bytes != ctx->bytes_already_downloaded) {
|
||||
mgos_clear_timer(ctx->wdt);
|
||||
ctx->wdt = mgos_set_timer(ctx->wdt_timeout_ms, 0, updater_abort, ctx);
|
||||
}
|
||||
ctx->last_reported_bytes = ctx->bytes_already_downloaded;
|
||||
ctx->last_reported_time = mg_time();
|
||||
mgos_upd_trigger_ota_event();
|
||||
}
|
||||
}
|
||||
|
||||
static int updater_process_int(struct update_context *ctx, const char *data,
|
||||
size_t len) {
|
||||
int ret;
|
||||
if (len != 0) {
|
||||
context_update(ctx, data, len);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
switch (ctx->update_state) {
|
||||
case US_INITED: {
|
||||
updater_set_status(ctx, US_WAITING_MANIFEST_HEADER);
|
||||
} /* fall through */
|
||||
case US_WAITING_MANIFEST_HEADER: {
|
||||
if ((ret = parse_zip_file_header(ctx)) <= 0) {
|
||||
if (ret == 0) {
|
||||
context_save_unprocessed(ctx);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
if (strncmp(ctx->info.current_file.name, MANIFEST_FILENAME,
|
||||
sizeof(MANIFEST_FILENAME)) != 0) {
|
||||
/* We've got file header, but it isn't not metadata */
|
||||
LOG(LL_ERROR, ("Get %s instead of %s", ctx->info.current_file.name,
|
||||
MANIFEST_FILENAME));
|
||||
return -1;
|
||||
}
|
||||
updater_set_status(ctx, US_WAITING_MANIFEST);
|
||||
} /* fall through */
|
||||
case US_WAITING_MANIFEST: {
|
||||
/*
|
||||
* Assume metadata isn't too big and might be cached
|
||||
* otherwise we need streaming json-parser
|
||||
*/
|
||||
if (ctx->data_len < ctx->info.current_file.size) {
|
||||
context_save_unprocessed(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ctx->current_file_crc != 0 &&
|
||||
cs_crc32(0, (const uint8_t *) ctx->data,
|
||||
ctx->info.current_file.size) != ctx->current_file_crc) {
|
||||
ctx->status_msg = "Invalid CRC";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ((ret = parse_manifest(ctx)) < 0) return ret;
|
||||
|
||||
if (strncasecmp(ctx->info.platform.ptr,
|
||||
CS_STRINGIFY_MACRO(FW_ARCHITECTURE),
|
||||
strlen(CS_STRINGIFY_MACRO(FW_ARCHITECTURE))) != 0) {
|
||||
LOG(LL_ERROR,
|
||||
("Wrong platform: want \"%s\", got \"%s\"",
|
||||
CS_STRINGIFY_MACRO(FW_ARCHITECTURE), ctx->info.platform.ptr));
|
||||
ctx->status_msg = "Wrong platform";
|
||||
ctx->ota_state = MGOS_OTA_STATE_ERROR;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ctx->ignore_same_version &&
|
||||
strncmp(ctx->info.version.ptr, build_version,
|
||||
ctx->info.version.len) == 0 &&
|
||||
strncmp(ctx->info.build_id.ptr, build_id, ctx->info.build_id.len) ==
|
||||
0) {
|
||||
ctx->status_msg = "Version is the same as current";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ((ret = mgos_upd_begin(ctx->dev_ctx, &ctx->info.parts)) < 0) {
|
||||
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
|
||||
LOG(LL_ERROR, ("Bad manifest: %d %s", ret, ctx->status_msg));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ctx->info.abort = false;
|
||||
mgos_event_trigger(MGOS_EVENT_OTA_BEGIN, &ctx->info);
|
||||
if (ctx->info.abort) {
|
||||
ctx->status_msg = "OTA aborted by the MGOS_EVENT_OTA_BEGIN handler";
|
||||
return -1;
|
||||
}
|
||||
|
||||
context_clear_current_file(ctx);
|
||||
updater_set_status(ctx, US_WAITING_FILE_HEADER);
|
||||
} /* fall through */
|
||||
case US_WAITING_FILE_HEADER: {
|
||||
if (ctx->data_len < 4) {
|
||||
context_save_unprocessed(ctx);
|
||||
return 0;
|
||||
}
|
||||
if (memcmp(ctx->data, &c_zip_cdir_magic, 4) == 0) {
|
||||
LOG(LL_INFO, ("Reached the end of archive"));
|
||||
updater_set_status(ctx, US_WRITE_FINISHED);
|
||||
break;
|
||||
}
|
||||
if ((ret = parse_zip_file_header(ctx)) <= 0) {
|
||||
if (ret == 0) context_save_unprocessed(ctx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum mgos_upd_file_action r =
|
||||
mgos_upd_file_begin(ctx->dev_ctx, &ctx->info.current_file);
|
||||
|
||||
if (r == MGOS_UPDATER_ABORT) {
|
||||
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
|
||||
return -1;
|
||||
} else if (r == MGOS_UPDATER_SKIP_FILE) {
|
||||
updater_set_status(ctx, US_SKIPPING_DATA);
|
||||
break;
|
||||
}
|
||||
updater_set_status(ctx, US_WAITING_FILE);
|
||||
ctx->current_file_crc_calc = 0;
|
||||
ctx->last_reported_bytes = 0;
|
||||
} /* fall through */
|
||||
case US_WAITING_FILE: {
|
||||
while (true) {
|
||||
struct mg_str to_process = {
|
||||
.p = ctx->data,
|
||||
.len = MIN(MIN(ctx->info.current_file.size -
|
||||
ctx->info.current_file.processed,
|
||||
ctx->data_len),
|
||||
MGOS_UPDATER_DATA_CHUNK_SIZE),
|
||||
};
|
||||
if (to_process.len < MGOS_UPDATER_DATA_CHUNK_SIZE) break;
|
||||
int num_processed = mgos_upd_file_data(
|
||||
ctx->dev_ctx, &ctx->info.current_file, to_process);
|
||||
if (num_processed < 0) {
|
||||
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
|
||||
return num_processed;
|
||||
} else if (num_processed > 0) {
|
||||
ctx->current_file_crc_calc =
|
||||
cs_crc32(ctx->current_file_crc_calc,
|
||||
(const uint8_t *) to_process.p, num_processed);
|
||||
context_remove_data(ctx, num_processed);
|
||||
ctx->info.current_file.processed += num_processed;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
mgos_updater_progress(ctx);
|
||||
|
||||
uint32_t bytes_left =
|
||||
ctx->info.current_file.size - ctx->info.current_file.processed;
|
||||
if (bytes_left > ctx->data_len) {
|
||||
context_save_unprocessed(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct mg_str tail = {.p = ctx->data, .len = bytes_left};
|
||||
assert(tail.len < MGOS_UPDATER_DATA_CHUNK_SIZE);
|
||||
|
||||
if (finalize_write(ctx, tail) < 0) {
|
||||
return -1;
|
||||
}
|
||||
context_clear_current_file(ctx);
|
||||
updater_set_status(ctx, US_WAITING_FILE_HEADER);
|
||||
break;
|
||||
}
|
||||
case US_SKIPPING_DATA: {
|
||||
uint32_t to_skip =
|
||||
MIN(ctx->data_len,
|
||||
ctx->info.current_file.size - ctx->info.current_file.processed);
|
||||
ctx->info.current_file.processed += to_skip;
|
||||
context_remove_data(ctx, to_skip);
|
||||
mgos_updater_progress(ctx);
|
||||
|
||||
if (ctx->info.current_file.processed < ctx->info.current_file.size) {
|
||||
context_save_unprocessed(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
context_clear_current_file(ctx);
|
||||
updater_set_status(ctx, US_SKIPPING_DESCRIPTOR);
|
||||
} /* fall through */
|
||||
case US_SKIPPING_DESCRIPTOR: {
|
||||
bool has_descriptor = ctx->current_file_has_descriptor;
|
||||
LOG(LL_DEBUG, ("Has descriptor : %d", has_descriptor));
|
||||
context_clear_current_file(ctx);
|
||||
ctx->current_file_has_descriptor = false;
|
||||
if (has_descriptor) {
|
||||
/* If file has descriptor we have to skip 12 bytes after its body */
|
||||
ctx->info.current_file.size = ZIP_FILE_DESCRIPTOR_SIZE;
|
||||
updater_set_status(ctx, US_SKIPPING_DATA);
|
||||
} else {
|
||||
updater_set_status(ctx, US_WAITING_FILE_HEADER);
|
||||
}
|
||||
|
||||
context_save_unprocessed(ctx);
|
||||
break;
|
||||
}
|
||||
case US_WRITE_FINISHED: {
|
||||
/* We will stay in this state until explicitly finalized. */
|
||||
return 0;
|
||||
}
|
||||
case US_FINALIZE: {
|
||||
ret = 1;
|
||||
ctx->status_msg = "Update applied, finalizing";
|
||||
if (ctx->fctx.commit_timeout > 0) {
|
||||
if (!mgos_upd_set_commit_timeout(ctx->fctx.commit_timeout)) {
|
||||
ctx->status_msg = "Cannot save update status";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if ((ret = mgos_upd_finalize(ctx->dev_ctx)) < 0) {
|
||||
ctx->status_msg = mgos_upd_get_status_msg(ctx->dev_ctx);
|
||||
return ret;
|
||||
}
|
||||
ctx->result = 1;
|
||||
ctx->need_reboot = 1;
|
||||
updater_finish(ctx);
|
||||
break;
|
||||
}
|
||||
case US_FINISHED: {
|
||||
/* After receiving manifest, fw & fs just skipping all data */
|
||||
context_remove_data(ctx, ctx->data_len);
|
||||
if (ctx->result_cb != NULL) {
|
||||
ctx->result_cb(ctx);
|
||||
ctx->result_cb = NULL;
|
||||
}
|
||||
return ctx->result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int updater_process(struct update_context *ctx, const char *data, size_t len) {
|
||||
ctx->bytes_already_downloaded += len;
|
||||
ctx->result = updater_process_int(ctx, data, len);
|
||||
if (ctx->result != 0) {
|
||||
updater_finish(ctx);
|
||||
}
|
||||
return ctx->result;
|
||||
}
|
||||
|
||||
int updater_finalize(struct update_context *ctx) {
|
||||
if (ctx->update_state == US_FINISHED) {
|
||||
return -1;
|
||||
} else if (ctx->update_state != US_WRITE_FINISHED) {
|
||||
if (ctx->status_msg == NULL) ctx->status_msg = "Wrong state for finalize";
|
||||
return -1;
|
||||
}
|
||||
updater_set_status(ctx, US_FINALIZE);
|
||||
return updater_process(ctx, NULL, 0);
|
||||
}
|
||||
|
||||
void updater_finish(struct update_context *ctx) {
|
||||
updater_set_status(ctx, US_FINISHED);
|
||||
ctx->ota_state =
|
||||
ctx->result == 1 ? MGOS_OTA_STATE_SUCCESS : MGOS_OTA_STATE_ERROR;
|
||||
mgos_upd_trigger_ota_event();
|
||||
if (ctx->update_state == US_FINISHED) return;
|
||||
updater_process_int(ctx, NULL, 0);
|
||||
}
|
||||
|
||||
void updater_context_free(struct update_context *ctx) {
|
||||
if (!is_update_finished(ctx)) {
|
||||
LOG(LL_ERROR, ("Update terminated unexpectedly"));
|
||||
}
|
||||
if (ctx == s_ctx) s_ctx = NULL;
|
||||
mgos_clear_timer(ctx->wdt);
|
||||
mgos_upd_hal_ctx_free(ctx->dev_ctx);
|
||||
mbuf_free(&ctx->unprocessed);
|
||||
free(ctx->manifest_data);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
void bin2hex(const uint8_t *src, int src_len, char *dst) {
|
||||
int i = 0;
|
||||
for (i = 0; i < src_len; i++) {
|
||||
sprintf(dst, "%02x", (int) *src);
|
||||
dst += 2;
|
||||
src += 1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool file_copy(const char *old_path, const char *new_path,
|
||||
const char *name, char tmp_name[MG_MAX_PATH]) {
|
||||
bool ret = false;
|
||||
FILE *old_f = NULL, *new_f = NULL;
|
||||
struct stat st;
|
||||
int readen, to_read = 0, total = 0;
|
||||
|
||||
LOG(LL_INFO, ("Copying %s", name));
|
||||
|
||||
sprintf(tmp_name, "%s/%s", old_path, name);
|
||||
old_f = fopen(tmp_name, "r");
|
||||
if (old_f == NULL) {
|
||||
LOG(LL_ERROR, ("Failed to open %s for reading", tmp_name));
|
||||
goto out;
|
||||
}
|
||||
if (stat(tmp_name, &st) != 0) {
|
||||
LOG(LL_ERROR, ("Cannot get previous %s size", tmp_name));
|
||||
goto out;
|
||||
}
|
||||
|
||||
sprintf(tmp_name, "%s/%s", new_path, name);
|
||||
new_f = fopen(tmp_name, "w");
|
||||
if (new_f == NULL) {
|
||||
LOG(LL_ERROR, ("Failed to open %s for writing", tmp_name));
|
||||
goto out;
|
||||
}
|
||||
|
||||
char buf[128];
|
||||
to_read = MIN(sizeof(buf), (size_t) st.st_size);
|
||||
while (to_read != 0) {
|
||||
if ((readen = fread(buf, 1, to_read, old_f)) < 0) {
|
||||
LOG(LL_ERROR, ("Failed to read %d bytes from %s", to_read, name));
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (fwrite(buf, 1, readen, new_f) != (size_t) readen) {
|
||||
LOG(LL_ERROR, ("Failed to write %d bytes to %s", readen, name));
|
||||
goto out;
|
||||
}
|
||||
|
||||
total += readen;
|
||||
to_read = MIN(sizeof(buf), (size_t)(st.st_size - total));
|
||||
}
|
||||
|
||||
LOG(LL_DEBUG, ("Wrote %d to %s", total, tmp_name));
|
||||
|
||||
ret = true;
|
||||
|
||||
out:
|
||||
if (old_f != NULL) fclose(old_f);
|
||||
if (new_f != NULL) {
|
||||
fclose(new_f);
|
||||
if (!ret) remove(tmp_name);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool mgos_upd_merge_fs(const char *old_fs_path, const char *new_fs_path) {
|
||||
bool ret = false;
|
||||
DIR *dir = opendir(old_fs_path);
|
||||
if (dir == NULL) {
|
||||
LOG(LL_ERROR, ("Failed to open root directory"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
struct dirent *de;
|
||||
while ((de = readdir(dir)) != NULL) {
|
||||
struct stat st;
|
||||
char tmp_name[MG_MAX_PATH + 1];
|
||||
sprintf(tmp_name, "%s/%s", new_fs_path, de->d_name);
|
||||
if (stat(tmp_name, &st) != 0) {
|
||||
/* File not found on the new fs, copy. */
|
||||
if (!file_copy(old_fs_path, new_fs_path, de->d_name, tmp_name)) {
|
||||
LOG(LL_ERROR, ("Failed to copy %s", de->d_name));
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
mgos_wdt_feed();
|
||||
}
|
||||
ret = true;
|
||||
|
||||
out:
|
||||
if (dir != NULL) closedir(dir);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool mgos_upd_commit() {
|
||||
if (mgos_upd_is_committed()) return false;
|
||||
mgos_upd_boot_commit();
|
||||
remove(UPDATER_CTX_FILE_NAME);
|
||||
mgos_upd_trigger_ota_event();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mgos_upd_is_committed() {
|
||||
struct mgos_upd_boot_state s;
|
||||
if (!mgos_upd_boot_get_state(&s)) return false;
|
||||
return s.is_committed;
|
||||
}
|
||||
|
||||
bool mgos_upd_revert(bool reboot) {
|
||||
if (mgos_upd_is_committed()) return false;
|
||||
mgos_upd_boot_revert();
|
||||
if (reboot) mgos_system_restart();
|
||||
return true;
|
||||
}
|
||||
|
||||
void mgos_upd_watchdog_cb(void *arg) {
|
||||
if (!mgos_upd_is_committed()) {
|
||||
/* Timer fired and update has not been committed. Revert! */
|
||||
LOG(LL_ERROR, ("Update commit timeout expired"));
|
||||
mgos_upd_revert(true /* reboot */);
|
||||
}
|
||||
(void) arg;
|
||||
}
|
||||
|
||||
int mgos_upd_get_commit_timeout() {
|
||||
size_t len;
|
||||
char *data = cs_read_file(UPDATER_CTX_FILE_NAME, &len);
|
||||
if (data == NULL) return 0;
|
||||
struct update_file_context *fctx = (struct update_file_context *) data;
|
||||
LOG(LL_INFO, ("Update state: %d", fctx->commit_timeout));
|
||||
int res = fctx->commit_timeout;
|
||||
free(data);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool mgos_upd_set_commit_timeout(int commit_timeout) {
|
||||
bool ret = false;
|
||||
LOG(LL_DEBUG, ("Writing update state to %s", UPDATER_CTX_FILE_NAME));
|
||||
FILE *fp = fopen(UPDATER_CTX_FILE_NAME, "w");
|
||||
if (fp == NULL) return false;
|
||||
struct update_file_context fctx;
|
||||
fctx.commit_timeout = commit_timeout;
|
||||
if (fwrite(&fctx, sizeof(fctx), 1, fp) == 1) {
|
||||
ret = true;
|
||||
}
|
||||
fclose(fp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void mgos_upd_boot_finish(bool is_successful, bool is_first) {
|
||||
/*
|
||||
* If boot is not successful, there's only one thing to do:
|
||||
* revert update (if any) and reboot.
|
||||
* If this was the first boot after an update, this will revert it.
|
||||
*/
|
||||
LOG(LL_DEBUG, ("%d %d", is_successful, is_first));
|
||||
if (!is_first) return;
|
||||
if (!is_successful) {
|
||||
mgos_upd_revert(true /* reboot */);
|
||||
/* Not reached */
|
||||
return;
|
||||
}
|
||||
/* We booted. Now see if we have any special instructions. */
|
||||
int commit_timeout = mgos_upd_get_commit_timeout();
|
||||
if (commit_timeout > 0) {
|
||||
LOG(LL_INFO, ("Arming commit watchdog for %d seconds", commit_timeout));
|
||||
mgos_set_timer(commit_timeout * 1000, 0 /* repeat */, mgos_upd_watchdog_cb,
|
||||
NULL);
|
||||
} else {
|
||||
mgos_upd_commit();
|
||||
}
|
||||
mgos_upd_trigger_ota_event();
|
||||
}
|
||||
|
||||
const char *mgos_ota_state_str(enum mgos_ota_state state) {
|
||||
switch (state) {
|
||||
case MGOS_OTA_STATE_IDLE:
|
||||
return "idle";
|
||||
case MGOS_OTA_STATE_PROGRESS:
|
||||
return "progress";
|
||||
case MGOS_OTA_STATE_ERROR:
|
||||
return "error";
|
||||
case MGOS_OTA_STATE_SUCCESS:
|
||||
return "success";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/* For FFI */
|
||||
const char *mgos_ota_status_get_msg(struct mgos_ota_status *s) {
|
||||
return s->msg;
|
||||
}
|
||||
|
||||
bool mgos_upd_get_status(struct mgos_ota_status *s) {
|
||||
struct mgos_upd_boot_state bs;
|
||||
memset(s, 0, sizeof(*s));
|
||||
if (s_ctx != NULL) {
|
||||
s->state = s_ctx->ota_state;
|
||||
s->msg = s_ctx->status_msg;
|
||||
if (s_ctx->zip_file_size > 0) {
|
||||
s->progress_percent =
|
||||
s_ctx->bytes_already_downloaded * 100.0 / s_ctx->zip_file_size;
|
||||
}
|
||||
}
|
||||
if (s->msg == NULL) s->msg = mgos_ota_state_str(s->state);
|
||||
if (!mgos_upd_boot_get_state(&bs)) return false;
|
||||
s->is_committed = bs.is_committed;
|
||||
s->partition = bs.active_slot;
|
||||
s->commit_timeout = mgos_upd_get_commit_timeout();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mgos_ota_common_init(void) {
|
||||
return true;
|
||||
}
|
370
libs/ota-common/src/stm32/stm32_updater.c
Normal file
370
libs/ota-common/src/stm32/stm32_updater.c
Normal file
@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2018 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "common/cs_crc32.h"
|
||||
#include "common/cs_dbg.h"
|
||||
#include "common/str_util.h"
|
||||
|
||||
#include "frozen.h"
|
||||
|
||||
#include "mgos_boot_cfg.h"
|
||||
#include "mgos_vfs.h"
|
||||
#include "mgos_vfs_dev.h"
|
||||
#include "mgos_updater_hal.h"
|
||||
#include "mgos_updater_util.h"
|
||||
|
||||
#include "stm32_vfs_dev_flash.h"
|
||||
|
||||
struct mgos_upd_hal_ctx {
|
||||
const char *status_msg;
|
||||
struct mgos_vfs_dev *app_dev, *fs_dev, *bl_dev;
|
||||
struct mg_str app_file_name, app_cs_sha1;
|
||||
unsigned int app_bl_size, app_bl_cfg_size, app_fs_size, update_bl;
|
||||
struct mg_str fs_file_name, fs_cs_sha1;
|
||||
uintptr_t app_org;
|
||||
size_t file_offset, app_bl_cfg_offset, app_fs_offset, app_app_offset;
|
||||
uint32_t app_len, app_crc32;
|
||||
struct mgos_vfs_dev *cur_dev;
|
||||
size_t cur_dev_num_erased;
|
||||
int8_t dst_slot;
|
||||
};
|
||||
|
||||
struct mgos_upd_hal_ctx *mgos_upd_hal_ctx_create(void) {
|
||||
struct mgos_upd_hal_ctx *ctx =
|
||||
(struct mgos_upd_hal_ctx *) calloc(1, sizeof(*ctx));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
const char *mgos_upd_get_status_msg(struct mgos_upd_hal_ctx *ctx) {
|
||||
return ctx->status_msg;
|
||||
}
|
||||
|
||||
int mgos_upd_begin(struct mgos_upd_hal_ctx *ctx, struct json_token *parts) {
|
||||
const struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
|
||||
if (bcfg == NULL) return -1;
|
||||
struct json_token app_file_name = JSON_INVALID_TOKEN,
|
||||
app_cs_sha1 = JSON_INVALID_TOKEN;
|
||||
struct json_token fs_file_name = JSON_INVALID_TOKEN,
|
||||
fs_cs_sha1 = JSON_INVALID_TOKEN;
|
||||
json_scanf(parts->ptr, parts->len,
|
||||
"{app: {src: %T, cs_sha1: %T, bl_size: %u, bl_cfg_size: %u, "
|
||||
" fs_size: %u, update_bl: %B}, "
|
||||
"fs: {src: %T, cs_sha1: %T}}",
|
||||
&app_file_name, &app_cs_sha1, &ctx->app_bl_size,
|
||||
&ctx->app_bl_cfg_size, &ctx->app_fs_size, &ctx->update_bl,
|
||||
&fs_file_name, &fs_cs_sha1);
|
||||
if (app_file_name.len == 0 || app_cs_sha1.len == 0 ||
|
||||
(ctx->update_bl && ctx->app_bl_size == 0)) {
|
||||
ctx->status_msg = "Incomplete update package";
|
||||
return -2;
|
||||
}
|
||||
ctx->app_file_name = mg_mk_str_n(app_file_name.ptr, app_file_name.len);
|
||||
ctx->app_cs_sha1 = mg_mk_str_n(app_cs_sha1.ptr, app_cs_sha1.len);
|
||||
ctx->fs_file_name = mg_mk_str_n(fs_file_name.ptr, fs_file_name.len);
|
||||
ctx->fs_cs_sha1 = mg_mk_str_n(fs_cs_sha1.ptr, fs_cs_sha1.len);
|
||||
ctx->app_bl_cfg_offset = ctx->app_bl_size;
|
||||
ctx->app_fs_offset = ctx->app_bl_cfg_offset + ctx->app_bl_cfg_size;
|
||||
ctx->app_app_offset = ctx->app_fs_offset + ctx->app_fs_size;
|
||||
ctx->app_org = FLASH_BASE + ctx->app_app_offset;
|
||||
/* Try to put into a directly bootable slot, if possible. */
|
||||
ctx->dst_slot =
|
||||
mgos_boot_cfg_find_slot(bcfg, ctx->app_org, true /* want_fs */, -1, -1);
|
||||
if (ctx->dst_slot < 0) {
|
||||
/* Ok, try any available slot, boot loader will perform a swap. */
|
||||
ctx->dst_slot = mgos_boot_cfg_find_slot(bcfg, 0 /* map_addr */,
|
||||
true /* want_fs */, -1, -1);
|
||||
}
|
||||
if (ctx->dst_slot < 0) {
|
||||
ctx->status_msg = "No slots available for update";
|
||||
return -3;
|
||||
}
|
||||
const struct mgos_boot_slot *sl = &bcfg->slots[ctx->dst_slot];
|
||||
LOG(LL_INFO, ("Picked slot %d, app -> %s, FS -> %s", ctx->dst_slot,
|
||||
sl->cfg.app_dev, sl->cfg.fs_dev));
|
||||
ctx->app_dev = mgos_vfs_dev_open(sl->cfg.app_dev);
|
||||
if (ctx->app_dev == NULL) {
|
||||
ctx->status_msg = "Failed to open app_dev";
|
||||
return -4;
|
||||
}
|
||||
ctx->fs_dev = mgos_vfs_dev_open(sl->cfg.fs_dev);
|
||||
if (ctx->fs_dev == NULL) {
|
||||
ctx->status_msg = "Failed to open fs_dev";
|
||||
return -5;
|
||||
}
|
||||
if (ctx->update_bl) {
|
||||
ctx->bl_dev = mgos_vfs_dev_create(MGOS_VFS_DEV_TYPE_STM32_FLASH, NULL);
|
||||
if (ctx->bl_dev == NULL ||
|
||||
!stm32_flash_dev_init(ctx->bl_dev, 0, ctx->app_bl_size,
|
||||
false /* ese */)) {
|
||||
ctx->status_msg = "Failed to open bl_dev";
|
||||
return -6;
|
||||
}
|
||||
}
|
||||
LOG(LL_INFO, ("BL size %u (update? %d) + %u cfg; FS size %u; app org 0x%lx",
|
||||
ctx->app_bl_size, ctx->update_bl, ctx->app_bl_cfg_size,
|
||||
ctx->app_fs_size, (unsigned long) ctx->app_org));
|
||||
/* To simplify logic while writing. */
|
||||
if (ctx->app_bl_size % MGOS_UPDATER_DATA_CHUNK_SIZE != 0 ||
|
||||
ctx->app_bl_cfg_size % MGOS_UPDATER_DATA_CHUNK_SIZE != 0 ||
|
||||
ctx->app_fs_size % MGOS_UPDATER_DATA_CHUNK_SIZE != 0) {
|
||||
ctx->status_msg = "Invalid size";
|
||||
return -7;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
enum mgos_upd_file_action mgos_upd_file_begin(
|
||||
struct mgos_upd_hal_ctx *ctx, const struct mgos_upd_file_info *fi) {
|
||||
enum mgos_upd_file_action res = MGOS_UPDATER_SKIP_FILE;
|
||||
ctx->file_offset = 0;
|
||||
if (mg_vcmp(&ctx->app_file_name, fi->name) == 0) {
|
||||
if (fi->size < ctx->app_app_offset) {
|
||||
ctx->status_msg = "App file too short";
|
||||
res = MGOS_UPDATER_ABORT;
|
||||
goto out;
|
||||
}
|
||||
ctx->app_len = fi->size - ctx->app_app_offset;
|
||||
res = MGOS_UPDATER_PROCESS_FILE;
|
||||
} else if (mg_vcmp(&ctx->fs_file_name, fi->name) == 0) {
|
||||
res = MGOS_UPDATER_PROCESS_FILE;
|
||||
}
|
||||
out:
|
||||
return res;
|
||||
}
|
||||
|
||||
int mgos_upd_file_data(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi,
|
||||
struct mg_str data) {
|
||||
int res = -1;
|
||||
size_t write_offset = 0;
|
||||
struct mgos_vfs_dev *dev = NULL;
|
||||
if (mg_vcmp(&ctx->app_file_name, fi->name) == 0) {
|
||||
if (ctx->file_offset < ctx->app_bl_size) {
|
||||
/* Boot loader. Write if instructed. */
|
||||
if (ctx->update_bl) {
|
||||
dev = ctx->bl_dev;
|
||||
write_offset = ctx->file_offset;
|
||||
}
|
||||
} else if (ctx->file_offset < ctx->app_fs_offset) {
|
||||
/* Boot loader config is never updated during OTA. */
|
||||
} else if (ctx->file_offset < ctx->app_app_offset) {
|
||||
/* FS image - write unless we have a separate file.
|
||||
* Generally speaking it should be one or the other, not both. */
|
||||
if (ctx->fs_file_name.len == 0) {
|
||||
dev = ctx->fs_dev;
|
||||
write_offset = ctx->file_offset - ctx->app_fs_offset;
|
||||
}
|
||||
} else {
|
||||
/* This is app. */
|
||||
dev = ctx->app_dev;
|
||||
write_offset = ctx->file_offset - ctx->app_app_offset;
|
||||
ctx->app_crc32 =
|
||||
cs_crc32(ctx->app_crc32, (const uint8_t *) data.p, data.len);
|
||||
}
|
||||
} else if (mg_vcmp(&ctx->fs_file_name, fi->name) == 0) {
|
||||
dev = ctx->fs_dev;
|
||||
}
|
||||
if (dev != ctx->cur_dev) {
|
||||
ctx->cur_dev = dev;
|
||||
ctx->cur_dev_num_erased = 0;
|
||||
}
|
||||
LOG(LL_DEBUG,
|
||||
("fn %s ds %d fo %d | %d %d %d | dev %s wo %d", fi->name, (int) data.len,
|
||||
(int) ctx->file_offset, (int) ctx->app_bl_size, (int) ctx->app_fs_offset,
|
||||
(int) ctx->app_app_offset, (dev ? dev->name : "-"), (int) write_offset));
|
||||
if (dev != NULL) {
|
||||
/* See if we need to erase before writing. */
|
||||
size_t write_end = write_offset + data.len;
|
||||
if (write_end > ctx->cur_dev_num_erased) {
|
||||
size_t erase_len = 0;
|
||||
size_t erase_sizes[MGOS_VFS_DEV_NUM_ERASE_SIZES];
|
||||
size_t dev_size = mgos_vfs_dev_get_size(dev);
|
||||
size_t headroom = dev_size - ctx->cur_dev_num_erased;
|
||||
if (mgos_vfs_dev_get_erase_sizes(dev, erase_sizes) == 0) {
|
||||
erase_len = write_end - ctx->cur_dev_num_erased;
|
||||
/* Use the largest erase size smaller than the remaining space ahead. */
|
||||
for (int i = 0; i < (int) ARRAY_SIZE(erase_sizes); i++) {
|
||||
if (erase_sizes[i] > 0 && erase_sizes[i] < headroom) {
|
||||
erase_len = erase_sizes[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Just nuke the whole thing */
|
||||
erase_len = headroom;
|
||||
}
|
||||
LOG(LL_DEBUG, ("Erase %s %d @ 0x%lx", dev->name, (int) erase_len,
|
||||
(unsigned long) ctx->cur_dev_num_erased));
|
||||
enum mgos_vfs_dev_err eres =
|
||||
mgos_vfs_dev_erase(dev, ctx->cur_dev_num_erased, erase_len);
|
||||
if (eres != 0) {
|
||||
ctx->status_msg = "Erase failed";
|
||||
LOG(LL_INFO,
|
||||
("%s: erase %d failed: %d", dev->name, (int) erase_len, eres));
|
||||
goto out;
|
||||
}
|
||||
ctx->cur_dev_num_erased += erase_len;
|
||||
}
|
||||
enum mgos_vfs_dev_err wres =
|
||||
mgos_vfs_dev_write(dev, write_offset, data.len, data.p);
|
||||
if (wres < 0) {
|
||||
LOG(LL_ERROR,
|
||||
("%s @ %d => wr %s: %d @ %d = %d", fi->name, (int) ctx->file_offset,
|
||||
dev->name, (int) data.len, (int) write_offset, wres));
|
||||
res = wres;
|
||||
} else {
|
||||
res = (int) data.len;
|
||||
}
|
||||
} else {
|
||||
res = (int) data.len; /* Skip */
|
||||
}
|
||||
out:
|
||||
ctx->file_offset += data.len;
|
||||
return res;
|
||||
}
|
||||
|
||||
int mgos_upd_file_end(struct mgos_upd_hal_ctx *ctx,
|
||||
const struct mgos_upd_file_info *fi, struct mg_str tail) {
|
||||
int res = -1;
|
||||
if (tail.len > 0) {
|
||||
int wres = mgos_upd_file_data(ctx, fi, tail);
|
||||
if (wres != (int) tail.len) {
|
||||
res = wres;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
/* TODO(rojer): Verify SHA1. */
|
||||
res = (int) tail.len;
|
||||
out:
|
||||
return res;
|
||||
}
|
||||
|
||||
int mgos_upd_finalize(struct mgos_upd_hal_ctx *ctx) {
|
||||
int res = -1;
|
||||
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
|
||||
struct mgos_boot_slot_state *ss;
|
||||
if (bcfg == NULL) goto out;
|
||||
bcfg->revert_slot = bcfg->active_slot;
|
||||
bcfg->active_slot = ctx->dst_slot;
|
||||
bcfg->flags &= ~(MGOS_BOOT_F_COMMITTED);
|
||||
bcfg->flags |= (MGOS_BOOT_F_FIRST_BOOT_A | MGOS_BOOT_F_FIRST_BOOT_B);
|
||||
bcfg->flags |= (MGOS_BOOT_F_MERGE_FS);
|
||||
ss = &bcfg->slots[ctx->dst_slot].state;
|
||||
ss->app_len = ctx->app_len;
|
||||
ss->app_org = ctx->app_org;
|
||||
ss->app_crc32 = ctx->app_crc32;
|
||||
ss->app_flags = 0;
|
||||
LOG(LL_INFO, ("Updating boot config"));
|
||||
res = (mgos_boot_cfg_write(bcfg, true /* dump */) ? 1 : -1);
|
||||
|
||||
out:
|
||||
return res;
|
||||
}
|
||||
|
||||
void mgos_upd_hal_ctx_free(struct mgos_upd_hal_ctx *ctx) {
|
||||
if (ctx == NULL) return;
|
||||
mgos_vfs_dev_close(ctx->app_dev);
|
||||
mgos_vfs_dev_close(ctx->fs_dev);
|
||||
mgos_vfs_dev_close(ctx->bl_dev);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
int mgos_upd_create_snapshot() {
|
||||
/* TODO */
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool mgos_upd_boot_get_state(struct mgos_upd_boot_state *bs) {
|
||||
const struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
|
||||
if (bcfg == NULL) return false;
|
||||
memset(bs, 0, sizeof(*bs));
|
||||
bs->active_slot = bcfg->active_slot;
|
||||
bs->revert_slot = bcfg->revert_slot;
|
||||
bs->is_committed = !!(bcfg->flags & MGOS_BOOT_F_COMMITTED);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mgos_upd_boot_set_state(const struct mgos_upd_boot_state *bs) {
|
||||
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
|
||||
if (bcfg == NULL) return false;
|
||||
bcfg->active_slot = bs->active_slot;
|
||||
bcfg->revert_slot = bs->revert_slot;
|
||||
if (bs->is_committed) {
|
||||
bcfg->flags |= MGOS_BOOT_F_COMMITTED;
|
||||
} else {
|
||||
bcfg->flags &= ~(MGOS_BOOT_F_COMMITTED);
|
||||
}
|
||||
return mgos_boot_cfg_write(bcfg, true /* dump */);
|
||||
}
|
||||
|
||||
int mgos_upd_apply_update(void) {
|
||||
int res = -1;
|
||||
struct mgos_vfs_dev *old_fs_dev = NULL;
|
||||
const struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
|
||||
if (bcfg == NULL) goto out;
|
||||
if (bcfg->revert_slot < 0) {
|
||||
LOG(LL_ERROR, ("Revert slot not set!"));
|
||||
goto out;
|
||||
}
|
||||
if (!mgos_vfs_mount_dev_name("/old",
|
||||
bcfg->slots[bcfg->revert_slot].cfg.fs_dev,
|
||||
CS_STRINGIFY_MACRO(MGOS_ROOT_FS_TYPE),
|
||||
CS_STRINGIFY_MACRO(MGOS_ROOT_FS_OPTS))) {
|
||||
LOG(LL_ERROR, ("Failed to mount old file system"));
|
||||
goto out;
|
||||
}
|
||||
res = (mgos_upd_merge_fs("/old", "/") ? 0 : -2);
|
||||
mgos_vfs_umount("/old");
|
||||
out:
|
||||
mgos_vfs_dev_close(old_fs_dev);
|
||||
return res;
|
||||
}
|
||||
|
||||
static bool mgos_boot_commit_slot(struct mgos_boot_cfg *bcfg, int8_t slot) {
|
||||
bcfg->active_slot = slot;
|
||||
bcfg->revert_slot = -1;
|
||||
bcfg->flags |= MGOS_BOOT_F_COMMITTED;
|
||||
bcfg->flags &= ~(MGOS_BOOT_F_FIRST_BOOT_A | MGOS_BOOT_F_MERGE_FS);
|
||||
return (mgos_boot_cfg_write(bcfg, false /* dump */));
|
||||
}
|
||||
|
||||
void mgos_upd_boot_commit(void) {
|
||||
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
|
||||
if (bcfg == NULL) return;
|
||||
int8_t active_slot = bcfg->active_slot;
|
||||
if (mgos_boot_commit_slot(bcfg, active_slot)) {
|
||||
LOG(LL_INFO, ("Committed slot %d", active_slot));
|
||||
}
|
||||
}
|
||||
|
||||
void mgos_upd_boot_revert(void) {
|
||||
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
|
||||
if (bcfg == NULL) return;
|
||||
int8_t revert_slot = bcfg->revert_slot;
|
||||
if (mgos_boot_commit_slot(bcfg, revert_slot)) {
|
||||
LOG(LL_INFO, ("Reverted to slot %d", revert_slot));
|
||||
}
|
||||
}
|
||||
|
||||
bool mgos_upd_is_first_boot(void) {
|
||||
struct mgos_boot_cfg *bcfg = mgos_boot_cfg_get();
|
||||
if (bcfg == NULL) return false;
|
||||
return (bcfg->flags & MGOS_BOOT_F_FIRST_BOOT_A) != 0;
|
||||
}
|
14
libs/ota-http-client/LICENSE
Normal file
14
libs/ota-http-client/LICENSE
Normal file
@ -0,0 +1,14 @@
|
||||
Copyright (c) 2018 Cesanta Software Limited
|
||||
All rights reserved
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
26
libs/ota-http-client/README.md
Normal file
26
libs/ota-http-client/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Implementation of Mongoose OS OTA HTTP client
|
||||
|
||||
This library adds a device configuration section called `update`, where
|
||||
a device could be configured to poll a specified HTTP URL for a new
|
||||
app firmware.
|
||||
|
||||
Also, this library adds a C API to fetch a new firmware from the given
|
||||
URL and update programmatically.
|
||||
|
||||
## Configuration section
|
||||
|
||||
The library adds the following object to the device configuration:
|
||||
|
||||
|
||||
```javascript
|
||||
"update": {
|
||||
"commit_timeout": 0, // OTA commit timeout
|
||||
"url": "", // HTTP URL to poll
|
||||
"interval": 0, // Polling interval
|
||||
"ssl_ca_file": "ca.pem", // TLS CA cert file
|
||||
"ssl_client_cert_file": "", // TLS cert file
|
||||
"ssl_server_name": "", // TLS server name
|
||||
"enable_post": true
|
||||
}
|
||||
```
|
||||
|
@ -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 */
|
||||
|
6
libs/ota-http-client/mjs_fs/api_ota.js
Normal file
6
libs/ota-http-client/mjs_fs/api_ota.js
Normal file
@ -0,0 +1,6 @@
|
||||
let OTA = {
|
||||
// ## **`OTA.evdataOtaStatusMsg(evdata)`**
|
||||
// Getter function for the `evdata` given to the event callback for the event
|
||||
// `Event.OTA_STATUS`, see `Event.addHandler()` in `api_events.js`.
|
||||
evdataOtaStatusMsg: ffi('char *mgos_ota_status_get_msg(void *)'),
|
||||
};
|
@ -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
|
||||
|
@ -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 */
|
||||
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2014-2016 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*/
|
||||
|
||||
#ifndef CS_MOS_LIBS_OTA_HTTP_SERVER_SRC_MGOS_OTA_HTTP_SERVER_H_
|
||||
#define CS_MOS_LIBS_OTA_HTTP_SERVER_SRC_MGOS_OTA_HTTP_SERVER_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#if MGOS_ENABLE_UPDATER
|
||||
bool mgos_ota_http_server_init(void);
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif /* __cplusplus */
|
||||
|
||||
#endif /* CS_MOS_LIBS_OTA_HTTP_SERVER_SRC_MGOS_OTA_HTTP_SERVER_H_ */
|
@ -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
|
@ -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;
|
||||
}
|
14
libs/rpc-service-ota/LICENSE
Normal file
14
libs/rpc-service-ota/LICENSE
Normal file
@ -0,0 +1,14 @@
|
||||
Copyright (c) 2018 Cesanta Software Limited
|
||||
All rights reserved
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the ""License"");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an ""AS IS"" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
89
libs/rpc-service-ota/README.md
Normal file
89
libs/rpc-service-ota/README.md
Normal file
@ -0,0 +1,89 @@
|
||||
# RPC Service - OTA (Over The Air updates)
|
||||
|
||||
This service provides an ability to manage OTA on devices remotely.
|
||||
It is possible to call this service programmatically via serial, HTTP/RESTful,
|
||||
Websocket, MQTT or other transports
|
||||
(see [RPC section](/docs/mos/userguide/rpc.md)) or via the `mos` tool.
|
||||
|
||||
See in-depth description of our OTA mechanism at
|
||||
[Updating firmware reliably - embedded.com](http://www.embedded.com/design/prototyping-and-development/4443082/Updating-firmware-reliably).
|
||||
|
||||
See OTA video tutorial:
|
||||
|
||||
<iframe width="560" src="https://www.youtube.com/embed/o05sBDfaFO8"
|
||||
height="315" align="center" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
Below is a list of exported RPC methods and arguments:
|
||||
|
||||
## OTA.Update
|
||||
Trigger OTA firmware update. Arguments:
|
||||
```javascript
|
||||
{
|
||||
"url": "https://foo.com/fw123.zip", // Required. URL to the new firmware.
|
||||
"commit_timeout": "300" // Optional. Time frame in seconds to do
|
||||
// OTA.Commit after reboot. If commit is
|
||||
// not done during the timeout, OTA rolls back.
|
||||
}
|
||||
```
|
||||
|
||||
A new firmware gets downloaded to the separate flash partition,
|
||||
and is marked dirty. When the download is complete, device is rebooted.
|
||||
After reboot, a firmware partition could become committed by calling
|
||||
`OTA.Commit` - in which case, it is marked as "good". Otherwise, a device
|
||||
reboots back into the old firmware after the `commit_timeout` seconds.
|
||||
Example usage:
|
||||
|
||||
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.Update '{"url": "http://1.2.3.4/fw.zip", "commit_timeout": 300}'</code></pre>
|
||||
|
||||
|
||||
## OTA.Commit
|
||||
Commit current firmware. Arguments: none.
|
||||
|
||||
Example usage:
|
||||
|
||||
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.Commit</code></pre>
|
||||
|
||||
|
||||
## OTA.Revert
|
||||
Rolls back to the previous firmware. Arguments: none.
|
||||
|
||||
Example usage:
|
||||
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.Revert</code></pre>
|
||||
|
||||
|
||||
## OTA.CreateSnapshot
|
||||
Create new firmware patition with the copy of currently running firmware. Arguments:
|
||||
```javascript
|
||||
{
|
||||
// Optional. If true, then current firmware is uncommited, and needs to
|
||||
// be explicitly commited after the first reboot. Otherwise, it'll reboot
|
||||
// into the created snapshot. This option is useful if a dangerous, risky
|
||||
// live update is to be done on the living device. Then, if the update
|
||||
// fails and device bricks, it'll revert to the created good snapshot.
|
||||
"set_as_revert": false,
|
||||
// Optional. Same meaning as for OTA.Update
|
||||
"commit_timeout": "300"
|
||||
}
|
||||
```
|
||||
|
||||
Example usage:
|
||||
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.CreateSnapshot</code></pre>
|
||||
|
||||
|
||||
## OTA.GetBootState
|
||||
Get current boot state. Arguments: none.
|
||||
|
||||
Example usage:
|
||||
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.GetBootState
|
||||
{
|
||||
"active_slot": 0, # Currently active flash partition.
|
||||
"is_committed": true, # Current firmware is marked as "good" (committed).
|
||||
"revert_slot": 0, # If uncommitted, slot to roll back to.
|
||||
"commit_timeout": 0 # Commit timeout.
|
||||
}</code></pre>
|
||||
|
||||
## OTA.SetBootState
|
||||
Get current boot state. Arguments: see `OTA.GetBootState` reply section.
|
||||
|
||||
Example usage:
|
||||
<pre class="command-line language-bash" data-user="chris" data-host="localhost" data-output="2-100"><code>mos call OTA.SetBootState '{"revert_slot": 1}'</code></pre>
|
@ -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_
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user