Desk presence detection using an HLK-LD2420 24GHz radar sensor and LaskaKit ESP32-LPKit.
Python 3 and PlatformIO:
python3 -m venv venv
source venv/bin/activate # fish: source venv/bin/activate.fish
pip install platformioOn macOS you also need the CH9102 USB driver for the LaskaKit
programmer. Install from
https://www.wch.cn/downloads/CH34XSER_MAC_ZIP.html if
/dev/cu.wchusbserial* doesn't appear when the board is plugged in.
The LD2420 module has 5 pins. Some modules don't expose a TX pin — OT1 acts as serial data output instead.
LD2420 sensor ESP32-LPKit
+-----------+ +-----------+
| VCC ------+------>---+ 3.3V |
| GND ------+------>---+ GND |
| OT1 ------+------>---+ GPIO17 | (sensor data out -> ESP32 receives)
| RX ------+----<-----+ GPIO16 | (ESP32 sends -> sensor data in)
| OT2 | | | (not connected)
+-----------+ +-----------+
pio run -t upload && pio device monitorThe LD2420 protocol driver lives in lib/LD2420/ and can be copied
into other PlatformIO projects. Usage:
#include <LD2420.h>
LD2420 radar(Serial2, 17, 16); // UART, RX pin, TX pin
void setup() {
radar.setMaxDistance(2.1); // 2.1m = gate 3
radar.setTimeout(10);
radar.setDebug(&Serial); // optional: verbose config output
radar.begin();
}
void loop() {
radar.update();
if (radar.isPresent()) {
// someone is at the desk
}
}For full gate threshold control, build an LD2420Config struct and
pass it via radar.setConfig(cfg) before begin().
- MCU: LaskaKit ESP32-LPKit (ESP32-WROOM) with CH9102 USB-C programmer
- Sensor: HLK-LD2420 24GHz radar presence sensor (3.3V, powered directly from ESP32)
- Firmware version: v1.6.1
- Serial port:
/dev/cu.wchusbserial*
- UART2 pins: RX=GPIO17, TX=GPIO16 (swapped from what pinout suggests — verified working)
- I2C power gate: GPIO4 must be HIGH to enable I2C (SDA=21, SCL=22) — not used in this project
- Programmer shows as
/dev/cu.wchusbserial*(CH9102 driver)
| Parameter | LD2410 | LD2420 |
|---|---|---|
| UART baud rate | 256000 | 115200 |
| Gates | 9 × 0.75m | 16 × 0.7m |
| Max range | 6m | 11.2m (practical ~8m) |
| Sensitivity format | 0-100 (higher=more) | Energy thresh (higher=less) |
| Config protocol ver | 0x0001 | 0x0002 |
All frames are little-endian.
Command frames:
FD FC FB FA | length(2) | command(2) | payload(N) | 04 03 02 01
- Length field = 2 (command bytes) + payload size
Energy data frames (output at ~10 Hz when in Energy mode):
F4 F3 F2 F1 | length(2) | presence(1) | distance(2) | gate_energy(2) × 16 | F8 F7 F6 F5
Simple mode output (factory default):
ASCII text: ON\r\n or OFF\r\n followed by Range XX\r\n
(distance in cm)
| Command | Code | Payload format |
|---|---|---|
| Enter config | 0x00FF | 02 00 (protocol version) |
| Exit config (NVM) | 0x00FE | (none) |
| Read firmware | 0x0000 | (none) |
| Write gate params | 0x0007 | [reg_addr(2) + value(4)] × N |
| Read gate params | 0x0008 | [reg_addr(2)] × N (see below) |
| Write system param | 0x0012 | param_id(2) + mode(2) + padding(2) |
| Restart | 0x0068 | (none, no reply) |
Read gate params response: [value(4)] × N starting at byte 10.
The sensor infers the register count from the frame length. Do NOT prepend a register count — the sensor interprets those bytes as a register address, silently writing to the wrong location.
Correct single register write (cmd 0x0007):
FD FC FB FA 08 00 07 00 [reg_addr 2B] [value 4B] 04 03 02 01
Wrong (what we had initially — caused all writes to be garbled):
FD FC FB FA 0A 00 07 00 01 00 [reg_addr 2B] [value 4B] 04 03 02 01
^^^^^ sensor reads this as reg 0x0001
| Register | Address | Description |
|---|---|---|
| Min gate | 0x0000 | Minimum detection gate |
| Max gate | 0x0001 | Maximum detection gate |
| Timeout | 0x0004 | No-presence timeout (seconds) |
| Move threshold gate N | 0x0010 + N | N = 0-15 |
| Still threshold gate N | 0x0020 + N | N = 0-15 |
Payload is always 6 bytes: param_id(2) + mode(2) + padding(2)
| Mode | Value | Output format |
|---|---|---|
| Simple | 0x0064 | ASCII ON/OFF + Range (factory default) |
| Energy | 0x0004 | Binary frames with per-gate energy |
| Debug | 0x0000 | Doppler + range raw data |
- Enter config mode (0x00FF with protocol version 0x0002)
- Write min_gate + max_gate + timeout in ONE 0x0007 frame (3 register pairs, 18 bytes payload)
- Loop gates 0-15: write move+still thresholds per gate (2 register pairs per 0x0007 frame, 12 bytes payload, 125μs delay between gates)
- Set system mode (0x0012)
- Exit config mode (0x00FE — saves to NVM)
Factory defaults:
- Gate 0: move=60000, still=40000
- Gate 1: move=30000, still=20000
- Gate 2: move=400, still=200
- Gates 3-6: move=250, still=200
- Gate 7+: move=250, still=100-150
Thresholds are energy values — higher = less sensitive. Set to 65535 to effectively disable a gate.
Tuning approach:
- Run in Energy mode with nobody present, note idle energy per gate
- Set thresholds ~50% above idle energy
- Walk through detection zone, verify presence triggers
- For through-wall blocking: set gates beyond the room to 65535 AND reduce MAX_GATE as a hard cutoff
Response command byte = sent command | 0x0100. Status at bytes 4-5: 0 = success.
- MIN_GATE=0, MAX_GATE=3 (0-2.1m, covers person sitting at desk)
- TIMEOUT=10s
- Gates 0-3: factory-default thresholds
- Gates 4-15: disabled (65535)
- Energy mode active — per-gate energy output for monitoring
- Through-wall detection to sleeping room (behind 15cm brick wall) confirmed blocked
pio run -t upload && pio device monitor#define RAW_DUMP— raw hex byte dump for debugging wiring/baud issues- Comment it out — full config + monitoring (Energy or Simple mode auto-detected)
- Always do a raw byte dump first — confirms wiring before touching protocol
- The LD2420 has no TX pin on some modules — OT1 serves as serial output
- Protocol has no num_regs field — this was the main bug; sensor infers count from length
- MAX_GATE is a hard cutoff — most effective way to prevent through-wall detection
- Config is saved to NVM on exit config — persists across power cycles
- ESPHome source is the authoritative protocol reference — manufacturer docs are incomplete
- Loose wires = silent failure — sensor just stops responding, solder connections for production