// 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 #include #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; }