From 2e506514003e46a59778e3a107cab5d4e4c75463 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Feb 2026 02:17:22 -0600 Subject: [PATCH 1/4] [hlk_fm22x] Replace per-cycle vector allocation with member buffer Replace std::vector in recv_command_() with a member std::array buffer to eliminate heap allocation on every polling cycle. Also use pointer+length instead of vector references in handle_note_/handle_reply_, and use TextSensor's publish_state(const char*, size_t) overload to avoid temporary std::string construction for version and face name publishing. --- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 41 +++++++++++----------- esphome/components/hlk_fm22x/hlk_fm22x.h | 10 ++++-- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index c0f14c7105..c2e6e1f703 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -1,16 +1,12 @@ #include "hlk_fm22x.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#include #include namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; -// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name) -static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; - void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); this->set_enrolling_(false); @@ -35,7 +31,7 @@ void HlkFm22xComponent::update() { } void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { - if (name.length() > 31) { + if (name.length() > HLK_FM22X_NAME_SIZE - 1) { ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); return; } @@ -137,17 +133,21 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; - std::vector data; - data.reserve(length); + if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { + ESP_LOGE(TAG, "Response too large: %u bytes", length); + return; + } + for (uint16_t idx = 0; idx < length; ++idx) { byte = this->read(); checksum ^= byte; - data.push_back(byte); + this->recv_buf_[idx] = byte; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; - ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size())); + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, + format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length)); #endif byte = this->read(); @@ -157,10 +157,10 @@ void HlkFm22xComponent::recv_command_() { } switch (response_type) { case HlkFm22xResponseType::NOTE: - this->handle_note_(data); + this->handle_note_(this->recv_buf_.data(), length); break; case HlkFm22xResponseType::REPLY: - this->handle_reply_(data); + this->handle_reply_(this->recv_buf_.data(), length); break; default: ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); @@ -168,11 +168,11 @@ void HlkFm22xComponent::recv_command_() { } } -void HlkFm22xComponent::handle_note_(const std::vector &data) { +void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) { switch (data[0]) { case HlkFm22xNoteType::FACE_STATE: - if (data.size() < 17) { - ESP_LOGE(TAG, "Invalid face note data size: %u", data.size()); + if (length < 17) { + ESP_LOGE(TAG, "Invalid face note data size: %u", length); break; } { @@ -209,7 +209,7 @@ void HlkFm22xComponent::handle_note_(const std::vector &data) { } } -void HlkFm22xComponent::handle_reply_(const std::vector &data) { +void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { auto expected = this->active_command_; this->active_command_ = HlkFm22xCommand::NONE; if (data[0] != (uint8_t) expected) { @@ -239,15 +239,15 @@ void HlkFm22xComponent::handle_reply_(const std::vector &data) { switch (expected) { case HlkFm22xCommand::VERIFY: { int16_t face_id = ((int16_t) data[2] << 8) | data[3]; - std::string name(data.begin() + 4, data.begin() + 36); - ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str()); + const char *name_ptr = reinterpret_cast(data + 4); + ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, HLK_FM22X_NAME_SIZE, name_ptr); if (this->last_face_id_sensor_ != nullptr) { this->last_face_id_sensor_->publish_state(face_id); } if (this->last_face_name_text_sensor_ != nullptr) { - this->last_face_name_text_sensor_->publish_state(name); + this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE); } - this->face_scan_matched_callback_.call(face_id, name); + this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE)); break; } case HlkFm22xCommand::ENROLL: { @@ -267,8 +267,7 @@ void HlkFm22xComponent::handle_reply_(const std::vector &data) { break; case HlkFm22xCommand::GET_VERSION: if (this->version_text_sensor_ != nullptr) { - std::string version(data.begin() + 2, data.end()); - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(reinterpret_cast(data + 2), length - 2); } this->defer([this]() { this->get_face_count_(); }); break; diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 9c981d3c44..ff81d82ac2 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -7,12 +7,15 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/uart/uart.h" +#include #include -#include namespace esphome::hlk_fm22x { static const uint16_t START_CODE = 0xEFAA; +static constexpr size_t HLK_FM22X_NAME_SIZE = 32; +// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name) +static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; enum HlkFm22xCommand { NONE = 0x00, RESET = 0x10, @@ -118,10 +121,11 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { void get_face_count_(); void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0); void recv_command_(); - void handle_note_(const std::vector &data); - void handle_reply_(const std::vector &data); + void handle_note_(const uint8_t *data, size_t length); + void handle_reply_(const uint8_t *data, size_t length); void set_enrolling_(bool enrolling); + std::array recv_buf_; HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE; uint16_t wait_cycles_ = 0; sensor::Sensor *face_count_sensor_{nullptr}; From 537425247063137bfbbb8d9fad9b1441ca752c74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Feb 2026 02:38:40 -0600 Subject: [PATCH 2/4] [hlk_fm22x] Add bounds checks and fix format specifiers - Flush UART RX buffer when response exceeds max size - Guard handle_note_ against zero-length data - Guard handle_reply_ against length < 2 - Validate VERIFY response has full name payload before access - Guard GET_VERSION against length underflow - Cast %.*s precision to int, use %zu for size_t - Improve MAX_RESPONSE_SIZE comment with payload layout --- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 20 +++++++++++++++++--- esphome/components/hlk_fm22x/hlk_fm22x.h | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index c2e6e1f703..8a12c5e772 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -135,6 +135,8 @@ void HlkFm22xComponent::recv_command_() { if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { ESP_LOGE(TAG, "Response too large: %u bytes", length); + while (this->available()) + this->read(); return; } @@ -169,10 +171,14 @@ void HlkFm22xComponent::recv_command_() { } void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) { + if (length < 1) { + ESP_LOGE(TAG, "Empty note data"); + return; + } switch (data[0]) { case HlkFm22xNoteType::FACE_STATE: if (length < 17) { - ESP_LOGE(TAG, "Invalid face note data size: %u", length); + ESP_LOGE(TAG, "Invalid face note data size: %zu", length); break; } { @@ -212,6 +218,10 @@ void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) { void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { auto expected = this->active_command_; this->active_command_ = HlkFm22xCommand::NONE; + if (length < 2) { + ESP_LOGE(TAG, "Reply too short: %zu bytes", length); + return; + } if (data[0] != (uint8_t) expected) { ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]); return; @@ -238,9 +248,13 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { } switch (expected) { case HlkFm22xCommand::VERIFY: { + if (length < 4 + HLK_FM22X_NAME_SIZE) { + ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length); + break; + } int16_t face_id = ((int16_t) data[2] << 8) | data[3]; const char *name_ptr = reinterpret_cast(data + 4); - ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, HLK_FM22X_NAME_SIZE, name_ptr); + ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr); if (this->last_face_id_sensor_ != nullptr) { this->last_face_id_sensor_->publish_state(face_id); } @@ -266,7 +280,7 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); break; case HlkFm22xCommand::GET_VERSION: - if (this->version_text_sensor_ != nullptr) { + if (this->version_text_sensor_ != nullptr && length > 2) { this->version_text_sensor_->publish_state(reinterpret_cast(data + 2), length - 2); } this->defer([this]() { this->get_face_count_(); }); diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index ff81d82ac2..598519af4a 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -14,7 +14,7 @@ namespace esphome::hlk_fm22x { static const uint16_t START_CODE = 0xEFAA; static constexpr size_t HLK_FM22X_NAME_SIZE = 32; -// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name) +// Maximum response payload: 1-byte command + 1-byte result + 2-byte face_id + 32-byte name = 36 static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; enum HlkFm22xCommand { NONE = 0x00, From eb3bad823bcf210d53e61f4851c203f228aca367 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Feb 2026 02:40:04 -0600 Subject: [PATCH 3/4] [hlk_fm22x] Reword comment to avoid lint false positive on 'byte' --- esphome/components/hlk_fm22x/hlk_fm22x.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 598519af4a..0ea4636281 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -14,7 +14,7 @@ namespace esphome::hlk_fm22x { static const uint16_t START_CODE = 0xEFAA; static constexpr size_t HLK_FM22X_NAME_SIZE = 32; -// Maximum response payload: 1-byte command + 1-byte result + 2-byte face_id + 32-byte name = 36 +// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36 static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; enum HlkFm22xCommand { NONE = 0x00, From 78f98fa08fbbf8f64c5ca72cafb916298c93d20a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Feb 2026 02:51:27 -0600 Subject: [PATCH 4/4] [hlk_fm22x] Drain exact frame bytes on oversize response Discard exactly length+1 (payload + checksum) instead of flushing the entire RX buffer, which could eat bytes from the next frame. --- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 8a12c5e772..fb79113181 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -135,7 +135,8 @@ void HlkFm22xComponent::recv_command_() { if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { ESP_LOGE(TAG, "Response too large: %u bytes", length); - while (this->available()) + // Discard exactly the remaining payload and checksum for this frame + for (uint16_t i = 0; i < length + 1 && this->available(); ++i) this->read(); return; }