ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
hlk_fm22x.cpp
Go to the documentation of this file.
1#include "hlk_fm22x.h"
2#include "esphome/core/log.h"
4#include <cinttypes>
5
7
8ESPHOME_LOG_TAG(TAG, "hlk_fm22x");
9static constexpr uint32_t PAYLOAD_TIMEOUT_MS = 20;
10
12 ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
13 this->set_enrolling_(false);
14 this->discard_available();
15 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
16}
17
20 if (this->wait_cycles_ > 600) {
21 ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
23 this->mark_failed();
24 } else {
25 this->reset();
26 }
27 }
28 }
29 this->recv_command_();
30}
31
33 if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
34 ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
35 return;
36 }
37 ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str());
38 std::array<uint8_t, 35> data{};
39 data[0] = 0; // admin
40 std::copy(name.begin(), name.end(), data.begin() + 1);
41 // Remaining bytes are already zero-initialized
42 data[33] = (uint8_t) direction;
43 data[34] = 10; // timeout
44 this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size());
45 this->set_enrolling_(true);
46}
47
49 ESP_LOGI(TAG, "Verify face");
50 static const uint8_t DATA[] = {0, 0};
51 this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA));
52}
53
54void HlkFm22xComponent::delete_face(int16_t face_id) {
55 ESP_LOGI(TAG, "Deleting face in slot %d", face_id);
56 const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)};
57 this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data));
58}
59
61 ESP_LOGI(TAG, "Deleting all stored faces");
63}
64
66 ESP_LOGD(TAG, "Getting face count");
68}
69
71 ESP_LOGI(TAG, "Resetting module");
73 this->wait_cycles_ = 0;
74 this->set_enrolling_(false);
76}
77
78void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) {
79 ESP_LOGV(TAG, "Send command: 0x%.2X", command);
81 ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_);
82 return;
83 }
84 this->wait_cycles_ = 0;
85 this->active_command_ = command;
86 this->discard_available();
87 this->write((uint8_t) (START_CODE >> 8));
88 this->write((uint8_t) (START_CODE & 0xFF));
89 this->write((uint8_t) command);
90 uint16_t data_size = size;
91 this->write((uint8_t) (data_size >> 8));
92 this->write((uint8_t) (data_size & 0xFF));
93
94 uint8_t checksum = 0;
95 checksum ^= (uint8_t) command;
96 checksum ^= (data_size >> 8);
97 checksum ^= (data_size & 0xFF);
98 for (size_t i = 0; i < size; i++) {
99 this->write(data[i]);
100 checksum ^= data[i];
101 }
102
103 this->write(checksum);
104 this->active_command_ = command;
105 this->wait_cycles_ = 0;
106}
107
109 uint8_t byte, checksum = 0;
110 uint16_t length = 0;
111
112 if (this->available() < 7) {
113 ++this->wait_cycles_;
114 return;
115 }
116 this->wait_cycles_ = 0;
117
118 if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) {
119 ESP_LOGE(TAG, "Invalid start code");
120 return;
121 }
122
123 byte = this->read();
124 checksum ^= byte;
125 HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte;
126
127 byte = this->read();
128 checksum ^= byte;
129 length = byte << 8;
130 byte = this->read();
131 checksum ^= byte;
132 length |= byte;
133
134 // Wait for remaining data (payload + checksum) to arrive.
135 // Header bytes are already consumed, so we must finish reading this message.
136 uint32_t start = millis();
137 while (this->available() < length + 1) {
138 if (millis() - start > PAYLOAD_TIMEOUT_MS) {
139 ESP_LOGE(TAG, "Timeout waiting for payload (%u bytes)", length);
140 // Drain any partial payload bytes to resync the parser
141 this->discard_available();
142 return;
143 }
144 delay(1);
145 }
146
147 // Read up to buffer size; discard excess bytes while still computing checksum
148 // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes)
149 // but handlers only need the first few bytes
150 size_t to_store = std::min(static_cast<size_t>(length), HLK_FM22X_MAX_RESPONSE_SIZE);
151 for (uint16_t idx = 0; idx < length; ++idx) {
152 byte = this->read();
153 checksum ^= byte;
154 if (idx < to_store) {
155 this->recv_buf_[idx] = byte;
156 }
157 }
158
159 ESP_LOGV_HEX(TAG, this->recv_buf_.data(), to_store, HLK_FM22X_MAX_RESPONSE_SIZE, "Recv type: 0x%.2X, data: %s",
160 response_type);
161
162 byte = this->read();
163 if (byte != checksum) {
164 ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte);
165 return;
166 }
167 switch (response_type) {
169 this->handle_note_(this->recv_buf_.data(), to_store);
170 break;
172 this->handle_reply_(this->recv_buf_.data(), to_store);
173 break;
174 default:
175 ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
176 break;
177 }
178}
179
180void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
181 if (length < 1) {
182 ESP_LOGE(TAG, "Empty note data");
183 return;
184 }
185 switch (data[0]) {
187 if (length < 17) {
188 ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
189 break;
190 }
191 {
192 int16_t info[8];
193 uint8_t offset = 1;
194 for (int16_t &i : info) {
195 i = encode_value_le<int16_t>(&data[offset]);
196 offset += 2;
197 }
198 ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d",
199 info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
200 this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
201 }
202 break;
204 ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
205 switch (this->active_command_) {
207 this->set_enrolling_(false);
209 break;
212 break;
213 default:
214 break;
215 }
217 this->wait_cycles_ = 0;
218 break;
219 default:
220 ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]);
221 break;
222 }
223}
224
225void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
226 auto expected = this->active_command_;
228 if (length < 2) {
229 ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
230 return;
231 }
232 if (data[0] != (uint8_t) expected) {
233 ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
234 return;
235 }
236
237 if (data[1] != HlkFm22xResult::SUCCEEDED) {
238 ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]);
239 switch (expected) {
241 this->set_enrolling_(false);
242 this->enrollment_failed_callback_.call(data[1]);
243 break;
245 if (data[1] == HlkFm22xResult::REJECTED) {
247 } else {
248 this->face_scan_invalid_callback_.call(data[1]);
249 }
250 break;
251 default:
252 break;
253 }
254 return;
255 }
256 switch (expected) {
258 if (length < 4 + HLK_FM22X_NAME_SIZE) {
259 ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length);
260 break;
261 }
262 int16_t face_id = encode_value<int16_t>(&data[2]);
263 const char *name_ptr = reinterpret_cast<const char *>(data + 4);
264 ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
265 if (this->last_face_id_sensor_ != nullptr) {
266 this->last_face_id_sensor_->publish_state(face_id);
267 }
268 if (this->last_face_name_text_sensor_ != nullptr) {
269 this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
270 }
271 this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
272 break;
273 }
275 int16_t face_id = encode_value<int16_t>(&data[2]);
277 ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction);
278 this->enrollment_done_callback_.call(face_id, (uint8_t) direction);
279 this->set_enrolling_(false);
280 this->defer([this]() { this->get_face_count_(); });
281 break;
282 }
284 if (this->status_sensor_ != nullptr) {
285 this->status_sensor_->publish_state(data[2]);
286 }
287 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
288 break;
290 if (this->version_text_sensor_ != nullptr && length > 2) {
291 this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
292 }
293 this->defer([this]() { this->get_face_count_(); });
294 break;
296 if (this->face_count_sensor_ != nullptr) {
297 this->face_count_sensor_->publish_state(data[2]);
298 }
299 break;
301 ESP_LOGI(TAG, "Deleted face");
302 break;
304 ESP_LOGI(TAG, "Deleted all faces");
305 break;
307 ESP_LOGI(TAG, "Module reset");
308 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
309 break;
310 default:
311 ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_);
312 break;
313 }
314}
315
317 if (this->enrolling_binary_sensor_ != nullptr) {
318 this->enrolling_binary_sensor_->publish_state(enrolling);
319 }
320}
321
323 ESP_LOGCONFIG(TAG, "HLK_FM22X:");
324 LOG_UPDATE_INTERVAL(this);
325 if (this->version_text_sensor_) {
326 LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
327 ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str());
328 }
329 if (this->enrolling_binary_sensor_) {
330 LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
331 ESP_LOGCONFIG(TAG, " Current Value: %s",
332 this->enrolling_binary_sensor_->state ? LOG_STR_LITERAL("ON") : LOG_STR_LITERAL("OFF"));
333 }
334 if (this->face_count_sensor_) {
335 LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
336 ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state());
337 }
338 if (this->status_sensor_) {
339 LOG_SENSOR(" ", "Status", this->status_sensor_);
340 ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state());
341 }
342 if (this->last_face_id_sensor_) {
343 LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_);
344 ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state());
345 }
346 if (this->last_face_name_text_sensor_) {
347 LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_);
348 ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str());
349 }
350}
351
352} // namespace esphome::hlk_fm22x
uint8_t checksum
Definition bl0906.h:3
void mark_failed()
Mark this component as failed.
void defer(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call with a const char* name.
void publish_state(bool new_state)
Publish a new state to the front-end.
bool state
The current state of this binary sensor. Also used as the backing storage for StatefulEntityBase.
void delete_face(int16_t face_id)
Definition hlk_fm22x.cpp:54
void handle_reply_(const uint8_t *data, size_t length)
text_sensor::TextSensor * last_face_name_text_sensor_
Definition hlk_fm22x.h:134
CallbackManager< void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> face_info_callback_
Definition hlk_fm22x.h:139
void enroll_face(const std::string &name, HlkFm22xFaceDirection direction)
Definition hlk_fm22x.cpp:32
void send_command_(HlkFm22xCommand command, const uint8_t *data=nullptr, size_t size=0)
Definition hlk_fm22x.cpp:78
CallbackManager< void(uint8_t)> face_scan_invalid_callback_
Definition hlk_fm22x.h:136
std::array< uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE > recv_buf_
Definition hlk_fm22x.h:127
CallbackManager< void()> face_scan_unmatched_callback_
Definition hlk_fm22x.h:138
CallbackManager< void(int16_t, uint8_t)> enrollment_done_callback_
Definition hlk_fm22x.h:140
CallbackManager< void(uint8_t)> enrollment_failed_callback_
Definition hlk_fm22x.h:141
text_sensor::TextSensor * version_text_sensor_
Definition hlk_fm22x.h:135
void handle_note_(const uint8_t *data, size_t length)
CallbackManager< void(int16_t, std::string)> face_scan_matched_callback_
Definition hlk_fm22x.h:137
binary_sensor::BinarySensor * enrolling_binary_sensor_
Definition hlk_fm22x.h:133
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
float get_state() const
Getter-syntax for .state.
Definition sensor.h:98
const std::string & get_state() const
Getter-syntax for .state.
void publish_state(const std::string &state)
Definition text_sensor.h:42
void discard_available()
Read and drop every byte waiting in the receive buffer.
Definition uart.cpp:36
size_t write(uint8_t data)
Definition uart.h:105
FanDirection direction
Definition fan.h:5
ESPHOME_LOG_TAG(TAG, "hlk_fm22x")
uint16_t size
Definition helpers.cpp:34
constexpr T encode_value_le(const uint8_t *bytes)
Encode a value from its constituent bytes (from least to most significant) in an array with length si...
Definition helpers.h:1039
constexpr T encode_value(const uint8_t *bytes)
Encode a value from its constituent bytes (from most to least significant) in an array with length si...
Definition helpers.h:1019
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
uint16_t length
Definition tt21100.cpp:0