Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
target
Cargo.lock
.DS_Store

#agent files
GEMINI.md
27 changes: 24 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ld2410s"
version = "0.1.2"
version = "0.2.0"
authors = ["Max van der Schee <26795741+mvdschee@users.noreply.github.com>"]
description = "HLK-LD2410S driver with backend-agnostic UART ownership for serialport or esp-idf-hal"
edition = "2024"
Expand All @@ -18,22 +18,35 @@ debug = true
opt-level = "z"

[[example]]
name = "desktop"
path = "examples/desktop.rs"
name = "serial"
path = "examples/serial.rs"
required-features = ["serial"]

[[example]]
name = "esp"
path = "examples/esp.rs"
required-features = ["embedded"]

[[example]]
name = "async_serial"
path = "examples/async_serial.rs"
required-features = ["async"]

[[example]]
name = "async_esp"
path = "examples/async_esp.rs"
required-features = ["embedded", "async"]

[dependencies]
heapless = "0.9.2"
embedded-io-async = { version = "0.7.0", optional = true }
embassy-time = { version = "0.5.0", optional = true }

[features]
default = []
serial = ["serialport"]
embedded = ["esp-idf-hal"]
async = ["embedded-io-async", "embassy-time"]

[dependencies.serialport]
version = "4.8.1"
Expand All @@ -42,3 +55,11 @@ optional = true
[dependencies.esp-idf-hal]
version = "0.45.2"
optional = true


[dev-dependencies]
tokio = { version = "1.49.0", features = ["full"] }
tokio-serial = "5.4.5"
embedded-io-adapters = { version = "0.7.0", features = ["tokio-1"] }
anyhow = "1.0.100"
embassy-time = { version = "0.5.0", features = ["std", "generic-queue-8"] }
138 changes: 60 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,141 +1,123 @@
# LD2410S Rust Driver

