esp32/tools/ld2410s-c3-calib/main/main.c
raw ยท 15973 bytes
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_random.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "nvs_flash.h"
#include "radar.h"
static const char *TAG="bench";
static char token[33];
static esp_netif_t *station;
extern const uint8_t web_start[] asm("_binary_web_html_start");
extern const uint8_t web_end[] asm("_binary_web_html_end");
static esp_err_t json_reply(httpd_req_t *req,cJSON *obj,const char *status) {
char *text=cJSON_PrintUnformatted(obj); cJSON_Delete(obj);
if(!text) return httpd_resp_send_err(req,HTTPD_500_INTERNAL_SERVER_ERROR,"Out of memory");
httpd_resp_set_type(req,"application/json"); httpd_resp_set_hdr(req,"Cache-Control","no-store");
if(status) httpd_resp_set_status(req,status);
esp_err_t err=httpd_resp_send(req,text,HTTPD_RESP_USE_STRLEN); free(text); return err;
}
static esp_err_t error_reply(httpd_req_t *req,const char *message,const char *status) {
cJSON *obj=cJSON_CreateObject(); cJSON_AddStringToObject(obj,"error",message); return json_reply(req,obj,status);
}
static bool valid_host(httpd_req_t *req) {
char host[64];
if(httpd_req_get_hdr_value_str(req,"Host",host,sizeof(host))!=ESP_OK) return false;
char *port=strchr(host,':');
if(port) { if(strcmp(port,":80")) return false; *port=0; }
if(!strcmp(host,"192.168.4.1")) return true;
if(station) {
esp_netif_ip_info_t ip;
if(esp_netif_get_ip_info(station,&ip)==ESP_OK && ip.ip.addr) {
char addr[24]; snprintf(addr,sizeof(addr),IPSTR,IP2STR(&ip.ip));
if(!strcmp(host,addr)) return true;
}
}
return false;
}
static bool authorized(httpd_req_t *req) {
if(!valid_host(req)) { error_reply(req,"Open the dashboard using the ESP32 IP address","403 Forbidden"); return false; }
char supplied[40];
if(httpd_req_get_hdr_value_str(req,"X-Bench-Token",supplied,sizeof(supplied))!=ESP_OK || strcmp(supplied,token)) {
error_reply(req,"Reload the dashboard to refresh the session","403 Forbidden"); return false;
}
return true;
}
static cJSON *reading_json(const ld_reading *r,uint32_t stamp) {
cJSON *o=cJSON_CreateObject();
cJSON_AddNumberToObject(o,"uptime_ms",stamp); cJSON_AddNumberToObject(o,"state",r->state);
cJSON_AddBoolToObject(o,"present",r->state>=2); cJSON_AddNumberToObject(o,"distance_cm",r->distance_cm);
cJSON_AddStringToObject(o,"mode",r->energy_valid?"standard":"minimal");
if(r->energy_valid) { cJSON *energy=cJSON_AddArrayToObject(o,"energy"); for(unsigned i=0;i<16;i++) cJSON_AddItemToArray(energy,cJSON_CreateNumber(r->energy[i])); }
return o;
}
static cJSON *info_json(const radar_state *s) {
cJSON *o=cJSON_CreateObject(); cJSON_AddStringToObject(o,"firmware",s->firmware);
cJSON_AddNumberToObject(o,"uart_rx",CONFIG_BENCH_UART_RX); cJSON_AddNumberToObject(o,"uart_tx",CONFIG_BENCH_UART_TX);
cJSON_AddStringToObject(o,"firmware_raw",s->firmware_raw); cJSON_AddStringToObject(o,"serial",s->serial); return o;
}
static radar_state *snapshot(void) {
radar_state *s=malloc(sizeof(*s)); if(s) radar_copy(s); return s;
}
static esp_err_t state_reply(httpd_req_t *req) {
radar_state *s=snapshot(); if(!s) return error_reply(req,"Out of memory","500 Internal Server Error");
cJSON *o=cJSON_CreateObject();
uint32_t now=(uint32_t)(esp_timer_get_time()/1000);
cJSON_AddBoolToObject(o,"connected",s->seen && (uint32_t)(now-s->last_ms)<10000);
cJSON_AddBoolToObject(o,"demo",false);
cJSON_AddItemToObject(o,"latest",s->seen?reading_json(&s->latest,s->last_ms):cJSON_CreateObject());
if(s->seen) cJSON_AddNumberToObject(o,"age",(uint32_t)(now-s->last_ms)/1000.0); else cJSON_AddNullToObject(o,"age");
if(s->settings_valid) cJSON_AddItemToObject(o,"settings",radar_settings_json(&s->settings)); else cJSON_AddNullToObject(o,"settings");
cJSON_AddItemToObject(o,"info",info_json(s)); cJSON_AddStringToObject(o,"error",s->error);
cJSON_AddNumberToObject(o,"reports",s->reports); cJSON_AddNumberToObject(o,"samples",s->history_count);
cJSON_AddNumberToObject(o,"bad_bytes",s->bad_bytes); cJSON_AddNumberToObject(o,"heap_free",esp_get_free_heap_size());
cJSON_AddBoolToObject(o,"backup_available",s->backup_valid);
cJSON *job=cJSON_AddObjectToObject(o,"job"); cJSON_AddNumberToObject(job,"id",s->job_id);
cJSON_AddBoolToObject(job,"busy",s->job_busy); cJSON_AddStringToObject(job,"action",s->job_action); cJSON_AddStringToObject(job,"status",s->job_status);
if(s->calibration_state) {
static const char *names[]={"idle","countdown โ leave the detection area","running","complete โ read settings to see new thresholds","failed or timed out โ completion unconfirmed"};
cJSON *c=cJSON_AddObjectToObject(o,"calibration"); cJSON_AddNumberToObject(c,"state",s->calibration_state);
cJSON_AddStringToObject(c,"status",names[s->calibration_state]); cJSON_AddNumberToObject(c,"progress",s->calibration_progress/100.0);
cJSON_AddNumberToObject(c,"remaining_s",s->calibration_state==1?(int32_t)(s->calibration_due_ms-now)/1000.0:0);
} else cJSON_AddNullToObject(o,"calibration");
cJSON *history=cJSON_AddArrayToObject(o,"history");
for(unsigned i=0;i<s->history_count;i++) {
unsigned index=(s->history_next+HISTORY_SIZE-s->history_count+i)%HISTORY_SIZE;
const radar_sample *r=&s->history[index]; cJSON *item=cJSON_CreateObject();
cJSON_AddNumberToObject(item,"uptime_ms",r->uptime_ms); cJSON_AddNumberToObject(item,"distance_cm",r->reading.distance_cm);
cJSON_AddBoolToObject(item,"present",r->reading.state>=2); cJSON_AddItemToArray(history,item);
}
cJSON *logs=cJSON_AddArrayToObject(o,"logs");
for(unsigned i=0;i<s->log_count;i++) cJSON_AddItemToArray(logs,cJSON_CreateString(s->logs[(s->log_next+LOG_SIZE-s->log_count+i)%LOG_SIZE]));
cJSON *network=cJSON_AddObjectToObject(o,"network"); cJSON_AddStringToObject(network,"ap_ssid",CONFIG_BENCH_AP_SSID);
cJSON_AddStringToObject(network,"ap_ip","192.168.4.1");
if(station) { esp_netif_ip_info_t ip; if(esp_netif_get_ip_info(station,&ip)==ESP_OK && ip.ip.addr) { char addr[24]; snprintf(addr,sizeof(addr),IPSTR,IP2STR(&ip.ip)); cJSON_AddStringToObject(network,"sta_ip",addr); } }
free(s); return json_reply(req,o,NULL);
}
static esp_err_t backup_reply(httpd_req_t *req,bool previous) {
radar_state *s=snapshot(); if(!s) return error_reply(req,"Out of memory","500 Internal Server Error");
if(previous?!s->backup_valid:!s->settings_valid) { free(s); return error_reply(req,"No settings available; read the sensor first","409 Conflict"); }
cJSON *o=cJSON_CreateObject(); cJSON_AddStringToObject(o,"format","ld2410s-settings-v1");
cJSON_AddItemToObject(o,"info",info_json(s)); cJSON_AddItemToObject(o,"settings",radar_settings_json(previous?&s->backup:&s->settings));
free(s); return json_reply(req,o,NULL);
}
static esp_err_t csv_reply(httpd_req_t *req) {
radar_state *s=snapshot(); if(!s) return error_reply(req,"Out of memory","500 Internal Server Error");
httpd_resp_set_type(req,"text/csv"); httpd_resp_set_hdr(req,"Cache-Control","no-store");
char line[400]; int len=snprintf(line,sizeof(line),"uptime_ms,state,present,distance_cm");
for(unsigned i=0;i<16;i++) len+=snprintf(line+len,sizeof(line)-len,",energy_%u",i);
len+=snprintf(line+len,sizeof(line)-len,"\n");
esp_err_t err=httpd_resp_send_chunk(req,line,len);
for(unsigned i=0;i<s->history_count && err==ESP_OK;i++) {
const radar_sample *r=&s->history[(s->history_next+HISTORY_SIZE-s->history_count+i)%HISTORY_SIZE];
len=snprintf(line,sizeof(line),"%"PRIu32",%u,%u,%u",r->uptime_ms,r->reading.state,r->reading.state>=2,r->reading.distance_cm);
for(unsigned k=0;k<16;k++) {
if(r->reading.energy_valid) len+=snprintf(line+len,sizeof(line)-len,",%"PRIu32,r->reading.energy[k]);
else len+=snprintf(line+len,sizeof(line)-len,",");
}
len+=snprintf(line+len,sizeof(line)-len,"\n"); err=httpd_resp_send_chunk(req,line,len);
}
free(s); if(err==ESP_OK) err=httpd_resp_send_chunk(req,NULL,0); return err;
}
static esp_err_t root(httpd_req_t *req) {
if(!valid_host(req)) return error_reply(req,"Open the dashboard using the ESP32 IP address","403 Forbidden");
httpd_resp_set_type(req,"text/html; charset=utf-8"); httpd_resp_set_hdr(req,"Cache-Control","no-store");
const char *start=(const char *)web_start,*marker=strstr(start,"__TOKEN__");
if(!marker) return httpd_resp_send_err(req,HTTPD_500_INTERNAL_SERVER_ERROR,"Missing UI token marker");
esp_err_t err=httpd_resp_send_chunk(req,start,marker-start);
if(err==ESP_OK) err=httpd_resp_send_chunk(req,token,strlen(token));
if(err==ESP_OK) err=httpd_resp_send_chunk(req,marker+9,(const char *)web_end-marker-9-1);
if(err==ESP_OK) err=httpd_resp_send_chunk(req,NULL,0);
return err;
}
static esp_err_t get_api(httpd_req_t *req) {
if(!authorized(req)) return ESP_OK;
if(!strcmp(req->uri,"/api/state")) return state_reply(req);
if(!strcmp(req->uri,"/api/backup")) return backup_reply(req,false);
if(!strcmp(req->uri,"/api/previous-backup")) return backup_reply(req,true);
if(!strcmp(req->uri,"/api/csv")) return csv_reply(req);
return error_reply(req,"Not found","404 Not Found");
}
static bool integer(const cJSON *o,const char *key,unsigned lo,unsigned hi,uint16_t *out) {
const cJSON *v=cJSON_GetObjectItemCaseSensitive(o,key);
if(!cJSON_IsNumber(v) || !isfinite(v->valuedouble) || v->valuedouble<lo || v->valuedouble>hi || floor(v->valuedouble)!=v->valuedouble) return false;
*out=(uint16_t)v->valuedouble; return true;
}
static esp_err_t post_api(httpd_req_t *req) {
if(!authorized(req)) return ESP_OK;
if(req->content_len<2 || req->content_len>4096) return error_reply(req,"Invalid request size","400 Bad Request");
char *text=malloc(req->content_len+1); if(!text) return error_reply(req,"Out of memory","500 Internal Server Error");
size_t used=0;
while(used<req->content_len) {
int n=httpd_req_recv(req,text+used,req->content_len-used);
if(n<=0) { free(text); return error_reply(req,"Request receive failed","408 Request Timeout"); }
used+=n;
}
text[used]=0; cJSON *args=cJSON_Parse(text); free(text);
if(!cJSON_IsObject(args)) { cJSON_Delete(args); return error_reply(req,"Invalid JSON object","400 Bad Request"); }
radar_job j={0}; const char *error=NULL;
if(!strcmp(req->uri,"/api/read")) j.kind=JOB_READ;
else if(!strcmp(req->uri,"/api/write") || !strcmp(req->uri,"/api/validate")) {
if(!radar_parse_settings(cJSON_GetObjectItemCaseSensitive(args,"settings"),&j.settings,&error)) { cJSON_Delete(args); return error_reply(req,error,"400 Bad Request"); }
if(!strcmp(req->uri,"/api/validate")) { cJSON_Delete(args); cJSON *o=cJSON_CreateObject(); cJSON_AddBoolToObject(o,"valid",true); return json_reply(req,o,NULL); }
j.kind=JOB_WRITE;
} else if(!strcmp(req->uri,"/api/mode")) {
const cJSON *v=cJSON_GetObjectItemCaseSensitive(args,"mode");
if(!cJSON_IsString(v) || (strcmp(v->valuestring,"standard") && strcmp(v->valuestring,"minimal"))) { cJSON_Delete(args); return error_reply(req,"Invalid mode","400 Bad Request"); }
j.kind=!strcmp(v->valuestring,"standard")?JOB_STANDARD:JOB_MINIMAL;
} else if(!strcmp(req->uri,"/api/calibrate")) {
j.kind=JOB_CALIBRATE;
if(!integer(args,"trigger",1,65535,&j.trigger) || !integer(args,"hold",1,65535,&j.hold) || !integer(args,"seconds",10,600,&j.seconds) || !integer(args,"delay",0,120,&j.delay)) { cJSON_Delete(args); return error_reply(req,"Invalid calibration factors, scan time, or start delay","400 Bad Request"); }
} else if(!strcmp(req->uri,"/api/restore")) {
radar_state *s=snapshot();
if(!s) { cJSON_Delete(args); return error_reply(req,"Out of memory","500 Internal Server Error"); }
if(!s->backup_valid) { free(s); cJSON_Delete(args); return error_reply(req,"No previous backup available","409 Conflict"); }
j.kind=JOB_RESTORE; j.settings=s->backup; free(s);
} else { cJSON_Delete(args); return error_reply(req,"Not found","404 Not Found"); }
cJSON_Delete(args); uint32_t id;
if(!radar_submit(&j,&id,&error)) return error_reply(req,error,"409 Conflict");
cJSON *o=cJSON_CreateObject(); cJSON_AddNumberToObject(o,"job_id",id); return json_reply(req,o,"202 Accepted");
}
static void wifi_event(void *arg,esp_event_base_t base,int32_t id,void *data) {
(void)arg;
if(base==WIFI_EVENT && id==WIFI_EVENT_STA_START) esp_wifi_connect();
if(base==WIFI_EVENT && id==WIFI_EVENT_STA_DISCONNECTED) { ESP_LOGW(TAG,"Home Wi-Fi disconnected; AP remains available; retrying"); esp_wifi_connect(); }
if(base==IP_EVENT && id==IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *event=data; ESP_LOGI(TAG,"Home network dashboard: http://"IPSTR"/",IP2STR(&event->ip_info.ip)); }
}
static void wifi_start(void) {
size_t ap_ssid_len=strlen(CONFIG_BENCH_AP_SSID),ap_password_len=strlen(CONFIG_BENCH_AP_PASSWORD);
size_t sta_ssid_len=strlen(CONFIG_BENCH_STA_SSID),sta_password_len=strlen(CONFIG_BENCH_STA_PASSWORD);
if(!ap_ssid_len || ap_ssid_len>32 || ap_password_len<8 || ap_password_len>63 || sta_ssid_len>32 || sta_password_len>63) { ESP_LOGE(TAG,"Invalid Wi-Fi configuration; check menuconfig"); abort(); }
ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_ap();
if(sta_ssid_len) station=esp_netif_create_default_wifi_sta();
wifi_init_config_t config=WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&config));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,wifi_event,NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,wifi_event,NULL));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(sta_ssid_len?WIFI_MODE_APSTA:WIFI_MODE_AP));
wifi_config_t ap={.ap={.ssid_len=ap_ssid_len,.channel=1,.authmode=WIFI_AUTH_WPA2_PSK,.max_connection=3,.pmf_cfg={.required=false}}};
memcpy(ap.ap.ssid,CONFIG_BENCH_AP_SSID,ap_ssid_len); memcpy(ap.ap.password,CONFIG_BENCH_AP_PASSWORD,ap_password_len);
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP,&ap));
if(sta_ssid_len) {
wifi_config_t sta={0}; memcpy(sta.sta.ssid,CONFIG_BENCH_STA_SSID,sta_ssid_len); memcpy(sta.sta.password,CONFIG_BENCH_STA_PASSWORD,sta_password_len);
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA,&sta));
}
ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
ESP_LOGI(TAG,"Connect to Wi-Fi '%s'; open http://192.168.4.1/",CONFIG_BENCH_AP_SSID);
}
void app_main(void) {
/* Preserve existing NVS; do not automatically erase an old board's data. */
ESP_ERROR_CHECK(nvs_flash_init());
if(CONFIG_BENCH_UART_TX==CONFIG_BENCH_UART_RX) { ESP_LOGE(TAG,"UART TX and RX must use different pins"); abort(); }
for(unsigned i=0;i<4;i++) snprintf(token+8*i,9,"%08"PRIx32,esp_random());
radar_start(); wifi_start();
httpd_config_t cfg=HTTPD_DEFAULT_CONFIG(); cfg.uri_match_fn=httpd_uri_match_wildcard;
cfg.stack_size=8192; cfg.lru_purge_enable=true; cfg.max_open_sockets=4;
httpd_handle_t server; ESP_ERROR_CHECK(httpd_start(&server,&cfg));
const httpd_uri_t routes[]={
{.uri="/",.method=HTTP_GET,.handler=root},
{.uri="/api/*",.method=HTTP_GET,.handler=get_api},
{.uri="/api/*",.method=HTTP_POST,.handler=post_api},
};
for(unsigned i=0;i<sizeof(routes)/sizeof(routes[0]);i++) ESP_ERROR_CHECK(httpd_register_uri_handler(server,&routes[i]));
}