feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
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:
@@ -0,0 +1,122 @@
|
||||
#include "device_reset.h"
|
||||
#include "call_client.h"
|
||||
#include "device_ota.h"
|
||||
#include "ble_cloud.h"
|
||||
#include "cJSON.h"
|
||||
#include "nvs.h"
|
||||
#include "mbedtls/platform_util.h"
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef struct { uint32_t version,phase; char body[512]; } reset_record_t;
|
||||
// phase 1: request persisted, result uncertain; 2: cloud committed, cleanup
|
||||
// pending; 3: local cleanup committed. Never delete this recovery namespace.
|
||||
static esp_err_t load(reset_record_t *r) {
|
||||
memset(r,0,sizeof(*r));nvs_handle_t n;
|
||||
esp_err_t e=nvs_open("factory_reset",NVS_READONLY,&n);
|
||||
if(e==ESP_ERR_NVS_NOT_FOUND)return ESP_OK;
|
||||
if(e!=ESP_OK)return e;
|
||||
size_t size=sizeof(*r);e=nvs_get_blob(n,"record",r,&size);nvs_close(n);
|
||||
if(e==ESP_ERR_NVS_NOT_FOUND){memset(r,0,sizeof(*r));return ESP_OK;}
|
||||
if(e==ESP_OK && (size!=sizeof(*r)||r->version!=1||r->phase<1||r->phase>3||!memchr(r->body,0,sizeof(r->body))))return ESP_ERR_INVALID_STATE;
|
||||
return e;
|
||||
}
|
||||
static esp_err_t save(const reset_record_t *r) {
|
||||
nvs_handle_t n;esp_err_t e=nvs_open("factory_reset",NVS_READWRITE,&n);
|
||||
if(e!=ESP_OK)return e;
|
||||
e=nvs_set_blob(n,"record",r,sizeof(*r));if(e==ESP_OK)e=nvs_commit(n);
|
||||
reset_record_t check;size_t size=sizeof(check);
|
||||
if(e==ESP_OK)e=nvs_get_blob(n,"record",&check,&size);
|
||||
if(e==ESP_OK && (size!=sizeof(check)||memcmp(r,&check,size)))e=ESP_FAIL;
|
||||
nvs_close(n);return e;
|
||||
}
|
||||
static esp_err_t clear_keys(const char *space,const char *const *keys) {
|
||||
nvs_handle_t n;esp_err_t e=nvs_open(space,NVS_READWRITE,&n);if(e!=ESP_OK)return e;
|
||||
if(!keys)e=nvs_erase_all(n);
|
||||
else for(int i=0;keys[i]&&e==ESP_OK;i++) {
|
||||
e=nvs_erase_key(n,keys[i]);if(e==ESP_ERR_NVS_NOT_FOUND)e=ESP_OK;
|
||||
}
|
||||
if(e==ESP_OK)e=nvs_commit(n);
|
||||
nvs_close(n);return e;
|
||||
}
|
||||
static esp_err_t cleanup(reset_record_t *r) {
|
||||
// Keep firmware/OTA metadata and both forms of permanent device identity.
|
||||
const char *const control[]={"speaker","display_name","standby","result","probe",NULL};
|
||||
const char *const platform[]={"instance_id","product_key","device_name","volc_secret","pending_conv",NULL};
|
||||
esp_err_t e=clear_keys("voice",NULL);
|
||||
if(e==ESP_OK)e=clear_keys("platform",platform);
|
||||
if(e==ESP_OK)e=clear_keys("control",control);
|
||||
if(e==ESP_OK)e=clear_keys("ble_setup",NULL);
|
||||
if(e==ESP_OK)e=clear_keys("wifi",NULL);
|
||||
if(e==ESP_OK){r->phase=3;e=save(r);} // A cut before this commit repeats cleanup.
|
||||
return e;
|
||||
}
|
||||
esp_err_t device_reset_boot_cleanup(void) {
|
||||
reset_record_t r;esp_err_t e=load(&r);
|
||||
if(e==ESP_OK && r.phase==2){e=save(&r);if(e==ESP_OK)e=cleanup(&r);}
|
||||
return e;
|
||||
}
|
||||
static void wipe(char *s){if(s){mbedtls_platform_zeroize(s,strlen(s));free(s);}}
|
||||
static esp_err_t post(const char *path,const char *body,cJSON **data) {
|
||||
char *raw=NULL;esp_err_t e=platform_request("POST",path,body,NULL,&raw);
|
||||
if(e==ESP_OK) {
|
||||
cJSON *root=raw?cJSON_Parse(raw):NULL,*value=cJSON_GetObjectItemCaseSensitive(root,"data");
|
||||
if(!cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root,"ok"))||!cJSON_IsObject(value))e=ESP_ERR_INVALID_RESPONSE;
|
||||
else {*data=cJSON_Duplicate(value,true);if(!*data)e=ESP_ERR_NO_MEM;}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
wipe(raw);return e;
|
||||
}
|
||||
static const char *str(const cJSON *j,const char *key) {
|
||||
cJSON *v=cJSON_GetObjectItemCaseSensitive(j,key);return cJSON_IsString(v)?v->valuestring:"";
|
||||
}
|
||||
static bool response_matches(const cJSON *data,const cJSON *request) {
|
||||
cJSON *actual=cJSON_GetObjectItemCaseSensitive(data,"reset_counter"),*expected=cJSON_GetObjectItemCaseSensitive(request,"reset_counter");
|
||||
if(strcmp(str(data,"reset_id"),str(request,"reset_id")) || strcmp(str(data,"activation_state"),"CLAIMABLE") ||
|
||||
strcmp(str(data,"license_action"),"RETAINED") || !cJSON_IsNumber(actual)||!cJSON_IsNumber(expected)||actual->valuedouble!=expected->valuedouble)return false;
|
||||
const char *required[]={"clear_volc_credential","clear_user_context","clear_session_state","clear_network_credentials","reboot_to_claimable"};
|
||||
cJSON *actions=cJSON_GetObjectItemCaseSensitive(data,"local_actions");
|
||||
if(!cJSON_IsArray(actions)||cJSON_GetArraySize(actions)!=5)return false;
|
||||
for(int i=0;i<5;i++) {int count=0;const cJSON *a=NULL;cJSON_ArrayForEach(a,actions)if(cJSON_IsString(a)&&!strcmp(a->valuestring,required[i]))count++;if(count!=1)return false;}
|
||||
return true;
|
||||
}
|
||||
esp_err_t device_reset_from_setup(const char *physical_event_id) {
|
||||
if(!physical_event_id||strlen(physical_event_id)!=36||!ble_cloud_configured())return ESP_ERR_INVALID_ARG;
|
||||
reset_record_t r;esp_err_t e=load(&r);if(e!=ESP_OK)return e;
|
||||
if(r.phase==2){e=save(&r);return e==ESP_OK?cleanup(&r):e;}
|
||||
// Same confirmed BLE job may retry after local cleanup finished.
|
||||
cJSON *request=r.phase?cJSON_Parse(r.body):NULL;
|
||||
if(r.phase==3 && request && !strcmp(str(request,"reset_id"),physical_event_id)){cJSON_Delete(request);return ESP_OK;}
|
||||
cJSON_Delete(request);request=NULL;
|
||||
// BLE setup boots before normal SNTP/call initialization. Establish signed
|
||||
// server time before DEVREQ1 timestamps and the durable occurred_at value.
|
||||
if(ble_cloud_sync_time()!=ESP_OK)return ESP_FAIL;
|
||||
if(r.phase!=1) {
|
||||
cJSON *state=NULL;e=post("/api/v1/device/factory-reset/state","{}",&state);
|
||||
if(e!=ESP_OK){cJSON_Delete(state);return e;}
|
||||
cJSON *counter=cJSON_GetObjectItemCaseSensitive(state,"reset_counter");
|
||||
if(!cJSON_IsNumber(counter)||counter->valuedouble<0||counter->valuedouble>=9007199254740991.0||counter->valuedouble!=(double)(int64_t)counter->valuedouble){cJSON_Delete(state);return ESP_ERR_INVALID_RESPONSE;}
|
||||
double next=counter->valuedouble+1;cJSON_Delete(state);
|
||||
time_t now=time(NULL);struct tm utc;char occurred[32];
|
||||
if(now<1700000000 || !gmtime_r(&now,&utc) || !strftime(occurred,sizeof(occurred),"%Y-%m-%dT%H:%M:%SZ",&utc))return ESP_ERR_INVALID_STATE;
|
||||
request=cJSON_CreateObject();if(!request)return ESP_ERR_NO_MEM;
|
||||
cJSON_AddStringToObject(request,"reset_id",physical_event_id);cJSON_AddNumberToObject(request,"reset_counter",next);
|
||||
cJSON_AddStringToObject(request,"occurred_at",occurred);cJSON_AddStringToObject(request,"trigger","device_menu");
|
||||
cJSON_AddStringToObject(request,"firmware_version",device_ota_version());cJSON_AddNumberToObject(request,"pending_turn_count",0);
|
||||
char *body=cJSON_PrintUnformatted(request);cJSON_Delete(request);request=NULL;
|
||||
if(!body)return ESP_ERR_NO_MEM;
|
||||
if(strlen(body)>=sizeof(r.body)){wipe(body);return ESP_ERR_INVALID_SIZE;}
|
||||
memset(&r,0,sizeof(r));r.version=1;r.phase=1;strcpy(r.body,body);wipe(body);
|
||||
if((e=save(&r))!=ESP_OK)return e;
|
||||
}
|
||||
request=cJSON_Parse(r.body);if(!request)return ESP_ERR_INVALID_STATE;
|
||||
cJSON *response=NULL;e=post("/api/v1/device/factory-reset",r.body,&response);
|
||||
if(e==ESP_OK && !response_matches(response,request))e=ESP_ERR_INVALID_RESPONSE;
|
||||
cJSON_Delete(request);cJSON_Delete(response);
|
||||
if(e!=ESP_OK)return e; // Keep the same signed body for uncertain retries.
|
||||
r.phase=2;if((e=save(&r))!=ESP_OK)return e;
|
||||
return cleanup(&r);
|
||||
}
|
||||
Reference in New Issue
Block a user