Add support for the XPT2046 touchscreen controller (#1542)

This commit is contained in:
Stanislav Meduna 2021-05-17 03:03:58 +02:00 committed by GitHub
parent 9ecead2645
commit d0eaebe19f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 562 additions and 0 deletions

View file

@ -123,3 +123,4 @@ esphome/components/web_server_base/* @OttoWinter
esphome/components/whirlpool/* @glmnet
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xpt2046/* @numo68

View file

@ -0,0 +1,129 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome import pins
from esphome.components import spi
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_THRESHOLD, CONF_TRIGGER_ID
CODEOWNERS = ["@numo68"]
AUTO_LOAD = ["binary_sensor"]
DEPENDENCIES = ["spi"]
MULTI_CONF = True
CONF_REPORT_INTERVAL = "report_interval"
CONF_CALIBRATION_X_MIN = "calibration_x_min"
CONF_CALIBRATION_X_MAX = "calibration_x_max"
CONF_CALIBRATION_Y_MIN = "calibration_y_min"
CONF_CALIBRATION_Y_MAX = "calibration_y_max"
CONF_DIMENSION_X = "dimension_x"
CONF_DIMENSION_Y = "dimension_y"
CONF_SWAP_X_Y = "swap_x_y"
CONF_IRQ_PIN = "irq_pin"
xpt2046_ns = cg.esphome_ns.namespace("xpt2046")
CONF_XPT2046_ID = "xpt2046_id"
XPT2046Component = xpt2046_ns.class_(
"XPT2046Component", cg.PollingComponent, spi.SPIDevice
)
XPT2046OnStateTrigger = xpt2046_ns.class_(
"XPT2046OnStateTrigger", automation.Trigger.template(cg.int_, cg.int_, cg.bool_)
)
def validate_xpt2046(config):
if (
abs(
cv.int_(config[CONF_CALIBRATION_X_MAX])
- cv.int_(config[CONF_CALIBRATION_X_MIN])
)
< 1000
):
raise cv.Invalid("Calibration X values difference < 1000")
if (
abs(
cv.int_(config[CONF_CALIBRATION_Y_MAX])
- cv.int_(config[CONF_CALIBRATION_Y_MIN])
)
< 1000
):
raise cv.Invalid("Calibration Y values difference < 1000")
return config
def report_interval(value):
if value == "never":
return 4294967295 # uint32_t max
return cv.positive_time_period_milliseconds(value)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(XPT2046Component),
cv.Optional(CONF_IRQ_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range(
min=0, max=4095
),
cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range(
min=0, max=4095
),
cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range(
min=0, max=4095
),
cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range(
min=0, max=4095
),
cv.Optional(CONF_DIMENSION_X, default=100): cv.positive_not_null_int,
cv.Optional(CONF_DIMENSION_Y, default=100): cv.positive_not_null_int,
cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095),
cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval,
cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean,
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
XPT2046OnStateTrigger
),
}
),
}
)
.extend(cv.polling_component_schema("50ms"))
.extend(spi.spi_device_schema()),
validate_xpt2046,
)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield spi.register_spi_device(var, config)
cg.add(var.set_threshold(config[CONF_THRESHOLD]))
cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL]))
cg.add(var.set_dimensions(config[CONF_DIMENSION_X], config[CONF_DIMENSION_Y]))
cg.add(
var.set_calibration(
config[CONF_CALIBRATION_X_MIN],
config[CONF_CALIBRATION_X_MAX],
config[CONF_CALIBRATION_Y_MIN],
config[CONF_CALIBRATION_Y_MAX],
)
)
if CONF_SWAP_X_Y in config:
cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y]))
if CONF_IRQ_PIN in config:
pin = yield cg.gpio_pin_expression(config[CONF_IRQ_PIN])
cg.add(var.set_irq_pin(pin))
for conf in config.get(CONF_ON_STATE, []):
yield automation.build_automation(
var.get_on_state_trigger(),
[(cg.int_, "x"), (cg.int_, "y"), (cg.bool_, "touched")],
conf,
)

View file

@ -0,0 +1,57 @@
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 (
xpt2046_ns,
XPT2046Component,
CONF_XPT2046_ID,
)
CONF_X_MIN = "x_min"
CONF_X_MAX = "x_max"
CONF_Y_MIN = "y_min"
CONF_Y_MAX = "y_max"
DEPENDENCIES = ["xpt2046"]
XPT2046Button = xpt2046_ns.class_("XPT2046Button", binary_sensor.BinarySensor)
def validate_xpt2046_button(config):
if cv.int_(config[CONF_X_MAX]) < cv.int_(config[CONF_X_MIN]) or cv.int_(
config[CONF_Y_MAX]
) < cv.int_(config[CONF_Y_MIN]):
raise cv.Invalid("x_max is less than x_min or y_max is less than y_min")
return config
CONFIG_SCHEMA = cv.All(
binary_sensor.BINARY_SENSOR_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(XPT2046Button),
cv.GenerateID(CONF_XPT2046_ID): cv.use_id(XPT2046Component),
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_xpt2046_button,
)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield binary_sensor.register_binary_sensor(var, config)
hub = yield cg.get_variable(config[CONF_XPT2046_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_button(var))

View file

@ -0,0 +1,217 @@
#include "xpt2046.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <algorithm>
namespace esphome {
namespace xpt2046 {
static const char *TAG = "xpt2046";
void XPT2046Component::setup() {
if (this->irq_pin_ != nullptr) {
// The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state
// while the channels are read and wiring it as an interrupt is not straightforward and would
// need careful masking. A GPIO poll is cheap so we'll just use that.
this->irq_pin_->setup(); // INPUT
}
spi_setup();
read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin
}
void XPT2046Component::loop() {
if (this->irq_pin_ != nullptr) {
// Force immediate update if a falling edge (= touched is seen) Ignore if still active
// (that would mean that we missed the release because of a too long update interval)
bool val = this->irq_pin_->digital_read();
if (!val && this->last_irq_ && !this->touched) {
ESP_LOGD(TAG, "Falling penirq edge, forcing update");
update();
}
this->last_irq_ = val;
}
}
void XPT2046Component::update() {
int16_t data[6];
bool touch = false;
unsigned long now = millis();
this->z_raw = 0;
// In case the penirq pin is present only do the SPI transaction if it reports a touch (is low).
// The touch has to be also confirmed with checking the pressure over threshold
if (this->irq_pin_ == nullptr || !this->irq_pin_->digital_read()) {
enable();
int16_t z1 = read_adc_(0xB1 /* Z1 */);
int16_t z2 = read_adc_(0xC1 /* Z2 */);
this->z_raw = z1 + 4095 - z2;
touch = (this->z_raw >= this->threshold_);
if (touch) {
read_adc_(0x91 /* Y */); // dummy Y measure, 1st is always noisy
data[0] = read_adc_(0xD1 /* X */);
data[1] = read_adc_(0x91 /* Y */); // make 3 x-y measurements
data[2] = read_adc_(0xD1 /* X */);
data[3] = read_adc_(0x91 /* Y */);
data[4] = read_adc_(0xD1 /* X */);
}
data[5] = read_adc_(0x90 /* Y */); // Last Y touch power down
disable();
}
if (touch) {
this->x_raw = best_two_avg(data[0], data[2], data[4]);
this->y_raw = best_two_avg(data[1], data[3], data[5]);
} else {
this->x_raw = this->y_raw = 0;
}
ESP_LOGV(TAG, "Update [x, y] = [%d, %d], z = %d%s", this->x_raw, this->y_raw, this->z_raw, (touch ? " touched" : ""));
if (touch) {
// Normalize raw data according to calibration min and max
int16_t x_val = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_);
int16_t y_val = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_);
if (this->swap_x_y_) {
std::swap(x_val, y_val);
}
if (this->invert_x_) {
x_val = 0x7fff - x_val;
}
if (this->invert_y_) {
y_val = 0x7fff - y_val;
}
x_val = (int16_t)((int) x_val * this->x_dim_ / 0x7fff);
y_val = (int16_t)((int) y_val * this->y_dim_ / 0x7fff);
if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) {
ESP_LOGD(TAG, "Raw [x, y] = [%d, %d], transformed = [%d, %d]", this->x_raw, this->y_raw, x_val, y_val);
this->x = x_val;
this->y = y_val;
this->touched = true;
this->last_pos_ms_ = now;
this->on_state_trigger_->process(this->x, this->y, true);
for (auto *button : this->buttons_)
button->touch(this->x, this->y);
}
} else {
if (this->touched) {
ESP_LOGD(TAG, "Released [%d, %d]", this->x, this->y);
this->touched = false;
this->on_state_trigger_->process(this->x, this->y, false);
for (auto *button : this->buttons_)
button->release();
}
}
}
void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) {
this->x_raw_min_ = std::min(x_min, x_max);
this->x_raw_max_ = std::max(x_min, x_max);
this->y_raw_min_ = std::min(y_min, y_max);
this->y_raw_max_ = std::max(y_min, y_max);
this->invert_x_ = (x_min > x_max);
this->invert_y_ = (y_min > y_max);
}
void XPT2046Component::dump_config() {
ESP_LOGCONFIG(TAG, "XPT2046:");
LOG_PIN(" IRQ Pin: ", this->irq_pin_);
ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_);
ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_);
ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_);
ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_);
ESP_LOGCONFIG(TAG, " X dim: %d", this->x_dim_);
ESP_LOGCONFIG(TAG, " Y dim: %d", this->y_dim_);
if (this->swap_x_y_) {
ESP_LOGCONFIG(TAG, " Swap X/Y");
}
ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_);
ESP_LOGCONFIG(TAG, " Report interval: %u", this->report_millis_);
LOG_UPDATE_INTERVAL(this);
}
float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; }
int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) {
int16_t da, db, dc;
int16_t reta = 0;
da = (x > y) ? x - y : y - x;
db = (x > z) ? x - z : z - x;
dc = (z > y) ? z - y : y - z;
if (da <= db && da <= dc) {
reta = (x + y) >> 1;
} else if (db <= da && db <= dc) {
reta = (x + z) >> 1;
} else {
reta = (y + z) >> 1;
}
return reta;
}
int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_val) {
int16_t ret;
if (val <= min_val) {
ret = 0;
} else if (val >= max_val) {
ret = 0x7fff;
} else {
ret = (int16_t)((int) 0x7fff * (val - min_val) / (max_val - min_val));
}
return ret;
}
int16_t XPT2046Component::read_adc_(uint8_t ctrl) {
uint8_t data[2];
write_byte(ctrl);
data[0] = read_byte();
data[1] = read_byte();
return ((data[0] << 8) | data[1]) >> 3;
}
void XPT2046OnStateTrigger::process(int x, int y, bool touched) { this->trigger(x, y, touched); }
void XPT2046Button::touch(int16_t x, int16_t y) {
bool touched = (x >= this->x_min_ && x <= this->x_max_ && y >= this->y_min_ && y <= this->y_max_);
if (touched) {
this->publish_state(true);
this->state_ = true;
} else {
release();
}
}
void XPT2046Button::release() {
if (this->state_) {
this->publish_state(false);
this->state_ = false;
}
}
} // namespace xpt2046
} // namespace esphome

