ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 * pc_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 "protocore_config.h"
17
18#if PC_ENABLE_LD2410
19
20#include <string.h>
21
22#if defined(ARDUINO)
23#include <Arduino.h>
24#endif
25namespace
26{
27const uint8_t HDR[4] = {0xF4, 0xF3, 0xF2, 0xF1};
28const uint8_t FTR[4] = {0xF8, 0xF7, 0xF6, 0xF5};
29const uint8_t CMD_HDR[4] = {0xFD, 0xFC, 0xFB, 0xFA};
30const uint8_t CMD_FTR[4] = {0x04, 0x03, 0x02, 0x01};
31
32const uint16_t LEN_BASIC = 13; // payload length for a basic target frame
33const uint16_t LEN_ENGINEERING = 35; // ... and for an engineering-mode frame
34
35uint16_t rd16(const uint8_t *p)
36{
37 return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
38}
39
40// Build one command frame; returns its length or 0 if @p cap is too small.
41size_t cmd_frame(uint8_t *buf, size_t cap, uint16_t word, const uint8_t *val, size_t vlen)
42{
43 size_t need = 4 + 2 + 2 + vlen + 4;
44 if (!buf || cap < need)
45 {
46 return 0;
47 }
48 size_t i = 0;
49 for (int k = 0; k < 4; k++)
50 {
51 buf[i++] = CMD_HDR[k];
52 }
53 uint16_t dl = (uint16_t)(2 + vlen);
54 buf[i++] = (uint8_t)(dl & 0xFF);
55 buf[i++] = (uint8_t)(dl >> 8);
56 buf[i++] = (uint8_t)(word & 0xFF);
57 buf[i++] = (uint8_t)(word >> 8);
58 for (size_t k = 0; k < vlen; k++)
59 {
60 buf[i++] = val[k];
61 }
62 for (int k = 0; k < 4; k++)
63 {
64 buf[i++] = CMD_FTR[k];
65 }
66 return i;
67}
68} // namespace
69
70bool pc_ld2410_parse_report(const uint8_t *f, size_t len, Ld2410Report *out)
71{
72 if (!f || !out || len < (size_t)(6 + LEN_BASIC + 4))
73 {
74 return false;
75 }
76 if (memcmp(f, HDR, 4) != 0)
77 {
78 return false;
79 }
80 uint16_t dl = rd16(f + 4);
81 if ((size_t)(6 + dl + 4) != len)
82 {
83 return false; // length field must frame the buffer exactly
84 }
85 if (memcmp(f + 6 + dl, FTR, 4) != 0)
86 {
87 return false;
88 }
89
90 const uint8_t *p = f + 6;
92 memset(&r, 0, sizeof(r));
93 if (p[0] == 0x02)
94 {
95 if (dl != LEN_BASIC)
96 {
97 return false;
98 }
99 r.engineering = 0;
100 }
101 else if (p[0] == 0x01)
102 {
103 if (dl != LEN_ENGINEERING)
104 {
105 return false;
106 }
107 r.engineering = 1;
108 }
109 else
110 {
111 return false; // unknown data type
112 }
113 if (p[1] != 0xAA)
114 {
115 return false; // intra-frame head marker
116 }
117
118 r.state = p[2];
119 r.moving_cm = rd16(p + 3);
120 r.moving_energy = p[5];
121 r.static_cm = rd16(p + 6);
122 r.static_energy = p[8];
123 r.detect_cm = rd16(p + 9);
124
125 if (r.engineering)
126 {
127 r.max_moving_gate = p[11];
128 r.max_static_gate = p[12];
129 for (int i = 0; i < LD2410_MAX_GATES; i++)
130 {
131 r.moving_gate_energy[i] = p[13 + i];
132 }
133 for (int i = 0; i < LD2410_MAX_GATES; i++)
134 {
135 r.static_gate_energy[i] = p[22 + i];
136 }
137 r.light = p[31];
138 r.out_pin = p[32];
139 if (p[33] != 0x55)
140 {
141 return false; // tail
142 }
143 }
144 else if (p[11] != 0x55)
145 {
146 return false; // tail
147 }
148 *out = r;
149 return true;
150}
151
153{
154 s->pos = 0;
155 s->total = 0;
156 s->hdr_match = 0;
157 s->phase = 0;
158}
159
160bool pc_ld2410_stream_push(Ld2410Stream *s, uint8_t b, Ld2410Report *out)
161{
162 switch (s->phase)
163 {
164 case 0: // sync on the 4-byte header (bytes are distinct, so resync restarts at most at [0])
165 if (b == HDR[s->hdr_match])
166 {
167 s->buf[s->hdr_match++] = b;
168 if (s->hdr_match == 4)
169 {
170 s->pos = 4;
171 s->phase = 1;
172 }
173 }
174 else
175 {
176 s->hdr_match = (b == HDR[0]) ? 1 : 0;
177 if (s->hdr_match)
178 {
179 s->buf[0] = b;
180 }
181 }
182 return false;
183 case 1: // little-endian length field
184 s->buf[s->pos++] = b;
185 if (s->pos == 6)
186 {
187 // Widen before comparing: 6 + 0xFFFF + 4 wraps a uint16_t, defeating the guard.
188 uint32_t total = 6u + (uint32_t)rd16(s->buf + 4) + 4u;
189 if (total > LD2410_FRAME_MAX)
190 {
191 pc_ld2410_stream_reset(s); // absurd length: drop and resync
192 return false;
193 }
194 s->total = (uint16_t)total;
195 s->phase = 2;
196 }
197 return false;
198 default: // body + footer
199 s->buf[s->pos++] = b;
200 if (s->pos >= s->total)
201 {
202 bool ok = pc_ld2410_parse_report(s->buf, s->total, out);
204 return ok;
205 }
206 return false;
207 }
208}
209
210bool pc_ld2410_present(const Ld2410Report *r)
211{
212 return r && r->state != Ld2410State::LD2410_STATE_NONE;
213}
214
215uint16_t pc_ld2410_distance_cm(const Ld2410Report *r)
216{
217 if (!r)
218 {
219 return 0;
220 }
222 {
223 return r->moving_cm;
224 }
226 {
227 return r->static_cm;
228 }
229 return 0;
230}
231
232size_t pc_ld2410_cmd_config_enable(uint8_t *buf, size_t cap)
233{
234 static const uint8_t v[2] = {0x01, 0x00}; // value 0x0001
235 return cmd_frame(buf, cap, 0x00FF, v, 2);
236}
237size_t pc_ld2410_cmd_config_end(uint8_t *buf, size_t cap)
238{
239 return cmd_frame(buf, cap, 0x00FE, nullptr, 0);
240}
241size_t pc_ld2410_cmd_engineering(uint8_t *buf, size_t cap, bool on)
242{
243 return cmd_frame(buf, cap, on ? 0x0062 : 0x0063, nullptr, 0);
244}
245size_t pc_ld2410_cmd_restart(uint8_t *buf, size_t cap)
246{
247 return cmd_frame(buf, cap, 0x00A3, nullptr, 0);
248}
249
250// --- LD2410B-only ----------------------------------------------------------
251size_t pc_ld2410_cmd_bluetooth(uint8_t *buf, size_t cap, bool on)
252{
253 const uint8_t v[2] = {(uint8_t)(on ? 0x01 : 0x00), 0x00}; // value 0x0001 on / 0x0000 off
254 return cmd_frame(buf, cap, 0x00A4, v, 2);
255}
256
257size_t pc_ld2410_cmd_get_mac(uint8_t *buf, size_t cap)
258{
259 static const uint8_t v[2] = {0x01, 0x00}; // value 0x0001
260 return cmd_frame(buf, cap, 0x00A5, v, 2);
261}
262
263size_t pc_ld2410_cmd_set_bt_password(uint8_t *buf, size_t cap, const char password[6])
264{
265 if (!password)
266 {
267 return 0;
268 }
269 // Exactly 6 octets, natural order - the spec's worked example sends "HiLink" as 48 69 4C 69 6E 6B.
270 const uint8_t v[6] = {(uint8_t)password[0], (uint8_t)password[1], (uint8_t)password[2],
271 (uint8_t)password[3], (uint8_t)password[4], (uint8_t)password[5]};
272 return cmd_frame(buf, cap, 0x00A9, v, 6);
273}
274
275// --- command-ACK decoding --------------------------------------------------
276bool pc_ld2410_parse_ack(const uint8_t *f, size_t len, Ld2410Ack *out)
277{
278 // layout: four header bytes, two length bytes, two command-word bytes, two status bytes, optional data, four footer
279 // bytes
280 if (!f || !out || len < 14)
281 {
282 return false;
283 }
284 for (int k = 0; k < 4; k++)
285 {
286 if (f[k] != CMD_HDR[k])
287 {
288 return false;
289 }
290 }
291 size_t dl = (size_t)f[4] | ((size_t)f[5] << 8); // intra-frame length: word + status + data
292 if (dl < 4 || len != 4 + 2 + dl + 4)
293 {
294 return false; // the declared length must account for exactly this frame
295 }
296 for (int k = 0; k < 4; k++)
297 {
298 if (f[6 + dl + k] != CMD_FTR[k])
299 {
300 return false;
301 }
302 }
303 out->command = (uint16_t)((uint16_t)f[6] | ((uint16_t)f[7] << 8));
304 out->status = (uint16_t)((uint16_t)f[8] | ((uint16_t)f[9] << 8));
305 out->payload_len = dl - 4;
306 out->payload = out->payload_len ? f + 10 : nullptr;
307 return true;
308}
309
310bool pc_ld2410_ack_ok(const Ld2410Ack *ack)
311{
312 return ack && ack->status == 0;
313}
314
315bool pc_ld2410_ack_mac(const Ld2410Ack *ack, uint8_t mac[6])
316{
317 if (!ack || !mac || ack->command != 0x01A5 || ack->status != 0 || ack->payload_len < 6 || !ack->payload)
318 {
319 return false;
320 }
321 for (int k = 0; k < 6; k++)
322 {
323 mac[k] = ack->payload[k];
324 }
325 return true;
326}
327
328// ---------------------------------------------------------------------------
329// UART binding
330// ---------------------------------------------------------------------------
331
332#if defined(ARDUINO)
333
334namespace
335{
336// All LD2410 UART-binding state, owned by one instance (internal linkage): the frame stream
337// assembler, the last decoded report, and the have-report flag, grouped so it is one named
338// owner, unreachable from any other translation unit.
339struct Ld2410Ctx
340{
341 Ld2410Stream stream;
342 Ld2410Report last;
343 bool have = false;
344};
345Ld2410Ctx s_ld;
346} // namespace
347
348bool pc_ld2410_begin(int rx_pin, int tx_pin)
349{
350 pc_ld2410_stream_reset(&s_ld.stream);
351 s_ld.have = false;
352 Serial2.begin(PC_LD2410_BAUD, SERIAL_8N1, rx_pin, tx_pin);
353 return true;
354}
355
356bool pc_ld2410_poll()
357{
358 bool fresh = false;
359 while (Serial2.available())
360 {
361 Ld2410Report r;
362 if (pc_ld2410_stream_push(&s_ld.stream, (uint8_t)Serial2.read(), &r))
363 {
364 s_ld.last = r;
365 s_ld.have = true;
366 fresh = true;
367 }
368 }
369 return fresh;
370}
371
373{
374 return s_ld.have ? &s_ld.last : nullptr;
375}
376
377bool pc_ld2410_set_engineering(bool on)
378{
379 uint8_t f[16];
380 size_t n;
381 n = pc_ld2410_cmd_config_enable(f, sizeof(f));
382 Serial2.write(f, n);
383 n = pc_ld2410_cmd_engineering(f, sizeof(f), on);
384 Serial2.write(f, n);
385 n = pc_ld2410_cmd_config_end(f, sizeof(f));
386 Serial2.write(f, n);
387 Serial2.flush();
388 return true;
389}
390
392{
393 uint8_t f[16];
394 size_t n;
395 n = pc_ld2410_cmd_config_enable(f, sizeof(f));
396 Serial2.write(f, n);
397 n = pc_ld2410_cmd_restart(f, sizeof(f));
398 Serial2.write(f, n);
399 n = pc_ld2410_cmd_config_end(f, sizeof(f));
400 Serial2.write(f, n);
401 Serial2.flush();
402 return true;
403}
404
405#else // host build: no UART. The codec above is host-tested.
406
407bool pc_ld2410_begin(int, int)
408{
409 return false;
410}
411bool pc_ld2410_poll()
412{
413 return false;
414}
416{
417 return nullptr;
418}
420{
421 return false;
422}
424{
425 return false;
426}
427
428#endif // ARDUINO
429
430#endif // PC_ENABLE_LD2410
HLK-LD2410 24 GHz mmWave presence / motion radar codec (PC_ENABLE_LD2410).
bool pc_ld2410_ack_ok(const Ld2410Ack *ack)
True if ack reports success (Ld2410Ack::status == 0).
size_t pc_ld2410_cmd_bluetooth(uint8_t *buf, size_t cap, bool on)
LD2410B: turn the Bluetooth radio on (value 0x0001) or off (0x0000). Word 0x00A4.
size_t pc_ld2410_cmd_engineering(uint8_t *buf, size_t cap, bool on)
Enable (0x0062) or disable (0x0063) engineering mode.
size_t pc_ld2410_cmd_get_mac(uint8_t *buf, size_t cap)
LD2410B: query the module's Bluetooth MAC address (word 0x00A5, value 0x0001).
bool pc_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 pc_ld2410_parse_ack(const uint8_t *frame, size_t len, Ld2410Ack *out)
Decode one command-ACK frame (header, intra-frame length, footer and length agreement all checked).
bool pc_ld2410_poll()
Pump the UART through the stream.
bool pc_ld2410_set_engineering(bool on)
Enable/disable engineering mode (brackets the command with enable/end).
size_t pc_ld2410_cmd_set_bt_password(uint8_t *buf, size_t cap, const char password[6])
LD2410B: set the 6-octet Bluetooth control password (word 0x00A9). Takes effect after a restart and s...
bool pc_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....
bool pc_ld2410_present(const Ld2410Report *r)
true if r shows any target (moving or stationary).
#define LD2410_MAX_GATES
Range gates the LD2410 reports energy for in engineering mode (gate 0..8).
Definition ld2410.h:33
size_t pc_ld2410_cmd_restart(uint8_t *buf, size_t cap)
Restart the module (word 0x00A3).
void pc_ld2410_stream_reset(Ld2410Stream *s)
Reset a stream to the syncing state.
uint16_t pc_ld2410_distance_cm(const Ld2410Report *r)
Best available target distance (cm): the moving distance if moving, else stationary.
const Ld2410Report * pc_ld2410_last()
The most recently decoded report, or nullptr before the first one arrives.
size_t pc_ld2410_cmd_config_end(uint8_t *buf, size_t cap)
"End configuration" (word 0x00FE).
#define LD2410_FRAME_MAX
Largest assembled frame: header(4) + len(2) + payload(<=60) + footer(4), rounded up.
Definition ld2410.h:36
bool pc_ld2410_restart()
Restart the module (brackets the command with enable/end).
bool pc_ld2410_begin(int rx_pin, int tx_pin)
Open UART2 at PC_LD2410_BAUD on rx_pin / tx_pin.
bool pc_ld2410_ack_mac(const Ld2410Ack *ack, uint8_t mac[6])
Extract the 6-octet MAC from a get-MAC ACK (word 0x01A5) into mac, in wire order.
size_t pc_ld2410_cmd_config_enable(uint8_t *buf, size_t cap)
"Enable configuration" (word 0x00FF, value 0x0001).
User-facing configuration for ProtoCore.
#define PC_LD2410_BAUD
LD2410 UART baud rate (the module's fixed factory default is 256000).
A decoded command-ACK frame. payload points into the caller's frame (not copied).
Definition ld2410.h:145
uint16_t status
0 = success, 1 = failure.
Definition ld2410.h:147
size_t payload_len
octets at payload.
Definition ld2410.h:149
const uint8_t * payload
command-specific data after the status word (nullptr if none).
Definition ld2410.h:148
uint16_t command
ACK command word: the request word | 0x0100 (e.g. 0x01A5 for get-MAC).
Definition ld2410.h:146
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