// 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. #include #include #include "rd03e.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 #define RD03E_FRAME_LEN 6 static const uint8_t FRAME_HEADER = 0xAA; static const uint8_t FRAME_FOOTER[2] = { 0x55, 0x55 }; 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, .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 } 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 } 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; }