DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ld2410.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ld2410.cpp
6 * @brief HLK-LD2410 mmWave radar codec - implementation. See ld2410.h.
7 *
8 * Frame layout (little-endian lengths / distances), per the Hi-Link serial protocol:
9 * report: F4 F3 F2 F1 | len(2) | type | AA | state | mv_cm(2) mv_e | st_cm(2) st_e |
10 * det_cm(2) | [engineering: maxMvGate maxStGate mvE[9] stE[9] light out] |
11 * 55 | check | F8 F7 F6 F5 (len = 0x0D basic, 0x23 engineering)
12 * command: FD FC FB FA | len(2) | word(2) | [value] | 04 03 02 01
13 */
14
16#include "ServerConfig.h"
17
18#if DETWS_ENABLE_LD2410
19
20#include <string.h>
21
22namespace
23{
24const uint8_t HDR[4] = {0xF4, 0xF3, 0xF2, 0xF1};
25const uint8_t FTR[4] = {0xF8, 0xF7, 0xF6, 0xF5};
26const uint8_t CMD_HDR[4] = {0xFD, 0xFC, 0xFB, 0xFA};
27const uint8_t CMD_FTR[4] = {0x04, 0x03, 0x02, 0x01};
28
29const uint16_t LEN_BASIC = 13; // payload length for a basic target frame
30const uint16_t LEN_ENGINEERING = 35; // ... and for an engineering-mode frame
31
32uint16_t rd16(const uint8_t *p)
33{
34 return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
35}
36
37// Build one command frame; returns its length or 0 if @p cap is too small.
38size_t cmd_frame(uint8_t *buf, size_t cap, uint16_t word, const uint8_t *val, size_t vlen)
39{
40 size_t need = 4 + 2 + 2 + vlen + 4;
41 if (!buf || cap < need)
42 return 0;
43 size_t i = 0;
44 for (int k = 0; k < 4; k++)
45 buf[i++] = CMD_HDR[k];
46 uint16_t dl = (uint16_t)(2 + vlen);
47 buf[i++] = (uint8_t)(dl & 0xFF);
48 buf[i++] = (uint8_t)(dl >> 8);
49 buf[i++] = (uint8_t)(word & 0xFF);
50 buf[i++] = (uint8_t)(word >> 8);
51 for (size_t k = 0; k < vlen; k++)
52 buf[i++] = val[k];
53 for (int k = 0; k < 4; k++)
54 buf[i++] = CMD_FTR[k];
55 return i;
56}
57} // namespace
58
59bool ld2410_parse_report(const uint8_t *f, size_t len, Ld2410Report *out)
60{
61 if (!f || !out || len < (size_t)(6 + LEN_BASIC + 4))
62 return false;
63 if (memcmp(f, HDR, 4) != 0)
64 return false;
65 uint16_t dl = rd16(f + 4);
66 if ((size_t)(6 + dl + 4) != len)
67 return false; // length field must frame the buffer exactly
68 if (memcmp(f + 6 + dl, FTR, 4) != 0)
69 return false;
70
71 const uint8_t *p = f + 6;
73 memset(&r, 0, sizeof(r));
74 if (p[0] == 0x02)
75 {
76 if (dl != LEN_BASIC)
77 return false;
78 r.engineering = 0;
79 }
80 else if (p[0] == 0x01)
81 {
82 if (dl != LEN_ENGINEERING)
83 return false;
84 r.engineering = 1;
85 }
86 else
87 {
88 return false; // unknown data type
89 }
90 if (p[1] != 0xAA)
91 return false; // intra-frame head marker
92
93 r.state = p[2];
94 r.moving_cm = rd16(p + 3);
95 r.moving_energy = p[5];
96 r.static_cm = rd16(p + 6);
97 r.static_energy = p[8];
98 r.detect_cm = rd16(p + 9);
99
100 if (r.engineering)
101 {
102 r.max_moving_gate = p[11];
103 r.max_static_gate = p[12];
104 for (int i = 0; i < LD2410_MAX_GATES; i++)
105 r.moving_gate_energy[i] = p[13 + i];
106 for (int i = 0; i < LD2410_MAX_GATES; i++)
107 r.static_gate_energy[i] = p[22 + i];
108 r.light = p[31];
109 r.out_pin = p[32];
110 if (p[33] != 0x55)
111 return false; // tail
112 }
113 else if (p[11] != 0x55)
114 {
115 return false; // tail
116 }
117 *out = r;
118 return true;
119}
120
122{
123 s->pos = 0;
124 s->total = 0;
125 s->hdr_match = 0;
126 s->phase = 0;
127}
128
129bool ld2410_stream_push(Ld2410Stream *s, uint8_t b, Ld2410Report *out)
130{
131 switch (s->phase)
132 {
133 case 0: // sync on the 4-byte header (bytes are distinct, so resync restarts at most at [0])
134 if (b == HDR[s->hdr_match])
135 {
136 s->buf[s->hdr_match++] = b;
137 if (s->hdr_match == 4)
138 {
139 s->pos = 4;
140 s->phase = 1;
141 }
142 }
143 else
144 {
145 s->hdr_match = (b == HDR[0]) ? 1 : 0;
146 if (s->hdr_match)
147 s->buf[0] = b;
148 }
149 return false;
150 case 1: // little-endian length field
151 s->buf[s->pos++] = b;
152 if (s->pos == 6)
153 {
154 // Widen before comparing: 6 + 0xFFFF + 4 wraps a uint16_t, defeating the guard.
155 uint32_t total = 6u + (uint32_t)rd16(s->buf + 4) + 4u;
156 if (total > LD2410_FRAME_MAX)
157 {
158 ld2410_stream_reset(s); // absurd length: drop and resync
159 return false;
160 }
161 s->total = (uint16_t)total;
162 s->phase = 2;
163 }
164 return false;
165 default: // body + footer
166 s->buf[s->pos++] = b;
167 if (s->pos >= s->total)
168 {
169 bool ok = ld2410_parse_report(s->buf, s->total, out);
171 return ok;
172 }
173 return false;
174 }
175}
176
177bool ld2410_present(const Ld2410Report *r)
178{
179 return r && r->state != Ld2410State::LD2410_STATE_NONE;
180}
181
182uint16_t ld2410_distance_cm(const Ld2410Report *r)
183{
184 if (!r)
185 return 0;
187 return r->moving_cm;
189 return r->static_cm;
190 return 0;
191}
192
193size_t ld2410_cmd_config_enable(uint8_t *buf, size_t cap)
194{
195 const uint8_t v[2] = {0x01, 0x00}; // value 0x0001
196 return cmd_frame(buf, cap, 0x00FF, v, 2);
197}
198size_t ld2410_cmd_config_end(uint8_t *buf, size_t cap)
199{
200 return cmd_frame(buf, cap, 0x00FE, nullptr, 0);
201}
202size_t ld2410_cmd_engineering(uint8_t *buf, size_t cap, bool on)
203{
204 return cmd_frame(buf, cap, on ? 0x0062 : 0x0063, nullptr, 0);
205}
206size_t ld2410_cmd_restart(uint8_t *buf, size_t cap)
207{
208 return cmd_frame(buf, cap, 0x00A3, nullptr, 0);
209}
210
211// ---------------------------------------------------------------------------
212// UART binding
213// ---------------------------------------------------------------------------
214
215#if defined(ARDUINO)
216
217#include <Arduino.h>
218
219namespace
220{
221// All LD2410 UART-binding state, owned by one instance (internal linkage): the frame stream
222// assembler, the last decoded report, and the have-report flag, grouped so it is one named
223// owner, unreachable from any other translation unit.
224struct Ld2410Ctx
225{
226 Ld2410Stream stream;
227 Ld2410Report last;
228 bool have = false;
229};
230Ld2410Ctx s_ld;
231} // namespace
232
233bool ld2410_begin(int rx_pin, int tx_pin)
234{
235 ld2410_stream_reset(&s_ld.stream);
236 s_ld.have = false;
237 Serial2.begin(DETWS_LD2410_BAUD, SERIAL_8N1, rx_pin, tx_pin);
238 return true;
239}
240
241bool ld2410_poll()
242{
243 bool fresh = false;
244 while (Serial2.available())
245 {
246 Ld2410Report r;
247 if (ld2410_stream_push(&s_ld.stream, (uint8_t)Serial2.read(), &r))
248 {
249 s_ld.last = r;
250 s_ld.have = true;
251 fresh = true;
252 }
253 }
254 return fresh;
255}
256
258{
259 return s_ld.have ? &s_ld.last : nullptr;
260}
261
262bool ld2410_set_engineering(bool on)
263{
264 uint8_t f[16];
265 size_t n;
266 n = ld2410_cmd_config_enable(f, sizeof(f));
267 Serial2.write(f, n);
268 n = ld2410_cmd_engineering(f, sizeof(f), on);
269 Serial2.write(f, n);
270 n = ld2410_cmd_config_end(f, sizeof(f));
271 Serial2.write(f, n);
272 Serial2.flush();
273 return true;
274}
275
276bool ld2410_restart()
277{
278 uint8_t f[16];
279 size_t n;
280 n = ld2410_cmd_config_enable(f, sizeof(f));
281 Serial2.write(f, n);
282 n = ld2410_cmd_restart(f, sizeof(f));
283 Serial2.write(f, n);
284 n = ld2410_cmd_config_end(f, sizeof(f));
285 Serial2.write(f, n);
286 Serial2.flush();
287 return true;
288}
289
290#else // host build: no UART. The codec above is host-tested.
291
292bool ld2410_begin(int, int)
293{
294 return false;
295}
296bool ld2410_poll()
297{
298 return false;
299}
301{
302 return nullptr;
303}
304bool ld2410_set_engineering(bool)
305{
306 return false;
307}
308bool ld2410_restart()
309{
310 return false;
311}
312
313#endif // ARDUINO
314
315#endif // DETWS_ENABLE_LD2410
User-facing configuration for DeterministicESPAsyncWebServer.
#define DETWS_LD2410_BAUD
LD2410 UART baud rate (the module's fixed factory default is 256000).
HLK-LD2410 24 GHz mmWave presence / motion radar codec (DETWS_ENABLE_LD2410).
bool ld2410_present(const Ld2410Report *r)
true if r shows any target (moving or stationary).
size_t ld2410_cmd_restart(uint8_t *buf, size_t cap)
Restart the module (word 0x00A3).
const Ld2410Report * ld2410_last()
The most recently decoded report, or nullptr before the first one arrives.
bool ld2410_restart()
Restart the module (brackets the command with enable/end).
size_t ld2410_cmd_engineering(uint8_t *buf, size_t cap, bool on)
Enable (0x0062) or disable (0x0063) engineering mode.
uint16_t ld2410_distance_cm(const Ld2410Report *r)
Best available target distance (cm): the moving distance if moving, else stationary.
bool ld2410_stream_push(Ld2410Stream *s, uint8_t byte, Ld2410Report *out)
Feed one received byte. When it completes a valid report frame, fills out and returns true; otherwise...
bool ld2410_parse_report(const uint8_t *frame, size_t len, Ld2410Report *out)
Decode one whole LD2410 report frame (header F4 F3 F2 F1 .. footer F8 F7 F6 F5). Pure - no I/O....
#define LD2410_MAX_GATES
Range gates the LD2410 reports energy for in engineering mode (gate 0..8).
Definition ld2410.h:33
bool ld2410_begin(int rx_pin, int tx_pin)
Open UART2 at DETWS_LD2410_BAUD on rx_pin / tx_pin.
size_t ld2410_cmd_config_enable(uint8_t *buf, size_t cap)
"Enable configuration" (word 0x00FF, value 0x0001).
#define LD2410_FRAME_MAX
Largest assembled frame: header(4) + len(2) + payload(<=60) + footer(4), rounded up.
Definition ld2410.h:36
bool ld2410_poll()
Pump the UART through the stream.
bool ld2410_set_engineering(bool on)
Enable/disable engineering mode (brackets the command with enable/end).
size_t ld2410_cmd_config_end(uint8_t *buf, size_t cap)
"End configuration" (word 0x00FE).
void ld2410_stream_reset(Ld2410Stream *s)
Reset a stream to the syncing state.
A decoded LD2410 target report. Engineering fields are 0 unless engineering.
Definition ld2410.h:49
uint16_t detect_cm
overall detection distance (cm)
Definition ld2410.h:56
uint8_t static_gate_energy[LD2410_MAX_GATES]
per-gate stationary energy (0-100)
Definition ld2410.h:61
uint16_t moving_cm
moving target distance (cm)
Definition ld2410.h:52
uint8_t out_pin
OUT pin level (0/1)
Definition ld2410.h:63
uint8_t max_static_gate
highest configured stationary gate
Definition ld2410.h:59
uint8_t state
one of LD2410_STATE_*
Definition ld2410.h:51
uint8_t static_energy
stationary target energy (0-100)
Definition ld2410.h:55
uint8_t engineering
1 if this was an engineering-mode frame (per-gate energies valid)
Definition ld2410.h:50
uint8_t moving_gate_energy[LD2410_MAX_GATES]
per-gate moving energy (0-100)
Definition ld2410.h:60
uint8_t moving_energy
moving target energy (0-100)
Definition ld2410.h:53
uint8_t max_moving_gate
highest configured moving gate
Definition ld2410.h:58
uint8_t light
photosensor level (0-255)
Definition ld2410.h:62
uint16_t static_cm
stationary target distance (cm)
Definition ld2410.h:54
static constexpr uint8_t LD2410_STATE_NONE
no target
Definition ld2410.h:41
static constexpr uint8_t LD2410_STATE_BOTH
both a moving and a stationary target
Definition ld2410.h:44
static constexpr uint8_t LD2410_STATE_STATIC
stationary target only
Definition ld2410.h:43
static constexpr uint8_t LD2410_STATE_MOVING
moving target only
Definition ld2410.h:42
Byte-by-byte report-frame reassembler (fixed buffer, resyncs on noise).
Definition ld2410.h:76
uint16_t pos
bytes collected so far
Definition ld2410.h:78
uint8_t hdr_match
header bytes matched while syncing (phase = pos<4)
Definition ld2410.h:80
uint16_t total
expected full-frame length (known after the length field)
Definition ld2410.h:79
uint8_t buf[LD2410_FRAME_MAX]
frame under construction
Definition ld2410.h:77
uint8_t phase
0 sync, 1 length, 2 body
Definition ld2410.h:81