feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
@@ -0,0 +1,602 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
#include <setjmp.h>
#include <openssl/sha.h>
typedef int esp_err_t;
typedef int nvs_handle_t;
#define ESP_OK 0
#define ESP_FAIL -1
#define ESP_ERR_NO_MEM 1
#define ESP_ERR_INVALID_RESPONSE 2
#define ESP_ERR_NOT_FOUND 3
#define ESP_ERR_INVALID_ARG 4
#define ESP_ERR_INVALID_SIZE 5
#define ESP_ERR_TIMEOUT 6
#define ESP_ERR_INVALID_CRC 7
#define ESP_ERR_NVS_NOT_FOUND 8
#define ESP_ERR_HTTP_EAGAIN 0x7007
#define NVS_READONLY 0
#define NVS_READWRITE 1
#define DEVICE_FIRMWARE_VERSION "host-original"
#define ESP_PARTITION_SUBTYPE_APP_OTA_MIN 16
#define ESP_PARTITION_SUBTYPE_APP_OTA_MAX 31
#define ESP_OTA_IMG_PENDING_VERIFY 1
#define ESP_OTA_IMG_VALID 2
#define ESP_LOGI(...) ((void)0)
#define ESP_LOGE(...) ((void)0)
#define ESP_LOGW(...) ((void)0)
#define MALLOC_CAP_INTERNAL (1U<<11)
#define MALLOC_CAP_8BIT (1U<<2)
#define OTA_WITH_SEQUENTIAL_WRITES 0
#define pdTRUE 1
#define pdPASS 1
#define portMAX_DELAY UINT32_MAX
typedef struct { uint32_t address,size,subtype; } esp_partition_t;
typedef int esp_ota_img_states_t;
typedef int esp_ota_handle_t;
static const esp_partition_t parts[]={{0x10000,131072,16},{0x410000,131072,17}};
static uint8_t flash[2][131072],image_bytes[65536+123];
static size_t image_size=2048;
static int running,boot_partition,image_states[2],mark_error,boot_error,end_error;
static int begun,written,aborted,restarts,rollbacks,marked,downloaded,reported_success,reported_failed,accepted;
static int manifest_requests,manifest_error,report_error,busy,maintenance,leaves,http_status,header_size,short_read,read_error;
static int timer_create_error,timer_start_error,timer_armed,timer_deleted;
static int64_t monotonic,clock_step;
static int read_script[64],read_script_len,read_script_index,read_calls,read_timeout_ms,http_errno;
static size_t read_at_offsets[64],read_wanted[64],write_sizes[64];
static int timeout_values[64],timeout_count,write_calls,write_fail_on,write_fail_code;
static int64_t read_advances[64],heartbeat_advance,write_advance,begin_advance,open_advance,header_advance,end_advance;
static int64_t sha_start_advance,sha_update_advance,sha_finish_advance;
static int begin_error,begin_error_handle,open_error,http_init_error,sha_error_stage;
static int opened,headers_fetched,closed,client_cleaned,client_live,ota_handle_live,ended,boot_sets,transfer_first_read_call;
static size_t begin_size;
static const esp_partition_t *begin_target;
static unsigned alloc_calls,heap_fail_first,heap_frees,heap_live;
static size_t alloc_sizes[8],allocated_size;
static unsigned alloc_caps[8];
static void *allocated_buffer;
static size_t read_offset,write_offset;
static char last_report[16];
static char last_report_message[100];
static cJSON *manifest_json;
static jmp_buf restart_jump;
static bool allow_restart;
// Execute the real production worker synchronously when the parent takes the
// semaphore. Suspend escapes with longjmp; parent must join/delete before reset.
// This proves SDK-call context and ownership, not ESP32 cache/MMU behavior.
typedef struct fake_sem { bool given; } *SemaphoreHandle_t;
typedef struct fake_task { void (*entry)(void *); void *arg; bool suspended; } *TaskHandle_t;
static TaskHandle_t live_worker;
static SemaphoreHandle_t live_sem;
static bool internal_worker_context;
static jmp_buf worker_suspend_jump;
static int sem_attempts,sem_created,sem_deleted,task_attempts,task_created,task_deleted;
static int sem_fail_on,task_fail_on,worker_runs,sem_gives,sem_takes,sha_live,rollback_error;
static int64_t cleanup_advance;
static SemaphoreHandle_t xSemaphoreCreateBinary(void){
assert(!internal_worker_context&&!live_sem&&!live_worker);
if(++sem_attempts==sem_fail_on)return NULL;
live_sem=calloc(1,sizeof(*live_sem));assert(live_sem);sem_created++;return live_sem;
}
static int xTaskCreateWithCaps(void (*entry)(void *),const char *name,unsigned stack,void *arg,unsigned priority,TaskHandle_t *result,unsigned caps){
assert(!internal_worker_context&&live_sem&&!live_worker);
assert(!strcmp(name,"ota_flash")&&stack==8192&&priority==3&&caps==(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
// The real TLS client, internal write buffer and SHA context must be released
// before the temporary 8KiB DRAM stack allocation, including low-memory cases.
assert(!client_live&&!heap_live&&!sha_live);
if(++task_attempts==task_fail_on)return 0;
live_worker=calloc(1,sizeof(*live_worker));assert(live_worker);
live_worker->entry=entry;live_worker->arg=arg;*result=live_worker;task_created++;return pdPASS;
}
static int xSemaphoreGive(SemaphoreHandle_t sem){
assert(internal_worker_context&&sem==live_sem&&!sem->given);sem->given=true;sem_gives++;return pdTRUE;
}
static void vTaskSuspend(TaskHandle_t task){
assert(internal_worker_context&&task==NULL&&live_worker&&live_sem->given);
live_worker->suspended=true;longjmp(worker_suspend_jump,1);
}
static int xSemaphoreTake(SemaphoreHandle_t sem,unsigned timeout){
assert(!internal_worker_context&&sem==live_sem&&live_worker&&timeout==portMAX_DELAY);
assert(!sem->given);worker_runs++;internal_worker_context=true;
if(!setjmp(worker_suspend_jump)){live_worker->entry(live_worker->arg);assert(!"worker returned without suspend");}
internal_worker_context=false;assert(sem->given&&live_worker->suspended);sem_takes++;return pdTRUE;
}
static void vTaskDeleteWithCaps(TaskHandle_t task){
assert(!internal_worker_context&&task==live_worker&&task->suspended&&live_sem->given);
free(task);live_worker=NULL;task_deleted++;
}
static void vSemaphoreDelete(SemaphoreHandle_t sem){
assert(!internal_worker_context&&sem==live_sem&&!live_worker);
free(sem);live_sem=NULL;sem_deleted++;
}
static void assert_worker_cleanup(void){
assert(!live_worker&&!live_sem&&!internal_worker_context);
assert(task_created==task_deleted&&sem_created==sem_deleted);
assert(worker_runs==sem_gives&&sem_gives==sem_takes&&sem_takes==task_deleted);
}
static const char *platform_error="";
static const char *task="40000000-0000-4000-8000-000000000001";
static struct {char key[16];unsigned char bytes[512];size_t size;} records[8];
static int record_count,fail_save_phase,fail_installed,fail_readback;
static bool fail_receipt_commit;
static unsigned char staged[512];static size_t staged_size;static char staged_key[16];
static void *heap_caps_malloc(size_t size,unsigned caps){
assert(alloc_calls<8);alloc_sizes[alloc_calls]=size;alloc_caps[alloc_calls]=caps;
if(++alloc_calls<=heap_fail_first)return NULL;
assert(!heap_live);allocated_buffer=malloc(size);assert(allocated_buffer);allocated_size=size;heap_live++;return allocated_buffer;
}
static void heap_caps_free(void *buffer){
if(!buffer)return;assert(heap_live==1&&buffer==allocated_buffer);free(buffer);allocated_buffer=NULL;heap_live--;heap_frees++;
}
static int nvs_open(const char *space,int mode,int *handle){assert(!strcmp(space,"control"));*handle=1;return 0;}
static void nvs_close(int n){}
static int nvs_set_blob(int n,const char *key,const void *bytes,size_t size){assert(size<=sizeof(staged)&&strlen(key)<sizeof(staged_key));strcpy(staged_key,key);memcpy(staged,bytes,size);staged_size=size;return 0;}
static int nvs_commit(int n){
if(!strcmp(staged_key,"ota")){uint32_t phase;memcpy(&phase,staged+4,4);if((fail_save_phase&&phase==(unsigned)fail_save_phase)||(fail_receipt_commit&&phase==0))return ESP_FAIL;}
if(!strncmp(staged_key,"installed",9)&&fail_installed)return ESP_FAIL;
int i;for(i=0;i<record_count;i++)if(!strcmp(records[i].key,staged_key))break;
assert(i<8);if(i==record_count)record_count++;strcpy(records[i].key,staged_key);memcpy(records[i].bytes,staged,staged_size);records[i].size=staged_size;return 0;
}
static int nvs_get_blob(int n,const char *key,void *bytes,size_t *size){
for(int i=0;i<record_count;i++)if(!strcmp(records[i].key,key)){if(*size<records[i].size)return ESP_ERR_INVALID_SIZE;*size=records[i].size;memcpy(bytes,records[i].bytes,*size);if(fail_readback)return ESP_FAIL;return 0;}return ESP_ERR_NVS_NOT_FOUND;
}
typedef struct {char item[37];bool occupied;} StaticQueue_t;
typedef StaticQueue_t *QueueHandle_t;
static QueueHandle_t xQueueCreateStatic(int n,int size,uint8_t *storage,StaticQueue_t *q){assert(n==1&&size==37);q->occupied=false;return q;}
static int xQueueSend(QueueHandle_t q,const void *item,int timeout){if(q->occupied)return 0;memcpy(q->item,item,37);q->occupied=true;return pdTRUE;}
static int xQueueReceive(QueueHandle_t q,void *item,int timeout){if(!q->occupied)return 0;memcpy(item,q->item,37);q->occupied=false;return pdTRUE;}
typedef struct {void (*callback)(void *);const char *name;} esp_timer_create_args_t;
typedef void *esp_timer_handle_t;
static void (*timer_callback)(void *);
static int esp_timer_create(const esp_timer_create_args_t *a,esp_timer_handle_t *h){if(timer_create_error)return ESP_FAIL;*h=(void*)1;timer_callback=a->callback;return 0;}
static int esp_timer_start_once(esp_timer_handle_t h,int64_t us){assert(us==180000000);if(timer_start_error)return ESP_FAIL;timer_armed=1;return 0;}
static int esp_timer_stop(esp_timer_handle_t h){timer_armed=0;return 0;}
static int esp_timer_delete(esp_timer_handle_t h){timer_deleted++;return 0;}
static int64_t esp_timer_get_time(void){monotonic+=clock_step;return monotonic;}
static const esp_partition_t *esp_ota_get_running_partition(void){return &parts[running];}
static const esp_partition_t *esp_ota_get_next_update_partition(const esp_partition_t *p){return &parts[1-running];}
static int esp_ota_get_state_partition(const esp_partition_t *p,int *value){*value=image_states[p==&parts[1]];return *value<0?ESP_ERR_NOT_FOUND:0;}
static int esp_ota_set_boot_partition(const esp_partition_t *p){assert(internal_worker_context&&!heap_live&&!client_live&&!sha_live);boot_sets++;if(boot_error)return ESP_FAIL;boot_partition=p==&parts[1];return 0;}
static int esp_ota_mark_app_valid_cancel_rollback(void){if(mark_error)return ESP_FAIL;marked++;image_states[running]=ESP_OTA_IMG_VALID;return 0;}
static void esp_restart(void){assert_worker_cleanup();restarts++;assert(allow_restart);longjmp(restart_jump,1);}
static int esp_ota_mark_app_invalid_rollback(void){assert(internal_worker_context);rollbacks++;if(rollback_error)return ESP_FAIL;boot_partition=1-running;return 0;}
static int esp_partition_read(const esp_partition_t *p,uint32_t offset,void *bytes,size_t size){assert(offset+size<=p->size);memcpy(bytes,flash[p==&parts[1]]+offset,size);return 0;}
static int esp_ota_begin(const esp_partition_t *p,size_t size,esp_ota_handle_t *handle){
assert(p==&parts[1-running]&&read_calls==transfer_first_read_call&&!ota_handle_live);begun++;begin_target=p;begin_size=size;write_offset=0;
assert(size>=1024&&size<=p->size);monotonic+=begin_advance;
if(!begin_error||begin_error_handle){*handle=(p==&parts[1])+1;ota_handle_live=*handle;}
if(begin_error)return begin_error;
size_t erased=(size+4095)&~(size_t)4095;assert(erased<=p->size);memset(flash[p==&parts[1]],0xff,erased);return 0;
}
static int esp_ota_write(esp_ota_handle_t handle,const void *bytes,size_t size){
assert(handle==ota_handle_live&&handle-1!=running&&write_calls<64);write_sizes[write_calls++]=size;monotonic+=write_advance;
if(write_fail_on&&write_calls==write_fail_on)return write_fail_code;
assert(write_offset+size<=parts[handle-1].size);memcpy(flash[handle-1]+write_offset,bytes,size);write_offset+=size;written++;return 0;
}
static int esp_ota_end(esp_ota_handle_t handle){assert(internal_worker_context&&!heap_live&&!client_live&&!sha_live);assert(handle==ota_handle_live);ended++;monotonic+=end_advance;ota_handle_live=0;return end_error?ESP_FAIL:0;}
static int esp_ota_abort(esp_ota_handle_t handle){assert(handle==ota_handle_live);ota_handle_live=0;aborted++;return 0;}
static const char *esp_err_to_name(int err){return "INJECTED_FAILURE";}
static int esp_crt_bundle_attach(void *v){return 0;}
typedef struct {const char *url;int (*crt_bundle_attach)(void *);int timeout_ms,disable_auto_redirect,buffer_size,buffer_size_tx,keep_alive_enable;} esp_http_client_config_t;
typedef void *esp_http_client_handle_t;
static void *esp_http_client_init(const esp_http_client_config_t *cfg){assert(cfg->disable_auto_redirect&&cfg->crt_bundle_attach&&cfg->buffer_size==16384&&!client_live);read_offset=0;transfer_first_read_call=read_calls;read_timeout_ms=cfg->timeout_ms;downloaded++;if(http_init_error)return NULL;client_live=1;return (void*)1;}
static int esp_http_client_open(void *h,int length){assert(h==(void*)1&&client_live);opened++;monotonic+=open_advance;return open_error;}
static int64_t esp_http_client_fetch_headers(void *h){assert(h==(void*)1&&client_live);headers_fetched++;monotonic+=header_advance;return header_size;}
static int esp_http_client_get_status_code(void *h){return http_status;}
static int esp_http_client_set_timeout_ms(void *h,int timeout_ms){assert(h==(void*)1&&client_live&&timeout_ms>0&&timeout_ms<=20000&&timeout_count<64);timeout_values[timeout_count++]=timeout_ms;read_timeout_ms=timeout_ms;return 0;}
static int esp_http_client_get_errno(void *h){assert(h==(void*)1);return http_errno;}
static int esp_http_client_read(void *h,char *buf,size_t size){
assert(h==(void*)1&&client_live&&read_calls<64&&heap_live&&buf==allocated_buffer&&size<=allocated_size);int index=read_calls++;read_at_offsets[index]=read_offset;read_wanted[index]=size;
int result=read_error?-1:short_read?0:read_script_index<read_script_len?read_script[read_script_index++]:(int)size;
http_errno=result==-ESP_ERR_HTTP_EAGAIN?11:result<0?5:0;
monotonic+=read_advances[index];if(result==-ESP_ERR_HTTP_EAGAIN)monotonic+=(int64_t)read_timeout_ms*1000;
if(result<=0)return result;
assert((size_t)result<=size&&read_offset+(size_t)result<=image_size);memcpy(buf,image_bytes+read_offset,result);read_offset+=result;return result;
}
static void esp_http_client_close(void *h){assert(h==(void*)1&&client_live);closed++;}
static void esp_http_client_cleanup(void *h){assert(h==(void*)1&&client_live);client_live=0;client_cleaned++;monotonic+=cleanup_advance;}
typedef SHA256_CTX mbedtls_sha256_context;
static void mbedtls_sha256_init(mbedtls_sha256_context *ctx){sha_live++;memset(ctx,0,sizeof(*ctx));}
static void mbedtls_sha256_free(mbedtls_sha256_context *ctx){assert(sha_live>0);sha_live--;}
static int mbedtls_sha256_starts(mbedtls_sha256_context *ctx,int is224){monotonic+=sha_start_advance;if(sha_error_stage==1)return -1;return SHA256_Init(ctx)?0:-1;}
static int mbedtls_sha256_update(mbedtls_sha256_context *ctx,const void *bytes,size_t size){monotonic+=sha_update_advance;if(sha_error_stage==2)return -1;return SHA256_Update(ctx,bytes,size)?0:-1;}
static int mbedtls_sha256_finish(mbedtls_sha256_context *ctx,unsigned char hash[32]){monotonic+=sha_finish_advance;if(sha_error_stage==3)return -1;return SHA256_Final(hash,ctx)?0:-1;}
static void mbedtls_platform_zeroize(void *bytes,size_t size){memset(bytes,0,size);}
static bool call_client_enter_maintenance(void){if(busy)return false;assert(!maintenance);maintenance=1;return true;}
static void call_client_leave_maintenance(void){assert(maintenance);maintenance=0;leaves++;}
static void device_control_heartbeat(void){monotonic+=heartbeat_advance;heartbeat_advance=0;}
static void device_control_prepare_restart(void){}
static const char *ble_cloud_origin(void){return "https://platform.example";}
static const char *ble_factory_model(void){return "ECHOEAR-S3";}
static const char *platform_last_error_code(void){return platform_error;}
const char *device_ota_version(void);
int64_t device_ota_version_code(void);
static int platform_request(const char *method,const char *path,const char *body,const char *id,char **raw){
assert(!strcmp(method,"POST"));*raw=NULL;cJSON *request=cJSON_Parse(body);assert(request);
assert(!strcmp(cJSON_GetObjectItem(request,"task_id")->valuestring,task));
if(!strcmp(path,"/api/v1/device/ota/manifest")){manifest_requests++;cJSON_Delete(request);if(manifest_error)return ESP_FAIL;cJSON *root=cJSON_CreateObject();cJSON_AddItemToObject(root,"data",cJSON_Duplicate(manifest_json,true));*raw=cJSON_PrintUnformatted(root);cJSON_Delete(root);return 0;}
assert(!strcmp(path,"/api/v1/device/ota/report"));const char *status=cJSON_GetObjectItem(request,"status")->valuestring;snprintf(last_report,sizeof(last_report),"%s",status);
const cJSON *message=cJSON_GetObjectItem(request,"message");snprintf(last_report_message,sizeof(last_report_message),"%s",cJSON_IsString(message)?message->valuestring:"");
if(!strcmp(status,"accepted"))accepted++;else if(!strcmp(status,"success"))reported_success++;else if(!strcmp(status,"failed"))reported_failed++;else assert(false);
cJSON_Delete(request);if(report_error)return ESP_FAIL;*raw=strdup("{\"data\":{}}");return 0;
}
/* PRODUCTION_OTA */
static void reboot_to(int slot){
running=slot;maintenance=0;ready=false;boot_timer=NULL;requests=NULL;timer_armed=0;timer_callback=NULL;memset(&state,0,sizeof(state));memset(&installed,0,sizeof(installed));
device_ota_boot_guard();
}
static void fresh(void){
assert(!heap_live&&!client_live&&!ota_handle_live&&!sha_live);assert_worker_cleanup();
sem_attempts=sem_created=sem_deleted=task_attempts=task_created=task_deleted=0;
sem_fail_on=task_fail_on=worker_runs=sem_gives=sem_takes=rollback_error=0;cleanup_advance=0;
task="40000000-0000-4000-8000-000000000001";
cJSON_Delete(manifest_json);manifest_json=NULL;
running=boot_partition=record_count=begun=written=aborted=restarts=rollbacks=marked=downloaded=reported_success=reported_failed=accepted=manifest_requests=0;
mark_error=boot_error=end_error=manifest_error=report_error=busy=maintenance=leaves=short_read=read_error=timer_create_error=timer_start_error=timer_deleted=0;
fail_save_phase=fail_installed=fail_readback=0;fail_receipt_commit=false;last_report[0]=0;monotonic=0;clock_step=1000;
read_script_len=read_script_index=read_calls=timeout_count=write_calls=write_fail_on=http_errno=0;write_fail_code=95;
heartbeat_advance=write_advance=begin_advance=open_advance=header_advance=end_advance=sha_start_advance=sha_update_advance=sha_finish_advance=0;
begin_error=open_error=http_init_error=sha_error_stage=opened=headers_fetched=closed=client_cleaned=ended=boot_sets=transfer_first_read_call=0;begin_error_handle=1;begin_target=NULL;begin_size=0;
alloc_calls=heap_fail_first=heap_frees=0;allocated_size=0;allocated_buffer=NULL;image_size=2048;
memset(alloc_sizes,0,sizeof(alloc_sizes));memset(alloc_caps,0,sizeof(alloc_caps));memset(write_sizes,0,sizeof(write_sizes));memset(read_wanted,0,sizeof(read_wanted));
memset(read_script,0,sizeof(read_script));memset(read_at_offsets,0,sizeof(read_at_offsets));memset(read_advances,0,sizeof(read_advances));last_report_message[0]=0;
platform_error="";
image_states[0]=ESP_OTA_IMG_VALID;image_states[1]=ESP_OTA_IMG_PENDING_VERIFY;
for(size_t i=0;i<sizeof(image_bytes);i++)image_bytes[i]=(i*31+17)%251;
memset(flash,0,sizeof(flash));header_size=image_size;http_status=200;
unsigned char hash[32];char hex[65];SHA256(image_bytes,image_size,hash);hash_hex(hash,hex);
struct timeval now;gettimeofday(&now,NULL);
manifest_json=cJSON_CreateObject();cJSON_AddStringToObject(manifest_json,"task_id",task);cJSON_AddStringToObject(manifest_json,"model","ECHOEAR-S3");
cJSON_AddStringToObject(manifest_json,"version","new-11");cJSON_AddNumberToObject(manifest_json,"version_code",11);cJSON_AddNumberToObject(manifest_json,"size",image_size);
cJSON_AddStringToObject(manifest_json,"sha256",hex);cJSON_AddStringToObject(manifest_json,"download_url","https://platform.example/f/test-ticket");cJSON_AddNumberToObject(manifest_json,"expires_at_ms",(double)now.tv_sec*1000+30000);
reboot_to(0);device_ota_boot_ready();assert(ready);
}
static void set_string(const char *key,const char *value){assert(cJSON_ReplaceItemInObject(manifest_json,key,cJSON_CreateString(value)));}
static void set_number(const char *key,double value){assert(cJSON_ReplaceItemInObject(manifest_json,key,cJSON_CreateNumber(value)));}
static void install(void){device_ota_request(task);allow_restart=true;if(!setjmp(restart_jump))device_ota_poll();allow_restart=false;}
static void boot_ready_allow_rollback(void){allow_restart=true;if(!setjmp(restart_jump))device_ota_boot_ready();allow_restart=false;}
static void original_record(void){installed_t old={.format=1,.address=parts[0].address,.code=10};strcpy(old.version,"old-10");assert(persist("installed",&old,sizeof(old))==0);reboot_to(0);device_ota_boot_ready();assert(device_ota_version_code()==10);}
static void test_normal(void){
fresh();original_record();install();assert(restarts==1&&boot_partition==1&&state.phase==2&&accepted==1);assert(!memcmp(flash[1],image_bytes,image_size));
reboot_to(1);assert(timer_armed);device_ota_boot_ready();assert(ready&&state.phase==3&&device_ota_version_code()==11&&marked==1&&!timer_armed);
device_ota_poll();assert(reported_success==1&&state.phase==0);int prior=manifest_requests;device_ota_request(task);device_ota_poll();assert(prior==manifest_requests&&downloaded==1);
reboot_to(1);device_ota_boot_ready();assert(device_ota_version_code()==11);
}
static void test_rejections(void){
for(int i=0;i<8;i++){fresh();switch(i){case 0:set_string("model","wrong");break;case 1:set_number("size",999999);break;case 2:set_number("version_code",0);break;case 3:set_number("expires_at_ms",1);break;case 4:set_string("sha256","bad");break;case 5:set_string("download_url","https://evil.example/f/test-ticket");break;case 6:set_string("task_id","wrong");break;case 7:busy=1;break;}
install();assert(!begun&&!restarts&&boot_partition==0&&state.phase==4&&!maintenance);device_ota_poll();assert(reported_failed==1);}
fresh();manifest_error=1;install();assert(!begun&&!restarts&&!maintenance);
}
static void test_download_failures(void){
for(int i=0;i<7;i++){fresh();switch(i){case 0:image_bytes[0]^=1;break;case 1:http_status=302;break;case 2:header_size--;break;case 3:short_read=1;break;case 4:read_error=1;break;case 5:end_error=1;break;case 6:clock_step=500000000;break;}
install();assert(!restarts&&boot_partition==0&&state.phase==4&&!maintenance);device_ota_poll();assert(reported_failed==1);}
for(int phase=1;phase<=2;phase++){fresh();fail_save_phase=phase;install();assert(!restarts&&boot_partition==0&&!maintenance);if(phase==1)assert(!accepted&&!begun);}
fresh();report_error=1;install();assert(!begun&&!restarts&&!maintenance);
fresh();boot_error=1;install();assert(!restarts&&boot_partition==0&&state.phase==4);
}
static void script_reads(const int *values,size_t count){assert(count<=64);memcpy(read_script,values,count*sizeof(*values));read_script_len=count;}
static void assert_download_receipt(const char *detail){
assert(state.phase==4&&!restarts&&boot_partition==0&&!maintenance&&aborted==1);
assert(strstr(state.message,detail)&&strlen(state.message)<sizeof(state.message));
assert(!strstr(state.message,"https://")&&!strstr(state.message,"test-ticket"));
char expected[sizeof(state.message)];strcpy(expected,state.message);device_ota_poll();
assert(reported_failed==1&&!strcmp(last_report_message,expected)&&state.phase==0);
}
static void test_read_recovery(void){
fresh();clock_step=0;const int reads[]={-ESP_ERR_HTTP_EAGAIN,-ESP_ERR_HTTP_EAGAIN,512,-ESP_ERR_HTTP_EAGAIN,-ESP_ERR_HTTP_EAGAIN,1536};script_reads(reads,6);
install();assert(restarts==1&&boot_partition==1&&state.phase==2&&downloaded==1&&begun==1&&read_calls==6&&written==2);
assert(!memcmp(flash[1],image_bytes,image_size)&&read_offset==image_size&&write_offset==image_size);
assert(read_at_offsets[0]==0&&read_at_offsets[1]==0&&read_at_offsets[2]==0&&read_at_offsets[3]==512&&read_at_offsets[4]==512&&read_at_offsets[5]==512);
assert(timeout_count==8);for(int i=0;i<timeout_count;i++)assert(timeout_values[i]==20000);
assert(monotonic==80000000&&!aborted&&strstr(state.message,"OTA ms="));
}
static void test_read_stall_limit(void){
fresh();clock_step=0;const int reads[]={-ESP_ERR_HTTP_EAGAIN,-ESP_ERR_HTTP_EAGAIN,-ESP_ERR_HTTP_EAGAIN,2048};script_reads(reads,4);
install();assert(read_calls==3&&downloaded==1&&begun==1&&!written&&read_offset==0&&write_offset==0&&monotonic==60000000);
assert_download_receipt("OTA read e=0x7007 off=0/2048 rc=-28679 errno=11");
}
static void test_read_eof_and_error(void){
const int failures[]={0,-1,-0x7005};
for(size_t i=0;i<sizeof(failures)/sizeof(failures[0]);i++){
fresh();clock_step=0;const int reads[]={512,failures[i],1536};script_reads(reads,3);install();
assert(read_calls==2&&written==1&&downloaded==1&&read_offset==512&&write_offset==512);
char detail[80];snprintf(detail,sizeof(detail),"OTA read e=0xffffffff off=512/2048 rc=%d errno=%d",failures[i],failures[i]<0?5:0);
assert_download_receipt(detail);
}
}
static void test_write_failure_offset(void){
fresh();clock_step=0;const int reads[]={512,512,1024};script_reads(reads,3);write_fail_on=2;install();
assert(read_calls==2&&write_calls==2&&written==1&&read_offset==1024&&write_offset==512);
assert(!memcmp(flash[1],image_bytes,512)&&flash[1][512]==0xff);
assert_download_receipt("OTA write e=0x5f off=512/2048 rc=512 errno=0");
}
static void test_read_deadline(void){
// A pending heartbeat must not extend the fixed 900-second budget.
fresh();clock_step=0;heartbeat_advance=900000001;install();
assert(!read_calls&&timeout_count==2&&!written);assert_download_receipt("OTA deadline e=0x6 off=0/2048 rc=0 errno=0");
// The last wait is clipped to the remaining 500ms and cannot be retried.
fresh();clock_step=0;heartbeat_advance=899500000;const int stalled[]={-ESP_ERR_HTTP_EAGAIN,2048};script_reads(stalled,2);install();
assert(read_calls==1&&timeout_count==3&&timeout_values[2]==500&&monotonic==900000000&&!written);
assert_download_receipt("OTA deadline e=0x6 off=0/2048 rc=-28679 errno=11");
// Even a positive read that finishes at the deadline cannot start a write.
fresh();clock_step=0;heartbeat_advance=899500000;const int positive[]={512,1536};script_reads(positive,2);read_advances[0]=500000;install();
assert(read_calls==1&&timeout_values[2]==500&&read_offset==512&&!written&&write_offset==0);
assert_download_receipt("OTA deadline e=0x6 off=0/2048 rc=512 errno=0");
// If a write reaches the deadline, diagnostics count only completed writes.
fresh();clock_step=0;script_reads(positive,2);write_advance=900000000;install();
assert(read_calls==1&&written==1&&write_offset==512&&monotonic==900000000);
assert_download_receipt("OTA deadline e=0x6 off=512/2048 rc=512 errno=0");
}
static void test_long_download(void){
fresh();clock_step=0;const int reads[]={1024,1024};script_reads(reads,2);read_advances[0]=read_advances[1]=350000000;install();
assert(restarts==1&&boot_partition==1&&state.phase==2&&monotonic==700000000&&downloaded==1&&begun==1&&written==2&&!aborted);
assert(!memcmp(flash[1],image_bytes,image_size)&&strstr(state.message,"OTA ms="));
}
static void assert_resources_clean(unsigned allocations,unsigned frees,int aborts,int clients){
assert(alloc_calls==allocations&&heap_frees==frees&&!heap_live&&!client_live&&!ota_handle_live);
assert(aborted==aborts&&closed==clients&&client_cleaned==clients);
}
static void assert_failed_safe(const char *detail,unsigned allocations,unsigned frees,int aborts,int clients){
assert(state.phase==4&&!restarts&&!boot_sets&&boot_partition==0&&!maintenance);
assert(strstr(state.message,detail)&&!strstr(state.message,"OTA ms=")&&!strstr(state.message,"https://")&&!strstr(state.message,"test-ticket"));
assert_resources_clean(allocations,frees,aborts,clients);
char expected[sizeof(state.message)];strcpy(expected,state.message);device_ota_poll();
assert(reported_failed==1&&!strcmp(last_report_message,expected)&&state.phase==0);
}
static void test_known_size_preerase(void){
fresh();clock_step=0;memset(flash[0],0xa7,sizeof(flash[0]));install();
assert(restarts==1&&boot_partition==1&&boot_sets==1&&begun==1&&begin_target==&parts[1]&&begin_size==image_size);
assert(opened==1&&headers_fetched==1&&read_calls==1&&read_wanted[0]==image_size&&write_sizes[0]==image_size);
assert(alloc_sizes[0]==8192&&alloc_caps[0]==(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
assert(!memcmp(flash[1],image_bytes,image_size));
for(size_t i=0;i<sizeof(flash[0]);i++)assert(flash[0][i]==0xa7);
for(size_t i=image_size;i<4096;i++)assert(flash[1][i]==0xff);
assert(flash[1][4096]==0);assert_resources_clean(1,1,0,1);
}
static void test_target_guard(void){
for(int i=0;i<5;i++){
fresh();clock_step=0;state.size=image_size;
esp_partition_t alias=parts[0];const esp_partition_t *target=&parts[1];
if(i==0)target=&parts[0];else if(i==1)target=&alias;else if(i==2)target=NULL;
else if(i==3)state.size=1023;else state.size=parts[1].size+1;
assert(download("https://platform.example/f/test-ticket",target)==ESP_ERR_INVALID_SIZE);
assert(!begun&&!downloaded&&!opened&&!headers_fetched&&!read_calls&&!written&&!restarts&&!boot_sets);
assert_resources_clean(0,0,0,0);
}
}
static void test_begin_error_and_deadline(void){
for(int returns_handle=0;returns_handle<2;returns_handle++){
fresh();clock_step=0;begin_error=ESP_FAIL;begin_error_handle=returns_handle;begin_advance=7000;install();
assert(begun==1&&begin_size==image_size&&!read_calls&&!written&&!ended&&monotonic==7000);
assert_failed_safe("OTA begin e=0xffffffff off=0/2048 rc=0 errno=0",1,1,returns_handle,1);
}
// Preerase consumes the same fixed budget, rather than creating a new one.
fresh();clock_step=0;begin_advance=900000000;install();
assert(begun==1&&!read_calls&&!written&&!ended&&monotonic==900000000&&timeout_count==2);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=0 errno=0",1,1,1,1);
// Only 500ms remains after erase; the first read cannot obtain a fresh 20s.
fresh();clock_step=0;begin_advance=899500000;const int stalled[]={-ESP_ERR_HTTP_EAGAIN,2048};script_reads(stalled,2);install();
assert(read_calls==1&&timeout_count==3&&timeout_values[0]==20000&&timeout_values[1]==20000&&timeout_values[2]==500&&!written);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=-28679 errno=11",1,1,1,1);
}
static void test_setup_deadlines(void){
// Neither open nor headers gets to reset the deadline or begin erasing late.
fresh();clock_step=0;open_advance=900000000;install();
assert(opened==1&&!headers_fetched&&!begun&&!read_calls&&timeout_count==1);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=0 errno=0",1,1,0,1);
fresh();clock_step=0;header_advance=900000000;install();
assert(opened==1&&headers_fetched==1&&!begun&&!read_calls&&timeout_count==2);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=0 errno=0",1,1,0,1);
fresh();clock_step=0;open_advance=899500000;header_advance=500000;install();
assert(timeout_count==2&&timeout_values[0]==20000&&timeout_values[1]==500&&!begun&&!read_calls);
assert_failed_safe("OTA deadline e=0x6 off=0/2048 rc=0 errno=0",1,1,0,1);
// SHA finalization and SDK end are inside the budget even after all writes.
fresh();clock_step=0;sha_finish_advance=900000000;install();
assert(written==1&&write_offset==image_size&&!ended);
assert_failed_safe("OTA deadline e=0x6 off=2048/2048",1,1,1,1);
fresh();clock_step=0;end_advance=900000000;install();assert(ended==1&&written==1);
assert_failed_safe("OTA deadline e=0x6 off=2048/2048",1,1,0,1);
}
static void test_buffer_fallbacks(void){
for(unsigned failures=0;failures<2;failures++){
fresh();clock_step=0;heap_fail_first=failures;image_size=sizeof(image_bytes);header_size=image_size;
unsigned char digest[32];char hex[65];SHA256(image_bytes,image_size,digest);hash_hex(digest,hex);set_number("size",image_size);set_string("sha256",hex);
install();size_t chunk=failures?4096:8192;unsigned count=(image_size+chunk-1)/chunk;
assert(restarts==1&&boot_partition==1&&begun==1&&begin_size==image_size&&read_calls==(int)count&&written==(int)count);
assert(allocated_size==chunk&&alloc_sizes[0]==8192&&(!failures||alloc_sizes[1]==4096));
for(unsigned i=0;i<alloc_calls;i++)assert(alloc_caps[i]==(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT));
for(unsigned i=0;i<count;i++){size_t expected=i+1<count?chunk:image_size-i*chunk;assert(read_wanted[i]==expected&&write_sizes[i]==expected);}
assert(read_offset==image_size&&write_offset==image_size&&!memcmp(flash[1],image_bytes,image_size));
char expected[20];snprintf(expected,sizeof(expected),"buf=%u",(unsigned)chunk);assert(strstr(state.message,expected));
assert_resources_clean(failures+1,1,0,1);
// Verify the large image through the real post-boot flash SHA path too.
reboot_to(1);device_ota_boot_ready();assert(ready&&state.phase==3&&device_ota_version_code()==11);device_ota_poll();assert(reported_success==1);
}
fresh();clock_step=0;heap_fail_first=2;install();
assert(!downloaded&&!opened&&!headers_fetched&&!begun&&!read_calls&&!written);
assert(alloc_sizes[0]==8192&&alloc_sizes[1]==4096);assert_failed_safe("OTA failed:",2,0,0,0);
}
static void test_failure_cleanup(void){
fresh();clock_step=0;http_init_error=1;install();assert(downloaded==1&&!opened&&!begun);assert_failed_safe("OTA failed:",1,1,0,0);
fresh();clock_step=0;open_error=ESP_FAIL;install();assert(opened==1&&!headers_fetched&&!begun);assert_failed_safe("OTA open",1,1,0,1);
for(int bad_header=0;bad_header<2;bad_header++){
fresh();clock_step=0;if(bad_header)header_size--;else http_status=302;install();
assert(headers_fetched==1&&!begun&&!read_calls);assert_failed_safe("OTA headers",1,1,0,1);
}
for(int sha_stage=1;sha_stage<=3;sha_stage++){
fresh();clock_step=0;sha_error_stage=sha_stage;install();assert(begun==1&&!ended);
assert_failed_safe(sha_stage==1?"OTA sha.init":sha_stage==2?"OTA sha.upd":"OTA sha.end",1,1,1,1);
}
fresh();clock_step=0;image_bytes[0]^=1;install();assert(!ended);assert_failed_safe("OTA sha.end e=0x7",1,1,1,1);
fresh();clock_step=0;end_error=1;install();assert(ended==1);assert_failed_safe("OTA end",1,1,0,1);
// Successful download metrics must never conceal later journal or boot failure.
fresh();clock_step=0;fail_save_phase=2;install();assert(ended==1);assert_failed_safe("OTA journal failed:",1,1,0,1);
fresh();clock_step=0;boot_error=1;install();assert(ended==1&&boot_sets==1&&state.phase==4&&!restarts&&boot_partition==0);
assert(strstr(state.message,"OTA boot selection failed:")&&!strstr(state.message,"OTA ms="));assert_resources_clean(1,1,0,1);
}
static void test_success_metrics(void){
fresh();clock_step=0;begin_advance=7000;open_advance=11000;header_advance=13000;end_advance=19000;write_advance=5000;sha_update_advance=3000;
const int reads[]={1024,1024};script_reads(reads,2);read_advances[0]=17000;read_advances[1]=23000;install();
const char *expected="OTA ms=106 erase=7 read=40 write=10 sha=6 buf=8192";
assert(restarts==1&&state.phase==2&&monotonic==106000&&!strcmp(state.message,expected));assert_resources_clean(1,1,0,1);
reboot_to(1);assert(!strcmp(state.message,expected));device_ota_boot_ready();assert(state.phase==3&&!strcmp(state.message,expected));
device_ota_poll();assert(reported_success==1&&!strcmp(last_report_message,expected)&&state.phase==0);
reboot_to(1);device_ota_boot_ready();assert(device_ota_version_code()==11&&!strcmp(state.message,expected));
}
static void test_power_and_boot(void){
fresh();original_record();install();reboot_to(0);device_ota_boot_ready();assert(state.phase==4&&ready&&device_ota_version_code()==10);device_ota_poll();assert(reported_failed==1);
fresh();install();flash[1][0]^=1;reboot_to(1);boot_ready_allow_rollback();assert(rollbacks==1&&boot_partition==0&&!ready);
fresh();install();reboot_to(1);assert(timer_callback&&timer_armed);allow_restart=true;if(!setjmp(restart_jump))timer_callback(NULL);allow_restart=false;assert(restarts==2&&!ready);
fresh();fail_save_phase=2;install();reboot_to(0);fail_save_phase=0;device_ota_boot_ready();assert(ready&&state.phase==4);
}
static void test_metadata_failure(void){
fresh();original_record();install();reboot_to(1);mark_error=1;device_ota_boot_ready();assert(!ready&&timer_armed);mark_error=0;
reboot_to(0);device_ota_boot_ready();assert(ready&&state.phase==4);assert(device_ota_version_code()==10);assert(!strcmp(device_ota_version(),"old-10"));
// Once both slots have been used, failing the next candidate still restores
// the exact version metadata for the previous, already updated slot.
fresh();original_record();install();reboot_to(1);device_ota_boot_ready();device_ota_poll();
task="40000000-0000-4000-8000-000000000002";set_string("task_id",task);set_string("version","new-12");set_number("version_code",12);
install();image_states[0]=ESP_OTA_IMG_PENDING_VERIFY;reboot_to(0);mark_error=1;device_ota_boot_ready();assert(!ready);mark_error=0;
reboot_to(1);device_ota_boot_ready();assert(ready&&device_ota_version_code()==11&&!strcmp(device_ota_version(),"new-11"));
fresh();original_record();install();reboot_to(1);fail_installed=1;device_ota_boot_ready();assert(!ready&&timer_armed);fail_installed=0;reboot_to(0);device_ota_boot_ready();assert(device_ota_version_code()==10);
}
static void test_timer_failure(void){
for(int i=0;i<2;i++){fresh();install();if(i)timer_start_error=1;else timer_create_error=1;
allow_restart=true;if(!setjmp(restart_jump))reboot_to(1);allow_restart=false;assert(rollbacks==1&&boot_partition==0&&!ready);}
}
static void test_numeric_validation(void){
for(int i=0;i<3;i++){fresh();set_number("version_code",i==0?11.5:i==1?1e100:INFINITY);install();assert(!begun&&!restarts);}
}
static void test_corrupt_records(void){
fresh();original_record();fail_readback=1;reboot_to(0);device_ota_boot_ready();assert(!ready&&!device_ota_supported());device_ota_request(task);device_ota_poll();assert(!begun);
fresh();install();for(int i=0;i<record_count;i++)if(!strcmp(records[i].key,"ota"))records[i].size=3;
reboot_to(1);device_ota_boot_ready();assert(!ready&&timer_armed);assert(!marked);
}
static void test_receipt_and_manifest_recovery(void){
fresh();manifest_error=1;install();assert(manifest_requests==1&&!begun&&!maintenance);
manifest_error=0;allow_restart=true;if(!setjmp(restart_jump))device_ota_poll();allow_restart=false;
assert(manifest_requests==2&&restarts==1&&boot_partition==1);
fresh();manifest_error=1;install();device_ota_poll();device_ota_poll();device_ota_poll();
assert(manifest_requests==3&&!begun&&!maintenance);
fresh();manifest_error=1;platform_error="OTA_TASK_UNAVAILABLE";install();device_ota_poll();assert(manifest_requests==1);
fresh();install();reboot_to(1);device_ota_boot_ready();
// Force the terminal receipt's phase=0 commit to fail and retain the journal.
fail_receipt_commit=true;device_ota_poll();assert(state.phase==3);
fail_receipt_commit=false;device_ota_poll();assert(state.phase==0);
fresh();install();reboot_to(1);device_ota_boot_ready();
report_error=1;platform_error="OTA_FINISHED";device_ota_poll();assert(state.phase==0);
fresh();install();reboot_to(1);device_ota_boot_ready();report_error=1;
device_ota_poll();assert(state.phase==3);report_error=0;device_ota_poll();assert(state.phase==0);
}
static void test_internal_finalize(void){
// The first internal operation finalizes an active OTA handle. Allocation
// failure must abort it, whereas an SDK end failure has consumed it already.
fresh();clock_step=0;sem_fail_on=1;install();
assert(ended==0&&aborted==1&&task_attempts==0&&state.phase==4&&boot_partition==0&&!restarts);
assert_resources_clean(1,1,1,1);assert_worker_cleanup();
fresh();clock_step=0;task_fail_on=1;install();
assert(ended==0&&aborted==1&&sem_created==1&&sem_deleted==1&&task_created==0&&state.phase==4&&boot_partition==0&&!restarts);
assert_resources_clean(1,1,1,1);assert_worker_cleanup();
fresh();clock_step=0;end_error=1;install();
assert(ended==1&&aborted==0&&boot_sets==0&&task_created==1&&state.phase==4&&boot_partition==0&&!restarts);
assert_resources_clean(1,1,0,1);assert_worker_cleanup();
// The second operation selects the candidate. It must never abort an ended
// SDK handle or restart after either allocation or boot-selection failure.
fresh();clock_step=0;sem_fail_on=2;install();
assert(ended==1&&aborted==0&&boot_sets==0&&task_created==1&&state.phase==4&&boot_partition==0&&!restarts);
assert(strstr(state.message,"OTA boot selection failed:"));assert_worker_cleanup();
fresh();clock_step=0;task_fail_on=2;install();
assert(ended==1&&aborted==0&&boot_sets==0&&task_created==1&&sem_created==2&&state.phase==4&&boot_partition==0&&!restarts);
assert_worker_cleanup();
fresh();clock_step=0;boot_error=1;install();
assert(ended==1&&aborted==0&&boot_sets==1&&task_created==2&&state.phase==4&&boot_partition==0&&!restarts);
assert_worker_cleanup();
fresh();clock_step=0;install();
assert(ended==1&&boot_sets==1&&restarts==1&&boot_partition==1&&state.phase==2&&task_created==2);
assert_resources_clean(1,1,0,1);assert_worker_cleanup();
// A global deadline reached while releasing TLS prevents verification.
fresh();clock_step=0;cleanup_advance=900000000;install();
assert(ended==0&&aborted==1&&task_attempts==0&&state.phase==4&&strstr(state.message,"OTA deadline"));
assert_resources_clean(1,1,1,1);assert_worker_cleanup();
// If verification consumed the handle before its deadline expires, the
// caller records timeout and retains the old slot without aborting it again.
fresh();clock_step=0;end_advance=900000000;install();
assert(ended==1&&aborted==0&&boot_sets==0&&state.phase==4&&boot_partition==0&&strstr(state.message,"OTA deadline"));
assert_worker_cleanup();
// Completion distinguishes a worker allocation error from a completed SDK
// operation error; callers use this to maintain the handle's ownership.
fresh();bool completed=true;sem_fail_on=1;
assert(ota_internal_flash_call(OTA_FLASH_BOOT,0,&parts[0],&completed)==ESP_ERR_NO_MEM&&!completed);assert_worker_cleanup();
fresh();completed=true;task_fail_on=1;
assert(ota_internal_flash_call(OTA_FLASH_BOOT,0,&parts[0],&completed)==ESP_ERR_NO_MEM&&!completed);assert_worker_cleanup();
fresh();completed=false;boot_error=1;
assert(ota_internal_flash_call(OTA_FLASH_BOOT,0,&parts[0],&completed)==ESP_FAIL&&completed);assert_worker_cleanup();
}
static void test_internal_boot_and_rollback(void){
// First serial-installed image establishes its running slot as fallback.
fresh();clock_step=0;image_states[0]=-1;reboot_to(0);device_ota_boot_ready();
assert(ready&&boot_sets==1&&marked==1&&boot_partition==0&&task_created==1);assert_worker_cleanup();
fresh();image_states[0]=-1;reboot_to(0);task_fail_on=1;device_ota_boot_ready();
assert(!ready&&boot_sets==0&&marked==0&&boot_partition==0);assert_worker_cleanup();
fresh();image_states[0]=-1;reboot_to(0);boot_error=1;device_ota_boot_ready();
assert(!ready&&boot_sets==1&&marked==0&&boot_partition==0);assert_worker_cleanup();
// Corrupt candidate executes rollback internally; reset happens in parent
// only after both worker and semaphore have been deleted.
fresh();install();flash[1][0]^=1;reboot_to(1);boot_ready_allow_rollback();
assert(rollbacks==1&&boot_partition==0&&restarts==2&&!ready&&task_created==3);assert_worker_cleanup();
fresh();install();flash[1][0]^=1;reboot_to(1);rollback_error=1;device_ota_boot_ready();
assert(rollbacks==1&&boot_partition==1&&restarts==1&&!ready&&timer_armed&&state.phase==4);assert_worker_cleanup();
for(int fail=0;fail<2;fail++){
fresh();install();flash[1][0]^=1;reboot_to(1);
if(fail)task_fail_on=task_attempts+1;else sem_fail_on=sem_attempts+1;
device_ota_boot_ready();assert(rollbacks==0&&boot_partition==1&&restarts==1&&!ready&&timer_armed);assert_worker_cleanup();
}
// Failed health-timer setup must use the same safe worker, then restart even
// if explicit rollback fails: the bootloader owns pending-image rollback.
for(int fail=0;fail<2;fail++){
fresh();install();timer_create_error=1;rollback_error=fail;allow_restart=true;
if(!setjmp(restart_jump))reboot_to(1);allow_restart=false;
assert(rollbacks==1&&restarts==2&&!ready&&boot_partition==(fail?1:0));assert_worker_cleanup();
}
fresh();bool completed=false;rollback_error=1;
assert(ota_internal_flash_call(OTA_FLASH_ROLLBACK,0,NULL,&completed)==ESP_FAIL&&completed);
assert(rollbacks==1&&!restarts&&boot_partition==0);assert_worker_cleanup();
}
int main(int argc,char **argv){
const char *pick=argc>1?argv[1]:"all";
if(!strcmp(pick,"all")||!strcmp(pick,"normal"))test_normal();
if(!strcmp(pick,"all")||!strcmp(pick,"rejections"))test_rejections();
if(!strcmp(pick,"all")||!strcmp(pick,"download"))test_download_failures();
if(!strcmp(pick,"all")||!strcmp(pick,"read-recovery"))test_read_recovery();
if(!strcmp(pick,"all")||!strcmp(pick,"read-stall"))test_read_stall_limit();
if(!strcmp(pick,"all")||!strcmp(pick,"read-eof"))test_read_eof_and_error();
if(!strcmp(pick,"all")||!strcmp(pick,"write-offset"))test_write_failure_offset();
if(!strcmp(pick,"all")||!strcmp(pick,"deadline"))test_read_deadline();
if(!strcmp(pick,"all")||!strcmp(pick,"long-download"))test_long_download();
if(!strcmp(pick,"all")||!strcmp(pick,"preerase"))test_known_size_preerase();
if(!strcmp(pick,"all")||!strcmp(pick,"target-guard"))test_target_guard();
if(!strcmp(pick,"all")||!strcmp(pick,"erase-deadline"))test_begin_error_and_deadline();
if(!strcmp(pick,"all")||!strcmp(pick,"setup-deadline"))test_setup_deadlines();
if(!strcmp(pick,"all")||!strcmp(pick,"buffer"))test_buffer_fallbacks();
if(!strcmp(pick,"all")||!strcmp(pick,"cleanup"))test_failure_cleanup();
if(!strcmp(pick,"all")||!strcmp(pick,"metrics"))test_success_metrics();
if(!strcmp(pick,"all")||!strcmp(pick,"power"))test_power_and_boot();
if(!strcmp(pick,"all")||!strcmp(pick,"metadata"))test_metadata_failure();
if(!strcmp(pick,"all")||!strcmp(pick,"timer"))test_timer_failure();
if(!strcmp(pick,"all")||!strcmp(pick,"numeric"))test_numeric_validation();
if(!strcmp(pick,"all")||!strcmp(pick,"corrupt"))test_corrupt_records();
if(!strcmp(pick,"all")||!strcmp(pick,"recovery"))test_receipt_and_manifest_recovery();
if(!strcmp(pick,"all")||!strcmp(pick,"internal-finalize"))test_internal_finalize();
if(!strcmp(pick,"all")||!strcmp(pick,"internal-boot-rollback"))test_internal_boot_and_rollback();
assert_worker_cleanup();assert(!heap_live&&!client_live&&!ota_handle_live&&!sha_live);cJSON_Delete(manifest_json);printf("PASS production OTA: %s\n",pick);return 0;
}