View file

@ -0,0 +1,124 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace xpt2046 {
class XPT2046OnStateTrigger : public Trigger<int, int, bool> {
public:
void process(int x, int y, bool touched);
};
class XPT2046Button : public binary_sensor::BinarySensor {
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(int16_t x, int16_t y);
void release();
protected:
int16_t x_min_, x_max_, y_min_, y_max_;
bool state_{false};
};
class XPT2046Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
public:
/// Set the logical touch screen dimensions.
void set_dimensions(int16_t x, int16_t y) {
this->x_dim_ = x;
this->y_dim_ = y;
}
/// Set the coordinates for the touch screen edges.
void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max);
/// If true the x and y axes will be swapped
void set_swap_x_y(bool val) { this->swap_x_y_ = val; }
/// Set the interval to report the touch point perodically.
void set_report_interval(uint32_t interval) { this->report_millis_ = interval; }
/// Set the threshold for the touch detection.
void set_threshold(int16_t threshold) { this->threshold_ = threshold; }
/// Set the pin used to detect the touch.
void set_irq_pin(GPIOPin *pin) { this->irq_pin_ = pin; }
/// Get an access to the on_state automation trigger
XPT2046OnStateTrigger *get_on_state_trigger() const { return this->on_state_trigger_; }
/// Register a virtual button to the component.
void register_button(XPT2046Button *button) { this->buttons_.push_back(button); }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
/** Detect the touch if the irq pin is specified.
*
* If the touch is detected and the component does not already know about it
* the update() is called immediately. If the irq pin is not specified
* the loop() is a no-op.
*/
void loop() override;
/** Read and process the values from the hardware.
*
* Read the raw x, y and touch pressure values from the chip, detect the touch,
* and if touched, transform to the user x and y coordinates. If the state has
* changed or if the value should be reported again due to the
* report interval, run the action and inform the virtual buttons.
*/
void update() override;
/**@{*/
/** Coordinates of the touch position.
*
* The values are set immediately before the on_state action with touched == true
* is triggered. The action with touched == false sends the coordinates of the last
* reported touch.
*/
int16_t x{0}, y{0};
/**@}*/
/// True if the component currently detects the touch
bool touched{false};
/**@{*/
/** Raw sensor values of the coordinates and the pressure.
*
* The values are set each time the update() method is called.
*/
int16_t x_raw{0}, y_raw{0}, z_raw{0};
/**@}*/
protected:
static int16_t best_two_avg(int16_t x, int16_t y, int16_t z);
static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val);
int16_t read_adc_(uint8_t ctrl);
int16_t threshold_;
int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_;
int16_t x_dim_, y_dim_;
bool invert_x_, invert_y_;
bool swap_x_y_;
uint32_t report_millis_;
unsigned long last_pos_ms_{0};
GPIOPin *irq_pin_{nullptr};
bool last_irq_{true};
XPT2046OnStateTrigger *on_state_trigger_{new XPT2046OnStateTrigger()};
std::vector<XPT2046Button *> buttons_{};
};
} // namespace xpt2046
} // namespace esphome

