diff --git a/CODEOWNERS b/CODEOWNERS index e0f9d2d2ef..a851e62a3e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -519,6 +519,7 @@ esphome/components/sound_level/* @kahrendt esphome/components/spa06_base/* @danielkent-net esphome/components/spa06_i2c/* @danielkent-net esphome/components/spa06_spi/* @danielkent-net +esphome/components/spd2010/* @grischard esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spd2010/__init__.py b/esphome/components/spd2010/__init__.py new file mode 100644 index 0000000000..3fc6c6e940 --- /dev/null +++ b/esphome/components/spd2010/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@grischard"] +DEPENDENCIES = ["i2c"] diff --git a/esphome/components/spd2010/spd2010_touchscreen.cpp b/esphome/components/spd2010/spd2010_touchscreen.cpp new file mode 100644 index 0000000000..94fad87d7d --- /dev/null +++ b/esphome/components/spd2010/spd2010_touchscreen.cpp @@ -0,0 +1,211 @@ +// SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD +// SPDX-License-Identifier: Apache-2.0 +// Protocol adapted from esp_lcd_touch_spd2010 2.0.1 for ESPHome. +// https://github.com/espressif/esp-bsp/tree/master/components/lcd_touch/esp_lcd_touch_spd2010 + +#include "spd2010_touchscreen.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::spd2010 { + +ESPHOME_LOG_TAG(TAG, "spd2010.touchscreen"); + +static constexpr uint16_t REG_CLEAR_INTERRUPT = 0x0002; +static constexpr uint16_t REG_CPU_START = 0x0004; +static constexpr uint16_t REG_STATUS = 0x0020; +static constexpr uint16_t REG_VERSION = 0x0026; +static constexpr uint16_t REG_TOUCH_START = 0x0046; +static constexpr uint16_t REG_POINT_MODE = 0x0050; +static constexpr uint16_t REG_HDP_STATUS = 0x02FC; +static constexpr uint16_t REG_HDP = 0x0300; + +// Low status byte +static constexpr uint8_t STATUS_POINT = 0x01; +static constexpr uint8_t STATUS_GESTURE = 0x02; +static constexpr uint8_t STATUS_AUX = 0x08; +// High status byte +static constexpr uint8_t STATUS_BIOS = 0x40; +static constexpr uint8_t STATUS_CPU = 0x20; +static constexpr uint8_t STATUS_RUNNING = 0x08; + +static constexpr uint8_t HDP_STATUS_DONE = 0x82; +static constexpr uint8_t HDP_STATUS_MORE = 0x00; + +static constexpr uint8_t MAX_CONTACT_ID = 0x0A; +static constexpr uint8_t GESTURE_REPORT_ID = 0xF6; +static constexpr size_t REPORT_HEADER_LENGTH = 4; +static constexpr size_t CONTACT_LENGTH = 6; +static constexpr size_t MAX_REPORT_LENGTH = REPORT_HEADER_LENGTH + 10 * CONTACT_LENGTH; +static constexpr uint8_t MAX_DRAIN_READS = 8; + +static constexpr uint32_t RESET_PULSE_WIDTH = + 20; // longer than the datasheet says, but the controller seems to need it +static constexpr uint32_t RESET_DELAY = 20; + +bool SPD2010Touchscreen::read_register_(uint16_t reg, uint8_t *data, size_t length) const { + const uint8_t command[] = {static_cast(reg), static_cast(reg >> 8u)}; + if (this->write(command, sizeof(command)) != i2c::ERROR_OK) + return false; + // The controller needs a STOP and a delay before a commandless receive. + delay_microseconds_safe(200); + return this->read(data, length) == i2c::ERROR_OK; +} + +bool SPD2010Touchscreen::write_command_(uint16_t reg, uint16_t value) const { + const uint8_t command[]{ + static_cast(reg), + static_cast(reg >> 8u), + static_cast(value), + static_cast(value >> 8u), + }; + return this->write(command, sizeof(command)) == i2c::ERROR_OK; +} + +bool SPD2010Touchscreen::clear_interrupt_() const { return this->write_command_(REG_CLEAR_INTERRUPT, 1); } + +void SPD2010Touchscreen::setup() { + if (this->x_raw_max_ == this->x_raw_min_) + this->x_raw_max_ = this->display_->get_native_width() - 1; + if (this->y_raw_max_ == this->y_raw_min_) + this->y_raw_max_ = this->display_->get_native_height() - 1; + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + this->set_timeout(RESET_PULSE_WIDTH, [this] { + this->reset_pin_->digital_write(true); + this->set_timeout(RESET_DELAY, [this] { this->initialize_(); }); + }); + } else { + this->initialize_(); + } +} + +void SPD2010Touchscreen::initialize_() { + uint8_t version[18]; + if (!this->read_register_(REG_VERSION, version, sizeof(version))) { + this->mark_failed(LOG_STR("Failed to read firmware version")); + return; + } + ESP_LOGD(TAG, "Firmware version: 0x%04X", encode_uint16(version[5], version[4])); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + this->ready_ = true; +} + +bool SPD2010Touchscreen::finish_report_() { + // A corrupt status must not cause an unbounded drain loop or buffer overflow. + for (uint8_t attempt = 0; attempt != MAX_DRAIN_READS; attempt++) { + uint8_t status[8]; + if (!this->read_register_(REG_HDP_STATUS, status, sizeof(status))) + return false; + if (status[5] == HDP_STATUS_DONE) + return this->clear_interrupt_(); + const uint16_t length = encode_uint16(status[3], status[2]); + if (status[5] != HDP_STATUS_MORE || length == 0 || length > MAX_REPORT_LENGTH) + break; + uint8_t remaining[MAX_REPORT_LENGTH]; + if (!this->read_register_(REG_HDP, remaining, length)) + return false; + } + ESP_LOGW(TAG, "Invalid or unfinished report"); + (void) this->clear_interrupt_(); + return false; +} + +// Returns true only when touch data was read correctly and should be published. +bool SPD2010Touchscreen::read_data_() { + uint8_t status[4]; + if (!this->read_register_(REG_STATUS, status, sizeof(status))) { + this->status_set_warning(LOG_STR("Failed to read status")); + return false; + } + const uint8_t status_low = status[0]; + const uint8_t status_high = status[1]; + const uint16_t length = encode_uint16(status[3], status[2]); + + if (status_high & STATUS_BIOS) { + this->status_set_warning(LOG_STR("BIOS status; start CPU")); + (void) (this->clear_interrupt_() && this->write_command_(REG_CPU_START, 1)); + return false; + } + if (status_high & STATUS_CPU) { + this->status_set_warning(LOG_STR("CPU status; stop point mode, start touch")); + (void) (this->write_command_(REG_POINT_MODE, 0) && this->write_command_(REG_TOUCH_START, 0) && + this->clear_interrupt_()); + return false; + } + if ((status_high & STATUS_RUNNING) && length == 0) { + if (!this->clear_interrupt_()) { + this->status_set_warning(LOG_STR("Failed to clear interrupt")); + return false; + } + this->status_clear_warning(); + return true; + } + if (!(status_low & (STATUS_POINT | STATUS_GESTURE))) { + if ((status_high & STATUS_RUNNING) && (status_low & STATUS_AUX) && !this->clear_interrupt_()) { + this->status_set_warning(LOG_STR("Failed to clear interrupt")); + return false; + } + this->status_clear_warning(); + return false; + } + if (length < REPORT_HEADER_LENGTH || length > MAX_REPORT_LENGTH || + (length - REPORT_HEADER_LENGTH) % CONTACT_LENGTH != 0) { + ESP_LOGW(TAG, "Invalid report length: %u", length); + this->status_set_warning(LOG_STR("Invalid report length")); + (void) this->clear_interrupt_(); + return false; + } + uint8_t report[MAX_REPORT_LENGTH]; + if (!this->read_register_(REG_HDP, report, length) || !this->finish_report_()) { + this->status_set_warning(LOG_STR("Failed to read report")); + return false; + } + // Gesture reports are not used. + if (!(status_low & STATUS_POINT) || + (length > REPORT_HEADER_LENGTH && report[REPORT_HEADER_LENGTH] == GESTURE_REPORT_ID)) { + this->status_clear_warning(); + return false; + } + // Check every ID before adding any point, so a bad report leaves the stored touches unchanged. + for (size_t offset = REPORT_HEADER_LENGTH; offset < length; offset += CONTACT_LENGTH) { + if (report[offset] > MAX_CONTACT_ID) { + this->status_set_warning(LOG_STR("Invalid contact ID")); + return false; + } + } + for (size_t offset = REPORT_HEADER_LENGTH; offset < length; offset += CONTACT_LENGTH) { + const uint8_t *contact = report + offset; + // Zero strength marks a released contact. + if (contact[4] == 0) + continue; + const uint16_t x = ((contact[3] & 0xF0u) << 4u) | contact[1]; + const uint16_t y = ((contact[3] & 0x0Fu) << 8u) | contact[2]; + this->add_raw_touch_position_(contact[0], x, y, contact[4]); + } + this->status_clear_warning(); + return true; +} + +void SPD2010Touchscreen::update_touches() { + if (!this->ready_ || !this->read_data_()) + this->skip_update_ = true; +} + +void SPD2010Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, + "SPD2010 Touchscreen:\n" + " Raw X range: %d-%d\n" + " Raw Y range: %d-%d", + this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +} // namespace esphome::spd2010 diff --git a/esphome/components/spd2010/spd2010_touchscreen.h b/esphome/components/spd2010/spd2010_touchscreen.h new file mode 100644 index 0000000000..8817396872 --- /dev/null +++ b/esphome/components/spd2010/spd2010_touchscreen.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/hal.h" + +namespace esphome::spd2010 { + +class SPD2010Touchscreen final : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void initialize_(); + void update_touches() override; + bool read_data_(); + bool read_register_(uint16_t reg, uint8_t *data, size_t length) const; + bool write_command_(uint16_t reg, uint16_t value) const; + bool clear_interrupt_() const; + bool finish_report_(); + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + bool ready_{false}; +}; + +} // namespace esphome::spd2010 diff --git a/esphome/components/spd2010/touchscreen.py b/esphome/components/spd2010/touchscreen.py new file mode 100644 index 0000000000..7ead6a3418 --- /dev/null +++ b/esphome/components/spd2010/touchscreen.py @@ -0,0 +1,33 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c, touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN +from esphome.types import ConfigType + +spd2010_ns = cg.esphome_ns.namespace("spd2010") +SPD2010Touchscreen = spd2010_ns.class_( + "SPD2010Touchscreen", touchscreen.Touchscreen, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + touchscreen.touchscreen_schema("50ms") + .extend( + { + cv.GenerateID(): cv.declare_id(SPD2010Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x53)) +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/tests/components/spd2010/common.yaml b/tests/components/spd2010/common.yaml new file mode 100644 index 0000000000..0692137638 --- /dev/null +++ b/tests/components/spd2010/common.yaml @@ -0,0 +1,25 @@ +display: + - platform: ssd1306_i2c + id: spd2010_display + model: SSD1306_128X64 + +touchscreen: + - platform: spd2010 + id: spd2010_touchscreen + display: spd2010_display + interrupt_pin: ${interrupt_pin} + reset_pin: ${reset_pin} + - platform: spd2010 + id: spd2010_polling_touchscreen + display: spd2010_display + address: 0x54 + update_interval: 100ms + calibration: + x_min: 0 + x_max: 479 + y_min: 0 + y_max: 479 + transform: + swap_xy: true + mirror_x: true + mirror_y: false diff --git a/tests/components/spd2010/test.esp32-idf.yaml b/tests/components/spd2010/test.esp32-idf.yaml new file mode 100644 index 0000000000..ca2be87d29 --- /dev/null +++ b/tests/components/spd2010/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + interrupt_pin: "20" + reset_pin: "21" + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + spd2010: !include common.yaml