mirror of
https://github.com/Threnklyn/esphome-dev.git
synced 2026-05-23 14:18:28 +02:00
Updating the touchscreen interface structure (#4596)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: NP v/d Spek <github_mail@lumensoft.nl> Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Gustavo Ambrozio <gustavo@gustavo.eng.br>
This commit is contained in:
@@ -3,44 +3,84 @@ import esphome.codegen as cg
|
||||
|
||||
from esphome.components import display
|
||||
from esphome import automation
|
||||
from esphome.const import CONF_ON_TOUCH
|
||||
from esphome.const import CONF_ON_TOUCH, CONF_ON_RELEASE
|
||||
from esphome.core import coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
CODEOWNERS = ["@jesserockz", "@nielsnl68"]
|
||||
DEPENDENCIES = ["display"]
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
touchscreen_ns = cg.esphome_ns.namespace("touchscreen")
|
||||
|
||||
Touchscreen = touchscreen_ns.class_("Touchscreen")
|
||||
Touchscreen = touchscreen_ns.class_("Touchscreen", cg.PollingComponent)
|
||||
TouchRotation = touchscreen_ns.enum("TouchRotation")
|
||||
TouchPoint = touchscreen_ns.struct("TouchPoint")
|
||||
TouchPoints_t = cg.std_vector.template(TouchPoint)
|
||||
TouchPoints_t_const_ref = TouchPoints_t.operator("ref").operator("const")
|
||||
TouchListener = touchscreen_ns.class_("TouchListener")
|
||||
|
||||
CONF_DISPLAY = "display"
|
||||
CONF_TOUCHSCREEN_ID = "touchscreen_id"
|
||||
CONF_REPORT_INTERVAL = "report_interval" # not used yet:
|
||||
CONF_ON_UPDATE = "on_update"
|
||||
|
||||
CONF_MIRROR_X = "mirror_x"
|
||||
CONF_MIRROR_Y = "mirror_y"
|
||||
CONF_SWAP_XY = "swap_xy"
|
||||
CONF_TRANSFORM = "transform"
|
||||
|
||||
|
||||
TOUCHSCREEN_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DISPLAY): cv.use_id(display.DisplayBuffer),
|
||||
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_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),
|
||||
}
|
||||
)
|
||||
).extend(cv.polling_component_schema("50ms"))
|
||||
|
||||
|
||||
async def register_touchscreen(var, config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
disp = await cg.get_variable(config[CONF_DISPLAY])
|
||||
cg.add(var.set_display(disp))
|
||||
|
||||
if CONF_TRANSFORM in config:
|
||||
transform = config[CONF_TRANSFORM]
|
||||
cg.add(var.set_swap_xy(transform[CONF_SWAP_XY]))
|
||||
cg.add(var.set_mirror_x(transform[CONF_MIRROR_X]))
|
||||
cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y]))
|
||||
|
||||
if CONF_ON_TOUCH in config:
|
||||
await automation.build_automation(
|
||||
var.get_touch_trigger(),
|
||||
[(TouchPoint, "touch")],
|
||||
[(TouchPoint, "touch"), (TouchPoints_t_const_ref, "touches")],
|
||||
config[CONF_ON_TOUCH],
|
||||
)
|
||||
|
||||
if CONF_ON_UPDATE in config:
|
||||
await automation.build_automation(
|
||||
var.get_update_trigger(),
|
||||
[(TouchPoints_t_const_ref, "touches")],
|
||||
config[CONF_ON_UPDATE],
|
||||
)
|
||||
|
||||
if CONF_ON_RELEASE in config:
|
||||
await automation.build_automation(
|
||||
var.get_release_trigger(),
|
||||
[],
|
||||
config[CONF_ON_RELEASE],
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
|
||||
@@ -14,11 +14,10 @@ void TouchscreenBinarySensor::touch(TouchPoint tp) {
|
||||
if (this->page_ != nullptr) {
|
||||
touched &= this->page_ == this->parent_->get_display()->get_active_page();
|
||||
}
|
||||
|
||||
if (touched) {
|
||||
this->publish_state(true);
|
||||
} else {
|
||||
release();
|
||||
this->release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,27 +7,128 @@ namespace touchscreen {
|
||||
|
||||
static const char *const TAG = "touchscreen";
|
||||
|
||||
void Touchscreen::set_display(display::Display *display) {
|
||||
this->display_ = display;
|
||||
this->display_width_ = display->get_width();
|
||||
this->display_height_ = display->get_height();
|
||||
this->rotation_ = static_cast<TouchRotation>(display->get_rotation());
|
||||
void TouchscreenInterrupt::gpio_intr(TouchscreenInterrupt *store) { store->touched = true; }
|
||||
|
||||
if (this->rotation_ == ROTATE_90_DEGREES || this->rotation_ == ROTATE_270_DEGREES) {
|
||||
std::swap(this->display_width_, this->display_height_);
|
||||
void Touchscreen::attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type) {
|
||||
irq_pin->attach_interrupt(TouchscreenInterrupt::gpio_intr, &this->store_, type);
|
||||
this->store_.init = true;
|
||||
this->store_.touched = false;
|
||||
}
|
||||
|
||||
void Touchscreen::update() {
|
||||
if (!this->store_.init) {
|
||||
this->store_.touched = true;
|
||||
} else {
|
||||
// no need to poll if we have interrupts.
|
||||
this->stop_poller();
|
||||
}
|
||||
}
|
||||
|
||||
void Touchscreen::send_release_() {
|
||||
for (auto *listener : this->touch_listeners_)
|
||||
listener->release();
|
||||
void Touchscreen::loop() {
|
||||
if (this->store_.touched) {
|
||||
this->first_touch_ = this->touches_.empty();
|
||||
this->need_update_ = false;
|
||||
this->is_touched_ = false;
|
||||
this->skip_update_ = false;
|
||||
for (auto &tp : this->touches_) {
|
||||
if (tp.second.state == STATE_PRESSED || tp.second.state == STATE_UPDATED) {
|
||||
tp.second.state = tp.second.state | STATE_RELEASING;
|
||||
} else {
|
||||
tp.second.state = STATE_RELEASED;
|
||||
}
|
||||
tp.second.x_prev = tp.second.x;
|
||||
tp.second.y_prev = tp.second.y;
|
||||
}
|
||||
this->update_touches();
|
||||
if (this->skip_update_) {
|
||||
for (auto &tp : this->touches_) {
|
||||
tp.second.state = tp.second.state & -STATE_RELEASING;
|
||||
}
|
||||
} else {
|
||||
this->store_.touched = false;
|
||||
this->defer([this]() { this->send_touches_(); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Touchscreen::send_touch_(TouchPoint tp) {
|
||||
ESP_LOGV(TAG, "Touch (x=%d, y=%d)", tp.x, tp.y);
|
||||
this->touch_trigger_.trigger(tp);
|
||||
for (auto *listener : this->touch_listeners_)
|
||||
listener->touch(tp);
|
||||
void Touchscreen::set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) {
|
||||
TouchPoint tp;
|
||||
uint16_t x, y;
|
||||
if (this->touches_.count(id) == 0) {
|
||||
tp.state = STATE_PRESSED;
|
||||
tp.id = id;
|
||||
} else {
|
||||
tp = this->touches_[id];
|
||||
tp.state = STATE_UPDATED;
|
||||
}
|
||||
tp.x_raw = x_raw;
|
||||
tp.y_raw = y_raw;
|
||||
tp.z_raw = z_raw;
|
||||
|
||||
x = this->normalize_(x_raw, this->x_raw_min_, this->x_raw_max_, this->invert_x_);
|
||||
y = this->normalize_(y_raw, this->y_raw_min_, this->y_raw_max_, this->invert_y_);
|
||||
|
||||
if (this->swap_x_y_) {
|
||||
std::swap(x, y);
|
||||
}
|
||||
|
||||
tp.x = (uint16_t) ((int) x * this->get_width_() / 0x1000);
|
||||
tp.y = (uint16_t) ((int) y * this->get_height_() / 0x1000);
|
||||
|
||||
if (tp.state == STATE_PRESSED) {
|
||||
tp.x_org = tp.x;
|
||||
tp.y_org = tp.y;
|
||||
}
|
||||
|
||||
this->touches_[id] = tp;
|
||||
|
||||
this->is_touched_ = true;
|
||||
if ((tp.x != tp.x_prev) || (tp.y != tp.y_prev)) {
|
||||
this->need_update_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Touchscreen::send_touches_() {
|
||||
if (!this->is_touched_) {
|
||||
this->release_trigger_.trigger();
|
||||
for (auto *listener : this->touch_listeners_)
|
||||
listener->release();
|
||||
this->touches_.clear();
|
||||
} else {
|
||||
TouchPoints_t touches;
|
||||
for (auto tp : this->touches_) {
|
||||
touches.push_back(tp.second);
|
||||
}
|
||||
if (this->first_touch_) {
|
||||
TouchPoint tp = this->touches_.begin()->second;
|
||||
this->touch_trigger_.trigger(tp, touches);
|
||||
for (auto *listener : this->touch_listeners_) {
|
||||
listener->touch(tp);
|
||||
}
|
||||
}
|
||||
if (this->need_update_) {
|
||||
this->update_trigger_.trigger(touches);
|
||||
for (auto *listener : this->touch_listeners_) {
|
||||
listener->update(touches);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int16_t Touchscreen::normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted) {
|
||||
int16_t ret;
|
||||
|
||||
if (val <= min_val) {
|
||||
ret = 0;
|
||||
} else if (val >= max_val) {
|
||||
ret = 0xfff;
|
||||
} else {
|
||||
ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val));
|
||||
}
|
||||
|
||||
ret = (inverted) ? 0xfff - ret : ret;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace touchscreen
|
||||
|
||||
@@ -1,54 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
namespace esphome {
|
||||
namespace touchscreen {
|
||||
|
||||
static const uint8_t STATE_RELEASED = 0x00;
|
||||
static const uint8_t STATE_PRESSED = 0x01;
|
||||
static const uint8_t STATE_UPDATED = 0x02;
|
||||
static const uint8_t STATE_RELEASING = 0x04;
|
||||
|
||||
struct TouchPoint {
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
uint8_t id;
|
||||
uint8_t state;
|
||||
int16_t x_raw{0}, y_raw{0}, z_raw{0};
|
||||
uint16_t x_prev{0}, y_prev{0};
|
||||
uint16_t x_org{0}, y_org{0};
|
||||
uint16_t x{0}, y{0};
|
||||
int8_t state{0};
|
||||
};
|
||||
|
||||
using TouchPoints_t = std::vector<TouchPoint>;
|
||||
|
||||
struct TouchscreenInterrupt {
|
||||
volatile bool touched{true};
|
||||
bool init{false};
|
||||
static void gpio_intr(TouchscreenInterrupt *store);
|
||||
};
|
||||
|
||||
class TouchListener {
|
||||
public:
|
||||
virtual void touch(TouchPoint tp) = 0;
|
||||
virtual void touch(TouchPoint tp) {}
|
||||
virtual void update(const TouchPoints_t &tpoints) {}
|
||||
virtual void release() {}
|
||||
};
|
||||
|
||||
enum TouchRotation {
|
||||
ROTATE_0_DEGREES = 0,
|
||||
ROTATE_90_DEGREES = 90,
|
||||
ROTATE_180_DEGREES = 180,
|
||||
ROTATE_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
class Touchscreen {
|
||||
class Touchscreen : public PollingComponent {
|
||||
public:
|
||||
void set_display(display::Display *display);
|
||||
void set_display(display::Display *display) { this->display_ = display; }
|
||||
display::Display *get_display() const { return this->display_; }
|
||||
|
||||
Trigger<TouchPoint> *get_touch_trigger() { return &this->touch_trigger_; }
|
||||
void set_mirror_x(bool invert_x) { this->invert_x_ = invert_x; }
|
||||
void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; }
|
||||
void set_swap_xy(bool swap) { this->swap_x_y_ = swap; }
|
||||
|
||||
void 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);
|
||||
if (x_min > x_max)
|
||||
this->invert_x_ = true;
|
||||
if (y_min > y_max)
|
||||
this->invert_y_ = true;
|
||||
}
|
||||
|
||||
Trigger<TouchPoint, const TouchPoints_t &> *get_touch_trigger() { return &this->touch_trigger_; }
|
||||
Trigger<const TouchPoints_t &> *get_update_trigger() { return &this->update_trigger_; }
|
||||
Trigger<> *get_release_trigger() { return &this->release_trigger_; }
|
||||
|
||||
void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); }
|
||||
|
||||
virtual void update_touches() = 0;
|
||||
|
||||
optional<TouchPoint> get_touch() { return this->touches_.begin()->second; }
|
||||
|
||||
TouchPoints_t get_touches() {
|
||||
TouchPoints_t touches;
|
||||
for (auto i : this->touches_) {
|
||||
touches.push_back(i.second);
|
||||
}
|
||||
return touches;
|
||||
}
|
||||
|
||||
void update() override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
/// Call this function to send touch points to the `on_touch` listener and the binary_sensors.
|
||||
void send_touch_(TouchPoint tp);
|
||||
void send_release_();
|
||||
|
||||
uint16_t display_width_;
|
||||
uint16_t display_height_;
|
||||
display::Display *display_;
|
||||
TouchRotation rotation_;
|
||||
Trigger<TouchPoint> touch_trigger_;
|
||||
void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type);
|
||||
|
||||
void set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0);
|
||||
|
||||
void send_touches_();
|
||||
|
||||
int16_t normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted = false);
|
||||
|
||||
uint16_t get_width_() { return this->display_->get_width(); }
|
||||
|
||||
uint16_t get_height_() { return this->display_->get_height(); }
|
||||
|
||||
display::Display *display_{nullptr};
|
||||
|
||||
int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0};
|
||||
bool invert_x_{false}, invert_y_{false}, swap_x_y_{false};
|
||||
|
||||
Trigger<TouchPoint, const TouchPoints_t &> touch_trigger_;
|
||||
Trigger<const TouchPoints_t &> update_trigger_;
|
||||
Trigger<> release_trigger_;
|
||||
std::vector<TouchListener *> touch_listeners_;
|
||||
|
||||
std::map<uint8_t, TouchPoint> touches_;
|
||||
TouchscreenInterrupt store_;
|
||||
|
||||
bool first_touch_{true};
|
||||
bool need_update_{false};
|
||||
bool is_touched_{false};
|
||||
bool skip_update_{false};
|
||||
};
|
||||
|
||||
} // namespace touchscreen
|
||||
|
||||
Reference in New Issue
Block a user