test: make firmware logic bugs catchable without hardware (Workstream F)
The firmware has never been flashed, and a real bug already reached the repo because of it: RD03E_FRAME_LEN was 5 for a 6-byte frame, so the footer check collided with the distance high byte and EVERY distance reading was garbage — always `lo | 0x5500`, about 218 metres, regardless of what the sensor saw. That was pure logic with no hardware dependency. It should have been catchable on a laptop, and there was simply no way to run the code. Extracted the hardware-free logic out of the three drivers — rd03e_parse, bmp280_compensate, mems_level — as moves rather than rewrites, carrying the explanatory comments along with the code they explain. The drivers now own only their bus I/O and call into the pure units, so nothing changes for the real device. `./run_tests.sh` builds them with gcc -Wall -Wextra -Werror plus a dependency-free assert harness: 175 checks, 0 failed, from a clean tree. Proven to catch the actual bug rather than assumed to: reintroducing FRAME_LEN 5 fails four checks, including one that reads "a simple-report frame is 6 bytes, not 5", plus the truncated-frame and 5-byte-window cases. Restored, green again. This does NOT make the firmware verified, and the README says so plainly — it is called a narrow exception and scoped to pure logic. Wiring, timing, real register behaviour and the reconstructed RD-03E frame format all still need the physical board. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Quantumancy sensor node — main component.
|
||||
#
|
||||
# The *_parse / *_compensate / *_level units are ESP-IDF-free pure logic,
|
||||
# split out of their drivers so they can also be compiled and tested on a
|
||||
# host with plain gcc — see ../test/run_tests.sh. They are listed here too
|
||||
# because the on-device build needs them linked in exactly the same way.
|
||||
#
|
||||
# device_config.h is intentionally NOT listed as a source: it's a header the
|
||||
# seeker generates locally (see device_config.h.example + README.md) and is
|
||||
# gitignored. If it's missing, the build will fail on the #include in
|
||||
@@ -13,8 +18,11 @@ idf_component_register(
|
||||
"telemetry_client.c"
|
||||
"sensor_registry.c"
|
||||
"bmp280.c"
|
||||
"bmp280_compensate.c"
|
||||
"rd03e.c"
|
||||
"rd03e_parse.c"
|
||||
"mems_mic.c"
|
||||
"mems_level.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
REQUIRES
|
||||
|
||||
@@ -4,17 +4,20 @@
|
||||
// BMP280 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 and the compensation formula below are
|
||||
// transcribed as directly as possible from the datasheet's section 3.11.1
|
||||
// (register map) and 3.11.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).
|
||||
// sensor. Register addresses are transcribed as directly as possible from
|
||||
// the datasheet's section 3.11.1 (register map) 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).
|
||||
//
|
||||
// This file owns only the I2C traffic. The calibration/ADC byte decoding
|
||||
// and the §3.11.3 compensation maths live in bmp280_compensate.h/.c, which
|
||||
// is ESP-IDF-free and unit-tested on a host with plain gcc (see ../test/).
|
||||
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <math.h>
|
||||
#include "bmp280.h"
|
||||
#include "bmp280_compensate.h"
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -41,21 +44,6 @@ static const char *TAG = "bmp280";
|
||||
|
||||
#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;
|
||||
} bmp280_calib_t;
|
||||
|
||||
static i2c_master_bus_handle_t s_bus = NULL;
|
||||
static i2c_master_dev_handle_t s_dev = NULL;
|
||||
static bmp280_calib_t s_calib;
|
||||
@@ -70,31 +58,13 @@ 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 buf[24]; // 0x88..0x9F
|
||||
uint8_t buf[BMP280_CALIB_LEN]; // 0x88..0x9F
|
||||
|
||||
esp_err_t err = read_regs(REG_CALIB00, buf, sizeof(buf));
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
s_calib.dig_T1 = u16(buf[0], buf[1]);
|
||||
s_calib.dig_T2 = s16(buf[2], buf[3]);
|
||||
s_calib.dig_T3 = s16(buf[4], buf[5]);
|
||||
s_calib.dig_P1 = u16(buf[6], buf[7]);
|
||||
s_calib.dig_P2 = s16(buf[8], buf[9]);
|
||||
s_calib.dig_P3 = s16(buf[10], buf[11]);
|
||||
s_calib.dig_P4 = s16(buf[12], buf[13]);
|
||||
s_calib.dig_P5 = s16(buf[14], buf[15]);
|
||||
s_calib.dig_P6 = s16(buf[16], buf[17]);
|
||||
s_calib.dig_P7 = s16(buf[18], buf[19]);
|
||||
s_calib.dig_P8 = s16(buf[20], buf[21]);
|
||||
s_calib.dig_P9 = s16(buf[22], buf[23]);
|
||||
bmp280_calib_from_regs(buf, &s_calib);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -162,36 +132,6 @@ fail:
|
||||
return err;
|
||||
}
|
||||
|
||||
// Bosch datasheet 3.11.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
|
||||
}
|
||||
|
||||
esp_err_t bmp280_read(sensor_reading_t *out, size_t max_out, size_t *out_count) {
|
||||
*out_count = 0;
|
||||
if (!s_ready) {
|
||||
@@ -222,16 +162,16 @@ esp_err_t bmp280_read(sensor_reading_t *out, size_t max_out, size_t *out_count)
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw[6];
|
||||
uint8_t raw[BMP280_RAW_LEN];
|
||||
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_P = 0, adc_T = 0;
|
||||
bmp280_adc_from_regs(raw, &adc_P, &adc_T);
|
||||
|
||||
double t_fine = 0.0;
|
||||
double temp_c = compensate_temperature(adc_T, &t_fine);
|
||||
double press_pa = compensate_pressure(adc_P, t_fine);
|
||||
double temp_c = bmp280_compensate_temperature(&s_calib, adc_T, &t_fine);
|
||||
double press_pa = bmp280_compensate_pressure(&s_calib, adc_P, t_fine);
|
||||
|
||||
size_t n = 0;
|
||||
memset(&out[n], 0, sizeof(out[n]));
|
||||
|
||||
66
firmware/esp32p4-sensor-node/main/bmp280_compensate.c
Normal file
66
firmware/esp32p4-sensor-node/main/bmp280_compensate.c
Normal file
@@ -0,0 +1,66 @@
|
||||
// BMP280 compensation maths — pure logic. See bmp280_compensate.h.
|
||||
|
||||
#include "bmp280_compensate.h"
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void bmp280_calib_from_regs(const uint8_t buf[BMP280_CALIB_LEN], bmp280_calib_t *out) {
|
||||
if (buf == NULL || out == NULL) {
|
||||
return;
|
||||
}
|
||||
out->dig_T1 = u16(buf[0], buf[1]);
|
||||
out->dig_T2 = s16(buf[2], buf[3]);
|
||||
out->dig_T3 = s16(buf[4], buf[5]);
|
||||
out->dig_P1 = u16(buf[6], buf[7]);
|
||||
out->dig_P2 = s16(buf[8], buf[9]);
|
||||
out->dig_P3 = s16(buf[10], buf[11]);
|
||||
out->dig_P4 = s16(buf[12], buf[13]);
|
||||
out->dig_P5 = s16(buf[14], buf[15]);
|
||||
out->dig_P6 = s16(buf[16], buf[17]);
|
||||
out->dig_P7 = s16(buf[18], buf[19]);
|
||||
out->dig_P8 = s16(buf[20], buf[21]);
|
||||
out->dig_P9 = s16(buf[22], buf[23]);
|
||||
}
|
||||
|
||||
void bmp280_adc_from_regs(const uint8_t raw[BMP280_RAW_LEN], int32_t *out_adc_P, int32_t *out_adc_T) {
|
||||
if (raw == NULL || out_adc_P == NULL || out_adc_T == NULL) {
|
||||
return;
|
||||
}
|
||||
*out_adc_P = ((int32_t)raw[0] << 12) | ((int32_t)raw[1] << 4) | (raw[2] >> 4);
|
||||
*out_adc_T = ((int32_t)raw[3] << 12) | ((int32_t)raw[4] << 4) | (raw[5] >> 4);
|
||||
}
|
||||
|
||||
// Bosch datasheet 3.11.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).
|
||||
|
||||
double bmp280_compensate_temperature(const bmp280_calib_t *c, int32_t adc_T, double *out_t_fine) {
|
||||
double var1 = (((double)adc_T) / 16384.0 - ((double)c->dig_T1) / 1024.0) * ((double)c->dig_T2);
|
||||
double var2 = ((((double)adc_T) / 131072.0 - ((double)c->dig_T1) / 8192.0) *
|
||||
(((double)adc_T) / 131072.0 - ((double)c->dig_T1) / 8192.0)) * ((double)c->dig_T3);
|
||||
*out_t_fine = var1 + var2;
|
||||
return (var1 + var2) / 5120.0; // degrees C
|
||||
}
|
||||
|
||||
double bmp280_compensate_pressure(const bmp280_calib_t *c, int32_t adc_P, double t_fine) {
|
||||
double var1 = (t_fine / 2.0) - 64000.0;
|
||||
double var2 = var1 * var1 * ((double)c->dig_P6) / 32768.0;
|
||||
var2 = var2 + var1 * ((double)c->dig_P5) * 2.0;
|
||||
var2 = (var2 / 4.0) + (((double)c->dig_P4) * 65536.0);
|
||||
var1 = (((double)c->dig_P3) * var1 * var1 / 524288.0 + ((double)c->dig_P2) * var1) / 524288.0;
|
||||
var1 = (1.0 + var1 / 32768.0) * ((double)c->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)c->dig_P9) * p * p / 2147483648.0;
|
||||
var2 = p * ((double)c->dig_P8) / 32768.0;
|
||||
p = p + (var1 + var2 + ((double)c->dig_P7)) / 16.0;
|
||||
return p; // Pa
|
||||
}
|
||||
66
firmware/esp32p4-sensor-node/main/bmp280_compensate.h
Normal file
66
firmware/esp32p4-sensor-node/main/bmp280_compensate.h
Normal file
@@ -0,0 +1,66 @@
|
||||
// Bosch BMP280 compensation maths + calibration/ADC decoding — PURE LOGIC.
|
||||
//
|
||||
// ESP-IDF-free by design: no I2C, no esp_err_t, no FreeRTOS, no logging.
|
||||
// Only <stdint.h>/<stddef.h>, so it compiles and is testable on a host
|
||||
// with plain gcc (see ../test/). `bmp280.c` does the I2C traffic and calls
|
||||
// in here for every byte-order decision and every line of arithmetic.
|
||||
//
|
||||
// The formulas are Bosch datasheet (rev 1.23) §3.11.3's double-precision
|
||||
// reference implementation, transcribed near-verbatim, with the variable
|
||||
// names kept close to the original so it's checkable against the datasheet
|
||||
// PDF side-by-side. The register map decoded below is §3.11.1.
|
||||
//
|
||||
// What host tests can prove here: byte-order/packing of the calibration
|
||||
// block and the 20-bit ADC words, and that the arithmetic behaves
|
||||
// sanely and monotonically. What they CANNOT prove: that this exact
|
||||
// silicon returns the register contents we assume.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Calibration block is 24 bytes at 0x88..0x9F (dig_T1..dig_P9).
|
||||
#define BMP280_CALIB_LEN 24
|
||||
// Burst measurement read is 6 bytes from 0xF7: press(3) + temp(3).
|
||||
#define BMP280_RAW_LEN 6
|
||||
|
||||
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;
|
||||
} bmp280_calib_t;
|
||||
|
||||
// Decode the 24-byte calibration block. Each coefficient is stored
|
||||
// little-endian (LSB first) in the register map.
|
||||
void bmp280_calib_from_regs(const uint8_t buf[BMP280_CALIB_LEN], bmp280_calib_t *out);
|
||||
|
||||
// Decode the 6-byte burst read into the two 20-bit ADC words. Pressure
|
||||
// comes first (0xF7..0xF9), then temperature (0xFA..0xFC); each is
|
||||
// MSB/LSB/XLSB with the XLSB's top nibble carrying the low 4 bits.
|
||||
void bmp280_adc_from_regs(const uint8_t raw[BMP280_RAW_LEN], int32_t *out_adc_P, int32_t *out_adc_T);
|
||||
|
||||
// Returns degrees C, and writes the shared `t_fine` intermediate that the
|
||||
// pressure compensation needs.
|
||||
double bmp280_compensate_temperature(const bmp280_calib_t *c, int32_t adc_T, double *out_t_fine);
|
||||
|
||||
// Returns Pa. Returns exactly 0.0 when the datasheet's own divide-by-zero
|
||||
// guard trips (var1 == 0, i.e. an all-zero / unread calibration block).
|
||||
double bmp280_compensate_pressure(const bmp280_calib_t *c, int32_t adc_P, double t_fine);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
26
firmware/esp32p4-sensor-node/main/mems_level.c
Normal file
26
firmware/esp32p4-sensor-node/main/mems_level.c
Normal file
@@ -0,0 +1,26 @@
|
||||
// MEMS mic level maths — pure logic. See mems_level.h.
|
||||
|
||||
#include "mems_level.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
double mems_level_rms(const int32_t *samples, size_t n_samples) {
|
||||
if (samples == NULL || n_samples == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
double sum_sq = 0.0;
|
||||
for (size_t i = 0; i < n_samples; i++) {
|
||||
double sample = (double)(samples[i] >> 8);
|
||||
sum_sq += sample * sample;
|
||||
}
|
||||
return sqrt(sum_sq / (double)n_samples);
|
||||
}
|
||||
|
||||
double mems_level_dbfs(double rms) {
|
||||
if (rms < 1.0) {
|
||||
return MEMS_DBFS_FLOOR; // effective noise floor
|
||||
}
|
||||
double dbfs = 20.0 * log10(rms / MEMS_FULL_SCALE_24BIT);
|
||||
if (dbfs < MEMS_DBFS_FLOOR) dbfs = MEMS_DBFS_FLOOR;
|
||||
return dbfs;
|
||||
}
|
||||
47
firmware/esp32p4-sensor-node/main/mems_level.h
Normal file
47
firmware/esp32p4-sensor-node/main/mems_level.h
Normal file
@@ -0,0 +1,47 @@
|
||||
// I2S MEMS microphone level maths (RMS -> dBFS) — PURE LOGIC.
|
||||
//
|
||||
// ESP-IDF-free by design: no I2S, no esp_err_t, no logging. Only
|
||||
// <stdint.h>/<stddef.h> and <math.h> in the .c, so it compiles and is
|
||||
// testable on a host with plain gcc (see ../test/). `mems_mic.c` does the
|
||||
// I2S read and calls in here for the arithmetic.
|
||||
//
|
||||
// The INMP441 family outputs 24-bit signed PCM, MSB-first, left-justified
|
||||
// in a 32-bit I2S slot (Philips/standard I2S timing). The right-shift-by-8
|
||||
// used below to recover the 24-bit sample from the 32-bit slot, and the
|
||||
// dBFS reference level (2^23, a 24-bit signed sample's full-scale
|
||||
// magnitude), are the commonly-documented values for this exact mic family
|
||||
// — but "commonly documented" is not "verified against this specific
|
||||
// board." Host tests prove the arithmetic (full scale reads ~0 dBFS,
|
||||
// silence reads the floor and never -inf/NaN); they cannot prove the shift
|
||||
// amount matches this module revision's real bit alignment.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// dBFS reference: full-scale magnitude of a 24-bit signed sample.
|
||||
#define MEMS_FULL_SCALE_24BIT (8388608.0) // 2^23
|
||||
|
||||
// Level reported for a true-silent (or sub-LSB) input, and the clamp
|
||||
// applied to anything quieter.
|
||||
#define MEMS_DBFS_FLOOR (-120.0)
|
||||
|
||||
// RMS over a block of raw 32-bit I2S slots. The mic's 24-bit sample is
|
||||
// left-justified in the 32-bit slot -- shift right 8 to recover it before
|
||||
// squaring, so the magnitude lines up with MEMS_FULL_SCALE_24BIT.
|
||||
// Returns 0.0 for an empty block.
|
||||
double mems_level_rms(const int32_t *samples, size_t n_samples);
|
||||
|
||||
// dBFS: 20*log10(rms / full_scale). A true-silent input gives rms=0,
|
||||
// which is -inf in dB -- clamp to a floor rather than emit a value the
|
||||
// JSON encoder/backend can't handle.
|
||||
double mems_level_dbfs(double rms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -2,13 +2,12 @@
|
||||
//
|
||||
// UNVERIFIED AGAINST REAL HARDWARE. Written against ESP-IDF's documented
|
||||
// `driver/i2s_std.h` API (the current idiomatic I2S driver, superseding the
|
||||
// older monolithic `driver/i2s.h`) and the INMP441 family's well-documented
|
||||
// output format: 24-bit signed PCM, MSB-first, left-justified in a 32-bit
|
||||
// I2S slot (Philips/standard I2S timing). The right-shift-by-8 used below
|
||||
// to recover the 24-bit sample from the 32-bit slot, and the dBFS
|
||||
// reference level (2^23, a 24-bit signed sample's full-scale magnitude),
|
||||
// are the commonly-documented values for this exact mic family — but
|
||||
// "commonly documented" is not "verified against this specific board," so
|
||||
// older monolithic `driver/i2s.h`). This file owns only the I2S traffic;
|
||||
// the RMS -> dBFS maths, the 24-bit-in-32-bit-slot shift and the honesty
|
||||
// notes about both live in mems_level.h/.c, which is ESP-IDF-free and
|
||||
// unit-tested on a host with plain gcc (see ../test/).
|
||||
//
|
||||
// "Commonly documented" is not "verified against this specific board," so
|
||||
// treat the very first real readings as a sanity check, not a given: talk
|
||||
// near the mic and confirm the reported level actually rises before
|
||||
// trusting it unattended.
|
||||
@@ -18,6 +17,7 @@
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include "mems_mic.h"
|
||||
#include "mems_level.h"
|
||||
#include "driver/i2s_std.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -25,8 +25,7 @@
|
||||
|
||||
static const char *TAG = "mems_mic";
|
||||
|
||||
// dBFS reference: full-scale magnitude of a 24-bit signed sample.
|
||||
#define FULL_SCALE_24BIT (8388608.0) // 2^23
|
||||
// dBFS reference level and noise floor live in mems_level.h.
|
||||
|
||||
static i2s_chan_handle_t s_rx_chan = NULL;
|
||||
static bool s_ready = false;
|
||||
@@ -119,26 +118,9 @@ esp_err_t mems_mic_read(sensor_reading_t *out, size_t max_out, size_t *out_count
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// RMS over the block. The mic's 24-bit sample is left-justified in the
|
||||
// 32-bit I2S slot -- shift right 8 to recover it before squaring, so
|
||||
// the magnitude lines up with FULL_SCALE_24BIT below.
|
||||
double sum_sq = 0.0;
|
||||
for (size_t i = 0; i < n_samples; i++) {
|
||||
double sample = (double)(s_sample_buf[i] >> 8);
|
||||
sum_sq += sample * sample;
|
||||
}
|
||||
double rms = sqrt(sum_sq / (double)n_samples);
|
||||
|
||||
// dBFS: 20*log10(rms / full_scale). A true-silent input gives rms=0,
|
||||
// which is -inf in dB -- clamp to a floor rather than emit a value the
|
||||
// JSON encoder/backend can't handle.
|
||||
double dbfs;
|
||||
if (rms < 1.0) {
|
||||
dbfs = -120.0; // effective noise floor
|
||||
} else {
|
||||
dbfs = 20.0 * log10(rms / FULL_SCALE_24BIT);
|
||||
if (dbfs < -120.0) dbfs = -120.0;
|
||||
}
|
||||
// All level arithmetic is in the pure, host-tested unit.
|
||||
double rms = mems_level_rms(s_sample_buf, n_samples);
|
||||
double dbfs = mems_level_dbfs(rms);
|
||||
|
||||
memset(&out[0], 0, sizeof(out[0]));
|
||||
strncpy(out[0].sensor_type, "evp", SENSOR_READING_TYPE_MAXLEN - 1);
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
// Ai-Thinker RD-03E driver — see rd03e.h for wiring and honesty notes.
|
||||
//
|
||||
// UNVERIFIED AGAINST REAL HARDWARE, and the frame format below is
|
||||
// reconstructed from a third-party bring-up write-up (electroniclinic.com's
|
||||
// RD-03E/ESP32 tutorial), not Ai-Thinker's own datasheet PDF (not available
|
||||
// while writing this) — treat this as the least-certain protocol detail in
|
||||
// this driver. What's cross-confirmed from multiple independent sources:
|
||||
// UART is 256000 baud / 8N1, and the module also has a separate, more
|
||||
// complex configuration-frame protocol (0xFD 0xFC 0xFB 0xFA header /
|
||||
// 0x04 0x03 0x02 0x01 footer) for calibration and firmware queries — this
|
||||
// driver does NOT implement that; it only reads the module's free-running
|
||||
// "simple report" output frames, which need no configuration to start
|
||||
// streaming after power-up.
|
||||
//
|
||||
// Simple report frame, as reconstructed (6 bytes total):
|
||||
// [0] 0xAA frame header
|
||||
// [1] gesture code raw value, meaning not confirmed against an
|
||||
// official datasheet — reported as-is in
|
||||
// metadata rather than translated to a label
|
||||
// that might be wrong
|
||||
// [2] distance lo byte distance_cm = lo | (hi << 8), little-endian
|
||||
// [3] distance hi byte
|
||||
// [4..5] 0x55 0x55 frame footer
|
||||
// Real bring-up should verify this against a logic analyzer capture before
|
||||
// trusting field values, same as the LD2410 driver this replaced.
|
||||
// UNVERIFIED AGAINST REAL HARDWARE. This file owns only the UART I/O; the
|
||||
// frame format, the frame scanner, and the honesty notes about how that
|
||||
// format was reconstructed all live in rd03e_parse.h/.c, which is
|
||||
// ESP-IDF-free so it can be unit-tested on a host with plain gcc (see
|
||||
// ../test/). Read rd03e_parse.h before trusting any field value from here.
|
||||
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include "rd03e.h"
|
||||
#include "rd03e_parse.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -36,18 +19,12 @@ static const char *TAG = "rd03e";
|
||||
|
||||
#define RD03E_RX_BUF_SIZE 512
|
||||
#define RD03E_SCRATCH_SIZE 256
|
||||
#define RD03E_FRAME_LEN 6
|
||||
|
||||
static const uint8_t FRAME_HEADER = 0xAA;
|
||||
static const uint8_t FRAME_FOOTER[2] = { 0x55, 0x55 };
|
||||
// Frame layout, frame length and the header/footer constants live in
|
||||
// rd03e_parse.h — one definition, host-tested.
|
||||
|
||||
static bool s_ready = false;
|
||||
|
||||
typedef struct {
|
||||
uint8_t gesture;
|
||||
uint16_t distance_cm;
|
||||
} rd03e_frame_t;
|
||||
|
||||
esp_err_t rd03e_init(void) {
|
||||
uart_config_t cfg = {
|
||||
.baud_rate = RD03E_UART_BAUD,
|
||||
@@ -107,23 +84,9 @@ esp_err_t rd03e_read(sensor_reading_t *out, size_t max_out, size_t *out_count) {
|
||||
return ESP_OK; // nothing new isn't a driver failure
|
||||
}
|
||||
|
||||
bool parsed_any = false;
|
||||
rd03e_frame_t latest = {0};
|
||||
|
||||
// Scan for the newest complete, validated frame in whatever arrived
|
||||
// this cycle; keep overwriting `latest` so we report the freshest one.
|
||||
for (int i = 0; i + RD03E_FRAME_LEN <= len; i++) {
|
||||
if (buf[i] != FRAME_HEADER) {
|
||||
continue;
|
||||
}
|
||||
if (memcmp(&buf[i + 4], FRAME_FOOTER, 2) != 0) {
|
||||
continue; // not a real header byte, or a corrupted frame
|
||||
}
|
||||
latest.gesture = buf[i + 1];
|
||||
latest.distance_cm = (uint16_t)buf[i + 2] | ((uint16_t)buf[i + 3] << 8);
|
||||
parsed_any = true;
|
||||
i += RD03E_FRAME_LEN - 1; // loop's i++ moves past this frame
|
||||
}
|
||||
// All frame-finding/validation is in the pure, host-tested unit.
|
||||
bool parsed_any = rd03e_parse_latest(buf, (size_t)len, &latest);
|
||||
|
||||
if (!parsed_any) {
|
||||
ESP_LOGD(TAG, "no complete/valid RD-03E frame in this read window");
|
||||
|
||||
43
firmware/esp32p4-sensor-node/main/rd03e_parse.c
Normal file
43
firmware/esp32p4-sensor-node/main/rd03e_parse.c
Normal file
@@ -0,0 +1,43 @@
|
||||
// RD-03E frame scanner — pure logic, host-testable. See rd03e_parse.h.
|
||||
|
||||
#include "rd03e_parse.h"
|
||||
|
||||
bool rd03e_parse_latest(const uint8_t *buf, size_t len, rd03e_frame_t *out) {
|
||||
if (buf == NULL || out == NULL || len < RD03E_FRAME_LEN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parsed_any = false;
|
||||
rd03e_frame_t latest = {0};
|
||||
|
||||
// Scan for the newest complete, validated frame in whatever arrived
|
||||
// this cycle; keep overwriting `latest` so we report the freshest one.
|
||||
//
|
||||
// The `i + RD03E_FRAME_LEN <= len` bound is what makes a truncated
|
||||
// trailing frame get ignored rather than read past the buffer: a
|
||||
// partial frame at the end simply never satisfies the bound.
|
||||
for (size_t i = 0; i + RD03E_FRAME_LEN <= len; i++) {
|
||||
if (buf[i] != RD03E_FRAME_HEADER) {
|
||||
continue;
|
||||
}
|
||||
// Footer lives at [4] and [5] of the frame. If this offset only
|
||||
// *looks* like a header (a 0xAA that is really a distance byte, a
|
||||
// gesture code, or line noise), the footer check rejects it and
|
||||
// the scan resynchronises on the next byte.
|
||||
if (buf[i + 4] != RD03E_FRAME_FOOTER0 || buf[i + 5] != RD03E_FRAME_FOOTER1) {
|
||||
continue; // not a real header byte, or a corrupted frame
|
||||
}
|
||||
latest.gesture = buf[i + 1];
|
||||
// Little-endian: low byte first. Getting this backwards, or
|
||||
// letting the footer bytes bleed into the high byte, is exactly
|
||||
// the bug this unit exists to make testable.
|
||||
latest.distance_cm = (uint16_t)((uint16_t)buf[i + 2] | ((uint16_t)buf[i + 3] << 8));
|
||||
parsed_any = true;
|
||||
i += RD03E_FRAME_LEN - 1; // loop's i++ moves past this frame
|
||||
}
|
||||
|
||||
if (parsed_any) {
|
||||
*out = latest;
|
||||
}
|
||||
return parsed_any;
|
||||
}
|
||||
77
firmware/esp32p4-sensor-node/main/rd03e_parse.h
Normal file
77
firmware/esp32p4-sensor-node/main/rd03e_parse.h
Normal file
@@ -0,0 +1,77 @@
|
||||
// Ai-Thinker RD-03E "simple report" frame scanner — PURE LOGIC.
|
||||
//
|
||||
// This unit is deliberately free of ESP-IDF: no UART, no esp_err_t, no
|
||||
// FreeRTOS, no logging. It includes only <stdint.h>/<stddef.h>/<stdbool.h>
|
||||
// so it can be compiled and tested on a host with plain gcc (see
|
||||
// ../test/). `rd03e.c` does the UART I/O and calls in here to do the
|
||||
// actual parsing.
|
||||
//
|
||||
// The reason this split exists: the first version of this firmware had
|
||||
// RD03E_FRAME_LEN set to 5 for a 6-byte frame, so the footer check read
|
||||
// the distance high byte instead of the second footer byte and every
|
||||
// distance reading came back as `lo | 0x5500` (~218 m). That was pure
|
||||
// logic with zero hardware dependency and should have been catchable on a
|
||||
// laptop. Now it is.
|
||||
//
|
||||
// UNVERIFIED AGAINST REAL HARDWARE, and the frame format below is
|
||||
// reconstructed from a third-party bring-up write-up (electroniclinic.com's
|
||||
// RD-03E/ESP32 tutorial), not Ai-Thinker's own datasheet PDF (not available
|
||||
// while writing this) — treat this as the least-certain protocol detail in
|
||||
// this driver. What's cross-confirmed from multiple independent sources:
|
||||
// UART is 256000 baud / 8N1, and the module also has a separate, more
|
||||
// complex configuration-frame protocol (0xFD 0xFC 0xFB 0xFA header /
|
||||
// 0x04 0x03 0x02 0x01 footer) for calibration and firmware queries — this
|
||||
// driver does NOT implement that; it only reads the module's free-running
|
||||
// "simple report" output frames, which need no configuration to start
|
||||
// streaming after power-up.
|
||||
//
|
||||
// Simple report frame, as reconstructed (6 bytes total):
|
||||
// [0] 0xAA frame header
|
||||
// [1] gesture code raw value, meaning not confirmed against an
|
||||
// official datasheet — reported as-is in
|
||||
// metadata rather than translated to a label
|
||||
// that might be wrong
|
||||
// [2] distance lo byte distance_cm = lo | (hi << 8), little-endian
|
||||
// [3] distance hi byte
|
||||
// [4..5] 0x55 0x55 frame footer
|
||||
// Real bring-up should verify this against a logic analyzer capture before
|
||||
// trusting field values, same as the LD2410 driver this replaced.
|
||||
//
|
||||
// Host tests prove the SHAPE of the parse (byte order, frame length,
|
||||
// footer validation, resynchronisation) against this reconstructed spec.
|
||||
// They cannot prove the reconstructed spec is what the silicon emits.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Total bytes in one simple-report frame: header + gesture + 2 distance
|
||||
// bytes + 2 footer bytes. Six, not five — see the note above.
|
||||
#define RD03E_FRAME_LEN 6
|
||||
|
||||
#define RD03E_FRAME_HEADER 0xAAu
|
||||
#define RD03E_FRAME_FOOTER0 0x55u
|
||||
#define RD03E_FRAME_FOOTER1 0x55u
|
||||
|
||||
typedef struct {
|
||||
uint8_t gesture;
|
||||
uint16_t distance_cm;
|
||||
} rd03e_frame_t;
|
||||
|
||||
// Scan `buf` (`len` bytes) for complete, footer-validated simple-report
|
||||
// frames and write the NEWEST one (highest offset) to *out.
|
||||
//
|
||||
// Returns true if at least one valid frame was found, false otherwise
|
||||
// (in which case *out is untouched). A NULL buf, a NULL out, or a buffer
|
||||
// shorter than one frame are all "no frame", not a crash.
|
||||
bool rd03e_parse_latest(const uint8_t *buf, size_t len, rd03e_frame_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user