// SDK #include "cJSON.h" #include "esp_http_server.h" #include "esp_log.h" #include "esp_partition.h" #include "esp_system.h" #include "esp_timer.h" #include "nvs.h" #include "nvs_flash.h" // Project #include "appstate.hpp" #include "types.hpp" #include #define OTA_SCRATCH_BUFSIZE 4096 internal void ota_restart_timer_callback(void *arg) { esp_restart(); } internal esp_err_t api_ota_frontend_handler(httpd_req_t *req) { httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); uint8_t target_slot = g_Active_WWW_Partition == 0 ? 1 : 0; const char *target_label = target_slot == 0 ? "www_0" : "www_1"; const esp_partition_t *partition = esp_partition_find_first( ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_LITTLEFS, target_label); if (!partition) { ESP_LOGE("OTA", "Could not find partition %s", target_label); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Partition not found"); return ESP_FAIL; } ESP_LOGI("OTA", "Starting OTA to partition %s (size %ld)", target_label, partition->size); esp_err_t err = esp_partition_erase_range(partition, 0, partition->size); if (err != ESP_OK) { ESP_LOGE("OTA", "Failed to erase partition: %s", esp_err_to_name(err)); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to erase partition"); return ESP_FAIL; } char *buf = (char *)malloc(OTA_SCRATCH_BUFSIZE); if (!buf) { ESP_LOGE("OTA", "Failed to allocate buffer"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); return ESP_FAIL; } int total_read = 0; int remaining = req->content_len; bool first_chunk = true; while (remaining > 0) { int recv_len = httpd_req_recv(req, buf, MIN(remaining, OTA_SCRATCH_BUFSIZE)); if (recv_len <= 0) { if (recv_len == HTTPD_SOCK_ERR_TIMEOUT) { continue; } ESP_LOGE("OTA", "Receive failed"); free(buf); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Receive failed"); return ESP_FAIL; } if (first_chunk) { if ((uint8_t)buf[0] == 0xE9) { ESP_LOGE("OTA", "Magic 0xE9 detected. This looks like a FIRMWARE bin, " "but you are uploading to FRONTEND slot!"); free(buf); httpd_resp_send_err( req, HTTPD_400_BAD_REQUEST, "Invalid file: This is a Firmware binary, not a UI binary."); return ESP_FAIL; } first_chunk = false; } err = esp_partition_write(partition, total_read, buf, recv_len); if (err != ESP_OK) { ESP_LOGE("OTA", "Failed to write to partition: %s", esp_err_to_name(err)); free(buf); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Flash write failed"); return ESP_FAIL; } total_read += recv_len; remaining -= recv_len; } free(buf); ESP_LOGI("OTA", "OTA complete. Written %d bytes. Updating NVS...", total_read); nvs_handle_t my_handle; if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK) { err = nvs_set_u8(my_handle, "www_part", target_slot); if (err == ESP_OK) { nvs_commit(my_handle); } nvs_close(my_handle); } else { ESP_LOGE("OTA", "Failed to open NVS to update partition index"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "NVS update failed"); return ESP_FAIL; } httpd_resp_set_type(req, "application/json"); cJSON *root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "status", "success"); cJSON_AddStringToObject(root, "message", "Update successful, rebooting..."); const char *response_text = cJSON_Print(root); httpd_resp_sendstr(req, response_text); free((void *)response_text); cJSON_Delete(root); // Trigger reboot const esp_timer_create_args_t restart_timer_args = { .callback = &ota_restart_timer_callback, .arg = (void *)0, .dispatch_method = ESP_TIMER_TASK, .name = "ota_restart_timer", .skip_unhandled_events = false}; esp_timer_handle_t restart_timer; esp_timer_create(&restart_timer_args, &restart_timer); esp_timer_start_once(restart_timer, 1'000'000); return ESP_OK; } internal const httpd_uri_t api_ota_frontend_uri = {.uri = "/api/ota/frontend", .method = HTTP_POST, .handler = api_ota_frontend_handler, .user_ctx = NULL};