ESPHome 2026.2.1
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";
9
11 ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
12 this->set_enrolling_(false);
13 while (this->available() > 0) {
14 this->read();
15 }
16 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
17}
18
21 if (this->wait_cycles_ > 600) {
22 ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
24 this->mark_failed();
25 } else {
26 this->reset();
27 }
28 }
29 }
30 this->recv_command_();
31}
32
34 if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
35 ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
36 return;
37 }
38 ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str());
39 std::array<uint8_t, 35> data{};
40 data[0] = 0; // admin
41 std::copy(name.begin(), name.end(), data.begin() + 1);
42 // Remaining bytes are already zero-initialized
43 data[33] = (uint8_t) direction;
44 data[34] = 10; // timeout
45 this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size());
46 this->set_enrolling_(true);
47}
48
50 ESP_LOGI(TAG, "Verify face");
51 static const uint8_t DATA[] = {0, 0};
52 this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA));
53}
54
55void HlkFm22xComponent::delete_face(int16_t face_id) {
56 ESP_LOGI(TAG, "Deleting face in slot %d", face_id);
57 const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)};
58 this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data));
59}
60
62 ESP_LOGI(TAG, "Deleting all stored faces");
64}
65
67 ESP_LOGD(TAG, "Getting face count");
69}
70
72 ESP_LOGI(TAG, "Resetting module");
74 this->wait_cycles_ = 0;
75 this->set_enrolling_(false);
77}
78
79void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) {
80 ESP_LOGV(TAG, "Send command: 0x%.2X", command);
82 ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_);
83 return;
84 }
85 this->wait_cycles_ = 0;
86 this->active_command_ = command;
87 while (this->available() > 0)
88 this->read();
89 this->write((uint8_t) (START_CODE >> 8));
90 this->write((uint8_t) (START_CODE & 0xFF));
91 this->write((uint8_t) command);
92 uint16_t data_size = size;
93 this->write((uint8_t) (data_size >> 8));
94 this->write((uint8_t) (data_size & 0xFF));
95
96 uint8_t checksum = 0;
97 checksum ^= (uint8_t) command;
98 checksum ^= (data_size >> 8);
99 checksum ^= (data_size & 0xFF);
100 for (size_t i = 0; i < size; i++) {
101 this->write(data[i]);
102 checksum ^= data[i];
103 }
104
105 this->write(checksum);
106 this->active_command_ = command;
107 this->wait_cycles_ = 0;
108}
109
111 uint8_t byte, checksum = 0;
112 uint16_t length = 0;
113
114 if (this->available() < 7) {
115 ++this->wait_cycles_;
116 return;
117 }
118 this->wait_cycles_ = 0;
119
120 if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) {
121 ESP_LOGE(TAG, "Invalid start code");
122 return;
123 }
124
125 byte = this->read();
126 checksum ^= byte;
127 HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte;
128
129 byte = this->read();
130 checksum ^= byte;
131 length = byte << 8;
132 byte = this->read();
133 checksum ^= byte;
134 length |= byte;
135
136 if (length > HLK_FM22X_MAX_RESPONSE_SIZE) {
137 ESP_LOGE(TAG, "Response too large: %u bytes", length);
138 // Discard exactly the remaining payload and checksum for this frame
139 for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i)
140 this->read();
141 return;
142 }
143
144 for (uint16_t idx = 0; idx < length; ++idx) {
145 byte = this->read();
146 checksum ^= byte;
147 this->recv_buf_[idx] = byte;
148 }
149
150#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
151 char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
152 ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type,
153 format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length));
154#endif
155
156 byte = this->read();
157 if (byte != checksum) {
158 ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte);
159 return;
160 }
161 switch (response_type) {
163 this->handle_note_(this->recv_buf_.data(), length);
164 break;
166 this->handle_reply_(this->recv_buf_.data(), length);
167 break;
168 default:
169 ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
170 break;
171 }
172}
173
174void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
175 if (length < 1) {
176 ESP_LOGE(TAG, "Empty note data");
177 return;
178 }
179 switch (data[0]) {
181 if (length < 17) {
182 ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
183 break;
184 }
185 {
186 int16_t info[8];
187 uint8_t offset = 1;
188 for (int16_t &i : info) {
189 i = ((int16_t) data[offset + 1] << 8) | data[offset];
190 offset += 2;
191 }
192 ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d",
193 info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
194 this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
195 }
196 break;
198 ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
199 switch (this->active_command_) {
201 this->set_enrolling_(false);
203 break;
206 break;
207 default:
208 break;
209 }
211 this->wait_cycles_ = 0;
212 break;
213 default:
214 ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]);
215 break;
216 }
217}
218
219void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
220 auto expected = this->active_command_;
222 if (length < 2) {
223 ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
224 return;
225 }
226 if (data[0] != (uint8_t) expected) {
227 ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
228 return;
229 }
230
231 if (data[1] != HlkFm22xResult::SUCCESS) {
232 ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]);
233 switch (expected) {
235 this->set_enrolling_(false);
236 this->enrollment_failed_callback_.call(data[1]);
237 break;
239 if (data[1] == HlkFm22xResult::REJECTED) {
241 } else {
242 this->face_scan_invalid_callback_.call(data[1]);
243 }
244 break;
245 default:
246 break;
247 }
248 return;
249 }
250 switch (expected) {
252 if (length < 4 + HLK_FM22X_NAME_SIZE) {
253 ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length);
254 break;
255 }
256 int16_t face_id = ((int16_t) data[2] << 8) | data[3];
257 const char *name_ptr = reinterpret_cast<const char *>(data + 4);
258 ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
259 if (this->last_face_id_sensor_ != nullptr) {
260 this->last_face_id_sensor_->publish_state(face_id);
261 }
262 if (this->last_face_name_text_sensor_ != nullptr) {
263 this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
264 }
265 this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
266 break;
267 }
269 int16_t face_id = ((int16_t) data[2] << 8) | data[3];
271 ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction);
272 this->enrollment_done_callback_.call(face_id, (uint8_t) direction);
273 this->set_enrolling_(false);
274 this->defer([this]() { this->get_face_count_(); });
275 break;
276 }
278 if (this->status_sensor_ != nullptr) {
279 this->status_sensor_->publish_state(data[2]);
280 }
281 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
282 break;
284 if (this->version_text_sensor_ != nullptr && length > 2) {
285 this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
286 }
287 this->defer([this]() { this->get_face_count_(); });
288 break;
290 if (this->face_count_sensor_ != nullptr) {
291 this->face_count_sensor_->publish_state(data[2]);
292 }
293 break;
295 ESP_LOGI(TAG, "Deleted face");
296 break;
298 ESP_LOGI(TAG, "Deleted all faces");
299 break;
301 ESP_LOGI(TAG, "Module reset");
302 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
303 break;
304 default:
305 ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_);
306 break;
307 }
308}
309
311 if (this->enrolling_binary_sensor_ != nullptr) {
312 this->enrolling_binary_sensor_->publish_state(enrolling);
313 }
314}
315
317 ESP_LOGCONFIG(TAG, "HLK_FM22X:");
318 LOG_UPDATE_INTERVAL(this);
319 if (this->version_text_sensor_) {
320 LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
321 ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str());
322 }
323 if (this->enrolling_binary_sensor_) {
324 LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
325 ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
326 }
327 if (this->face_count_sensor_) {
328 LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
329 ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state());
330 }
331 if (this->status_sensor_) {
332 LOG_SENSOR(" ", "Status", this->status_sensor_);
333 ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state());
334 }
335 if (this->last_face_id_sensor_) {
336 LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_);
337 ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state());
338 }
339 if (this->last_face_name_text_sensor_) {
340 LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_);
341 ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str());
342 }
343}
344
345} // namespace esphome::hlk_fm22x
uint8_t checksum
Definition bl0906.h:3
virtual 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:479
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:55
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:33
void send_command_(HlkFm22xCommand command, const uint8_t *data=nullptr, size_t size=0)
Definition hlk_fm22x.cpp:79
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:112
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:729
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:978
uint16_t length
Definition tt21100.cpp:0