ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
hmmd.h
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 hmmd.h
6 * @brief Waveshare HMMD 24 GHz mmWave human micro-motion radar codec (PC_ENABLE_HMMD).
7 *
8 * The HMMD (Waveshare's FMCW micro-motion detection module, built on the S3KM1110 / SXKMxxx0 class
9 * of radar SoC) reports human presence and range over a 115200-baud UART, and additionally drives a
10 * bare GPIO **OUT** pin. It is a close relative of the HLK-LD2410 (`services/ld2410`) and shares its
11 * framing exactly - two frame kinds, the same magic sequences, a little-endian intra-frame length:
12 *
13 * @code
14 * report: F4 F3 F2 F1 | len(2) | detect(1) | distance(2) | gate_energy[16](2 each) | F8 F7 F6 F5
15 * command: FD FC FB FA | len(2) | word(2) | [value] | 04 03 02 01
16 * @endcode
17 *
18 * A report's intra-frame length is @ref PC_HMMD_REPORT_LEN (1 + 2 + 16*2 = 35), so a whole report
19 * frame is 4 + 2 + 35 + 4 = @ref PC_HMMD_FRAME_MAX octets. Everything multi-octet is LITTLE-endian.
20 * Unlike the LD2410 the payload carries no head/tail marker or check byte - the header, footer, and
21 * the length agreeing with the buffer are the whole of the validation.
22 *
23 * Where the LD2410 reports a moving/stationary split with 9 range gates, the HMMD reports a single
24 * detection flag, one distance, and the per-gate energy of 16 gates - it is a micro-motion detector,
25 * so "still person breathing" is the case it is built to catch.
26 *
27 * This codec is pure and host-tested: ::pc_hmmd_parse_report decodes one report frame and
28 * ::HmmdStream reassembles frames byte-by-byte from a UART with resync on noise (no heap, fixed
29 * buffer), mirroring `Ld2410Stream`. The command encoders build the config frames, and
30 * ::pc_hmmd_parse_ack decodes the module's replies.
31 *
32 * The module's GPIO OUT pin is a bare active-high presence line with no protocol at all. Feed it to
33 * @ref PresenceCore from `services/rcwl0516` (the shared one-GPIO presence facade) to get the same
34 * debounced, hold-extended presence the RCWL-0516 gets; that is an application-level wiring choice,
35 * so this service deliberately does not depend on that one.
36 *
37 * Framing, the report payload layout, the command words, and the open/close command-mode encoding
38 * were taken from the public `2Grey/s3km1110` reference library and cross-checked for internal
39 * consistency (its `kMaxFrameLength` of 45 and `kDistanceGateCount` of 16 agree exactly with the
40 * 35-octet report payload derived here). No vendor SDK is used or required.
41 *
42 * @author Douglas Quigg (dstroy0)
43 * @date 2026
44 */
45
46#ifndef PROTOCORE_HMMD_H
47#define PROTOCORE_HMMD_H
48
49#include "protocore_config.h"
50
51#if PC_ENABLE_HMMD
52
53#include <stddef.h>
54#include <stdint.h>
55
56/** @brief Range gates the HMMD reports energy for (gate 0..15). */
57#define PC_HMMD_GATES 16
58
59/** @brief Intra-frame length of a report: detect(1) + distance(2) + 16 gates x 2. */
60#define PC_HMMD_REPORT_LEN 35
61
62/** @brief Largest assembled frame: header(4) + len(2) + payload(35) + footer(4). */
63#define PC_HMMD_FRAME_MAX 45
64
65/** @brief A decoded HMMD target report. */
66struct HmmdReport
67{
68 uint8_t detected; ///< 1 if a target is present.
69 uint16_t distance_cm; ///< target distance (cm); meaningless unless detected.
70 uint16_t gate_energy[PC_HMMD_GATES]; ///< per-gate energy, gate 0..15.
71};
72
73/**
74 * @brief Decode one whole HMMD report frame (header `F4 F3 F2 F1` .. footer `F8 F7 F6 F5`).
75 * Pure - no I/O. Validates the header, the footer, and that the intra-frame length is exactly
76 * @ref PC_HMMD_REPORT_LEN and frames the buffer.
77 * @return true on a valid frame; false on any mismatch or a short buffer.
78 */
79bool pc_hmmd_parse_report(const uint8_t *frame, size_t len, HmmdReport *out);
80
81/** @brief Byte-by-byte report-frame reassembler (fixed buffer, resyncs on noise). */
82struct HmmdStream
83{
84 uint8_t buf[PC_HMMD_FRAME_MAX]; ///< frame under construction
85 uint16_t pos; ///< octets collected so far
86 uint16_t total; ///< expected full-frame length (known after the length field)
87 uint8_t hdr_match; ///< header octets matched while syncing
88 uint8_t phase; ///< 0 sync, 1 length, 2 body
89};
90
91/** @brief Reset a stream to the syncing state. */
92void pc_hmmd_stream_reset(HmmdStream *s);
93
94/**
95 * @brief Feed one received octet. When it completes a valid report frame, fills @p out and returns
96 * true; otherwise false (still syncing / mid-frame / bad frame - it resyncs).
97 */
98bool pc_hmmd_stream_push(HmmdStream *s, uint8_t byte, HmmdReport *out);
99
100/** @brief true if @p r shows a target. */
101bool pc_hmmd_present(const HmmdReport *r);
102
103/** @brief Target distance (cm), or 0 when nothing is detected. */
104uint16_t pc_hmmd_distance_cm(const HmmdReport *r);
105
106// --- Config-command encoders (build a full `FD FC FB FA` .. `04 03 02 01` frame) ---------------
107// Each returns the frame length written, or 0 if @p cap is too small. Commands other than
108// open/close must be bracketed by open/close command mode.
109
110/**
111 * @brief Build an arbitrary command frame: @p word plus @p vlen octets of @p value.
112 *
113 * The named encoders below are thin wrappers over this. It is public because the module's register
114 * and parameter maps are larger than the handful of commands worth naming here.
115 */
116size_t pc_hmmd_cmd_build(uint8_t *buf, size_t cap, uint16_t word, const uint8_t *value, size_t vlen);
117
118/** @brief "Open command mode" (word 0x00FF, value 0x0001). */
119size_t pc_hmmd_cmd_open(uint8_t *buf, size_t cap);
120/** @brief "Close command mode" (word 0x00FE, no value). */
121size_t pc_hmmd_cmd_close(uint8_t *buf, size_t cap);
122/** @brief Read the radar firmware version (word 0x0000). */
123size_t pc_hmmd_cmd_read_firmware(uint8_t *buf, size_t cap);
124/** @brief Read the module serial number (word 0x0011). */
125size_t pc_hmmd_cmd_read_serial(uint8_t *buf, size_t cap);
126/** @brief Read the parameter configuration (word 0x0008). */
127size_t pc_hmmd_cmd_read_config(uint8_t *buf, size_t cap);
128
129/**
130 * @brief Read a register (word 0x0002), with @p vlen octets of caller-supplied selector @p value.
131 *
132 * The selector payload is passed through verbatim rather than modelled: the reference this codec was
133 * built against does not specify the register map, so inventing a layout here would be a guess. The
134 * framing is exact; what goes in the value is the caller's per their module documentation.
135 */
136size_t pc_hmmd_cmd_read_register(uint8_t *buf, size_t cap, const uint8_t *value, size_t vlen);
137
138// --- Command-ACK decoding ----------------------------------------------------------------------
139
140/** @brief A decoded command-ACK frame. @ref payload points into the caller's frame (not copied). */
141struct HmmdAck
142{
143 uint16_t command; ///< the ACK's command word, as sent on the wire.
144 const uint8_t *payload; ///< octets following the command word (nullptr if none).
145 size_t payload_len; ///< octets at @ref payload.
146};
147
148/**
149 * @brief Decode one command-ACK frame (header, intra-frame length, footer and length agreement all
150 * checked). @return false if @p frame is not a well-formed ACK.
151 *
152 * The ACK's payload is handed back whole rather than split into a status word: the reference only
153 * establishes that the ACK echoes the request's command octet, so how many leading octets are a
154 * status is not something this codec asserts.
155 */
156bool pc_hmmd_parse_ack(const uint8_t *frame, size_t len, HmmdAck *out);
157
158/**
159 * @brief True if @p ack is the reply to request @p word.
160 *
161 * Matches on the low octet, which is what the reference verifies. This family conventionally sets
162 * bit 8 in the reply (0x0008 -> 0x0108), but that is not asserted here.
163 */
164bool pc_hmmd_ack_matches(const HmmdAck *ack, uint16_t word);
165
166// --- Binding (no-ops on a host build) ----------------------------------------------------------
167
168/** @brief Open UART2 at PC_HMMD_BAUD on @p rx_pin / @p tx_pin. @return true on ESP32. */
169bool pc_hmmd_begin(int rx_pin, int tx_pin);
170
171/** @brief Pump the UART through the stream. @return true if a fresh report was decoded. */
172bool pc_hmmd_poll();
173
174/** @brief The most recently decoded report, or nullptr before the first one arrives. */
175const HmmdReport *pc_hmmd_last();
176
177#endif // PC_ENABLE_HMMD
178#endif // PROTOCORE_HMMD_H
User-facing configuration for ProtoCore.