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>
118 lines
4.2 KiB
C
118 lines
4.2 KiB
C
// Ai-Thinker RD-03E driver — see rd03e.h for wiring and honesty notes.
|
|
//
|
|
// 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"
|
|
#include "freertos/task.h"
|
|
|
|
static const char *TAG = "rd03e";
|
|
|
|
#define RD03E_RX_BUF_SIZE 512
|
|
#define RD03E_SCRATCH_SIZE 256
|
|
|
|
// Frame layout, frame length and the header/footer constants live in
|
|
// rd03e_parse.h — one definition, host-tested.
|
|
|
|
static bool s_ready = false;
|
|
|
|
esp_err_t rd03e_init(void) {
|
|
uart_config_t cfg = {
|
|
.baud_rate = RD03E_UART_BAUD,
|
|
.data_bits = UART_DATA_8_BITS,
|
|
.parity = UART_PARITY_DISABLE,
|
|
.stop_bits = UART_STOP_BITS_1,
|
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
|
.source_clk = UART_SCLK_DEFAULT,
|
|
};
|
|
|
|
esp_err_t err = uart_param_config(RD03E_UART_PORT, &cfg);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
// uart_set_pin(port, tx_pin, rx_pin, rts_pin, cts_pin) -- our TX GPIO
|
|
// wires to the module's RX (labeled "RX" on the module), and our RX
|
|
// GPIO wires to the module's TX output (labeled "OT1" on the module).
|
|
err = uart_set_pin(RD03E_UART_PORT, RD03E_UART_TX_GPIO, RD03E_UART_RX_GPIO,
|
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
err = uart_driver_install(RD03E_UART_PORT, RD03E_RX_BUF_SIZE, 0, 0, NULL, 0);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
s_ready = true;
|
|
ESP_LOGI(TAG, "RD-03E UART init ok on port %d (%d baud)", RD03E_UART_PORT, RD03E_UART_BAUD);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t rd03e_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 < 1) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
uint8_t buf[RD03E_SCRATCH_SIZE];
|
|
// The module free-runs its simple report frames, so a short read should
|
|
// find at least one complete frame already queued in the RX ring buffer.
|
|
int len = uart_read_bytes(RD03E_UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(200));
|
|
if (len < 0) {
|
|
ESP_LOGW(TAG, "uart_read_bytes error");
|
|
return ESP_FAIL;
|
|
}
|
|
if (len == 0) {
|
|
ESP_LOGD(TAG, "no UART data from RD-03E this cycle");
|
|
return ESP_OK; // nothing new isn't a driver failure
|
|
}
|
|
|
|
rd03e_frame_t latest = {0};
|
|
// 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");
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Reported as a numeric distance (not a boolean "presence" flag, unlike
|
|
// the LD2410 this replaced) — a handheld "sense a presence at a
|
|
// distance" device wants magnitude, and it lets the backend's
|
|
// statistical anomaly detector treat "something got suddenly close" as
|
|
// the anomaly signal, the same way it already treats a temperature
|
|
// spike, rather than only firing on a coarse absent/present transition.
|
|
memset(&out[0], 0, sizeof(out[0]));
|
|
strncpy(out[0].sensor_type, "presence", SENSOR_READING_TYPE_MAXLEN - 1);
|
|
out[0].value = (double)latest.distance_cm;
|
|
strncpy(out[0].unit, "cm", SENSOR_READING_UNIT_MAXLEN - 1);
|
|
|
|
cJSON *meta = cJSON_CreateObject();
|
|
if (meta != NULL) {
|
|
// Raw code, not translated to a label -- see the honesty note above
|
|
// about the gesture byte's exact meaning being unconfirmed.
|
|
cJSON_AddNumberToObject(meta, "gesture_code", latest.gesture);
|
|
}
|
|
out[0].metadata = meta;
|
|
|
|
*out_count = 1;
|
|
return ESP_OK;
|
|
}
|