Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified build/TianShanOS.bin
Binary file not shown.
Binary file modified build/bootloader/bootloader.bin
Binary file not shown.
Binary file modified build/www.bin
Binary file not shown.
61 changes: 61 additions & 0 deletions components/ts_ota/include/ts_ota.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,32 @@ esp_err_t ts_ota_deinit(void);
*/
esp_err_t ts_ota_start(const ts_ota_config_t *config);

/**
* @brief Synchronously flash app firmware from a local file
*
* This call only returns ESP_OK after the firmware image has been written to
* the next OTA partition and that partition has been selected for next boot.
*
* @param filepath Path to TianShanOS.bin
* @param allow_downgrade Allow flashing a version lower than the running app
* @param progress_cb Progress callback (optional)
* @param user_data User data for callback
* @return ESP_OK when the app partition write has completed successfully
*/
esp_err_t ts_ota_flash_file_sync(const char *filepath,
bool allow_downgrade,
ts_ota_progress_cb_t progress_cb,
void *user_data);

/**
* @brief Check if SD card/local-file app OTA is running
*
* Covers both asynchronous SD-card OTA and synchronous local-file flashing.
*
* @return true when app partition is being written from a local file
*/
bool ts_ota_sdcard_is_running(void);

/**
* @brief Abort ongoing OTA update
*
Expand Down Expand Up @@ -399,6 +425,13 @@ bool ts_ota_https_is_running(void);
// WWW Partition OTA API
// ============================================================================

/**
* @brief Canonical recovery image paths
*/
#define TS_OTA_RECOVERY_FIRMWARE_PATH "/sdcard/recovery/TianShanOS.bin"
#define TS_OTA_RECOVERY_WWW_PATH "/sdcard/recovery/www.bin"
#define TS_OTA_RECOVERY_MANIFEST_PATH "/sdcard/recovery/manifest.json"

/**
* @brief Initialize WWW OTA module
* @return ESP_OK on success
Expand Down Expand Up @@ -434,6 +467,21 @@ esp_err_t ts_ota_www_start_sdcard(const char *filepath,
ts_ota_progress_cb_t progress_cb,
void *user_data);

/**
* @brief Synchronously flash WWW partition from a local file
*
* This call only returns ESP_OK after the file has been written to the www
* partition and the WebUI OTA progress state is pending reboot.
*
* @param filepath Path to www.bin file
* @param progress_cb Progress callback (optional)
* @param user_data User data for callback
* @return ESP_OK when the partition write has completed successfully
*/
esp_err_t ts_ota_www_flash_file_sync(const char *filepath,
ts_ota_progress_cb_t progress_cb,
void *user_data);

/**
* @brief Abort WWW partition OTA
* @return ESP_OK on success
Expand Down Expand Up @@ -490,6 +538,19 @@ esp_err_t ts_ota_check_recovery(void);
*/
esp_err_t ts_ota_create_recovery_manifest(const char *version, bool force);

/**
* @brief Write recovery manifest from files currently in /sdcard/recovery
*
* Browser-proxy OTA uses this to keep recovery files as cached artifacts
* without arming boot-time recovery.
*
* @param include_www Include /sdcard/recovery/www.bin in the manifest
* @param force Force recovery regardless of version when armed
* @param armed Allow boot-time recovery to consume this manifest
* @return ESP_OK on success
*/
esp_err_t ts_ota_write_recovery_manifest_from_files(bool include_www, bool force, bool armed);

// ============================================================================
// Unified Download API (Recovery Directory Mode)
// ============================================================================
Expand Down
91 changes: 68 additions & 23 deletions components/ts_ota/src/ts_ota_download.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ static const char *TAG = "ts_ota_download";

