Standalone ESP32/PlatformIO library for the HLK-LD2410S 24GHz ultra-low-power mmWave human presence sensor.
IMPORTANT: The LD2410S has a different protocol than the LD2410, LD2410B, and LD2410C. This library implements the V1.00 protocol (FW 1.1.x), verified against a serial sniff of the official HLK-LD2410S_TOOL v1.3.0.1.
| Sensor Pin | Name | Function | Voltage |
|---|---|---|---|
| 1 | 3V3 | Power input | 3.0-3.6V |
| 2 | GND | Ground | -- |
| 3 | OT1 | UART TX (sensor output) | 0-3.3V |
| 4 | RX | UART RX (sensor input) | 0-3.3V |
| 5 | OT2 | GPIO presence (HIGH=presence) | 0-3.3V |
- UART: 115200 baud, 8N1 (NOT 256000 like LD2410B/C)
- Power: 3.3V only (NOT 5V tolerant)
- Current: 45 uA (0.5 Hz) to 446 uA (8 Hz) average, 118 mA peak
- Streaming parser handles minimal (5-byte) and standard (per-gate energy) report frames
- Full command set: firmware version, serial number, generic params, thresholds (all 16 gates)
- Optional auto-calibration and output mode switching
- FreeRTOS mutex for multi-task safety
- Runtime-toggleable debug hex dump output
- Robust config command handling with retry and stale-state clearing
- Optional power control via external load switch or FET (
LD2410S_Powerclass)
#include "LD2410S.h"
LD2410S sensor;
void setup() {
sensor.begin(Serial2, 16, 15); // RX pin, TX pin
// Sensor must be activated with writeGenericParams + endConfig to start streaming.
// See test program for the full activation sequence.
}
void loop() {
sensor.update();
if (sensor.presenceDetected()) {
Serial.printf("Detected at %u cm\n", sensor.distance_cm());
}
}Two official protocol documents exist with incompatible threshold command layouts:
| Document | FW Version | Threshold Commands |
|---|---|---|
| Serial Communication Protocol V1.00 (2024-08-23) | FW 1.1.x | 0x72/0x73 = trigger (all 16 gates), 0x76/0x77 = hold (all 16 gates) |
| User Manual V1.2 (2023-10-31) | Older FW | 0x72/0x73 = gates 0-7 (trigger+hold), 0x74/0x75 = gates 8-15 SNR (trigger+hold) |
This library implements the V1.00 layout (FW 1.1.x), verified by sniffing the official HLK tool communicating with a FW 1.1.1 module. Key differences from V1.2:
- EnableConfig sends protocol version
0x0002(not0x0001) - Trigger thresholds (cmd 0x72/0x73) cover all 16 gates in one command
- Hold thresholds (cmd 0x76/0x77) cover all 16 gates in a separate command
- No 0x74/0x75 commands — SNR gates (8-15) are included in the same commands as absolute gates (0-7)
- Firmware version ACK layout: equipment(2) + reserved(2) + major(2) + minor(2) + patch(2)
| Method | Description |
|---|---|
bool begin(HardwareSerial &serial, int rxPin, int txPin, unsigned long baud = 115200) |
Initialize the sensor UART connection. rxPin is the ESP32 pin connected to the sensor's OT1/TX output. txPin is the ESP32 pin connected to the sensor's RX input. Creates a FreeRTOS mutex for thread-safe command access. Returns true on success. |
void end() |
Reset all library state (parser, sensor data, ACK buffers). Call before power cycling the sensor. Does not deinitialize the UART — use LD2410S_Power::powerOff() or Serial1.end() for that. |
void update() |
Parse incoming UART data and update sensor state. Call frequently from loop() or a FreeRTOS task. Non-blocking — processes all currently available bytes. |
These return the most recent values parsed by update(). All are const and thread-safe to read.
| Method | Description |
|---|---|
bool presenceDetected() |
Returns true if a moving or stationary target is detected. |
uint8_t targetState() |
Raw target state byte: 0x00/0x01 = no target, 0x02 = moving, 0x03 = stationary. See LD2410S_TargetState enum. |
uint16_t distance_cm() |
Distance to the detected target in centimeters. Returns 0 when no target. |
uint32_t gateEnergy(uint8_t gate) |
Energy value for a specific gate (0-15). Only populated in standard output mode. Gates 0-7 are absolute energy, gates 8-15 are SNR relative. |
bool energyDataAvailable() |
Returns true if the sensor is in standard output mode and per-gate energy data is being received. |
uint16_t autoThresholdProgress() |
Progress of auto-calibration (0-10000, where 10000 = 100%). Only updates during an active auto-threshold scan. |
All configuration commands return true on success. They handle the enableConfig/endConfig protocol internally. Thread-safe via FreeRTOS mutex with 1-second timeout.
Important: Config commands are unreliable while the sensor is actively streaming. Use stopStreaming() first, or use the test program's withStreamingStopped() pattern.
| Method | Description |
|---|---|
bool readFirmwareVersion(uint16_t &major, uint16_t &minor, uint16_t &patch) |
Read the sensor's firmware version. Example: v1.1.1. |
bool readSerialNumber(char *sn, uint8_t &len) |
Read the sensor's serial number (up to 8 characters). sn must be at least 9 bytes for null terminator. |
bool writeSerialNumber(const char *sn, uint8_t len) |
Write a serial number to the sensor (1-8 characters). |
bool readGenericParams(LD2410S_GenericParams ¶ms) |
Read all generic parameters: farthest/nearest gate, unmanned delay, status/distance report frequency, response speed. |
bool writeGenericParams(const LD2410S_GenericParams ¶ms) |
Write all generic parameters. This is also what activates streaming — the sensor begins outputting data after a successful write + endConfig. |
bool readThresholds(LD2410S_Thresholds &thresholds) |
Read trigger and hold thresholds for all 16 gates. Internally issues two commands: 0x73 (trigger) and 0x77 (hold). |
bool writeThresholds(const LD2410S_Thresholds &thresholds) |
Write trigger and hold thresholds for all 16 gates. Internally issues two commands: 0x72 (trigger) and 0x76 (hold). |
bool switchOutputMode(bool standardMode) |
Switch between minimal (5-byte) and standard (per-gate energy) output modes. true = standard, false = minimal. |
bool startAutoThreshold(uint16_t triggerFactor, uint16_t holdFactor, uint16_t scanTimeSec) |
Start the sensor's built-in auto-calibration. The room must be empty during the scan. Defaults: triggerFactor=2, holdFactor=1, scanTime=120s. Monitor progress via autoThresholdProgress(). |
bool factoryReset() |
Restore the sensor to factory default settings. Streaming will stop after reset — call startStreaming() or writeGenericParams() to resume. |
The LD2410S cannot reliably process config commands while actively streaming data.
Use stopStreaming() / startStreaming() to pause and resume streaming around
configuration operations.
| Method | Description |
|---|---|
bool stopStreaming() |
Enter config mode, which halts UART data output. Config commands are now reliable. Returns true if config mode was entered successfully. |
bool startStreaming() |
Read current generic params (or use defaults), write them back, and exit config mode. This resumes UART data output. Returns true on success. |
sensor.stopStreaming();
// Config commands are now reliable
sensor.readFirmwareVersion(major, minor, patch);
sensor.readGenericParams(params);
sensor.startStreaming();
// Live data resumes| Method | Description |
|---|---|
void setDebug(bool enable) |
Enable or disable verbose hex dump of UART traffic during commands. Useful for protocol debugging. |
bool debugEnabled() |
Returns current debug output state. |
Controls sensor power via a GPIO that drives an external load switch or MOSFET. The GPIO does not sink the sensor's current directly — the LD2410S draws ~118mA peak during radar scans, which exceeds ESP32 GPIO capability.
Suitable external switches:
- High-side load switch IC (e.g. TPS22918, AP2281) on the 3.3V rail
- Low-side N-channel MOSFET (e.g. 2N7002, BSS138) on the ground path
GPIO HIGH = sensor ON, GPIO LOW = sensor OFF.
| Method | Description |
|---|---|
LD2410S_Power(int enablePin, int uartRxPin, int uartTxPin) |
Constructor. enablePin is the GPIO connected to the load switch/FET gate. uartRxPin/uartTxPin are the UART pins (needed for safe floating during power off). |
void begin() |
Initialize the enable GPIO as output, default state OFF. Call once in setup(). |
bool powerOn(HardwareSerial &serial, unsigned long bootTimeoutMs = 2000) |
Float UART pins, assert enable HIGH, wait 100ms for rail stabilization, hold TX HIGH to prevent boot glitch, initialize UART, then wait up to bootTimeoutMs for the first valid data frame (0x6E header). Returns true if data was detected. |
void powerOff(HardwareSerial &serial) |
Deinitialize UART via serial.end(), float UART pins to high-impedance (prevents backfeed through ESD diodes), then deassert enable LOW. |
bool powerCycle(HardwareSerial &serial, unsigned long offTimeMs = 500, unsigned long bootTimeoutMs = 2000) |
Full power cycle: powerOff(), wait offTimeMs for capacitors to discharge, then powerOn(). Returns true if sensor responds after restart. |
bool isPoweredOn() |
Returns true if the sensor is currently powered on. |
int enablePin() |
Returns the enable GPIO number. |
int uartRxPin() |
Returns the UART RX GPIO number. |
int uartTxPin() |
Returns the UART TX GPIO number. |
#include "LD2410S_Power.h"
LD2410S_Power power(40, 16, 15); // enable pin, RX pin, TX pin
LD2410S sensor;
void setup() {
power.begin(); // GPIO init, sensor OFF
power.powerOn(Serial1); // enable switch, init UART, wait for boot
sensor.begin(Serial1, 16, 15); // init library on the open UART
// activate streaming (writeGenericParams + exitConfig)...
}
void restartSensor() {
sensor.end(); // reset library state
power.powerCycle(Serial1); // off 500ms, on, wait for boot
sensor.begin(Serial1, 16, 15); // re-init library
}UART pins are floated before disabling power to prevent backfeed current through ESD diodes.
| Field | Type | Range | Description |
|---|---|---|---|
farthestGate |
uint8_t |
1-16 | Maximum detection distance in gates (gate x 0.7m) |
nearestGate |
uint8_t |
0-16 | Minimum detection distance in gates |
unmannedDelay_s |
uint16_t |
10-120 | Seconds after last detection before reporting "no one" |
statusReportFreq |
float |
0.5-8.0 | How often the sensor reports presence status (Hz) |
distanceReportFreq |
float |
0.5-8.0 | How often the sensor reports distance (Hz) |
responseSpeed |
uint8_t |
5 or 10 | 5 = Normal, 10 = Fast |
Trigger and hold thresholds for all 16 gates.
| Field | Type | Description |
|---|---|---|
trigger[16] |
uint32_t[] |
Threshold that triggers detection per gate |
hold[16] |
uint32_t[] |
Threshold that sustains detection per gate |
Gates 0-7 use absolute energy values (typical range 10-95). Gates 8-15 use SNR-relative values (typical range 5-63). Hold values can exceed documented ranges (values up to ~134 observed on FW 1.1.1).
| Value | Name | Description |
|---|---|---|
0x00 |
TARGET_NONE_0 |
No target detected |
0x01 |
TARGET_NONE_1 |
No target detected (alternate) |
0x02 |
TARGET_MOVING |
Moving target detected |
0x03 |
TARGET_STATIONARY |
Stationary target detected |
The included test program (src/main.cpp) provides interactive serial console
commands for testing all sensor functions:
stop, start, info, version, params, thresholds, status, setgate, setdelay,
setthreshold, autocalib, standard, minimal, factoryreset,
poweron, poweroff, powercycle, debug
Commands that read or write sensor config automatically stop and restart streaming.
Use stop/start for manual control. Live data display and parsing are
suppressed while streaming is stopped.
Power commands (poweron/poweroff/powercycle) require LD2410S_GND_PIN to be
set to a valid GPIO number (default is -1 = disabled). Requires an external load
switch or MOSFET — do not connect the sensor's ground directly to a GPIO.
The V1.00 protocol (FW 1.1.x) uses separate commands for trigger and hold thresholds, each covering all 16 gates:
| Command | ID | Direction | Description |
|---|---|---|---|
| Write trigger | 0x0072 | Host->Sensor | Set trigger thresholds for gates 0-15 |
| Read trigger | 0x0073 | Host->Sensor | Get trigger thresholds for gates 0-15 |
| Write hold | 0x0076 | Host->Sensor | Set hold thresholds for gates 0-15 |
| Read hold | 0x0077 | Host->Sensor | Get hold thresholds for gates 0-15 |
Each command takes 16 parameter words (0x00-0x0F) and returns 16 x uint32_t values.
Gates 0-7 (0-5.6m) use absolute energy thresholds. Gates 8-15 (5.6-11.2m) use SNR-relative thresholds. Both types are handled identically in the protocol — the distinction is in the hardware's detection algorithm, not the command interface.
Tested on ESP32-S3-WROOM1 (N8R2, 8MB flash, 2MB PSRAM) Sensor UART on GPIO 15 (TX) / GPIO 16 (RX) via Serial2.
Breaking changes:
LD2410S_Thresholdsnow covers all 16 gates (was 8). Fields:trigger[16],hold[16]- Removed
LD2410S_SNRThresholdsstruct andreadSNRThresholds()/writeSNRThresholds()methods - Removed
restart()method (not documented in LD2410S protocol) NUM_GATESchanged from 8 to 16
Bug fixes:
- Fixed minimal frame parser logic error (dead code path, tail byte check never triggered)
- Fixed
enableConfig()sending wrong protocol version (was 0x01, now 0x02 per V1.00 spec) - Fixed
readFirmwareVersion()reading wrong ACK data offsets (was returning v1.1.0 for FW 1.1.1) - Fixed threshold commands using V1.2 layout instead of V1.00 layout
- Fixed missing
uint32_tcasts on shift operations in gate energy parsing - Simplified ACK command word derivation:
cmdWord | 0x0100
Improvements:
readThresholds()/writeThresholds()now handle all 16 gates using two commands (trigger + hold)setthresholdtest command now accepts gates 0-15 (was split intosetthreshold0-7 andsetsnr8-15)- Threshold display shows all 16 gates in a table with abs/SNR labels
Copyright Hyak Consulting LLC. All rights reserved.