From 0651716b9657dc109b168235ea280a9848b583ad Mon Sep 17 00:00:00 2001 From: SenexCrenshaw <35600301+SenexCrenshaw@users.noreply.github.com> Date: Wed, 14 Jul 2021 20:51:15 -0400 Subject: [PATCH] Nextion upload and sensors (#1464) Co-authored-by: Senex Crenshaw --- CODEOWNERS | 5 + esphome/components/nextion/__init__.py | 5 + esphome/components/nextion/automation.h | 30 + esphome/components/nextion/base_component.py | 126 ++ esphome/components/nextion/binary_sensor.py | 34 - .../nextion/binary_sensor/__init__.py | 54 + .../binary_sensor/nextion_binarysensor.cpp | 69 + .../binary_sensor/nextion_binarysensor.h | 42 + esphome/components/nextion/display.py | 75 +- esphome/components/nextion/nextion.cpp | 1143 ++++++++++++++--- esphome/components/nextion/nextion.h | 527 +++++++- esphome/components/nextion/nextion_base.h | 58 + .../components/nextion/nextion_commands.cpp | 234 ++++ .../components/nextion/nextion_component.cpp | 116 ++ .../components/nextion/nextion_component.h | 49 + .../nextion/nextion_component_base.h | 95 ++ esphome/components/nextion/nextion_upload.cpp | 343 +++++ esphome/components/nextion/sensor/__init__.py | 99 ++ .../nextion/sensor/nextion_sensor.cpp | 110 ++ .../nextion/sensor/nextion_sensor.h | 49 + esphome/components/nextion/switch/__init__.py | 39 + .../nextion/switch/nextion_switch.cpp | 52 + .../nextion/switch/nextion_switch.h | 34 + .../nextion/text_sensor/__init__.py | 38 + .../text_sensor/nextion_textsensor.cpp | 49 + .../nextion/text_sensor/nextion_textsensor.h | 32 + esphome/components/uart/uart.h | 1 + script/ci-custom.py | 3 +- tests/test1.yaml | 9 - tests/test3.yaml | 38 +- 30 files changed, 3295 insertions(+), 263 deletions(-) create mode 100644 esphome/components/nextion/automation.h create mode 100644 esphome/components/nextion/base_component.py delete mode 100644 esphome/components/nextion/binary_sensor.py create mode 100644 esphome/components/nextion/binary_sensor/__init__.py create mode 100644 esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp create mode 100644 esphome/components/nextion/binary_sensor/nextion_binarysensor.h create mode 100644 esphome/components/nextion/nextion_base.h create mode 100644 esphome/components/nextion/nextion_commands.cpp create mode 100644 esphome/components/nextion/nextion_component.cpp create mode 100644 esphome/components/nextion/nextion_component.h create mode 100644 esphome/components/nextion/nextion_component_base.h create mode 100644 esphome/components/nextion/nextion_upload.cpp create mode 100644 esphome/components/nextion/sensor/__init__.py create mode 100644 esphome/components/nextion/sensor/nextion_sensor.cpp create mode 100644 esphome/components/nextion/sensor/nextion_sensor.h create mode 100644 esphome/components/nextion/switch/__init__.py create mode 100644 esphome/components/nextion/switch/nextion_switch.cpp create mode 100644 esphome/components/nextion/switch/nextion_switch.h create mode 100644 esphome/components/nextion/text_sensor/__init__.py create mode 100644 esphome/components/nextion/text_sensor/nextion_textsensor.cpp create mode 100644 esphome/components/nextion/text_sensor/nextion_textsensor.h diff --git a/CODEOWNERS b/CODEOWNERS index d6769800cd..557fe7cc08 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -73,6 +73,11 @@ esphome/components/midea_ac/* @dudanov esphome/components/midea_dongle/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/network/* @esphome/core +esphome/components/nextion/* @senexcrenshaw +esphome/components/nextion/binary_sensor/* @senexcrenshaw +esphome/components/nextion/sensor/* @senexcrenshaw +esphome/components/nextion/switch/* @senexcrenshaw +esphome/components/nextion/text_sensor/* @senexcrenshaw esphome/components/nfc/* @jesserockz esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index 67a49df9fa..924d58198d 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -1,3 +1,8 @@ import esphome.codegen as cg +from esphome.components import uart nextion_ns = cg.esphome_ns.namespace("nextion") +Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) +nextion_ref = Nextion.operator("ref") + +CONF_NEXTION_ID = "nextion_id" diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h new file mode 100644 index 0000000000..5f4219acb1 --- /dev/null +++ b/esphome/components/nextion/automation.h @@ -0,0 +1,30 @@ +#pragma once +#include "esphome/core/automation.h" +#include "nextion.h" + +namespace esphome { +namespace nextion { + +class SetupTrigger : public Trigger<> { + public: + explicit SetupTrigger(Nextion *nextion) { + nextion->add_setup_state_callback([this]() { this->trigger(); }); + } +}; + +class SleepTrigger : public Trigger<> { + public: + explicit SleepTrigger(Nextion *nextion) { + nextion->add_sleep_state_callback([this]() { this->trigger(); }); + } +}; + +class WakeTrigger : public Trigger<> { + public: + explicit WakeTrigger(Nextion *nextion) { + nextion->add_wake_state_callback([this]() { this->trigger(); }); + } +}; + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py new file mode 100644 index 0000000000..3bf828aafa --- /dev/null +++ b/esphome/components/nextion/base_component.py @@ -0,0 +1,126 @@ +from string import ascii_letters, digits +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.components import color + +from . import CONF_NEXTION_ID +from . import Nextion + +CONF_VARIABLE_NAME = "variable_name" +CONF_COMPONENT_NAME = "component_name" +CONF_WAVE_CHANNEL_ID = "wave_channel_id" +CONF_WAVE_MAX_VALUE = "wave_max_value" +CONF_PRECISION = "precision" +CONF_WAVEFORM_SEND_LAST_VALUE = "waveform_send_last_value" +CONF_TFT_URL = "tft_url" +CONF_ON_SLEEP = "on_sleep" +CONF_ON_WAKE = "on_wake" +CONF_ON_SETUP = "on_setup" +CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" +CONF_WAKE_UP_PAGE = "wake_up_page" +CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" +CONF_WAVE_MAX_LENGTH = "wave_max_length" +CONF_BACKGROUND_COLOR = "background_color" +CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" +CONF_FOREGROUND_COLOR = "foreground_color" +CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" +CONF_FONT_ID = "font_id" +CONF_VISIBLE = "visible" + + +def NextionName(value): + valid_chars = ascii_letters + digits + "." + if not isinstance(value, str) or len(value) > 29: + raise cv.Invalid("Must be a string less than 29 characters") + + for char in value: + if char not in valid_chars: + raise cv.Invalid( + "Must only consist of upper/lowercase characters, numbers and the period '.'. The character '{}' cannot be used.".format( + char + ) + ) + + return value + + +CONFIG_BASE_COMPONENT_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_NEXTION_ID): cv.use_id(Nextion), + cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color), + cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color), + cv.Optional(CONF_VISIBLE, default=True): cv.boolean, + } +) + + +CONFIG_TEXT_COMPONENT_SCHEMA = CONFIG_BASE_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_COMPONENT_NAME): NextionName, + cv.Optional(CONF_FONT_ID): cv.int_range(min=0, max=255), + } + ) +) + +CONFIG_BINARY_SENSOR_SCHEMA = CONFIG_BASE_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_COMPONENT_NAME): NextionName, + cv.Optional(CONF_VARIABLE_NAME): NextionName, + } + ) +) + +CONFIG_SENSOR_COMPONENT_SCHEMA = CONFIG_BINARY_SENSOR_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_FONT_ID): cv.int_range(min=0, max=255), + } + ) +) + + +CONFIG_SWITCH_COMPONENT_SCHEMA = CONFIG_SENSOR_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_FOREGROUND_PRESSED_COLOR): cv.use_id(color), + cv.Optional(CONF_BACKGROUND_PRESSED_COLOR): cv.use_id(color), + } + ) +) + + +async def setup_component_core_(var, config, arg): + + if CONF_VARIABLE_NAME in config: + cg.add(var.set_variable_name(config[CONF_VARIABLE_NAME])) + elif CONF_COMPONENT_NAME in config: + cg.add( + var.set_variable_name( + config[CONF_COMPONENT_NAME], + config[CONF_COMPONENT_NAME] + arg, + ) + ) + + if CONF_BACKGROUND_COLOR in config: + color_component = await cg.get_variable(config[CONF_BACKGROUND_COLOR]) + cg.add(var.set_background_color(color_component)) + + if CONF_BACKGROUND_PRESSED_COLOR in config: + color_component = await cg.get_variable(config[CONF_BACKGROUND_PRESSED_COLOR]) + cg.add(var.set_background_pressed_color(color_component)) + + if CONF_FOREGROUND_COLOR in config: + color_component = await cg.get_variable(config[CONF_FOREGROUND_COLOR]) + cg.add(var.set_foreground_color(color_component)) + + if CONF_FOREGROUND_PRESSED_COLOR in config: + color_component = await cg.get_variable(config[CONF_FOREGROUND_PRESSED_COLOR]) + cg.add(var.set_foreground_pressed_color(color_component)) + + if CONF_FONT_ID in config: + cg.add(var.set_font_id(config[CONF_FONT_ID])) + + if CONF_VISIBLE in config: + cg.add(var.set_visible(config[CONF_VISIBLE])) diff --git a/esphome/components/nextion/binary_sensor.py b/esphome/components/nextion/binary_sensor.py deleted file mode 100644 index ed4e8d832a..0000000000 --- a/esphome/components/nextion/binary_sensor.py +++ /dev/null @@ -1,34 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import binary_sensor -from esphome.const import CONF_COMPONENT_ID, CONF_PAGE_ID, CONF_ID -from . import nextion_ns -from .display import Nextion - -DEPENDENCIES = ["display"] - -CONF_NEXTION_ID = "nextion_id" - -NextionTouchComponent = nextion_ns.class_( - "NextionTouchComponent", binary_sensor.BinarySensor -) - -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(NextionTouchComponent), - cv.GenerateID(CONF_NEXTION_ID): cv.use_id(Nextion), - cv.Required(CONF_PAGE_ID): cv.uint8_t, - cv.Required(CONF_COMPONENT_ID): cv.uint8_t, - } -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) - - hub = await cg.get_variable(config[CONF_NEXTION_ID]) - cg.add(hub.register_touch_component(var)) - - cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) - cg.add(var.set_page_id(config[CONF_PAGE_ID])) diff --git a/esphome/components/nextion/binary_sensor/__init__.py b/esphome/components/nextion/binary_sensor/__init__.py new file mode 100644 index 0000000000..090fae3429 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/__init__.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor + +from esphome.const import CONF_COMPONENT_ID, CONF_PAGE_ID, CONF_ID +from .. import nextion_ns, CONF_NEXTION_ID + + +from ..base_component import ( + setup_component_core_, + CONFIG_BINARY_SENSOR_SCHEMA, + CONF_VARIABLE_NAME, + CONF_COMPONENT_NAME, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionBinarySensor = nextion_ns.class_( + "NextionBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionBinarySensor), + cv.Optional(CONF_PAGE_ID): cv.uint8_t, + cv.Optional(CONF_COMPONENT_ID): cv.uint8_t, + } + ) + .extend(CONFIG_BINARY_SENSOR_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_at_least_one_key( + CONF_PAGE_ID, + CONF_COMPONENT_ID, + CONF_COMPONENT_NAME, + CONF_VARIABLE_NAME, + ), +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await binary_sensor.register_binary_sensor(var, config) + await cg.register_component(var, config) + + if config.keys() >= {CONF_PAGE_ID, CONF_COMPONENT_ID}: + cg.add(hub.register_touch_component(var)) + cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) + cg.add(var.set_page_id(config[CONF_PAGE_ID])) + + if CONF_COMPONENT_NAME in config or CONF_VARIABLE_NAME in config: + await setup_component_core_(var, config, ".val") + cg.add(hub.register_binarysensor_component(var)) diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp new file mode 100644 index 0000000000..bf6e74cb38 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -0,0 +1,69 @@ +#include "nextion_binarysensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_binarysensor"; + +void NextionBinarySensor::process_bool(const std::string &variable_name, bool state) { + if (!this->nextion_->is_setup()) + return; + + if (this->variable_name_.empty()) // This is a touch component + return; + + if (this->variable_name_ == variable_name) { + this->publish_state(state); + ESP_LOGD(TAG, "Processed binarysensor \"%s\" state %s", variable_name.c_str(), state ? "ON" : "OFF"); + } +} + +void NextionBinarySensor::process_touch(uint8_t page_id, uint8_t component_id, bool state) { + if (this->page_id_ == page_id && this->component_id_ == component_id) { + this->publish_state(state); + } +} + +void NextionBinarySensor::update() { + if (!this->nextion_->is_setup()) + return; + + if (this->variable_name_.empty()) // This is a touch component + return; + + this->nextion_->add_to_get_queue(this); +} + +void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (this->component_id_ == 0) // This is a legacy touch component + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); + } + } + + if (publish) { + this->publish_state(state); + } else { + this->state = state; + this->has_state_ = true; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for sensor \"%s\" state %s", this->variable_name_.c_str(), + ONOFF(this->variable_name_.c_str())); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h new file mode 100644 index 0000000000..b6b23ada85 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -0,0 +1,42 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionBinarySensor; + +class NextionBinarySensor : public NextionComponent, + public binary_sensor::BinarySensorInitiallyOff, + public PollingComponent { + public: + NextionBinarySensor(NextionBase *nextion) { this->nextion_ = nextion; } + + void update_component() override { this->update(); } + void update() override; + void send_state_to_nextion() override { this->set_state(this->state, false); }; + void process_bool(const std::string &variable_name, bool state) override; + void process_touch(uint8_t page_id, uint8_t component_id, bool state) override; + + // Set the components page id for Nextion Touch Component + void set_page_id(uint8_t page_id) { page_id_ = page_id; } + // Set the components component id for Nextion Touch Component + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + + void set_state(bool state) override { this->set_state(state, true, true); } + void set_state(bool state, bool publish) override { this->set_state(state, publish, true); } + void set_state(bool state, bool publish, bool send_to_nextion) override; + + NextionQueueType get_queue_type() override { return NextionQueueType::BINARY_SENSOR; } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value != 0, publish, send_to_nextion); + } + + protected: + uint8_t page_id_; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 7d7018a4c4..e693b2f1ec 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,20 +1,58 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.components import display, uart -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_BRIGHTNESS -from . import nextion_ns +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_BRIGHTNESS, + CONF_TRIGGER_ID, +) + +from . import Nextion, nextion_ns, nextion_ref +from .base_component import ( + CONF_ON_SLEEP, + CONF_ON_WAKE, + CONF_ON_SETUP, + CONF_TFT_URL, + CONF_TOUCH_SLEEP_TIMEOUT, + CONF_WAKE_UP_PAGE, + CONF_AUTO_WAKE_ON_TOUCH, +) + +CODEOWNERS = ["@senexcrenshaw"] DEPENDENCIES = ["uart"] -AUTO_LOAD = ["binary_sensor"] +AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] -Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) -NextionRef = Nextion.operator("ref") +SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) +SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) +WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) CONFIG_SCHEMA = ( display.BASIC_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Nextion), + cv.Optional(CONF_TFT_URL): cv.string, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_ON_SETUP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), + } + ), + cv.Optional(CONF_ON_SLEEP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SleepTrigger), + } + ), + cv.Optional(CONF_ON_WAKE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger), + } + ), + cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), + cv.Optional(CONF_WAKE_UP_PAGE): cv.positive_int, + cv.Optional(CONF_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean, } ) .extend(cv.polling_component_schema("5s")) @@ -31,8 +69,33 @@ async def to_code(config): cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(NextionRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) + if CONF_TFT_URL in config: + cg.add_define("USE_TFT_UPLOAD") + cg.add(var.set_tft_url(config[CONF_TFT_URL])) + + if CONF_TOUCH_SLEEP_TIMEOUT in config: + cg.add(var.set_touch_sleep_timeout_internal(config[CONF_TOUCH_SLEEP_TIMEOUT])) + + if CONF_WAKE_UP_PAGE in config: + cg.add(var.set_wake_up_page_internal(config[CONF_WAKE_UP_PAGE])) + + if CONF_AUTO_WAKE_ON_TOUCH in config: + cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + await display.register_display(var, config) + + for conf in config.get(CONF_ON_SETUP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_SLEEP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_WAKE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fe0767342b..9a5424917f 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,5 +1,7 @@ #include "nextion.h" +#include "esphome/core/util.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace nextion { @@ -7,69 +9,171 @@ namespace nextion { static const char *const TAG = "nextion"; void Nextion::setup() { - this->send_command_no_ack(""); - this->send_command_printf("bkcmd=3"); - this->set_backlight_brightness(static_cast(brightness_ * 100)); - this->goto_page("0"); + this->is_setup_ = false; + this->ignore_is_setup_ = true; + + // Wake up the nextion + this->send_command_("bkcmd=0"); + this->send_command_("sleep=0"); + + this->send_command_("bkcmd=0"); + this->send_command_("sleep=0"); + + // Reboot it + this->send_command_("rest"); + + this->ignore_is_setup_ = false; } -float Nextion::get_setup_priority() const { return setup_priority::PROCESSOR; } + +bool Nextion::send_command_(const std::string &command) { + if (!this->ignore_is_setup_ && !this->is_setup()) { + return false; + } + + ESP_LOGN(TAG, "send_command %s", command.c_str()); + + this->write_str(command.c_str()); + const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; + this->write_array(to_send, sizeof(to_send)); + return true; +} + +bool Nextion::check_connect_() { + if (this->get_is_connected_()) + return true; + + if (this->comok_sent_ == 0) { + this->reset_(false); + + this->ignore_is_setup_ = true; + this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating + this->send_command_("connect"); + + this->comok_sent_ = millis(); + this->ignore_is_setup_ = false; + + return false; + } + + if (millis() - this->comok_sent_ <= 500) // Wait 500 ms + return false; + + std::string response; + + this->recv_ret_string_(response, 0, false); + if (response.empty() || response.find("comok") == std::string::npos) { +#ifdef NEXTION_PROTOCOL_LOG + ESP_LOGN(TAG, "Bad connect request %s", response.c_str()); + for (int i = 0; i < response.length(); i++) { + ESP_LOGN(TAG, "response %s %d %d %c", response.c_str(), i, response[i], response[i]); + } +#endif + + ESP_LOGW(TAG, "Nextion is not connected! "); + comok_sent_ = 0; + return false; + } + + this->ignore_is_setup_ = true; + ESP_LOGI(TAG, "Nextion is connected"); + this->is_connected_ = true; + + ESP_LOGN(TAG, "connect request %s", response.c_str()); + + size_t start; + size_t end = 0; + std::vector connect_info; + while ((start = response.find_first_not_of(',', end)) != std::string::npos) { + end = response.find(',', start); + connect_info.push_back(response.substr(start, end - start)); + } + + if (connect_info.size() == 7) { + ESP_LOGN(TAG, "Received connect_info %zu", connect_info.size()); + + this->device_model_ = connect_info[2]; + this->firmware_version_ = connect_info[3]; + this->serial_number_ = connect_info[5]; + this->flash_size_ = connect_info[6]; + } else { + ESP_LOGE(TAG, "Nextion returned bad connect value \"%s\"", response.c_str()); + } + + this->ignore_is_setup_ = false; + this->dump_config(); + return true; +} + +void Nextion::reset_(bool reset_nextion) { + uint8_t d; + + while (this->available()) { // Clear receive buffer + this->read_byte(&d); + }; + this->nextion_queue_.clear(); +} + +void Nextion::dump_config() { + ESP_LOGCONFIG(TAG, "Nextion:"); + ESP_LOGCONFIG(TAG, " Device Model: %s", this->device_model_.c_str()); + ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str()); + ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str()); + ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False"); + + if (this->touch_sleep_timeout_ != 0) { + ESP_LOGCONFIG(TAG, " Touch Timeout: %d", this->touch_sleep_timeout_); + } + + if (this->wake_up_page_ != -1) { + ESP_LOGCONFIG(TAG, " Wake Up Page : %d", this->wake_up_page_); + } +} + +float Nextion::get_setup_priority() const { return setup_priority::DATA; } void Nextion::update() { + if (!this->is_setup()) { + return; + } if (this->writer_.has_value()) { (*this->writer_)(*this); } } -void Nextion::send_command_no_ack(const char *command) { - // Flush RX... - this->loop(); - this->write_str(command); - const uint8_t data[3] = {0xFF, 0xFF, 0xFF}; - this->write_array(data, sizeof(data)); +void Nextion::add_sleep_state_callback(std::function &&callback) { + this->sleep_callback_.add(std::move(callback)); } -bool Nextion::ack_() { - if (!this->wait_for_ack_) - return true; +void Nextion::add_wake_state_callback(std::function &&callback) { + this->wake_callback_.add(std::move(callback)); +} - uint32_t start = millis(); - while (!this->read_until_ack_()) { - if (millis() - start > 100) { - ESP_LOGW(TAG, "Waiting for ACK timed out!"); - return false; - } +void Nextion::add_setup_state_callback(std::function &&callback) { + this->setup_callback_.add(std::move(callback)); +} + +void Nextion::update_all_components() { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return; + + for (auto *binarysensortype : this->binarysensortype_) { + binarysensortype->update_component(); + } + for (auto *sensortype : this->sensortype_) { + sensortype->update_component(); + } + for (auto *switchtype : this->switchtype_) { + switchtype->update_component(); + } + for (auto *textsensortype : this->textsensortype_) { + textsensortype->update_component(); } - return true; } -void Nextion::set_component_text(const char *component, const char *text) { - this->send_command_printf("%s.txt=\"%s\"", component, text); -} -void Nextion::set_component_value(const char *component, int value) { - this->send_command_printf("%s.val=%d", component, value); -} -void Nextion::display_picture(int picture_id, int x_start, int y_start) { - this->send_command_printf("pic %d %d %d", x_start, y_start, picture_id); -} -void Nextion::set_component_background_color(const char *component, const char *color) { - this->send_command_printf("%s.bco=\"%s\"", component, color); -} -void Nextion::set_component_pressed_background_color(const char *component, const char *color) { - this->send_command_printf("%s.bco2=\"%s\"", component, color); -} -void Nextion::set_component_font_color(const char *component, const char *color) { - this->send_command_printf("%s.pco=\"%s\"", component, color); -} -void Nextion::set_component_pressed_font_color(const char *component, const char *color) { - this->send_command_printf("%s.pco2=\"%s\"", component, color); -} -void Nextion::set_component_coordinates(const char *component, int x, int y) { - this->send_command_printf("%s.xcen=%d", component, x); - this->send_command_printf("%s.ycen=%d", component, y); -} -void Nextion::set_component_font(const char *component, uint8_t font_id) { - this->send_command_printf("%s.font=%d", component, font_id); -} -void Nextion::goto_page(const char *page) { this->send_command_printf("page %s", page); } + bool Nextion::send_command_printf(const char *format, ...) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return false; + char buffer[256]; va_list arg; va_start(arg, format); @@ -79,208 +183,911 @@ bool Nextion::send_command_printf(const char *format, ...) { ESP_LOGW(TAG, "Building command for format '%s' failed!", format); return false; } - this->send_command_no_ack(buffer); - if (!this->ack_()) { - ESP_LOGW(TAG, "Sending command '%s' failed because no ACK was received", buffer); + + if (this->send_command_(buffer)) { + this->add_no_result_to_queue_("send_command_printf"); + return true; + } + return false; +} + +#ifdef NEXTION_PROTOCOL_LOG +void Nextion::print_queue_members_() { + ESP_LOGN(TAG, "print_queue_members_ (top 10) size %zu", this->nextion_queue_.size()); + ESP_LOGN(TAG, "*******************************************"); + int count = 0; + for (auto *i : this->nextion_queue_) { + if (count++ == 10) + break; + + if (i == nullptr) { + ESP_LOGN(TAG, "Nextion queue is null"); + } else { + ESP_LOGN(TAG, "Nextion queue type: %d:%s , name: %s", i->component->get_queue_type(), + i->component->get_queue_type_string().c_str(), i->component->get_variable_name().c_str()); + } + } + ESP_LOGN(TAG, "*******************************************"); +} +#endif + +void Nextion::loop() { + if (!this->check_connect_() || this->is_updating_) + return; + + if (this->nextion_reports_is_setup_ && !this->sent_setup_commands_) { + this->ignore_is_setup_ = true; + this->sent_setup_commands_ = true; + this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. + + this->set_backlight_brightness(this->brightness_); + this->goto_page("0"); + + this->set_auto_wake_on_touch(this->auto_wake_on_touch_); + + if (this->touch_sleep_timeout_ != 0) { + this->set_touch_sleep_timeout(this->touch_sleep_timeout_); + } + + if (this->wake_up_page_ != -1) { + this->set_wake_up_page(this->wake_up_page_); + } + + this->ignore_is_setup_ = false; + } + + this->process_serial_(); // Receive serial data + this->process_nextion_commands_(); // Process nextion return commands + + if (!this->nextion_reports_is_setup_) { + if (this->started_ms_ == 0) + this->started_ms_ = millis(); + + if (this->started_ms_ + this->startup_override_ms_ < millis()) { + ESP_LOGD(TAG, "Manually set nextion report ready"); + this->nextion_reports_is_setup_ = true; + } + } +} + +bool Nextion::remove_from_q_(bool report_empty) { + if (this->nextion_queue_.empty()) { + if (report_empty) + ESP_LOGE(TAG, "Nextion queue is empty!"); return false; } + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; + + ESP_LOGN(TAG, "Removing %s from the queue", component->get_variable_name().c_str()); + + if (component->get_queue_type() == NextionQueueType::NO_RESULT) { + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + delete component; + } + delete nb; + this->nextion_queue_.pop_front(); return true; } -void Nextion::hide_component(const char *component) { this->send_command_printf("vis %s,0", component); } -void Nextion::show_component(const char *component) { this->send_command_printf("vis %s,1", component); } -void Nextion::enable_component_touch(const char *component) { this->send_command_printf("tsw %s,1", component); } -void Nextion::disable_component_touch(const char *component) { this->send_command_printf("tsw %s,0", component); } -void Nextion::add_waveform_data(int component_id, uint8_t channel_number, uint8_t value) { - this->send_command_printf("add %d,%u,%u", component_id, channel_number, value); -} -void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { - this->send_command_printf("fill %d,%d,%d,%d,%s", x1, y1, width, height, color); -} -void Nextion::line(int x1, int y1, int x2, int y2, const char *color) { - this->send_command_printf("line %d,%d,%d,%d,%s", x1, y1, x2, y2, color); -} -void Nextion::rectangle(int x1, int y1, int width, int height, const char *color) { - this->send_command_printf("draw %d,%d,%d,%d,%s", x1, y1, x1 + width, y1 + height, color); -} -void Nextion::circle(int center_x, int center_y, int radius, const char *color) { - this->send_command_printf("cir %d,%d,%d,%s", center_x, center_y, radius, color); -} -void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { - this->send_command_printf("cirs %d,%d,%d,%s", center_x, center_y, radius, color); -} -bool Nextion::read_until_ack_() { - while (this->available() >= 4) { - // flush preceding filler bytes - uint8_t temp; - while (this->available() && this->peek_byte(&temp) && temp == 0xFF) - this->read_byte(&temp); - if (!this->available()) - break; +void Nextion::process_serial_() { + uint8_t d; - uint8_t event; - // event type - this->read_byte(&event); + while (this->available()) { + read_byte(&d); + this->command_data_ += d; + } +} +// nextion.tech/instruction-set/ +void Nextion::process_nextion_commands_() { + if (this->command_data_.length() == 0) { + return; + } - uint8_t data[255]; - // total length of data (including end bytes) - uint8_t data_length = 0; - // message is terminated by three consecutive 0xFF - // this variable keeps track of ohow many of those have - // been received - uint8_t end_length = 0; - while (this->available() && end_length < 3 && data_length < sizeof(data)) { - uint8_t byte; - this->read_byte(&byte); - if (byte == 0xFF) { - end_length++; - } else { - end_length = 0; - } - data[data_length++] = byte; + size_t to_process_length = 0; + std::string to_process; + + ESP_LOGN(TAG, "this->command_data_ %s length %d", this->command_data_.c_str(), this->command_data_.length()); +#ifdef NEXTION_PROTOCOL_LOG + this->print_queue_members_(); +#endif + while ((to_process_length = this->command_data_.find(COMMAND_DELIMITER)) != std::string::npos) { + ESP_LOGN(TAG, "print_queue_members_ size %zu", this->nextion_queue_.size()); + while (to_process_length + COMMAND_DELIMITER.length() < this->command_data_.length() && + static_cast(this->command_data_[to_process_length + COMMAND_DELIMITER.length()]) == 0xFF) { + ++to_process_length; + ESP_LOGN(TAG, "Add extra 0xFF to process"); } - if (end_length != 3) { - ESP_LOGW(TAG, "Received unknown filler end bytes from Nextion!"); - continue; - } + this->nextion_event_ = this->command_data_[0]; - data_length -= 3; // remove filler bytes + to_process_length -= 1; + to_process = this->command_data_.substr(1, to_process_length); - bool invalid_data_length = false; - switch (event) { - case 0x01: // successful execution of instruction (ACK) - return true; - case 0x00: // invalid instruction + switch (this->nextion_event_) { + case 0x00: // instruction sent by user has failed ESP_LOGW(TAG, "Nextion reported invalid instruction!"); + this->remove_from_q_(); + break; - case 0x02: // component ID invalid - ESP_LOGW(TAG, "Nextion reported component ID invalid!"); + case 0x01: // instruction sent by user was successful + + ESP_LOGVV(TAG, "instruction sent by user was successful"); + ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", this->nextion_queue_.empty() ? "True" : "False"); + + this->remove_from_q_(); + if (!this->is_setup_) { + if (this->nextion_queue_.empty()) { + ESP_LOGD(TAG, "Nextion is setup"); + this->is_setup_ = true; + this->setup_callback_.call(); + } + } + break; - case 0x03: // page ID invalid + case 0x02: // invalid Component ID or name was used + this->remove_from_q_(); + break; + case 0x03: // invalid Page ID or name was used ESP_LOGW(TAG, "Nextion reported page ID invalid!"); + this->remove_from_q_(); break; - case 0x04: // picture ID invalid + case 0x04: // invalid Picture ID was used ESP_LOGW(TAG, "Nextion reported picture ID invalid!"); + this->remove_from_q_(); break; - case 0x05: // font ID invalid + case 0x05: // invalid Font ID was used ESP_LOGW(TAG, "Nextion reported font ID invalid!"); + this->remove_from_q_(); break; - case 0x11: // baud rate setting invalid + case 0x06: // File operation fails + ESP_LOGW(TAG, "Nextion File operation fail!"); + break; + case 0x09: // Instructions with CRC validation fails their CRC check + ESP_LOGW(TAG, "Nextion Instructions with CRC validation fails their CRC check!"); + break; + case 0x11: // invalid Baud rate was used ESP_LOGW(TAG, "Nextion reported baud rate invalid!"); break; - case 0x12: // curve control ID number or channel number is invalid - ESP_LOGW(TAG, "Nextion reported control/channel ID invalid!"); + case 0x12: // invalid Waveform ID or Channel # was used + + if (!this->nextion_queue_.empty()) { + int index = 0; + int found = -1; + for (auto &nb : this->nextion_queue_) { + NextionComponentBase *component = nb->component; + + if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { + ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!", + component->get_component_id(), component->get_wave_channel_id()); + + ESP_LOGN(TAG, "Removing waveform from queue with component id %d and waveform id %d", + component->get_component_id(), component->get_wave_channel_id()); + + found = index; + + delete component; + delete nb; + + break; + } + ++index; + } + + if (found != -1) { + this->nextion_queue_.erase(this->nextion_queue_.begin() + found); + } else { + ESP_LOGW( + TAG, + "Nextion reported invalid Waveform ID or Channel # was used but no waveform sensor in queue found!"); + } + } break; case 0x1A: // variable name invalid - ESP_LOGW(TAG, "Nextion reported variable name invalid!"); + this->remove_from_q_(); + break; case 0x1B: // variable operation invalid ESP_LOGW(TAG, "Nextion reported variable operation invalid!"); + this->remove_from_q_(); break; case 0x1C: // failed to assign ESP_LOGW(TAG, "Nextion reported failed to assign variable!"); + this->remove_from_q_(); break; case 0x1D: // operate EEPROM failed ESP_LOGW(TAG, "Nextion reported operating EEPROM failed!"); break; case 0x1E: // parameter quantity invalid ESP_LOGW(TAG, "Nextion reported parameter quantity invalid!"); + this->remove_from_q_(); break; case 0x1F: // IO operation failed ESP_LOGW(TAG, "Nextion reported component I/O operation invalid!"); break; case 0x20: // undefined escape characters ESP_LOGW(TAG, "Nextion reported undefined escape characters!"); + this->remove_from_q_(); break; case 0x23: // too long variable name ESP_LOGW(TAG, "Nextion reported too long variable name!"); + this->remove_from_q_(); + + break; + case 0x24: // Serial Buffer overflow occurs + ESP_LOGW(TAG, "Nextion reported Serial Buffer overflow!"); break; case 0x65: { // touch event return data - if (data_length != 3) { - invalid_data_length = true; + if (to_process_length != 3) { + ESP_LOGW(TAG, "Touch event data is expecting 3, received %zu", to_process_length); + break; } - uint8_t page_id = data[0]; - uint8_t component_id = data[1]; - uint8_t touch_event = data[2]; // 0 -> release, 1 -> press + uint8_t page_id = to_process[0]; + uint8_t component_id = to_process[1]; + uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press ESP_LOGD(TAG, "Got touch page=%u component=%u type=%s", page_id, component_id, touch_event ? "PRESS" : "RELEASE"); for (auto *touch : this->touch_) { - touch->process(page_id, component_id, touch_event); + touch->process_touch(page_id, component_id, touch_event != 0); } break; } - case 0x67: - case 0x68: { // touch coordinate data - if (data_length != 5) { - invalid_data_length = true; + case 0x67: { // Touch Coordinate (awake) + break; + } + case 0x68: { // touch coordinate data (sleep) + + if (to_process_length != 5) { + ESP_LOGW(TAG, "Touch coordinate data is expecting 5, received %zu", to_process_length); + ESP_LOGW(TAG, "%s", to_process.c_str()); break; } - uint16_t x = (uint16_t(data[0]) << 8) | data[1]; - uint16_t y = (uint16_t(data[2]) << 8) | data[3]; - uint8_t touch_event = data[4]; // 0 -> release, 1 -> press + + uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; + uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; + uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press ESP_LOGD(TAG, "Got touch at x=%u y=%u type=%s", x, y, touch_event ? "PRESS" : "RELEASE"); break; } - case 0x66: // sendme page id + case 0x66: { + break; + } // sendme page id + + // 0x70 0x61 0x62 0x31 0x32 0x33 0xFF 0xFF 0xFF + // Returned when using get command for a string. + // Each byte is converted to char. + // data: ab123 case 0x70: // string variable data return + { + if (this->nextion_queue_.empty()) { + ESP_LOGW(TAG, "ERROR: Received string return but the queue is empty"); + break; + } + + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; + + if (component->get_queue_type() != NextionQueueType::TEXT_SENSOR) { + ESP_LOGE(TAG, "ERROR: Received string return but next in queue \"%s\" is not a text sensor", + component->get_variable_name().c_str()); + } else { + ESP_LOGN(TAG, "Received get_string response: \"%s\" for component id: %s, type: %s", to_process.c_str(), + component->get_variable_name().c_str(), component->get_queue_type_string().c_str()); + component->set_state_from_string(to_process, true, false); + } + + delete nb; + this->nextion_queue_.pop_front(); + + break; + } + // 0x71 0x01 0x02 0x03 0x04 0xFF 0xFF 0xFF + // Returned when get command to return a number + // 4 byte 32-bit value in little endian order. + // (0x01+0x02*256+0x03*65536+0x04*16777216) + // data: 67305985 case 0x71: // numeric variable data return - case 0x86: // device automatically enters into sleep mode + { + if (this->nextion_queue_.empty()) { + ESP_LOGE(TAG, "ERROR: Received numeric return but the queue is empty"); + break; + } + + if (to_process_length == 0) { + ESP_LOGE(TAG, "ERROR: Received numeric return but no data!"); + break; + } + + int dataindex = 0; + + int value = 0; + + for (int i = 0; i < 4; ++i) { + value += to_process[i] << (8 * i); + ++dataindex; + } + + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; + + if (component->get_queue_type() != NextionQueueType::SENSOR && + component->get_queue_type() != NextionQueueType::BINARY_SENSOR && + component->get_queue_type() != NextionQueueType::SWITCH) { + ESP_LOGE(TAG, "ERROR: Received numeric return but next in queue \"%s\" is not a valid sensor type %d", + component->get_variable_name().c_str(), component->get_queue_type()); + } else { + ESP_LOGN(TAG, "Received numeric return for variable %s, queue type %d:%s, value %d", + component->get_variable_name().c_str(), component->get_queue_type(), + component->get_queue_type_string().c_str(), value); + component->set_state_from_int(value, true, false); + } + + delete nb; + this->nextion_queue_.pop_front(); + + break; + } + + case 0x86: { // device automatically enters into sleep mode + ESP_LOGVV(TAG, "Received Nextion entering sleep automatically"); + this->is_sleeping_ = true; + this->sleep_callback_.call(); + break; + } case 0x87: // device automatically wakes up + { + ESP_LOGVV(TAG, "Received Nextion leaves sleep automatically"); + this->is_sleeping_ = false; + this->wake_callback_.call(); + break; + } case 0x88: // system successful start up - case 0x89: // start SD card upgrade - case 0xFD: // data transparent transmit finished - case 0xFE: // data transparent transmit ready + { + ESP_LOGD(TAG, "system successful start up %zu", to_process_length); + this->nextion_reports_is_setup_ = true; break; + } + case 0x89: { // start SD card upgrade + break; + } + // Data from nextion is + // 0x90 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // 00/01 - Single byte for on/off + // FF FF FF - End + case 0x90: { // Switched component + std::string variable_name; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad switch component data received for 0x90 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + variable_name = to_process.substr(0, index); + ++index; + + ESP_LOGN(TAG, "Got Switch variable_name=%s value=%d", variable_name.c_str(), to_process[0] != 0); + + for (auto *switchtype : this->switchtype_) { + switchtype->process_bool(variable_name, to_process[index] != 0); + } + break; + } + // Data from nextion is + // 0x91 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // variable length of 0x71 return data: prints temp1.val,0 + // FF FF FF - End + case 0x91: { // Sensor component + std::string variable_name; + uint8_t index = 0; + + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) != 4) { + ESP_LOGE(TAG, "Bad sensor component data received for 0x91 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + index = to_process.find('\0'); + variable_name = to_process.substr(0, index); + // // Get variable name + int value = 0; + for (int i = 0; i < 4; ++i) { + value += to_process[i + index + 1] << (8 * i); + } + + ESP_LOGN(TAG, "Got sensor variable_name=%s value=%d", variable_name.c_str(), value); + + for (auto *sensor : this->sensortype_) { + sensor->process_sensor(variable_name, value); + } + break; + } + + // Data from nextion is + // 0x92 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // variable length of 0x70 return formatted data (bytes) that contain the text prints temp1.txt,0 + // 00 - NULL + // FF FF FF - End + case 0x92: { // Text Sensor Component + std::string variable_name; + std::string text_value; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad text sensor component data received for 0x92 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + variable_name = to_process.substr(0, index); + ++index; + + text_value = to_process.substr(index); + + ESP_LOGN(TAG, "Got Text Sensor variable_name=%s value=%s", variable_name.c_str(), text_value.c_str()); + + // NextionTextSensorResponseQueue *nq = new NextionTextSensorResponseQueue; + // nq->variable_name = variable_name; + // nq->state = text_value; + // this->textsensorq_.push_back(nq); + for (auto *textsensortype : this->textsensortype_) { + textsensortype->process_text(variable_name, text_value); + } + break; + } + // Data from nextion is + // 0x93 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // 00/01 - Single byte for on/off + // FF FF FF - End + case 0x93: { // Binary Sensor component + std::string variable_name; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad binary sensor component data received for 0x92 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + variable_name = to_process.substr(0, index); + ++index; + + ESP_LOGN(TAG, "Got Binary Sensor variable_name=%s value=%d", variable_name.c_str(), to_process[index] != 0); + + for (auto *binarysensortype : this->binarysensortype_) { + binarysensortype->process_bool(&variable_name[0], to_process[index] != 0); + } + break; + } + case 0xFD: { // data transparent transmit finished + ESP_LOGVV(TAG, "Nextion reported data transmit finished!"); + break; + } + case 0xFE: { // data transparent transmit ready + ESP_LOGVV(TAG, "Nextion reported ready for transmit!"); + + int index = 0; + int found = -1; + for (auto &nb : this->nextion_queue_) { + auto component = nb->component; + if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { + size_t buffer_to_send = component->get_wave_buffer().size() < 255 ? component->get_wave_buffer().size() + : 255; // ADDT command can only send 255 + + this->write_array(component->get_wave_buffer().data(), static_cast(buffer_to_send)); + + ESP_LOGN(TAG, "Nextion sending waveform data for component id %d and waveform id %d, size %zu", + component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); + + if (component->get_wave_buffer().size() <= 255) { + component->get_wave_buffer().clear(); + } else { + component->get_wave_buffer().erase(component->get_wave_buffer().begin(), + component->get_wave_buffer().begin() + buffer_to_send); + } + found = index; + delete component; + delete nb; + break; + } + ++index; + } + + if (found == -1) { + ESP_LOGE(TAG, "No waveforms in queue to send data!"); + break; + } else { + this->nextion_queue_.erase(this->nextion_queue_.begin() + found); + } + break; + } default: - ESP_LOGW(TAG, "Received unknown event from nextion: 0x%02X", event); + ESP_LOGW(TAG, "Received unknown event from nextion: 0x%02X", this->nextion_event_); break; } - if (invalid_data_length) { - ESP_LOGW(TAG, "Invalid data length from nextion!"); + + // ESP_LOGN(TAG, "nextion_event_ deleting from 0 to %d", to_process_length + COMMAND_DELIMITER.length() + 1); + this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); + // App.feed_wdt(); Remove before master merge + this->process_serial_(); + } + + uint32_t ms = millis(); + + if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { + for (int i = 0; i < this->nextion_queue_.size(); i++) { + NextionComponentBase *component = this->nextion_queue_[i]->component; + if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) { + if (this->nextion_queue_[i]->queue_time == 0) + ESP_LOGD(TAG, "Removing old queue type \"%s\" name \"%s\" queue_time 0", + component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); + + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + + ESP_LOGD(TAG, "Removing old queue type \"%s\" name \"%s\"", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str()); + + if (component->get_queue_type() == NextionQueueType::NO_RESULT) { + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + delete component; + } + + delete this->nextion_queue_[i]; + + this->nextion_queue_.erase(this->nextion_queue_.begin() + i); + + } else { + break; + } + } + } + ESP_LOGN(TAG, "Loop End"); + // App.feed_wdt(); Remove before master merge + this->process_serial_(); +} // namespace nextion + +void Nextion::set_nextion_sensor_state(int queue_type, const std::string &name, float state) { + this->set_nextion_sensor_state(static_cast(queue_type), name, state); +} + +void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state) { + ESP_LOGN(TAG, "Received state for variable %s, state %lf for queue type %d", name.c_str(), state, queue_type); + + switch (queue_type) { + case NextionQueueType::SENSOR: { + for (auto *sensor : this->sensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state, true, true); + break; + } + } + break; + } + case NextionQueueType::BINARY_SENSOR: { + for (auto *sensor : this->binarysensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state != 0, true, true); + break; + } + } + break; + } + case NextionQueueType::SWITCH: { + for (auto *sensor : this->switchtype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state != 0, true, true); + break; + } + } + break; + } + default: { + ESP_LOGW(TAG, "set_nextion_sensor_state does not support a queue type %d", queue_type); + } + } +} + +void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { + ESP_LOGD(TAG, "Received state for variable %s, state %s", name.c_str(), state.c_str()); + + for (auto *sensor : this->textsensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state, true, true); + break; + } + } +} + +void Nextion::all_components_send_state_(bool force_update) { + ESP_LOGD(TAG, "all_components_send_state_ "); + for (auto *binarysensortype : this->binarysensortype_) { + if (force_update || binarysensortype->get_needs_to_send_update()) + binarysensortype->send_state_to_nextion(); + } + for (auto *sensortype : this->sensortype_) { + if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_chan_id() == 0) + sensortype->send_state_to_nextion(); + } + for (auto *switchtype : this->switchtype_) { + if (force_update || switchtype->get_needs_to_send_update()) + switchtype->send_state_to_nextion(); + } + for (auto *textsensortype : this->textsensortype_) { + if (force_update || textsensortype->get_needs_to_send_update()) + textsensortype->send_state_to_nextion(); + } +} + +void Nextion::update_components_by_prefix(const std::string &prefix) { + for (auto *binarysensortype : this->binarysensortype_) { + if (binarysensortype->get_variable_name().find(prefix, 0) != std::string::npos) + binarysensortype->update_component_settings(true); + } + for (auto *sensortype : this->sensortype_) { + if (sensortype->get_variable_name().find(prefix, 0) != std::string::npos) + sensortype->update_component_settings(true); + } + for (auto *switchtype : this->switchtype_) { + if (switchtype->get_variable_name().find(prefix, 0) != std::string::npos) + switchtype->update_component_settings(true); + } + for (auto *textsensortype : this->textsensortype_) { + if (textsensortype->get_variable_name().find(prefix, 0) != std::string::npos) + textsensortype->update_component_settings(true); + } +} + +uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag) { + uint16_t ret = 0; + uint8_t c = 0; + uint8_t nr_of_ff_bytes = 0; + uint64_t start; + bool exit_flag = false; + bool ff_flag = false; + + start = millis(); + + while ((timeout == 0 && this->available()) || millis() - start <= timeout) { + this->read_byte(&c); + if (c == 0xFF) + nr_of_ff_bytes++; + else { + nr_of_ff_bytes = 0; + ff_flag = false; + } + + if (nr_of_ff_bytes >= 3) + ff_flag = true; + + response += (char) c; + if (recv_flag) { + if (response.find(0x05) != std::string::npos) { + exit_flag = true; + } + } + App.feed_wdt(); + delay(1); + + if (exit_flag || ff_flag) { + break; } } - return false; + if (ff_flag) + response = response.substr(0, response.length() - 3); // Remove last 3 0xFF + + ret = response.length(); + return ret; } -void Nextion::loop() { - while (this->available() >= 4) { - this->read_until_ack_(); + +/** + * @brief + * + * @param variable_name Name for the queue + */ +void Nextion::add_no_result_to_queue_(const std::string &variable_name) { + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component->set_variable_name(variable_name); + + nextion_queue->queue_time = millis(); + + this->nextion_queue_.push_back(nextion_queue); + + ESP_LOGN(TAG, "Add to queue type: NORESULT component %s", nextion_queue->component->get_variable_name().c_str()); +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param command + */ +void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) { + if ((!this->is_setup() && !this->ignore_is_setup_) || command.empty()) + return; + + if (this->send_command_(command)) { + this->add_no_result_to_queue_(variable_name); } } -#ifdef USE_TIME -void Nextion::set_nextion_rtc_time(time::ESPTime time) { - this->send_command_printf("rtc0=%u", time.year); - this->send_command_printf("rtc1=%u", time.month); - this->send_command_printf("rtc2=%u", time.day_of_month); - this->send_command_printf("rtc3=%u", time.hour); - this->send_command_printf("rtc4=%u", time.minute); - this->send_command_printf("rtc5=%u", time.second); -} -#endif -void Nextion::set_backlight_brightness(uint8_t brightness) { this->send_command_printf("dim=%u", brightness); } -void Nextion::set_touch_sleep_timeout(uint16_t timeout) { this->send_command_printf("thsp=%u", timeout); } +bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, + ...) { + if ((!this->is_setup() && !this->ignore_is_setup_)) + return false; -void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } -void Nextion::set_component_text_printf(const char *component, const char *format, ...) { + char buffer[256]; va_list arg; va_start(arg, format); - char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); va_end(arg); - if (ret > 0) - this->set_component_text(component, buffer); -} -void Nextion::set_wait_for_ack(bool wait_for_ack) { this->wait_for_ack_ = wait_for_ack; } + if (ret <= 0) { + ESP_LOGW(TAG, "Building command for format '%s' failed!", format); + return false; + } -void NextionTouchComponent::process(uint8_t page_id, uint8_t component_id, bool on) { - if (this->page_id_ == page_id && this->component_id_ == component_id) { - this->publish_state(on); + this->add_no_result_to_queue_with_command_(variable_name, buffer); + return true; +} + +/** + * @brief Sends a formatted command to the nextion + * + * @param variable_name Variable name for the queue + * @param format The printf-style command format, like "vis %s,0" + * @param ... The format arguments + */ +bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return false; + + char buffer[256]; + va_list arg; + va_start(arg, format); + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret <= 0) { + ESP_LOGW(TAG, "Building command for format '%s' failed!", format); + return false; + } + + this->add_no_result_to_queue_with_command_(variable_name, buffer); + return true; +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param variable_name_to_send Variable name for the left of the command + * @param state_value Value to set + * @param is_sleep_safe The command is safe to send when the Nextion is sleeping + */ + +void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) { + this->add_no_result_to_queue_with_set(component->get_variable_name(), component->get_variable_name_to_send(), + state_value); +} + +void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, int state_value) { + this->add_no_result_to_queue_with_set_internal_(variable_name, variable_name_to_send, state_value); +} + +void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name, + const std::string &variable_name_to_send, int state_value, + bool is_sleep_safe) { + if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) + return; + + this->add_no_result_to_queue_with_ignore_sleep_printf_(variable_name, "%s=%d", variable_name_to_send.c_str(), + state_value); +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param variable_name_to_send Variable name for the left of the command + * @param state_value Sting value to set + * @param is_sleep_safe The command is safe to send when the Nextion is sleeping + */ +void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) { + this->add_no_result_to_queue_with_set(component->get_variable_name(), component->get_variable_name_to_send(), + state_value); +} +void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value) { + this->add_no_result_to_queue_with_set_internal_(variable_name, variable_name_to_send, state_value); +} + +void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value, bool is_sleep_safe) { + if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) + return; + + this->add_no_result_to_queue_with_printf_(variable_name, "%s=\"%s\"", variable_name_to_send.c_str(), + state_value.c_str()); +} + +void Nextion::add_to_get_queue(NextionComponentBase *component) { + if ((!this->is_setup() && !this->ignore_is_setup_)) + return; + + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + nextion_queue->component = component; + nextion_queue->queue_time = millis(); + + ESP_LOGN(TAG, "Add to queue type: %s component %s", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str()); + + std::string command = "get " + component->get_variable_name_to_send(); + + if (this->send_command_(command)) { + this->nextion_queue_.push_back(nextion_queue); } } +/** + * @brief Add addt command to the queue + * + * @param component_id The waveform component id + * @param wave_chan_id The waveform channel to send it to + * @param buffer_to_send The buffer size + * @param buffer_size The buffer data + */ +void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return; + + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->queue_time = millis(); + + size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() + : 255; // ADDT command can only send 255 + + std::string command = "addt " + to_string(component->get_component_id()) + "," + + to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send); + if (this->send_command_(command)) { + this->nextion_queue_.push_back(nextion_queue); + } +} + +void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } + +ESPDEPRECATED("set_wait_for_ack(bool) is deprecated and has no effect") +void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "This command is depreciated"); } + } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index a55ff747ee..2389cc6235 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1,9 +1,21 @@ #pragma once -#include "esphome/core/component.h" +#include #include "esphome/core/defines.h" #include "esphome/components/uart/uart.h" -#include "esphome/components/binary_sensor/binary_sensor.h" +#include "nextion_base.h" +#include "nextion_component.h" +#include "esphome/components/display/display_color_utils.h" + +#if defined(USE_ETHERNET) || defined(USE_WIFI) +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif +#endif #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" @@ -12,12 +24,14 @@ namespace esphome { namespace nextion { -class NextionTouchComponent; class Nextion; +class NextionComponentBase; using nextion_writer_t = std::function; -class Nextion : public PollingComponent, public uart::UARTDevice { +static const std::string COMMAND_DELIMITER{static_cast(255), static_cast(255), static_cast(255)}; + +class Nextion : public NextionBase, public PollingComponent, public uart::UARTDevice { public: /** * Set the text of a component to a static string. @@ -73,9 +87,20 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * This will change the image of the component `pic` to the image with ID `4`. */ - void set_component_picture(const char *component, const char *picture) { - this->send_command_printf("%s.val=%s", component, picture); - } + void set_component_picture(const char *component, const char *picture); + /** + * Set the background color of a component. + * @param component The component name. + * @param color The color (as a uint32_t). + * + * Example: + * ```cpp + * it.set_component_background_color("button", 0xFF0000); + * ``` + * + * This will change the background color of the component `button` to red. + */ + void set_component_background_color(const char *component, uint32_t color); /** * Set the background color of a component. * @param component The component name. @@ -83,7 +108,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_background_color("button", "17013"); + * it.set_component_background_color("button", "RED"); * ``` * * This will change the background color of the component `button` to blue. @@ -91,6 +116,33 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void set_component_background_color(const char *component, const char *color); + /** + * Set the background color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_background_color("button", color); + * ``` + * + * This will change the background color of the component `button` to what color contains. + */ + void set_component_background_color(const char *component, Color color) override; + /** + * Set the pressed background color of a component. + * @param component The component name. + * @param color The color (as a int). + * + * Example: + * ```cpp + * it.set_component_pressed_background_color("button", 0xFF0000 ); + * ``` + * + * This will change the pressed background color of the component `button` to red. This is the background color that + * is shown when the component is pressed. + */ + void set_component_pressed_background_color(const char *component, uint32_t color); /** * Set the pressed background color of a component. * @param component The component name. @@ -98,7 +150,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_pressed_background_color("button", "17013"); + * it.set_component_pressed_background_color("button", "RED"); * ``` * * This will change the pressed background color of the component `button` to blue. This is the background color that @@ -107,6 +159,63 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ void set_component_pressed_background_color(const char *component, const char *color); + /** + * Set the pressed background color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_pressed_background_color("button", color); + * ``` + * + * This will change the pressed background color of the component `button` to blue. This is the background color that + * is shown when the component is pressed. Use this [color + * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI + * colors. + */ + void set_component_pressed_background_color(const char *component, Color color) override; + + /** + * Set the picture id of a component. + * @param component The component name. + * @param pic_id The picture ID. + * + * Example: + * ```cpp + * it.set_component_pic("textview", 1); + * ``` + * + * This will change the picture id of the component `textview`. + */ + void set_component_pic(const char *component, uint8_t pic_id); + /** + * Set the background picture id of component. + * @param component The component name. + * @param pic_id The picture ID. + * + * Example: + * ```cpp + * it.set_component_picc("textview", 1); + * ``` + * + * This will change the background picture id of the component `textview`. + */ + void set_component_picc(const char *component, uint8_t pic_id); + + /** + * Set the font color of a component. + * @param component The component name. + * @param color The color (as a uint32_t ). + * + * Example: + * ```cpp + * it.set_component_font_color("textview", 0xFF0000); + * ``` + * + * This will change the font color of the component `textview` to a red color. + */ + void set_component_font_color(const char *component, uint32_t color); /** * Set the font color of a component. * @param component The component name. @@ -114,7 +223,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_font_color("textview", "17013"); + * it.set_component_font_color("textview", "RED"); * ``` * * This will change the font color of the component `textview` to a blue color. @@ -122,6 +231,34 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void set_component_font_color(const char *component, const char *color); + /** + * Set the font color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_font_color("textview", color); + * ``` + * + * This will change the font color of the component `textview` to a blue color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_font_color(const char *component, Color color) override; + /** + * Set the pressed font color of a component. + * @param component The component name. + * @param color The color (as a uint32_t). + * + * Example: + * ```cpp + * it.set_component_pressed_font_color("button", 0xFF0000); + * ``` + * + * This will change the pressed font color of the component `button` to a red. + */ + void set_component_pressed_font_color(const char *component, uint32_t color); /** * Set the pressed font color of a component. * @param component The component name. @@ -129,7 +266,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_pressed_font_color("button", "17013"); + * it.set_component_pressed_font_color("button", "RED"); * ``` * * This will change the pressed font color of the component `button` to a blue color. @@ -137,6 +274,21 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void set_component_pressed_font_color(const char *component, const char *color); + /** + * Set the pressed font color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_pressed_font_color("button", color); + * ``` + * + * This will change the pressed font color of the component `button` to a blue color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_pressed_font_color(const char *component, Color color) override; /** * Set the coordinates of a component on screen. * @param component The component name. @@ -163,7 +315,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Changes the font of the component named `textveiw`. Font IDs are set in the Nextion Editor. */ - void set_component_font(const char *component, uint8_t font_id); + void set_component_font(const char *component, uint8_t font_id) override; #ifdef USE_TIME /** * Send the current time to the nextion display. @@ -195,7 +347,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Hides the component named `button`. */ - void hide_component(const char *component); + void hide_component(const char *component) override; /** * Show a component. * @param component The component name. @@ -207,7 +359,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Shows the component named `button`. */ - void show_component(const char *component); + void show_component(const char *component) override; /** * Enable touch for a component. * @param component The component name. @@ -239,6 +391,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * @param value The value to write. */ void add_waveform_data(int component_id, uint8_t channel_number, uint8_t value); + void open_waveform_channel(int component_id, uint8_t channel_number, uint8_t value); /** * Display a picture at coordinates. * @param picture_id The picture id. @@ -263,7 +416,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * fill_area(50, 50, 100, 100, "17013"); + * fill_area(50, 50, 100, 100, "RED"); * ``` * * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with @@ -271,6 +424,24 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * convert color codes to Nextion HMI colors */ void fill_area(int x1, int y1, int width, int height, const char *color); + /** + * Fill a rectangle with a color. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width to draw. + * @param height The height to draw. + * @param color The color to draw with (as Color). + * + * Example: + * ```cpp + * fill_area(50, 50, 100, 100, color); + * ``` + * + * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with + * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to + * convert color codes to Nextion HMI colors + */ + void fill_area(int x1, int y1, int width, int height, Color color); /** * Draw a line on the screen. * @param x1 The starting x coordinate. @@ -290,6 +461,25 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ void line(int x1, int y1, int x2, int y2, const char *color); + /** + * Draw a line on the screen. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param x2 The ending x coordinate. + * @param y2 The ending y coordinate. + * @param color The color to draw with (as Color). + * + * Example: + * ```cpp + * it.line(50, 50, 75, 75, "17013"); + * ``` + * + * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate + * `75` with the color of blue. Use this [color + * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI + * colors. + */ + void line(int x1, int y1, int x2, int y2, Color color); /** * Draw a rectangle outline. * @param x1 The starting x coordinate. @@ -309,6 +499,25 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ void rectangle(int x1, int y1, int width, int height, const char *color); + /** + * Draw a rectangle outline. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @param color The color to draw with (as Color). + * + * Example: + * ```cpp + * it.rectangle(25, 35, 40, 50, "17013"); + * ``` + * + * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a + * length of `50` with color of blue. Use this [color + * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI + * colors. + */ + void rectangle(int x1, int y1, int width, int height, Color color); /** * Draw a circle outline * @param center_x The center x coordinate. @@ -317,6 +526,14 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * @param color The color to draw with (as a string). */ void circle(int center_x, int center_y, int radius, const char *color); + /** + * Draw a circle outline + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (as Color). + */ + void circle(int center_x, int center_y, int radius, Color color); /** * Draw a filled circled. * @param center_x The center x coordinate. @@ -334,19 +551,36 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void filled_circle(int center_x, int center_y, int radius, const char *color); - - /** Set the brightness of the backlight. - * - * @param brightness The brightness, from 0 to 100. + /** + * Draw a filled circled. + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (as Color). * * Example: * ```cpp - * it.set_backlight_brightness(30); + * it.filled_cricle(25, 25, 10, color); + * ``` + * + * Makes a filled circle at the x cordinates `25` and y coordinate `25` with a radius of `10` with a color of blue. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void filled_circle(int center_x, int center_y, int radius, Color color); + + /** Set the brightness of the backlight. + * + * @param brightness The brightness percentage from 0 to 1.0. + * + * Example: + * ```cpp + * it.set_backlight_brightness(.3); * ``` * * Changes the brightness of the display to 30%. */ - void set_backlight_brightness(uint8_t brightness); + void set_backlight_brightness(float brightness); /** * Set the touch sleep timeout of the display. * @param timeout Timeout in seconds. @@ -360,10 +594,46 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * `thup`. */ void set_touch_sleep_timeout(uint16_t timeout); + /** + * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. + * @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * wakes up to current page. + * + * Example: + * ```cpp + * it.set_wake_up_page(2); + * ``` + * + * The display will wake up to page 2. + */ + void set_wake_up_page(uint8_t page_id = 255); + /** + * Sets if Nextion should auto-wake from sleep when touch press occurs. + * @param auto_wake True or false. When auto_wake is true and Nextion is in sleep mode, + * the first touch will only trigger the auto wake mode and not trigger a Touch Event. + * + * Example: + * ```cpp + * it.set_auto_wake_on_touch(true); + * ``` + * + * The display will wake up by touch. + */ + void set_auto_wake_on_touch(bool auto_wake); + /** + * Sets Nextion mode between sleep and awake + * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. + */ + void sleep(bool sleep); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void register_touch_component(NextionTouchComponent *obj) { this->touch_.push_back(obj); } + void register_touch_component(NextionComponentBase *obj) { this->touch_.push_back(obj); } + void register_switch_component(NextionComponentBase *obj) { this->switchtype_.push_back(obj); } + void register_binarysensor_component(NextionComponentBase *obj) { this->binarysensortype_.push_back(obj); } + void register_sensor_component(NextionComponentBase *obj) { this->sensortype_.push_back(obj); } + void register_textsensor_component(NextionComponentBase *obj) { this->textsensortype_.push_back(obj); } + void setup() override; void set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override; @@ -371,11 +641,9 @@ class Nextion : public PollingComponent, public uart::UARTDevice { void loop() override; void set_writer(const nextion_writer_t &writer); - /** - * Manually send a raw command to the display and don't wait for an acknowledgement packet. - * @param command The command to write, for example "vis b0,0". - */ - void send_command_no_ack(const char *command); + // This function has been deprecated + void set_wait_for_ack(bool wait_for_ack); + /** * Manually send a raw formatted command to the display. * @param format The printf-style command format, like "vis %s,0" @@ -384,28 +652,199 @@ class Nextion : public PollingComponent, public uart::UARTDevice { */ bool send_command_printf(const char *format, ...) __attribute__((format(printf, 2, 3))); - void set_wait_for_ack(bool wait_for_ack); +#ifdef USE_TFT_UPLOAD + /** + * Set the tft file URL. https seems problamtic with arduino.. + */ + void set_tft_url(const std::string &tft_url) { this->tft_url_ = tft_url; } + +#endif + + /** + * Upload the tft file and softreset the Nextion + */ + void upload_tft(); + void dump_config() override; + + /** + * Softreset the Nextion + */ + void soft_reset(); + + /** Add a callback to be notified of sleep state changes. + * + * @param callback The void() callback. + */ + void add_sleep_state_callback(std::function &&callback); + + /** Add a callback to be notified of wake state changes. + * + * @param callback The void() callback. + */ + void add_wake_state_callback(std::function &&callback); + + /** Add a callback to be notified when the nextion completes its initialize setup. + * + * @param callback The void() callback. + */ + void add_setup_state_callback(std::function &&callback); + + void update_all_components(); + + /** + * @brief Set the nextion sensor state object. + * + * @param[in] queue_type + * Index of NextionQueueType. + * + * @param[in] name + * Component/variable name. + * + * @param[in] state + * State to set. + */ + void set_nextion_sensor_state(int queue_type, const std::string &name, float state); + void set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state); + void set_nextion_text_state(const std::string &name, const std::string &state); + + void add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) override; + void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, + int state_value) override; + + void add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) override; + void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, + const std::string &state_value) override; + + void add_to_get_queue(NextionComponentBase *component) override; + + void add_addt_command_to_queue(NextionComponentBase *component) override; + + void update_components_by_prefix(const std::string &prefix); + + void set_touch_sleep_timeout_internal(uint32_t touch_sleep_timeout) { + this->touch_sleep_timeout_ = touch_sleep_timeout; + } + void set_wake_up_page_internal(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; } + void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } protected: - bool ack_(); - bool read_until_ack_(); + std::deque nextion_queue_; + uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); + void all_components_send_state_(bool force_update = false); + uint64_t comok_sent_ = 0; + bool remove_from_q_(bool report_empty = true); + /** + * @brief + * Sends commands ignoring of the Nextion has been setup. + */ + bool ignore_is_setup_ = false; + bool nextion_reports_is_setup_ = false; + uint8_t nextion_event_; + + void process_nextion_commands_(); + void process_serial_(); + bool is_updating_ = false; + uint32_t touch_sleep_timeout_ = 0; + int wake_up_page_ = -1; + bool auto_wake_on_touch_ = true; + + /** + * Manually send a raw command to the display and don't wait for an acknowledgement packet. + * @param command The command to write, for example "vis b0,0". + */ + bool send_command_(const std::string &command); + void add_no_result_to_queue_(const std::string &variable_name); + bool add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, ...) + __attribute__((format(printf, 3, 4))); + void add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command); + + bool add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) + __attribute__((format(printf, 3, 4))); + + void add_no_result_to_queue_with_set_internal_(const std::string &variable_name, + const std::string &variable_name_to_send, int state_value, + bool is_sleep_safe = false); + + void add_no_result_to_queue_with_set_internal_(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value, bool is_sleep_safe = false); + +#ifdef USE_TFT_UPLOAD +#if defined(USE_ETHERNET) || defined(USE_WIFI) +#ifdef ARDUINO_ARCH_ESP8266 + WiFiClient *wifi_client_{nullptr}; + BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr}; + WiFiClient *get_wifi_client_(); +#endif + + /** + * will request chunk_size chunks from the web server + * and send each to the nextion + * @param int contentLength Total size of the file + * @param uint32_t chunk_size + * @return true if success, false for failure. + */ + int content_length_ = 0; + int tft_size_ = 0; + int upload_by_chunks_(HTTPClient *http, int range_start); + + bool upload_with_range_(uint32_t range_start, uint32_t range_end); + + /** + * start update tft file to nextion. + * + * @param const uint8_t *file_buf + * @param size_t buf_size + * @return true if success, false for failure. + */ + bool upload_from_buffer_(const uint8_t *file_buf, size_t buf_size); + void upload_end_(); + +#endif + +#endif + + bool get_is_connected_() { return this->is_connected_; } + + bool check_connect_(); + + std::vector touch_; + std::vector switchtype_; + std::vector sensortype_; + std::vector textsensortype_; + std::vector binarysensortype_; + CallbackManager setup_callback_{}; + CallbackManager sleep_callback_{}; + CallbackManager wake_callback_{}; - std::vector touch_; optional writer_; - bool wait_for_ack_{true}; float brightness_{1.0}; + + std::string device_model_; + std::string firmware_version_; + std::string serial_number_; + std::string flash_size_; + + void remove_front_no_sensors_(); + +#ifdef USE_TFT_UPLOAD + std::string tft_url_; + uint8_t *transfer_buffer_{nullptr}; + size_t transfer_buffer_size_; + bool upload_first_chunk_sent_ = false; +#endif + +#ifdef NEXTION_PROTOCOL_LOG + void print_queue_members_(); +#endif + void reset_(bool reset_nextion = true); + + std::string command_data_; + bool is_connected_ = false; + uint32_t startup_override_ms_ = 8000; + uint32_t max_q_age_ms_ = 8000; + uint32_t started_ms_ = 0; + bool sent_setup_commands_ = false; }; - -class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { - public: - void set_page_id(uint8_t page_id) { page_id_ = page_id; } - void set_component_id(uint8_t component_id) { component_id_ = component_id; } - void process(uint8_t page_id, uint8_t component_id, bool on); - - protected: - uint8_t page_id_; - uint8_t component_id_; -}; - } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h new file mode 100644 index 0000000000..a24fd74060 --- /dev/null +++ b/esphome/components/nextion/nextion_base.h @@ -0,0 +1,58 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" +#include "nextion_component_base.h" +namespace esphome { +namespace nextion { + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define NEXTION_PROTOCOL_LOG +#endif + +#ifdef NEXTION_PROTOCOL_LOG +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define ESP_LOGN(tag, ...) ESP_LOGVV(tag, __VA_ARGS__) +#else +#define ESP_LOGN(tag, ...) ESP_LOGD(tag, __VA_ARGS__) +#endif +#else +#define ESP_LOGN(tag, ...) \ + {} +#endif + +class NextionBase; + +class NextionBase { + public: + virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) = 0; + virtual void add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, int state_value) = 0; + + virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) = 0; + virtual void add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value) = 0; + + virtual void add_addt_command_to_queue(NextionComponentBase *component) = 0; + + virtual void add_to_get_queue(NextionComponentBase *component) = 0; + + virtual void set_component_background_color(const char *component, Color color) = 0; + virtual void set_component_pressed_background_color(const char *component, Color color) = 0; + virtual void set_component_font_color(const char *component, Color color) = 0; + virtual void set_component_pressed_font_color(const char *component, Color color) = 0; + virtual void set_component_font(const char *component, uint8_t font_id) = 0; + + virtual void show_component(const char *component) = 0; + virtual void hide_component(const char *component) = 0; + + bool is_sleeping() { return this->is_sleeping_; } + bool is_setup() { return this->is_setup_; } + + protected: + bool is_setup_ = false; + bool is_sleeping_ = false; +}; + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp new file mode 100644 index 0000000000..931b934ba2 --- /dev/null +++ b/esphome/components/nextion/nextion_commands.cpp @@ -0,0 +1,234 @@ +#include "nextion.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion"; + +// Sleep safe commands +void Nextion::soft_reset() { this->send_command_("rest"); } + +void Nextion::set_wake_up_page(uint8_t page_id) { + if (page_id > 255) { + ESP_LOGD(TAG, "Wake up page of bounds, range 0-255"); + return; + } + this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", page_id, true); +} + +void Nextion::set_touch_sleep_timeout(uint16_t timeout) { + if (timeout < 3 || timeout > 65535) { + ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535"); + return; + } + + this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", timeout, true); +} + +void Nextion::sleep(bool sleep) { + if (sleep) { // Set sleep + this->is_sleeping_ = true; + this->add_no_result_to_queue_with_set_internal_("sleep", "sleep", 1, true); + } else { // Turn off sleep. Wait for a sleep_wake return before setting sleep off + this->add_no_result_to_queue_with_set_internal_("sleep_wake", "sleep", 0, true); + } +} +// End sleep safe commands + +// Set Colors +void Nextion::set_component_background_color(const char *component, uint32_t color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, color); +} + +void Nextion::set_component_background_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%s", component, color); +} + +void Nextion::set_component_background_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_pressed_background_color(const char *component, uint32_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, color); +} + +void Nextion::set_component_pressed_background_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%s", component, color); +} + +void Nextion::set_component_pressed_background_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_pic(const char *component, uint8_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%d", component, pic_id); +} + +void Nextion::set_component_picc(const char *component, uint8_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.picc=%d", component, pic_id); +} + +void Nextion::set_component_font_color(const char *component, uint32_t color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, color); +} + +void Nextion::set_component_font_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%s", component, color); +} + +void Nextion::set_component_font_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_pressed_font_color(const char *component, uint32_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, color); +} + +void Nextion::set_component_pressed_font_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", " %s.pco2=%s", component, color); +} + +void Nextion::set_component_pressed_font_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_text_printf(const char *component, const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[256]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + this->set_component_text(component, buffer); +} + +// General Nextion +void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %s", page); } + +void Nextion::set_backlight_brightness(float brightness) { + if (brightness < 0 || brightness > 1.0) { + ESP_LOGD(TAG, "Brightness out of bounds, percentage range 0-1.0"); + return; + } + this->add_no_result_to_queue_with_set("backlight_brightness", "dim", static_cast(brightness * 100)); +} + +void Nextion::set_auto_wake_on_touch(bool auto_wake) { + this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake ? 1 : 0); +} + +// General Component +void Nextion::set_component_font(const char *component, uint8_t font_id) { + this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%d", component, font_id); +} + +void Nextion::hide_component(const char *component) { + this->add_no_result_to_queue_with_printf_("hide_component", "vis %s,0", component); +} + +void Nextion::show_component(const char *component) { + this->add_no_result_to_queue_with_printf_("show_component", "vis %s,1", component); +} + +void Nextion::enable_component_touch(const char *component) { + this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); +} + +void Nextion::disable_component_touch(const char *component) { + this->add_no_result_to_queue_with_printf_("disable_component_touch", "tsw %s,0", component); +} + +void Nextion::set_component_picture(const char *component, const char *picture) { + this->add_no_result_to_queue_with_printf_("set_component_picture", "%s.val=%s", component, picture); +} + +void Nextion::set_component_text(const char *component, const char *text) { + this->add_no_result_to_queue_with_printf_("set_component_text", "%s.txt=\"%s\"", component, text); +} + +void Nextion::set_component_value(const char *component, int value) { + this->add_no_result_to_queue_with_printf_("set_component_value", "%s.val=%d", component, value); +} + +void Nextion::add_waveform_data(int component_id, uint8_t channel_number, uint8_t value) { + this->add_no_result_to_queue_with_printf_("add_waveform_data", "add %d,%u,%u", component_id, channel_number, value); +} + +void Nextion::open_waveform_channel(int component_id, uint8_t channel_number, uint8_t value) { + this->add_no_result_to_queue_with_printf_("open_waveform_channel", "addt %d,%u,%u", component_id, channel_number, + value); +} + +void Nextion::set_component_coordinates(const char *component, int x, int y) { + this->add_no_result_to_queue_with_printf_("set_component_coordinates command 1", "%s.xcen=%d", component, x); + this->add_no_result_to_queue_with_printf_("set_component_coordinates command 2", "%s.ycen=%d", component, y); +} + +// Drawing +void Nextion::display_picture(int picture_id, int x_start, int y_start) { + this->add_no_result_to_queue_with_printf_("display_picture", "pic %d %d %d", x_start, y_start, picture_id); +} + +void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { + this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%s", x1, y1, width, height, color); +} + +void Nextion::fill_area(int x1, int y1, int width, int height, Color color) { + this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%d", x1, y1, width, height, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::line(int x1, int y1, int x2, int y2, const char *color) { + this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%s", x1, y1, x2, y2, color); +} + +void Nextion::line(int x1, int y1, int x2, int y2, Color color) { + this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%d", x1, y1, x2, y2, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::rectangle(int x1, int y1, int width, int height, const char *color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%s", x1, y1, x1 + width, y1 + height, color); +} + +void Nextion::rectangle(int x1, int y1, int width, int height, Color color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%d", x1, y1, x1 + width, y1 + height, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::circle(int center_x, int center_y, int radius, const char *color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%s", center_x, center_y, radius, color); +} + +void Nextion::circle(int center_x, int center_y, int radius, Color color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%d", center_x, center_y, radius, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%s", center_x, center_y, radius, color); +} + +void Nextion::filled_circle(int center_x, int center_y, int radius, Color color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%d", center_x, center_y, radius, + display::ColorUtil::color_to_565(color)); +} + +#ifdef USE_TIME +void Nextion::set_nextion_rtc_time(time::ESPTime time) { + this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year); + this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month); + this->add_no_result_to_queue_with_printf_("rtc2", "rtc2=%u", time.day_of_month); + this->add_no_result_to_queue_with_printf_("rtc3", "rtc3=%u", time.hour); + this->add_no_result_to_queue_with_printf_("rtc4", "rtc4=%u", time.minute); + this->add_no_result_to_queue_with_printf_("rtc5", "rtc5=%u", time.second); +} +#endif + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp new file mode 100644 index 0000000000..bbb2cf6cb2 --- /dev/null +++ b/esphome/components/nextion/nextion_component.cpp @@ -0,0 +1,116 @@ +#include "nextion_component.h" + +namespace esphome { +namespace nextion { + +void NextionComponent::set_background_color(Color bco) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->bco_ = bco; + this->bco_needs_update_ = true; + this->bco_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_background_pressed_color(Color bco2) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + + this->bco2_ = bco2; + this->bco2_needs_update_ = true; + this->bco2_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_foreground_color(Color pco) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->pco_ = pco; + this->pco_needs_update_ = true; + this->pco_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_foreground_pressed_color(Color pco2) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->pco2_ = pco2; + this->pco2_needs_update_ = true; + this->pco2_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_font_id(uint8_t font_id) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->font_id_ = font_id; + this->font_id_needs_update_ = true; + this->font_id_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_visible(bool visible) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->visible_ = visible; + this->visible_needs_update_ = true; + this->visible_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::update_component_settings(bool force_update) { + if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ || + (!this->visible_needs_update_ && !this->visible_)) { + this->needs_to_send_update_ = true; + return; + } + + if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) { + std::string name_to_send = this->variable_name_; + + size_t pos = name_to_send.find_last_of('.'); + if (pos != std::string::npos) { + name_to_send = name_to_send.substr(pos + 1); + } + + this->visible_needs_update_ = false; + + if (this->visible_) { + this->nextion_->show_component(name_to_send.c_str()); + this->send_state_to_nextion(); + } else { + this->nextion_->hide_component(name_to_send.c_str()); + return; + } + } + + if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) { + this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); + this->bco_needs_update_ = false; + } + if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) { + this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_); + this->bco2_needs_update_ = false; + } + if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) { + this->nextion_->set_component_font_color(this->variable_name_.c_str(), this->pco_); + this->pco_needs_update_ = false; + } + if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) { + this->nextion_->set_component_pressed_font_color(this->variable_name_.c_str(), this->pco2_); + this->pco2_needs_update_ = false; + } + + if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) { + this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_); + this->font_id_needs_update_ = false; + } +} +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_component.h b/esphome/components/nextion/nextion_component.h new file mode 100644 index 0000000000..2f3c4f3c16 --- /dev/null +++ b/esphome/components/nextion/nextion_component.h @@ -0,0 +1,49 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" +#include "nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionComponent; + +class NextionComponent : public NextionComponentBase { + public: + void update_component_settings() override { this->update_component_settings(false); }; + + void update_component_settings(bool force_update) override; + + void set_background_color(Color bco); + void set_background_pressed_color(Color bco2); + void set_foreground_color(Color pco); + void set_foreground_pressed_color(Color pco2); + void set_font_id(uint8_t font_id); + void set_visible(bool visible); + + protected: + NextionBase *nextion_; + + bool bco_needs_update_ = false; + bool bco_is_set_ = false; + Color bco_; + bool bco2_needs_update_ = false; + bool bco2_is_set_ = false; + Color bco2_; + bool pco_needs_update_ = false; + bool pco_is_set_ = false; + Color pco_; + bool pco2_needs_update_ = false; + bool pco2_is_set_ = false; + Color pco2_; + uint8_t font_id_ = 0; + bool font_id_needs_update_ = false; + bool font_id_is_set_ = false; + + bool visible_ = true; + bool visible_needs_update_ = false; + bool visible_is_set_ = false; + + // void send_state_to_nextion() = 0; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h new file mode 100644 index 0000000000..71ad803bc4 --- /dev/null +++ b/esphome/components/nextion/nextion_component_base.h @@ -0,0 +1,95 @@ +#pragma once +#include +#include "esphome/core/defines.h" + +namespace esphome { +namespace nextion { + +enum NextionQueueType { + NO_RESULT = 0, + SENSOR = 1, + BINARY_SENSOR = 2, + SWITCH = 3, + TEXT_SENSOR = 4, + WAVEFORM_SENSOR = 5, +}; + +static const char *const NEXTION_QUEUE_TYPE_STRINGS[] = {"NO_RESULT", "SENSOR", "BINARY_SENSOR", + "SWITCH", "TEXT_SENSOR", "WAVEFORM_SENSOR"}; + +class NextionComponentBase; + +class NextionQueue { + public: + virtual ~NextionQueue() = default; + NextionComponentBase *component; + uint32_t queue_time = 0; +}; + +class NextionComponentBase { + public: + virtual ~NextionComponentBase() = default; + + void set_variable_name(const std::string &variable_name, const std::string &variable_name_to_send = "") { + variable_name_ = variable_name; + if (variable_name_to_send.empty()) { + variable_name_to_send_ = variable_name; + } else { + variable_name_to_send_ = variable_name_to_send; + } + } + + virtual void update_component_settings(){}; + virtual void update_component_settings(bool force_update){}; + + virtual void update_component(){}; + virtual void process_sensor(const std::string &variable_name, int state){}; + virtual void process_touch(uint8_t page_id, uint8_t component_id, bool on){}; + virtual void process_text(const std::string &variable_name, const std::string &text_value){}; + virtual void process_bool(const std::string &variable_name, bool on){}; + + virtual void set_state(float state){}; + virtual void set_state(float state, bool publish){}; + virtual void set_state(float state, bool publish, bool send_to_nextion){}; + + virtual void set_state(bool state){}; + virtual void set_state(bool state, bool publish){}; + virtual void set_state(bool state, bool publish, bool send_to_nextion){}; + + virtual void set_state(const std::string &state) {} + virtual void set_state(const std::string &state, bool publish) {} + virtual void set_state(const std::string &state, bool publish, bool send_to_nextion){}; + + uint8_t get_component_id() { return this->component_id_; } + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + + uint8_t get_wave_channel_id() { return this->wave_chan_id_; } + void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + + std::vector get_wave_buffer() { return this->wave_buffer_; } + size_t get_wave_buffer_size() { return this->wave_buffer_.size(); } + + std::string get_variable_name() { return this->variable_name_; } + std::string get_variable_name_to_send() { return this->variable_name_to_send_; } + virtual NextionQueueType get_queue_type() { return NextionQueueType::NO_RESULT; } + virtual std::string get_queue_type_string() { return NEXTION_QUEUE_TYPE_STRINGS[this->get_queue_type()]; } + virtual void set_state_from_int(int state_value, bool publish, bool send_to_nextion){}; + virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){}; + virtual void send_state_to_nextion(){}; + bool get_needs_to_send_update() { return this->needs_to_send_update_; } + uint8_t get_wave_chan_id() { return this->wave_chan_id_; } + void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } + + protected: + std::string variable_name_; + std::string variable_name_to_send_; + + uint8_t component_id_ = 0; + uint8_t wave_chan_id_ = UINT8_MAX; + std::vector wave_buffer_; + int wave_max_length_ = 255; + + bool needs_to_send_update_; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp new file mode 100644 index 0000000000..6a681af6c5 --- /dev/null +++ b/esphome/components/nextion/nextion_upload.cpp @@ -0,0 +1,343 @@ + +#include "nextion.h" +#include "esphome/core/application.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion_upload"; + +#if defined(USE_TFT_UPLOAD) && (defined(USE_ETHERNET) || defined(USE_WIFI)) + +// Followed guide +// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 + +int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { + int range_end = 0; + + if (range_start == 0 && this->transfer_buffer_size_ > 16384) { // Start small at the first run in case of a big skip + range_end = 16384 - 1; + } else { + range_end = range_start + this->transfer_buffer_size_ - 1; + } + + if (range_end > this->tft_size_) + range_end = this->tft_size_; + + bool begin_status = false; +#ifdef ARDUINO_ARCH_ESP32 + begin_status = http->begin(this->tft_url_.c_str()); +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#ifndef CLANG_TIDY + http->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + http->setRedirectLimit(3); + begin_status = http->begin(*this->get_wifi_client_(), this->tft_url_.c_str()); +#endif +#endif + + char range_header[64]; + sprintf(range_header, "bytes=%d-%d", range_start, range_end); + + ESP_LOGD(TAG, "Requesting range: %s", range_header); + + int tries = 1; + int code = 0; + while (tries <= 5) { +#ifdef ARDUINO_ARCH_ESP32 + begin_status = http->begin(this->tft_url_.c_str()); +#endif +#ifndef CLANG_TIDY +#ifdef ARDUINO_ARCH_ESP8266 + begin_status = http->begin(*this->get_wifi_client_(), this->tft_url_.c_str()); +#endif +#endif + + ++tries; + if (!begin_status) { + ESP_LOGD(TAG, "upload_by_chunks_: connection failed"); + continue; + } + + http->addHeader("Range", range_header); + + code = http->GET(); + if (code == 200 || code == 206) { + break; + } + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retries(%d/5)", this->tft_url_.c_str(), + HTTPClient::errorToString(code).c_str(), tries); + http->end(); + App.feed_wdt(); + delay(500); // NOLINT + } + + if (tries > 5) { + return -1; + } + + std::string recv_string; + size_t size = 0; + int sent = 0; + int range = range_end - range_start; + + while (sent < range) { + size = http->getStreamPtr()->available(); + if (!size) { + App.feed_wdt(); + delay(0); + continue; + } + int c = http->getStreamPtr()->readBytes( + &this->transfer_buffer_[sent], ((size > this->transfer_buffer_size_) ? this->transfer_buffer_size_ : size)); + sent += c; + } + http->end(); + ESP_LOGN(TAG, "this->content_length_ %d sent %d", this->content_length_, sent); + for (uint32_t i = 0; i < range; i += 4096) { + this->write_array(&this->transfer_buffer_[i], 4096); + this->content_length_ -= 4096; + ESP_LOGN(TAG, "this->content_length_ %d range %d range_end %d range_start %d", this->content_length_, range, + range_end, range_start); + + if (!this->upload_first_chunk_sent_) { + this->upload_first_chunk_sent_ = true; + delay(500); // NOLINT + App.feed_wdt(); + } + + this->recv_ret_string_(recv_string, 2048, true); + if (recv_string[0] == 0x08) { + uint32_t result = 0; + for (int i = 0; i < 4; ++i) { + result += static_cast(recv_string[i + 1]) << (8 * i); + } + if (result > 0) { + ESP_LOGD(TAG, "Nextion reported new range %d", result); + this->content_length_ = this->tft_size_ - result; + return result; + } + } + recv_string.clear(); + } + return range_end + 1; +} + +void Nextion::upload_tft() { + if (this->is_updating_) { + ESP_LOGD(TAG, "Currently updating"); + return; + } + + if (!network_is_connected()) { + ESP_LOGD(TAG, "network is not connected"); + return; + } + + this->is_updating_ = true; + + HTTPClient http; + http.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along + bool begin_status = false; +#ifdef ARDUINO_ARCH_ESP32 + begin_status = http.begin(this->tft_url_.c_str()); +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#ifndef CLANG_TIDY + http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + http.setRedirectLimit(3); + begin_status = http.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); +#endif +#endif + + if (!begin_status) { + this->is_updating_ = false; + ESP_LOGD(TAG, "connection failed"); +#ifdef ARDUINO_ARCH_ESP32 + if (psramFound()) + free(this->transfer_buffer_); + else +#endif + delete this->transfer_buffer_; + return; + } else { + ESP_LOGD(TAG, "Connected"); + } + + http.addHeader("Range", "bytes=0-255"); + const char *header_names[] = {"Content-Range"}; + http.collectHeaders(header_names, 1); + ESP_LOGD(TAG, "Requesting URL: %s", this->tft_url_.c_str()); + + http.setReuse(true); + // try up to 5 times. DNS sometimes needs a second try or so + int tries = 1; + int code = http.GET(); + delay(100); // NOLINT + + App.feed_wdt(); + while (code != 200 && code != 206 && tries <= 5) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", this->tft_url_.c_str(), + HTTPClient::errorToString(code).c_str(), tries); + + delay(250); // NOLINT + App.feed_wdt(); + code = http.GET(); + ++tries; + } + + if ((code != 200 && code != 206) || tries > 5) { + this->upload_end_(); + } + + String content_range_string = http.header("Content-Range"); + content_range_string.remove(0, 12); + this->content_length_ = content_range_string.toInt(); + this->tft_size_ = content_length_; + http.end(); + + if (this->content_length_ < 4096) { + ESP_LOGE(TAG, "Failed to get file size"); + this->upload_end_(); + } + + ESP_LOGD(TAG, "Updating Nextion %s...", this->device_model_.c_str()); + // The Nextion will ignore the update command if it is sleeping + + this->send_command_("sleep=0"); + this->set_backlight_brightness(1.0); + delay(250); // NOLINT + + App.feed_wdt(); + + char command[128]; + // Tells the Nextion the content length of the tft file and baud rate it will be sent at + // Once the Nextion accepts the command it will wait until the file is successfully uploaded + // If it fails for any reason a power cycle of the display will be needed + sprintf(command, "whmi-wris %d,%d,1", this->content_length_, this->parent_->get_baud_rate()); + + // Clear serial receive buffer + uint8_t d; + while (this->available()) { + this->read_byte(&d); + }; + + this->send_command_(command); + + App.feed_wdt(); + + std::string response; + ESP_LOGD(TAG, "Waiting for upgrade response"); + this->recv_ret_string_(response, 2000, true); // This can take some time to return + + // The Nextion display will, if it's ready to accept data, send a 0x05 byte. + ESP_LOGD(TAG, "Upgrade response is %s %zu", response.c_str(), response.length()); + + for (int i = 0; i < response.length(); i++) { + ESP_LOGD(TAG, "Available %d : 0x%02X", i, response[i]); + } + + if (response.find(0x05) != std::string::npos) { + ESP_LOGD(TAG, "preparation for tft update done"); + } else { + ESP_LOGD(TAG, "preparation for tft update failed %d \"%s\"", response[0], response.c_str()); + this->upload_end_(); + } + + // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 +#ifdef ARDUINO_ARCH_ESP32 + uint32_t chunk_size = 8192; + if (psramFound()) { + chunk_size = this->content_length_; + } else { + if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand + int chunk = int((ESP.getFreeHeap() - 32768) / 4096); + chunk_size = chunk * 4096; + chunk_size = chunk_size > 65536 ? 65536 : chunk_size; + } else if (ESP.getFreeHeap() < 10240) { + chunk_size = 4096; + } + } +#else + uint32_t chunk_size = ESP.getFreeHeap() < 10240 ? 4096 : 8192; +#endif + + if (this->transfer_buffer_ == nullptr) { +#ifdef ARDUINO_ARCH_ESP32 + if (psramFound()) { + ESP_LOGD(TAG, "Allocating PSRAM buffer size %d, Free PSRAM size is %u", chunk_size, ESP.getFreePsram()); + this->transfer_buffer_ = (uint8_t *) ps_malloc(chunk_size); + if (this->transfer_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer size %d!", chunk_size); + this->upload_end_(); + } + } else { +#endif + ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); + this->transfer_buffer_ = new uint8_t[chunk_size]; + if (!this->transfer_buffer_) { // Try a smaller size + ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); + chunk_size = 4096; + ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); + this->transfer_buffer_ = new uint8_t[chunk_size]; + + if (!this->transfer_buffer_) + this->upload_end_(); +#ifdef ARDUINO_ARCH_ESP32 + } +#endif + } + + this->transfer_buffer_size_ = chunk_size; + } + + ESP_LOGD(TAG, "Updating tft from \"%s\" with a file size of %d using %zu chunksize, Heap Size %d", + this->tft_url_.c_str(), this->content_length_, this->transfer_buffer_size_, ESP.getFreeHeap()); + + int result = 0; + while (this->content_length_ > 0) { + result = this->upload_by_chunks_(&http, result); + if (result < 0) { + ESP_LOGD(TAG, "Error updating Nextion!"); + this->upload_end_(); + } + App.feed_wdt(); + ESP_LOGD(TAG, "Heap Size %d, Bytes left %d", ESP.getFreeHeap(), this->content_length_); + } + ESP_LOGD(TAG, "Succesfully updated Nextion!"); + + this->upload_end_(); +} + +void Nextion::upload_end_() { + ESP_LOGD(TAG, "Restarting Nextion"); + this->soft_reset(); + delay(1500); // NOLINT + ESP_LOGD(TAG, "Restarting esphome"); + ESP.restart(); +} + +#ifdef ARDUINO_ARCH_ESP8266 +WiFiClient *Nextion::get_wifi_client_() { + if (this->tft_url_.compare(0, 6, "https:") == 0) { + if (this->wifi_client_secure_ == nullptr) { + this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); + this->wifi_client_secure_->setInsecure(); + this->wifi_client_secure_->setBufferSizes(512, 512); + } + return this->wifi_client_secure_; + } + + if (this->wifi_client_ == nullptr) { + this->wifi_client_ = new WiFiClient(); + } + return this->wifi_client_; +} +#endif + +#else +void Nextion::upload_tft() { ESP_LOGW(TAG, "tft_url, WIFI or Ethernet components are needed. Cannot upload."); } +#endif +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py new file mode 100644 index 0000000000..f8a383e3ac --- /dev/null +++ b/esphome/components/nextion/sensor/__init__.py @@ -0,0 +1,99 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor + +from esphome.const import ( + CONF_ID, + UNIT_EMPTY, + ICON_EMPTY, + CONF_COMPONENT_ID, + DEVICE_CLASS_EMPTY, +) +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONFIG_SENSOR_COMPONENT_SCHEMA, + CONF_VARIABLE_NAME, + CONF_COMPONENT_NAME, + CONF_PRECISION, + CONF_WAVE_CHANNEL_ID, + CONF_WAVE_MAX_VALUE, + CONF_WAVEFORM_SEND_LAST_VALUE, + CONF_WAVE_MAX_LENGTH, +) + + +CODEOWNERS = ["@senexcrenshaw"] + +NextionSensor = nextion_ns.class_("NextionSensor", sensor.Sensor, cg.PollingComponent) + + +def CheckWaveID(value): + value = cv.int_(value) + if value < 0 or value > 3: + raise cv.Invalid(f"Valid range for {CONF_WAVE_CHANNEL_ID} is 0-3") + return value + + +def _validate(config): + if CONF_WAVE_CHANNEL_ID in config and CONF_COMPONENT_ID not in config: + raise cv.Invalid( + f"{CONF_COMPONENT_ID} is required when {CONF_WAVE_CHANNEL_ID} is set" + ) + + return config + + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY) + .extend( + { + cv.GenerateID(): cv.declare_id(NextionSensor), + cv.Optional(CONF_PRECISION, default=0): cv.int_range(min=0, max=8), + cv.Optional(CONF_WAVE_CHANNEL_ID): CheckWaveID, + cv.Optional(CONF_COMPONENT_ID): cv.uint8_t, + cv.Optional(CONF_WAVE_MAX_LENGTH, default=255): cv.int_range( + min=1, max=1024 + ), + cv.Optional(CONF_WAVE_MAX_VALUE, default=100): cv.int_range( + min=1, max=1024 + ), + cv.Optional(CONF_WAVEFORM_SEND_LAST_VALUE, default=True): cv.boolean, + } + ) + .extend(CONFIG_SENSOR_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_exactly_one_key(CONF_COMPONENT_ID, CONF_COMPONENT_NAME, CONF_VARIABLE_NAME), + _validate, +) + + +async def to_code(config): + + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + cg.add(hub.register_sensor_component(var)) + + await setup_component_core_(var, config, ".val") + + if CONF_PRECISION in config: + cg.add(var.set_precision(config[CONF_PRECISION])) + + if CONF_COMPONENT_ID in config: + cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) + + if CONF_WAVE_CHANNEL_ID in config: + cg.add(var.set_wave_channel_id(config[CONF_WAVE_CHANNEL_ID])) + + if CONF_WAVEFORM_SEND_LAST_VALUE in config: + cg.add(var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE])) + + if CONF_WAVE_MAX_VALUE in config: + cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE])) + + if CONF_WAVE_MAX_LENGTH in config: + cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH])) diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp new file mode 100644 index 0000000000..bbcb465d85 --- /dev/null +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -0,0 +1,110 @@ +#include "nextion_sensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_sensor"; + +void NextionSensor::process_sensor(const std::string &variable_name, int state) { + if (!this->nextion_->is_setup()) + return; + + if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) { + this->publish_state(state); + ESP_LOGD(TAG, "Processed sensor \"%s\" state %d", variable_name.c_str(), state); + } +} + +void NextionSensor::add_to_wave_buffer(float state) { + this->needs_to_send_update_ = true; + + int wave_state = (int) ((state / (float) this->wave_maxvalue_) * 100); + + wave_buffer_.push_back(wave_state); + + if (this->wave_buffer_.size() > this->wave_max_length_) { + this->wave_buffer_.erase(this->wave_buffer_.begin()); + } +} + +void NextionSensor::update() { + if (!this->nextion_->is_setup()) + return; + + if (this->wave_chan_id_ == UINT8_MAX) { + this->nextion_->add_to_get_queue(this); + } else { + if (this->send_last_value_) { + this->add_to_wave_buffer(this->last_value_); + } + + this->wave_update_(); + } +} + +void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (isnan(state)) + return; + + if (this->wave_chan_id_ == UINT8_MAX) { + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + + if (this->precision_ > 0) { + double to_multiply = pow(10, this->precision_); + int state_value = (int) (state * to_multiply); + + this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value); + } else { + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); + } + } + } + } else { + if (this->send_last_value_) { + this->last_value_ = state; // Update will handle setting the buffer + } else { + this->add_to_wave_buffer(state); + } + } + + if (this->wave_chan_id_ == UINT8_MAX) { + if (publish) { + this->publish_state(state); + } else { + this->raw_state = state; + this->state = state; + this->has_state_ = true; + } + } + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for sensor \"%s\" state %lf", this->variable_name_.c_str(), state); +} + +void NextionSensor::wave_update_() { + if (this->nextion_->is_sleeping() || this->wave_buffer_.empty()) { + return; + } + +#ifdef NEXTION_PROTOCOL_LOG + size_t buffer_to_send = + this->wave_buffer_.size() < 255 ? this->wave_buffer_.size() : 255; // ADDT command can only send 255 + + ESP_LOGN(TAG, "wave_update send %zu of %zu value(s) to wave nextion component id %d and wave channel id %d", + buffer_to_send, this->wave_buffer_.size(), this->component_id_, this->wave_chan_id_); +#endif + + this->nextion_->add_addt_command_to_queue(this); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h new file mode 100644 index 0000000000..e4dde9a513 --- /dev/null +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -0,0 +1,49 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionSensor; + +class NextionSensor : public NextionComponent, public sensor::Sensor, public PollingComponent { + public: + NextionSensor(NextionBase *nextion) { this->nextion_ = nextion; } + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + + void update_component() override { this->update(); } + void update() override; + void add_to_wave_buffer(float state); + void set_precision(uint8_t precision) { this->precision_ = precision; } + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; } + void process_sensor(const std::string &variable_name, int state) override; + + void set_state(float state) override { this->set_state(state, true, true); } + void set_state(float state, bool publish) override { this->set_state(state, publish, true); } + void set_state(float state, bool publish, bool send_to_nextion) override; + + void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; } + uint8_t get_wave_chan_id() { return this->wave_chan_id_; } + void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } + NextionQueueType get_queue_type() override { + return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR; + } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value, publish, send_to_nextion); + } + + protected: + uint8_t precision_ = 0; + uint32_t wave_maxvalue_ = 255; + + float last_value_ = 0; + bool send_last_value_ = true; + void wave_update_(); +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/switch/__init__.py b/esphome/components/nextion/switch/__init__.py new file mode 100644 index 0000000000..068681fa14 --- /dev/null +++ b/esphome/components/nextion/switch/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch + +from esphome.const import CONF_ID +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONF_COMPONENT_NAME, + CONF_VARIABLE_NAME, + CONFIG_SWITCH_COMPONENT_SCHEMA, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionSwitch = nextion_ns.class_("NextionSwitch", switch.Switch, cg.PollingComponent) + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionSwitch), + } + ) + .extend(CONFIG_SWITCH_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_exactly_one_key(CONF_COMPONENT_NAME, CONF_VARIABLE_NAME), +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await switch.register_switch(var, config) + + cg.add(hub.register_switch_component(var)) + + await setup_component_core_(var, config, ".val") diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp new file mode 100644 index 0000000000..1f32ad3425 --- /dev/null +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -0,0 +1,52 @@ +#include "nextion_switch.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_switch"; + +void NextionSwitch::process_bool(const std::string &variable_name, bool on) { + if (!this->nextion_->is_setup()) + return; + if (this->variable_name_ == variable_name) { + this->publish_state(on); + + ESP_LOGD(TAG, "Processed switch \"%s\" state %s", variable_name.c_str(), state ? "ON" : "OFF"); + } +} + +void NextionSwitch::update() { + if (!this->nextion_->is_setup()) + return; + this->nextion_->add_to_get_queue(this); +} + +void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); + } + } + if (publish) { + this->publish_state(state); + } else { + this->state = state; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Updated switch \"%s\" state %s", this->variable_name_.c_str(), ONOFF(state)); +} + +void NextionSwitch::write_state(bool state) { this->set_state(state); } + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/switch/nextion_switch.h b/esphome/components/nextion/switch/nextion_switch.h new file mode 100644 index 0000000000..1548287473 --- /dev/null +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -0,0 +1,34 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionSwitch; + +class NextionSwitch : public NextionComponent, public switch_::Switch, public PollingComponent { + public: + NextionSwitch(NextionBase *nextion) { this->nextion_ = nextion; } + + void update() override; + void update_component() override { this->update(); } + void process_bool(const std::string &variable_name, bool on) override; + + void set_state(bool state) override { this->set_state(state, true, true); } + void set_state(bool state, bool publish) override { this->set_state(state, publish, true); } + void set_state(bool state, bool publish, bool send_to_nextion) override; + + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + NextionQueueType get_queue_type() override { return NextionQueueType::SWITCH; } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value != 0, publish, send_to_nextion); + } + + protected: + void write_state(bool state) override; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/text_sensor/__init__.py b/esphome/components/nextion/text_sensor/__init__.py new file mode 100644 index 0000000000..9c170dd807 --- /dev/null +++ b/esphome/components/nextion/text_sensor/__init__.py @@ -0,0 +1,38 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID + +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONFIG_TEXT_COMPONENT_SCHEMA, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionTextSensor = nextion_ns.class_( + "NextionTextSensor", text_sensor.TextSensor, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionTextSensor), + } + ) + .extend(CONFIG_TEXT_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")) +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + + cg.add(hub.register_textsensor_component(var)) + + await setup_component_core_(var, config, ".txt") diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp new file mode 100644 index 0000000000..08f032df74 --- /dev/null +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -0,0 +1,49 @@ +#include "nextion_textsensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion_textsensor"; + +void NextionTextSensor::process_text(const std::string &variable_name, const std::string &text_value) { + if (!this->nextion_->is_setup()) + return; + if (this->variable_name_ == variable_name) { + this->publish_state(text_value); + ESP_LOGD(TAG, "Processed text_sensor \"%s\" state \"%s\"", variable_name.c_str(), text_value.c_str()); + } +} + +void NextionTextSensor::update() { + if (!this->nextion_->is_setup()) + return; + this->nextion_->add_to_get_queue(this); +} + +void NextionTextSensor::set_state(const std::string &state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->nextion_->add_no_result_to_queue_with_set(this, state); + } + } + + if (publish) { + this->publish_state(state); + } else { + this->state = state; + this->has_state_ = true; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for text_sensor \"%s\" state \"%s\"", this->variable_name_.c_str(), state.c_str()); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.h b/esphome/components/nextion/text_sensor/nextion_textsensor.h new file mode 100644 index 0000000000..5716d0a008 --- /dev/null +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -0,0 +1,32 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionTextSensor; + +class NextionTextSensor : public NextionComponent, public text_sensor::TextSensor, public PollingComponent { + public: + NextionTextSensor(NextionBase *nextion) { this->nextion_ = nextion; } + void update() override; + void update_component() override { this->update(); } + void on_state_changed(const std::string &state); + + void process_text(const std::string &variable_name, const std::string &text_value) override; + + void set_state(const std::string &state, bool publish) override { this->set_state(state, publish, true); } + void set_state(const std::string &state) override { this->set_state(state, true, true); } + void set_state(const std::string &state, bool publish, bool send_to_nextion) override; + + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + NextionQueueType get_queue_type() override { return NextionQueueType::TEXT_SENSOR; } + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value, publish, send_to_nextion); + } +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 6dd62070da..cd54290c73 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -56,6 +56,7 @@ class ESP8266SoftwareSerial { class UARTComponent : public Component, public Stream { public: void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } + uint32_t get_baud_rate() const { return baud_rate_; } uint32_t get_config(); diff --git a/script/ci-custom.py b/script/ci-custom.py index 02f193c6e0..d79e5b5e2f 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +from helpers import git_ls_files, filter_changed import codecs import collections import fnmatch @@ -12,7 +13,6 @@ import functools import argparse sys.path.append(os.path.dirname(__file__)) -from helpers import git_ls_files, filter_changed def find_all(a_str, sub): @@ -562,6 +562,7 @@ def lint_inclusive_language(fname, match): "esphome/components/number/number.h", "esphome/components/output/binary_output.h", "esphome/components/output/float_output.h", + "esphome/components/nextion/nextion_base.h", "esphome/components/sensor/sensor.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", diff --git a/tests/test1.yaml b/tests/test1.yaml index 1c522a23d4..f9fe7d106d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1055,10 +1055,6 @@ binary_sensor: pin: GPIO27 threshold: 1000 id: btn_left - - platform: nextion - page_id: 0 - component_id: 2 - name: 'Nextion Component 2 Touch' - platform: template name: 'Garage Door Open' id: garage_door @@ -1882,11 +1878,6 @@ display: intensity: 3 lambda: |- it.print("1234"); - - platform: nextion - uart_id: uart0 - lambda: |- - it.set_component_value("gauge", 50); - it.set_component_text("textview", "Hello World!"); - platform: pcd8544 cs_pin: GPIO23 dc_pin: GPIO23 diff --git a/tests/test3.yaml b/tests/test3.yaml index c709539309..acd975d794 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -269,6 +269,7 @@ wled: adalight: + sensor: - platform: apds9960 type: proximity @@ -534,6 +535,15 @@ sensor: export_reactive_energy: name: 'Export Reactive Energy' + - platform: nextion + id: testnumber + name: 'testnumber' + variable_name: testnumber + - platform: nextion + id: testwave + name: 'testwave' + component_id: 2 + wave_channel_id: 1 time: - platform: homeassistant @@ -605,7 +615,14 @@ binary_sensor: binary_sensors: - id: custom_binary_sensor name: Custom Binary Sensor - + - platform: nextion + page_id: 0 + component_id: 2 + name: 'Nextion Component 2 Touch' + - platform: nextion + id: r0_sensor + name: 'R0 Sensor' + component_name: page0.r0 globals: - id: my_global_string type: std::string @@ -653,6 +670,11 @@ text_sensor: text_sensors: - id: custom_text_sensor name: Custom Text Sensor + - platform: nextion + name: text0 + id: text0 + update_interval: 4s + component_name: text0 script: - id: my_script @@ -704,6 +726,10 @@ switch: switches: - id: custom_switch name: Custom Switch + - platform: nextion + id: r0 + name: 'R0 Switch' + component_name: page0.r0 custom_component: lambda: |- @@ -1086,6 +1112,16 @@ display: id: my_matrix lambda: |- it.printdigit("hello"); + - platform: nextion + uart_id: uart1 + tft_url: 'http://esphome.io/default35.tft' + update_interval: 5s + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' http_request: useragent: esphome/device