ESPHome 2026.3.3
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
8static const char *const 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 while (this->available() > 0) {
15 this->read();
16 }
17 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
18}
19
22 if (this->wait_cycles_ > 600) {
23 ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
25 this->mark_failed();
26 } else {
27 this->reset();
28 }
29 }
30 }
31 this->recv_command_();
32}
33
35 if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
36 ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
37 return;
38 }
39 ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str());
40 std::array<uint8_t, 35> data{};
41 data[0] = 0; // admin
42 std::copy(name.begin(), name.end(), data.begin() + 1);
43 // Remaining bytes are already zero-initialized
44 data[33] = (uint8_t) direction;
45 data[34] = 10; // timeout
46 this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size());
47 this->set_enrolling_(true);
48}
49
51 ESP_LOGI(TAG, "Verify face");
52 static const uint8_t DATA[] = {0, 0};
53 this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA));
54}
55
56void HlkFm22xComponent::delete_face(int16_t face_id) {
57 ESP_LOGI(TAG, "Deleting face in slot %d", face_id);
58 const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)};
59 this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data));
60}
61
63 ESP_LOGI(TAG, "Deleting all stored faces");
65}
66
68 ESP_LOGD(TAG, "Getting face count");
70}
71
73 ESP_LOGI(TAG, "Resetting module");
75 this->wait_cycles_ = 0;
76 this->set_enrolling_(false);
78}
79
80void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) {
81 ESP_LOGV(TAG, "Send command: 0x%.2X", command);
83 ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_);
84 return;
85 }
86 this->wait_cycles_ = 0;
87 this->active_command_ = command;
88 while (this->available() > 0)
89 this->read();
90 this->write((uint8_t) (START_CODE >> 8));
91 this->write((uint8_t) (START_CODE & 0xFF));
92 this->write((uint8_t) command);
93 uint16_t data_size = size;
94 this->write((uint8_t) (data_size >> 8));
95 this->write((uint8_t) (data_size & 0xFF));
96
97 uint8_t checksum = 0;
98 checksum ^= (uint8_t) command;
99 checksum ^= (data_size >> 8);
100 checksum ^= (data_size & 0xFF);
101 for (size_t i = 0; i < size; i++) {
102 this->write(data[i]);
103 checksum ^= data[i];
104 }
105
106 this->write(checksum);
107 this->active_command_ = command;
108 this->wait_cycles_ = 0;
109}
110
112 uint8_t byte, checksum = 0;
113 uint16_t length = 0;
114
115 if (this->available() < 7) {
116 ++this->wait_cycles_;
117 return;
118 }
119 this->wait_cycles_ = 0;
120
121 if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) {
122 ESP_LOGE(TAG, "Invalid start code");
123 return;
124 }
125
126 byte = this->read();
127 checksum ^= byte;
128 HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte;
129
130 byte = this->read();
131 checksum ^= byte;
132 length = byte << 8;
133 byte = this->read();
134 checksum ^= byte;
135 length |= byte;
136
137 // Wait for remaining data (payload + checksum) to arrive.
138 // Header bytes are already consumed, so we must finish reading this message.
139 uint32_t start = millis();
140 while (this->available() < length + 1) {
141 if (millis() - start > PAYLOAD_TIMEOUT_MS) {
142 ESP_LOGE(TAG, "Timeout waiting for payload (%u bytes)", length);
143 // Drain any partial payload bytes to resync the parser
144 while (this->available() > 0) {
145 this->read();
146 }
147 return;
148 }
149 delay(1);
150 }
151
152 // Read up to buffer size; discard excess bytes while still computing checksum
153 // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes)
154 // but handlers only need the first few bytes
155 size_t to_store = std::min(static_cast<size_t>(length), HLK_FM22X_MAX_RESPONSE_SIZE);
156 for (uint16_t idx = 0; idx < length; ++idx) {
157 byte = this->read();
158 checksum ^= byte;
159 if (idx < to_store) {
160 this->recv_buf_[idx] = byte;
161 }
162 }
163
164#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
165 char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
166 ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type,
167 format_hex_pretty_to(hex_buf, this->recv_buf_.data(), to_store));
168#endif
169
170 byte = this->read();
171 if (byte != checksum) {
172 ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte);
173 return;
174 }
175 switch (response_type) {
177 this->handle_note_(this->recv_buf_.data(), to_store);
178 break;
180 this->handle_reply_(this->recv_buf_.data(), to_store);
181 break;
182 default:
183 ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
184 break;
185 }
186}
187
188void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
189 if (length < 1) {
190 ESP_LOGE(TAG, "Empty note data");
191 return;
192 }
193 switch (data[0]) {
195 if (length < 17) {
196 ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
197 break;
198 }
199 {
200 int16_t info[8];
201 uint8_t offset = 1;
202 for (int16_t &i : info) {
203 i = ((int16_t) data[offset + 1] << 8) | data[offset];
204 offset += 2;
205 }
206 ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d",
207 info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
208 this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
209 }
210 break;
212 ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
213 switch (this->active_command_) {
215 this->set_enrolling_(false);
217 break;
220 break;
221 default:
222 break;
223 }
225 this->wait_cycles_ = 0;
226 break;
227 default:
228 ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]);
229 break;
230 }
231}
232
233void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
234 auto expected = this->active_command_;
236 if (length < 2) {
237 ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
238 return;
239 }
240 if (data[0] != (uint8_t) expected) {
241 ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
242 return;
243 }
244
245 if (data[1] != HlkFm22xResult::SUCCESS) {
246 ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]);
247 switch (expected) {
249 this->set_enrolling_(false);
250 this->enrollment_failed_callback_.call(data[1]);
251 break;
253 if (data[1] == HlkFm22xResult::REJECTED) {
255 } else {
256 this->face_scan_invalid_callback_.call(data[1]);
257 }
258 break;
259 default:
260 break;
261 }
262 return;
263 }
264 switch (expected) {
266 if (length < 4 + HLK_FM22X_NAME_SIZE) {
267 ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length);
268 break;
269 }
270 int16_t face_id = ((int16_t) data[2] << 8) | data[3];
271 const char *name_ptr = reinterpret_cast<const char *>(data + 4);
272 ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
273 if (this->last_face_id_sensor_ != nullptr) {
274 this->last_face_id_sensor_->publish_state(face_id);
275 }
276 if (this->last_face_name_text_sensor_ != nullptr) {
277 this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
278 }
279 this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
280 break;
281 }
283 int16_t face_id = ((int16_t) data[2] << 8) | data[3];
285 ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction);
286 this->enrollment_done_callback_.call(face_id, (uint8_t) direction);
287 this->set_enrolling_(false);
288 this->defer([this]() { this->get_face_count_(); });
289 break;
290 }
292 if (this->status_sensor_ != nullptr) {
293 this->status_sensor_->publish_state(data[2]);
294 }
295 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
296 break;
298 if (this->version_text_sensor_ != nullptr && length > 2) {
299 this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
300 }
301 this->defer([this]() { this->get_face_count_(); });
302 break;
304 if (this->face_count_sensor_ != nullptr) {
305 this->face_count_sensor_->publish_state(data[2]);
306 }
307 break;
309 ESP_LOGI(TAG, "Deleted face");
310 break;
312 ESP_LOGI(TAG, "Deleted all faces");
313 break;
315 ESP_LOGI(TAG, "Module reset");
316 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
317 break;
318 default:
319 ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_);
320 break;
321 }
322}
323
325 if (this->enrolling_binary_sensor_ != nullptr) {
326 this->enrolling_binary_sensor_->publish_state(enrolling);
327 }
328}
329
331 ESP_LOGCONFIG(TAG, "HLK_FM22X:");
332 LOG_UPDATE_INTERVAL(this);
333 if (this->version_text_sensor_) {
334 LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
335 ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str());
336 }
337 if (this->enrolling_binary_sensor_) {
338 LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
339 ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
340 }
341 if (this->face_count_sensor_) {
342 LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
343 ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state());
344 }
345 if (this->status_sensor_) {
346 LOG_SENSOR(" ", "Status", this->status_sensor_);
347 ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state());
348 }
349 if (this->last_face_id_sensor_) {
350 LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_);
351 ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state());
352 }
353 if (this->last_face_name_text_sensor_) {
354 LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_);
355 ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str());
356 }
357}
358
359} // namespace esphome::hlk_fm22x
uint8_t checksum
Definition bl0906.h:3
void mark_failed()
Mark this component as failed.
ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") void defer(const std voi defer)(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call.
Definition component.h:501
void publish_state(bool new_state)
Publish a new state to the front-end.
void delete_face(int16_t face_id)
Definition hlk_fm22x.cpp:56
void handle_reply_(const uint8_t *data, size_t length)
text_sensor::TextSensor * last_face_name_text_sensor_
Definition hlk_fm22x.h:135
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:140
void enroll_face(const std::string &name, HlkFm22xFaceDirection direction)
Definition hlk_fm22x.cpp:34
void send_command_(HlkFm22xCommand command, const uint8_t *data=nullptr, size_t size=0)
Definition hlk_fm22x.cpp:80
CallbackManager< void(uint8_t)> face_scan_invalid_callback_
Definition hlk_fm22x.h:137
std::array< uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE > recv_buf_
Definition hlk_fm22x.h:128
CallbackManager< void()> face_scan_unmatched_callback_
Definition hlk_fm22x.h:139
CallbackManager< void(int16_t, uint8_t)> enrollment_done_callback_
Definition hlk_fm22x.h:141
CallbackManager< void(uint8_t)> enrollment_failed_callback_
Definition hlk_fm22x.h:142
text_sensor::TextSensor * version_text_sensor_
Definition hlk_fm22x.h:136
void handle_note_(const uint8_t *data, size_t length)
CallbackManager< void(int16_t, std::string)> face_scan_matched_callback_
Definition hlk_fm22x.h:138
binary_sensor::BinarySensor * enrolling_binary_sensor_
Definition hlk_fm22x.h:134
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.cpp:118
const std::string & get_state() const
Getter-syntax for .state.
void publish_state(const std::string &state)
size_t write(uint8_t data)
Definition uart.h:57
FanDirection direction
Definition fan.h:5
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:353
size_t size
Definition helpers.h:929
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1200
void HOT delay(uint32_t ms)
Definition core.cpp:28
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
static void uint32_t
uint16_t length
Definition tt21100.cpp:0