ESPHome 2026.1.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 <array>
5#include <cinttypes>
6
8
9static const char *const TAG = "hlk_fm22x";
10
11// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name)
12static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
13
15 ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
16 this->set_enrolling_(false);
17 while (this->available()) {
18 this->read();
19 }
20 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
21}
22
25 if (this->wait_cycles_ > 600) {
26 ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
28 this->mark_failed();
29 } else {
30 this->reset();
31 }
32 }
33 }
34 this->recv_command_();
35}
36
38 if (name.length() > 31) {
39 ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
40 return;
41 }
42 ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str());
43 std::array<uint8_t, 35> data{};
44 data[0] = 0; // admin
45 std::copy(name.begin(), name.end(), data.begin() + 1);
46 // Remaining bytes are already zero-initialized
47 data[33] = (uint8_t) direction;
48 data[34] = 10; // timeout
49 this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size());
50 this->set_enrolling_(true);
51}
52
54 ESP_LOGI(TAG, "Verify face");
55 static const uint8_t DATA[] = {0, 0};
56 this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA));
57}
58
59void HlkFm22xComponent::delete_face(int16_t face_id) {
60 ESP_LOGI(TAG, "Deleting face in slot %d", face_id);
61 const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)};
62 this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data));
63}
64
66 ESP_LOGI(TAG, "Deleting all stored faces");
68}
69
71 ESP_LOGD(TAG, "Getting face count");
73}
74
76 ESP_LOGI(TAG, "Resetting module");
78 this->wait_cycles_ = 0;
79 this->set_enrolling_(false);
81}
82
83void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) {
84 ESP_LOGV(TAG, "Send command: 0x%.2X", command);
86 ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_);
87 return;
88 }
89 this->wait_cycles_ = 0;
90 this->active_command_ = command;
91 while (this->available())
92 this->read();
93 this->write((uint8_t) (START_CODE >> 8));
94 this->write((uint8_t) (START_CODE & 0xFF));
95 this->write((uint8_t) command);
96 uint16_t data_size = size;
97 this->write((uint8_t) (data_size >> 8));
98 this->write((uint8_t) (data_size & 0xFF));
99
100 uint8_t checksum = 0;
101 checksum ^= (uint8_t) command;
102 checksum ^= (data_size >> 8);
103 checksum ^= (data_size & 0xFF);
104 for (size_t i = 0; i < size; i++) {
105 this->write(data[i]);
106 checksum ^= data[i];
107 }
108
109 this->write(checksum);
110 this->active_command_ = command;
111 this->wait_cycles_ = 0;
112}
113
115 uint8_t byte, checksum = 0;
116 uint16_t length = 0;
117
118 if (this->available() < 7) {
119 ++this->wait_cycles_;
120 return;
121 }
122 this->wait_cycles_ = 0;
123
124 if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) {
125 ESP_LOGE(TAG, "Invalid start code");
126 return;
127 }
128
129 byte = this->read();
130 checksum ^= byte;
131 HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte;
132
133 byte = this->read();
134 checksum ^= byte;
135 length = byte << 8;
136 byte = this->read();
137 checksum ^= byte;
138 length |= byte;
139
140 std::vector<uint8_t> data;
141 data.reserve(length);
142 for (uint16_t idx = 0; idx < length; ++idx) {
143 byte = this->read();
144 checksum ^= byte;
145 data.push_back(byte);
146 }
147
148#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
149 char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
150 ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size()));
151#endif
152
153 byte = this->read();
154 if (byte != checksum) {
155 ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte);
156 return;
157 }
158 switch (response_type) {
160 this->handle_note_(data);
161 break;
163 this->handle_reply_(data);
164 break;
165 default:
166 ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
167 break;
168 }
169}
170
171void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
172 switch (data[0]) {
174 if (data.size() < 17) {
175 ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
176 break;
177 }
178 {
179 int16_t info[8];
180 uint8_t offset = 1;
181 for (int16_t &i : info) {
182 i = ((int16_t) data[offset + 1] << 8) | data[offset];
183 offset += 2;
184 }
185 ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d",
186 info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
187 this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
188 }
189 break;
191 ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
192 switch (this->active_command_) {
194 this->set_enrolling_(false);
196 break;
199 break;
200 default:
201 break;
202 }
204 this->wait_cycles_ = 0;
205 break;
206 default:
207 ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]);
208 break;
209 }
210}
211
212void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
213 auto expected = this->active_command_;
215 if (data[0] != (uint8_t) expected) {
216 ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
217 return;
218 }
219
220 if (data[1] != HlkFm22xResult::SUCCESS) {
221 ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]);
222 switch (expected) {
224 this->set_enrolling_(false);
225 this->enrollment_failed_callback_.call(data[1]);
226 break;
228 if (data[1] == HlkFm22xResult::REJECTED) {
230 } else {
231 this->face_scan_invalid_callback_.call(data[1]);
232 }
233 break;
234 default:
235 break;
236 }
237 return;
238 }
239 switch (expected) {
241 int16_t face_id = ((int16_t) data[2] << 8) | data[3];
242 std::string name(data.begin() + 4, data.begin() + 36);
243 ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
244 if (this->last_face_id_sensor_ != nullptr) {
245 this->last_face_id_sensor_->publish_state(face_id);
246 }
247 if (this->last_face_name_text_sensor_ != nullptr) {
249 }
250 this->face_scan_matched_callback_.call(face_id, name);
251 break;
252 }
254 int16_t face_id = ((int16_t) data[2] << 8) | data[3];
256 ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction);
257 this->enrollment_done_callback_.call(face_id, (uint8_t) direction);
258 this->set_enrolling_(false);
259 this->defer([this]() { this->get_face_count_(); });
260 break;
261 }
263 if (this->status_sensor_ != nullptr) {
264 this->status_sensor_->publish_state(data[2]);
265 }
266 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
267 break;
269 if (this->version_text_sensor_ != nullptr) {
270 std::string version(data.begin() + 2, data.end());
271 this->version_text_sensor_->publish_state(version);
272 }
273 this->defer([this]() { this->get_face_count_(); });
274 break;
276 if (this->face_count_sensor_ != nullptr) {
277 this->face_count_sensor_->publish_state(data[2]);
278 }
279 break;
281 ESP_LOGI(TAG, "Deleted face");
282 break;
284 ESP_LOGI(TAG, "Deleted all faces");
285 break;
287 ESP_LOGI(TAG, "Module reset");
288 this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
289 break;
290 default:
291 ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_);
292 break;
293 }
294}
295
297 if (this->enrolling_binary_sensor_ != nullptr) {
298 this->enrolling_binary_sensor_->publish_state(enrolling);
299 }
300}
301
303 ESP_LOGCONFIG(TAG, "HLK_FM22X:");
304 LOG_UPDATE_INTERVAL(this);
305 if (this->version_text_sensor_) {
306 LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
307 ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str());
308 }
309 if (this->enrolling_binary_sensor_) {
310 LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
311 ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
312 }
313 if (this->face_count_sensor_) {
314 LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
315 ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state());
316 }
317 if (this->status_sensor_) {
318 LOG_SENSOR(" ", "Status", this->status_sensor_);
319 ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state());
320 }
321 if (this->last_face_id_sensor_) {
322 LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_);
323 ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state());
324 }
325 if (this->last_face_name_text_sensor_) {
326 LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_);
327 ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str());
328 }
329}
330
331} // namespace esphome::hlk_fm22x
uint8_t checksum
Definition bl0906.h:3
virtual void mark_failed()
Mark this component as failed.
void defer(const std::string &name, std::function< void()> &&f)
Defer a callback to the next loop() call.
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:59
text_sensor::TextSensor * last_face_name_text_sensor_
Definition hlk_fm22x.h:131
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:136
void enroll_face(const std::string &name, HlkFm22xFaceDirection direction)
Definition hlk_fm22x.cpp:37
void handle_reply_(const std::vector< uint8_t > &data)
void send_command_(HlkFm22xCommand command, const uint8_t *data=nullptr, size_t size=0)
Definition hlk_fm22x.cpp:83
CallbackManager< void(uint8_t)> face_scan_invalid_callback_
Definition hlk_fm22x.h:133
CallbackManager< void()> face_scan_unmatched_callback_
Definition hlk_fm22x.h:135
void handle_note_(const std::vector< uint8_t > &data)
CallbackManager< void(int16_t, uint8_t)> enrollment_done_callback_
Definition hlk_fm22x.h:137
CallbackManager< void(uint8_t)> enrollment_failed_callback_
Definition hlk_fm22x.h:138
text_sensor::TextSensor * version_text_sensor_
Definition hlk_fm22x.h:132
CallbackManager< void(int16_t, std::string)> face_scan_matched_callback_
Definition hlk_fm22x.h:134
binary_sensor::BinarySensor * enrolling_binary_sensor_
Definition hlk_fm22x.h:130
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:77
float get_state() const
Getter-syntax for .state.
Definition sensor.cpp:124
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:3
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:331
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:735
uint16_t length
Definition tt21100.cpp:0