[gsl3670] Add new touchscreen component (#16285)

This commit is contained in:
Clyde Stubbs
2026-07-09 12:04:43 +12:00
committed by GitHub
parent 7c130fc970
commit 8ccf0dbd37
9 changed files with 780 additions and 25 deletions
+1
View File
@@ -209,6 +209,7 @@ esphome/components/gree/switch/* @nagyrobi
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte
esphome/components/gsl3670/* @clydebarrow
esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@clydebarrow"]
@@ -0,0 +1,167 @@
#include "gsl3670_touchscreen.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome::gsl3670 {
static const char *const TAG = "gsl3670.touchscreen";
static const size_t MAX_TOUCHES = 3;
// ---------------------------------------------------------------------------
// setup() – mirrors esp_lcd_touch_gsl3670_init() in the Seeed BSP:
// clear_reg → reset → load_fw → startup_chip → reset → startup_chip
// ---------------------------------------------------------------------------
void GSL3670Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up GSL3670 touchscreen...");
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
}
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = this->display_->get_native_height();
}
this->clear_reg_();
this->reset_();
this->load_firmware_();
this->startup_chip_();
this->reset_();
this->startup_chip_();
ESP_LOGCONFIG(TAG, "GSL3670 initialised OK");
}
void GSL3670Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG,
"GSL3670 Touchscreen:\n"
" X-raw-max: %d\n"
" Y-raw-max: %d\n",
this->x_raw_max_, this->y_raw_max_);
LOG_I2C_DEVICE(this);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
ESP_LOGCONFIG(TAG, " Firmware records: %zu", this->firmware_len_);
}
// ---------------------------------------------------------------------------
// update_touches() – mirrors esp_lcd_touch_gsl3670_read_data() in Seeed BSP
// ---------------------------------------------------------------------------
void GSL3670Touchscreen::update_touches() {
uint8_t buf[44] = {};
auto err = this->read_register(0x80, buf, sizeof(buf));
if (err != i2c::ERROR_OK) {
ESP_LOGW(TAG, "I2C read failed (%d)", err);
return;
}
uint8_t finger_num = clamp_at_most(buf[0], MAX_TOUCHES);
// Build gsl_touch_info exactly as the Seeed driver does
for (uint8_t j = 0; j != finger_num; j++) {
// buf[(j+1)*4 + 0..3]: byte0=y_lo, byte1=y_hi, byte2=x_lo, byte3=id|x_hi
auto x = (uint16_t) (((buf[(j + 1) * 4 + 3] & 0x0f) << 8) | buf[(j + 1) * 4 + 2]);
auto y = (uint16_t) ((buf[(j + 1) * 4 + 1] << 8) | buf[(j + 1) * 4 + 0]);
auto id = (buf[(j + 1) * 4 + 3] >> 4) & 0x0f;
ESP_LOGV(TAG, "Touch id=%u, x=%u y=%u", id, x, y);
if (x <= 8192 && y <= 8192)
this->add_raw_touch_position_(id, x, y);
}
}
// ---------------------------------------------------------------------------
// clear_reg_() – mirrors esp_lcd_touch_gsl3670_clear_reg()
// GPIO reset → write 0x01 to 0x88 → write 0x04 to 0xe4 → write 0x00 to 0xe0
// ---------------------------------------------------------------------------
void GSL3670Touchscreen::clear_reg_() {
ESP_LOGD(TAG, "clear_reg");
// GPIO reset pulse
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
delay(5);
}
this->write_reg8_(0x88, 0x01);
// delay(5);
this->write_reg8_(0xe4, 0x04);
// delay(5);
this->write_reg8_(0xe0, 0x00);
// delay(5);
}
// ---------------------------------------------------------------------------
// reset_() – mirrors touch_gsl3670_reset()
// GPIO reset → write 0x04 to 0xe4 → write 4×0x00 to 0xbc
// ---------------------------------------------------------------------------
void GSL3670Touchscreen::reset_() {
ESP_LOGD(TAG, "reset");
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
delay(5);
}
this->write_reg8_(0xe4, 0x04);
uint8_t zeros[4] = {0, 0, 0, 0};
this->write_reg_(0xbc, zeros, 4);
}
void GSL3670Touchscreen::load_firmware_() {
if (firmware_ == nullptr || firmware_len_ == 0) {
ESP_LOGW(TAG, "No firmware supplied – skipping");
return;
}
ESP_LOGD(TAG, "Loading firmware (%zu blocks)...", firmware_len_);
static constexpr size_t FW_BLK_SIZE = 128 + 4;
for (size_t i = 0; i != this->firmware_len_; i++) {
auto offset = i * FW_BLK_SIZE;
uint8_t val = this->firmware_[offset + 0];
ESP_LOGV(TAG, "Firmware address 0x%02X", val);
this->write_reg_(0xf0, &val, 1);
this->write_reg_(0, this->firmware_ + offset + 4, 128);
}
ESP_LOGD(TAG, "Firmware load complete");
}
// ---------------------------------------------------------------------------
// startup_chip_() – mirrors esp_lcd_touch_gsl3670_startup_chip()
// write 0x00 to 0xe0
// ---------------------------------------------------------------------------
void GSL3670Touchscreen::startup_chip_() {
ESP_LOGD(TAG, "startup_chip");
this->write_reg8_(0xe0, 0x00);
delay(5);
}
// ---------------------------------------------------------------------------
// I2C helpers
// ---------------------------------------------------------------------------
bool GSL3670Touchscreen::write_reg_(uint8_t reg, const uint8_t *data, size_t len) {
auto err = this->write_register(reg, data, len);
if (err != i2c::ERROR_OK) {
ESP_LOGW(TAG, "I2C write reg 0x%02X len %zu failed (%d)", reg, len, err);
return false;
}
return true;
}
bool GSL3670Touchscreen::write_reg8_(uint8_t reg, uint8_t val) { return write_reg_(reg, &val, 1); }
} // namespace esphome::gsl3670
@@ -0,0 +1,50 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::gsl3670 {
// ---------------------------------------------------------------------------
// GSL3670 touchscreen ESPHome component
// ---------------------------------------------------------------------------
class GSL3670Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
/// Supply the firmware table (generated by codegen from the YAML)
void set_firmware(const uint8_t *fw, size_t len) {
this->firmware_ = fw;
this->firmware_len_ = len;
}
void set_interrupt_pin(InternalGPIOPin *pin) { interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { reset_pin_ = pin; }
// touchscreen::Touchscreen / Component interface
void setup() override;
void dump_config() override;
protected:
void update_touches() override;
private:
// ---------- init steps (mirrors esp_lcd_touch_gsl3670_init) ----------
void clear_reg_(); // GPIO reset + 0x88/0xe4/0xe0 sequence
void reset_(); // GPIO reset + 0xe4/0xbc sequence
void load_firmware_(); // write GSLX670_FW table
void startup_chip_(); // 0x00→0xe0 + gsl_DataInit
// ---------- I2C helpers ----------
bool write_reg_(uint8_t reg, const uint8_t *data, size_t len);
bool write_reg8_(uint8_t reg, uint8_t val);
InternalGPIOPin *interrupt_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
const uint8_t *firmware_{nullptr};
size_t firmware_len_{0};
};
} // namespace esphome::gsl3670
File diff suppressed because it is too large Load Diff
+64 -25
View File
@@ -60,40 +60,79 @@ def validate_calibration(calibration_config):
return calibration_config
CALIBRATION_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095),
cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095),
cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095),
cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095),
}
),
validate_calibration,
)
def option_with_default(option: str, defaults: dict, required: bool = False):
if option in defaults or not required:
return cv.Optional(option, default=defaults.get(option, cv.UNDEFINED))
return cv.Required(option)
def touchscreen_schema(default_touch_timeout=cv.UNDEFINED, calibration_required=False):
calibration = (
cv.Required(CONF_CALIBRATION)
if calibration_required
else cv.Optional(CONF_CALIBRATION)
)
_CALIBRATION_KEYS = {CONF_X_MIN, CONF_X_MAX, CONF_Y_MIN, CONF_Y_MAX}
_TRANSFORM_KEYS = {CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y}
def _calibration_schema(defaults: dict, required: bool) -> dict:
"""
Generate Calibration schema. If defaults are provided for all suboptions,
the entire calibration config is optional with a populated default value.
Otherwise, it's optional or required as specified.
"""
if _CALIBRATION_KEYS.issubset(defaults):
key = cv.Optional(
CONF_CALIBRATION,
default={k: v for k, v in defaults.items() if k in _CALIBRATION_KEYS},
)
elif required:
key = cv.Required(CONF_CALIBRATION)
else:
key = cv.Optional(CONF_CALIBRATION)
return {
key: cv.All(
cv.Schema(
{
option_with_default(x, defaults, True): cv.int_range(
min=0, max=4095
)
for x in _CALIBRATION_KEYS
}
),
validate_calibration,
)
}
def _transform_schema(defaults: dict) -> dict:
if _TRANSFORM_KEYS.issubset(defaults):
key = cv.Optional(
CONF_TRANSFORM,
default={k: v for k, v in defaults.items() if k in _TRANSFORM_KEYS},
)
else:
key = cv.Optional(CONF_TRANSFORM)
return {
key: cv.Schema(
{
cv.Optional(x, default=defaults.get(x, False)): cv.boolean
for x in _TRANSFORM_KEYS
}
)
}
def touchscreen_schema(
default_touch_timeout=cv.UNDEFINED,
calibration_required=False,
defaults: dict = None,
) -> cv.Schema:
defaults = defaults or {}
return cv.Schema(
{
cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
}
),
cv.Optional(CONF_TOUCH_TIMEOUT, default=default_touch_timeout): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
calibration: CALIBRATION_SCHEMA,
**_transform_schema(defaults),
**_calibration_schema(defaults, calibration_required),
cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True),
cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True),
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
xl9535:
id: expander
display:
- platform: mipi_spi
spi_id: spi_bus
model: t-display-s3-pro
psram:
mode: quad
touchscreen:
# Firmware downloaded from the model's default release URL and cached.
- platform: gsl3670
model: seeed-reterminal-d1001
interrupt_pin: 18
# Explicit firmware URL + SHA-256 override.
- platform: gsl3670
model: seeed-reterminal-d1001
reset_pin: 10
interrupt_pin: 11
firmware:
url: https://github.com/esphome-libs/gsl3670-firmware/releases/download/v1.0.0/seeed-d1001-fw.bin
sha256: 2e50501ad83656fb6fa3d92591f9f31add4d442c8e8a79f29f5c4d335bd127a4