# DFRobot C4001 mmWave presence sensor on ESP32 Live-data and calibration rig for a C4001 24 GHz radar. The board hosts a web page showing presence, distance, velocity and signal energy in real time, with sliders for every sensor parameter. Two boards are supported. Both are kept working; nothing is deleted when you switch. ## Choosing a board Change one line in `platformio.ini`: ```ini [platformio] default_envs = yd_esp32_s3 ; or: xiao_esp32c6 ``` Or build one without editing the file: ```bash pio run -e xiao_esp32c6 -t upload ``` Pin maps live in [src/board_pins.h](src/board_pins.h), one block per board. To add a board: add an `[env:...]` with a new `-DBOARD_xxx` flag, then a matching block in that header. Nothing else in the firmware is board-specific. | | YD-ESP32-S3 | XIAO ESP32-C6 | |---|---|---| | env | `yd_esp32_s3` | `xiao_esp32c6` | | MCU | ESP32-S3-WROOM-1 N16R8, rev v0.2 | ESP32-C6FH4 rev v0.1 | | Flash / PSRAM | 16 MB quad / 8 MB octal | 4 MB / none | | MAC | `24:58:7c:cd:9a:d8` | `e4:b3:23:b4:7b:c4` | | Serial | UART0 → CH343 (`1a86:55d3`), COM port | native USB Serial/JTAG (`303a:1001`) | | Free heap | ~301 KB | ~250 KB | Both verified: builds clean, flashes, boots, joins WiFi, serves the UI. ## Wiring the C4001 Power the sensor from **3V3, not 5V.** The C4001 accepts 3.3–5 V, but at 5 V its `OUT` pin can swing to 5 V and neither ESP32's GPIOs are 5 V tolerant. **Prefer I2C.** See the vendor-library warning below — the UART path in DFRobot's driver has loops with no timeout. I2C is bounded and safe. The firmware auto-detects which bus the sensor is on, so wire whichever your board's mode switch is set to. ### YD-ESP32-S3 The YD board has GPIO numbers printed on the silkscreen, so read the board itself — the header order below is only for orientation. | C4001 | Pin | Header | |---|---|---| | VIN | 3V3 | P1, top | | GND | GND | P1 bottom, or either GND on P2 | | SCL | **GPIO9** | P1 | | SDA | **GPIO8** | P1 | | TX (UART) | **GPIO18** | P1 | | RX (UART) | **GPIO17** | P1 | | OUT | GPIO16 | P1, optional | Header order, USB-C end at the top: ``` P1 (left) P2 (right) 1 3V3 1 GND 2 3V3 2 GPIO43 U0TXD -> CH343 3 RST 3 GPIO44 U0RXD <- CH343 4 GPIO4 4 GPIO1 5 GPIO5 5 GPIO2 6 GPIO6 6 GPIO42 7 GPIO7 7 GPIO41 8 GPIO15 8 GPIO40 9 GPIO16 <- C4001 OUT 9 GPIO39 10 GPIO17 -> C4001 RX 10 GPIO38 11 GPIO18 <- C4001 TX 11 GPIO37 ** PSRAM ** 12 GPIO8 <- C4001 SDA 12 GPIO36 ** PSRAM ** 13 GPIO3 13 GPIO35 ** PSRAM ** 14 GPIO46 strapping 14 GPIO0 BOOT button 15 GPIO9 <- C4001 SCL 15 GPIO45 strapping 16 GPIO10 16 GPIO48 RGB WS2812 17 GPIO11 17 GPIO47 18 GPIO12 18 GPIO21 19 GPIO13 19 GPIO20 USB D+ 20 GPIO14 20 GPIO19 USB D- 21 5V 21 GND 22 GND 22 GND ``` Do not use for I/O: **GPIO35/36/37** (octal PSRAM — the R8 in N16R8), **GPIO43/44** (UART0 to the CH343), **GPIO19/20** (native USB), GPIO0/45/46 (strapping), GPIO48 (RGB LED), GPIO26–32 (flash, not broken out). The board has **two USB-C sockets**. Use the one marked **COM** (CH343) — that's what `default_envs = yd_esp32_s3` assumes, and it puts `Serial` on a real UART instead of USB CDC. ### XIAO ESP32-C6 The XIAO's pads are unlabelled. Component side up, USB-C at the top; pin 1 of each column is nearest the connector. ``` ┌───┐ USB-C ┌───┐ ┌─────┴───┴───────┴───┴─────┐ D0 ────┤ o o ├──── VBUS (5 V) GPIO0 │ │ D1 ────┤ o o ├──── GND GPIO1 │ │ D2 ────┤ o seeed studio o ├──── 3V3 GPIO2 │ XIAO-ESP32-C6 │ D3 ────┤ o o ├──── D10 GPIO18 GPIO21 │ │ D4 ────┤ o o ├──── D9 GPIO20 GPIO22 │ │ D5 ────┤ o o ├──── D8 GPIO19 GPIO23 │ │ D6 ────┤ o [u.FL] [ant] o ├──── D7 GPIO17 GPIO16 └───────────────────────────┘ ``` | C4001 | XIAO pin | |---|---| | VIN | 3V3 | | GND | GND | | SCL | **D5** (GPIO23) | | SDA | **D4** (GPIO22) | | TX (UART) | **D9** (GPIO20) | | RX (UART) | **D10** (GPIO18) | | OUT | D2 (GPIO2), optional | Not on the headers but used by the firmware: GPIO15 user LED (active LOW), GPIO3 RF switch enable (LOW = on), GPIO14 antenna select (LOW = on-board ceramic). Without the RF switch lines, WiFi range is centimetres. ## Setup ```bash cp include/secrets.h.example include/secrets.h $EDITOR include/secrets.h # your WiFi; the file is gitignored ``` Skipping this is fine — the board then goes straight to AP mode (`xiao-mmwave` / `mmwave1234`). ```bash pio run # build the default env pio run -t upload # flash pio device monitor # 115200 ``` Web UI at the IP printed on serial, or `http://mmwave.local/`. ## Traps found the hard way **The vendor library's UART path can hang forever.** In `DFRobot_C4001.cpp`, `sensorStop()` is `while(1){...}` that only exits on a `"sensorStop"` reply, and the UART branch of `getStatus()` is `while(len == 0){...}` with no timeout. Calling either with no sensor attached hangs `setup()` permanently and the board never reaches WiFi. This firmware therefore probes UART with a raw 1.5 s read on `Serial1` and only hands the port to the library once bytes actually arrive. **The I2C path is bounded and safe** — prefer I2C. **Never point the sensor UART at the console pins.** GPIO43/44 on the S3 and GPIO16/17 (D6/D7) on the C6 are the chips' hard-wired `U0TXD`/`U0RXD`. Attaching a second UART to them deadlocks the UART driver and trips the interrupt watchdog — the board boot-loops before reaching WiFi. **Floating I2C lines can stall the peripheral.** With nothing wired, SDA/SCL sit low and `Wire.endTransmission()` may never return. The firmware checks both lines read high with pull-ups before touching `Wire`. **`Serial.setTxTimeoutMs(0)` is a trap on USB CDC boards.** In `HWCDC::write`, `tries` is a `uint32_t` seeded from that value and the no-progress path does a bare `tries--`, so 0 underflows to 4.29 billion and spins for ~49 days. Use a small non-zero value. Only relevant when `Serial` is USB CDC (the C6 env). **DFRobot's wiring table is wrong.** It pairs the sensor's RX with the MCU's RX, and the library swaps the `rxpin`/`txpin` parameter names between the header and the `.cpp`. Correct is sensor TX → board RX. **Watch power on bus-powered boards.** A 24 GHz radar plus WiFi TX bursts on one USB port is marginal. Symptoms are not a crash: no panic, no backtrace, the USB device simply disconnects and re-enumerates. If that happens, add a 100–470 µF bulk capacitor across the sensor's VIN/GND and use a direct port rather than a hub. ## Using it The page shows a presence indicator, live target count / range / speed / energy, a rolling 60-second chart, and a calibration panel that changes with the active sensor mode. Header pills report board, link type, uptime, boot count, last reset reason, WiFi drops and free heap — enough to answer "did it crash?" without a serial cable. **Presence mode** (`eExitMode`) reports occupancy only. Parameters: min/max/trigger range, trigger and keep sensitivity (0–9), trigger delay (×0.01 s), keep timeout (×0.5 s). **Speed mode** (`eSpeedMode`) reports target count, distance, velocity and signal energy. Parameters: min/max range and a detection threshold (×0.1). Calibrate in speed mode — it shows the numbers behind the decision — then switch to presence mode for normal use. Settings apply immediately but are volatile; **Save to sensor flash** to persist them. ## Layout ``` platformio.ini board envs; default_envs selects one src/board_pins.h per-board pin map + LED abstraction src/main.cpp detection, polling, WiFi, HTTP API src/web_ui.h single-page UI, inlined into flash include/secrets.h.example WiFi template (real one is gitignored) ``` HTTP API: `GET /api/live`, `GET /api/config`, `POST /api/config`, `POST /api/action` (`start`/`stop`/`save`/`reset`/`factory`).