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:
131
firmware/esp32p4-sensor-node/main/wifi_manager.c
Normal file
131
firmware/esp32p4-sensor-node/main/wifi_manager.c
Normal file
@@ -0,0 +1,131 @@
|
||||
// Wi-Fi station-mode connection manager — see wifi_manager.h for the
|
||||
// hardware caveat about ESP32-P4 not having an integrated radio.
|
||||
//
|
||||
// Structure follows ESP-IDF's own documented Wi-Fi station example pattern
|
||||
// (event-driven connect via WIFI_EVENT/IP_EVENT + an event group the rest
|
||||
// of the app can block on), extended with an exponential-backoff reconnect
|
||||
// instead of giving up after N attempts — a sensor node left running for
|
||||
// weeks needs to ride out a router reboot or a home Wi-Fi outage
|
||||
// indefinitely, not park itself in a permanent failure state.
|
||||
//
|
||||
// UNVERIFIED AGAINST REAL HARDWARE — see README.md.
|
||||
|
||||
#include <inttypes.h>
|
||||
#include "wifi_manager.h"
|
||||
#include "device_config.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "freertos/event_groups.h"
|
||||
|
||||
static const char *TAG = "wifi_manager";
|
||||
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
|
||||
#define WIFI_RECONNECT_BASE_DELAY_MS 1000
|
||||
#define WIFI_RECONNECT_MAX_DELAY_MS 30000
|
||||
#define WIFI_RECONNECT_BACKOFF_CAP_SHIFT 5 // 1000ms << 5 = 32000ms, then clamped to MAX
|
||||
|
||||
static EventGroupHandle_t s_wifi_event_group = NULL;
|
||||
static esp_timer_handle_t s_reconnect_timer = NULL;
|
||||
static uint32_t s_retry_count = 0;
|
||||
|
||||
static void reconnect_timer_cb(void *arg) {
|
||||
ESP_LOGI(TAG, "attempting Wi-Fi reconnect");
|
||||
esp_err_t err = esp_wifi_connect();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_connect() failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
static void schedule_reconnect(void) {
|
||||
uint32_t shift = s_retry_count < WIFI_RECONNECT_BACKOFF_CAP_SHIFT ? s_retry_count : WIFI_RECONNECT_BACKOFF_CAP_SHIFT;
|
||||
uint32_t delay_ms = WIFI_RECONNECT_BASE_DELAY_MS << shift;
|
||||
if (delay_ms > WIFI_RECONNECT_MAX_DELAY_MS) {
|
||||
delay_ms = WIFI_RECONNECT_MAX_DELAY_MS;
|
||||
}
|
||||
s_retry_count++;
|
||||
ESP_LOGW(TAG, "reconnecting in %" PRIu32 " ms (attempt %" PRIu32 ")", delay_ms, s_retry_count);
|
||||
esp_timer_stop(s_reconnect_timer); // no-op if not running; keeps this idempotent
|
||||
esp_timer_start_once(s_reconnect_timer, (uint64_t)delay_ms * 1000ULL);
|
||||
}
|
||||
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data) {
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
wifi_event_sta_disconnected_t *disc = (wifi_event_sta_disconnected_t *)event_data;
|
||||
ESP_LOGW(TAG, "Wi-Fi disconnected (reason %d)", disc ? disc->reason : -1);
|
||||
schedule_reconnect();
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *evt = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "got IP: " IPSTR, IP2STR(&evt->ip_info.ip));
|
||||
s_retry_count = 0;
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_start(void) {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
s_wifi_event_group = xEventGroupCreate();
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
wifi_init_config_t init_cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&init_cfg));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL));
|
||||
|
||||
const esp_timer_create_args_t timer_args = {
|
||||
.callback = &reconnect_timer_cb,
|
||||
.name = "wifi_reconnect",
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &s_reconnect_timer));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.sta = {
|
||||
.ssid = DEVICE_WIFI_SSID,
|
||||
.password = DEVICE_WIFI_PASSWORD,
|
||||
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "Wi-Fi station starting, SSID \"%s\"", DEVICE_WIFI_SSID);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_wait_connected(TickType_t timeout_ticks) {
|
||||
if (s_wifi_event_group == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
EventBits_t bits = xEventGroupWaitBits(
|
||||
s_wifi_event_group, WIFI_CONNECTED_BIT,
|
||||
pdFALSE /* don't clear on exit */, pdFALSE /* any bit */, timeout_ticks);
|
||||
return (bits & WIFI_CONNECTED_BIT) ? ESP_OK : ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
bool wifi_manager_is_connected(void) {
|
||||
if (s_wifi_event_group == NULL) {
|
||||
return false;
|
||||
}
|
||||
return (xEventGroupGetBits(s_wifi_event_group) & WIFI_CONNECTED_BIT) != 0;
|
||||
}
|
||||
Reference in New Issue
Block a user