Files
story/firmware/echoear-platform-agent/main/device_ota.c
T

386 lines
24 KiB
C
Raw Normal View History

#include "device_ota.h"
#include "device_control.h"
#include "ble_cloud.h"
#include "call_client.h"
#include "firmware_version.h"
#include "cJSON.h"
#include "esp_app_desc.h"
#include "esp_crt_bundle.h"
#include "esp_http_client.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_partition.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/idf_additions.h"
#include "mbedtls/sha256.h"
#include "mbedtls/platform_util.h"
#include "nvs.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <math.h>
typedef struct {
uint32_t format,phase,address,size; // 1=download,2=boot pending,3=success,4=failed,0=reported
int64_t code;
char id[37],version[81],sha[65],message[100];
} ota_state_t;
typedef struct {uint32_t format,address;int64_t code;char version[81];} installed_t;
static ota_state_t state;
static installed_t installed;
static esp_timer_handle_t boot_timer;
static QueueHandle_t requests;
static StaticQueue_t queue_state;
static uint8_t queue_storage[37];
static bool ready;
static bool storage_ready;
static char retry_id[37];
static unsigned manifest_retries;
typedef enum {OTA_FLASH_END,OTA_FLASH_BOOT,OTA_FLASH_ROLLBACK} ota_flash_op_t;
static esp_err_t ota_internal_flash_call(ota_flash_op_t op,esp_ota_handle_t handle,const esp_partition_t *target,bool *completed);
static const char *TAG="device_ota";
static const char *str(const cJSON *j,const char *k){cJSON *v=cJSON_GetObjectItemCaseSensitive(j,k);return cJSON_IsString(v)?v->valuestring:"";}
static int64_t number(const cJSON *j,const char *k){
cJSON *v=cJSON_GetObjectItemCaseSensitive(j,k);
if(!cJSON_IsNumber(v) || !isfinite(v->valuedouble) || v->valuedouble<0 ||
v->valuedouble>9007199254740991.0 || floor(v->valuedouble)!=v->valuedouble)return -1;
return (int64_t)v->valuedouble;
}
static void wipe(char *v){if(v){mbedtls_platform_zeroize(v,strlen(v));free(v);}}
static esp_err_t post(const char *path,cJSON *j,cJSON **data){
char *body=cJSON_PrintUnformatted(j),*raw=NULL;if(!body)return ESP_ERR_NO_MEM;
esp_err_t e=platform_request("POST",path,body,NULL,&raw);wipe(body);
if(e==ESP_OK){cJSON *root=raw?cJSON_Parse(raw):NULL;const cJSON *v=cJSON_GetObjectItemCaseSensitive(root,"data");
if(!cJSON_IsObject(v))e=ESP_ERR_INVALID_RESPONSE;
else if(data){*data=cJSON_Duplicate(v,true);if(!*data)e=ESP_ERR_NO_MEM;}
cJSON_Delete(root);
}wipe(raw);return e;
}
static esp_err_t persist(const char *key,const void *data,size_t size){
nvs_handle_t n;esp_err_t e=nvs_open("control",NVS_READWRITE,&n);if(e!=ESP_OK)return e;
e=nvs_set_blob(n,key,data,size);if(e==ESP_OK)e=nvs_commit(n);nvs_close(n);return e;
}
static esp_err_t load_result(const char *key,void *data,size_t size){
nvs_handle_t n;esp_err_t e=nvs_open("control",NVS_READONLY,&n);if(e!=ESP_OK)return e;
size_t actual=size;e=nvs_get_blob(n,key,data,&actual);nvs_close(n);
return e==ESP_OK && actual!=size?ESP_ERR_INVALID_SIZE:e;
}
static esp_err_t save(void){return persist("ota",&state,sizeof(state));}
static bool installed_key(const esp_partition_t *part,char key[16]){
if(!part || part->subtype<ESP_PARTITION_SUBTYPE_APP_OTA_MIN || part->subtype>ESP_PARTITION_SUBTYPE_APP_OTA_MAX)return false;
snprintf(key,16,"installed%u",(unsigned)(part->subtype-ESP_PARTITION_SUBTYPE_APP_OTA_MIN));return true;
}
static bool load_installed(const esp_partition_t *running){
char key[16];memset(&installed,0,sizeof(installed));if(!installed_key(running,key))return false;
esp_err_t err=load_result(key,&installed,sizeof(installed));
// Keep the old single-slot record intact: an older fallback image still
// reads it. New images record each slot separately before boot confirmation.
bool legacy=err==ESP_ERR_NVS_NOT_FOUND;
if(legacy)err=load_result("installed",&installed,sizeof(installed));
if(err==ESP_ERR_NVS_NOT_FOUND){memset(&installed,0,sizeof(installed));return true;}
if(err!=ESP_OK || installed.format!=1 || installed.code<=0 || installed.code>9007199254740991LL ||
!installed.version[0] || !memchr(installed.version,0,sizeof(installed.version))){memset(&installed,0,sizeof(installed));return false;}
if(installed.address!=running->address){memset(&installed,0,sizeof(installed));return legacy;}
return true;
}
static void hash_hex(const uint8_t hash[32],char hex[65]){for(int i=0;i<32;i++)sprintf(hex+2*i,"%02x",hash[i]);hex[64]=0;}
static bool flash_matches(const esp_partition_t *part){
if(!part || state.size<1024 || state.size>part->size)return false;
unsigned char *buffer=malloc(4096),hash[32]={0};if(!buffer)return false;
mbedtls_sha256_context sha;mbedtls_sha256_init(&sha);bool ok=mbedtls_sha256_starts(&sha,0)==0;
for(uint32_t offset=0;ok && offset<state.size;offset+=4096){
size_t n=state.size-offset;if(n>4096)n=4096;
ok=esp_partition_read(part,offset,buffer,n)==ESP_OK && mbedtls_sha256_update(&sha,buffer,n)==0;
}
if(ok)ok=mbedtls_sha256_finish(&sha,hash)==0;
mbedtls_sha256_free(&sha);free(buffer);char hex[65];hash_hex(hash,hex);return ok && !strcmp(hex,state.sha);
}
static void boot_timeout(void *arg){ESP_LOGE(TAG,"New firmware health deadline expired; restarting for rollback");esp_restart();}
void device_ota_boot_guard(void){
ready=false;storage_ready=true;
retry_id[0]=0;manifest_retries=0;
requests=xQueueCreateStatic(1,37,queue_storage,&queue_state);
esp_err_t journal=load_result("ota",&state,sizeof(state));
if(journal==ESP_ERR_NVS_NOT_FOUND)memset(&state,0,sizeof(state));
else if(journal!=ESP_OK || state.format!=1 || state.phase>4 || !memchr(state.id,0,sizeof(state.id)) ||
!memchr(state.version,0,sizeof(state.version)) || !memchr(state.sha,0,sizeof(state.sha)) || !memchr(state.message,0,sizeof(state.message))){memset(&state,0,sizeof(state));storage_ready=false;}
const esp_partition_t *running=esp_ota_get_running_partition();
if(!load_installed(running))storage_ready=false;
esp_ota_img_states_t image;
if(running && esp_ota_get_state_partition(running,&image)==ESP_OK && image==ESP_OTA_IMG_PENDING_VERIFY){
esp_timer_create_args_t args={.callback=boot_timeout,.name="ota_health"};
if(esp_timer_create(&args,&boot_timer)!=ESP_OK || esp_timer_start_once(boot_timer,180000000)!=ESP_OK){
ESP_LOGE(TAG,"Cannot arm OTA health deadline; restoring previous image");
ota_internal_flash_call(OTA_FLASH_ROLLBACK,0,NULL,NULL);
esp_restart();
}
}
}
void device_ota_boot_ready(void){
// An unreadable record is not equivalent to a factory image with version 0.
// Leave OTA disabled; a pending image retains its rollback deadline.
if(!storage_ready)return;
const esp_partition_t *running=esp_ota_get_running_partition();
esp_ota_img_states_t image;
esp_err_t image_state=running?esp_ota_get_state_partition(running,&image):ESP_ERR_NOT_FOUND;
bool pending=image_state==ESP_OK && image==ESP_OTA_IMG_PENDING_VERIFY;
// Establish a valid fallback for the first image flashed over USB. Without
// this entry, the first OTA would have no previously confirmed A/B slot.
if(running && image_state==ESP_ERR_NOT_FOUND && running->subtype>=ESP_PARTITION_SUBTYPE_APP_OTA_MIN && running->subtype<=ESP_PARTITION_SUBTYPE_APP_OTA_MAX){
if(ota_internal_flash_call(OTA_FLASH_BOOT,0,running,NULL)!=ESP_OK || esp_ota_mark_app_valid_cancel_rollback()!=ESP_OK)return;
}
if(state.phase==2){
if(running && running->address==state.address && flash_matches(running)){
installed_t value={.format=1,.address=running->address,.code=state.code};
snprintf(value.version,sizeof(value.version),"%s",state.version);
char key[16];if(!installed_key(running,key) || persist(key,&value,sizeof(value))!=ESP_OK)return;
if(pending && esp_ota_mark_app_valid_cancel_rollback()!=ESP_OK)return;
installed=value;state.phase=3;
ESP_LOGI(TAG,"OTA boot verified: slot=0x%lx version_code=%lld",(unsigned long)running->address,(long long)state.code);
}else{state.phase=4;snprintf(state.message,sizeof(state.message),"Boot verification failed or previous firmware restored");
if(pending){save();if(ota_internal_flash_call(OTA_FLASH_ROLLBACK,0,NULL,NULL)==ESP_OK)esp_restart();return;}}
if(save()!=ESP_OK)return;
}else if(state.phase==1){state.phase=4;snprintf(state.message,sizeof(state.message),"Download interrupted before installation");if(save()!=ESP_OK)return;}
else if(pending){
// Untracked images cannot claim an OTA task; normal health checks still
// allow an intentional serial-flashed build to become bootable.
if(esp_ota_mark_app_valid_cancel_rollback()!=ESP_OK)return;
}
if(boot_timer){esp_timer_stop(boot_timer);esp_timer_delete(boot_timer);boot_timer=NULL;}
ready=true;
if(running)ESP_LOGI(TAG,"OTA ready: slot=0x%lx firmware=%s version_code=%lld",(unsigned long)running->address,device_ota_version(),(long long)device_ota_version_code());
}
bool device_ota_supported(void){return ready && esp_ota_get_next_update_partition(NULL)!=NULL;}
const char *device_ota_version(void){return installed.code>0?installed.version:DEVICE_FIRMWARE_VERSION;}
int64_t device_ota_version_code(void){return installed.code>0?installed.code:0;}
void device_ota_request(const char *id){if(requests && id && strlen(id)==36){char value[37];memcpy(value,id,37);xQueueSend(requests,value,0);}}
static bool report(const char *status){
cJSON *j=cJSON_CreateObject();cJSON_AddStringToObject(j,"task_id",state.id);cJSON_AddStringToObject(j,"status",status);
if(!strcmp(status,"success")){cJSON_AddStringToObject(j,"firmware_version",state.version);cJSON_AddNumberToObject(j,"version_code",state.code);}
if(state.message[0])cJSON_AddStringToObject(j,"message",state.message);
esp_err_t err=post("/api/v1/device/ota/report",j,NULL);cJSON_Delete(j);
// The server may already have committed a terminal receipt whose response
// was lost. A finished task must not block every future remote upgrade.
return err==ESP_OK || (!strcmp(platform_last_error_code(),"OTA_FINISHED") && strcmp(status,"accepted"));
}
static bool same_origin_download(const char *url){
const char *origin=ble_cloud_origin();size_t n=strlen(origin);
return !strncmp(origin,"https://",8) && !strncmp(url,origin,n) && !strncmp(url+n,"/f/",3) &&
strlen(url)<256 && !strchr(url,'\r') && !strchr(url,'\n');
}
typedef struct {
int64_t start_us,begin_us,read_us,write_us,sha_us,end_us;
int64_t read_max_us,write_max_us,sha_max_us;
uint32_t reads,writes,partial_reads,eagain_reads;
} ota_perf_t;
static unsigned long ota_ms(int64_t us){return us>0?(unsigned long)(us/1000):0;}
static void ota_elapsed(int64_t started,int64_t *sum,int64_t *maximum){
int64_t elapsed=esp_timer_get_time()-started;
*sum+=elapsed;if(maximum && elapsed>*maximum)*maximum=elapsed;
}
static esp_err_t ota_http_timeout(esp_http_client_handle_t client,int64_t deadline){
int64_t remaining=deadline-esp_timer_get_time();if(remaining<1000)return ESP_ERR_TIMEOUT;
int timeout_ms=remaining/1000;if(timeout_ms>20000)timeout_ms=20000;
return esp_http_client_set_timeout_ms(client,timeout_ms);
}
typedef struct {
ota_flash_op_t op;
esp_ota_handle_t handle;
const esp_partition_t *target;
SemaphoreHandle_t done;
esp_err_t result;
} ota_flash_job_t;
static void ota_flash_worker(void *arg){
ota_flash_job_t *job=arg;
switch(job->op){
case OTA_FLASH_END:job->result=esp_ota_end(job->handle);break;
case OTA_FLASH_BOOT:job->result=esp_ota_set_boot_partition(job->target);break;
case OTA_FLASH_ROLLBACK:job->result=esp_ota_mark_app_invalid_rollback();break;
default:job->result=ESP_ERR_INVALID_ARG;break;
}
xSemaphoreGive(job->done);
for(;;)vTaskSuspend(NULL);
}
static esp_err_t ota_internal_flash_call(ota_flash_op_t op,esp_ota_handle_t handle,const esp_partition_t *target,bool *completed){
if(completed)*completed=false;
SemaphoreHandle_t done=xSemaphoreCreateBinary();
if(!done)return ESP_ERR_NO_MEM;
ota_flash_job_t job={.op=op,.handle=handle,.target=target,.done=done,.result=ESP_FAIL};
TaskHandle_t worker=NULL;
// Image verification remaps flash and freezes caches. The control worker
// has a PSRAM stack, so end, boot selection and rollback need a DRAM stack.
// SDK WithCaps deletion suspends and joins a non-current task before freeing
// its stack/TCB; the worker never deletes itself or allocates a cleanup task.
if(xTaskCreateWithCaps(ota_flash_worker,"ota_flash",8192,&job,3,&worker,
MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT)!=pdPASS){vSemaphoreDelete(done);return ESP_ERR_NO_MEM;}
xSemaphoreTake(done,portMAX_DELAY);
vTaskDeleteWithCaps(worker);
vSemaphoreDelete(done);
if(completed)*completed=true;
return job.result;
}
static esp_err_t download(const char *url,const esp_partition_t *target){
if(!same_origin_download(url))return ESP_ERR_INVALID_ARG;
const esp_partition_t *running=esp_ota_get_running_partition();
if(!target || !running || target->address==running->address || state.size<1024 || state.size>target->size)return ESP_ERR_INVALID_SIZE;
ota_perf_t perf={.start_us=esp_timer_get_time()};
const int64_t deadline=perf.start_us+900000000;
// Flash cannot directly program from PSRAM on SPI1: the SDK otherwise
// copies through a 32-byte temporary buffer. Keep the write buffer in DRAM.
// Match the SDK's 8KiB flash write chunk without exhausting internal heap.
size_t buf_size=8192;unsigned char *buf=NULL;
for(;buf_size>=4096;buf_size/=2){
buf=heap_caps_malloc(buf_size,MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT);if(buf)break;
}
if(!buf)return ESP_ERR_NO_MEM;
esp_http_client_config_t cfg={.url=url,.crt_bundle_attach=esp_crt_bundle_attach,.timeout_ms=20000,
.disable_auto_redirect=true,.buffer_size=16384,.buffer_size_tx=512,.keep_alive_enable=false};
esp_http_client_handle_t client=esp_http_client_init(&cfg);if(!client){heap_caps_free(buf);return ESP_ERR_NO_MEM;}
esp_ota_handle_t handle=0;bool active=false;
unsigned char hash[32];mbedtls_sha256_context sha;mbedtls_sha256_init(&sha);
const char *stage="open";
esp_err_t err=ota_http_timeout(client,deadline);
if(err==ESP_OK)err=esp_http_client_open(client,0);
if(err==ESP_OK && esp_timer_get_time()>=deadline){stage="deadline";err=ESP_ERR_TIMEOUT;}
if(err==ESP_OK){stage="headers";err=ota_http_timeout(client,deadline);}
if(err==ESP_OK){int64_t length=esp_http_client_fetch_headers(client);
if(esp_http_client_get_status_code(client)!=200 || length!=(int64_t)state.size)err=ESP_ERR_INVALID_SIZE;}
if(err==ESP_OK && esp_timer_get_time()>=deadline){stage="deadline";err=ESP_ERR_TIMEOUT;}
if(err==ESP_OK){
stage="begin";int64_t started=esp_timer_get_time();
// A known length lets the SDK combine aligned 4KiB sectors into 64KiB
// erases before receiving the body. Never erase the running A/B slot.
err=esp_ota_begin(target,state.size,&handle);active=handle!=0;
ota_elapsed(started,&perf.begin_us,NULL);
// The SDK may assign its handle before an erase failure; abort it below.
}
if(err==ESP_OK && esp_timer_get_time()>=deadline){stage="deadline";err=ESP_ERR_TIMEOUT;}
if(err==ESP_OK){stage="sha.init";int64_t started=esp_timer_get_time();
if(mbedtls_sha256_starts(&sha,0)!=0)err=ESP_FAIL;
ota_elapsed(started,&perf.sha_us,&perf.sha_max_us);}
uint32_t total=0;int64_t heartbeat_due=0,progress_due=perf.start_us+30000000;
ESP_LOGI(TAG,"OTA buffer: internal=%u free_internal=%u largest_internal=%u erase_ms=%lu",
(unsigned)buf_size,(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT),ota_ms(perf.begin_us));
unsigned stalled_reads=0;int read_rc=0,socket_errno=0;
while(err==ESP_OK && total<state.size){
if(esp_timer_get_time()>=heartbeat_due){device_control_heartbeat();heartbeat_due=esp_timer_get_time()+30000000;}
stage="timeout";err=ota_http_timeout(client,deadline);
if(err!=ESP_OK){if(err==ESP_ERR_TIMEOUT)stage="deadline";break;}
size_t wanted=state.size-total;if(wanted>buf_size)wanted=buf_size;
stage="read";int64_t started=esp_timer_get_time();
int count=esp_http_client_read(client,(char*)buf,wanted);read_rc=count;perf.reads++;
int64_t read_before=perf.read_us;
ota_elapsed(started,&perf.read_us,&perf.read_max_us);
socket_errno=count<=0?esp_http_client_get_errno(client):0;
if(count>0 && (size_t)count<wanted)perf.partial_reads++;
if(count==-ESP_ERR_HTTP_EAGAIN)perf.eagain_reads++;
if(perf.read_us-read_before>=1000000)ESP_LOGI(TAG,"OTA slow read: off=%lu wanted=%u rc=%d ms=%lu",
(unsigned long)total,(unsigned)wanted,count,ota_ms(perf.read_us-read_before));
if(esp_timer_get_time()>=deadline){stage="deadline";err=ESP_ERR_TIMEOUT;break;}
// A timed-out read leaves this connection and write offset intact.
// Three consecutive 20s waits bound a stall; real progress resets it.
if(count==-ESP_ERR_HTTP_EAGAIN){if(++stalled_reads<3)continue;err=ESP_ERR_HTTP_EAGAIN;break;}
if(count<=0){err=ESP_FAIL;break;}
stalled_reads=0;stage="write";
started=esp_timer_get_time();err=esp_ota_write(handle,buf,count);perf.writes++;
ota_elapsed(started,&perf.write_us,&perf.write_max_us);
if(err!=ESP_OK)break;
total+=count;
stage="sha.upd";started=esp_timer_get_time();if(mbedtls_sha256_update(&sha,buf,count)!=0)err=ESP_FAIL;
ota_elapsed(started,&perf.sha_us,&perf.sha_max_us);
if(err==ESP_OK && esp_timer_get_time()>=deadline){stage="deadline";err=ESP_ERR_TIMEOUT;}
int64_t now=esp_timer_get_time();
if(now>=progress_due){
ESP_LOGI(TAG,"OTA progress: bytes=%lu/%lu ms=%lu read_ms=%lu write_ms=%lu",
(unsigned long)total,(unsigned long)state.size,ota_ms(now-perf.start_us),ota_ms(perf.read_us),ota_ms(perf.write_us));
progress_due=now+30000000;
}
}
if(err==ESP_OK){stage="sha.end";char hex[65];int64_t started=esp_timer_get_time();if(mbedtls_sha256_finish(&sha,hash)!=0)err=ESP_FAIL;
else{hash_hex(hash,hex);if(strcmp(hex,state.sha))err=ESP_ERR_INVALID_CRC;
else ESP_LOGI(TAG,"OTA image digest verified: bytes=%lu",(unsigned long)total);}
ota_elapsed(started,&perf.sha_us,&perf.sha_max_us);}
if(err==ESP_OK && esp_timer_get_time()>=deadline){stage="deadline";err=ESP_ERR_TIMEOUT;}
// Release TLS and the DRAM write buffer before allocating the temporary
// verification stack, keeping the receive and verification peaks separate.
mbedtls_sha256_free(&sha);heap_caps_free(buf);buf=NULL;
esp_http_client_close(client);esp_http_client_cleanup(client);client=NULL;
if(err==ESP_OK && esp_timer_get_time()>=deadline){stage="deadline";err=ESP_ERR_TIMEOUT;}
if(err==ESP_OK){stage="end";int64_t started=esp_timer_get_time();bool completed=false;
err=ota_internal_flash_call(OTA_FLASH_END,handle,NULL,&completed);if(completed)active=false;
ota_elapsed(started,&perf.end_us,NULL);}
int64_t elapsed=esp_timer_get_time()-perf.start_us;
if(err==ESP_OK && elapsed>=900000000){stage="deadline";err=ESP_ERR_TIMEOUT;}
ESP_LOGI(TAG,"OTA timing: bytes=%lu total_ms=%lu erase_ms=%lu read_ms=%lu/%lu write_ms=%lu/%lu sha_ms=%lu/%lu end_ms=%lu reads=%lu writes=%lu partial=%lu eagain=%lu",
(unsigned long)total,ota_ms(elapsed),ota_ms(perf.begin_us),ota_ms(perf.read_us),ota_ms(perf.read_max_us),
ota_ms(perf.write_us),ota_ms(perf.write_max_us),ota_ms(perf.sha_us),ota_ms(perf.sha_max_us),ota_ms(perf.end_us),
(unsigned long)perf.reads,(unsigned long)perf.writes,(unsigned long)perf.partial_reads,(unsigned long)perf.eagain_reads);
if(err!=ESP_OK)snprintf(state.message,sizeof(state.message),
"OTA %s e=0x%x off=%lu/%lu rc=%d errno=%d; original retained",stage,(unsigned)err,
(unsigned long)total,(unsigned long)state.size,read_rc,socket_errno);
else snprintf(state.message,sizeof(state.message),"OTA ms=%lu erase=%lu read=%lu write=%lu sha=%lu buf=%u",
ota_ms(elapsed),ota_ms(perf.begin_us),ota_ms(perf.read_us),ota_ms(perf.write_us),ota_ms(perf.sha_us),(unsigned)buf_size);
if(active)esp_ota_abort(handle);
return err;
}
void device_ota_poll(void){
if(!ready)return;
if(state.phase==3 || state.phase==4){
if(!report(state.phase==3?"success":"failed"))return;
uint32_t previous=state.phase;state.phase=0;
if(save()!=ESP_OK){state.phase=previous;return;}
ESP_LOGI(TAG,"OTA terminal receipt settled");
}
char id[37];if(!requests || xQueueReceive(requests,id,0)!=pdTRUE)return;
if(!strcmp(id,state.id))return;
if(strcmp(id,retry_id)){memcpy(retry_id,id,sizeof(retry_id));manifest_retries=0;}
bool idle=call_client_enter_maintenance();
cJSON *j=cJSON_CreateObject(),*manifest=NULL;cJSON_AddStringToObject(j,"task_id",id);
esp_err_t err=post("/api/v1/device/ota/manifest",j,&manifest);cJSON_Delete(j);
if(err!=ESP_OK){
// MQTT QoS acknowledges delivery before this worker fetches HTTPS. A
// transient HTTP error must not silently discard the only wakeup.
// Re-fetch at most twice; the server still enforces the original 30s
// acceptance deadline and no URL/digest from MQTT is ever trusted.
if(strcmp(platform_last_error_code(),"OTA_TASK_UNAVAILABLE") && ++manifest_retries<=2)device_ota_request(id);
if(idle)call_client_leave_maintenance();
return;
}
retry_id[0]=0;manifest_retries=0;
const esp_partition_t *target=esp_ota_get_next_update_partition(NULL);
const char *digest=str(manifest,"sha256"),*version=str(manifest,"version");
int64_t code=number(manifest,"version_code"),size=number(manifest,"size");
struct timeval now;gettimeofday(&now,NULL);
bool valid=target && size>=1024 && size<=target->size && code>device_ota_version_code() &&
!strcmp(id,str(manifest,"task_id")) && !strcmp(ble_factory_model(),str(manifest,"model")) &&
strlen(digest)==64 && strspn(digest,"0123456789abcdef")==64 && strlen(version)>0 && strlen(version)<81 &&
(int64_t)now.tv_sec*1000+now.tv_usec/1000<number(manifest,"expires_at_ms") && same_origin_download(str(manifest,"download_url"));
if(!valid || !idle){
memset(&state,0,sizeof(state));state.format=1;state.phase=4;
snprintf(state.id,sizeof(state.id),"%s",id);
snprintf(state.message,sizeof(state.message),"%s",idle?"Image incompatible, too large, obsolete or expired":"Device busy; conversation was not interrupted");
save();cJSON_Delete(manifest);if(idle)call_client_leave_maintenance();return;
}
memset(&state,0,sizeof(state));state.format=1;state.phase=1;state.address=target->address;state.size=size;state.code=code;
snprintf(state.id,sizeof(state.id),"%s",id);snprintf(state.version,sizeof(state.version),"%s",version);snprintf(state.sha,sizeof(state.sha),"%s",digest);
err=save();
if(err==ESP_OK && !report("accepted"))err=ESP_FAIL;
if(err==ESP_OK)ESP_LOGI(TAG,"OTA accepted: target_slot=0x%lx bytes=%lu version_code=%lld",(unsigned long)target->address,(unsigned long)state.size,(long long)state.code);
if(err==ESP_OK)err=download(str(manifest,"download_url"),target);
cJSON_Delete(manifest);
if(err==ESP_OK){state.phase=2;err=save();
if(err!=ESP_OK)snprintf(state.message,sizeof(state.message),"OTA journal failed: %s; original retained",esp_err_to_name(err));}
if(err==ESP_OK){err=ota_internal_flash_call(OTA_FLASH_BOOT,0,target,NULL);
if(err!=ESP_OK)snprintf(state.message,sizeof(state.message),"OTA boot selection failed: %s; original retained",esp_err_to_name(err));}
if(err==ESP_OK){ESP_LOGI(TAG,"Verified image installed; restarting for boot validation");device_control_prepare_restart();esp_restart();}
state.phase=4;if(!state.message[0])snprintf(state.message,sizeof(state.message),"OTA failed: %s; original firmware retained",esp_err_to_name(err));save();
call_client_leave_maintenance();
}