Add YD-ESP32-S3 support, boot diagnostics, and hang guards
Board is now selected by `default_envs` in platformio.ini rather than being hardcoded. Both boards stay supported and both targets build. - src/board_pins.h holds one block per board plus an ledSet() abstraction, so main.cpp has no board-specific code. The C6's active-low LED and the S3's WS2812 RGB go through the same call, and the C6 antenna-switch lines are behind #if HAS_RF_SWITCH. - YD-ESP32-S3 env targets esp32-s3-devkitc1-n16r8 (verified against the actual part: ESP32-S3 rev v0.2, 16MB quad flash, 8MB octal PSRAM, CH343 bridge). Serial is UART0 -> CH343, not USB CDC. Fixes for hangs that left the board alive on USB but dead on the network: - The vendor library's UART path has loops with no timeout: sensorStop() is while(1) that only exits on a "sensorStop" reply, and the UART branch of getStatus() is while(len == 0). Calling either with no sensor attached hung setup() permanently. UART is now probed with a raw 1.5s read on Serial1, and the library only gets the port once bytes actually arrive. The I2C path is bounded, so prefer I2C. - Floating SDA/SCL can stall Wire.endTransmission() forever. Check both lines read high with pull-ups before touching Wire. - Serial.setTxTimeoutMs(0) is a trap: HWCDC::write seeds a uint32_t from it and does a bare tries--, so 0 underflows to 4.29e9 and spins for ~49 days. Use 10, and only on USB CDC boards. Diagnostics, so "did it crash?" is answerable without a serial cable: - Boot count in RTC memory, esp_reset_reason(), free/min heap and WiFi drop count exposed via /api/live and shown as header pills in the UI. - WiFi auto-reconnect, and retry of station mode from the AP fallback. Without it a single dropout left the board unreachable forever, which looks exactly like a crash. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
b9162db707
commit
9c1c3fe1e8
5 files changed
+466
-188
No files matched your search
@@ -1,43 +1,117 @@
|
|||||||
# XIAO ESP32-C6 + DFRobot C4001 mmWave presence sensor
|
# DFRobot C4001 mmWave presence sensor on ESP32
|
||||||
|
|
||||||
Live-data and calibration rig for a C4001 24 GHz radar on a Seeed XIAO ESP32-C6.
|
Live-data and calibration rig for a C4001 24 GHz radar. The board hosts a web
|
||||||
The board hosts a web page showing presence, distance, velocity and signal
|
page showing presence, distance, velocity and signal energy in real time, with
|
||||||
energy in real time, with sliders for every sensor parameter.
|
sliders for every sensor parameter.
|
||||||
|
|
||||||
## Verified on this machine
|
Two boards are supported. Both are kept working; nothing is deleted when you
|
||||||
|
switch.
|
||||||
|
|
||||||
| Thing | Value |
|
## Choosing a board
|
||||||
|---|---|
|
|
||||||
| Chip | ESP32-C6FH4 (QFN32) rev v0.1, single core + LP core, 160 MHz |
|
|
||||||
| Flash | 4 MB embedded (mfr 0x20, dev 0x4016) |
|
|
||||||
| Radios | WiFi 6, BT 5 LE, IEEE 802.15.4 |
|
|
||||||
| MAC | `e4:b3:23:b4:7b:c4` |
|
|
||||||
| Port | `/dev/ttyACM0` (native USB Serial/JTAG, VID `303a` PID `1001`) |
|
|
||||||
| Toolchain | PlatformIO 6.1.19, espressif32 `2026.02.30`, Arduino core 3.3.7 |
|
|
||||||
| Build | succeeds, 57.6% flash / 11.9% RAM |
|
|
||||||
|
|
||||||
Flashing was confirmed working: esptool connected, uploaded its stub loader,
|
Change one line in `platformio.ini`:
|
||||||
ran it, and read the flash chip ID back.
|
|
||||||
|
|
||||||
> **Arduino core version matters.** ESP32-C6 needs Arduino-ESP32 **3.x**. The
|
```ini
|
||||||
> installed platform is the pioarduino fork (`2026.02.30`), which ships 3.3.7 and
|
[platformio]
|
||||||
> includes the `XIAO_ESP32C6` variant. The old registry `espressif32 @ 6.x` has no
|
default_envs = yd_esp32_s3 ; or: xiao_esp32c6
|
||||||
> C6 support — don't downgrade.
|
```
|
||||||
|
|
||||||
## Pin diagram
|
Or build one without editing the file:
|
||||||
|
|
||||||
The XIAO's pads are unlabelled on the top side. Hold the board **component side
|
```bash
|
||||||
up with the USB-C connector at the top** — pin 1 of each column is nearest the
|
pio run -e xiao_esp32c6 -t upload
|
||||||
USB connector.
|
```
|
||||||
|
|
||||||
|
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 ┌───┐
|
┌───┐ USB-C ┌───┐
|
||||||
┌─────┴───┴───────┴───┴─────┐
|
┌─────┴───┴───────┴───┴─────┐
|
||||||
D0 ────┤ o o ├──── VBUS (5 V from USB)
|
D0 ────┤ o o ├──── VBUS (5 V)
|
||||||
GPIO0 │ │
|
GPIO0 │ │
|
||||||
D1 ────┤ o o ├──── GND
|
D1 ────┤ o o ├──── GND
|
||||||
GPIO1 │ │
|
GPIO1 │ │
|
||||||
D2 ────┤ o seeed studio o ├──── 3V3 (3.3 V out)
|
D2 ────┤ o seeed studio o ├──── 3V3
|
||||||
GPIO2 │ XIAO-ESP32-C6 │
|
GPIO2 │ XIAO-ESP32-C6 │
|
||||||
D3 ────┤ o o ├──── D10 GPIO18
|
D3 ────┤ o o ├──── D10 GPIO18
|
||||||
GPIO21 │ │
|
GPIO21 │ │
|
||||||
@@ -49,72 +123,19 @@ USB connector.
|
|||||||
GPIO16 └───────────────────────────┘
|
GPIO16 └───────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
Full mapping, from Seeed's official pinout sheet cross-checked against the
|
| C4001 | XIAO pin |
|
||||||
Arduino variant header:
|
|
||||||
|
|
||||||
| Silkscreen | GPIO | Alt function |
|
|
||||||
|---|---|---|
|
|
||||||
| D0 | 0 | ADC1 / A0, LP_GPIO0 |
|
|
||||||
| D1 | 1 | ADC1 / A1, LP_GPIO1 |
|
|
||||||
| D2 | 2 | ADC1 / A2, LP_GPIO2 |
|
|
||||||
| D3 | 21 | — |
|
|
||||||
| D4 | 22 | **I2C SDA** |
|
|
||||||
| D5 | 23 | **I2C SCL** |
|
|
||||||
| D6 | 16 | **U0 TXD** |
|
|
||||||
| D7 | 17 | **U0 RXD** |
|
|
||||||
| D8 | 19 | SPI SCK |
|
|
||||||
| D9 | 20 | SPI MISO |
|
|
||||||
| D10 | 18 | SPI MOSI |
|
|
||||||
|
|
||||||
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, HIGH = external u.FL).
|
|
||||||
|
|
||||||
## 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 the C6's GPIOs are not 5 V tolerant. Running the
|
|
||||||
sensor at 3.3 V makes every logic level match and removes the question entirely.
|
|
||||||
|
|
||||||
The firmware auto-detects the interface at boot, so wire whichever your board's
|
|
||||||
mode switch is set to.
|
|
||||||
|
|
||||||
**UART (works on every C4001 SKU):**
|
|
||||||
|
|
||||||
| C4001 | XIAO | Note |
|
|
||||||
|---|---|---|
|
|
||||||
| VIN | 3V3 | |
|
|
||||||
| GND | GND | |
|
|
||||||
| TX | **D9** (GPIO20) | sensor transmit → board receive |
|
|
||||||
| RX | **D10** (GPIO18) | sensor receive ← board transmit |
|
|
||||||
| OUT | D2 (GPIO2) | optional |
|
|
||||||
|
|
||||||
**I2C (if your board has an I2C/UART switch set to I2C):**
|
|
||||||
|
|
||||||
| C4001 | XIAO |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| VIN | 3V3 |
|
| VIN | 3V3 |
|
||||||
| GND | GND |
|
| GND | GND |
|
||||||
| SCL | **D5** (GPIO23) |
|
| SCL | **D5** (GPIO23) |
|
||||||
| SDA | **D4** (GPIO22) |
|
| SDA | **D4** (GPIO22) |
|
||||||
|
| TX (UART) | **D9** (GPIO20) |
|
||||||
|
| RX (UART) | **D10** (GPIO18) |
|
||||||
| OUT | D2 (GPIO2), optional |
|
| OUT | D2 (GPIO2), optional |
|
||||||
|
|
||||||
`OUT` is the sensor's own thresholded presence output. It is not required — the
|
Not on the headers but used by the firmware: GPIO15 user LED (active LOW),
|
||||||
firmware reads everything over the data link — but wiring it lets you compare the
|
GPIO3 RF switch enable (LOW = on), GPIO14 antenna select (LOW = on-board
|
||||||
sensor's internal decision against the raw numbers, which is genuinely useful
|
ceramic). Without the RF switch lines, WiFi range is centimetres.
|
||||||
while calibrating.
|
|
||||||
|
|
||||||
> **Don't use D6/D7 for the sensor UART.** They are GPIO16/GPIO17, this chip's
|
|
||||||
> hard-wired `U0TXD`/`U0RXD`. UART0 already owns them, so pointing `Serial1` at
|
|
||||||
> those pins deadlocks the UART driver and trips the interrupt watchdog — the
|
|
||||||
> board boot-loops before it ever reaches WiFi. Found this the hard way; the
|
|
||||||
> UART pins above route UART1 through the GPIO matrix to pins with no default
|
|
||||||
> peripheral function.
|
|
||||||
|
|
||||||
> **Heads up on TX/RX.** DFRobot's own example table pairs the sensor's RX with
|
|
||||||
> the ESP32's RX, which is wrong. Their library also swaps the `rxpin`/`txpin`
|
|
||||||
> parameter names between the header and the `.cpp`. The table above is the
|
|
||||||
> electrically correct wiring: **sensor TX → board RX**, sensor RX → board TX.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -123,33 +144,59 @@ cp include/secrets.h.example include/secrets.h
|
|||||||
$EDITOR include/secrets.h # your WiFi; the file is gitignored
|
$EDITOR include/secrets.h # your WiFi; the file is gitignored
|
||||||
```
|
```
|
||||||
|
|
||||||
Skipping this is fine — the board then goes straight to AP mode.
|
Skipping this is fine — the board then goes straight to AP mode
|
||||||
|
(`xiao-mmwave` / `mmwave1234`).
|
||||||
## Build, flash, monitor
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pio run # build
|
pio run # build the default env
|
||||||
pio run -t upload # flash /dev/ttyACM0
|
pio run -t upload # flash
|
||||||
pio device monitor # 115200 baud
|
pio device monitor # 115200
|
||||||
```
|
```
|
||||||
|
|
||||||
If upload fails to auto-reset, hold **B** (boot), tap **R** (reset), release **B**,
|
Web UI at the IP printed on serial, or `http://mmwave.local/`.
|
||||||
then upload.
|
|
||||||
|
## 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
|
## Using it
|
||||||
|
|
||||||
On boot the firmware joins your WiFi; if that fails within 15 s it starts an
|
|
||||||
access point:
|
|
||||||
|
|
||||||
- **SSID** `xiao-mmwave` · **password** `mmwave1234`
|
|
||||||
- Web UI at the IP printed on serial, or `http://mmwave.local/`
|
|
||||||
|
|
||||||
The page shows a presence indicator, live target count / range / speed / energy,
|
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
|
a rolling 60-second chart, and a calibration panel that changes with the active
|
||||||
sensor mode. Serial output at boot tells you which interface was detected and
|
sensor mode. Header pills report board, link type, uptime, boot count, last
|
||||||
prints the full probe log if nothing was found.
|
reset reason, WiFi drops and free heap — enough to answer "did it crash?"
|
||||||
|
without a serial cable.
|
||||||
### The two sensor modes
|
|
||||||
|
|
||||||
**Presence mode** (`eExitMode`) reports occupancy only. Parameters: min/max/trigger
|
**Presence mode** (`eExitMode`) reports occupancy only. Parameters: min/max/trigger
|
||||||
range, trigger and keep sensitivity (0–9), trigger delay (×0.01 s), keep timeout
|
range, trigger and keep sensitivity (0–9), trigger delay (×0.01 s), keep timeout
|
||||||
@@ -158,18 +205,18 @@ range, trigger and keep sensitivity (0–9), trigger delay (×0.01 s), keep time
|
|||||||
**Speed mode** (`eSpeedMode`) reports target count, distance, velocity and signal
|
**Speed mode** (`eSpeedMode`) reports target count, distance, velocity and signal
|
||||||
energy. Parameters: min/max range and a detection threshold (×0.1).
|
energy. Parameters: min/max range and a detection threshold (×0.1).
|
||||||
|
|
||||||
Calibrate in speed mode, because it shows you the numbers behind the decision.
|
Calibrate in speed mode — it shows the numbers behind the decision — then switch
|
||||||
Then switch to presence mode for normal operation. The in-page guide walks
|
to presence mode for normal use. Settings apply immediately but are volatile;
|
||||||
through it. Settings apply immediately but are volatile — **Save to sensor flash**
|
**Save to sensor flash** to persist them.
|
||||||
to persist them.
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
platformio.ini board, framework, C4001 library from git
|
platformio.ini board envs; default_envs selects one
|
||||||
include/secrets.h.example template for WiFi creds (real one is gitignored)
|
src/board_pins.h per-board pin map + LED abstraction
|
||||||
src/main.cpp detection, polling, WiFi, HTTP API
|
src/main.cpp detection, polling, WiFi, HTTP API
|
||||||
src/web_ui.h the single-page UI, inlined into flash
|
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`,
|
HTTP API: `GET /api/live`, `GET /api/config`, `POST /api/config`,
|
||||||
|
|||||||
+41
-18
@@ -1,28 +1,51 @@
|
|||||||
; XIAO ESP32-C6 + DFRobot C4001 24GHz mmWave presence sensor
|
; XIAO ESP32-C6 / YD-ESP32-S3 + DFRobot C4001 24GHz mmWave presence sensor
|
||||||
;
|
;
|
||||||
; Board detected on this machine:
|
; ============================================================================
|
||||||
; ESP32-C6FH4 (QFN32) rev v0.1, 4MB embedded flash, USB-Serial/JTAG
|
; TO SWITCH BOARDS: change `default_envs` below to one of the [env:] names.
|
||||||
; MAC e4:b3:23:b4:7b:c4 on /dev/ttyACM0
|
; yd_esp32_s3 -> YD-ESP32-S3 (ESP32-S3-WROOM-1 N16R8), CH343 COM port
|
||||||
;
|
; xiao_esp32c6 -> Seeed XIAO ESP32-C6, native USB Serial/JTAG
|
||||||
; NOTE: ESP32-C6 needs Arduino-ESP32 core 3.x. The installed platform
|
; Pin maps for each live in src/board_pins.h.
|
||||||
; (espressif32 @ 2026.02.30, the pioarduino fork) ships core 3.3.7 and has
|
; Or build one explicitly without editing this file: pio run -e xiao_esp32c6
|
||||||
; the XIAO_ESP32C6 variant, so this builds as-is. The old espressif32 6.x
|
; ============================================================================
|
||||||
; from PlatformIO's registry does NOT support C6 -- don't downgrade.
|
|
||||||
|
|
||||||
[env:seeed_xiao_esp32c6]
|
[platformio]
|
||||||
platform = espressif32
|
default_envs = yd_esp32_s3
|
||||||
board = seeed_xiao_esp32c6
|
|
||||||
|
; ---------------------------------------------------------------- shared ----
|
||||||
|
[env]
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_filters = esp32_exception_decoder, time
|
monitor_filters = esp32_exception_decoder, time
|
||||||
upload_port = /dev/ttyACM0
|
|
||||||
monitor_port = /dev/ttyACM0
|
|
||||||
|
|
||||||
; Pulled straight from DFRobot's repo. The registry copy lags behind.
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
|
; Registry copy lags behind; take it straight from DFRobot.
|
||||||
https://github.com/DFRobot/DFRobot_C4001.git
|
https://github.com/DFRobot/DFRobot_C4001.git
|
||||||
|
|
||||||
|
; ----------------------------------------------------- YD-ESP32-S3 N16R8 ----
|
||||||
|
; Detected on this machine: ESP32-S3 (QFN56) rev v0.2, 16MB quad flash,
|
||||||
|
; 8MB octal PSRAM, MAC 24:58:7c:cd:9a:d8, CH343 bridge (1a86:55d3).
|
||||||
|
;
|
||||||
|
; The board has two USB-C sockets. This env assumes the one marked COM (the
|
||||||
|
; CH343 UART bridge). Serial is therefore real UART0, not USB CDC, which also
|
||||||
|
; sidesteps the CDC-blocking behaviour that bit us on the C6.
|
||||||
|
[env:yd_esp32_s3]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc1-n16r8
|
||||||
build_flags =
|
build_flags =
|
||||||
-DARDUINO_USB_CDC_ON_BOOT=1 ; Serial -> USB CDC, so the monitor works over the native port
|
-DBOARD_YD_ESP32_S3
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=0 ; Serial -> UART0 -> CH343 -> /dev/ttyACM0
|
||||||
|
-DCORE_DEBUG_LEVEL=1
|
||||||
|
|
||||||
|
; ------------------------------------------------------ Seeed XIAO ESP32-C6 --
|
||||||
|
; Detected earlier: ESP32-C6FH4 (QFN32) rev v0.1, 4MB flash,
|
||||||
|
; MAC e4:b3:23:b4:7b:c4, native USB Serial/JTAG (303a:1001).
|
||||||
|
;
|
||||||
|
; Needs Arduino-ESP32 core 3.x. The installed platform is the pioarduino fork
|
||||||
|
; (2026.02.30, core 3.3.7) which has the XIAO_ESP32C6 variant. The old registry
|
||||||
|
; espressif32 6.x has no C6 support -- don't downgrade.
|
||||||
|
[env:xiao_esp32c6]
|
||||||
|
platform = espressif32
|
||||||
|
board = seeed_xiao_esp32c6
|
||||||
|
build_flags =
|
||||||
|
-DBOARD_XIAO_C6
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=1 ; Serial -> native USB CDC
|
||||||
-DCORE_DEBUG_LEVEL=1
|
-DCORE_DEBUG_LEVEL=1
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Per-board pin map and LED abstraction.
|
||||||
|
//
|
||||||
|
// The board is selected in platformio.ini via `default_envs` -- see the big
|
||||||
|
// comment at the top of that file. Each env defines exactly one BOARD_* macro,
|
||||||
|
// and everything board-specific lives here so main.cpp stays generic.
|
||||||
|
//
|
||||||
|
// Adding a board: add an env in platformio.ini with a new -DBOARD_xxx flag,
|
||||||
|
// then add a matching block below.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#if defined(BOARD_YD_ESP32_S3)
|
||||||
|
|
||||||
|
#define BOARD_NAME "YD-ESP32-S3 (ESP32-S3-WROOM-1 N16R8)"
|
||||||
|
|
||||||
|
// P1 header. GPIO8/9 are the S3's documented Arduino I2C defaults.
|
||||||
|
static const uint8_t PIN_I2C_SDA = 8;
|
||||||
|
static const uint8_t PIN_I2C_SCL = 9;
|
||||||
|
// Adjacent on P1, and clear of every reserved pin below.
|
||||||
|
static const uint8_t PIN_UART_TX = 17; // -> C4001 RX
|
||||||
|
static const uint8_t PIN_UART_RX = 18; // <- C4001 TX
|
||||||
|
static const uint8_t PIN_SENSOR_OUT = 16; // <- C4001 OUT (optional)
|
||||||
|
|
||||||
|
// Reserved on this board, do not reuse:
|
||||||
|
// GPIO26-32 SPI flash GPIO35-37 octal PSRAM (the R8 in N16R8)
|
||||||
|
// GPIO43/44 UART0 -> CH343 GPIO19/20 native USB D-/D+
|
||||||
|
// GPIO0 boot button GPIO45/46 strapping
|
||||||
|
// GPIO48 WS2812 RGB LED
|
||||||
|
// The GPIO43/44 exclusion is the same trap that boot-looped the C6 on D6/D7:
|
||||||
|
// pointing a second UART at the console pins deadlocks the driver.
|
||||||
|
|
||||||
|
#define LED_IS_RGB 1
|
||||||
|
static const uint8_t PIN_LED = 48;
|
||||||
|
#define HAS_RF_SWITCH 0
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#elif defined(BOARD_XIAO_C6)
|
||||||
|
|
||||||
|
#define BOARD_NAME "Seeed XIAO ESP32-C6"
|
||||||
|
|
||||||
|
static const uint8_t PIN_I2C_SDA = D4; // GPIO22
|
||||||
|
static const uint8_t PIN_I2C_SCL = D5; // GPIO23
|
||||||
|
// NOT D6/D7 -- those are GPIO16/17, the C6's hard-wired U0TXD/U0RXD. Serial1
|
||||||
|
// on them deadlocks the UART driver and boot-loops the board.
|
||||||
|
static const uint8_t PIN_UART_TX = D10; // GPIO18 -> C4001 RX
|
||||||
|
static const uint8_t PIN_UART_RX = D9; // GPIO20 <- C4001 TX
|
||||||
|
static const uint8_t PIN_SENSOR_OUT = D2; // GPIO2 <- C4001 OUT (optional)
|
||||||
|
|
||||||
|
#define LED_IS_RGB 0
|
||||||
|
static const uint8_t PIN_LED = LED_BUILTIN; // GPIO15, active LOW
|
||||||
|
#define LED_ACTIVE_LOW 1
|
||||||
|
|
||||||
|
// The XIAO routes its antenna through an RF switch that must be enabled in
|
||||||
|
// software or WiFi range is centimetres.
|
||||||
|
#define HAS_RF_SWITCH 1
|
||||||
|
static const uint8_t PIN_RF_ENABLE = 3; // LOW = enable switch
|
||||||
|
static const uint8_t PIN_RF_ANT_SEL = 14; // LOW = on-board ceramic, HIGH = u.FL
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
#else
|
||||||
|
#error "No board selected. Set default_envs in platformio.ini to one of the [env:...] names."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- LED helper ---
|
||||||
|
|
||||||
|
static inline void ledInit() {
|
||||||
|
#if !LED_IS_RGB
|
||||||
|
pinMode(PIN_LED, OUTPUT);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// One call site in main.cpp regardless of whether the board has a plain LED
|
||||||
|
// or an addressable RGB one.
|
||||||
|
static inline void ledSet(bool on) {
|
||||||
|
#if LED_IS_RGB
|
||||||
|
rgbLedWrite(PIN_LED, 0, on ? 40 : 0, 0); // dim green = presence
|
||||||
|
#elif LED_ACTIVE_LOW
|
||||||
|
digitalWrite(PIN_LED, on ? LOW : HIGH);
|
||||||
|
#else
|
||||||
|
digitalWrite(PIN_LED, on ? HIGH : LOW);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
+169
-57
@@ -1,23 +1,22 @@
|
|||||||
// XIAO ESP32-C6 + DFRobot C4001 24GHz mmWave presence sensor
|
// DFRobot C4001 24GHz mmWave presence sensor on an ESP32 dev board.
|
||||||
//
|
//
|
||||||
// Serves a live-data + calibration web UI. Auto-detects whether the C4001
|
// Serves a live-data + calibration web UI. Auto-detects whether the C4001 is
|
||||||
// is wired for I2C or UART so you don't have to care which way the board's
|
// wired for I2C or UART so you don't have to care which way the board's mode
|
||||||
// mode switch is set.
|
// switch is set.
|
||||||
//
|
//
|
||||||
// Wiring (see README.md for the physical pin diagram):
|
// Board is chosen in platformio.ini (default_envs); pin maps live in
|
||||||
// C4001 VIN -> XIAO 3V3 (3V3, not 5V -- keeps sensor logic at 3.3V
|
// src/board_pins.h. Power the sensor from 3V3 rather than 5V so its OUT pin
|
||||||
// so OUT can drive a C6 GPIO directly)
|
// can drive a GPIO directly -- these parts are not 5V tolerant.
|
||||||
// C4001 GND -> XIAO GND
|
// See README.md for the physical pin diagram for each board.
|
||||||
// I2C mode: C4001 SCL -> D5 (GPIO23) C4001 SDA -> D4 (GPIO22)
|
|
||||||
// UART mode: C4001 TX -> D9 (GPIO20) C4001 RX -> D10 (GPIO18)
|
|
||||||
// C4001 OUT -> D2 (GPIO2) (optional, the sensor's own thresholded output)
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <WebServer.h>
|
#include <WebServer.h>
|
||||||
#include <ESPmDNS.h>
|
#include <ESPmDNS.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
|
#include <esp_system.h>
|
||||||
#include "DFRobot_C4001.h"
|
#include "DFRobot_C4001.h"
|
||||||
|
#include "board_pins.h"
|
||||||
#include "web_ui.h"
|
#include "web_ui.h"
|
||||||
|
|
||||||
#if __has_include("secrets.h")
|
#if __has_include("secrets.h")
|
||||||
@@ -28,29 +27,8 @@
|
|||||||
#define WIFI_PASS ""
|
#define WIFI_PASS ""
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ---------------------------------------------------------------- pins ----
|
// Pin map and the LED helper come from board_pins.h, selected by the
|
||||||
|
// -DBOARD_* flag that platformio.ini's active env sets.
|
||||||
static const uint8_t PIN_SENSOR_OUT = D2; // GPIO2 <- C4001 OUT
|
|
||||||
static const uint8_t PIN_I2C_SDA = D4; // GPIO22
|
|
||||||
static const uint8_t PIN_I2C_SCL = D5; // GPIO23
|
|
||||||
// NOT D6/D7. Those are GPIO16/17, which are this chip's hard-wired
|
|
||||||
// U0TXD/U0RXD (see soc/uart_pins.h). UART0 already owns them, so pointing
|
|
||||||
// Serial1 at them deadlocks the UART driver and trips the interrupt
|
|
||||||
// watchdog -> boot loop. UART1 has no fixed pins on C6, so route it through
|
|
||||||
// the GPIO matrix to pins with no default peripheral instead.
|
|
||||||
static const uint8_t PIN_UART_TX = D10; // GPIO18 -> C4001 RX
|
|
||||||
static const uint8_t PIN_UART_RX = D9; // GPIO20 <- C4001 TX
|
|
||||||
|
|
||||||
// XIAO ESP32-C6 RF switch. Without these the on-board antenna is not
|
|
||||||
// connected and WiFi range is a few centimetres. Confirmed against Seeed's
|
|
||||||
// official pinout: GPIO3 Low=Enable, GPIO14 Low=internal / High=external.
|
|
||||||
static const uint8_t PIN_RF_ENABLE = 3;
|
|
||||||
static const uint8_t PIN_RF_ANT_SEL = 14;
|
|
||||||
|
|
||||||
// User LED is GPIO15 and is active LOW on this board.
|
|
||||||
static const uint8_t PIN_LED = LED_BUILTIN;
|
|
||||||
#define LED_ON LOW
|
|
||||||
#define LED_OFF HIGH
|
|
||||||
|
|
||||||
static const char *AP_SSID = "xiao-mmwave";
|
static const char *AP_SSID = "xiao-mmwave";
|
||||||
static const char *AP_PASS = "mmwave1234"; // WPA2 needs >= 8 chars
|
static const char *AP_PASS = "mmwave1234"; // WPA2 needs >= 8 chars
|
||||||
@@ -83,6 +61,30 @@ struct {
|
|||||||
uint32_t stamp = 0;
|
uint32_t stamp = 0;
|
||||||
} live;
|
} live;
|
||||||
|
|
||||||
|
// Survives a reboot (kept in RTC memory), so we can tell "this board has
|
||||||
|
// restarted 5 times" from "this board has been up since you flashed it".
|
||||||
|
RTC_DATA_ATTR static uint32_t bootCount = 0;
|
||||||
|
|
||||||
|
static const char *bootReason = "?";
|
||||||
|
static bool staMode = false; // true = joined your WiFi, false = own AP
|
||||||
|
static uint32_t wifiDrops = 0; // times the STA link has dropped since boot
|
||||||
|
static uint32_t lastWifiChk = 0;
|
||||||
|
|
||||||
|
static const char *resetReasonName(esp_reset_reason_t r) {
|
||||||
|
switch (r) {
|
||||||
|
case ESP_RST_POWERON: return "power-on";
|
||||||
|
case ESP_RST_EXT: return "external pin";
|
||||||
|
case ESP_RST_SW: return "software restart";
|
||||||
|
case ESP_RST_PANIC: return "PANIC (crash)";
|
||||||
|
case ESP_RST_INT_WDT: return "interrupt watchdog";
|
||||||
|
case ESP_RST_TASK_WDT: return "task watchdog";
|
||||||
|
case ESP_RST_WDT: return "watchdog";
|
||||||
|
case ESP_RST_BROWNOUT: return "BROWNOUT (supply dipped)";
|
||||||
|
case ESP_RST_DEEPSLEEP: return "deep-sleep wake";
|
||||||
|
default: return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static uint32_t lastPoll = 0;
|
static uint32_t lastPoll = 0;
|
||||||
// Set once the interface is known. I2C is fast; UART at 9600 baud costs ~60 ms
|
// Set once the interface is known. I2C is fast; UART at 9600 baud costs ~60 ms
|
||||||
// per register round-trip, and speed mode reads four of them, so polling it
|
// per register round-trip, and speed mode reads four of them, so polling it
|
||||||
@@ -97,24 +99,61 @@ static bool i2cProbe(uint8_t addr) {
|
|||||||
return Wire.endTransmission() == 0;
|
return Wire.endTransmission() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The C4001 UART begin() returns true unconditionally, so it proves nothing.
|
// With nothing wired (or a stuck slave holding the bus) SDA/SCL sit low, and
|
||||||
// Confirm the link by actually reading status back a few times.
|
// the I2C peripheral can block forever waiting for an idle bus -- that hangs
|
||||||
static bool uartResponds(DFRobot_C4001_UART *r) {
|
// setup() before WiFi ever starts. Check the lines are actually high with
|
||||||
for (int i = 0; i < 5; i++) {
|
// pull-ups on before handing them to Wire.
|
||||||
sSensorStatus_t st = r->getStatus();
|
static bool i2cBusIdle() {
|
||||||
if (st.initStatus == 1) return true;
|
pinMode(PIN_I2C_SDA, INPUT_PULLUP);
|
||||||
// A live-but-still-initialising sensor reports a sane work mode.
|
pinMode(PIN_I2C_SCL, INPUT_PULLUP);
|
||||||
if (st.workMode <= 1 && r->getMinRange() > 0) return true;
|
delayMicroseconds(200);
|
||||||
delay(200);
|
bool sda = digitalRead(PIN_I2C_SDA);
|
||||||
|
bool scl = digitalRead(PIN_I2C_SCL);
|
||||||
|
if (!sda || !scl) {
|
||||||
|
sensor.detectLog += "I2C: bus not idle (sda=" + String(sda) +
|
||||||
|
" scl=" + String(scl) + "), skipping I2C\n";
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw, time-bounded liveness check -- deliberately does NOT touch
|
||||||
|
// DFRobot_C4001.
|
||||||
|
//
|
||||||
|
// The vendor library's UART path has two loops with no timeout:
|
||||||
|
// sensorStop() -> while(1){...} exits only on a "sensorStop" reply
|
||||||
|
// getStatus() -> while(len == 0){...} spins forever if nothing arrives
|
||||||
|
// so calling any of it with no sensor attached hangs setup() permanently and
|
||||||
|
// the board never reaches WiFi. Prove there is something on the wire first;
|
||||||
|
// only then hand the port to the library.
|
||||||
|
//
|
||||||
|
// The I2C path is fine -- its readReg is bounded -- which is another reason to
|
||||||
|
// prefer I2C with this sensor.
|
||||||
|
static bool uartSensorPresent() {
|
||||||
|
Serial1.begin(9600, SERIAL_8N1, PIN_UART_RX, PIN_UART_TX);
|
||||||
|
delay(50);
|
||||||
|
while (Serial1.available()) Serial1.read(); // flush boot noise
|
||||||
|
|
||||||
|
// A running C4001 streams frames continuously; nudge it in case it's stopped.
|
||||||
|
Serial1.print("sensorStart\r\n");
|
||||||
|
|
||||||
|
uint32_t t0 = millis();
|
||||||
|
while (millis() - t0 < 1500) {
|
||||||
|
if (Serial1.available() > 0) return true;
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
Serial1.end();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void detectSensor() {
|
static void detectSensor() {
|
||||||
sensor.detectLog = "";
|
sensor.detectLog = "";
|
||||||
|
|
||||||
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
|
|
||||||
const uint8_t addrs[] = {DEVICE_ADDR_0, DEVICE_ADDR_1};
|
const uint8_t addrs[] = {DEVICE_ADDR_0, DEVICE_ADDR_1};
|
||||||
|
if (i2cBusIdle()) {
|
||||||
|
Serial.println(" probing I2C ...");
|
||||||
|
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
|
||||||
|
Wire.setTimeOut(100); // a stuck bus must not block forever
|
||||||
for (uint8_t a : addrs) {
|
for (uint8_t a : addrs) {
|
||||||
if (i2cProbe(a)) {
|
if (i2cProbe(a)) {
|
||||||
radarI2C = new DFRobot_C4001_I2C(&Wire, a);
|
radarI2C = new DFRobot_C4001_I2C(&Wire, a);
|
||||||
@@ -132,20 +171,21 @@ static void detectSensor() {
|
|||||||
sensor.detectLog += "I2C: no ACK at 0x" + String(a, HEX) + "\n";
|
sensor.detectLog += "I2C: no ACK at 0x" + String(a, HEX) + "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // end if (i2cBusIdle())
|
||||||
|
|
||||||
radarUART = new DFRobot_C4001_UART(&Serial1, 9600, PIN_UART_RX, PIN_UART_TX);
|
Serial.println(" probing UART ...");
|
||||||
radarUART->begin();
|
if (uartSensorPresent()) {
|
||||||
if (uartResponds(radarUART)) {
|
radarUART = new DFRobot_C4001_UART(&Serial1, 9600, PIN_UART_RX, PIN_UART_TX);
|
||||||
|
radarUART->begin();
|
||||||
radar = radarUART;
|
radar = radarUART;
|
||||||
sensor.present = true;
|
sensor.present = true;
|
||||||
sensor.iface = "UART";
|
sensor.iface = "UART";
|
||||||
sensor.detectLog += "UART: sensor responded on D9(rx)/D10(tx)\n";
|
sensor.detectLog += "UART: sensor responded on rx=GPIO" + String(PIN_UART_RX) +
|
||||||
|
" tx=GPIO" + String(PIN_UART_TX) + "\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sensor.detectLog += "UART: no response on D9(rx)/D10(tx) @ 9600\n";
|
sensor.detectLog += "UART: no response on rx=GPIO" + String(PIN_UART_RX) +
|
||||||
|
" tx=GPIO" + String(PIN_UART_TX) + " @ 9600\n";
|
||||||
delete radarUART;
|
|
||||||
radarUART = nullptr;
|
|
||||||
sensor.present = false;
|
sensor.present = false;
|
||||||
sensor.iface = "none";
|
sensor.iface = "none";
|
||||||
}
|
}
|
||||||
@@ -179,7 +219,7 @@ static void pollSensor() {
|
|||||||
live.energy = 0;
|
live.energy = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
digitalWrite(PIN_LED, live.presence ? LED_ON : LED_OFF);
|
ledSet(live.presence);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------- http handlers --
|
// --------------------------------------------------------- http handlers --
|
||||||
@@ -196,7 +236,8 @@ static void handleRoot() {
|
|||||||
|
|
||||||
static void handleLive() {
|
static void handleLive() {
|
||||||
String j = "{";
|
String j = "{";
|
||||||
j += "\"present\":" + String(sensor.present ? "true" : "false");
|
j += "\"board\":\"" + String(BOARD_NAME) + "\"";
|
||||||
|
j += ",\"present\":" + String(sensor.present ? "true" : "false");
|
||||||
j += ",\"iface\":\"" + sensor.iface + "\"";
|
j += ",\"iface\":\"" + sensor.iface + "\"";
|
||||||
j += ",\"mode\":" + String(sensor.mode);
|
j += ",\"mode\":" + String(sensor.mode);
|
||||||
j += ",\"presence\":" + String(live.presence ? "true" : "false");
|
j += ",\"presence\":" + String(live.presence ? "true" : "false");
|
||||||
@@ -209,6 +250,13 @@ static void handleLive() {
|
|||||||
j += ",\"initStatus\":" + String(live.initStatus);
|
j += ",\"initStatus\":" + String(live.initStatus);
|
||||||
j += ",\"uptime\":" + String(millis() / 1000);
|
j += ",\"uptime\":" + String(millis() / 1000);
|
||||||
j += ",\"rssi\":" + String(WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : 0);
|
j += ",\"rssi\":" + String(WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : 0);
|
||||||
|
j += ",\"wifiMode\":\"" + String(staMode ? "sta" : "ap") + "\"";
|
||||||
|
j += ",\"ip\":\"" + (staMode ? WiFi.localIP().toString() : WiFi.softAPIP().toString()) + "\"";
|
||||||
|
j += ",\"wifiDrops\":" + String(wifiDrops);
|
||||||
|
j += ",\"boots\":" + String(bootCount);
|
||||||
|
j += ",\"bootReason\":\"" + String(bootReason) + "\"";
|
||||||
|
j += ",\"heap\":" + String(ESP.getFreeHeap());
|
||||||
|
j += ",\"minHeap\":" + String(ESP.getMinFreeHeap());
|
||||||
j += ",\"detectLog\":\"";
|
j += ",\"detectLog\":\"";
|
||||||
for (size_t i = 0; i < sensor.detectLog.length(); i++) {
|
for (size_t i = 0; i < sensor.detectLog.length(); i++) {
|
||||||
char c = sensor.detectLog[i];
|
char c = sensor.detectLog[i];
|
||||||
@@ -306,16 +354,20 @@ static void handleAction() {
|
|||||||
// ----------------------------------------------------------------- wifi ---
|
// ----------------------------------------------------------------- wifi ---
|
||||||
|
|
||||||
static void startWiFi() {
|
static void startWiFi() {
|
||||||
|
#if HAS_RF_SWITCH
|
||||||
// Bring the RF switch up before any radio work.
|
// Bring the RF switch up before any radio work.
|
||||||
pinMode(PIN_RF_ENABLE, OUTPUT);
|
pinMode(PIN_RF_ENABLE, OUTPUT);
|
||||||
digitalWrite(PIN_RF_ENABLE, LOW); // enable RF switch
|
digitalWrite(PIN_RF_ENABLE, LOW); // enable RF switch
|
||||||
pinMode(PIN_RF_ANT_SEL, OUTPUT);
|
pinMode(PIN_RF_ANT_SEL, OUTPUT);
|
||||||
digitalWrite(PIN_RF_ANT_SEL, LOW); // LOW = on-board ceramic antenna
|
digitalWrite(PIN_RF_ANT_SEL, LOW); // LOW = on-board ceramic antenna
|
||||||
|
#endif
|
||||||
|
|
||||||
const char *ssid = WIFI_SSID;
|
const char *ssid = WIFI_SSID;
|
||||||
if (strlen(ssid) > 0) {
|
if (strlen(ssid) > 0) {
|
||||||
Serial.printf("WiFi: joining \"%s\" ...\n", ssid);
|
Serial.printf("WiFi: joining \"%s\" ...\n", ssid);
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.setAutoReconnect(true);
|
||||||
|
WiFi.persistent(false);
|
||||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||||
uint32_t t0 = millis();
|
uint32_t t0 = millis();
|
||||||
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) {
|
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) {
|
||||||
@@ -324,6 +376,7 @@ static void startWiFi() {
|
|||||||
}
|
}
|
||||||
Serial.println();
|
Serial.println();
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
staMode = true;
|
||||||
Serial.printf("WiFi: connected, IP http://%s/\n", WiFi.localIP().toString().c_str());
|
Serial.printf("WiFi: connected, IP http://%s/\n", WiFi.localIP().toString().c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -332,23 +385,81 @@ static void startWiFi() {
|
|||||||
Serial.println("WiFi: no secrets.h credentials, starting AP");
|
Serial.println("WiFi: no secrets.h credentials, starting AP");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
staMode = false;
|
||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
WiFi.softAP(AP_SSID, AP_PASS);
|
WiFi.softAP(AP_SSID, AP_PASS);
|
||||||
Serial.printf("WiFi: AP \"%s\" pass \"%s\" http://%s/\n",
|
Serial.printf("WiFi: AP \"%s\" pass \"%s\" http://%s/\n",
|
||||||
AP_SSID, AP_PASS, WiFi.softAPIP().toString().c_str());
|
AP_SSID, AP_PASS, WiFi.softAPIP().toString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Without this the board stays alive but permanently off-network after a
|
||||||
|
// single dropout -- indistinguishable from a crash, and the reason a
|
||||||
|
// "did it crash?" question had no answer.
|
||||||
|
static void maintainWiFi() {
|
||||||
|
if (millis() - lastWifiChk < 20000) return;
|
||||||
|
lastWifiChk = millis();
|
||||||
|
|
||||||
|
if (!staMode) {
|
||||||
|
// Fell back to AP at boot. Retry the real network rather than sitting on
|
||||||
|
// an island forever; the AP stays up in the meantime.
|
||||||
|
if (strlen(WIFI_SSID) == 0) return;
|
||||||
|
Serial.println("WiFi: retrying station join from AP fallback");
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||||
|
uint32_t t0 = millis();
|
||||||
|
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 8000) delay(100);
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
staMode = true;
|
||||||
|
Serial.printf("WiFi: recovered, IP http://%s/\n", WiFi.localIP().toString().c_str());
|
||||||
|
MDNS.begin(MDNS_HOST);
|
||||||
|
} else {
|
||||||
|
WiFi.mode(WIFI_AP);
|
||||||
|
WiFi.softAP(AP_SSID, AP_PASS);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
wifiDrops++;
|
||||||
|
Serial.printf("WiFi: link down (drop #%u), reconnecting\n", wifiDrops);
|
||||||
|
WiFi.disconnect();
|
||||||
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- setup ---
|
// ---------------------------------------------------------------- setup ---
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
|
// Critical on a headless board. Serial here is USB CDC. Its default 100 ms
|
||||||
|
// write timeout re-arms every time the buffer drains a little, so with the
|
||||||
|
// port enumerated but nobody reading (USB in a PC, no monitor open) boot
|
||||||
|
// logging can stall setup() before WiFi ever starts -- the board looks dead
|
||||||
|
// on the network while still enumerating over USB.
|
||||||
|
//
|
||||||
|
// Do NOT set this to 0. In HWCDC::write the no-progress path does a bare
|
||||||
|
// `tries--` on a uint32_t seeded from this value, so 0 underflows to
|
||||||
|
// 0xFFFFFFFF and spins for ~49 days instead of giving up. A small non-zero
|
||||||
|
// value is the actual "don't block" setting.
|
||||||
|
//
|
||||||
|
// Only applies when Serial is USB CDC. On a board wired through a UART
|
||||||
|
// bridge (YD-ESP32-S3 via CH343) Serial is a plain HardwareSerial, which
|
||||||
|
// has no such method and cannot stall this way.
|
||||||
|
#if ARDUINO_USB_CDC_ON_BOOT
|
||||||
|
Serial.setTxTimeoutMs(10);
|
||||||
|
#endif
|
||||||
uint32_t t0 = millis();
|
uint32_t t0 = millis();
|
||||||
while (!Serial && millis() - t0 < 2000) delay(10); // don't block if headless
|
while (!Serial && millis() - t0 < 2000) delay(10); // don't block if headless
|
||||||
|
|
||||||
Serial.println("\n=== XIAO ESP32-C6 + C4001 mmWave ===");
|
bootCount++;
|
||||||
|
bootReason = resetReasonName(esp_reset_reason());
|
||||||
|
Serial.println("\n=== " BOARD_NAME " + C4001 mmWave ===");
|
||||||
|
Serial.printf("Boot #%u, last reset: %s\n", bootCount, bootReason);
|
||||||
|
Serial.printf("I2C sda=%u scl=%u | UART rx=%u tx=%u | OUT=%u\n",
|
||||||
|
PIN_I2C_SDA, PIN_I2C_SCL, PIN_UART_RX, PIN_UART_TX, PIN_SENSOR_OUT);
|
||||||
|
|
||||||
pinMode(PIN_LED, OUTPUT);
|
ledInit();
|
||||||
digitalWrite(PIN_LED, LED_OFF);
|
ledSet(false);
|
||||||
pinMode(PIN_SENSOR_OUT, INPUT);
|
pinMode(PIN_SENSOR_OUT, INPUT);
|
||||||
|
|
||||||
Serial.println("Detecting C4001 ...");
|
Serial.println("Detecting C4001 ...");
|
||||||
@@ -386,6 +497,7 @@ void setup() {
|
|||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
server.handleClient();
|
server.handleClient();
|
||||||
|
maintainWiFi();
|
||||||
|
|
||||||
if (millis() - lastPoll >= POLL_MS) {
|
if (millis() - lastPoll >= POLL_MS) {
|
||||||
lastPoll = millis();
|
lastPoll = millis();
|
||||||
|
|||||||
+15
-3
@@ -73,12 +73,17 @@ summary{cursor:pointer;color:var(--accent);font-size:13px}
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>XIAO ESP32-C6 · C4001 mmWave</h1>
|
<h1>C4001 mmWave</h1>
|
||||||
|
<span class="pill">board <b id="p-board">-</b></span>
|
||||||
<span class="pill">link <b id="p-iface">-</b></span>
|
<span class="pill">link <b id="p-iface">-</b></span>
|
||||||
<span class="pill">mode <b id="p-mode">-</b></span>
|
<span class="pill">mode <b id="p-mode">-</b></span>
|
||||||
<span class="pill">init <b id="p-init">-</b></span>
|
<span class="pill">init <b id="p-init">-</b></span>
|
||||||
<span class="pill">rssi <b id="p-rssi">-</b></span>
|
<span class="pill">rssi <b id="p-rssi">-</b></span>
|
||||||
<span class="pill">up <b id="p-up">-</b></span>
|
<span class="pill">up <b id="p-up">-</b></span>
|
||||||
|
<span class="pill">boots <b id="p-boots">-</b></span>
|
||||||
|
<span class="pill">last reset <b id="p-reason">-</b></span>
|
||||||
|
<span class="pill">wifi drops <b id="p-drops">-</b></span>
|
||||||
|
<span class="pill">heap <b id="p-heap">-</b></span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -263,11 +268,18 @@ async function poll(){
|
|||||||
try { d = await (await fetch('/api/live')).json(); }
|
try { d = await (await fetch('/api/live')).json(); }
|
||||||
catch(e){ return; }
|
catch(e){ return; }
|
||||||
|
|
||||||
|
$('p-board').textContent = d.board;
|
||||||
$('p-iface').textContent = d.iface;
|
$('p-iface').textContent = d.iface;
|
||||||
$('p-mode').textContent = d.mode ? 'speed' : 'presence';
|
$('p-mode').textContent = d.mode ? 'speed' : 'presence';
|
||||||
$('p-init').textContent = d.initStatus ? 'ok' : 'not ready';
|
$('p-init').textContent = d.initStatus ? 'ok' : 'not ready';
|
||||||
$('p-rssi').textContent = d.rssi ? d.rssi+' dBm' : 'AP';
|
$('p-rssi').textContent = d.wifiMode === 'sta' ? d.rssi+' dBm' : 'AP mode';
|
||||||
$('p-up').textContent = d.uptime+' s';
|
$('p-up').textContent = d.uptime > 3600
|
||||||
|
? (d.uptime/3600).toFixed(1)+' h'
|
||||||
|
: (d.uptime > 90 ? Math.round(d.uptime/60)+' min' : d.uptime+' s');
|
||||||
|
$('p-boots').textContent = d.boots;
|
||||||
|
$('p-reason').textContent= d.bootReason;
|
||||||
|
$('p-drops').textContent = d.wifiDrops;
|
||||||
|
$('p-heap').textContent = Math.round(d.heap/1024)+' KB';
|
||||||
|
|
||||||
$('nosensor').classList.toggle('hide', d.present);
|
$('nosensor').classList.toggle('hide', d.present);
|
||||||
if (!d.present) $('detectlog').textContent = d.detectLog;
|
if (!d.present) $('detectlog').textContent = d.detectLog;
|
||||||
|
|||||||
Reference in new issue
Block a user