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.
386 lines
24 KiB
C
386 lines
24 KiB
C
#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();
|
|
}
|