|
|
@@ -0,0 +1,301 @@
|
|
|
+#include "ota_update.h"
|
|
|
+
|
|
|
+#include <inttypes.h>
|
|
|
+#include <stdio.h>
|
|
|
+#include <stdlib.h>
|
|
|
+#include <string.h>
|
|
|
+
|
|
|
+#include "config.h"
|
|
|
+#include "esp_app_desc.h"
|
|
|
+#include "esp_check.h"
|
|
|
+#include "esp_http_server.h"
|
|
|
+#include "esp_log.h"
|
|
|
+#include "esp_ota_ops.h"
|
|
|
+#include "esp_system.h"
|
|
|
+#include "esp_timer.h"
|
|
|
+#include "freertos/FreeRTOS.h"
|
|
|
+#include "freertos/task.h"
|
|
|
+
|
|
|
+#define OTA_RECEIVE_BUFFER_SIZE 4096
|
|
|
+#define OTA_ERROR_VISIBLE_US (10LL * 1000LL * 1000LL)
|
|
|
+#define OTA_TOKEN_HEADER "X-Guguji-OTA-Token"
|
|
|
+
|
|
|
+static const char *TAG = "ota_update";
|
|
|
+
|
|
|
+static httpd_handle_t g_http_server = NULL;
|
|
|
+static guguji_ota_prepare_callback_t g_prepare_callback = NULL;
|
|
|
+static void *g_callback_context = NULL;
|
|
|
+static volatile guguji_ota_state_t g_ota_state = GUGUJI_OTA_STATE_IDLE;
|
|
|
+static volatile int64_t g_error_visible_until_us = 0;
|
|
|
+
|
|
|
+_Static_assert(sizeof(GUGUJI_OTA_AUTH_TOKEN) <= 96, "OTA token must not exceed 95 characters");
|
|
|
+
|
|
|
+static bool constant_time_token_equal(const char *received, const char *expected)
|
|
|
+{
|
|
|
+ const size_t received_len = strlen(received);
|
|
|
+ const size_t expected_len = strlen(expected);
|
|
|
+ unsigned char difference = (unsigned char)(received_len ^ expected_len);
|
|
|
+ const size_t compare_len = received_len > expected_len ? received_len : expected_len;
|
|
|
+
|
|
|
+ for (size_t i = 0; i < compare_len; ++i) {
|
|
|
+ const unsigned char left = i < received_len ? (unsigned char)received[i] : 0;
|
|
|
+ const unsigned char right = i < expected_len ? (unsigned char)expected[i] : 0;
|
|
|
+ difference |= left ^ right;
|
|
|
+ }
|
|
|
+ return difference == 0;
|
|
|
+}
|
|
|
+
|
|
|
+static bool request_is_authorized(httpd_req_t *request)
|
|
|
+{
|
|
|
+ const size_t token_len = httpd_req_get_hdr_value_len(request, OTA_TOKEN_HEADER);
|
|
|
+ if (token_len == 0 || token_len >= 96) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ char token[96] = {0};
|
|
|
+ if (httpd_req_get_hdr_value_str(request, OTA_TOKEN_HEADER, token, sizeof(token)) != ESP_OK) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return constant_time_token_equal(token, GUGUJI_OTA_AUTH_TOKEN);
|
|
|
+}
|
|
|
+
|
|
|
+static esp_err_t send_json(httpd_req_t *request, const char *status, const char *json)
|
|
|
+{
|
|
|
+ httpd_resp_set_status(request, status);
|
|
|
+ httpd_resp_set_type(request, "application/json");
|
|
|
+ httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
|
|
+ return httpd_resp_sendstr(request, json);
|
|
|
+}
|
|
|
+
|
|
|
+static esp_err_t send_json_error(httpd_req_t *request, const char *status, const char *message)
|
|
|
+{
|
|
|
+ char response[192];
|
|
|
+ snprintf(response, sizeof(response), "{\"ok\":false,\"error\":\"%s\"}", message);
|
|
|
+ return send_json(request, status, response);
|
|
|
+}
|
|
|
+
|
|
|
+static esp_err_t authorize_or_reject(httpd_req_t *request)
|
|
|
+{
|
|
|
+ if (request_is_authorized(request)) {
|
|
|
+ return ESP_OK;
|
|
|
+ }
|
|
|
+ ESP_LOGW(TAG, "拒绝未授权的 OTA 请求");
|
|
|
+ send_json_error(request, "401 Unauthorized", "invalid OTA token");
|
|
|
+ return ESP_ERR_INVALID_STATE;
|
|
|
+}
|
|
|
+
|
|
|
+static esp_err_t ota_status_handler(httpd_req_t *request)
|
|
|
+{
|
|
|
+ if (authorize_or_reject(request) != ESP_OK) {
|
|
|
+ return ESP_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ const esp_partition_t *running = esp_ota_get_running_partition();
|
|
|
+ const esp_partition_t *next = esp_ota_get_next_update_partition(NULL);
|
|
|
+ const esp_app_desc_t *description = esp_app_get_description();
|
|
|
+ char response[320];
|
|
|
+ snprintf(
|
|
|
+ response,
|
|
|
+ sizeof(response),
|
|
|
+ "{\"ok\":true,\"project\":\"%s\",\"version\":\"%s\","
|
|
|
+ "\"running_partition\":\"%s\",\"next_partition\":\"%s\",\"ota_state\":%d}",
|
|
|
+ description->project_name,
|
|
|
+ description->version,
|
|
|
+ running != NULL ? running->label : "unknown",
|
|
|
+ next != NULL ? next->label : "unavailable",
|
|
|
+ (int)guguji_ota_get_state());
|
|
|
+ return send_json(request, "200 OK", response);
|
|
|
+}
|
|
|
+
|
|
|
+static void record_ota_failure(void)
|
|
|
+{
|
|
|
+ g_error_visible_until_us = esp_timer_get_time() + OTA_ERROR_VISIBLE_US;
|
|
|
+ g_ota_state = GUGUJI_OTA_STATE_RECENT_ERROR;
|
|
|
+}
|
|
|
+
|
|
|
+static esp_err_t ota_update_handler(httpd_req_t *request)
|
|
|
+{
|
|
|
+ if (authorize_or_reject(request) != ESP_OK) {
|
|
|
+ return ESP_OK;
|
|
|
+ }
|
|
|
+ if (guguji_ota_is_active()) {
|
|
|
+ return send_json_error(request, "409 Conflict", "OTA update already in progress");
|
|
|
+ }
|
|
|
+ if (request->content_len == 0) {
|
|
|
+ return send_json_error(request, "400 Bad Request", "firmware body is empty");
|
|
|
+ }
|
|
|
+
|
|
|
+ const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);
|
|
|
+ if (update_partition == NULL) {
|
|
|
+ return send_json_error(request, "500 Internal Server Error", "no OTA partition available");
|
|
|
+ }
|
|
|
+ if ((size_t)request->content_len > update_partition->size) {
|
|
|
+ return send_json_error(request, "413 Content Too Large", "firmware exceeds OTA partition size");
|
|
|
+ }
|
|
|
+ g_ota_state = GUGUJI_OTA_STATE_RECEIVING;
|
|
|
+ if (g_prepare_callback != NULL && !g_prepare_callback(g_callback_context)) {
|
|
|
+ g_ota_state = GUGUJI_OTA_STATE_IDLE;
|
|
|
+ ESP_LOGW(TAG, "机器人未处于安全失能状态,拒绝 OTA");
|
|
|
+ return send_json_error(request, "409 Conflict", "disarm the robot before OTA");
|
|
|
+ }
|
|
|
+
|
|
|
+ ESP_LOGI(
|
|
|
+ TAG,
|
|
|
+ "开始 OTA: 目标分区=%s, 地址=0x%08" PRIx32 ", 镜像=%zu bytes",
|
|
|
+ update_partition->label,
|
|
|
+ update_partition->address,
|
|
|
+ request->content_len);
|
|
|
+
|
|
|
+ uint8_t *buffer = malloc(OTA_RECEIVE_BUFFER_SIZE);
|
|
|
+ if (buffer == NULL) {
|
|
|
+ record_ota_failure();
|
|
|
+ return send_json_error(request, "500 Internal Server Error", "cannot allocate OTA buffer");
|
|
|
+ }
|
|
|
+
|
|
|
+ esp_ota_handle_t update_handle = 0;
|
|
|
+ bool ota_handle_open = false;
|
|
|
+ esp_err_t result = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle);
|
|
|
+ if (result == ESP_OK) {
|
|
|
+ ota_handle_open = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ size_t written = 0;
|
|
|
+ unsigned int last_progress = 0;
|
|
|
+ unsigned int timeout_count = 0;
|
|
|
+ while (result == ESP_OK && written < (size_t)request->content_len) {
|
|
|
+ const size_t remaining = (size_t)request->content_len - written;
|
|
|
+ const size_t wanted = remaining < OTA_RECEIVE_BUFFER_SIZE ? remaining : OTA_RECEIVE_BUFFER_SIZE;
|
|
|
+ const int received = httpd_req_recv(request, (char *)buffer, wanted);
|
|
|
+ if (received == HTTPD_SOCK_ERR_TIMEOUT && timeout_count++ < 5) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (received <= 0) {
|
|
|
+ ESP_LOGE(TAG, "OTA 上传连接中断: received=%d", received);
|
|
|
+ result = ESP_FAIL;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ timeout_count = 0;
|
|
|
+ result = esp_ota_write(update_handle, buffer, (size_t)received);
|
|
|
+ if (result != ESP_OK) {
|
|
|
+ ESP_LOGE(TAG, "写入 OTA 分区失败: %s", esp_err_to_name(result));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ written += (size_t)received;
|
|
|
+ const unsigned int progress = (unsigned int)(written * 100U / (size_t)request->content_len);
|
|
|
+ if (progress >= last_progress + 10U || progress == 100U) {
|
|
|
+ ESP_LOGI(TAG, "OTA 接收进度: %u%% (%zu/%zu bytes)", progress, written, request->content_len);
|
|
|
+ last_progress = progress;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ free(buffer);
|
|
|
+
|
|
|
+ if (result == ESP_OK && written != (size_t)request->content_len) {
|
|
|
+ result = ESP_ERR_INVALID_SIZE;
|
|
|
+ }
|
|
|
+ if (result == ESP_OK) {
|
|
|
+ result = esp_ota_end(update_handle);
|
|
|
+ ota_handle_open = false;
|
|
|
+ if (result == ESP_ERR_OTA_VALIDATE_FAILED) {
|
|
|
+ ESP_LOGE(TAG, "OTA 镜像校验失败,镜像损坏或目标芯片不匹配");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (result == ESP_OK) {
|
|
|
+ result = esp_ota_set_boot_partition(update_partition);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (result != ESP_OK) {
|
|
|
+ if (ota_handle_open) {
|
|
|
+ esp_ota_abort(update_handle);
|
|
|
+ }
|
|
|
+ ESP_LOGE(TAG, "OTA 失败: %s", esp_err_to_name(result));
|
|
|
+ record_ota_failure();
|
|
|
+ return send_json_error(request, "500 Internal Server Error", esp_err_to_name(result));
|
|
|
+ }
|
|
|
+
|
|
|
+ esp_app_desc_t new_description = {0};
|
|
|
+ if (esp_ota_get_partition_description(update_partition, &new_description) == ESP_OK) {
|
|
|
+ ESP_LOGI(TAG, "OTA 镜像校验通过: version=%s", new_description.version);
|
|
|
+ }
|
|
|
+ ESP_LOGI(TAG, "OTA 完成,1 秒后重启进入分区 %s", update_partition->label);
|
|
|
+ send_json(request, "200 OK", "{\"ok\":true,\"message\":\"update accepted; rebooting\"}");
|
|
|
+ vTaskDelay(pdMS_TO_TICKS(1000));
|
|
|
+ esp_restart();
|
|
|
+ return ESP_OK;
|
|
|
+}
|
|
|
+
|
|
|
+esp_err_t guguji_ota_server_start(
|
|
|
+ guguji_ota_prepare_callback_t prepare_callback,
|
|
|
+ void *callback_context)
|
|
|
+{
|
|
|
+ if (g_http_server != NULL) {
|
|
|
+ return ESP_ERR_INVALID_STATE;
|
|
|
+ }
|
|
|
+
|
|
|
+ g_prepare_callback = prepare_callback;
|
|
|
+ g_callback_context = callback_context;
|
|
|
+
|
|
|
+ httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
|
|
+ config.server_port = GUGUJI_OTA_HTTP_PORT;
|
|
|
+ config.stack_size = 8192;
|
|
|
+ config.max_open_sockets = 4;
|
|
|
+ config.recv_wait_timeout = 15;
|
|
|
+ config.send_wait_timeout = 10;
|
|
|
+ config.lru_purge_enable = true;
|
|
|
+
|
|
|
+ ESP_RETURN_ON_ERROR(httpd_start(&g_http_server, &config), TAG, "启动 OTA HTTP 服务失败");
|
|
|
+
|
|
|
+ const httpd_uri_t status_uri = {
|
|
|
+ .uri = "/ota/status",
|
|
|
+ .method = HTTP_GET,
|
|
|
+ .handler = ota_status_handler,
|
|
|
+ };
|
|
|
+ const httpd_uri_t update_uri = {
|
|
|
+ .uri = "/ota/update",
|
|
|
+ .method = HTTP_POST,
|
|
|
+ .handler = ota_update_handler,
|
|
|
+ };
|
|
|
+ ESP_RETURN_ON_ERROR(httpd_register_uri_handler(g_http_server, &status_uri), TAG, "注册 OTA 状态接口失败");
|
|
|
+ ESP_RETURN_ON_ERROR(httpd_register_uri_handler(g_http_server, &update_uri), TAG, "注册 OTA 上传接口失败");
|
|
|
+
|
|
|
+ const esp_partition_t *running = esp_ota_get_running_partition();
|
|
|
+ const esp_app_desc_t *description = esp_app_get_description();
|
|
|
+ ESP_LOGI(
|
|
|
+ TAG,
|
|
|
+ "OTA 服务已启动: port=%d, version=%s, partition=%s",
|
|
|
+ GUGUJI_OTA_HTTP_PORT,
|
|
|
+ description->version,
|
|
|
+ running != NULL ? running->label : "unknown");
|
|
|
+ return ESP_OK;
|
|
|
+}
|
|
|
+
|
|
|
+esp_err_t guguji_ota_confirm_running_image(void)
|
|
|
+{
|
|
|
+ const esp_partition_t *running = esp_ota_get_running_partition();
|
|
|
+ if (running == NULL) {
|
|
|
+ return ESP_ERR_NOT_FOUND;
|
|
|
+ }
|
|
|
+
|
|
|
+ esp_ota_img_states_t state;
|
|
|
+ esp_err_t result = esp_ota_get_state_partition(running, &state);
|
|
|
+ if (result == ESP_OK && state == ESP_OTA_IMG_PENDING_VERIFY) {
|
|
|
+ result = esp_ota_mark_app_valid_cancel_rollback();
|
|
|
+ if (result == ESP_OK) {
|
|
|
+ ESP_LOGI(TAG, "新固件运行检查通过,已确认镜像并取消回滚");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return (result == ESP_ERR_NOT_SUPPORTED || result == ESP_ERR_NOT_FOUND) ? ESP_OK : result;
|
|
|
+}
|
|
|
+
|
|
|
+guguji_ota_state_t guguji_ota_get_state(void)
|
|
|
+{
|
|
|
+ if (g_ota_state == GUGUJI_OTA_STATE_RECENT_ERROR && esp_timer_get_time() >= g_error_visible_until_us) {
|
|
|
+ g_ota_state = GUGUJI_OTA_STATE_IDLE;
|
|
|
+ }
|
|
|
+ return g_ota_state;
|
|
|
+}
|
|
|
+
|
|
|
+bool guguji_ota_is_active(void)
|
|
|
+{
|
|
|
+ return guguji_ota_get_state() == GUGUJI_OTA_STATE_RECEIVING;
|
|
|
+}
|