diff --git a/CODEOWNERS b/CODEOWNERS index 351d9f5fc9..bfa4a7e59f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,6 +54,7 @@ esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk +esphome/components/ektf2232/* @jesserockz esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz diff --git a/esphome/components/ektf2232/__init__.py b/esphome/components/ektf2232/__init__.py new file mode 100644 index 0000000000..0427bda4fb --- /dev/null +++ b/esphome/components/ektf2232/__init__.py @@ -0,0 +1,80 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins, automation +from esphome.components import i2c +from esphome.const import CONF_HEIGHT, CONF_ID, CONF_ROTATION, CONF_WIDTH + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] + +ektf2232_ns = cg.esphome_ns.namespace("ektf2232") +EKTF2232Touchscreen = ektf2232_ns.class_( + "EKTF2232Touchscreen", cg.Component, i2c.I2CDevice +) +TouchPoint = ektf2232_ns.struct("TouchPoint") +TouchListener = ektf2232_ns.class_("TouchListener") + +EKTF2232Rotation = ektf2232_ns.enum("EKTF2232Rotation") + +CONF_EKTF2232_ID = "ektf2232_id" +CONF_INTERRUPT_PIN = "interrupt_pin" +CONF_RTS_PIN = "rts_pin" +CONF_ON_TOUCH = "on_touch" + +ROTATIONS = { + 0: EKTF2232Rotation.ROTATE_0_DEGREES, + 90: EKTF2232Rotation.ROTATE_90_DEGREES, + 180: EKTF2232Rotation.ROTATE_180_DEGREES, + 270: EKTF2232Rotation.ROTATE_270_DEGREES, +} + + +def validate_rotation(value): + value = cv.string(value) + if value.endswith("°"): + value = value[:-1] + return cv.enum(ROTATIONS, int=True)(value) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EKTF2232Touchscreen), + cv.Required(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_HEIGHT, default=758): cv.int_, + cv.Optional(CONF_WIDTH, default=1024): cv.int_, + cv.Optional(CONF_ROTATION, default=0): validate_rotation, + cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), + } + ) + .extend(i2c.i2c_device_schema(0x15)) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) + rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN]) + cg.add(var.set_rts_pin(rts_pin)) + + cg.add( + var.set_display_details( + config[CONF_WIDTH], + config[CONF_HEIGHT], + config[CONF_ROTATION], + ) + ) + + if CONF_ON_TOUCH in config: + await automation.build_automation( + var.get_touch_trigger(), [(TouchPoint, "touch")], config[CONF_ON_TOUCH] + ) diff --git a/esphome/components/ektf2232/binary_sensor/__init__.py b/esphome/components/ektf2232/binary_sensor/__init__.py new file mode 100644 index 0000000000..349c45b31c --- /dev/null +++ b/esphome/components/ektf2232/binary_sensor/__init__.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome.components import binary_sensor +from esphome.const import CONF_ID + +from .. import ektf2232_ns, CONF_EKTF2232_ID, EKTF2232Touchscreen, TouchListener + +DEPENDENCIES = ["ektf2232"] + +EKTF2232Button = ektf2232_ns.class_( + "EKTF2232Button", binary_sensor.BinarySensor, TouchListener +) + +CONF_X_MIN = "x_min" +CONF_X_MAX = "x_max" +CONF_Y_MIN = "y_min" +CONF_Y_MAX = "y_max" + + +def validate_coords(config): + if ( + config[CONF_X_MAX] < config[CONF_X_MIN] + or config[CONF_Y_MAX] < config[CONF_Y_MIN] + ): + raise cv.Invalid( + f"{CONF_X_MAX} is less than {CONF_X_MIN} or {CONF_Y_MAX} is less than {CONF_Y_MIN}" + ) + return config + + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EKTF2232Button), + cv.GenerateID(CONF_EKTF2232_ID): cv.use_id(EKTF2232Touchscreen), + cv.Required(CONF_X_MIN): cv.int_range(min=0, max=2000), + cv.Required(CONF_X_MAX): cv.int_range(min=0, max=2000), + cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=2000), + cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=2000), + } + ), + validate_coords, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await binary_sensor.register_binary_sensor(var, config) + hub = await cg.get_variable(config[CONF_EKTF2232_ID]) + cg.add( + var.set_area( + config[CONF_X_MIN], + config[CONF_X_MAX], + config[CONF_Y_MIN], + config[CONF_Y_MAX], + ) + ) + cg.add(hub.register_listener(var)) diff --git a/esphome/components/ektf2232/binary_sensor/ektf2232_binary_sensor.cpp b/esphome/components/ektf2232/binary_sensor/ektf2232_binary_sensor.cpp new file mode 100644 index 0000000000..a6fdf8b76c --- /dev/null +++ b/esphome/components/ektf2232/binary_sensor/ektf2232_binary_sensor.cpp @@ -0,0 +1,19 @@ +#include "ektf2232_binary_sensor.h" + +namespace esphome { +namespace ektf2232 { + +void EKTF2232Button::touch(TouchPoint tp) { + bool touched = (tp.x >= this->x_min_ && tp.x <= this->x_max_ && tp.y >= this->y_min_ && tp.y <= this->y_max_); + + if (touched) { + this->publish_state(true); + } else { + release(); + } +} + +void EKTF2232Button::release() { this->publish_state(false); } + +} // namespace ektf2232 +} // namespace esphome diff --git a/esphome/components/ektf2232/binary_sensor/ektf2232_binary_sensor.h b/esphome/components/ektf2232/binary_sensor/ektf2232_binary_sensor.h new file mode 100644 index 0000000000..170dfcdebb --- /dev/null +++ b/esphome/components/ektf2232/binary_sensor/ektf2232_binary_sensor.h @@ -0,0 +1,27 @@ +#pragma once + +#include "../ektf2232.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace ektf2232 { + +class EKTF2232Button : public binary_sensor::BinarySensor, public TouchListener { + public: + /// Set the touch screen area where the button will detect the touch. + void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { + this->x_min_ = x_min; + this->x_max_ = x_max; + this->y_min_ = y_min; + this->y_max_ = y_max; + } + + void touch(TouchPoint tp) override; + void release() override; + + protected: + int16_t x_min_, x_max_, y_min_, y_max_; +}; + +} // namespace ektf2232 +} // namespace esphome diff --git a/esphome/components/ektf2232/ektf2232.cpp b/esphome/components/ektf2232/ektf2232.cpp new file mode 100644 index 0000000000..da16dc3cfe --- /dev/null +++ b/esphome/components/ektf2232/ektf2232.cpp @@ -0,0 +1,168 @@ +#include "ektf2232.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ektf2232 { + +static const char *const TAG = "ektf2232"; + +static const uint8_t SOFT_RESET_CMD[4] = {0x77, 0x77, 0x77, 0x77}; +static const uint8_t HELLO[4] = {0x55, 0x55, 0x55, 0x55}; +static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00}; +static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; +static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; + +void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; } + +void EKTF2232Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + + this->store_.pin = this->interrupt_pin_->to_isr(); + this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_, + gpio::INTERRUPT_FALLING_EDGE); + + this->rts_pin_->setup(); + + this->hard_reset_(); + if (!this->soft_reset_()) { + ESP_LOGE(TAG, "Failed to soft reset EKT2232!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + + // Get touch resolution + uint8_t received[4]; + this->write(GET_X_RES, 4); + if (this->read(received, 4)) { + ESP_LOGE(TAG, "Failed to read X resolution!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); + + this->write(GET_Y_RES, 4); + if (this->read(received, 4)) { + ESP_LOGE(TAG, "Failed to read Y resolution!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); + this->store_.touch = false; + + this->set_power_state(true); +} + +void EKTF2232Touchscreen::loop() { + if (!this->store_.touch) + return; + this->store_.touch = false; + + uint8_t touch_count = 0; + std::vector touches; + + uint8_t raw[8]; + this->read(raw, 8); + for (int i = 0; i < 8; i++) + if (raw[7] & (1 << i)) + touch_count++; + + if (touch_count == 0) { + for (auto *listener : this->touch_listeners_) + listener->release(); + return; + } + + touch_count = std::min(touch_count, 2); + + ESP_LOGV(TAG, "Touch count: %d", touch_count); + + for (int i = 0; i < touch_count; i++) { + uint8_t *d = raw + 1 + (i * 3); + uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1]; + uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2]; + + raw_x = raw_x * this->display_height_ - 1; + raw_y = raw_y * this->display_width_ - 1; + + TouchPoint tp; + switch (this->rotation_) { + case ROTATE_0_DEGREES: + tp.y = raw_x / this->x_resolution_; + tp.x = this->display_width_ - 1 - (raw_y / this->y_resolution_); + break; + case ROTATE_90_DEGREES: + tp.x = raw_x / this->x_resolution_; + tp.y = raw_y / this->y_resolution_; + break; + case ROTATE_180_DEGREES: + tp.y = this->display_height_ - 1 - (raw_x / this->x_resolution_); + tp.x = raw_y / this->y_resolution_; + break; + case ROTATE_270_DEGREES: + tp.x = this->display_height_ - 1 - (raw_x / this->x_resolution_); + tp.y = this->display_width_ - 1 - (raw_y / this->y_resolution_); + break; + } + + ESP_LOGV(TAG, "Touch %d: (x=%d, y=%d)", i, tp.x, tp.y); + this->touch_trigger_->trigger(tp); + for (auto *listener : this->touch_listeners_) + listener->touch(tp); + } +} + +void EKTF2232Touchscreen::set_power_state(bool enable) { + uint8_t data[] = {0x54, 0x50, 0x00, 0x01}; + data[1] |= (enable << 3); + this->write(data, 4); +} + +bool EKTF2232Touchscreen::get_power_state() { + uint8_t received[4]; + this->write(GET_POWER_STATE_CMD, 4); + this->store_.touch = false; + this->read(received, 4); + return (received[1] >> 3) & 1; +} + +void EKTF2232Touchscreen::hard_reset_() { + this->rts_pin_->digital_write(false); + delay(15); + this->rts_pin_->digital_write(true); + delay(15); +} + +bool EKTF2232Touchscreen::soft_reset_() { + auto err = this->write(SOFT_RESET_CMD, 4); + if (err != i2c::ERROR_OK) + return false; + + uint8_t received[4]; + uint16_t timeout = 1000; + while (!this->store_.touch && timeout > 0) { + delay(1); + timeout--; + } + if (timeout > 0) + this->store_.touch = true; + this->read(received, 4); + this->store_.touch = false; + + return !memcmp(received, HELLO, 4); +} + +void EKTF2232Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "EKT2232 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" RTS Pin: ", this->rts_pin_); +} + +} // namespace ektf2232 +} // namespace esphome diff --git a/esphome/components/ektf2232/ektf2232.h b/esphome/components/ektf2232/ektf2232.h new file mode 100644 index 0000000000..0d6fb7a699 --- /dev/null +++ b/esphome/components/ektf2232/ektf2232.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ektf2232 { + +struct EKTF2232TouchscreenStore { + volatile bool touch; + ISRInternalGPIOPin pin; + + static void gpio_intr(EKTF2232TouchscreenStore *store); +}; + +struct TouchPoint { + uint16_t x; + uint16_t y; +}; + +class TouchListener { + public: + virtual void touch(TouchPoint tp) = 0; + virtual void release(); +}; + +enum EKTF2232Rotation : uint8_t { + ROTATE_0_DEGREES = 0, + ROTATE_90_DEGREES, + ROTATE_180_DEGREES, + ROTATE_270_DEGREES, +}; + +class EKTF2232Touchscreen : public Component, public i2c::I2CDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } + + void set_display_details(uint16_t width, uint16_t height, EKTF2232Rotation rotation) { + this->display_width_ = width; + this->display_height_ = height; + this->rotation_ = rotation; + } + + void set_power_state(bool enable); + bool get_power_state(); + + Trigger *get_touch_trigger() const { return this->touch_trigger_; } + + void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); } + + protected: + void hard_reset_(); + bool soft_reset_(); + + InternalGPIOPin *interrupt_pin_; + GPIOPin *rts_pin_; + EKTF2232TouchscreenStore store_; + uint16_t x_resolution_; + uint16_t y_resolution_; + + uint16_t display_width_; + uint16_t display_height_; + EKTF2232Rotation rotation_; + Trigger *touch_trigger_ = new Trigger(); + std::vector touch_listeners_; +}; + +} // namespace ektf2232 +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index d6acbf1e65..b13f20a9b2 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -199,3 +199,12 @@ script: count: 5 then: - logger.log: "looping!" + +ektf2232: + interrupt_pin: GPIO36 + rts_pin: GPIO5 + rotation: 90 + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: ["touch.x", "touch.y"]