From fe55f3a43d9fdbb24d1746277d9e61f765ad3ef3 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 9 Jan 2023 19:06:54 -0800 Subject: [PATCH] Add support for matrix keypads (#4241) Co-authored-by: Samuel Sieb --- CODEOWNERS | 2 + esphome/components/key_provider/__init__.py | 6 ++ .../components/key_provider/key_provider.cpp | 13 +++ .../components/key_provider/key_provider.h | 21 ++++ esphome/components/matrix_keypad/__init__.py | 71 ++++++++++++ .../matrix_keypad/binary_sensor/__init__.py | 53 +++++++++ .../matrix_keypad_binary_sensor.h | 51 +++++++++ .../matrix_keypad/matrix_keypad.cpp | 102 ++++++++++++++++++ .../components/matrix_keypad/matrix_keypad.h | 46 ++++++++ 9 files changed, 365 insertions(+) create mode 100644 esphome/components/key_provider/__init__.py create mode 100644 esphome/components/key_provider/key_provider.cpp create mode 100644 esphome/components/key_provider/key_provider.h create mode 100644 esphome/components/matrix_keypad/__init__.py create mode 100644 esphome/components/matrix_keypad/binary_sensor/__init__.py create mode 100644 esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h create mode 100644 esphome/components/matrix_keypad/matrix_keypad.cpp create mode 100644 esphome/components/matrix_keypad/matrix_keypad.h diff --git a/CODEOWNERS b/CODEOWNERS index 7b3ce637de..a642274147 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -113,6 +113,7 @@ esphome/components/integration/* @OttoWinter esphome/components/interval/* @esphome/core esphome/components/json/* @OttoWinter esphome/components/kalman_combinator/* @Cat-Ion +esphome/components/key_provider/* @ssieb esphome/components/lcd_menu/* @numo68 esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core @@ -120,6 +121,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny +esphome/components/matrix_keypad/* @ssieb esphome/components/max31865/* @DAVe3283 esphome/components/max44009/* @berfenger esphome/components/max7219digit/* @rspaargaren diff --git a/esphome/components/key_provider/__init__.py b/esphome/components/key_provider/__init__.py new file mode 100644 index 0000000000..f397382ff2 --- /dev/null +++ b/esphome/components/key_provider/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@ssieb"] + +key_provider_ns = cg.esphome_ns.namespace("key_provider") +KeyProvider = key_provider_ns.class_("KeyProvider") diff --git a/esphome/components/key_provider/key_provider.cpp b/esphome/components/key_provider/key_provider.cpp new file mode 100644 index 0000000000..5a0e24b13f --- /dev/null +++ b/esphome/components/key_provider/key_provider.cpp @@ -0,0 +1,13 @@ +#include "key_provider.h" + +namespace esphome { +namespace key_provider { + +void KeyProvider::add_on_key_callback(std::function &&callback) { + this->key_callback_.add(std::move(callback)); +} + +void KeyProvider::send_key_(uint8_t key) { this->key_callback_.call(key); } + +} // namespace key_provider +} // namespace esphome diff --git a/esphome/components/key_provider/key_provider.h b/esphome/components/key_provider/key_provider.h new file mode 100644 index 0000000000..272d3eecad --- /dev/null +++ b/esphome/components/key_provider/key_provider.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace key_provider { + +/// interface for components that provide keypresses +class KeyProvider { + public: + void add_on_key_callback(std::function &&callback); + + protected: + void send_key_(uint8_t key); + + CallbackManager key_callback_{}; +}; + +} // namespace key_provider +} // namespace esphome diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py new file mode 100644 index 0000000000..1c549007b9 --- /dev/null +++ b/esphome/components/matrix_keypad/__init__.py @@ -0,0 +1,71 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import key_provider +from esphome.const import CONF_ID, CONF_PIN + +CODEOWNERS = ["@ssieb"] + +AUTO_LOAD = ["key_provider"] + +MULTI_CONF = True + +matrix_keypad_ns = cg.esphome_ns.namespace("matrix_keypad") +MatrixKeypad = matrix_keypad_ns.class_( + "MatrixKeypad", key_provider.KeyProvider, cg.Component +) + +CONF_KEYPAD_ID = "keypad_id" +CONF_ROWS = "rows" +CONF_COLUMNS = "columns" +CONF_KEYS = "keys" +CONF_DEBOUNCE_TIME = "debounce_time" +CONF_HAS_DIODES = "has_diodes" + + +def check_keys(obj): + if CONF_KEYS in obj: + if len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len(obj[CONF_COLUMNS]): + raise cv.Invalid("The number of key codes must equal the number of buttons") + return obj + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MatrixKeypad), + cv.Required(CONF_ROWS): cv.All( + cv.ensure_list({cv.Required(CONF_PIN): pins.gpio_output_pin_schema}), + cv.Length(min=1), + ), + cv.Required(CONF_COLUMNS): cv.All( + cv.ensure_list({cv.Required(CONF_PIN): pins.gpio_input_pin_schema}), + cv.Length(min=1), + ), + cv.Optional(CONF_KEYS): cv.string, + cv.Optional(CONF_DEBOUNCE_TIME, default=1): cv.int_range(min=1, max=100), + cv.Optional(CONF_HAS_DIODES): cv.boolean, + } + ), + check_keys, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + row_pins = [] + for conf in config[CONF_ROWS]: + pin = await cg.gpio_pin_expression(conf[CONF_PIN]) + row_pins.append(pin) + cg.add(var.set_rows(row_pins)) + col_pins = [] + for conf in config[CONF_COLUMNS]: + 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])) + 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/binary_sensor/__init__.py b/esphome/components/matrix_keypad/binary_sensor/__init__.py new file mode 100644 index 0000000000..204db98650 --- /dev/null +++ b/esphome/components/matrix_keypad/binary_sensor/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID, CONF_KEY +from .. import MatrixKeypad, matrix_keypad_ns, CONF_KEYPAD_ID + +CONF_ROW = "row" +CONF_COL = "col" + +DEPENDENCIES = ["matrix_keypad"] + +MatrixKeypadBinarySensor = matrix_keypad_ns.class_( + "MatrixKeypadBinarySensor", binary_sensor.BinarySensor +) + + +def check_button(obj): + if CONF_ROW in obj or CONF_COL in obj: + if CONF_KEY in obj: + raise cv.Invalid("You can't provide both a key and a position") + if CONF_ROW not in obj: + raise cv.Invalid("Missing row") + if CONF_COL not in obj: + raise cv.Invalid("Missing col") + elif CONF_KEY not in obj: + raise cv.Invalid("Missing key or position") + elif len(obj[CONF_KEY]) != 1: + raise cv.Invalid("Key must be one character") + return obj + + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MatrixKeypadBinarySensor), + cv.GenerateID(CONF_KEYPAD_ID): cv.use_id(MatrixKeypad), + cv.Optional(CONF_ROW): cv.int_, + cv.Optional(CONF_COL): cv.int_, + cv.Optional(CONF_KEY): cv.string, + } + ), + check_button, +) + + +async def to_code(config): + if CONF_KEY in config: + var = cg.new_Pvariable(config[CONF_ID], config[CONF_KEY][0]) + else: + var = cg.new_Pvariable(config[CONF_ID], config[CONF_ROW], config[CONF_COL]) + await binary_sensor.register_binary_sensor(var, config) + matrix_keypad = await cg.get_variable(config[CONF_KEYPAD_ID]) + cg.add(matrix_keypad.register_listener(var)) diff --git a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h new file mode 100644 index 0000000000..d8a217f55e --- /dev/null +++ b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/components/matrix_keypad/matrix_keypad.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace matrix_keypad { + +class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensor { + public: + MatrixKeypadBinarySensor(uint8_t key) : has_key_(true), key_(key){}; + MatrixKeypadBinarySensor(const char *key) : has_key_(true), key_((uint8_t) key[0]){}; + MatrixKeypadBinarySensor(int row, int col) : has_key_(false), row_(row), col_(col){}; + + void key_pressed(uint8_t key) override { + if (!this->has_key_) + return; + if (key == this->key_) + this->publish_state(true); + } + + void key_released(uint8_t key) override { + if (!this->has_key_) + return; + if (key == this->key_) + this->publish_state(false); + } + + void button_pressed(int row, int col) override { + if (this->has_key_) + return; + if ((row == this->row_) && (col == this->col_)) + this->publish_state(true); + } + + void button_released(int row, int col) override { + if (this->has_key_) + return; + if ((row == this->row_) && (col == this->col_)) + this->publish_state(false); + } + + protected: + bool has_key_; + uint8_t key_; + int row_; + int col_; +}; + +} // namespace matrix_keypad +} // namespace esphome diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp new file mode 100644 index 0000000000..f4e7bf4d23 --- /dev/null +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -0,0 +1,102 @@ +#include "matrix_keypad.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace matrix_keypad { + +static const char *const TAG = "matrix_keypad"; + +void MatrixKeypad::setup() { + for (auto *pin : this->rows_) { + if (!has_diodes_) { + pin->pin_mode(gpio::FLAG_INPUT); + } else { + pin->digital_write(true); + } + } + for (auto *pin : this->columns_) + pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); +} + +void MatrixKeypad::loop() { + static uint32_t active_start = 0; + static int active_key = -1; + uint32_t now = millis(); + int key = -1; + bool error = false; + int pos = 0, row, col; + for (auto *row : this->rows_) { + if (!has_diodes_) + row->pin_mode(gpio::FLAG_OUTPUT); + row->digital_write(false); + for (auto *col : this->columns_) { + if (!col->digital_read()) { + if (key != -1) { + error = true; + } else { + key = pos; + } + } + pos++; + } + row->digital_write(true); + if (!has_diodes_) + row->pin_mode(gpio::FLAG_INPUT); + } + if (error) + return; + + if (key != active_key) { + if ((active_key != -1) && (this->pressed_key_ == active_key)) { + row = this->pressed_key_ / this->columns_.size(); + col = this->pressed_key_ % this->columns_.size(); + ESP_LOGD(TAG, "key @ row %d, col %d released", row, col); + for (auto &listener : this->listeners_) + listener->button_released(row, col); + if (!this->keys_.empty()) { + uint8_t keycode = this->keys_[this->pressed_key_]; + ESP_LOGD(TAG, "key '%c' released", keycode); + for (auto &listener : this->listeners_) + listener->key_released(keycode); + } + this->pressed_key_ = -1; + } + + active_key = key; + if (key == -1) + return; + active_start = now; + } + + if ((this->pressed_key_ == key) || (now - active_start < this->debounce_time_)) + return; + + row = key / this->columns_.size(); + col = key % this->columns_.size(); + ESP_LOGD(TAG, "key @ row %d, col %d pressed", row, col); + for (auto &listener : this->listeners_) + listener->button_pressed(row, col); + if (!this->keys_.empty()) { + uint8_t keycode = this->keys_[key]; + ESP_LOGD(TAG, "key '%c' pressed", keycode); + for (auto &listener : this->listeners_) + listener->key_pressed(keycode); + this->send_key_(keycode); + } + this->pressed_key_ = key; +} + +void MatrixKeypad::dump_config() { + ESP_LOGCONFIG(TAG, "Matrix Keypad:"); + ESP_LOGCONFIG(TAG, " Rows:"); + for (auto &pin : this->rows_) + LOG_PIN(" Pin: ", pin); + ESP_LOGCONFIG(TAG, " Cols:"); + for (auto &pin : this->columns_) + LOG_PIN(" Pin: ", pin); +} + +void MatrixKeypad::register_listener(MatrixKeypadListener *listener) { this->listeners_.push_back(listener); } + +} // namespace matrix_keypad +} // namespace esphome diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h new file mode 100644 index 0000000000..9f5942be9a --- /dev/null +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/components/key_provider/key_provider.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome { +namespace matrix_keypad { + +class MatrixKeypadListener { + public: + virtual void button_pressed(int row, int col){}; + virtual void button_released(int row, int col){}; + virtual void key_pressed(uint8_t key){}; + virtual void key_released(uint8_t key){}; +}; + +class MatrixKeypad : public key_provider::KeyProvider, public Component { + public: + void setup() override; + void loop() override; + 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); }; + void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; + void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; + + void register_listener(MatrixKeypadListener *listener); + + protected: + std::vector rows_; + std::vector columns_; + std::string keys_; + int debounce_time_ = 0; + bool has_diodes_{false}; + int pressed_key_ = -1; + + std::vector listeners_{}; +}; + +} // namespace matrix_keypad +} // namespace esphome