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>
302 lines
11 KiB
C
302 lines
11 KiB
C
// Bosch BME280 driver — see bme280.h for wiring and honesty notes.
|
|
//
|
|
// UNVERIFIED AGAINST REAL HARDWARE: this has been written against the public
|
|
// BME280 datasheet (Bosch Sensortec, document rev 1.23) and ESP-IDF's
|
|
// documented `driver/i2c_master.h` API surface, and reasoned about carefully,
|
|
// but never compiled with a real ESP-IDF toolchain nor run against a real
|
|
// sensor. Register addresses, the calibration-word packing, and the
|
|
// compensation formulas below are transcribed as directly as possible from
|
|
// the datasheet's section 4.2.2 (register map) and 4.2.3 (double-precision
|
|
// compensation formula reference implementation) to minimize transcription
|
|
// risk, but a real bring-up should sanity-check first readings against a
|
|
// known-good reference (e.g. compare to a household thermometer/barometer).
|
|
|
|
#include <string.h>
|
|
#include <stdbool.h>
|
|
#include <math.h>
|
|
#include "bme280.h"
|
|
#include "driver/i2c_master.h"
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
|
|
static const char *TAG = "bme280";
|
|
|
|
// --- Register map (BME280 datasheet section 4.2.2) -------------------------
|
|
#define REG_CHIP_ID 0xD0
|
|
#define REG_RESET 0xE0
|
|
#define REG_CTRL_HUM 0xF2
|
|
#define REG_STATUS 0xF3
|
|
#define REG_CTRL_MEAS 0xF4
|
|
#define REG_CONFIG 0xF5
|
|
#define REG_PRESS_MSB 0xF7 // press(3) + temp(3) + hum(2) = 8 bytes, burst-read from here
|
|
#define REG_CALIB00 0x88 // dig_T1..dig_P9, 26 bytes: 0x88-0xA1
|
|
#define REG_CALIB_H1 0xA1 // dig_H1, 1 byte
|
|
#define REG_CALIB26 0xE1 // dig_H2..dig_H6, 7 bytes: 0xE1-0xE7
|
|
|
|
#define CHIP_ID_EXPECTED 0x60
|
|
#define RESET_MAGIC 0xB6
|
|
|
|
#define STATUS_MEASURING_BIT 0x08
|
|
|
|
typedef struct {
|
|
uint16_t dig_T1;
|
|
int16_t dig_T2;
|
|
int16_t dig_T3;
|
|
uint16_t dig_P1;
|
|
int16_t dig_P2;
|
|
int16_t dig_P3;
|
|
int16_t dig_P4;
|
|
int16_t dig_P5;
|
|
int16_t dig_P6;
|
|
int16_t dig_P7;
|
|
int16_t dig_P8;
|
|
int16_t dig_P9;
|
|
uint8_t dig_H1;
|
|
int16_t dig_H2;
|
|
uint8_t dig_H3;
|
|
int16_t dig_H4;
|
|
int16_t dig_H5;
|
|
int8_t dig_H6;
|
|
} bme280_calib_t;
|
|
|
|
static i2c_master_bus_handle_t s_bus = NULL;
|
|
static i2c_master_dev_handle_t s_dev = NULL;
|
|
static bme280_calib_t s_calib;
|
|
static bool s_ready = false;
|
|
|
|
static esp_err_t write_reg(uint8_t reg, uint8_t val) {
|
|
uint8_t buf[2] = { reg, val };
|
|
return i2c_master_transmit(s_dev, buf, sizeof(buf), 1000 /* ms */);
|
|
}
|
|
|
|
static esp_err_t read_regs(uint8_t reg, uint8_t *out, size_t len) {
|
|
return i2c_master_transmit_receive(s_dev, ®, 1, out, len, 1000 /* ms */);
|
|
}
|
|
|
|
static int16_t s16(uint8_t lsb, uint8_t msb) {
|
|
return (int16_t)((uint16_t)msb << 8 | lsb);
|
|
}
|
|
static uint16_t u16(uint8_t lsb, uint8_t msb) {
|
|
return (uint16_t)((uint16_t)msb << 8 | lsb);
|
|
}
|
|
|
|
static esp_err_t read_calibration(void) {
|
|
uint8_t buf1[26]; // 0x88..0xA1
|
|
uint8_t h1;
|
|
uint8_t buf2[7]; // 0xE1..0xE7
|
|
|
|
esp_err_t err = read_regs(REG_CALIB00, buf1, sizeof(buf1));
|
|
if (err != ESP_OK) return err;
|
|
err = read_regs(REG_CALIB_H1, &h1, 1);
|
|
if (err != ESP_OK) return err;
|
|
err = read_regs(REG_CALIB26, buf2, sizeof(buf2));
|
|
if (err != ESP_OK) return err;
|
|
|
|
s_calib.dig_T1 = u16(buf1[0], buf1[1]);
|
|
s_calib.dig_T2 = s16(buf1[2], buf1[3]);
|
|
s_calib.dig_T3 = s16(buf1[4], buf1[5]);
|
|
s_calib.dig_P1 = u16(buf1[6], buf1[7]);
|
|
s_calib.dig_P2 = s16(buf1[8], buf1[9]);
|
|
s_calib.dig_P3 = s16(buf1[10], buf1[11]);
|
|
s_calib.dig_P4 = s16(buf1[12], buf1[13]);
|
|
s_calib.dig_P5 = s16(buf1[14], buf1[15]);
|
|
s_calib.dig_P6 = s16(buf1[16], buf1[17]);
|
|
s_calib.dig_P7 = s16(buf1[18], buf1[19]);
|
|
s_calib.dig_P8 = s16(buf1[20], buf1[21]);
|
|
s_calib.dig_P9 = s16(buf1[22], buf1[23]);
|
|
// buf1[24] is reserved (0xA0), buf1[25] would be dig_H1 duplicate on some
|
|
// parts — we read dig_H1 explicitly from 0xA1 above instead of relying
|
|
// on that, to match the datasheet's documented address exactly.
|
|
s_calib.dig_H1 = h1;
|
|
|
|
// dig_H4/dig_H5 have an odd 12-bit packing across 3 bytes (datasheet
|
|
// 4.2.2, table 16):
|
|
// dig_H4 = (E4[11:4] << 4) | E5[3:0]
|
|
// dig_H5 = (E6[11:4] << 4) | (E5[7:4])
|
|
uint8_t e1 = buf2[0]; // 0xE1 -> dig_H2 lsb
|
|
uint8_t e2 = buf2[1]; // 0xE2 -> dig_H2 msb
|
|
uint8_t e3 = buf2[2]; // 0xE3 -> dig_H3
|
|
uint8_t e4 = buf2[3]; // 0xE4
|
|
uint8_t e5 = buf2[4]; // 0xE5
|
|
uint8_t e6 = buf2[5]; // 0xE6
|
|
uint8_t e7 = buf2[6]; // 0xE7 -> dig_H6
|
|
|
|
s_calib.dig_H2 = s16(e1, e2);
|
|
s_calib.dig_H3 = e3;
|
|
s_calib.dig_H4 = (int16_t)(((int8_t)e4 << 4) | (e5 & 0x0F));
|
|
s_calib.dig_H5 = (int16_t)(((int8_t)e6 << 4) | (e5 >> 4));
|
|
s_calib.dig_H6 = (int8_t)e7;
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t bme280_init(void) {
|
|
i2c_master_bus_config_t bus_cfg = {
|
|
.i2c_port = BME280_I2C_PORT,
|
|
.sda_io_num = BME280_I2C_SDA_GPIO,
|
|
.scl_io_num = BME280_I2C_SCL_GPIO,
|
|
.clk_source = I2C_CLK_SRC_DEFAULT,
|
|
.glitch_ignore_cnt = 7,
|
|
.flags.enable_internal_pullup = true,
|
|
};
|
|
esp_err_t err = i2c_new_master_bus(&bus_cfg, &s_bus);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
i2c_device_config_t dev_cfg = {
|
|
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
|
.device_address = BME280_I2C_ADDR,
|
|
.scl_speed_hz = BME280_I2C_CLK_HZ,
|
|
};
|
|
err = i2c_master_bus_add_device(s_bus, &dev_cfg, &s_dev);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
uint8_t chip_id = 0;
|
|
err = read_regs(REG_CHIP_ID, &chip_id, 1);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "chip id read failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
if (chip_id != CHIP_ID_EXPECTED) {
|
|
ESP_LOGW(TAG, "unexpected chip id 0x%02x (want 0x%02x) -- wrong wiring/address, or a BMP280 (no humidity)?", chip_id, CHIP_ID_EXPECTED);
|
|
// Don't hard-fail: a BMP280 (temp+pressure only, same register map
|
|
// minus humidity) would also land here and can still usefully report
|
|
// two of the three readings. We proceed and let real data speak.
|
|
}
|
|
|
|
err = write_reg(REG_RESET, RESET_MAGIC);
|
|
if (err != ESP_OK) return err;
|
|
vTaskDelay(pdMS_TO_TICKS(10)); // datasheet: allow >= 2ms after reset
|
|
|
|
err = read_calibration();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "calibration read failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
// Humidity oversampling x1. Must be written before ctrl_meas for the
|
|
// change to take effect (datasheet 5.4.3).
|
|
err = write_reg(REG_CTRL_HUM, 0x01);
|
|
if (err != ESP_OK) return err;
|
|
|
|
s_ready = true;
|
|
ESP_LOGI(TAG, "BME280 init ok (chip id 0x%02x)", chip_id);
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Bosch datasheet 4.2.3 double-precision reference compensation formulas,
|
|
// transcribed near-verbatim (variable names kept close to the original so
|
|
// it's checkable against the datasheet PDF side-by-side).
|
|
|
|
static double compensate_temperature(int32_t adc_T, double *out_t_fine) {
|
|
double var1 = (((double)adc_T) / 16384.0 - ((double)s_calib.dig_T1) / 1024.0) * ((double)s_calib.dig_T2);
|
|
double var2 = ((((double)adc_T) / 131072.0 - ((double)s_calib.dig_T1) / 8192.0) *
|
|
(((double)adc_T) / 131072.0 - ((double)s_calib.dig_T1) / 8192.0)) * ((double)s_calib.dig_T3);
|
|
*out_t_fine = var1 + var2;
|
|
return (var1 + var2) / 5120.0; // degrees C
|
|
}
|
|
|
|
static double compensate_pressure(int32_t adc_P, double t_fine) {
|
|
double var1 = (t_fine / 2.0) - 64000.0;
|
|
double var2 = var1 * var1 * ((double)s_calib.dig_P6) / 32768.0;
|
|
var2 = var2 + var1 * ((double)s_calib.dig_P5) * 2.0;
|
|
var2 = (var2 / 4.0) + (((double)s_calib.dig_P4) * 65536.0);
|
|
var1 = (((double)s_calib.dig_P3) * var1 * var1 / 524288.0 + ((double)s_calib.dig_P2) * var1) / 524288.0;
|
|
var1 = (1.0 + var1 / 32768.0) * ((double)s_calib.dig_P1);
|
|
if (var1 == 0.0) {
|
|
return 0.0; // avoid divide-by-zero per datasheet's own guard
|
|
}
|
|
double p = 1048576.0 - (double)adc_P;
|
|
p = (p - (var2 / 4096.0)) * 6250.0 / var1;
|
|
var1 = ((double)s_calib.dig_P9) * p * p / 2147483648.0;
|
|
var2 = p * ((double)s_calib.dig_P8) / 32768.0;
|
|
p = p + (var1 + var2 + ((double)s_calib.dig_P7)) / 16.0;
|
|
return p; // Pa
|
|
}
|
|
|
|
static double compensate_humidity(int32_t adc_H, double t_fine) {
|
|
double var_h = (t_fine - 76800.0);
|
|
var_h = (adc_H - (((double)s_calib.dig_H4) * 64.0 + ((double)s_calib.dig_H5) / 16384.0 * var_h)) *
|
|
(((double)s_calib.dig_H2) / 65536.0 * (1.0 + ((double)s_calib.dig_H6) / 67108864.0 * var_h *
|
|
(1.0 + ((double)s_calib.dig_H3) / 67108864.0 * var_h)));
|
|
var_h = var_h * (1.0 - ((double)s_calib.dig_H1) * var_h / 524288.0);
|
|
if (var_h > 100.0) var_h = 100.0;
|
|
if (var_h < 0.0) var_h = 0.0;
|
|
return var_h; // %RH
|
|
}
|
|
|
|
esp_err_t bme280_read(sensor_reading_t *out, size_t max_out, size_t *out_count) {
|
|
*out_count = 0;
|
|
if (!s_ready) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (max_out < 3) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
// Forced mode: osrs_t=1 (001), osrs_p=1 (001), mode=forced (01).
|
|
// ctrl_meas = 0b001_001_01 = 0x25
|
|
esp_err_t err = write_reg(REG_CTRL_MEAS, 0x25);
|
|
if (err != ESP_OK) return err;
|
|
|
|
// Poll status until the "measuring" bit clears, with a hard cap so a
|
|
// wedged bus/sensor can't hang the telemetry task forever.
|
|
for (int attempt = 0; attempt < 20; attempt++) {
|
|
uint8_t status = 0;
|
|
err = read_regs(REG_STATUS, &status, 1);
|
|
if (err != ESP_OK) return err;
|
|
if ((status & STATUS_MEASURING_BIT) == 0) {
|
|
break;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
if (attempt == 19) {
|
|
ESP_LOGW(TAG, "measurement did not complete in time");
|
|
return ESP_ERR_TIMEOUT;
|
|
}
|
|
}
|
|
|
|
uint8_t raw[8];
|
|
err = read_regs(REG_PRESS_MSB, raw, sizeof(raw));
|
|
if (err != ESP_OK) return err;
|
|
|
|
int32_t adc_P = ((int32_t)raw[0] << 12) | ((int32_t)raw[1] << 4) | (raw[2] >> 4);
|
|
int32_t adc_T = ((int32_t)raw[3] << 12) | ((int32_t)raw[4] << 4) | (raw[5] >> 4);
|
|
int32_t adc_H = ((int32_t)raw[6] << 8) | raw[7];
|
|
|
|
double t_fine = 0.0;
|
|
double temp_c = compensate_temperature(adc_T, &t_fine);
|
|
double press_pa = compensate_pressure(adc_P, t_fine);
|
|
double hum_pct = compensate_humidity(adc_H, t_fine);
|
|
|
|
size_t n = 0;
|
|
memset(&out[n], 0, sizeof(out[n]));
|
|
strncpy(out[n].sensor_type, "temperature", SENSOR_READING_TYPE_MAXLEN - 1);
|
|
out[n].value = temp_c;
|
|
strncpy(out[n].unit, "c", SENSOR_READING_UNIT_MAXLEN - 1);
|
|
out[n].metadata = NULL;
|
|
n++;
|
|
|
|
memset(&out[n], 0, sizeof(out[n]));
|
|
strncpy(out[n].sensor_type, "humidity", SENSOR_READING_TYPE_MAXLEN - 1);
|
|
out[n].value = hum_pct;
|
|
strncpy(out[n].unit, "pct", SENSOR_READING_UNIT_MAXLEN - 1);
|
|
out[n].metadata = NULL;
|
|
n++;
|
|
|
|
memset(&out[n], 0, sizeof(out[n]));
|
|
strncpy(out[n].sensor_type, "pressure", SENSOR_READING_TYPE_MAXLEN - 1);
|
|
out[n].value = press_pa / 100.0; // Pa -> hPa
|
|
strncpy(out[n].unit, "hpa", SENSOR_READING_UNIT_MAXLEN - 1);
|
|
out[n].metadata = NULL;
|
|
n++;
|
|
|
|
*out_count = n;
|
|
return ESP_OK;
|
|
}
|