#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 #include #include #include #include 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->subtypesubtype>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 && offset4096)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=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=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/1000address;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(); }