A Rust library for reading and controlling the **HLK-LD2410S** 24GHz radar presence sensor over UART.
Supports both **desktop** (via [serialport](https://crates.io/crates/serialport)) and **embedded** (via [esp-idf-hal](https://crates.io/crates/esp-idf-hal)) targets.
A **no_std**, **zero-heap** Rust library for reading and controlling the **HLK-LD2410S** 24GHz radar presence sensor over UART.

## ✨ Features

- Works with **desktop** and **ESP-IDF** environments using the same API.
- **Unified UART interface** so your application doesn’t need to handle serial quirks.
- Built-in **frame parsing** for:
- **Portability**: Core driver is `no_std` and uses zero heap allocations (via `heapless`).
- **Flexible IO**: Supports **Blocking** (Sync) and **Async** (via `embedded-io-async`).
- **Backend Agnostic**: Works on desktop (via `serialport`) and embedded (via `esp-idf-hal`) through a simple trait abstraction.
- **Unified Parsing**: Built-in frame parsing for:
- **Minimal Packets** (Presence state, Distance)
- **Standard/Engineering Packets** (Distance, Signal Energy arrays)
- **Firmware Version** & **Serial Number**
- **Configuration Support**:
- Switch between Minimal and Standard/Engineering modes.
- Configure reporting frequency (e.g., 8Hz).
- Configure response speed.
- Automatic caching of last reading if no fresh data is available.
- **Full Configuration**:
- Switch between Minimal and Standard modes.
- Set reporting frequencies, response speeds, and gate thresholds.
- Support for Automatic Calibration.

## 📚 Documentation

This library implements the protocol defined in the official HLK-LD2410S manuals:

- [HLK-LD2410S Serial Communication Protocol V1.00](./docs/HLK-LD2410S_serial_communication_protocol-V1.00.pdf) (26 Nov 2024)
- [HLK-LD2410S User Manual V1.2](./docs/HLK-LD2410S_User_manual-V1.2.pdf) (20 Apr 2024)
- [HLK-LD2410S Serial Communication Protocol V1.00](./docs/HLK-LD2410S_serial_communication_protocol-V1.00.pdf)
- [HLK-LD2410S User Manual V1.2](./docs/HLK-LD2410S_User_manual-V1.2.pdf)

## 📦 Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
ld2410s = { git = "https://github.com/mvdschee/ld2410s", tag = "v0.1.2", features = ["serial"] } # desktop serialport
# ld2410s = { git = "https://github.com/mvdschee/ld2410s", tag = "v0.1.2", features = ["embedded"] } # embedded ESP-IDF
# Desktop Serial (Sync)
ld2410s = { version = "0.2.0", features = ["serial"] }
# Async Support
# ld2410s = { version = "0.2.0", features = ["async"] }
# Embedded ESP-IDF
# ld2410s = { version = "0.2.0", features = ["embedded"] }
```

## 🚀 Examples

### Desktop (via USB-to-UART adapter)
### Desktop (Sync)

cargo run --example desktop --features serial
cargo run --example serial --features serial

```rs
let port = serialport::new("/dev/tty.usbserial-123", BAUD_RATE)
.timeout(Duration::from_millis(50))
.open()?;

// 1. Initialize sensor (Standard Mode = Engineering Data)
let mut sensor = LD2410S::new(SerialPortWrapper(port), OutputMode::Standard);
// LD2410S::new(UART, TIMER, MODE)
let mut sensor = LD2410S::new(SerialPortWrapper(port), StdTimer::default(), OutputMode::Standard);
sensor.init()?;

// 2. Configure for faster updates (8Hz)
sensor.set_distance_frequency(8.0)?;
sensor.set_status_frequency(8.0)?;
sensor.set_response_speed(10)?;

loop {
if let Some(reading) = sensor.read_latest()? {
match reading.data {
if let Ok(Some(packet)) = sensor.next_packet() {
match packet {
ld2410s::Packet::Standard(s) => println!("Dist: {} Energy: {:?}", s.distance_cm, s.energy),
_ => {}
}
}
}
```

### ESP32 (via `esp-idf-hal`)
### Desktop (Async via Tokio)

cargo run --example async_serial --features async

```rs
let port = tokio_serial::new("/dev/tty.usbserial-123", BAUD_RATE).open_native_async()?;
let mut sensor = LD2410SAsync::new(FromTokio::new(port), OutputMode::Standard);
sensor.init().await?;

loop {
if let Ok(Some(packet)) = sensor.next_packet().await {
// handle packet...
}
}
```

### ESP32 (Sync via `esp-idf-hal`)

cargo run --example esp --features embedded

```rs
let peripherals = Peripherals::take().unwrap();
let pins = peripherals.pins;
let cfg = Config::default().baudrate(BAUD_RATE.Hz());
let uart = UartDriver::new(
peripherals.uart1,
pins.gpio4,
pins.gpio5,
None, None,
&cfg,
)?;

let mut sensor = LD2410S::new(EspUartWrapper(uart), OutputMode::Standard);
let uart = UartDriver::new(peripherals.uart1, pins.gpio4, pins.gpio5, None, None, &cfg)?;
let mut sensor = LD2410S::new(EspUartWrapper(uart), EspTimer, OutputMode::Standard);
sensor.init()?;
sensor.set_distance_frequency(8.0)?;

loop {
if let Some(reading) = sensor.read_latest()? {
match reading.data {
ld2410s::Packet::Standard(s) => println!("Dist: {} Energy: {:?}", s.distance_cm, s.energy),
_ => {}
}
if let Ok(Some(packet)) = sensor.next_packet() {
// handle packet...
}
}
```

## ⚙️ Advanced Configuration

### Manual Thresholds

You can manually set the sensitivity (energy threshold) for each of the 16 distance gates.

- **Trigger Threshold**: Energy required to switch from Unoccupied -> Occupied.
- **Hold Threshold**: Energy required to maintain Occupied state.

```rs
// Set gates 0-15. Lower value = Higher sensitivity.
// Example: High sensitivity for close range (gates 0-2), lower for far (3-15).
let triggers: [u16; 16] = [
15, 15, 20, 30, 40, 50, 60, 60,
60, 60, 60, 60, 60, 60, 60, 60
];
sensor.set_trigger_thresholds(&triggers)?;

// Hold thresholds are usually slightly lower than trigger to prevent flickering
let holds: [u16; 16] = [
10, 10, 15, 25, 35, 45, 55, 55,
55, 55, 55, 55, 55, 55, 55, 55
];
sensor.set_hold_thresholds(&holds)?;
```

### Automatic Calibration
### ESP32 (Async via `esp-idf-hal`)

Use this **once** during installation with an **empty room**. The sensor will measure background noise and set thresholds automatically.
cargo run --example async_esp --features embedded,async

```rs
// 1. Trigger Factor (added to noise floor for Trigger)
// 2. Retention Factor (added to noise floor for Hold)
// 3. Scanning Time (seconds)
// Example: factor=2, retention=1, scan=120s
sensor.set_auto_threshold(2, 1, 120)?;
let uart = AsyncUartDriver::new(peripherals.uart1, pins.gpio4, pins.gpio5, None, None, &cfg)?;
let mut sensor = LD2410SAsync::new(uart, OutputMode::Standard);

block_on(async {
sensor.init().await.unwrap();
loop {
if let Ok(Some(packet)) = sensor.next_packet().await {
// handle packet...
}
}
})
```

## ⚙️ Feature Flags

- `serial` → Use [serialport](https://crates.io/crates/serialport) (desktop/hosted)
- `embedded` → Use [esp-idf-hal](https://crates.io/crates/esp-idf-hal) (ESP32)
- `serial` → Use [serialport](https://crates.io/crates/serialport) (desktop/hosted).
- `embedded` → Use [esp-idf-hal](https://crates.io/crates/esp-idf-hal) (ESP32).
- `async` → Use [embedded-io-async](https://crates.io/crates/embedded-io-async) and [embassy-time](https://crates.io/crates/embassy-time).

## 📝 License

Expand Down
52 changes: 52 additions & 0 deletions examples/async_esp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use esp_idf_hal::prelude::*;
use esp_idf_hal::task::block_on;
use esp_idf_hal::uart::{AsyncUartDriver, config::Config};
use ld2410s::asynchronous::LD2410SAsync;
use ld2410s::{BAUD_RATE, OutputMode, Packet};

fn main() -> anyhow::Result<()> {
esp_idf_svc::sys::link_patches();

let peripherals = Peripherals::take().unwrap();
let pins = peripherals.pins;
let tx = pins.gpio4;
let rx = pins.gpio5;
let cfg = Config::default().baudrate(BAUD_RATE.Hz());

// Initialize Async UART
let uart = AsyncUartDriver::new(
peripherals.uart1,
tx,
rx,
Option::<esp_idf_hal::gpio::AnyIOPin>::None,
Option::<esp_idf_hal::gpio::AnyIOPin>::None,
&cfg,
)?;

// Create Async Driver
// AsyncUartDriver implements embedded_io_async::Read and Write
let mut sensor = LD2410SAsync::new(uart, OutputMode::Standard);

// Run the async task
block_on(async {
println!("Initializing sensor...");
sensor.init().await.expect("Failed to init");

println!("Configuring sensor...");
sensor.set_distance_frequency(8.0).await.expect("Failed to config");
sensor.set_status_frequency(8.0).await.expect("Failed to config");
sensor.set_response_speed(10).await.expect("Failed to config");

loop {
match sensor.next_packet().await {
Ok(Some(packet)) => match packet {
Packet::Minimal(m) => println!("Minimal: {:?}", m),
Packet::Standard(s) => println!("Standard: {:?}", s),
_ => {}
},
Ok(None) => {}
Err(e) => println!("Error: {:?}", e),
}
}
})
}
57 changes: 57 additions & 0 deletions examples/async_serial.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use embedded_io_adapters::tokio_1::FromTokio;
use ld2410s::asynchronous::LD2410SAsync;
use ld2410s::{BAUD_RATE, OutputMode, Packet};
use std::time::Duration;
use tokio_serial::SerialPortBuilderExt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let port_name = "/dev/tty.usbserial-130"; // Adjust to your port

let mut port = tokio_serial::new(port_name, BAUD_RATE).open_native_async()?;

#[cfg(unix)]
port.set_exclusive(false)?;

// Wrap the tokio serial port in an adapter that implements embedded-io-async traits
let embedded_port = FromTokio::new(port);

// Create the async driver
let mut sensor = LD2410SAsync::new(embedded_port, OutputMode::Standard);

println!("Initializing sensor...");
sensor.init().await.expect("Failed to initialize sensor");

println!("Configuring sensor...");
sensor.set_distance_frequency(8.0).await.map_err(|e| anyhow::anyhow!("{:?}", e))?;
sensor.set_status_frequency(8.0).await.map_err(|e| anyhow::anyhow!("{:?}", e))?;
sensor.set_response_speed(10).await.map_err(|e| anyhow::anyhow!("{:?}", e))?;

println!("Reading firmware version...");
match sensor.read_firmware_version().await {
Ok(v) => println!(
"Firmware: Type={:08X} VerType={:04X} v{}.{}.{}",
v.equipment_type, v.version_type, v.major, v.minor, v.patch
),
Err(e) => eprintln!("Error reading firmware: {:?}", e),
}

println!("Starting poll loop...");
loop {
// Read packets asynchronously
match sensor.next_packet().await {
Ok(Some(packet)) => match packet {
Packet::Minimal(m) => println!("Minimal: {:?}", m),
Packet::Standard(s) => println!("Standard: {:?}", s),
Packet::Ack(ack) => println!("ACK: {:?}", ack),
Packet::Firmware(v) => println!("Firmware: {:?}", v),
Packet::SerialNumber(sn) => println!("Serial Number: {:?}", sn),
},
Ok(None) => {}
Err(e) => eprintln!("UART Error: {:?}", e),
}

// Yield to let other tasks run (not strictly needed with await, but good practice in loops)
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
Loading
Loading