feat(firmware): ESP32-P4 sensor node — Workstream I core skeleton
New firmware/esp32p4-sensor-node/ ESP-IDF (C, FreeRTOS) project skeleton per docs/superpowers/specs/2026-07-23-esp32-sensor-node-design.md's Workstream I: - Wi-Fi station-mode connect with exponential-backoff reconnect (wifi_manager.c), credentials from a gitignored main/device_config.h the seeker fills in (template: device_config.h.example). - Telemetry HTTP client (telemetry_client.c) POSTing the spec's exact contract shape to /api/device/telemetry with a Bearer token, via esp_http_client + cJSON. - BME280 I2C driver (bme280.c) with Bosch's public double-precision compensation formulas, using ESP-IDF's newer driver/i2c_master.h API. - LD2410 mmWave presence driver (ld2410.c) over UART, chosen over a plain PIR for its distance/motion data richness — its frame-offset parsing is flagged as the least-certain code in the firmware. - sensor_driver_t registry (sensor_driver.h, sensor_registry.c) so new sensors are a new driver file + one array line, no main-loop changes. - README.md: build steps, manual-config walkthrough, wiring/pinouts, and an explicit "what's verified vs. not" section plus a real hardware caveat (ESP32-P4 has no integrated Wi-Fi radio). UNVERIFIED AGAINST REAL HARDWARE per the spec's honesty-policy note — no ESP-IDF toolchain or physical boards available in this environment. Syntax-checked with gcc against hand-written ESP-IDF API stubs (not committed) as a best-effort substitute for a real idf.py build. Workstream J (RTL-SDR experimental module) is explicitly out of scope here; firmware/esp32p4-sensor-node/components/ is left in place for it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
166
firmware/esp32p4-sensor-node/main/telemetry_client.c
Normal file
166
firmware/esp32p4-sensor-node/main/telemetry_client.c
Normal file
@@ -0,0 +1,166 @@
|
||||
// Telemetry HTTP client — see telemetry_client.h.
|
||||
//
|
||||
// UNVERIFIED AGAINST REAL HARDWARE: written against ESP-IDF's documented
|
||||
// esp_http_client API and the design spec's contract (see header comment),
|
||||
// but never run against a live backend from real firmware. The request
|
||||
// shape (JSON body, header names) is copied verbatim from the spec, which
|
||||
// is binding for this workstream — see
|
||||
// docs/superpowers/specs/2026-07-23-esp32-sensor-node-design.md.
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "telemetry_client.h"
|
||||
#include "device_config.h"
|
||||
#include "sensor_registry.h"
|
||||
#include "wifi_manager.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "telemetry_client";
|
||||
|
||||
// Matches the backend contract's own cap ("cap readings array length, e.g.
|
||||
// 64 per request") with headroom to spare for this firmware's small,
|
||||
// fixed set of drivers.
|
||||
#define TELEMETRY_MAX_READINGS 32
|
||||
|
||||
static esp_err_t build_request_body(const sensor_reading_t *readings, size_t count,
|
||||
char **out_json, size_t *out_len) {
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (root == NULL) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
cJSON_Delete(readings[i].metadata);
|
||||
}
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
cJSON *arr = cJSON_AddArrayToObject(root, "readings");
|
||||
if (arr == NULL) {
|
||||
cJSON_Delete(root);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
cJSON_Delete(readings[i].metadata);
|
||||
}
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
cJSON *item = cJSON_CreateObject();
|
||||
if (item == NULL) {
|
||||
cJSON_Delete(root);
|
||||
for (size_t j = i; j < count; j++) {
|
||||
cJSON_Delete(readings[j].metadata);
|
||||
}
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
cJSON_AddStringToObject(item, "sensor_type", readings[i].sensor_type);
|
||||
cJSON_AddNumberToObject(item, "value", readings[i].value);
|
||||
cJSON_AddStringToObject(item, "unit", readings[i].unit);
|
||||
// metadata is required by the contract even when a driver has
|
||||
// nothing extra to say -- an empty object, never a missing key.
|
||||
cJSON *meta = readings[i].metadata ? readings[i].metadata : cJSON_CreateObject();
|
||||
cJSON_AddItemToObject(item, "metadata", meta);
|
||||
cJSON_AddItemToArray(arr, item);
|
||||
}
|
||||
|
||||
char *json = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root); // recursively frees every item + its metadata too
|
||||
if (json == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
*out_json = json;
|
||||
*out_len = strlen(json);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t telemetry_client_post_readings(const sensor_reading_t *readings, size_t count) {
|
||||
if (count == 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char *json = NULL;
|
||||
size_t json_len = 0;
|
||||
esp_err_t err = build_request_body(readings, count, &json, &json_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to build request body: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
char url[256];
|
||||
int url_n = snprintf(url, sizeof(url), "%s/api/device/telemetry", DEVICE_BACKEND_BASE_URL);
|
||||
if (url_n < 0 || (size_t)url_n >= sizeof(url)) {
|
||||
ESP_LOGE(TAG, "DEVICE_BACKEND_BASE_URL too long for url buffer");
|
||||
free(json);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
char auth_header[512];
|
||||
int auth_n = snprintf(auth_header, sizeof(auth_header), "Bearer %s", DEVICE_PAIRING_TOKEN);
|
||||
if (auth_n < 0 || (size_t)auth_n >= sizeof(auth_header)) {
|
||||
ESP_LOGE(TAG, "DEVICE_PAIRING_TOKEN too long for header buffer");
|
||||
free(json);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
.method = HTTP_METHOD_POST,
|
||||
.timeout_ms = 10000,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
if (client == NULL) {
|
||||
ESP_LOGE(TAG, "esp_http_client_init failed");
|
||||
free(json);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
esp_http_client_set_header(client, "Authorization", auth_header);
|
||||
esp_http_client_set_post_field(client, json, (int)json_len);
|
||||
|
||||
err = esp_http_client_perform(client);
|
||||
if (err == ESP_OK) {
|
||||
int status = esp_http_client_get_status_code(client);
|
||||
ESP_LOGI(TAG, "telemetry POST -> HTTP %d (%d readings, %d bytes)",
|
||||
status, (int)count, (int)json_len);
|
||||
if (status < 200 || status >= 300) {
|
||||
err = ESP_FAIL;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "telemetry POST transport error: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
esp_http_client_cleanup(client);
|
||||
free(json); // cJSON_PrintUnformatted allocates via cJSON's hooks (malloc by default)
|
||||
return err;
|
||||
}
|
||||
|
||||
static void telemetry_task(void *arg) {
|
||||
static sensor_reading_t s_readings[TELEMETRY_MAX_READINGS];
|
||||
|
||||
while (1) {
|
||||
if (!wifi_manager_is_connected()) {
|
||||
ESP_LOGI(TAG, "Wi-Fi not connected, skipping this reporting cycle");
|
||||
} else {
|
||||
size_t count = 0;
|
||||
sensor_registry_collect(s_readings, TELEMETRY_MAX_READINGS, &count);
|
||||
if (count == 0) {
|
||||
ESP_LOGD(TAG, "no readings produced this cycle");
|
||||
} else {
|
||||
esp_err_t err = telemetry_client_post_readings(s_readings, count);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "telemetry post did not succeed this cycle: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(DEVICE_REPORT_INTERVAL_SEC * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
void telemetry_client_start_task(void) {
|
||||
// Stack sized generously: cJSON body building + TLS handshake state for
|
||||
// esp_http_client both live on this task's stack.
|
||||
xTaskCreate(telemetry_task, "telemetry_task", 8192, NULL, 5, NULL);
|
||||
}
|
||||
Reference in New Issue
Block a user