From 0017b9c439db5c78988f7a0e1595d4b20e6acd8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Oct 2026 18:26:42 -0500 Subject: [PATCH] [matrix_keypad] Keep the keys in flash (#20110) --- esphome/components/matrix_keypad/__init__.py | 11 ++++++-- .../matrix_keypad/matrix_keypad.cpp | 8 +++--- .../components/matrix_keypad/matrix_keypad.h | 5 ++-- .../component_tests/matrix_keypad/__init__.py | 0 .../matrix_keypad/config/keys.yaml | 23 ++++++++++++++++ .../matrix_keypad/test_keys.py | 27 +++++++++++++++++++ 6 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 tests/component_tests/matrix_keypad/__init__.py create mode 100644 tests/component_tests/matrix_keypad/config/keys.yaml create mode 100644 tests/component_tests/matrix_keypad/test_keys.py diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 2e43eaf7e2..f9b744daf0 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -27,6 +27,10 @@ CONF_HAS_PULLDOWNS = "has_pulldowns" def check_keys(obj: ConfigType) -> ConfigType: + for ch in obj.get(CONF_KEYS, ""): + if not ch.isascii(): + # Each key is reported as one byte, so only ASCII characters can be key codes + raise cv.Invalid(f"Key code {ch!r} is not an ASCII character") if CONF_KEYS in obj and len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len( obj[CONF_COLUMNS] ): @@ -74,8 +78,11 @@ async def to_code(config: ConfigType) -> None: pin = await cg.gpio_pin_expression(conf[CONF_PIN]) col_pins.append(pin) cg.add(var.set_columns(col_pins)) - if CONF_KEYS in config: - cg.add(var.set_keys(config[CONF_KEYS])) + if (keys := config.get(CONF_KEYS)) is not None: + table = cg.shared_progmem_array( + "matrix_keypad_keys", cg.uint8, list(keys.encode()) + ) + cg.add(var.set_keys(table)) cg.add(var.set_debounce_time(config[CONF_DEBOUNCE_TIME])) if CONF_HAS_DIODES in config: cg.add(var.set_has_diodes(config[CONF_HAS_DIODES])) diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 3b71b50fd8..97641ac2d3 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -58,8 +58,8 @@ void MatrixKeypad::loop() { ESP_LOGD(TAG, "key @ row %d, col %d released", row, col); for (auto &listener : this->listeners_) listener->button_released(row, col); - if (this->pressed_key_ < (int) this->keys_.size()) { - uint8_t keycode = this->keys_[this->pressed_key_]; + if (this->keys_ != nullptr) { + uint8_t keycode = progmem_read_byte(&this->keys_[this->pressed_key_]); ESP_LOGD(TAG, "key '%c' released", keycode); for (auto &listener : this->listeners_) listener->key_released(keycode); @@ -81,8 +81,8 @@ void MatrixKeypad::loop() { ESP_LOGD(TAG, "key @ row %d, col %d pressed", row, col); for (auto &listener : this->listeners_) listener->button_pressed(row, col); - if (key < (int) this->keys_.size()) { - uint8_t keycode = this->keys_[key]; + if (this->keys_ != nullptr) { + uint8_t keycode = progmem_read_byte(&this->keys_[key]); ESP_LOGD(TAG, "key '%c' pressed", keycode); for (auto &trigger : this->key_triggers_) trigger->trigger(keycode); diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 8c9acc8e0c..2384691e32 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -27,7 +27,8 @@ class MatrixKeypad final : public key_provider::KeyProvider, public Component { void dump_config() override; void set_columns(std::vector pins) { columns_ = std::move(pins); }; void set_rows(std::vector pins) { rows_ = std::move(pins); }; - void set_keys(std::string keys) { keys_ = std::move(keys); }; + /// `keys` is a codegen PROGMEM table with one key code per button. + void set_keys(const uint8_t *keys) { this->keys_ = keys; } void set_debounce_time(uint32_t debounce_time) { debounce_time_ = debounce_time; }; void set_has_diodes(bool has_diodes) { has_diodes_ = has_diodes; }; void set_has_pulldowns(bool has_pulldowns) { has_pulldowns_ = has_pulldowns; }; @@ -38,7 +39,7 @@ class MatrixKeypad final : public key_provider::KeyProvider, public Component { protected: std::vector rows_; std::vector columns_; - std::string keys_; + const uint8_t *keys_{nullptr}; uint32_t debounce_time_ = 0; bool has_diodes_{false}; bool has_pulldowns_{false}; diff --git a/tests/component_tests/matrix_keypad/__init__.py b/tests/component_tests/matrix_keypad/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/matrix_keypad/config/keys.yaml b/tests/component_tests/matrix_keypad/config/keys.yaml new file mode 100644 index 0000000000..99d32ffd14 --- /dev/null +++ b/tests/component_tests/matrix_keypad/config/keys.yaml @@ -0,0 +1,23 @@ +esphome: + name: test + +esp8266: + board: esp01_1m + +matrix_keypad: + - id: keypad_a + rows: + - pin: 12 + - pin: 13 + columns: + - pin: 14 + - pin: 15 + keys: "1234" + - id: keypad_b + rows: + - pin: 4 + - pin: 5 + columns: + - pin: 0 + - pin: 2 + keys: "1234" diff --git a/tests/component_tests/matrix_keypad/test_keys.py b/tests/component_tests/matrix_keypad/test_keys.py new file mode 100644 index 0000000000..7993a20b70 --- /dev/null +++ b/tests/component_tests/matrix_keypad/test_keys.py @@ -0,0 +1,27 @@ +"""Matrix keypad key codes live in a shared PROGMEM table instead of a std::string.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components.matrix_keypad import check_keys +import esphome.config_validation as cv + + +def test_keys_use_shared_progmem_table( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("keys.yaml")) + + assert ( + "static constexpr uint8_t matrix_keypad_keys[] PROGMEM = {49, 50, 51, 52};" + in main_cpp + ) + assert main_cpp.count("->set_keys(matrix_keypad_keys);") == 2 + + +def test_non_ascii_keys_are_rejected() -> None: + with pytest.raises(cv.Invalid, match="'é' is not an ASCII"): + check_keys({"keys": "1é34"})