/* Recovery 目录路径 */
#define RECOVERY_DIR "/sdcard/recovery"
#define RECOVERY_FIRMWARE RECOVERY_DIR "/TianShanOS.bin"
#define RECOVERY_WWW RECOVERY_DIR "/www.bin"
#define RECOVERY_MANIFEST RECOVERY_DIR "/manifest.json"
#define RECOVERY_FIRMWARE TS_OTA_RECOVERY_FIRMWARE_PATH
#define RECOVERY_WWW TS_OTA_RECOVERY_WWW_PATH
#define RECOVERY_MANIFEST TS_OTA_RECOVERY_MANIFEST_PATH
#define RECOVERY_TEMP RECOVERY_DIR "/temp.bin"

/* PSRAM-first allocation */
Expand Down Expand Up @@ -69,8 +69,9 @@ static bool s_download_running = false;
static void download_task(void *arg);
static esp_err_t download_file_to_sdcard(const char *url, const char *dest_path,
bool skip_cert_verify, size_t *out_size);
static esp_err_t read_firmware_file_version(const char *path, char *version, size_t version_len);
static esp_err_t verify_firmware_file(const char *path, char *version, size_t version_len);
static esp_err_t create_manifest_file(const char *firmware_version, const char *www_version, bool force);
static esp_err_t create_manifest_file(const char *firmware_version, const char *www_name, bool force, bool armed);


