diff --git a/esphome/components/icnt86/__init__.py b/esphome/components/icnt86/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/icnt86/icnt86.cpp b/esphome/components/icnt86/icnt86.cpp new file mode 100644 index 0000000000..b9b1af2db4 --- /dev/null +++ b/esphome/components/icnt86/icnt86.cpp @@ -0,0 +1,170 @@ +#include "icnt86.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace icnt86 { + +static const char *const TAG = "icnt86"; + +static const uint8_t MAX_BUTTONS = 4; +static const uint8_t MAX_TOUCH_POINTS = 5; +static const uint8_t MAX_DATA_LEN = (7 + MAX_TOUCH_POINTS * 10); // 7 Header + (Points * 10 data bytes) + +#define UBYTE uint8_t +#define UWORD uint16_t +#define UDOUBLE uint32_t + +struct ICNT86ButtonReport { + uint16_t length; // Always 14 (0x000E) + uint8_t report_id; // Always 0x03 + uint16_t timestamp; // Number in units of 100 us + uint8_t btn_value; // Only use bit 0..3 + uint16_t btn_signal[MAX_BUTTONS]; +} __attribute__((packed)); + +struct ICNT86TouchRecord { + uint8_t : 5; + uint8_t touch_type : 3; + uint8_t tip : 1; + uint8_t event_id : 2; + uint8_t touch_id : 5; + uint16_t x; + uint16_t y; + uint8_t pressure; + uint16_t major_axis_length; + uint8_t orientation; +} __attribute__((packed)); + +void ICNT86TouchscreenStore::gpio_intr(ICNT86TouchscreenStore *store) { store->touch = true; } + +float ICNT86Touchscreen::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } + +void ICNT86Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up icnt86 Touchscreen..."); + + // Register interrupt pin + 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(ICNT86TouchscreenStore::gpio_intr, &this->store_, + gpio::INTERRUPT_FALLING_EDGE); + + // Perform reset if necessary + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_(); + } + + // Update display dimensions if they were updated during display setup + this->display_width_ = this->display_->get_width(); + this->display_height_ = this->display_->get_height(); + this->rotation_ = static_cast(this->display_->get_rotation()); + + // Trigger initial read to activate the interrupt + this->store_.touch = true; +} + +void ICNT86Touchscreen::loop() { + if (!this->store_.touch) { + return; + } + ESP_LOGD(TAG, "touch"); + + this->store_.touch = false; + + char buf[100]; + char mask[1] = {0x00}; + // Read report length + uint16_t data_len; + + this->ICNT_Read_(0x1001, buf, 1); + uint8_t touch_count = buf[0]; + ESP_LOGD(TAG, "Touch count: %d", touch_count); + + if (buf[0] == 0x00) { // No new touch + this->ICNT_Write_(0x1001, mask, 1); + delay(1); + return; + } else { + if (touch_count > 5 || touch_count < 1) { + this->ICNT_Write_(0x1001, mask, 1); + touch_count = 0; + for (auto *listener : this->touch_listeners_) + listener->release(); + + return; + } + this->ICNT_Read_(0x1002, buf, touch_count * 7); + this->ICNT_Write_(0x1001, mask, 1); + + for (UBYTE i = 0; i < touch_count; i++) { + UWORD X = ((UWORD) buf[2 + 7 * i] << 8) + buf[1 + 7 * i]; + UWORD Y = ((UWORD) buf[4 + 7 * i] << 8) + buf[3 + 7 * i]; + UWORD P = buf[5 + 7 * i]; + UWORD TouchEvenid = buf[6 + 7 * i]; + + TouchPoint tp; + switch (this->rotation_) { + case ROTATE_0_DEGREES: + // Origin is top right, so mirror X by default + tp.x = this->display_width_ - X; + tp.y = Y; + break; + case ROTATE_90_DEGREES: + tp.x = Y; + tp.y = X; + break; + case ROTATE_180_DEGREES: + tp.x = Y; + tp.y = this->display_height_ - Y; + break; + case ROTATE_270_DEGREES: + tp.x = this->display_height_ - Y; + tp.y = this->display_width_ - X; + break; + } + tp.id = TouchEvenid; + tp.state = P; + + ESP_LOGD(TAG, "Touch x: %d, y: %d, p: %d", X, Y, P); + + this->defer([this, tp]() { this->send_touch_(tp); }); + } + } +} + +void ICNT86Touchscreen::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void ICNT86Touchscreen::I2C_Write_Byte_(UWORD Reg, char *Data, UBYTE len) { + char wbuf[50] = {(Reg >> 8) & 0xff, Reg & 0xff}; + for (UBYTE i = 0; i < len; i++) { + wbuf[i + 2] = Data[i]; + } + this->write((const uint8_t *) wbuf, len + 2); +} + +void ICNT86Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "icnt86 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +void ICNT86Touchscreen::ICNT_Read_(UWORD Reg, char *Data, UBYTE len) { this->I2C_Read_Byte_(Reg, Data, len); } + +void ICNT86Touchscreen::ICNT_Write_(UWORD Reg, char *Data, UBYTE len) { this->I2C_Write_Byte_(Reg, Data, len); } +void ICNT86Touchscreen::I2C_Read_Byte_(UWORD Reg, char *Data, UBYTE len) { + char *rbuf = Data; + this->I2C_Write_Byte_(Reg, 0, 0); + this->read((uint8_t *) rbuf, len); +} + +} // namespace icnt86 +} // namespace esphome diff --git a/esphome/components/icnt86/icnt86.h b/esphome/components/icnt86/icnt86.h new file mode 100644 index 0000000000..81b03dc1d6 --- /dev/null +++ b/esphome/components/icnt86/icnt86.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace icnt86 { + +using namespace touchscreen; + +#define UBYTE uint8_t +#define UWORD uint16_t +#define UDOUBLE uint32_t + +struct ICNT86TouchscreenStore { + volatile bool touch; + ISRInternalGPIOPin pin; + + static void gpio_intr(ICNT86TouchscreenStore *store); +}; + +class ICNT86Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void reset_(); + void I2C_Write_Byte_(UWORD Reg, char *Data, UBYTE len); + void ICNT_Read_(UWORD Reg, char *Data, UBYTE len); + void ICNT_Write_(UWORD Reg, char *Data, UBYTE len); + void I2C_Read_Byte_(UWORD Reg, char *Data, UBYTE len); + void ICNT_ReadVersion_(); + + ICNT86TouchscreenStore store_; + + InternalGPIOPin *interrupt_pin_; + GPIOPin *reset_pin_{nullptr}; +}; + +} // namespace icnt86 +} // namespace esphome diff --git a/esphome/components/icnt86/touchscreen.py b/esphome/components/icnt86/touchscreen.py new file mode 100644 index 0000000000..2c38057c99 --- /dev/null +++ b/esphome/components/icnt86/touchscreen.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + + +CODEOWNERS = ["@siemon-geeroms"] +DEPENDENCIES = ["i2c"] + +icnt86_ns = cg.esphome_ns.namespace("icnt86") +ICNT86Touchscreen = icnt86_ns.class_( + "ICNT86Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) + +CONF_ICNT86_ID = "icnt86_id" +CONF_RTS_PIN = "rts_pin" + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ICNT86Touchscreen), + cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x48)) + .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) + await touchscreen.register_touchscreen(var, config) + + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) + + if CONF_RESET_PIN in config: + rts_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(rts_pin))