diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index 7694b8f732..e4df7b443c 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -43,6 +43,10 @@ SX1509KeyTrigger = sx1509_ns.class_( def check_keys(config: ConfigType) -> ConfigType: + for ch in config.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 config and len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS] @@ -98,8 +102,11 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_sleep_time(conf[CONF_SLEEP_TIME])) cg.add(var.set_scan_time(conf[CONF_SCAN_TIME])) cg.add(var.set_debounce_time(conf[CONF_DEBOUNCE_TIME])) - if keys := conf.get(CONF_KEYS): - cg.add(var.set_keys(keys)) + if (keys := conf.get(CONF_KEYS)) is not None: + table = cg.shared_progmem_array( + "sx1509_keys", cg.uint8, list(keys.encode()) + ) + cg.add(var.set_keys(table)) for tconf in conf.get(CONF_ON_KEY, []): trigger = cg.new_Pvariable(tconf[CONF_TRIGGER_ID]) cg.add(var.register_key_trigger(trigger)) diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 2397049000..ee8606db96 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -48,7 +48,7 @@ void SX1509Component::loop() { uint16_t key_data = this->read_key_data(); for (auto *binary_sensor : this->keypad_binary_sensors_) binary_sensor->process(key_data); - if (this->keys_.empty()) + if (this->keys_ == nullptr) return; if (key_data == 0) { this->last_key_ = 0; @@ -64,7 +64,9 @@ void SX1509Component::loop() { break; } col -= 8; - uint8_t key = this->keys_[row * this->cols_ + col]; + if (row >= this->rows_ || col >= this->cols_) // a partial read can leave only a row or a column bit + return; + uint8_t key = progmem_read_byte(&this->keys_[row * this->cols_ + col]); if (key == this->last_key_) return; this->last_key_ = key; diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index c7aed2cddd..ef6ee6ec5e 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -51,7 +51,8 @@ class SX1509Component final : public Component, this->cols_ = cols; this->has_keypad_ = true; }; - void set_keys(std::string keys) { this->keys_ = std::move(keys); }; // NOLINT(performance-unnecessary-value-param) + /// `keys` is a codegen PROGMEM table with one key code per button. + void set_keys(const uint8_t *keys) { this->keys_ = keys; } void set_sleep_time(uint16_t sleep_time) { this->sleep_time_ = sleep_time; }; void set_scan_time(uint8_t scan_time) { this->scan_time_ = scan_time; }; void set_debounce_time(uint8_t debounce_time = 1) { this->debounce_time_ = debounce_time; }; @@ -76,7 +77,7 @@ class SX1509Component final : public Component, bool has_keypad_ = false; uint8_t rows_ = 0; uint8_t cols_ = 0; - std::string keys_; + const uint8_t *keys_{nullptr}; uint16_t sleep_time_ = 128; uint8_t scan_time_ = 1; uint8_t debounce_time_ = 1; diff --git a/tests/component_tests/sx1509/__init__.py b/tests/component_tests/sx1509/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/sx1509/config/keys.yaml b/tests/component_tests/sx1509/config/keys.yaml new file mode 100644 index 0000000000..e5be4554c9 --- /dev/null +++ b/tests/component_tests/sx1509/config/keys.yaml @@ -0,0 +1,23 @@ +esphome: + name: test + +esp8266: + board: esp01_1m + +i2c: + sda: 4 + scl: 5 + +sx1509: + - id: sx_a + address: 0x3E + keypad: + key_rows: 2 + key_columns: 2 + keys: "abcd" + - id: sx_b + address: 0x3F + keypad: + key_rows: 2 + key_columns: 2 + keys: "abcd" diff --git a/tests/component_tests/sx1509/test_keys.py b/tests/component_tests/sx1509/test_keys.py new file mode 100644 index 0000000000..d7d41ae525 --- /dev/null +++ b/tests/component_tests/sx1509/test_keys.py @@ -0,0 +1,27 @@ +"""SX1509 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.sx1509 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 sx1509_keys[] PROGMEM = {97, 98, 99, 100};" + in main_cpp + ) + assert main_cpp.count("->set_keys(sx1509_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"})