/**
Expand Down Expand Up @@ -249,10 +250,12 @@ esp_err_t ts_ota_save_upload(const void *data, size_t len, bool is_firmware, boo
return ret;
}

// 创建 manifest
ret = create_manifest_file(version, NULL, true);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to create manifest (non-fatal)");
if (auto_flash) {
// Keep legacy auto-flash behavior armed for boot-time recovery.
ret = create_manifest_file(version, NULL, true, true);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to create manifest (non-fatal)");
}
}
}

Expand All @@ -268,6 +271,37 @@ esp_err_t ts_ota_save_upload(const void *data, size_t len, bool is_firmware, boo
return ESP_OK;
}

esp_err_t ts_ota_write_recovery_manifest_from_files(bool include_www, bool force, bool armed)
{
esp_err_t ret = ensure_recovery_dir();
if (ret != ESP_OK) {
return ret;
}

char version[64] = {0};
ret = read_firmware_file_version(RECOVERY_FIRMWARE, version, sizeof(version));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Recovery firmware is not valid for manifest: %s", esp_err_to_name(ret));
return ret;
}

const char *www_name = "";
if (include_www) {
struct stat st;
if (stat(RECOVERY_WWW, &st) != 0) {
ESP_LOGE(TAG, "Recovery www not found for manifest: %s", RECOVERY_WWW);
return ESP_ERR_NOT_FOUND;
}
if (st.st_size <= 0) {
ESP_LOGE(TAG, "Recovery www is empty");
return ESP_ERR_INVALID_SIZE;
}
www_name = "www.bin";
}

return create_manifest_file(version, www_name, force, armed);
}

/**
* @brief 从 recovery 目录刷入固件
*
Expand Down Expand Up @@ -424,8 +458,8 @@ static void download_task(void *arg)

ESP_LOGI(TAG, "Firmware verified: version=%s", version);

// 创建 manifest
create_manifest_file(version, NULL, false);
// Download-only recovery is an armed app-only package.
create_manifest_file(version, "", false, true);
}

ts_ota_update_progress(TS_OTA_STATE_IDLE, file_size, file_size, "下载完成");
Expand Down Expand Up @@ -697,14 +731,10 @@ static esp_err_t download_file_to_sdcard(const char *url, const char *dest_path,
return (ret == ESP_OK) ? ESP_FAIL : ret;
}

/**
* @brief 验证固件文件
*/
static esp_err_t verify_firmware_file(const char *path, char *version, size_t version_len)
static esp_err_t read_firmware_file_version(const char *path, char *version, size_t version_len)
{
FILE *f = fopen(path, "rb");
if (!f) {
ts_ota_set_error(TS_OTA_ERR_FILE_NOT_FOUND, "固件文件不存在");
return ESP_ERR_NOT_FOUND;
}

Expand All @@ -713,7 +743,6 @@ static esp_err_t verify_firmware_file(const char *path, char *version, size_t ve
uint8_t *header = OTA_MALLOC(header_size);
if (!header) {
fclose(f);
ts_ota_set_error(TS_OTA_ERR_INTERNAL, "内存不足");
return ESP_ERR_NO_MEM;
}

Expand All @@ -722,7 +751,6 @@ static esp_err_t verify_firmware_file(const char *path, char *version, size_t ve

if (read < header_size) {
OTA_FREE(header);
ts_ota_set_error(TS_OTA_ERR_VERIFY_FAILED, "固件文件过小");
return ESP_ERR_INVALID_SIZE;
}

Expand All @@ -731,7 +759,6 @@ static esp_err_t verify_firmware_file(const char *path, char *version, size_t ve

if (app_desc->magic_word != ESP_APP_DESC_MAGIC_WORD) {
OTA_FREE(header);
ts_ota_set_error(TS_OTA_ERR_VERIFY_FAILED, "无效的固件格式");
return ESP_ERR_INVALID_ARG;
}

Expand All @@ -747,10 +774,28 @@ static esp_err_t verify_firmware_file(const char *path, char *version, size_t ve
return ESP_OK;
}

/**
* @brief 验证固件文件
*/
static esp_err_t verify_firmware_file(const char *path, char *version, size_t version_len)
{
esp_err_t ret = read_firmware_file_version(path, version, version_len);
if (ret == ESP_ERR_NOT_FOUND) {
ts_ota_set_error(TS_OTA_ERR_FILE_NOT_FOUND, "固件文件不存在");
} else if (ret == ESP_ERR_NO_MEM) {
ts_ota_set_error(TS_OTA_ERR_INTERNAL, "内存不足");
} else if (ret == ESP_ERR_INVALID_SIZE) {
ts_ota_set_error(TS_OTA_ERR_VERIFY_FAILED, "固件文件过小");
} else if (ret == ESP_ERR_INVALID_ARG) {
ts_ota_set_error(TS_OTA_ERR_VERIFY_FAILED, "无效的固件格式");
}
return ret;
}

/**
* @brief 创建 manifest 文件
*/
static esp_err_t create_manifest_file(const char *firmware_version, const char *www_version, bool force)
static esp_err_t create_manifest_file(const char *firmware_version, const char *www_name, bool force, bool armed)
{
cJSON *manifest = cJSON_CreateObject();
if (!manifest) {
Expand All @@ -762,10 +807,10 @@ static esp_err_t create_manifest_file(const char *firmware_version, const char *
cJSON_AddStringToObject(manifest, "firmware_version", firmware_version);
cJSON_AddStringToObject(manifest, "firmware", "TianShanOS.bin");
}
if (www_version) {
cJSON_AddStringToObject(manifest, "www_version", www_version);
cJSON_AddStringToObject(manifest, "www", "www.bin");
if (www_name) {
cJSON_AddStringToObject(manifest, "www", www_name);
}
cJSON_AddBoolToObject(manifest, "armed", armed);
cJSON_AddBoolToObject(manifest, "force", force);
cJSON_AddBoolToObject(manifest, "delete_after", true);

Expand All @@ -776,7 +821,7 @@ static esp_err_t create_manifest_file(const char *firmware_version, const char *
cJSON_AddStringToObject(manifest, "firmware_sha256", sha256);
}
}
if (ts_storage_exists(RECOVERY_WWW)) {
if (www_name && www_name[0] && ts_storage_exists(RECOVERY_WWW)) {
if (calculate_file_sha256(RECOVERY_WWW, sha256, sizeof(sha256)) == ESP_OK) {
cJSON_AddStringToObject(manifest, "www_sha256", sha256);
}
Expand Down
33 changes: 22 additions & 11 deletions components/ts_ota/src/ts_ota_recovery.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* manifest.json 格式:
* {
* "version": "0.3.0",
* "armed": true,
* "force": false,
* "firmware": "TianShanOS.bin",
* "www": "www.bin"
Expand Down Expand Up @@ -49,17 +50,13 @@ static const char *TAG = "ts_recovery";

// Recovery 目录和文件路径
#define RECOVERY_DIR "/sdcard/recovery"
#define RECOVERY_MANIFEST RECOVERY_DIR "/manifest.json"
#define RECOVERY_FIRMWARE RECOVERY_DIR "/TianShanOS.bin"
#define RECOVERY_WWW RECOVERY_DIR "/www.bin"
#define RECOVERY_MANIFEST TS_OTA_RECOVERY_MANIFEST_PATH
#define RECOVERY_FIRMWARE TS_OTA_RECOVERY_FIRMWARE_PATH
#define RECOVERY_WWW TS_OTA_RECOVERY_WWW_PATH
#define RECOVERY_BUFFER_SIZE 4096

// 使用 PSRAM 如果可用
#if CONFIG_SPIRAM
#define RECOVERY_MALLOC(size) heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)
#else
#define RECOVERY_MALLOC(size) malloc(size)
#endif
// Flash writes must not read from PSRAM while cache is disabled.
#define RECOVERY_MALLOC(size) heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)

/**
* @brief Recovery manifest 结构
Expand All @@ -68,6 +65,7 @@ typedef struct {
char version[32]; // 目标版本
char firmware[64]; // 固件文件名
char www[64]; // WebUI 文件名
bool armed; // 是否允许启动时自动恢复
bool force; // 强制恢复(忽略版本比对)
bool delete_after; // 成功后删除 recovery 文件
} recovery_manifest_t;
Expand Down Expand Up @@ -116,6 +114,7 @@ static esp_err_t read_manifest(recovery_manifest_t *manifest)
memset(manifest, 0, sizeof(*manifest));
strcpy(manifest->firmware, "TianShanOS.bin");
strcpy(manifest->www, "www.bin");
manifest->armed = true;
manifest->delete_after = true;

// 读取字段
Expand All @@ -139,6 +138,11 @@ static esp_err_t read_manifest(recovery_manifest_t *manifest)
manifest->force = cJSON_IsTrue(force);
}

cJSON *armed = cJSON_GetObjectItem(root, "armed");
if (armed && cJSON_IsBool(armed)) {
manifest->armed = cJSON_IsTrue(armed);
}

cJSON *delete_after = cJSON_GetObjectItem(root, "delete_after");
if (delete_after && cJSON_IsBool(delete_after)) {
manifest->delete_after = cJSON_IsTrue(delete_after);
Expand Down Expand Up @@ -445,6 +449,7 @@ esp_err_t ts_ota_check_recovery(void)
memset(&manifest, 0, sizeof(manifest));
strcpy(manifest.firmware, "TianShanOS.bin");
strcpy(manifest.www, "www.bin");
manifest.armed = true;
manifest.force = true; // 无 manifest 时强制恢复
manifest.delete_after = true;
} else {
Expand All @@ -453,8 +458,13 @@ esp_err_t ts_ota_check_recovery(void)
}
}

ESP_LOGI(TAG, "Recovery manifest: version=%s, force=%d, firmware=%s, www=%s",
manifest.version, manifest.force, manifest.firmware, manifest.www);
ESP_LOGI(TAG, "Recovery manifest: version=%s, armed=%d, force=%d, firmware=%s, www=%s",
manifest.version, manifest.armed, manifest.force, manifest.firmware, manifest.www);

if (!manifest.armed) {
ESP_LOGI(TAG, "Recovery manifest is not armed, skipping boot-time recovery");
return ESP_OK;
}

// 检查是否需要恢复
if (!need_recovery(&manifest)) {
Expand Down Expand Up @@ -546,6 +556,7 @@ esp_err_t ts_ota_create_recovery_manifest(const char *version, bool force)
}

cJSON_AddStringToObject(root, "version", version ? version : "");
cJSON_AddBoolToObject(root, "armed", true);
cJSON_AddBoolToObject(root, "force", force);
cJSON_AddStringToObject(root, "firmware", "TianShanOS.bin");
cJSON_AddStringToObject(root, "www", "www.bin");
Expand Down
Loading
Loading