View file

@ -100,6 +100,15 @@ binary_sensor:
on_state:
then:
- lambda: 'ESP_LOGI("ar1:", "%d", x);'
- platform: xpt2046
xpt2046_id: touchscreen
id: touch_key0
x_min: 80
x_max: 160
y_min: 106
y_max: 212
on_state:
- lambda: 'ESP_LOGI("main", "key0: %s", (x ? "touch" : "release"));'
climate:
- platform: tuya
@ -180,3 +189,28 @@ external_components:
components: ["bh1750"]
- source: ../esphome/components
components: ["sntp"]
xpt2046:
id: touchscreen
cs_pin: 17
irq_pin: 16
update_interval: 50ms
report_interval: 1s
threshold: 400
dimension_x: 240
dimension_y: 320
calibration_x_min: 3860
calibration_x_max: 280
calibration_y_min: 340
calibration_y_max: 3860
swap_x_y: False
on_state:
- lambda: |-
ESP_LOGI("main", "args x=%d, y=%d, touched=%s", x, y, (touched ? "touch" : "release"));
ESP_LOGI("main", "member x=%d, y=%d, touched=%d, x_raw=%d, y_raw=%d, z_raw=%d",
id(touchscreen).x,
id(touchscreen).y,
(int) id(touchscreen).touched,
id(touchscreen).x_raw,
id(touchscreen).y_raw,
id(touchscreen).z_raw
);