From 0906559afe55d1e233d4ead938c6f439482ff29e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:38:55 +0000 Subject: [PATCH 01/35] Bump zeroconf from 0.128.0 to 0.128.4 (#5906) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cde39175da..39715232d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==19.3.0 -zeroconf==0.128.0 +zeroconf==0.128.4 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default From b62c099d54eb24c3dff0e53aa3561b524a9c512b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:22:41 +1100 Subject: [PATCH 02/35] Fix 18 bit displays. (#5908) --- esphome/components/ili9xxx/ili9xxx_display.h | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index db1274450a..bf4889afe1 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -30,13 +30,24 @@ class ILI9XXXDisplay : public display::DisplayBuffer, const uint8_t *addr = init_sequence; while ((cmd = *addr++) != 0) { num_args = *addr++ & 0x7F; - if (cmd == ILI9XXX_MADCTL) { - bits = *addr; - this->swap_xy_ = (bits & MADCTL_MV) != 0; - this->mirror_x_ = (bits & MADCTL_MX) != 0; - this->mirror_y_ = (bits & MADCTL_MY) != 0; - this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; - break; + bits = *addr; + switch (cmd) { + case ILI9XXX_MADCTL: { + this->swap_xy_ = (bits & MADCTL_MV) != 0; + this->mirror_x_ = (bits & MADCTL_MX) != 0; + this->mirror_y_ = (bits & MADCTL_MY) != 0; + this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; + break; + } + + case ILI9XXX_PIXFMT: { + if ((bits & 0xF) == 6) + this->is_18bitdisplay_ = true; + break; + } + + default: + break; } addr += num_args; } From 86e6a8a5033acaf9d1e83a7ec65624fab1f276ce Mon Sep 17 00:00:00 2001 From: Clemens Date: Tue, 12 Dec 2023 00:28:16 +0100 Subject: [PATCH 03/35] fix RGBW Mode on RP2040 (#5907) --- esphome/components/rp2040_pio_led_strip/led_strip.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index ce1836306f..c04419a9bf 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -70,9 +70,10 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { // assemble bits in buffer to 32 bit words with ex for GBR: 0bGGGGGGGGRRRRRRRRBBBBBBBB00000000 for (int i = 0; i < this->num_leds_; i++) { - uint8_t c1 = this->buf_[(i * 3) + 0]; - uint8_t c2 = this->buf_[(i * 3) + 1]; - uint8_t c3 = this->buf_[(i * 3) + 2]; + uint8_t multiplier = this->is_rgbw_ ? 4 : 3; + uint8_t c1 = this->buf_[(i * multiplier) + 0]; + uint8_t c2 = this->buf_[(i * multiplier) + 1]; + uint8_t c3 = this->buf_[(i * multiplier) + 2]; uint8_t w = this->is_rgbw_ ? this->buf_[(i * 4) + 3] : 0; uint32_t color = encode_uint32(c1, c2, c3, w); pio_sm_put_blocking(this->pio_, this->sm_, color); From b30430b0bda4f12e72281c987183d7c7a45cc659 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Tue, 12 Dec 2023 14:15:59 +1100 Subject: [PATCH 04/35] Add graphical display menu (#4105) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Alex Hermann --- CODEOWNERS | 1 + esphome/components/display/display.cpp | 7 + esphome/components/display/display.h | 15 ++ .../display_menu_base/display_menu_base.cpp | 12 + .../display_menu_base/display_menu_base.h | 5 + .../display_menu_base/menu_item.cpp | 23 ++ .../components/display_menu_base/menu_item.h | 4 + .../graphical_display_menu/__init__.py | 96 +++++++ .../graphical_display_menu.cpp | 243 ++++++++++++++++++ .../graphical_display_menu.h | 84 ++++++ esphome/core/defines.h | 1 + tests/test1.yaml | 116 ++++++++- 12 files changed, 598 insertions(+), 9 deletions(-) create mode 100644 esphome/components/graphical_display_menu/__init__.py create mode 100644 esphome/components/graphical_display_menu/graphical_display_menu.cpp create mode 100644 esphome/components/graphical_display_menu/graphical_display_menu.h diff --git a/CODEOWNERS b/CODEOWNERS index de80806eac..43b7647fe6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -116,6 +116,7 @@ esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco +esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/gree/* @orestismers esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 22454aeddb..88ee64ea55 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -166,6 +166,13 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in } #endif // USE_QR_CODE +#ifdef USE_GRAPHICAL_DISPLAY_MENU +void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) { + Rect rect(x, y, width, height); + menu->draw(this, &rect); +} +#endif // USE_GRAPHICAL_DISPLAY_MENU + void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width, int *height) { int x_offset, baseline; diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 7ce6d179ef..3afcfb9528 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -17,6 +17,10 @@ #include "esphome/components/qr_code/qr_code.h" #endif +#ifdef USE_GRAPHICAL_DISPLAY_MENU +#include "esphome/components/graphical_display_menu/graphical_display_menu.h" +#endif + namespace esphome { namespace display { @@ -392,6 +396,17 @@ class Display : public PollingComponent { void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); #endif +#ifdef USE_GRAPHICAL_DISPLAY_MENU + /** + * @param x The x coordinate of the upper left corner + * @param y The y coordinate of the upper left corner + * @param menu The GraphicalDisplayMenu to draw + * @param width Width of the menu + * @param height Height of the menu + */ + void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height); +#endif // USE_GRAPHICAL_DISPLAY_MENU + /** Get the text bounds of the given string. * * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp index 57da3cec35..0bfee338ca 100644 --- a/esphome/components/display_menu_base/display_menu_base.cpp +++ b/esphome/components/display_menu_base/display_menu_base.cpp @@ -172,6 +172,8 @@ void DisplayMenuComponent::show_main() { this->process_initial_(); + this->on_before_show(); + if (this->active_ && this->editing_) this->finish_editing_(); @@ -188,6 +190,8 @@ void DisplayMenuComponent::show_main() { } this->draw_and_update(); + + this->on_after_show(); } void DisplayMenuComponent::show() { @@ -196,18 +200,26 @@ void DisplayMenuComponent::show() { this->process_initial_(); + this->on_before_show(); + if (!this->active_) { this->active_ = true; this->draw_and_update(); } + + this->on_after_show(); } void DisplayMenuComponent::hide() { if (this->check_healthy_and_active_()) { + this->on_before_hide(); + if (this->editing_) this->finish_editing_(); this->active_ = false; this->update(); + + this->on_after_hide(); } } diff --git a/esphome/components/display_menu_base/display_menu_base.h b/esphome/components/display_menu_base/display_menu_base.h index 46bb0a8192..6208fcd3b4 100644 --- a/esphome/components/display_menu_base/display_menu_base.h +++ b/esphome/components/display_menu_base/display_menu_base.h @@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component { update(); } + virtual void on_before_show(){}; + virtual void on_after_show(){}; + virtual void on_before_hide(){}; + virtual void on_after_hide(){}; + uint8_t rows_; bool active_; MenuMode mode_; diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index bbe6ec0e89..2c7f34c493 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -5,6 +5,29 @@ namespace esphome { namespace display_menu_base { +const LogString *menu_item_type_to_string(MenuItemType type) { + switch (type) { + case MenuItemType::MENU_ITEM_LABEL: + return LOG_STR("MENU_ITEM_LABEL"); + case MenuItemType::MENU_ITEM_MENU: + return LOG_STR("MENU_ITEM_MENU"); + case MenuItemType::MENU_ITEM_BACK: + return LOG_STR("MENU_ITEM_BACK"); + case MenuItemType::MENU_ITEM_SELECT: + return LOG_STR("MENU_ITEM_SELECT"); + case MenuItemType::MENU_ITEM_NUMBER: + return LOG_STR("MENU_ITEM_NUMBER"); + case MenuItemType::MENU_ITEM_SWITCH: + return LOG_STR("MENU_ITEM_SWITCH"); + case MenuItemType::MENU_ITEM_COMMAND: + return LOG_STR("MENU_ITEM_COMMAND"); + case MenuItemType::MENU_ITEM_CUSTOM: + return LOG_STR("MENU_ITEM_CUSTOM"); + default: + return LOG_STR("UNKNOWN"); + } +} + void MenuItem::on_enter() { this->on_enter_callbacks_.call(); } void MenuItem::on_leave() { this->on_leave_callbacks_.call(); } diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h index a30f31e88f..36de146031 100644 --- a/esphome/components/display_menu_base/menu_item.h +++ b/esphome/components/display_menu_base/menu_item.h @@ -14,6 +14,7 @@ #endif #include +#include "esphome/core/log.h" namespace esphome { namespace display_menu_base { @@ -29,6 +30,9 @@ enum MenuItemType { MENU_ITEM_CUSTOM, }; +/// @brief Returns a string representation of a menu item type suitable for logging +const LogString *menu_item_type_to_string(MenuItemType type); + class MenuItem; class MenuItemMenu; using value_getter_t = std::function; diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py new file mode 100644 index 0000000000..dc49358efd --- /dev/null +++ b/esphome/components/graphical_display_menu/__init__.py @@ -0,0 +1,96 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, font, color +from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome import automation, core + +from esphome.components.display_menu_base import ( + DISPLAY_MENU_BASE_SCHEMA, + DisplayMenuComponent, + display_menu_to_code, +) + +CONF_DISPLAY = "display" +CONF_FONT = "font" +CONF_MENU_ITEM_VALUE = "menu_item_value" +CONF_FOREGROUND_COLOR = "foreground_color" +CONF_BACKGROUND_COLOR = "background_color" +CONF_ON_REDRAW = "on_redraw" + +graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu") +GraphicalDisplayMenu = graphical_display_menu_ns.class_( + "GraphicalDisplayMenu", DisplayMenuComponent +) +GraphicalDisplayMenuConstPtr = GraphicalDisplayMenu.operator("ptr").operator("const") +MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArguments") +MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator( + "const" +) +GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_( + "GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger +) + +CODEOWNERS = ["@MrMDavidson"] + +AUTO_LOAD = ["display_menu_base"] + +CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GraphicalDisplayMenu), + cv.Optional(CONF_DISPLAY): cv.use_id(display.DisplayBuffer), + cv.Required(CONF_FONT): cv.use_id(font.Font), + cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string), + cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_ON_REDRAW): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + GraphicalDisplayMenuOnRedrawTrigger + ) + } + ), + } + ) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if display_config := config.get(CONF_DISPLAY): + drawing_display = await cg.get_variable(display_config) + cg.add(var.set_display(drawing_display)) + + menu_font = await cg.get_variable(config[CONF_FONT]) + cg.add(var.set_font(menu_font)) + + if (menu_item_value_config := config.get(CONF_MENU_ITEM_VALUE, None)) is not None: + if isinstance(menu_item_value_config, core.Lambda): + template_ = await cg.templatable( + menu_item_value_config, + [(MenuItemValueArgumentsConstPtr, "it")], + cg.std_string, + ) + cg.add(var.set_menu_item_value(template_)) + else: + cg.add(var.set_menu_item_value(menu_item_value_config)) + + if foreground_color_config := config.get(CONF_FOREGROUND_COLOR): + foreground_color = await cg.get_variable(foreground_color_config) + cg.add(var.set_foreground_color(foreground_color)) + + if background_color_config := config.get(CONF_BACKGROUND_COLOR): + background_color = await cg.get_variable(background_color_config) + cg.add(var.set_background_color(background_color)) + + for conf in config.get(CONF_ON_REDRAW, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf + ) + + await display_menu_to_code(var, config) + + cg.add_define("USE_GRAPHICAL_DISPLAY_MENU") diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp new file mode 100644 index 0000000000..2e4c14fb7b --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -0,0 +1,243 @@ +#include "graphical_display_menu.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include "esphome/components/display/display.h" + +namespace esphome { +namespace graphical_display_menu { + +static const char *const TAG = "graphical_display_menu"; + +void GraphicalDisplayMenu::setup() { + if (this->display_ != nullptr) { + display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); }; + this->display_page_ = make_unique(writer); + } + + if (!this->menu_item_value_.has_value()) { + this->menu_item_value_ = [](const MenuItemValueArguments *it) { + std::string label = " "; + if (it->is_item_selected && it->is_menu_editing) { + label.append(">"); + label.append(it->item->get_value_text()); + label.append("<"); + } else { + label.append("("); + label.append(it->item->get_value_text()); + label.append(")"); + } + return label; + }; + } + + display_menu_base::DisplayMenuComponent::setup(); +} + +void GraphicalDisplayMenu::dump_config() { + ESP_LOGCONFIG(TAG, "Graphical Display Menu"); + ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr)); + ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr)); + ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr)); + ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr)); + ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick"); + ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_)); + ESP_LOGCONFIG(TAG, "Menu items:"); + for (size_t i = 0; i < this->displayed_item_->items_size(); i++) { + auto *item = this->displayed_item_->get_item(i); + ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(), + LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())), + YESNO(item->get_immediate_edit())); + } +} + +void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; } + +void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; } + +void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; } +void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; } + +void GraphicalDisplayMenu::on_before_show() { + if (this->display_ != nullptr) { + this->previous_display_page_ = this->display_->get_active_page(); + this->display_->show_page(this->display_page_.get()); + this->display_->clear(); + } else { + this->update(); + } +} + +void GraphicalDisplayMenu::on_before_hide() { + if (this->previous_display_page_ != nullptr) { + this->display_->show_page((display::DisplayPage *) this->previous_display_page_); + this->display_->clear(); + this->update(); + this->previous_display_page_ = nullptr; + } else { + this->update(); + } +} + +void GraphicalDisplayMenu::draw_and_update() { + this->update(); + + // If we're in advanced drawing mode we won't have a display and will instead require the update callback to do + // our drawing + if (this->display_ != nullptr) { + draw_menu(); + } +} + +void GraphicalDisplayMenu::draw_menu() { + if (this->display_ == nullptr) { + ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode"); + return; + } + display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height()); + this->draw_menu_internal_(this->display_, &bounds); +} + +void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *bounds) { + this->draw_menu_internal_(display, bounds); +} + +void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) { + int total_height = 0; + int y_padding = 2; + bool scroll_menu_items = false; + std::vector menu_dimensions; + int number_items_fit_to_screen = 0; + const int max_item_index = this->displayed_item_->items_size() - 1; + + for (size_t i = 0; i <= max_item_index; i++) { + const auto *item = this->displayed_item_->get_item(i); + const bool selected = i == this->cursor_index_; + const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); + + menu_dimensions.push_back(item_dimensions); + total_height += item_dimensions.h + (i == 0 ? 0 : y_padding); + + if (total_height <= bounds->h) { + number_items_fit_to_screen++; + } else { + // Scroll the display if the selected item or the item immediately after it overflows + if ((selected) || (i == this->cursor_index_ + 1)) { + scroll_menu_items = true; + } + } + } + + // Determine what items to draw + int first_item_index = 0; + int last_item_index = max_item_index; + + if (number_items_fit_to_screen <= 1) { + // If only one item can fit to the bounds draw the current cursor item + last_item_index = std::min(last_item_index, this->cursor_index_ + 1); + first_item_index = this->cursor_index_; + } else { + if (scroll_menu_items) { + // Attempt to draw the item after the current item (+1 for equality check in the draw loop) + last_item_index = std::min(last_item_index, this->cursor_index_ + 1); + + // Go back through the measurements to determine how many prior items we can fit + int height_left_to_use = bounds->h; + for (int i = last_item_index; i >= 0; i--) { + const display::Rect item_dimensions = menu_dimensions[i]; + height_left_to_use -= (item_dimensions.h + y_padding); + + if (height_left_to_use <= 0) { + // Ran out of space - this is our first item to draw + first_item_index = i; + break; + } + } + const int items_to_draw = last_item_index - first_item_index; + // Dont't draw last item partially if it is the selected item + if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) && + (first_item_index < max_item_index)) { + first_item_index++; + } + } + } + + // Render the items into the view port + display->start_clipping(*bounds); + + int y_offset = bounds->y; + for (size_t i = first_item_index; i <= last_item_index; i++) { + const auto *item = this->displayed_item_->get_item(i); + const bool selected = i == this->cursor_index_; + display::Rect dimensions = menu_dimensions[i]; + + dimensions.y = y_offset; + dimensions.x = bounds->x; + this->draw_item(display, item, &dimensions, selected); + + y_offset = dimensions.y + dimensions.h + y_padding; + } + + display->end_clipping(); +} + +display::Rect GraphicalDisplayMenu::measure_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, const bool selected) { + display::Rect dimensions(0, 0, 0, 0); + + if (selected) { + // TODO: Support selection glyph + dimensions.w += 0; + dimensions.h += 0; + } + + std::string label = item->get_text(); + if (item->has_value()) { + // Append to label + MenuItemValueArguments args(item, selected, this->editing_); + label.append(this->menu_item_value_.value(&args)); + } + + int x1; + int y1; + int width; + int height; + display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height); + + dimensions.w = std::min((int16_t) width, bounds->w); + dimensions.h = std::min((int16_t) height, bounds->h); + + return dimensions; +} + +inline void GraphicalDisplayMenu::draw_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, const bool selected) { + const auto background_color = selected ? this->foreground_color_ : this->background_color_; + const auto foreground_color = selected ? this->background_color_ : this->foreground_color_; + + // int background_width = std::max(bounds->width, available_width); + int background_width = bounds->w; + + if (selected) { + display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color); + } + + std::string label = item->get_text(); + if (item->has_value()) { + MenuItemValueArguments args(item, selected, this->editing_); + label.append(this->menu_item_value_.value(&args)); + } + + display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str()); +} + +void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) { + ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific " + "draw_item should be called."); +} + +void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); } + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h new file mode 100644 index 0000000000..96f2bd79fd --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -0,0 +1,84 @@ +#pragma once + +#include "esphome/core/color.h" +#include "esphome/components/display_menu_base/display_menu_base.h" +#include "esphome/components/display_menu_base/menu_item.h" +#include "esphome/core/automation.h" +#include + +namespace esphome { + +// forward declare from display namespace +namespace display { +class Display; +class DisplayPage; +class BaseFont; +class Rect; +} // namespace display + +namespace graphical_display_menu { + +const Color COLOR_ON(255, 255, 255, 255); +const Color COLOR_OFF(0, 0, 0, 0); + +struct MenuItemValueArguments { + MenuItemValueArguments(const display_menu_base::MenuItem *item, bool is_item_selected, bool is_menu_editing) { + this->item = item; + this->is_item_selected = is_item_selected; + this->is_menu_editing = is_menu_editing; + } + + const display_menu_base::MenuItem *item; + bool is_item_selected; + bool is_menu_editing; +}; + +class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { + public: + void setup() override; + void dump_config() override; + + void set_display(display::Display *display); + void set_font(display::BaseFont *font); + template void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; } + void set_foreground_color(Color foreground_color); + void set_background_color(Color background_color); + + void add_on_redraw_callback(std::function &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); } + + void draw(display::Display *display, const display::Rect *bounds); + + protected: + void draw_and_update() override; + void draw_menu() override; + void draw_menu_internal_(display::Display *display, const display::Rect *bounds); + void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override; + virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, bool selected); + virtual void draw_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, bool selected); + void update() override; + + void on_before_show() override; + void on_before_hide() override; + + std::unique_ptr display_page_{nullptr}; + const display::DisplayPage *previous_display_page_{nullptr}; + display::Display *display_{nullptr}; + display::BaseFont *font_{nullptr}; + TemplatableValue menu_item_value_; + Color foreground_color_{COLOR_ON}; + Color background_color_{COLOR_OFF}; + + CallbackManager on_redraw_callbacks_{}; +}; + +class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { + public: + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { + parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + } +}; + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b93b8c9270..e75abdb88f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_UART_DEBUGGER #define USE_WIFI #define USE_WIFI_AP +#define USE_GRAPHICAL_DISPLAY_MENU // Arduino-specific feature flags #ifdef USE_ARDUINO diff --git a/tests/test1.yaml b/tests/test1.yaml index c90cdf6d90..559b7ab8fc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1141,10 +1141,12 @@ sensor: value: !lambda "return -1;" on_clockwise: - logger.log: Clockwise - - display_menu.down: + - display_menu.down: test_lcd_menu + - display_menu.down: test_graphical_display_menu on_anticlockwise: - logger.log: Anticlockwise - - display_menu.up: + - display_menu.up: test_lcd_menu + - display_menu.up: test_graphical_display_menu - platform: pulse_width name: Pulse Width pin: @@ -1781,13 +1783,22 @@ binary_sensor: on_press: - if: condition: - display_menu.is_active: + display_menu.is_active: test_lcd_menu then: - - display_menu.enter: + - display_menu.enter: test_lcd_menu else: - - display_menu.left: - - display_menu.right: - - display_menu.show: + - display_menu.left: test_lcd_menu + - display_menu.right: test_lcd_menu + - display_menu.show: test_lcd_menu + - if: + condition: + display_menu.is_active: test_graphical_display_menu + then: + - display_menu.enter: test_graphical_display_menu + else: + - display_menu.left: test_graphical_display_menu + - display_menu.right: test_graphical_display_menu + - display_menu.show: test_graphical_display_menu - platform: template name: Garage Door Open id: garage_door @@ -3204,6 +3215,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 + id: st7735_display model: INITR_BLACKTAB cs_pin: allow_other_uses: true @@ -3997,6 +4009,7 @@ ld2420: uart_id: ld2420_uart lcd_menu: + id: test_lcd_menu display_id: my_lcd_gpio mark_back: 0x5e mark_selected: 0x3e @@ -4028,7 +4041,7 @@ lcd_menu: text: Show Main on_value: then: - - display_menu.show_main: + - display_menu.show_main: test_lcd_menu - type: select text: Enum Item immediate_edit: true @@ -4058,7 +4071,7 @@ lcd_menu: text: Hide on_value: then: - - display_menu.hide: + - display_menu.hide: test_lcd_menu - type: switch text: Switch switch: my_switch @@ -4078,6 +4091,91 @@ lcd_menu: then: lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +graphical_display_menu: + id: test_graphical_display_menu + display: st7735_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: 'Back' + - type: label + - type: menu + text: 'Submenu 1' + items: + - type: back + text: 'Back' + - type: menu + text: 'Submenu 21' + items: + - type: back + text: 'Back' + - type: command + text: 'Show Main' + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: 'Enum Item' + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: 'Number' + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: 'Hide' + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: 'Switch' + switch: my_switch + on_text: 'Bright' + off_text: 'Dark' + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' + alarm_control_panel: - platform: template id: alarmcontrolpanel1 From 39d026299e6834fbd3182f0f717e5c96414b053b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 12 Dec 2023 05:48:21 +0100 Subject: [PATCH 05/35] Added on_image callback to ESP32 Cam (#4860) --- esphome/components/esp32_camera/__init__.py | 17 +++++++++++++++ .../components/esp32_camera/esp32_camera.cpp | 4 ++-- .../components/esp32_camera/esp32_camera.h | 21 +++++++++++++++++-- tests/test4.yaml | 4 ++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 4cbdf7ca5c..ee8a889f4c 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -25,6 +25,11 @@ AUTO_LOAD = ["psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) +ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") +# Triggers +ESP32CameraImageTrigger = esp32_camera_ns.class_( + "ESP32CameraImageTrigger", automation.Trigger.template() +) ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( "ESP32CameraStreamStartTrigger", automation.Trigger.template(), @@ -139,6 +144,7 @@ CONF_IDLE_FRAMERATE = "idle_framerate" # stream trigger CONF_ON_STREAM_START = "on_stream_start" CONF_ON_STREAM_STOP = "on_stream_stop" +CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) @@ -221,6 +227,11 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ), } ), + cv.Optional(CONF_ON_IMAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -289,3 +300,9 @@ async def to_code(config): for conf in config.get(CONF_ON_STREAM_STOP, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_IMAGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(ESP32CameraImageData, "image")], conf + ) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index e4020a902e..99cb811fe4 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -335,8 +335,8 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { } /* ---------------- public API (specific) ---------------- */ -void ESP32Camera::add_image_callback(std::function)> &&f) { - this->new_image_callback_.add(std::move(f)); +void ESP32Camera::add_image_callback(std::function)> &&callback) { + this->new_image_callback_.add(std::move(callback)); } void ESP32Camera::add_stream_start_callback(std::function &&callback) { this->stream_start_callback_.add(std::move(callback)); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 5f88c6fda8..0c25381039 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -86,6 +86,11 @@ class CameraImage { uint8_t requesters_; }; +struct CameraImageData { + uint8_t *data; + size_t length; +}; + /* ---------------- CameraImageReader class ---------------- */ class CameraImageReader { public: @@ -147,12 +152,12 @@ class ESP32Camera : public Component, public EntityBase { void dump_config() override; float get_setup_priority() const override; /* public API (specific) */ - void add_image_callback(std::function)> &&f); void start_stream(CameraRequester requester); void stop_stream(CameraRequester requester); void request_image(CameraRequester requester); void update_camera_parameters(); + void add_image_callback(std::function)> &&callback); void add_stream_start_callback(std::function &&callback); void add_stream_stop_callback(std::function &&callback); @@ -196,7 +201,7 @@ class ESP32Camera : public Component, public EntityBase { uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; - CallbackManager)> new_image_callback_; + CallbackManager)> new_image_callback_{}; CallbackManager stream_start_callback_{}; CallbackManager stream_stop_callback_{}; @@ -207,6 +212,18 @@ class ESP32Camera : public Component, public EntityBase { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32Camera *global_esp32_camera; +class ESP32CameraImageTrigger : public Trigger { + public: + explicit ESP32CameraImageTrigger(ESP32Camera *parent) { + parent->add_image_callback([this](const std::shared_ptr &image) { + CameraImageData camera_image_data{}; + camera_image_data.length = image->get_data_length(); + camera_image_data.data = image->get_data_buffer(); + this->trigger(camera_image_data); + }); + } +}; + class ESP32CameraStreamStartTrigger : public Trigger<> { public: explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { diff --git a/tests/test4.yaml b/tests/test4.yaml index 05870d3b8f..9f15b84b12 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -861,6 +861,10 @@ esp32_camera: number: GPIO1 resolution: 640x480 jpeg_quality: 10 + on_image: + then: + - lambda: |- + ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); esp32_camera_web_server: - port: 8080 From 7fb10547edb0ba87b2801c8851a045d62e309ab6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:07 +0900 Subject: [PATCH 06/35] Bump actions/stale from 8.0.0 to 9.0.0 (#5899) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a2d3f2f77d..5f510ffe75 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.0.0 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -38,7 +38,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.0.0 with: days-before-pr-stale: -1 days-before-pr-close: -1 From 0117de5b78d49ba41f4be3fa07cde8fc85a7bf8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:22 +0900 Subject: [PATCH 07/35] Bump pylint from 3.0.2 to 3.0.3 (#5905) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index cb96f79587..eef69b7515 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==3.0.2 +pylint==3.0.3 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating black==23.11.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating From d0dd0e38db46cbe37de8a6f894a16e4be8120113 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:36 +0900 Subject: [PATCH 08/35] Bump frenck/action-yamllint from 1.4.1 to 1.4.2 (#5716) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/yaml-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml index a77bd2c078..c9f056b18c 100644 --- a/.github/workflows/yaml-lint.yml +++ b/.github/workflows/yaml-lint.yml @@ -19,4 +19,4 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.1.1 - name: Run yamllint - uses: frenck/action-yamllint@v1.4.1 + uses: frenck/action-yamllint@v1.4.2 From 47665164e80f22500e67994c470bb6fc7627d42c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:52:05 +0900 Subject: [PATCH 09/35] Bump dessant/lock-threads from 4.0.1 to 5.0.1 (#5820) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index b455e3f4ea..e3d75f6d58 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -18,7 +18,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4.0.1 + - uses: dessant/lock-threads@v5.0.1 with: pr-inactive-days: "1" pr-lock-reason: "" From ad79e4fe24fd24267391603f2657ecc92adf5762 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Mon, 11 Dec 2023 22:13:26 -0700 Subject: [PATCH 10/35] Add support for fan preset modes (#5694) Co-authored-by: J. Nick Koston --- esphome/components/api/api.proto | 4 ++ esphome/components/api/api_connection.cpp | 6 ++ esphome/components/api/api_pb2.cpp | 52 +++++++++++++++++ esphome/components/api/api_pb2.h | 6 ++ esphome/components/copy/fan/copy_fan.cpp | 5 ++ esphome/components/fan/__init__.py | 44 ++++++++++++++ esphome/components/fan/automation.h | 18 ++++++ esphome/components/fan/fan.cpp | 58 +++++++++++++++++-- esphome/components/fan/fan.h | 9 +++ esphome/components/fan/fan_traits.h | 10 ++++ esphome/components/hbridge/fan/__init__.py | 7 ++- .../components/hbridge/fan/hbridge_fan.cpp | 11 +++- esphome/components/hbridge/fan/hbridge_fan.h | 7 ++- esphome/components/speed/fan/__init__.py | 9 ++- esphome/components/speed/fan/speed_fan.cpp | 11 +++- esphome/components/speed/fan/speed_fan.h | 7 ++- esphome/const.py | 2 + tests/test1.yaml | 28 ++++++++- 18 files changed, 277 insertions(+), 17 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 2f33750686..34d137981e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -365,6 +365,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10; EntityCategory entity_category = 11; + repeated string supported_preset_modes = 12; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -387,6 +388,7 @@ message FanStateResponse { FanSpeed speed = 4 [deprecated = true]; FanDirection direction = 5; int32 speed_level = 6; + string preset_mode = 7; } message FanCommandRequest { option (id) = 31; @@ -405,6 +407,8 @@ message FanCommandRequest { FanDirection direction = 9; bool has_speed_level = 10; int32 speed_level = 11; + bool has_preset_mode = 12; + string preset_mode = 13; } // ==================== LIGHT ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0389df215f..b8f5b05538 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -293,6 +293,8 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { } if (traits.supports_direction()) resp.direction = static_cast(fan->direction); + if (traits.supports_preset_modes()) + resp.preset_mode = fan->preset_mode; return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::Fan *fan) { @@ -307,6 +309,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) { msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); + for (auto const &preset : traits.supported_preset_modes()) + msg.supported_preset_modes.push_back(preset); msg.disabled_by_default = fan->is_disabled_by_default(); msg.icon = fan->get_icon(); msg.entity_category = static_cast(fan->get_entity_category()); @@ -328,6 +332,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { } if (msg.has_direction) call.set_direction(static_cast(msg.direction)); + if (msg.has_preset_mode) + call.set_preset_mode(msg.preset_mode); call.perform(); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1e97a57bb1..01fb540e7e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1375,6 +1375,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi this->icon = value.as_string(); return true; } + case 12: { + this->supported_preset_modes.push_back(value.as_string()); + return true; + } default: return false; } @@ -1401,6 +1405,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->disabled_by_default); buffer.encode_string(10, this->icon); buffer.encode_enum(11, this->entity_category); + for (auto &it : this->supported_preset_modes) { + buffer.encode_string(12, it, true); + } } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { @@ -1451,6 +1458,12 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + for (const auto &it : this->supported_preset_modes) { + out.append(" supported_preset_modes: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } out.append("}"); } #endif @@ -1480,6 +1493,16 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } } +bool FanStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 7: { + this->preset_mode = value.as_string(); + return true; + } + default: + return false; + } +} bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -1497,6 +1520,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(4, this->speed); buffer.encode_enum(5, this->direction); buffer.encode_int32(6, this->speed_level); + buffer.encode_string(7, this->preset_mode); } #ifdef HAS_PROTO_MESSAGE_DUMP void FanStateResponse::dump_to(std::string &out) const { @@ -1527,6 +1551,10 @@ void FanStateResponse::dump_to(std::string &out) const { sprintf(buffer, "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -1572,6 +1600,20 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed_level = value.as_int32(); return true; } + case 12: { + this->has_preset_mode = value.as_bool(); + return true; + } + default: + return false; + } +} +bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 13: { + this->preset_mode = value.as_string(); + return true; + } default: return false; } @@ -1598,6 +1640,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(9, this->direction); buffer.encode_bool(10, this->has_speed_level); buffer.encode_int32(11, this->speed_level); + buffer.encode_bool(12, this->has_preset_mode); + buffer.encode_string(13, this->preset_mode); } #ifdef HAS_PROTO_MESSAGE_DUMP void FanCommandRequest::dump_to(std::string &out) const { @@ -1648,6 +1692,14 @@ void FanCommandRequest::dump_to(std::string &out) const { sprintf(buffer, "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); + + out.append(" has_preset_mode: "); + out.append(YESNO(this->has_preset_mode)); + out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a63e90b7b7..fc57348863 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -472,6 +472,7 @@ class ListEntitiesFanResponse : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + std::vector supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -490,6 +491,7 @@ class FanStateResponse : public ProtoMessage { enums::FanSpeed speed{}; enums::FanDirection direction{}; int32_t speed_level{0}; + std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -497,6 +499,7 @@ class FanStateResponse : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class FanCommandRequest : public ProtoMessage { @@ -512,6 +515,8 @@ class FanCommandRequest : public ProtoMessage { enums::FanDirection direction{}; bool has_speed_level{false}; int32_t speed_level{0}; + bool has_preset_mode{false}; + std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -519,6 +524,7 @@ class FanCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ListEntitiesLightResponse : public ProtoMessage { diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 74d9da279f..15a7f5e025 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -12,6 +12,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; + this->preset_mode = source_->preset_mode; this->publish_state(); }); @@ -19,6 +20,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; + this->preset_mode = source_->preset_mode; this->publish_state(); } @@ -33,6 +35,7 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); + traits.set_supported_preset_modes(base.supported_preset_modes()); return traits; } @@ -46,6 +49,8 @@ void CopyFan::control(const fan::FanCall &call) { call2.set_speed(*call.get_speed()); if (call.get_direction().has_value()) call2.set_direction(*call.get_direction()); + if (!call.get_preset_mode().empty()) + call2.set_preset_mode(call.get_preset_mode()); call2.perform(); } diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 23df3c2214..fd0f2f66cb 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_ON_SPEED_SET, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, + CONF_ON_PRESET_SET, CONF_TRIGGER_ID, CONF_DIRECTION, CONF_RESTORE_MODE, @@ -57,6 +58,9 @@ CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) +FanPresetSetTrigger = fan_ns.class_( + "FanPresetSetTrigger", automation.Trigger.template() +) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) @@ -101,9 +105,46 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), } ), + cv.Optional(CONF_ON_PRESET_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanPresetSetTrigger), + } + ), } ) +_PRESET_MODES_SCHEMA = cv.All( + cv.ensure_list(cv.string_strict), + cv.Length(min=1), +) + + +def validate_preset_modes(value): + # Check against defined schema + value = _PRESET_MODES_SCHEMA(value) + + # Ensure preset names are unique + errors = [] + presets = set() + for i, preset in enumerate(value): + # If name does not exist yet add it + if preset not in presets: + presets.add(preset) + continue + + # Otherwise it's an error + errors.append( + cv.Invalid( + f"Found duplicate preset name '{preset}'. Presets must have unique names.", + [i], + ) + ) + + if errors: + raise cv.MultipleInvalid(errors) + + return value + async def setup_fan_core_(var, config): await setup_entity(var, config) @@ -154,6 +195,9 @@ async def setup_fan_core_(var, config): for conf in config.get(CONF_ON_SPEED_SET, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PRESET_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_fan(var, config): diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 511acf5682..b5bdeb8a29 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -165,5 +165,23 @@ class FanSpeedSetTrigger : public Trigger<> { int last_speed_; }; +class FanPresetSetTrigger : public Trigger<> { + public: + FanPresetSetTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { + auto preset_mode = state->preset_mode; + auto should_trigger = preset_mode != this->last_preset_mode_; + this->last_preset_mode_ = preset_mode; + if (should_trigger) { + this->trigger(); + } + }); + this->last_preset_mode_ = state->preset_mode; + } + + protected: + std::string last_preset_mode_; +}; + } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 87566bad4a..95e3ae0758 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -32,9 +32,12 @@ void FanCall::perform() { if (this->direction_.has_value()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } - + if (!this->preset_mode_.empty()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str()); + } this->parent_.control(*this); } + void FanCall::validate_() { auto traits = this->parent_.get_traits(); @@ -62,6 +65,15 @@ void FanCall::validate_() { ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); this->direction_.reset(); } + + if (!this->preset_mode_.empty()) { + const auto &preset_modes = traits.supported_preset_modes(); + if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { + ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), + this->preset_mode_.c_str()); + this->preset_mode_.clear(); + } + } } FanCall FanRestoreState::to_call(Fan &fan) { @@ -70,6 +82,14 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_oscillating(this->oscillating); call.set_speed(this->speed); call.set_direction(this->direction); + + if (fan.get_traits().supports_preset_modes()) { + // Use stored preset index to get preset name + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); + } + } return call; } void FanRestoreState::apply(Fan &fan) { @@ -77,6 +97,14 @@ void FanRestoreState::apply(Fan &fan) { fan.oscillating = this->oscillating; fan.speed = this->speed; fan.direction = this->direction; + + if (fan.get_traits().supports_preset_modes()) { + // Use stored preset index to get preset name + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); + } + } fan.publish_state(); } @@ -100,7 +128,9 @@ void Fan::publish_state() { if (traits.supports_direction()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } - + if (traits.supports_preset_modes() && !this->preset_mode.empty()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str()); + } this->state_callback_.call(); this->save_state_(); } @@ -143,20 +173,36 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + + if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { + const auto &preset_modes = this->get_traits().supported_preset_modes(); + // Store index of current preset mode + auto preset_iterator = preset_modes.find(this->preset_mode); + if (preset_iterator != preset_modes.end()) + state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); + } + this->rtc_.save(&state); } void Fan::dump_traits_(const char *tag, const char *prefix) { - if (this->get_traits().supports_speed()) { + auto traits = this->get_traits(); + + if (traits.supports_speed()) { ESP_LOGCONFIG(tag, "%s Speed: YES", prefix); - ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, this->get_traits().supported_speed_count()); + ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, traits.supported_speed_count()); } - if (this->get_traits().supports_oscillation()) { + if (traits.supports_oscillation()) { ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix); } - if (this->get_traits().supports_direction()) { + if (traits.supports_direction()) { ESP_LOGCONFIG(tag, "%s Direction: YES", prefix); } + if (traits.supports_preset_modes()) { + ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); + for (const std::string &s : traits.supported_preset_modes()) + ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str()); + } } } // namespace fan diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index f9d317e675..b74187eb4a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -72,6 +72,11 @@ class FanCall { return *this; } optional get_direction() const { return this->direction_; } + FanCall &set_preset_mode(const std::string &preset_mode) { + this->preset_mode_ = preset_mode; + return *this; + } + std::string get_preset_mode() const { return this->preset_mode_; } void perform(); @@ -83,6 +88,7 @@ class FanCall { optional oscillating_; optional speed_; optional direction_{}; + std::string preset_mode_{}; }; struct FanRestoreState { @@ -90,6 +96,7 @@ struct FanRestoreState { int speed; bool oscillating; FanDirection direction; + uint8_t preset_mode; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); @@ -107,6 +114,8 @@ class Fan : public EntityBase { int speed{0}; /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; + // The current preset mode of the fan + std::string preset_mode{}; FanCall turn_on(); FanCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index e69d8e2e53..2ef6f8b7cc 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,3 +1,6 @@ +#include +#include + #pragma once namespace esphome { @@ -25,12 +28,19 @@ class FanTraits { bool supports_direction() const { return this->direction_; } /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } + /// Return the preset modes supported by the fan. + std::set supported_preset_modes() const { return this->preset_modes_; } + /// Set the preset modes supported by the fan. + void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } + /// Return if preset modes are supported + bool supports_preset_modes() const { return !this->preset_modes_.empty(); } protected: bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; + std::set preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 421883a1ff..424e944290 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import fan, output +from esphome.components.fan import validate_preset_modes from esphome.const import ( CONF_ID, CONF_DECAY_MODE, @@ -10,6 +11,7 @@ from esphome.const import ( CONF_PIN_A, CONF_PIN_B, CONF_ENABLE_PIN, + CONF_PRESET_MODES, ) from .. import hbridge_ns @@ -28,7 +30,6 @@ DECAY_MODE_OPTIONS = { # Actions BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) - CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( { cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), @@ -39,6 +40,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( ), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ).extend(cv.COMPONENT_SCHEMA) @@ -69,3 +71,6 @@ async def to_code(config): if CONF_ENABLE_PIN in config: enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable_pin)) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 44cf5ae049..605a9d4ef3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -33,7 +33,12 @@ void HBridgeFan::setup() { restore->apply(*this); this->write_state_(); } + + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } + void HBridgeFan::dump_config() { LOG_FAN("", "H-Bridge Fan", this); if (this->decay_mode_ == DECAY_MODE_SLOW) { @@ -42,9 +47,7 @@ void HBridgeFan::dump_config() { ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); } } -fan::FanTraits HBridgeFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); -} + void HBridgeFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); @@ -54,10 +57,12 @@ void HBridgeFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); this->write_state_(); this->publish_state(); } + void HBridgeFan::write_state_() { float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; if (speed == 0.0f) { // off means idle diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4389b97ccb..4234fccae3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/automation.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -20,10 +22,11 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } + void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; - fan::FanTraits get_traits() override; + fan::FanTraits get_traits() override { return this->traits_; } fan::FanCall brake(); @@ -34,6 +37,8 @@ class HBridgeFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; + fan::FanTraits traits_; + std::set preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 978e68d1e9..3acfb005bd 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,14 +1,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output +from esphome.components.fan import validate_preset_modes from esphome.const import ( + CONF_PRESET_MODES, + CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, - CONF_DIRECTION_OUTPUT, CONF_OUTPUT_ID, CONF_SPEED, CONF_SPEED_COUNT, ) + from .. import speed_ns SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) @@ -23,6 +26,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( "Configuring individual speeds is deprecated." ), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ).extend(cv.COMPONENT_SCHEMA) @@ -40,3 +44,6 @@ async def to_code(config): if CONF_DIRECTION_OUTPUT in config: direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT]) cg.add(var.set_direction(direction_output)) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3a65f2c365..41b222acd6 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -12,11 +12,14 @@ void SpeedFan::setup() { restore->apply(*this); this->write_state_(); } + + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } + void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } -fan::FanTraits SpeedFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); -} + void SpeedFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); @@ -26,10 +29,12 @@ void SpeedFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); this->write_state_(); this->publish_state(); } + void SpeedFan::write_state_() { float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; this->output_->set_level(speed); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 1fad53813a..ca0fe20e2a 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -15,7 +17,8 @@ class SpeedFan : public Component, public fan::Fan { void dump_config() override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - fan::FanTraits get_traits() override; + void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -25,6 +28,8 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; + fan::FanTraits traits_; + std::set preset_modes_{}; }; } // namespace speed diff --git a/esphome/const.py b/esphome/const.py index 2bc088c063..f688cd75f9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -504,6 +504,7 @@ CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" CONF_ON_MULTI_CLICK = "on_multi_click" CONF_ON_OPEN = "on_open" +CONF_ON_PRESET_SET = "on_preset_set" CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" @@ -601,6 +602,7 @@ CONF_PRESET = "preset" CONF_PRESET_BOOST = "preset_boost" CONF_PRESET_COMMAND_TOPIC = "preset_command_topic" CONF_PRESET_ECO = "preset_eco" +CONF_PRESET_MODES = "preset_modes" CONF_PRESET_SLEEP = "preset_sleep" CONF_PRESET_STATE_TOPIC = "preset_state_topic" CONF_PRESSURE = "pressure" diff --git a/tests/test1.yaml b/tests/test1.yaml index 559b7ab8fc..b77cff7619 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2944,6 +2944,33 @@ fan: on_speed_set: then: - logger.log: Fan speed was changed! + - platform: speed + id: fan_speed_presets + icon: mdi:weather-windy + output: pca_6 + speed_count: 10 + name: Speed Fan w/ Presets + oscillation_output: gpio_19 + direction_output: gpio_26 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! + - platform: hbridge + id: fan_hbridge_presets + icon: mdi:weather-windy + speed_count: 4 + name: H-bridge Fan w/ Presets + pin_a: pca_6 + pin_b: pca_7 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! - platform: bedjet name: My Bedjet fan bedjet_id: my_bedjet_client @@ -4193,4 +4220,3 @@ alarm_control_panel: then: - lambda: !lambda |- ESP_LOGD("TEST", "State change %s", alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())); - From cc7d167e8b0996ff24e75ea0feac386ec0ef6f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87etin=20Koca?= <_@cetinkoca.com> Date: Tue, 12 Dec 2023 08:17:56 +0300 Subject: [PATCH 11/35] Fix uninitialized climate target temperature (#5795) --- esphome/components/climate/climate.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index f90db3f52a..1bbb17322d 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -160,6 +160,8 @@ struct ClimateDeviceRestoreState { */ class Climate : public EntityBase { public: + Climate() {} + /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; /// The active state of the climate device. @@ -172,9 +174,9 @@ class Climate : public EntityBase { float target_temperature; struct { /// The minimum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_low; + float target_temperature_low{NAN}; /// The maximum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_high; + float target_temperature_high{NAN}; }; }; From 04720b8440b43c8183df004720323d106e8ff51a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:39:29 -1000 Subject: [PATCH 12/35] Bump aioesphomeapi from 19.3.0 to 20.0.0 (#5911) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 39715232d6..ae8d50f2ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.3.0 +aioesphomeapi==20.0.0 zeroconf==0.128.4 python-magic==0.4.27 From 8e92bb79589701da72120b26966cc139911bf88c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 21:59:10 +0000 Subject: [PATCH 13/35] Bump black from 23.11.0 to 23.12.0 (#5912) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- esphome/writer.py | 4 +++- requirements_test.txt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc22265f1f..36ec1894d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black args: diff --git a/esphome/writer.py b/esphome/writer.py index ad506b6ae6..83e95614a6 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -203,7 +203,9 @@ def write_platformio_project(): write_platformio_ini(content) -DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ +DEFINES_H_FORMAT = ( + ESPHOME_H_FORMAT +) = """\ #pragma once #include "esphome/core/macros.h" {} diff --git a/requirements_test.txt b/requirements_test.txt index eef69b7515..18c6dedf3e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.0.3 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating -black==23.11.0 # also change in .pre-commit-config.yaml when updating +black==23.12.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit From c6dc336c4acfb8a1d6142d342daf8ecccefc7fc5 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Tue, 12 Dec 2023 23:56:01 +0100 Subject: [PATCH 14/35] Updating the touchscreen interface structure (#4596) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: NP v/d Spek Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Gustavo Ambrozio --- CODEOWNERS | 8 +- .../__init__.py} | 8 +- .../ektf2232/{ => touchscreen}/ektf2232.cpp | 63 +----- .../ektf2232/{ => touchscreen}/ektf2232.h | 14 +- esphome/components/ft5x06/__init__.py | 6 + .../components/ft5x06/touchscreen/__init__.py | 26 +++ .../ft5x06/touchscreen/ft5x06_touchscreen.h | 124 +++++++++++ esphome/components/ft63x6/__init__.py | 1 + esphome/components/ft63x6/ft63x6.cpp | 99 +++++++++ esphome/components/ft63x6/ft63x6.h | 41 ++++ esphome/components/ft63x6/touchscreen.py | 44 ++++ .../components/gt911/touchscreen/__init__.py | 35 +-- .../gt911/touchscreen/gt911_touchscreen.cpp | 75 +++---- .../gt911/touchscreen/gt911_touchscreen.h | 15 +- esphome/components/ili9xxx/display.py | 6 +- esphome/components/inkplate6/display.py | 6 +- .../lilygo_t5_47/touchscreen/__init__.py | 8 +- .../touchscreen/lilygo_t5_47_touchscreen.cpp | 108 ++------- .../touchscreen/lilygo_t5_47_touchscreen.h | 16 +- esphome/components/touchscreen/__init__.py | 52 ++++- .../touchscreen_binary_sensor.cpp | 3 +- .../components/touchscreen/touchscreen.cpp | 131 +++++++++-- esphome/components/touchscreen/touchscreen.h | 109 +++++++-- .../tt21100/touchscreen/__init__.py | 8 +- .../tt21100/touchscreen/tt21100.cpp | 52 +---- .../components/tt21100/touchscreen/tt21100.h | 14 +- esphome/components/xpt2046/binary_sensor.py | 3 - esphome/components/xpt2046/touchscreen.py | 116 ---------- .../xpt2046/touchscreen/__init__.py | 93 ++++++++ .../xpt2046/touchscreen/xpt2046.cpp | 113 ++++++++++ .../components/xpt2046/touchscreen/xpt2046.h | 41 ++++ esphome/components/xpt2046/xpt2046.cpp | 207 ------------------ esphome/components/xpt2046/xpt2046.h | 107 --------- tests/test4.yaml | 79 ++++--- tests/test8.yaml | 2 + 35 files changed, 997 insertions(+), 836 deletions(-) rename esphome/components/ektf2232/{touchscreen.py => touchscreen/__init__.py} (89%) rename esphome/components/ektf2232/{ => touchscreen}/ektf2232.cpp (60%) rename esphome/components/ektf2232/{ => touchscreen}/ektf2232.h (67%) create mode 100644 esphome/components/ft5x06/__init__.py create mode 100644 esphome/components/ft5x06/touchscreen/__init__.py create mode 100644 esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h create mode 100644 esphome/components/ft63x6/__init__.py create mode 100644 esphome/components/ft63x6/ft63x6.cpp create mode 100644 esphome/components/ft63x6/ft63x6.h create mode 100644 esphome/components/ft63x6/touchscreen.py delete mode 100644 esphome/components/xpt2046/binary_sensor.py delete mode 100644 esphome/components/xpt2046/touchscreen.py create mode 100644 esphome/components/xpt2046/touchscreen/__init__.py create mode 100644 esphome/components/xpt2046/touchscreen/xpt2046.cpp create mode 100644 esphome/components/xpt2046/touchscreen/xpt2046.h delete mode 100644 esphome/components/xpt2046/xpt2046.cpp delete mode 100644 esphome/components/xpt2046/xpt2046.h diff --git a/CODEOWNERS b/CODEOWNERS index 43b7647fe6..c37eb3581b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,7 +88,7 @@ esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M -esphome/components/ektf2232/* @jesserockz +esphome/components/ektf2232/touchscreen/* @jesserockz esphome/components/emc2101/* @ellull esphome/components/ens160/* @vincentscode esphome/components/ens210/* @itn3rd77 @@ -110,6 +110,8 @@ esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/fs3000/* @kahrendt +esphome/components/ft5x06/* @clydebarrow +esphome/components/ft63x6/* @gpambrozio esphome/components/gcja5/* @gcormier esphome/components/globals/* @esphome/core esphome/components/gp8403/* @jesserockz @@ -331,7 +333,7 @@ esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka esphome/components/toshiba/* @kbx81 -esphome/components/touchscreen/* @jesserockz +esphome/components/touchscreen/* @jesserockz @nielsnl68 esphome/components/tsl2591/* @wjcarpenter esphome/components/tt21100/* @kroimon esphome/components/tuya/binary_sensor/* @jesserockz @@ -364,6 +366,6 @@ esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xl9535/* @mreditor97 -esphome/components/xpt2046/* @nielsnl68 @numo68 +esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/components/ektf2232/touchscreen.py b/esphome/components/ektf2232/touchscreen/__init__.py similarity index 89% rename from esphome/components/ektf2232/touchscreen.py rename to esphome/components/ektf2232/touchscreen/__init__.py index d937265e7a..c1fefb7f09 100644 --- a/esphome/components/ektf2232/touchscreen.py +++ b/esphome/components/ektf2232/touchscreen/__init__.py @@ -12,7 +12,6 @@ ektf2232_ns = cg.esphome_ns.namespace("ektf2232") EKTF2232Touchscreen = ektf2232_ns.class_( "EKTF2232Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) @@ -28,17 +27,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( ), cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, } - ) - .extend(i2c.i2c_device_schema(0x15)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x15)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/ektf2232/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp similarity index 60% rename from esphome/components/ektf2232/ektf2232.cpp rename to esphome/components/ektf2232/touchscreen/ektf2232.cpp index 80f5f8a8e2..1a2c0389af 100644 --- a/esphome/components/ektf2232/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -15,16 +15,12 @@ static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00}; static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; -void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; } - void EKTF2232Touchscreen::setup() { ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_, - gpio::INTERRUPT_FALLING_EDGE); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); this->rts_pin_->setup(); @@ -45,7 +41,7 @@ void EKTF2232Touchscreen::setup() { this->mark_failed(); return; } - this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); + this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); this->write(GET_Y_RES, 4); if (this->read(received, 4)) { @@ -54,19 +50,14 @@ void EKTF2232Touchscreen::setup() { this->mark_failed(); return; } - this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); - this->store_.touch = false; + this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); this->set_power_state(true); } -void EKTF2232Touchscreen::loop() { - if (!this->store_.touch) - return; - this->store_.touch = false; - +void EKTF2232Touchscreen::update_touches() { uint8_t touch_count = 0; - std::vector touches; + int16_t x_raw, y_raw; uint8_t raw[8]; this->read(raw, 8); @@ -75,45 +66,15 @@ void EKTF2232Touchscreen::loop() { touch_count++; } - if (touch_count == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - touch_count = std::min(touch_count, 2); ESP_LOGV(TAG, "Touch count: %d", touch_count); for (int i = 0; i < touch_count; i++) { uint8_t *d = raw + 1 + (i * 3); - uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1]; - uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2]; - - raw_x = raw_x * this->display_height_ - 1; - raw_y = raw_y * this->display_width_ - 1; - - TouchPoint tp; - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = raw_x / this->x_resolution_; - tp.x = this->display_width_ - 1 - (raw_y / this->y_resolution_); - break; - case ROTATE_90_DEGREES: - tp.x = raw_x / this->x_resolution_; - tp.y = raw_y / this->y_resolution_; - break; - case ROTATE_180_DEGREES: - tp.y = this->display_height_ - 1 - (raw_x / this->x_resolution_); - tp.x = raw_y / this->y_resolution_; - break; - case ROTATE_270_DEGREES: - tp.x = this->display_height_ - 1 - (raw_x / this->x_resolution_); - tp.y = this->display_width_ - 1 - (raw_y / this->y_resolution_); - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); + x_raw = (d[0] & 0xF0) << 4 | d[1]; + y_raw = (d[0] & 0x0F) << 8 | d[2]; + this->set_raw_touch_position_(i, x_raw, y_raw); } } @@ -126,7 +87,7 @@ void EKTF2232Touchscreen::set_power_state(bool enable) { bool EKTF2232Touchscreen::get_power_state() { uint8_t received[4]; this->write(GET_POWER_STATE_CMD, 4); - this->store_.touch = false; + this->store_.touched = false; this->read(received, 4); return (received[1] >> 3) & 1; } @@ -145,14 +106,14 @@ bool EKTF2232Touchscreen::soft_reset_() { uint8_t received[4]; uint16_t timeout = 1000; - while (!this->store_.touch && timeout > 0) { + while (!this->store_.touched && timeout > 0) { delay(1); timeout--; } if (timeout > 0) - this->store_.touch = true; + this->store_.touched = true; this->read(received, 4); - this->store_.touch = false; + this->store_.touched = false; return !memcmp(received, HELLO, 4); } diff --git a/esphome/components/ektf2232/ektf2232.h b/esphome/components/ektf2232/touchscreen/ektf2232.h similarity index 67% rename from esphome/components/ektf2232/ektf2232.h rename to esphome/components/ektf2232/touchscreen/ektf2232.h index e880b77f99..e9288d0a27 100644 --- a/esphome/components/ektf2232/ektf2232.h +++ b/esphome/components/ektf2232/touchscreen/ektf2232.h @@ -9,19 +9,11 @@ namespace esphome { namespace ektf2232 { -struct EKTF2232TouchscreenStore { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(EKTF2232TouchscreenStore *store); -}; - using namespace touchscreen; -class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } @@ -33,12 +25,10 @@ class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2 protected: void hard_reset_(); bool soft_reset_(); + void update_touches() override; InternalGPIOPin *interrupt_pin_; GPIOPin *rts_pin_; - EKTF2232TouchscreenStore store_; - uint16_t x_resolution_; - uint16_t y_resolution_; }; } // namespace ektf2232 diff --git a/esphome/components/ft5x06/__init__.py b/esphome/components/ft5x06/__init__.py new file mode 100644 index 0000000000..dceea71dd0 --- /dev/null +++ b/esphome/components/ft5x06/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +ft5x06_ns = cg.esphome_ns.namespace("ft5x06") diff --git a/esphome/components/ft5x06/touchscreen/__init__.py b/esphome/components/ft5x06/touchscreen/__init__.py new file mode 100644 index 0000000000..adeeac0d1a --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID +from .. import ft5x06_ns + +FT5x06ButtonListener = ft5x06_ns.class_("FT5x06ButtonListener") +FT5x06Touchscreen = ft5x06_ns.class_( + "FT5x06Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FT5x06Touchscreen), + } +).extend(i2c.i2c_device_schema(0x48)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await i2c.register_i2c_device(var, config) + await touchscreen.register_touchscreen(var, config) diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h new file mode 100644 index 0000000000..497d6c906c --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -0,0 +1,124 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ft5x06 { + +static const char *const TAG = "ft5x06.touchscreen"; + +enum VendorId { + FT5X06_ID_UNKNOWN = 0, + FT5X06_ID_1 = 0x51, + FT5X06_ID_2 = 0x11, + FT5X06_ID_3 = 0xCD, +}; + +enum FTCmd : uint8_t { + FT5X06_MODE_REG = 0x00, + FT5X06_ORIGIN_REG = 0x08, + FT5X06_RESOLUTION_REG = 0x0C, + FT5X06_VENDOR_ID_REG = 0xA8, + FT5X06_TD_STATUS = 0x02, + FT5X06_TOUCH_DATA = 0x03, + FT5X06_I_MODE = 0xA4, + FT5X06_TOUCH_MAX = 0x4C, +}; + +enum FTMode : uint8_t { + FT5X06_OP_MODE = 0, + FT5X06_SYSINFO_MODE = 0x10, + FT5X06_TEST_MODE = 0x40, +}; + +static const size_t MAX_TOUCHES = 5; // max number of possible touches reported + +class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override { + esph_log_config(TAG, "Setting up FT5x06 Touchscreen..."); + // wait 200ms after reset. + this->set_timeout(200, [this] { this->continue_setup_(); }); + } + + void continue_setup_(void) { + uint8_t data[4]; + if (!this->set_mode_(FT5X06_OP_MODE)) + return; + + if (!this->err_check_(this->read_register(FT5X06_VENDOR_ID_REG, data, 1), "Read Vendor ID")) + return; + switch (data[0]) { + case FT5X06_ID_1: + case FT5X06_ID_2: + case FT5X06_ID_3: + this->vendor_id_ = (VendorId) data[0]; + esph_log_d(TAG, "Read vendor ID 0x%X", data[0]); + break; + + default: + esph_log_e(TAG, "Unknown vendor ID 0x%X", data[0]); + this->mark_failed(); + return; + } + // reading the chip registers to get max x/y does not seem to work. + this->x_raw_max_ = this->display_->get_width(); + this->y_raw_max_ = this->display_->get_height(); + esph_log_config(TAG, "FT5x06 Touchscreen setup complete"); + } + + void update_touches() override { + uint8_t touch_cnt; + uint8_t data[MAX_TOUCHES][6]; + + if (!this->read_byte(FT5X06_TD_STATUS, &touch_cnt) || touch_cnt > MAX_TOUCHES) { + esph_log_w(TAG, "Failed to read status"); + return; + } + if (touch_cnt == 0) + return; + + if (!this->read_bytes(FT5X06_TOUCH_DATA, (uint8_t *) data, touch_cnt * 6)) { + esph_log_w(TAG, "Failed to read touch data"); + return; + } + for (uint8_t i = 0; i != touch_cnt; i++) { + uint8_t status = data[i][0] >> 6; + uint8_t id = data[i][2] >> 3; + uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]); + uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]); + + esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); + if (status == 0 || status == 2) { + this->set_raw_touch_position_(id, x, y); + } + } + } + + void dump_config() override { + esph_log_config(TAG, "FT5x06 Touchscreen:"); + esph_log_config(TAG, " Address: 0x%02X", this->address_); + esph_log_config(TAG, " Vendor ID: 0x%X", (int) this->vendor_id_); + } + + protected: + bool err_check_(i2c::ErrorCode err, const char *msg) { + if (err != i2c::ERROR_OK) { + this->mark_failed(); + esph_log_e(TAG, "%s failed - err 0x%X", msg, err); + return false; + } + return true; + } + bool set_mode_(FTMode mode) { + return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode"); + } + VendorId vendor_id_{FT5X06_ID_UNKNOWN}; +}; + +} // namespace ft5x06 +} // namespace esphome diff --git a/esphome/components/ft63x6/__init__.py b/esphome/components/ft63x6/__init__.py new file mode 100644 index 0000000000..b6d7d3580e --- /dev/null +++ b/esphome/components/ft63x6/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gpambrozio"] diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp new file mode 100644 index 0000000000..9198954253 --- /dev/null +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -0,0 +1,99 @@ +/**************************************************************************/ +/*! + Author: Gustavo Ambrozio + Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) +*/ +/**************************************************************************/ + +#include "ft63x6.h" +#include "esphome/core/log.h" + +// Registers +// Reference: https://focuslcds.com/content/FT6236.pdf +namespace esphome { +namespace ft63x6 { + +static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02; + +static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05; +static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03; +static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05; + +static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B; +static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09; +static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B; + +static const char *const TAG = "FT63X6Touchscreen"; + +void FT63X6Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up FT63X6Touchscreen Touchscreen..."); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + } + + this->hard_reset_(); + + // Get touch resolution + this->x_raw_max_ = 320; + this->y_raw_max_ = 480; +} + +void FT63X6Touchscreen::update_touches() { + int touch_count = this->read_touch_count_(); + if (touch_count == 0) { + return; + } + + uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1 + int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X); + int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y); + this->set_raw_touch_position_(touch_id, x, y); + + if (touch_count >= 2) { + touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01) + x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X); + y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y); + this->set_raw_touch_position_(touch_id, x, y); + } +} + +void FT63X6Touchscreen::hard_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + } +} + +void FT63X6Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); } + +// Touch functions +uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) { + uint8_t read_buf[2]; + read_buf[0] = this->read_byte_(coordinate); + read_buf[1] = this->read_byte_(coordinate + 1); + return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; +} +uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; } + +uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { + uint8_t byte = 0; + this->read_byte(addr, &byte); + return byte; +} + +} // namespace ft63x6 +} // namespace esphome diff --git a/esphome/components/ft63x6/ft63x6.h b/esphome/components/ft63x6/ft63x6.h new file mode 100644 index 0000000000..79b1991041 --- /dev/null +++ b/esphome/components/ft63x6/ft63x6.h @@ -0,0 +1,41 @@ +/**************************************************************************/ +/*! + Author: Gustavo Ambrozio + Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) +*/ +/**************************************************************************/ + +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace ft63x6 { + +using namespace touchscreen; + +class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void hard_reset_(); + uint8_t read_byte_(uint8_t addr); + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + + uint8_t read_touch_count_(); + uint16_t read_touch_coordinate_(uint8_t coordinate); + uint8_t read_touch_id_(uint8_t id_address); +}; + +} // namespace ft63x6 +} // namespace esphome diff --git a/esphome/components/ft63x6/touchscreen.py b/esphome/components/ft63x6/touchscreen.py new file mode 100644 index 0000000000..d77d9ca287 --- /dev/null +++ b/esphome/components/ft63x6/touchscreen.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +CODEOWNERS = ["@gpambrozio"] +DEPENDENCIES = ["i2c"] + +ft6336u_ns = cg.esphome_ns.namespace("ft63x6") +FT63X6Touchscreen = ft6336u_ns.class_( + "FT63X6Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONF_FT63X6_ID = "ft63x6_id" + + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FT63X6Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ).extend(i2c.i2c_device_schema(0x38)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN): + interrupt_pin = await cg.gpio_pin_expression(interrupt_pin_config) + cg.add(var.set_interrupt_pin(interrupt_pin)) + if reset_pin_config := config.get(CONF_RESET_PIN): + reset_pin = await cg.gpio_pin_expression(reset_pin_config) + cg.add(var.set_reset_pin(reset_pin)) diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py index 295e32b1b1..9a0d5cc169 100644 --- a/esphome/components/gt911/touchscreen/__init__.py +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import i2c, touchscreen -from esphome.const import CONF_INTERRUPT_PIN, CONF_ID, CONF_ROTATION +from esphome.const import CONF_INTERRUPT_PIN, CONF_ID from .. import gt911_ns @@ -11,36 +11,21 @@ GT911ButtonListener = gt911_ns.class_("GT911ButtonListener") GT911Touchscreen = gt911_ns.class_( "GT911Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) -ROTATIONS = { - 0: touchscreen.TouchRotation.ROTATE_0_DEGREES, - 90: touchscreen.TouchRotation.ROTATE_90_DEGREES, - 180: touchscreen.TouchRotation.ROTATE_180_DEGREES, - 270: touchscreen.TouchRotation.ROTATE_270_DEGREES, -} -CONFIG_SCHEMA = ( - touchscreen.TOUCHSCREEN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(GT911Touchscreen), - cv.Optional(CONF_ROTATION): cv.enum(ROTATIONS), - cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, - } - ) - .extend(i2c.i2c_device_schema(0x5D)) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GT911Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + } +).extend(i2c.i2c_device_schema(0x5D)) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) - interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_interrupt_pin(interrupt_pin)) - if CONF_ROTATION in config: - cg.add(var.set_rotation(ROTATIONS[config[CONF_ROTATION]])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 4d3e7e7903..adc577f5da 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -12,6 +12,7 @@ static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; +static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48}; static const size_t MAX_TOUCHES = 5; // max number of possible touches reported #define ERROR_CHECK(err) \ @@ -21,24 +22,35 @@ static const size_t MAX_TOUCHES = 5; // max number of possible touches reported return; \ } -void IRAM_ATTR HOT Store::gpio_intr(Store *store) { store->available = true; } - void GT911Touchscreen::setup() { i2c::ErrorCode err; ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen..."); - // datasheet says NOT to use pullup/down on the int line. - this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); - this->interrupt_pin_->setup(); // check the configuration of the int line. - uint8_t data; + uint8_t data[4]; err = this->write(GET_SWITCHES, 2); if (err == i2c::ERROR_OK) { - err = this->read(&data, 1); + err = this->read(data, 1); if (err == i2c::ERROR_OK) { - ESP_LOGD(TAG, "Read from switches: 0x%02X", data); - this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, - (data & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]); + if (this->interrupt_pin_ != nullptr) { + // datasheet says NOT to use pullup/down on the int line. + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, + (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + } + } + } + if (err == i2c::ERROR_OK) { + err = this->write(GET_MAX_VALUES, 2); + if (err == i2c::ERROR_OK) { + err = this->read(data, sizeof(data)); + if (err == i2c::ERROR_OK) { + this->x_raw_max_ = encode_uint16(data[1], data[0]); + this->y_raw_max_ = encode_uint16(data[3], data[2]); + esph_log_d(TAG, "Read max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_); + } } } if (err != i2c::ERROR_OK) { @@ -46,31 +58,28 @@ void GT911Touchscreen::setup() { this->mark_failed(); return; } + ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); } -void GT911Touchscreen::loop() { +void GT911Touchscreen::update_touches() { i2c::ErrorCode err; - touchscreen::TouchPoint tp; uint8_t touch_state = 0; uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte - if (!this->store_.available) - return; - this->store_.available = false; - err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); ERROR_CHECK(err); err = this->read(&touch_state, 1); ERROR_CHECK(err); this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE)); - - if ((touch_state & 0x80) == 0) - return; uint8_t num_of_touches = touch_state & 0x07; + + if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) { + this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet. + return; + } + if (num_of_touches == 0) - this->send_release_(); - if (num_of_touches > MAX_TOUCHES) // should never happen return; err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); @@ -80,29 +89,10 @@ void GT911Touchscreen::loop() { ERROR_CHECK(err); for (uint8_t i = 0; i != num_of_touches; i++) { - tp.id = data[i][0]; + uint16_t id = data[i][0]; uint16_t x = encode_uint16(data[i][2], data[i][1]); uint16_t y = encode_uint16(data[i][4], data[i][3]); - - switch (this->rotation_) { - case touchscreen::ROTATE_0_DEGREES: - tp.x = x; - tp.y = y; - break; - case touchscreen::ROTATE_90_DEGREES: - tp.x = y; - tp.y = this->display_width_ - x; - break; - case touchscreen::ROTATE_180_DEGREES: - tp.x = this->display_width_ - x; - tp.y = this->display_height_ - y; - break; - case touchscreen::ROTATE_270_DEGREES: - tp.x = this->display_height_ - y; - tp.y = x; - break; - } - this->defer([this, tp]() { this->send_touch_(tp); }); + this->set_raw_touch_position_(id, x, y); } auto keys = data[num_of_touches][0]; for (size_t i = 0; i != 4; i++) { @@ -115,7 +105,6 @@ void GT911Touchscreen::dump_config() { ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); LOG_I2C_DEVICE(this); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); - ESP_LOGCONFIG(TAG, " Rotation: %d", (int) this->rotation_); } } // namespace gt911 diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index dc9248bb4a..44875de5f1 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -8,30 +8,23 @@ namespace esphome { namespace gt911 { -struct Store { - volatile bool available; - - static void gpio_intr(Store *store); -}; - class GT911ButtonListener { public: virtual void update_button(uint8_t index, bool state) = 0; }; -class GT911Touchscreen : public touchscreen::Touchscreen, public Component, public i2c::I2CDevice { +class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; - void set_rotation(touchscreen::TouchRotation rotation) { this->rotation_ = rotation; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } protected: - InternalGPIOPin *interrupt_pin_; - Store store_; + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{}; std::vector button_listeners_; }; diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index f321b2ed63..cd68f1ae27 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -32,7 +32,11 @@ CODEOWNERS = ["@nielsnl68", "@clydebarrow"] ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") ILI9XXXDisplay = ili9xxx_ns.class_( - "ILI9XXXDisplay", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer + "ILI9XXXDisplay", + cg.PollingComponent, + spi.SPIDevice, + display.Display, + display.DisplayBuffer, ) ILI9XXXColorMode = ili9xxx_ns.enum("ILI9XXXColorMode") diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index a1ecfdd1d6..bcd9580448 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -39,7 +39,11 @@ CONF_VCOM_PIN = "vcom_pin" inkplate6_ns = cg.esphome_ns.namespace("inkplate6") Inkplate6 = inkplate6_ns.class_( - "Inkplate6", cg.PollingComponent, i2c.I2CDevice, display.DisplayBuffer + "Inkplate6", + cg.PollingComponent, + i2c.I2CDevice, + display.Display, + display.DisplayBuffer, ) InkplateModel = inkplate6_ns.enum("InkplateModel") diff --git a/esphome/components/lilygo_t5_47/touchscreen/__init__.py b/esphome/components/lilygo_t5_47/touchscreen/__init__.py index fe94120644..01b03c807f 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/__init__.py +++ b/esphome/components/lilygo_t5_47/touchscreen/__init__.py @@ -13,7 +13,6 @@ DEPENDENCIES = ["i2c"] LilygoT547Touchscreen = lilygo_t5_47_ns.class_( "LilygoT547Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) @@ -27,17 +26,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( pins.internal_gpio_input_pin_schema ), } - ) - .extend(i2c.i2c_device_schema(0x5A)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x5A)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b89cf2a724..eb61b6f31e 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -23,15 +23,12 @@ static const uint8_t READ_TOUCH[1] = {0x07}; return; \ } -void Store::gpio_intr(Store *store) { store->touch = true; } - void LilygoT547Touchscreen::setup() { ESP_LOGCONFIG(TAG, "Setting up Lilygo T5 4.7 Touchscreen..."); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); if (this->write(nullptr, 0) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Failed to communicate!"); @@ -41,19 +38,14 @@ void LilygoT547Touchscreen::setup() { } this->write_register(POWER_REGISTER, WAKEUP_CMD, 1); + + this->x_raw_max_ = this->get_width_(); + this->y_raw_max_ = this->get_height_(); } -void LilygoT547Touchscreen::loop() { - if (!this->store_.touch) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - this->store_.touch = false; - +void LilygoT547Touchscreen::update_touches() { uint8_t point = 0; uint8_t buffer[40] = {0}; - uint32_t sum_l = 0, sum_h = 0; i2c::ErrorCode err; err = this->write_register(TOUCH_REGISTER, READ_FLAGS, 1); @@ -69,102 +61,30 @@ void LilygoT547Touchscreen::loop() { point = buffer[5] & 0xF; - if (point == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } else if (point == 1) { + if (point == 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); ERROR_CHECK(err); err = this->read(&buffer[5], 2); ERROR_CHECK(err); - sum_l = buffer[5] << 8 | buffer[6]; } else if (point > 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); ERROR_CHECK(err); err = this->read(&buffer[5], 5 * (point - 1) + 3); ERROR_CHECK(err); - - sum_l = buffer[5 * point + 1] << 8 | buffer[5 * point + 2]; } this->write_register(TOUCH_REGISTER, CLEAR_FLAGS, 2); - for (int i = 0; i < 5 * point; i++) - sum_h += buffer[i]; + if (point == 0) + point = 1; - if (sum_l != sum_h) - point = 0; - - if (point) { - uint8_t offset; - for (int i = 0; i < point; i++) { - if (i == 0) { - offset = 0; - } else { - offset = 4; - } - - TouchPoint tp; - - tp.id = (buffer[i * 5 + offset] >> 4) & 0x0F; - tp.state = buffer[i * 5 + offset] & 0x0F; - if (tp.state == 0x06) - tp.state = 0x07; - - uint16_t y = (uint16_t) ((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); - uint16_t x = (uint16_t) ((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); - - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = this->display_height_ - y; - tp.x = x; - break; - case ROTATE_90_DEGREES: - tp.x = this->display_height_ - y; - tp.y = this->display_width_ - x; - break; - case ROTATE_180_DEGREES: - tp.y = y; - tp.x = this->display_width_ - x; - break; - case ROTATE_270_DEGREES: - tp.x = y; - tp.y = x; - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); - } - } else { - TouchPoint tp; - tp.id = (buffer[0] >> 4) & 0x0F; - tp.state = 0x06; - - uint16_t y = (uint16_t) ((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); - uint16_t x = (uint16_t) ((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); - - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = this->display_height_ - y; - tp.x = x; - break; - case ROTATE_90_DEGREES: - tp.x = this->display_height_ - y; - tp.y = this->display_width_ - x; - break; - case ROTATE_180_DEGREES: - tp.y = y; - tp.x = this->display_width_ - x; - break; - case ROTATE_270_DEGREES: - tp.x = y; - tp.y = x; - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); + uint16_t id, x_raw, y_raw; + for (uint8_t i = 0; i < point; i++) { + id = (buffer[i * 5] >> 4) & 0x0F; + y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F)); + x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F)); + this->set_raw_touch_position_(id, x_raw, y_raw); } this->status_clear_warning(); diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h index 3d00e0b117..6767bf0a71 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h @@ -6,29 +6,25 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace lilygo_t5_47 { -struct Store { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(Store *store); -}; - using namespace touchscreen; -class LilygoT547Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class LilygoT547Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; + void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + void update_touches() override; + InternalGPIOPin *interrupt_pin_; - Store store_; }; } // namespace lilygo_t5_47 diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index a4bdc8cafd..bc09c6364d 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -3,44 +3,84 @@ import esphome.codegen as cg from esphome.components import display from esphome import automation -from esphome.const import CONF_ON_TOUCH +from esphome.const import CONF_ON_TOUCH, CONF_ON_RELEASE from esphome.core import coroutine_with_priority -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@nielsnl68"] DEPENDENCIES = ["display"] IS_PLATFORM_COMPONENT = True touchscreen_ns = cg.esphome_ns.namespace("touchscreen") -Touchscreen = touchscreen_ns.class_("Touchscreen") +Touchscreen = touchscreen_ns.class_("Touchscreen", cg.PollingComponent) TouchRotation = touchscreen_ns.enum("TouchRotation") TouchPoint = touchscreen_ns.struct("TouchPoint") +TouchPoints_t = cg.std_vector.template(TouchPoint) +TouchPoints_t_const_ref = TouchPoints_t.operator("ref").operator("const") TouchListener = touchscreen_ns.class_("TouchListener") CONF_DISPLAY = "display" CONF_TOUCHSCREEN_ID = "touchscreen_id" +CONF_REPORT_INTERVAL = "report_interval" # not used yet: +CONF_ON_UPDATE = "on_update" + +CONF_MIRROR_X = "mirror_x" +CONF_MIRROR_Y = "mirror_y" +CONF_SWAP_XY = "swap_xy" +CONF_TRANSFORM = "transform" TOUCHSCREEN_SCHEMA = cv.Schema( { - cv.GenerateID(CONF_DISPLAY): cv.use_id(display.DisplayBuffer), + cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), } -) +).extend(cv.polling_component_schema("50ms")) async def register_touchscreen(var, config): + await cg.register_component(var, config) + disp = await cg.get_variable(config[CONF_DISPLAY]) cg.add(var.set_display(disp)) + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + if CONF_ON_TOUCH in config: await automation.build_automation( var.get_touch_trigger(), - [(TouchPoint, "touch")], + [(TouchPoint, "touch"), (TouchPoints_t_const_ref, "touches")], config[CONF_ON_TOUCH], ) + if CONF_ON_UPDATE in config: + await automation.build_automation( + var.get_update_trigger(), + [(TouchPoints_t_const_ref, "touches")], + config[CONF_ON_UPDATE], + ) + + if CONF_ON_RELEASE in config: + await automation.build_automation( + var.get_release_trigger(), + [], + config[CONF_ON_RELEASE], + ) + @coroutine_with_priority(100.0) async def to_code(config): diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 66df78b62a..6c26ae3626 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -14,11 +14,10 @@ void TouchscreenBinarySensor::touch(TouchPoint tp) { if (this->page_ != nullptr) { touched &= this->page_ == this->parent_->get_display()->get_active_page(); } - if (touched) { this->publish_state(true); } else { - release(); + this->release(); } } diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 20828aad8b..140f46b6f6 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -7,27 +7,128 @@ namespace touchscreen { static const char *const TAG = "touchscreen"; -void Touchscreen::set_display(display::Display *display) { - this->display_ = display; - this->display_width_ = display->get_width(); - this->display_height_ = display->get_height(); - this->rotation_ = static_cast(display->get_rotation()); +void TouchscreenInterrupt::gpio_intr(TouchscreenInterrupt *store) { store->touched = true; } - if (this->rotation_ == ROTATE_90_DEGREES || this->rotation_ == ROTATE_270_DEGREES) { - std::swap(this->display_width_, this->display_height_); +void Touchscreen::attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type) { + irq_pin->attach_interrupt(TouchscreenInterrupt::gpio_intr, &this->store_, type); + this->store_.init = true; + this->store_.touched = false; +} + +void Touchscreen::update() { + if (!this->store_.init) { + this->store_.touched = true; + } else { + // no need to poll if we have interrupts. + this->stop_poller(); } } -void Touchscreen::send_release_() { - for (auto *listener : this->touch_listeners_) - listener->release(); +void Touchscreen::loop() { + if (this->store_.touched) { + this->first_touch_ = this->touches_.empty(); + this->need_update_ = false; + this->is_touched_ = false; + this->skip_update_ = false; + for (auto &tp : this->touches_) { + if (tp.second.state == STATE_PRESSED || tp.second.state == STATE_UPDATED) { + tp.second.state = tp.second.state | STATE_RELEASING; + } else { + tp.second.state = STATE_RELEASED; + } + tp.second.x_prev = tp.second.x; + tp.second.y_prev = tp.second.y; + } + this->update_touches(); + if (this->skip_update_) { + for (auto &tp : this->touches_) { + tp.second.state = tp.second.state & -STATE_RELEASING; + } + } else { + this->store_.touched = false; + this->defer([this]() { this->send_touches_(); }); + } + } } -void Touchscreen::send_touch_(TouchPoint tp) { - ESP_LOGV(TAG, "Touch (x=%d, y=%d)", tp.x, tp.y); - this->touch_trigger_.trigger(tp); - for (auto *listener : this->touch_listeners_) - listener->touch(tp); +void Touchscreen::set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { + TouchPoint tp; + uint16_t x, y; + if (this->touches_.count(id) == 0) { + tp.state = STATE_PRESSED; + tp.id = id; + } else { + tp = this->touches_[id]; + tp.state = STATE_UPDATED; + } + tp.x_raw = x_raw; + tp.y_raw = y_raw; + tp.z_raw = z_raw; + + x = this->normalize_(x_raw, this->x_raw_min_, this->x_raw_max_, this->invert_x_); + y = this->normalize_(y_raw, this->y_raw_min_, this->y_raw_max_, this->invert_y_); + + if (this->swap_x_y_) { + std::swap(x, y); + } + + tp.x = (uint16_t) ((int) x * this->get_width_() / 0x1000); + tp.y = (uint16_t) ((int) y * this->get_height_() / 0x1000); + + if (tp.state == STATE_PRESSED) { + tp.x_org = tp.x; + tp.y_org = tp.y; + } + + this->touches_[id] = tp; + + this->is_touched_ = true; + if ((tp.x != tp.x_prev) || (tp.y != tp.y_prev)) { + this->need_update_ = true; + } +} + +void Touchscreen::send_touches_() { + if (!this->is_touched_) { + this->release_trigger_.trigger(); + for (auto *listener : this->touch_listeners_) + listener->release(); + this->touches_.clear(); + } else { + TouchPoints_t touches; + for (auto tp : this->touches_) { + touches.push_back(tp.second); + } + if (this->first_touch_) { + TouchPoint tp = this->touches_.begin()->second; + this->touch_trigger_.trigger(tp, touches); + for (auto *listener : this->touch_listeners_) { + listener->touch(tp); + } + } + if (this->need_update_) { + this->update_trigger_.trigger(touches); + for (auto *listener : this->touch_listeners_) { + listener->update(touches); + } + } + } +} + +int16_t Touchscreen::normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted) { + int16_t ret; + + if (val <= min_val) { + ret = 0; + } else if (val >= max_val) { + ret = 0xfff; + } else { + ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); + } + + ret = (inverted) ? 0xfff - ret : ret; + + return ret; } } // namespace touchscreen diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 6e07bcfea0..1fe304d967 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -1,54 +1,119 @@ #pragma once -#include "esphome/components/display/display_buffer.h" +#include "esphome/core/defines.h" +#include "esphome/components/display/display.h" + #include "esphome/core/automation.h" #include "esphome/core/hal.h" #include +#include namespace esphome { namespace touchscreen { +static const uint8_t STATE_RELEASED = 0x00; +static const uint8_t STATE_PRESSED = 0x01; +static const uint8_t STATE_UPDATED = 0x02; +static const uint8_t STATE_RELEASING = 0x04; + struct TouchPoint { - uint16_t x; - uint16_t y; uint8_t id; - uint8_t state; + int16_t x_raw{0}, y_raw{0}, z_raw{0}; + uint16_t x_prev{0}, y_prev{0}; + uint16_t x_org{0}, y_org{0}; + uint16_t x{0}, y{0}; + int8_t state{0}; +}; + +using TouchPoints_t = std::vector; + +struct TouchscreenInterrupt { + volatile bool touched{true}; + bool init{false}; + static void gpio_intr(TouchscreenInterrupt *store); }; class TouchListener { public: - virtual void touch(TouchPoint tp) = 0; + virtual void touch(TouchPoint tp) {} + virtual void update(const TouchPoints_t &tpoints) {} virtual void release() {} }; -enum TouchRotation { - ROTATE_0_DEGREES = 0, - ROTATE_90_DEGREES = 90, - ROTATE_180_DEGREES = 180, - ROTATE_270_DEGREES = 270, -}; - -class Touchscreen { +class Touchscreen : public PollingComponent { public: - void set_display(display::Display *display); + void set_display(display::Display *display) { this->display_ = display; } display::Display *get_display() const { return this->display_; } - Trigger *get_touch_trigger() { return &this->touch_trigger_; } + void set_mirror_x(bool invert_x) { this->invert_x_ = invert_x; } + void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; } + void set_swap_xy(bool swap) { this->swap_x_y_ = swap; } + + void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { + this->x_raw_min_ = std::min(x_min, x_max); + this->x_raw_max_ = std::max(x_min, x_max); + this->y_raw_min_ = std::min(y_min, y_max); + this->y_raw_max_ = std::max(y_min, y_max); + if (x_min > x_max) + this->invert_x_ = true; + if (y_min > y_max) + this->invert_y_ = true; + } + + Trigger *get_touch_trigger() { return &this->touch_trigger_; } + Trigger *get_update_trigger() { return &this->update_trigger_; } + Trigger<> *get_release_trigger() { return &this->release_trigger_; } void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); } + virtual void update_touches() = 0; + + optional get_touch() { return this->touches_.begin()->second; } + + TouchPoints_t get_touches() { + TouchPoints_t touches; + for (auto i : this->touches_) { + touches.push_back(i.second); + } + return touches; + } + + void update() override; + void loop() override; + protected: /// Call this function to send touch points to the `on_touch` listener and the binary_sensors. - void send_touch_(TouchPoint tp); - void send_release_(); - uint16_t display_width_; - uint16_t display_height_; - display::Display *display_; - TouchRotation rotation_; - Trigger touch_trigger_; + void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type); + + void set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); + + void send_touches_(); + + int16_t normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted = false); + + uint16_t get_width_() { return this->display_->get_width(); } + + uint16_t get_height_() { return this->display_->get_height(); } + + display::Display *display_{nullptr}; + + int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0}; + bool invert_x_{false}, invert_y_{false}, swap_x_y_{false}; + + Trigger touch_trigger_; + Trigger update_trigger_; + Trigger<> release_trigger_; std::vector touch_listeners_; + + std::map touches_; + TouchscreenInterrupt store_; + + bool first_touch_{true}; + bool need_update_{false}; + bool is_touched_{false}; + bool skip_update_{false}; }; } // namespace touchscreen diff --git a/esphome/components/tt21100/touchscreen/__init__.py b/esphome/components/tt21100/touchscreen/__init__.py index d96d389e69..4458ad0974 100644 --- a/esphome/components/tt21100/touchscreen/__init__.py +++ b/esphome/components/tt21100/touchscreen/__init__.py @@ -12,7 +12,6 @@ DEPENDENCIES = ["i2c"] TT21100Touchscreen = tt21100_ns.class_( "TT21100Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) TT21100ButtonListener = tt21100_ns.class_("TT21100ButtonListener") @@ -24,17 +23,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, } - ) - .extend(i2c.i2c_device_schema(0x24)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x24)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index 28a8c2d754..ff688fd0b0 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -44,8 +44,6 @@ struct TT21100TouchReport { TT21100TouchRecord touch_record[MAX_TOUCH_POINTS]; } __attribute__((packed)); -void TT21100TouchscreenStore::gpio_intr(TT21100TouchscreenStore *store) { store->touch = true; } - float TT21100Touchscreen::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } void TT21100Touchscreen::setup() { @@ -54,9 +52,8 @@ void TT21100Touchscreen::setup() { // Register interrupt pin this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(TT21100TouchscreenStore::gpio_intr, &this->store_, - gpio::INTERRUPT_FALLING_EDGE); + + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); // Perform reset if necessary if (this->reset_pin_ != nullptr) { @@ -65,19 +62,11 @@ void TT21100Touchscreen::setup() { } // Update display dimensions if they were updated during display setup - this->display_width_ = this->display_->get_width(); - this->display_height_ = this->display_->get_height(); - this->rotation_ = static_cast(this->display_->get_rotation()); - - // Trigger initial read to activate the interrupt - this->store_.touch = true; + this->x_raw_max_ = this->get_width_(); + this->y_raw_max_ = this->get_height_(); } -void TT21100Touchscreen::loop() { - if (!this->store_.touch) - return; - this->store_.touch = false; - +void TT21100Touchscreen::update_touches() { // Read report length uint16_t data_len; this->read((uint8_t *) &data_len, sizeof(data_len)); @@ -111,12 +100,6 @@ void TT21100Touchscreen::loop() { uint8_t touch_count = (data_len - (sizeof(*report) - sizeof(report->touch_record))) / sizeof(TT21100TouchRecord); - if (touch_count == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - for (int i = 0; i < touch_count; i++) { auto *touch = &report->touch_record[i]; @@ -126,30 +109,7 @@ void TT21100Touchscreen::loop() { i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, touch->pressure, touch->major_axis_length, touch->orientation); - TouchPoint tp; - switch (this->rotation_) { - case ROTATE_0_DEGREES: - // Origin is top right, so mirror X by default - tp.x = this->display_width_ - touch->x; - tp.y = touch->y; - break; - case ROTATE_90_DEGREES: - tp.x = touch->y; - tp.y = touch->x; - break; - case ROTATE_180_DEGREES: - tp.x = touch->x; - tp.y = this->display_height_ - touch->y; - break; - case ROTATE_270_DEGREES: - tp.x = this->display_height_ - touch->y; - tp.y = this->display_width_ - touch->x; - break; - } - tp.id = touch->tip; - tp.state = touch->pressure; - - this->defer([this, tp]() { this->send_touch_(tp); }); + this->set_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); } } } diff --git a/esphome/components/tt21100/touchscreen/tt21100.h b/esphome/components/tt21100/touchscreen/tt21100.h index 306360975f..5d1b2efe3c 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.h +++ b/esphome/components/tt21100/touchscreen/tt21100.h @@ -5,27 +5,21 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace tt21100 { using namespace touchscreen; -struct TT21100TouchscreenStore { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(TT21100TouchscreenStore *store); -}; - class TT21100ButtonListener { public: virtual void update_button(uint8_t index, uint16_t state) = 0; }; -class TT21100Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class TT21100Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; float get_setup_priority() const override; @@ -37,7 +31,7 @@ class TT21100Touchscreen : public Touchscreen, public Component, public i2c::I2C protected: void reset_(); - TT21100TouchscreenStore store_; + void update_touches() override; InternalGPIOPin *interrupt_pin_; GPIOPin *reset_pin_{nullptr}; diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py deleted file mode 100644 index 5a6cfe4919..0000000000 --- a/esphome/components/xpt2046/binary_sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("Rename this platform component to Touchscreen.") diff --git a/esphome/components/xpt2046/touchscreen.py b/esphome/components/xpt2046/touchscreen.py deleted file mode 100644 index 150d1cf396..0000000000 --- a/esphome/components/xpt2046/touchscreen.py +++ /dev/null @@ -1,116 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv - -from esphome import pins -from esphome.components import spi, touchscreen -from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_IRQ_PIN, CONF_THRESHOLD - -CODEOWNERS = ["@numo68", "@nielsnl68"] -DEPENDENCIES = ["spi"] - -XPT2046_ns = cg.esphome_ns.namespace("xpt2046") -XPT2046Component = XPT2046_ns.class_( - "XPT2046Component", - touchscreen.Touchscreen, - cg.PollingComponent, - spi.SPIDevice, -) - -CONF_REPORT_INTERVAL = "report_interval" -CONF_CALIBRATION_X_MIN = "calibration_x_min" -CONF_CALIBRATION_X_MAX = "calibration_x_max" -CONF_CALIBRATION_Y_MIN = "calibration_y_min" -CONF_CALIBRATION_Y_MAX = "calibration_y_max" -CONF_SWAP_X_Y = "swap_x_y" - -# obsolete Keys -CONF_DIMENSION_X = "dimension_x" -CONF_DIMENSION_Y = "dimension_y" - - -def validate_xpt2046(config): - if ( - abs( - cv.int_(config[CONF_CALIBRATION_X_MAX]) - - cv.int_(config[CONF_CALIBRATION_X_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration X values difference < 1000") - - if ( - abs( - cv.int_(config[CONF_CALIBRATION_Y_MAX]) - - cv.int_(config[CONF_CALIBRATION_Y_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration Y values difference < 1000") - - return config - - -def report_interval(value): - if value == "never": - return 4294967295 # uint32_t max - return cv.positive_time_period_milliseconds(value) - - -CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(XPT2046Component), - cv.Optional(CONF_INTERRUPT_PIN): cv.All( - pins.internal_gpio_input_pin_schema - ), - cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), - cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval, - cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean, - # obsolete Keys - cv.Optional(CONF_IRQ_PIN): cv.invalid("Rename IRQ_PIN to INTERUPT_PIN"), - cv.Optional(CONF_DIMENSION_X): cv.invalid( - "This key is now obsolete, please remove it" - ), - cv.Optional(CONF_DIMENSION_Y): cv.invalid( - "This key is now obsolete, please remove it" - ), - }, - ) - .extend(cv.polling_component_schema("50ms")) - .extend(spi.spi_device_schema()), -).add_extra(validate_xpt2046) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await spi.register_spi_device(var, config) - await touchscreen.register_touchscreen(var, config) - - cg.add(var.set_threshold(config[CONF_THRESHOLD])) - cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL])) - cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y])) - cg.add( - var.set_calibration( - config[CONF_CALIBRATION_X_MIN], - config[CONF_CALIBRATION_X_MAX], - config[CONF_CALIBRATION_Y_MIN], - config[CONF_CALIBRATION_Y_MAX], - ) - ) - - if CONF_INTERRUPT_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/__init__.py b/esphome/components/xpt2046/touchscreen/__init__.py new file mode 100644 index 0000000000..9f08f38c3f --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/__init__.py @@ -0,0 +1,93 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import spi, touchscreen +from esphome.const import CONF_ID, CONF_THRESHOLD, CONF_INTERRUPT_PIN + +CODEOWNERS = ["@numo68", "@nielsnl68"] +DEPENDENCIES = ["spi"] + +XPT2046_ns = cg.esphome_ns.namespace("xpt2046") +XPT2046Component = XPT2046_ns.class_( + "XPT2046Component", + touchscreen.Touchscreen, + spi.SPIDevice, +) + + +CONF_CALIBRATION_X_MIN = "calibration_x_min" +CONF_CALIBRATION_X_MAX = "calibration_x_max" +CONF_CALIBRATION_Y_MIN = "calibration_y_min" +CONF_CALIBRATION_Y_MAX = "calibration_y_max" + + +def validate_xpt2046(config): + if ( + abs( + cv.int_(config[CONF_CALIBRATION_X_MAX]) + - cv.int_(config[CONF_CALIBRATION_X_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration X values difference < 1000") + + if ( + abs( + cv.int_(config[CONF_CALIBRATION_Y_MAX]) + - cv.int_(config[CONF_CALIBRATION_Y_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration Y values difference < 1000") + + return config + + +CONFIG_SCHEMA = cv.All( + touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XPT2046Component), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), + }, + ) + ).extend(spi.spi_device_schema()), + validate_xpt2046, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await spi.register_spi_device(var, config) + + cg.add(var.set_threshold(config[CONF_THRESHOLD])) + + cg.add( + var.set_calibration( + config[CONF_CALIBRATION_X_MIN], + config[CONF_CALIBRATION_X_MAX], + config[CONF_CALIBRATION_Y_MIN], + config[CONF_CALIBRATION_Y_MAX], + ) + ) + + if CONF_INTERRUPT_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp new file mode 100644 index 0000000000..1a9c202af0 --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -0,0 +1,113 @@ +#include "xpt2046.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace xpt2046 { + +static const char *const TAG = "xpt2046"; + +void XPT2046Component::setup() { + if (this->irq_pin_ != nullptr) { + // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state + // while the channels are read and wiring it as an interrupt is not straightforward and would + // need careful masking. A GPIO poll is cheap so we'll just use that. + + this->irq_pin_->setup(); // INPUT + this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->irq_pin_->setup(); + this->attach_interrupt_(this->irq_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + this->spi_setup(); + this->read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin +} + +void XPT2046Component::update_touches() { + int16_t data[6], x_raw, y_raw, z_raw; + bool touch = false; + + enable(); + + int16_t touch_pressure_1 = this->read_adc_(0xB1 /* touch_pressure_1 */); + int16_t touch_pressure_2 = this->read_adc_(0xC1 /* touch_pressure_2 */); + ESP_LOGVV(TAG, "touch_pressure %d, %d", touch_pressure_1, touch_pressure_2); + z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; + + touch = (z_raw >= this->threshold_); + if (touch) { + read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy + data[0] = this->read_adc_(0x91 /* Y */); + data[1] = this->read_adc_(0xD1 /* X */); // make 3 x-y measurements + data[2] = this->read_adc_(0x91 /* Y */); + data[3] = this->read_adc_(0xD1 /* X */); + data[4] = this->read_adc_(0x91 /* Y */); + } + + data[5] = this->read_adc_(0xD0 /* X */); // Last X touch power down + + disable(); + + if (touch) { + x_raw = best_two_avg(data[1], data[3], data[5]); + y_raw = best_two_avg(data[0], data[2], data[4]); + + ESP_LOGV(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); + + this->set_raw_touch_position_(0, x_raw, y_raw, z_raw); + } +} + +void XPT2046Component::dump_config() { + ESP_LOGCONFIG(TAG, "XPT2046:"); + + LOG_PIN(" IRQ Pin: ", this->irq_pin_); + ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_); + ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); + ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); + ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); + + ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); + ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); + ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); + + ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); + + LOG_UPDATE_INTERVAL(this); +} + +float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } + +int16_t XPT2046Component::best_two_avg(int16_t value1, int16_t value2, int16_t value3) { + int16_t delta_a, delta_b, delta_c; + int16_t reta = 0; + + delta_a = (value1 > value2) ? value1 - value2 : value2 - value1; + delta_b = (value1 > value3) ? value1 - value3 : value3 - value1; + delta_c = (value3 > value2) ? value3 - value2 : value2 - value3; + + if (delta_a <= delta_b && delta_a <= delta_c) { + reta = (value1 + value2) >> 1; + } else if (delta_b <= delta_a && delta_b <= delta_c) { + reta = (value1 + value3) >> 1; + } else { + reta = (value2 + value3) >> 1; + } + + return reta; +} + +int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT + uint8_t data[2]; + + this->write_byte(ctrl); + delay(1); + data[0] = this->read_byte(); + data[1] = this->read_byte(); + + return ((data[0] << 8) | data[1]) >> 3; +} + +} // namespace xpt2046 +} // namespace esphome diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.h b/esphome/components/xpt2046/touchscreen/xpt2046.h new file mode 100644 index 0000000000..ff866bc86b --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/xpt2046.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace xpt2046 { + +using namespace touchscreen; + +class XPT2046Component : public Touchscreen, + public spi::SPIDevice { + public: + /// Set the threshold for the touch detection. + void set_threshold(int16_t threshold) { this->threshold_ = threshold; } + /// Set the pin used to detect the touch. + void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + static int16_t best_two_avg(int16_t value1, int16_t value2, int16_t value3); + + int16_t read_adc_(uint8_t ctrl); + + void update_touches() override; + + int16_t threshold_; + + InternalGPIOPin *irq_pin_{nullptr}; +}; + +} // namespace xpt2046 +} // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp deleted file mode 100644 index 078a1b01e9..0000000000 --- a/esphome/components/xpt2046/xpt2046.cpp +++ /dev/null @@ -1,207 +0,0 @@ -#include "xpt2046.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -#include -#include - -namespace esphome { -namespace xpt2046 { - -static const char *const TAG = "xpt2046"; - -void XPT2046TouchscreenStore::gpio_intr(XPT2046TouchscreenStore *store) { store->touch = true; } - -void XPT2046Component::setup() { - if (this->irq_pin_ != nullptr) { - // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state - // while the channels are read and wiring it as an interrupt is not straightforward and would - // need careful masking. A GPIO poll is cheap so we'll just use that. - - this->irq_pin_->setup(); // INPUT - this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - this->irq_pin_->setup(); - this->irq_pin_->attach_interrupt(XPT2046TouchscreenStore::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); - } - spi_setup(); - read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin -} - -void XPT2046Component::loop() { - if ((this->irq_pin_ != nullptr) && (this->store_.touch || this->touched)) { - this->store_.touch = false; - check_touch_(); - } -} - -void XPT2046Component::update() { - if (this->irq_pin_ == nullptr) - check_touch_(); -} - -void XPT2046Component::check_touch_() { - int16_t data[6]; - bool touch = false; - uint32_t now = millis(); - - enable(); - - int16_t touch_pressure_1 = read_adc_(0xB1 /* touch_pressure_1 */); - int16_t touch_pressure_2 = read_adc_(0xC1 /* touch_pressure_2 */); - - this->z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; - - touch = (this->z_raw >= this->threshold_); - if (touch) { - read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy - data[0] = read_adc_(0x91 /* Y */); - data[1] = read_adc_(0xD1 /* X */); // make 3 x-y measurements - data[2] = read_adc_(0x91 /* Y */); - data[3] = read_adc_(0xD1 /* X */); - data[4] = read_adc_(0x91 /* Y */); - } - - data[5] = read_adc_(0xD0 /* X */); // Last X touch power down - - disable(); - - if (touch) { - this->x_raw = best_two_avg(data[1], data[3], data[5]); - this->y_raw = best_two_avg(data[0], data[2], data[4]); - - ESP_LOGVV(TAG, "Update [x, y] = [%d, %d], z = %d", this->x_raw, this->y_raw, this->z_raw); - - TouchPoint touchpoint; - - touchpoint.x = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); - touchpoint.y = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); - - if (this->swap_x_y_) { - std::swap(touchpoint.x, touchpoint.y); - } - - if (this->invert_x_) { - touchpoint.x = 0xfff - touchpoint.x; - } - - if (this->invert_y_) { - touchpoint.y = 0xfff - touchpoint.y; - } - - switch (static_cast(this->display_->get_rotation())) { - case ROTATE_0_DEGREES: - break; - case ROTATE_90_DEGREES: - std::swap(touchpoint.x, touchpoint.y); - touchpoint.y = 0xfff - touchpoint.y; - break; - case ROTATE_180_DEGREES: - touchpoint.x = 0xfff - touchpoint.x; - touchpoint.y = 0xfff - touchpoint.y; - break; - case ROTATE_270_DEGREES: - std::swap(touchpoint.x, touchpoint.y); - touchpoint.x = 0xfff - touchpoint.x; - break; - } - - touchpoint.x = (int16_t) ((int) touchpoint.x * this->display_->get_width() / 0xfff); - touchpoint.y = (int16_t) ((int) touchpoint.y * this->display_->get_height() / 0xfff); - - if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) { - ESP_LOGV(TAG, "Touching at [%03X, %03X] => [%3d, %3d]", this->x_raw, this->y_raw, touchpoint.x, touchpoint.y); - - this->defer([this, touchpoint]() { this->send_touch_(touchpoint); }); - - this->x = touchpoint.x; - this->y = touchpoint.y; - this->touched = true; - this->last_pos_ms_ = now; - } - } - - if (!touch && this->touched) { - this->x_raw = this->y_raw = this->z_raw = 0; - ESP_LOGV(TAG, "Released [%d, %d]", this->x, this->y); - this->touched = false; - for (auto *listener : this->touch_listeners_) - listener->release(); - } -} - -void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { // NOLINT - this->x_raw_min_ = std::min(x_min, x_max); - this->x_raw_max_ = std::max(x_min, x_max); - this->y_raw_min_ = std::min(y_min, y_max); - this->y_raw_max_ = std::max(y_min, y_max); - this->invert_x_ = (x_min > x_max); - this->invert_y_ = (y_min > y_max); -} - -void XPT2046Component::dump_config() { - ESP_LOGCONFIG(TAG, "XPT2046:"); - - LOG_PIN(" IRQ Pin: ", this->irq_pin_); - ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_); - ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); - ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); - ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); - - ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); - ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); - ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); - - ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); - ESP_LOGCONFIG(TAG, " Report interval: %" PRIu32, this->report_millis_); - - LOG_UPDATE_INTERVAL(this); -} - -float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } - -int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) { // NOLINT - int16_t da, db, dc; // NOLINT - int16_t reta = 0; - - da = (x > y) ? x - y : y - x; - db = (x > z) ? x - z : z - x; - dc = (z > y) ? z - y : y - z; - - if (da <= db && da <= dc) { - reta = (x + y) >> 1; - } else if (db <= da && db <= dc) { - reta = (x + z) >> 1; - } else { - reta = (y + z) >> 1; - } - - return reta; -} - -int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_val) { - int16_t ret; - - if (val <= min_val) { - ret = 0; - } else if (val >= max_val) { - ret = 0xfff; - } else { - ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); - } - - return ret; -} - -int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT - uint8_t data[2]; - - write_byte(ctrl); - delay(1); - data[0] = read_byte(); - data[1] = read_byte(); - - return ((data[0] << 8) | data[1]) >> 3; -} - -} // namespace xpt2046 -} // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.h b/esphome/components/xpt2046/xpt2046.h deleted file mode 100644 index e7d9caba21..0000000000 --- a/esphome/components/xpt2046/xpt2046.h +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/spi/spi.h" -#include "esphome/components/touchscreen/touchscreen.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace xpt2046 { - -using namespace touchscreen; - -struct XPT2046TouchscreenStore { - volatile bool touch; - static void gpio_intr(XPT2046TouchscreenStore *store); -}; - -class XPT2046Component : public Touchscreen, - public PollingComponent, - public spi::SPIDevice { - public: - /// Set the logical touch screen dimensions. - void set_dimensions(int16_t x, int16_t y) { - this->display_width_ = x; - this->display_height_ = y; - } - /// Set the coordinates for the touch screen edges. - void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max); - /// If true the x and y axes will be swapped - void set_swap_x_y(bool val) { this->swap_x_y_ = val; } - - /// Set the interval to report the touch point perodically. - void set_report_interval(uint32_t interval) { this->report_millis_ = interval; } - uint32_t get_report_interval() { return this->report_millis_; } - - /// Set the threshold for the touch detection. - void set_threshold(int16_t threshold) { this->threshold_ = threshold; } - /// Set the pin used to detect the touch. - void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } - - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - - /** Detect the touch if the irq pin is specified. - * - * If the touch is detected and the component does not already know about it - * the update() is called immediately. If the irq pin is not specified - * the loop() is a no-op. - */ - void loop() override; - - /** Read and process the values from the hardware. - * - * Read the raw x, y and touch pressure values from the chip, detect the touch, - * and if touched, transform to the user x and y coordinates. If the state has - * changed or if the value should be reported again due to the - * report interval, run the action and inform the virtual buttons. - */ - void update() override; - - /**@{*/ - /** Coordinates of the touch position. - * - * The values are set immediately before the on_state action with touched == true - * is triggered. The action with touched == false sends the coordinates of the last - * reported touch. - */ - int16_t x{0}, y{0}; - /**@}*/ - - /// True if the component currently detects the touch - bool touched{false}; - - /**@{*/ - /** Raw sensor values of the coordinates and the pressure. - * - * The values are set each time the update() method is called. - */ - int16_t x_raw{0}, y_raw{0}, z_raw{0}; - /**@}*/ - - protected: - static int16_t best_two_avg(int16_t x, int16_t y, int16_t z); - static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val); - - int16_t read_adc_(uint8_t ctrl); - void check_touch_(); - - int16_t threshold_; - int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_; - - bool invert_x_, invert_y_; - bool swap_x_y_; - - uint32_t report_millis_; - uint32_t last_pos_ms_{0}; - - InternalGPIOPin *irq_pin_{nullptr}; - XPT2046TouchscreenStore store_; -}; - -} // namespace xpt2046 -} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 9f15b84b12..fc719abecb 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -464,6 +464,7 @@ binary_sensor: sx1509: sx1509_hub number: 3 + - platform: touchscreen touchscreen_id: lilygo_touchscreen id: touch_key1 @@ -483,6 +484,7 @@ binary_sensor: pin: max6956: max6956_1 number: 4 + mode: input: true pullup: true @@ -506,6 +508,7 @@ binary_sensor: input: true inverted: false + climate: - platform: tuya id: tuya_climate @@ -595,6 +598,8 @@ display: it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + auto touch = id(ft63_touchscreen)->get_touch(); + if (touch) { ESP_LOGD("touch", "%d/%d", touch.value().x, touch.value().y); } rotation: 0° update_interval: 16ms @@ -677,53 +682,53 @@ display: update_interval: 60s display_data_1_pin: - number: 5 + number: GPIO5 allow_other_uses: true display_data_2_pin: - number: 18 + number: GPIO18 allow_other_uses: true display_data_3_pin: - number: 19 + number: GPIO19 allow_other_uses: true display_data_5_pin: - number: 25 + number: GPIO25 allow_other_uses: true display_data_4_pin: - number: 23 + number: GPIO23 allow_other_uses: true display_data_6_pin: - number: 26 + number: GPIO26 allow_other_uses: true display_data_7_pin: - number: 27 + number: GPIO27 allow_other_uses: true ckv_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true sph_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true gmod_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true gpio0_enable_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true oe_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true spv_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true powerup_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true wakeup_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true vcom_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true number: - platform: tuya @@ -816,21 +821,16 @@ esp32_camera: allow_other_uses: true - number: GPIO35 allow_other_uses: true - - - number: GPIO34 - - - number: GPIO5 + - number: GPIO34 + - number: GPIO5 allow_other_uses: true - - - number: GPIO39 - - - number: GPIO18 + - number: GPIO39 allow_other_uses: true - - - number: GPIO36 + - number: GPIO18 allow_other_uses: true - - - number: GPIO19 + - number: GPIO36 + allow_other_uses: true + - number: GPIO19 allow_other_uses: true vsync_pin: allow_other_uses: true @@ -910,18 +910,16 @@ touchscreen: spi_id: spi_id_2 cs_pin: allow_other_uses: true - number: 17 + number: GPIO17 interrupt_pin: - number: 16 + number: GPIO16 display: inkplate_display update_interval: 50ms - report_interval: 1s threshold: 400 calibration_x_min: 3860 calibration_x_max: 280 calibration_y_min: 340 calibration_y_max: 3860 - swap_x_y: false on_touch: - logger.log: format: Touch at (%d, %d) @@ -938,10 +936,25 @@ touchscreen: format: Touch at (%d, %d) args: [touch.x, touch.y] - platform: gt911 - interrupt_pin: GPIO3 + interrupt_pin: + number: GPIO3 display: inkplate_display + - platform: ft63x6 + id: ft63_touchscreen + interrupt_pin: + allow_other_uses: true + number: GPIO39 + reset_pin: + allow_other_uses: true + number: GPIO5 + display: inkplate_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] + i2s_audio: i2s_lrclk_pin: allow_other_uses: true diff --git a/tests/test8.yaml b/tests/test8.yaml index 5e4a41080a..558e86e1f9 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -54,6 +54,7 @@ spi_device: display: - platform: ili9xxx + id: displ8 model: ili9342 cs_pin: GPIO5 dc_pin: GPIO4 @@ -67,6 +68,7 @@ i2c: touchscreen: - platform: tt21100 + display: displ8 interrupt_pin: number: GPIO3 ignore_strapping_warning: true From 259a6d52e1de45ce15aa5229c672e924419da84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Br=C3=A6mer?= <55437145+TH-Braemer@users.noreply.github.com> Date: Wed, 13 Dec 2023 00:26:08 +0100 Subject: [PATCH 15/35] A02yyuw (#5729) --- CODEOWNERS | 1 + esphome/components/a02yyuw/__init__.py | 1 + esphome/components/a02yyuw/a02yyuw.cpp | 43 ++++++++++++++++++++++++++ esphome/components/a02yyuw/a02yyuw.h | 27 ++++++++++++++++ esphome/components/a02yyuw/sensor.py | 41 ++++++++++++++++++++++++ tests/test4.yaml | 13 ++++++++ 6 files changed, 126 insertions(+) create mode 100644 esphome/components/a02yyuw/__init__.py create mode 100644 esphome/components/a02yyuw/a02yyuw.cpp create mode 100644 esphome/components/a02yyuw/a02yyuw.h create mode 100644 esphome/components/a02yyuw/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c37eb3581b..d509c98433 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -12,6 +12,7 @@ esphome/core/* @esphome/core # Integrations esphome/components/a01nyub/* @MrSuicideParrot +esphome/components/a02yyuw/* @TH-Braemer esphome/components/absolute_humidity/* @DAVe3283 esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core diff --git a/esphome/components/a02yyuw/__init__.py b/esphome/components/a02yyuw/__init__.py new file mode 100644 index 0000000000..6724dbb970 --- /dev/null +++ b/esphome/components/a02yyuw/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@TH-Braemer"] diff --git a/esphome/components/a02yyuw/a02yyuw.cpp b/esphome/components/a02yyuw/a02yyuw.cpp new file mode 100644 index 0000000000..ee378c3283 --- /dev/null +++ b/esphome/components/a02yyuw/a02yyuw.cpp @@ -0,0 +1,43 @@ +// Datasheet https://wiki.dfrobot.com/_A02YYUW_Waterproof_Ultrasonic_Sensor_SKU_SEN0311 + +#include "a02yyuw.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace a02yyuw { + +static const char *const TAG = "a02yyuw.sensor"; + +void A02yyuwComponent::loop() { + uint8_t data; + while (this->available() > 0) { + this->read_byte(&data); + if (this->buffer_.empty() && (data != 0xff)) + continue; + buffer_.push_back(data); + if (this->buffer_.size() == 4) + this->check_buffer_(); + } +} + +void A02yyuwComponent::check_buffer_() { + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; + if (this->buffer_[3] == checksum) { + float distance = (this->buffer_[1] << 8) + this->buffer_[2]; + if (distance > 30) { + ESP_LOGV(TAG, "Distance from sensor: %f mm", distance); + this->publish_state(distance); + } else { + ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + } + } else { + ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); + } + this->buffer_.clear(); +} + +void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); } + +} // namespace a02yyuw +} // namespace esphome diff --git a/esphome/components/a02yyuw/a02yyuw.h b/esphome/components/a02yyuw/a02yyuw.h new file mode 100644 index 0000000000..6ff370fdc3 --- /dev/null +++ b/esphome/components/a02yyuw/a02yyuw.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace a02yyuw { + +class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void loop() override; + void dump_config() override; + + protected: + void check_buffer_(); + + std::vector buffer_; +}; + +} // namespace a02yyuw +} // namespace esphome diff --git a/esphome/components/a02yyuw/sensor.py b/esphome/components/a02yyuw/sensor.py new file mode 100644 index 0000000000..5232b04546 --- /dev/null +++ b/esphome/components/a02yyuw/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + ICON_ARROW_EXPAND_VERTICAL, + DEVICE_CLASS_DISTANCE, +) + +CODEOWNERS = ["@TH-Braemer"] +DEPENDENCIES = ["uart"] +UNIT_MILLIMETERS = "mm" + +a02yyuw_ns = cg.esphome_ns.namespace("a02yyuw") +A02yyuwComponent = a02yyuw_ns.class_( + "A02yyuwComponent", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = sensor.sensor_schema( + A02yyuwComponent, + unit_of_measurement=UNIT_MILLIMETERS, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_DISTANCE, +).extend(uart.UART_DEVICE_SCHEMA) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "a02yyuw", + baud_rate=9600, + require_tx=False, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/tests/test4.yaml b/tests/test4.yaml index fc719abecb..089caf073b 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -84,6 +84,14 @@ uart: allow_other_uses: true number: GPIO26 baud_rate: 9600 + - id: uart_a02yyuw + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 + baud_rate: 9600 - id: uart_he60r tx_pin: number: GPIO18 @@ -357,6 +365,11 @@ sensor: name: "a01nyub Distance" uart_id: uart9600 state_topic: "esphome/sensor/a01nyub_sensor/state" + - platform: a02yyuw + id: a02yyuw_sensor + name: "a02yyuw Distance" + uart_id: uart_a02yyuw + state_topic: "esphome/sensor/a02yyuw_sensor/state" # # platform sensor.apds9960 requires component apds9960 From 03baaa94a8d506212ff3fdf9ada258887e717cc7 Mon Sep 17 00:00:00 2001 From: Theo Hussey Date: Tue, 12 Dec 2023 23:28:59 +0000 Subject: [PATCH 16/35] Fix AHT10 / AHT20 communication (#5198) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/aht10/aht10.cpp | 55 ++++++++++++++++++------------ esphome/components/aht10/aht10.h | 6 ++++ esphome/components/aht10/sensor.py | 11 ++++++ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 1ca06b458a..4d69a67487 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -21,36 +21,49 @@ namespace esphome { namespace aht10 { static const char *const TAG = "aht10"; -static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; +static const size_t SIZE_CALIBRATE_CMD = 3; +static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1, 0x08, 0x00}; +static const uint8_t AHT20_CALIBRATE_CMD[] = {0xBE, 0x08, 0x00}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms +static const uint8_t AHT10_CAL_ATTEMPTS = 10; +static const uint8_t AHT10_STATUS_BUSY = 0x80; void AHT10Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up AHT10..."); + const uint8_t *calibrate_cmd; + switch (this->variant_) { + case AHT10Variant::AHT20: + calibrate_cmd = AHT20_CALIBRATE_CMD; + ESP_LOGCONFIG(TAG, "Setting up AHT20"); + break; + case AHT10Variant::AHT10: + default: + calibrate_cmd = AHT10_CALIBRATE_CMD; + ESP_LOGCONFIG(TAG, "Setting up AHT10"); + } - if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) { + if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->mark_failed(); return; } - uint8_t data = 0; - if (this->write(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; - } - delay(AHT10_DEFAULT_DELAY); - if (this->read(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; - } - if (this->read(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; + uint8_t data = AHT10_STATUS_BUSY; + int cal_attempts = 0; + while (data & AHT10_STATUS_BUSY) { + delay(AHT10_DEFAULT_DELAY); + if (this->read(&data, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->mark_failed(); + return; + } + ++cal_attempts; + if (cal_attempts > AHT10_CAL_ATTEMPTS) { + ESP_LOGE(TAG, "AHT10 calibration timed out!"); + this->mark_failed(); + return; + } } if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED ESP_LOGE(TAG, "AHT10 calibration failed!"); @@ -62,7 +75,7 @@ void AHT10Component::setup() { } void AHT10Component::update() { - if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->status_set_warning(); return; @@ -89,7 +102,7 @@ void AHT10Component::update() { break; } else { ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); - if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->status_set_warning(); return; diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index 4d0eaa5919..3840609d56 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" @@ -7,12 +9,15 @@ namespace esphome { namespace aht10 { +enum AHT10Variant { AHT10, AHT20 }; + class AHT10Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void update() override; void dump_config() override; float get_setup_priority() const override; + void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -20,6 +25,7 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { protected: sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; + AHT10Variant variant_{}; }; } // namespace aht10 diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py index a52773b6d7..31b07c0e73 100644 --- a/esphome/components/aht10/sensor.py +++ b/esphome/components/aht10/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, + CONF_VARIANT, ) DEPENDENCIES = ["i2c"] @@ -17,6 +18,12 @@ DEPENDENCIES = ["i2c"] aht10_ns = cg.esphome_ns.namespace("aht10") AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice) +AHT10Variant = aht10_ns.enum("AHT10Variant") +AHT10_VARIANTS = { + "AHT10": AHT10Variant.AHT10, + "AHT20": AHT10Variant.AHT20, +} + CONFIG_SCHEMA = ( cv.Schema( { @@ -33,6 +40,9 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum( + AHT10_VARIANTS, upper=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -44,6 +54,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + cg.add(var.set_variant(config[CONF_VARIANT])) if temperature := config.get(CONF_TEMPERATURE): sens = await sensor.new_sensor(temperature) From 29002c8f45f69a9caf0887d3de07b9db67149f0d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:38:37 +1100 Subject: [PATCH 17/35] Fix crash when handling pin_check error (#5915) --- esphome/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/config.py b/esphome/config.py index 3461223490..745883c2ef 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -111,7 +111,12 @@ class Config(OrderedDict, fv.FinalValidateConfig): last_root = max( i for i, v in enumerate(error.path) if v is cv.ROOT_CONFIG_PATH ) - error.path = error.path[last_root + 1 :] + # can't change the path so re-create the error + error = vol.Invalid( + message=error.error_message, + path=error.path[last_root + 1 :], + error_type=error.error_type, + ) self.errors.append(error) def add_validation_step(self, step: "ConfigValidationStep"): From 8789925fe89d6f9e98083111335530ae64a3d9ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 13:48:14 -1000 Subject: [PATCH 18/35] Bump aioesphomeapi to 20.1.0 (#5914) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae8d50f2ea..0d08da2b45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==20.0.0 +aioesphomeapi==20.1.0 zeroconf==0.128.4 python-magic==0.4.27 From 69026f759958b6203fd021e255d42ba7664e2730 Mon Sep 17 00:00:00 2001 From: Mathias Pihl Date: Wed, 13 Dec 2023 00:50:55 +0100 Subject: [PATCH 19/35] Remove setpoint-change from error when calculating derivative in pid controller (#4737) Co-authored-by: Mathias Pihl --- esphome/components/pid/pid_controller.cpp | 11 ++++++++--- esphome/components/pid/pid_controller.h | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index 30f6038325..1a16f14542 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -16,7 +16,7 @@ float PIDController::update(float setpoint, float process_value) { calculate_proportional_term_(); calculate_integral_term_(); - calculate_derivative_term_(); + calculate_derivative_term_(setpoint); // u(t) := p(t) + i(t) + d(t) float output = proportional_term_ + integral_term_ + derivative_term_; @@ -69,13 +69,18 @@ void PIDController::calculate_integral_term_() { integral_term_ = accumulated_integral_; } -void PIDController::calculate_derivative_term_() { +void PIDController::calculate_derivative_term_(float setpoint) { // derivative_term_ // d(t) := K_d * de(t)/dt float derivative = 0.0f; - if (dt_ != 0.0f) + if (dt_ != 0.0f) { + // remove changes to setpoint from error + if (!std::isnan(previous_setpoint_) && previous_setpoint_ != setpoint) + previous_error_ -= previous_setpoint_ - setpoint; derivative = (error_ - previous_error_) / dt_; + } previous_error_ = error_; + previous_setpoint_ = setpoint; // smooth the derivative samples derivative = weighted_average_(derivative_list_, derivative, derivative_samples_); diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 05ce5f9224..e2a7030b57 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -49,12 +49,13 @@ struct PIDController { void calculate_proportional_term_(); void calculate_integral_term_(); - void calculate_derivative_term_(); + void calculate_derivative_term_(float setpoint); float weighted_average_(std::deque &list, float new_value, int samples); float calculate_relative_time_(); /// Error from previous update used for derivative term float previous_error_ = 0; + float previous_setpoint_ = NAN; /// Accumulated integral value float accumulated_integral_ = 0; uint32_t last_time_ = 0; From d0bcba3b3fef78a394c6937ba20d2ca88bc30150 Mon Sep 17 00:00:00 2001 From: Nicolas Gilles Date: Wed, 13 Dec 2023 00:55:20 +0100 Subject: [PATCH 20/35] ir_transmitter: add support for repeated commands in NEC protocol (#4995) Co-authored-by: Nicolas Gilles --- esphome/components/remote_base/__init__.py | 5 +++ .../components/remote_base/nec_protocol.cpp | 42 +++++++++++++++---- esphome/components/remote_base/nec_protocol.h | 3 ++ esphome/const.py | 1 + 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index a2411b1b12..3accd5038c 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import binary_sensor from esphome.const import ( + CONF_COMMAND_REPEATS, CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, @@ -638,6 +639,7 @@ NEC_SCHEMA = cv.Schema( { cv.Required(CONF_ADDRESS): cv.hex_uint16_t, cv.Required(CONF_COMMAND): cv.hex_uint16_t, + cv.Optional(CONF_COMMAND_REPEATS, default=1): cv.uint16_t, } ) @@ -650,6 +652,7 @@ def nec_binary_sensor(var, config): NECData, ("address", config[CONF_ADDRESS]), ("command", config[CONF_COMMAND]), + ("command_repeats", config[CONF_COMMAND_REPEATS]), ) ) ) @@ -671,6 +674,8 @@ async def nec_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint16) cg.add(var.set_command(template_)) + template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint16) + cg.add(var.set_command_repeats(template_)) # Pioneer diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index d5c68784ee..6ea9a8583c 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -13,10 +13,14 @@ static const uint32_t BIT_ONE_LOW_US = 1690; static const uint32_t BIT_ZERO_LOW_US = 560; void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { - dst->reserve(68); + ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, + data.command_repeats); + + dst->reserve(2 + 32 + 32 * data.command_repeats + 2); dst->set_carrier_frequency(38000); dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint16_t mask = 1; mask; mask <<= 1) { if (data.address & mask) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); @@ -25,11 +29,13 @@ void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { } } - for (uint16_t mask = 1; mask; mask <<= 1) { - if (data.command & mask) { - dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); - } else { - dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + for (uint16_t repeats = 0; repeats < data.command_repeats; repeats++) { + for (uint16_t mask = 1; mask; mask <<= 1) { + if (data.command & mask) { + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } } } @@ -39,6 +45,7 @@ optional NECProtocol::decode(RemoteReceiveData src) { NECData data{ .address = 0, .command = 0, + .command_repeats = 1, }; if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) return {}; @@ -63,11 +70,32 @@ optional NECProtocol::decode(RemoteReceiveData src) { } } + while (src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US) || src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + uint16_t command = 0; + for (uint16_t mask = 1; mask; mask <<= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + command |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + command &= ~mask; + } else { + return {}; + } + } + + // Make sure the extra/repeated data matches original command + if (command != data.command) { + return {}; + } + + data.command_repeats += 1; + } + src.expect_mark(BIT_HIGH_US); return data; } void NECProtocol::dump(const NECData &data) { - ESP_LOGI(TAG, "Received NEC: address=0x%04X, command=0x%04X", data.address, data.command); + ESP_LOGI(TAG, "Received NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, + data.command_repeats); } } // namespace remote_base diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 593a3efe17..71e1bccba8 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -8,6 +8,7 @@ namespace remote_base { struct NECData { uint16_t address; uint16_t command; + uint16_t command_repeats; bool operator==(const NECData &rhs) const { return address == rhs.address && command == rhs.command; } }; @@ -25,11 +26,13 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); data.command = this->command_.value(x...); + data.command_repeats = this->command_repeats_.value(x...); NECProtocol().encode(dst, data); } }; diff --git a/esphome/const.py b/esphome/const.py index f688cd75f9..1aabe81db3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -131,6 +131,7 @@ CONF_COLOR_PALETTE = "color_palette" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" +CONF_COMMAND_REPEATS = "command_repeats" CONF_COMMAND_RETAIN = "command_retain" CONF_COMMAND_TOPIC = "command_topic" CONF_COMMENT = "comment" From a72725f4b40266646aa46886d042428d76504ae3 Mon Sep 17 00:00:00 2001 From: Cossid <83468485+Cossid@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:04:17 -0600 Subject: [PATCH 21/35] BP1658CJ - Fix timing for all platforms, now consistent with other drivers (#5799) --- esphome/components/bp1658cj/bp1658cj.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index 05c3f790c2..4b74cc85f5 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -90,40 +90,41 @@ void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) { void BP1658CJ::write_bit_(bool value) { this->data_pin_->digital_write(value); - this->clock_pin_->digital_write(true); - delayMicroseconds(BP1658CJ_DELAY); - + this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); } void BP1658CJ::write_byte_(uint8_t data) { for (uint8_t mask = 0x80; mask; mask >>= 1) { this->write_bit_(data & mask); - delayMicroseconds(BP1658CJ_DELAY); } // ack bit this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->clock_pin_->digital_write(true); - delayMicroseconds(BP1658CJ_DELAY); - this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); } void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { this->data_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); for (uint32_t i = 0; i < size; i++) { this->write_byte_(buffer[i]); - delayMicroseconds(BP1658CJ_DELAY); } this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); this->data_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); } } // namespace bp1658cj From 6c7a133faa722f0a7d9c808094aba0ef1321bd70 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Wed, 13 Dec 2023 02:23:02 +0100 Subject: [PATCH 22/35] Add humidity support to climate (#5732) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api.proto | 8 ++ esphome/components/api/api_connection.cpp | 10 +++ esphome/components/api/api_pb2.cpp | 79 ++++++++++++++++++- esphome/components/api/api_pb2.h | 8 ++ .../bang_bang/bang_bang_climate.cpp | 13 +++ .../components/bang_bang/bang_bang_climate.h | 4 + esphome/components/bang_bang/climate.py | 6 ++ esphome/components/climate/__init__.py | 43 ++++++++++ esphome/components/climate/automation.h | 2 + esphome/components/climate/climate.cpp | 54 ++++++++++++- esphome/components/climate/climate.h | 19 +++++ esphome/components/climate/climate_traits.h | 17 ++++ esphome/components/mqtt/mqtt_climate.cpp | 42 +++++++++- esphome/components/mqtt/mqtt_climate.h | 3 + esphome/components/mqtt/mqtt_const.h | 4 + esphome/components/pid/climate.py | 7 +- esphome/components/pid/pid_climate.cpp | 13 +++ esphome/components/pid/pid_climate.h | 3 + esphome/components/thermostat/climate.py | 6 ++ .../thermostat/thermostat_climate.cpp | 15 ++++ .../thermostat/thermostat_climate.h | 3 + esphome/const.py | 4 + tests/test3.yaml | 3 + 23 files changed, 359 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 34d137981e..04db649aef 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -859,6 +859,10 @@ message ListEntitiesClimateResponse { string icon = 19; EntityCategory entity_category = 20; float visual_current_temperature_step = 21; + bool supports_current_humidity = 22; + bool supports_target_humidity = 23; + float visual_min_humidity = 24; + float visual_max_humidity = 25; } message ClimateStateResponse { option (id) = 47; @@ -879,6 +883,8 @@ message ClimateStateResponse { string custom_fan_mode = 11; ClimatePreset preset = 12; string custom_preset = 13; + float current_humidity = 14; + float target_humidity = 15; } message ClimateCommandRequest { option (id) = 48; @@ -907,6 +913,8 @@ message ClimateCommandRequest { ClimatePreset preset = 19; bool has_custom_preset = 20; string custom_preset = 21; + bool has_target_humidity = 22; + float target_humidity = 23; } // ==================== NUMBER ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b8f5b05538..d5ab00a822 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -560,6 +560,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { resp.custom_preset = climate->custom_preset.value(); if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); + if (traits.get_supports_current_humidity()) + resp.current_humidity = climate->current_humidity; + if (traits.get_supports_target_humidity()) + resp.target_humidity = climate->target_humidity; return this->send_climate_state_response(resp); } bool APIConnection::send_climate_info(climate::Climate *climate) { @@ -576,7 +580,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.entity_category = static_cast(climate->get_entity_category()); msg.supports_current_temperature = traits.get_supports_current_temperature(); + msg.supports_current_humidity = traits.get_supports_current_humidity(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); + msg.supports_target_humidity = traits.get_supports_target_humidity(); for (auto mode : traits.get_supported_modes()) msg.supported_modes.push_back(static_cast(mode)); @@ -585,6 +591,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); + msg.visual_min_humidity = traits.get_visual_min_humidity(); + msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); @@ -615,6 +623,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { call.set_target_temperature_low(msg.target_temperature_low); if (msg.has_target_temperature_high) call.set_target_temperature_high(msg.target_temperature_high); + if (msg.has_target_humidity) + call.set_target_humidity(msg.target_humidity); if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 01fb540e7e..8dd34e7ef1 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3611,6 +3611,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->entity_category = value.as_enum(); return true; } + case 22: { + this->supports_current_humidity = value.as_bool(); + return true; + } + case 23: { + this->supports_target_humidity = value.as_bool(); + return true; + } default: return false; } @@ -3667,6 +3675,14 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val this->visual_current_temperature_step = value.as_float(); return true; } + case 24: { + this->visual_min_humidity = value.as_float(); + return true; + } + case 25: { + this->visual_max_humidity = value.as_float(); + return true; + } default: return false; } @@ -3705,6 +3721,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(19, this->icon); buffer.encode_enum(20, this->entity_category); buffer.encode_float(21, this->visual_current_temperature_step); + buffer.encode_bool(22, this->supports_current_humidity); + buffer.encode_bool(23, this->supports_target_humidity); + buffer.encode_float(24, this->visual_min_humidity); + buffer.encode_float(25, this->visual_max_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -3810,7 +3830,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->visual_current_temperature_step); out.append(buffer); out.append("\n"); - out.append("}"); + + out.append(" supports_current_humidity: "); + out.append(YESNO(this->supports_current_humidity)); + out.append("\n"); + + out.append(" supports_target_humidity: "); + out.append(YESNO(this->supports_target_humidity)); + out.append("\n"); + + out.append(" visual_min_humidity: "); + sprintf(buffer, "%g", this->visual_min_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" visual_max_humidity: "); + sprintf(buffer, "%g", this->visual_max_humidity); + out.append(buffer); + out.append("\n"); } #endif bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -3879,6 +3916,14 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->target_temperature_high = value.as_float(); return true; } + case 14: { + this->current_humidity = value.as_float(); + return true; + } + case 15: { + this->target_humidity = value.as_float(); + return true; + } default: return false; } @@ -3897,6 +3942,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, this->custom_fan_mode); buffer.encode_enum(12, this->preset); buffer.encode_string(13, this->custom_preset); + buffer.encode_float(14, this->current_humidity); + buffer.encode_float(15, this->target_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateStateResponse::dump_to(std::string &out) const { @@ -3958,7 +4005,16 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" custom_preset: "); out.append("'").append(this->custom_preset).append("'"); out.append("\n"); - out.append("}"); + + out.append(" current_humidity: "); + sprintf(buffer, "%g", this->current_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" target_humidity: "); + sprintf(buffer, "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); } #endif bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -4023,6 +4079,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->has_custom_preset = value.as_bool(); return true; } + case 22: { + this->has_target_humidity = value.as_bool(); + return true; + } default: return false; } @@ -4059,6 +4119,10 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->target_temperature_high = value.as_float(); return true; } + case 23: { + this->target_humidity = value.as_float(); + return true; + } default: return false; } @@ -4085,6 +4149,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(19, this->preset); buffer.encode_bool(20, this->has_custom_preset); buffer.encode_string(21, this->custom_preset); + buffer.encode_bool(22, this->has_target_humidity); + buffer.encode_float(23, this->target_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateCommandRequest::dump_to(std::string &out) const { @@ -4177,6 +4243,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(" custom_preset: "); out.append("'").append(this->custom_preset).append("'"); out.append("\n"); + + out.append(" has_target_humidity: "); + out.append(YESNO(this->has_target_humidity)); + out.append("\n"); + + out.append(" target_humidity: "); + sprintf(buffer, "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index fc57348863..02fc7b88f8 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -985,6 +985,10 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::string icon{}; enums::EntityCategory entity_category{}; float visual_current_temperature_step{0.0f}; + bool supports_current_humidity{false}; + bool supports_target_humidity{false}; + float visual_min_humidity{0.0f}; + float visual_max_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1010,6 +1014,8 @@ class ClimateStateResponse : public ProtoMessage { std::string custom_fan_mode{}; enums::ClimatePreset preset{}; std::string custom_preset{}; + float current_humidity{0.0f}; + float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1043,6 +1049,8 @@ class ClimateCommandRequest : public ProtoMessage { enums::ClimatePreset preset{}; bool has_custom_preset{false}; std::string custom_preset{}; + bool has_target_humidity{false}; + float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 20cb87025a..13b0cd2a09 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -15,6 +15,16 @@ void BangBangClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; + + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { @@ -47,6 +57,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, }); @@ -171,6 +183,7 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa BangBangClimate::BangBangClimate() : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 84bcd51f34..96368af34c 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -24,6 +24,7 @@ class BangBangClimate : public climate::Climate, public Component { void dump_config() override; void set_sensor(sensor::Sensor *sensor); + void set_humidity_sensor(sensor::Sensor *humidity_sensor); Trigger<> *get_idle_trigger() const; Trigger<> *get_cool_trigger() const; void set_supports_cool(bool supports_cool); @@ -48,6 +49,9 @@ class BangBangClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; + /** The trigger to call when the controller should switch to idle mode. * * In idle mode, the controller is assumed to have both heating and cooling disabled. diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index ac0c328000..9dde0ae1ac 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, + CONF_HUMIDITY_SENSOR, CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR, @@ -22,6 +23,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), @@ -47,6 +49,10 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + normal_config = BangBangClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 85242eb344..c9c3900a0c 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_AWAY, CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, + CONF_CURRENT_HUMIDITY_STATE_TOPIC, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -28,6 +29,8 @@ from esphome.const import ( CONF_SWING_MODE, CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_STATE_TOPIC, + CONF_TARGET_HUMIDITY_COMMAND_TOPIC, + CONF_TARGET_HUMIDITY_STATE_TOPIC, CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_STATE_TOPIC, @@ -106,6 +109,9 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) CONF_CURRENT_TEMPERATURE = "current_temperature" +CONF_MIN_HUMIDITY = "min_humidity" +CONF_MAX_HUMIDITY = "max_humidity" +CONF_TARGET_HUMIDITY = "target_humidity" visual_temperature = cv.float_with_unit( "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" @@ -153,6 +159,8 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, + cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int, + cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int, } ), cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( @@ -167,6 +175,9 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_CURRENT_HUMIDITY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), @@ -209,6 +220,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_ON_CONTROL): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), @@ -238,6 +255,10 @@ async def setup_climate_core_(var, config): visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], ) ) + if CONF_MIN_HUMIDITY in visual: + cg.add(var.set_visual_min_humidity_override(visual[CONF_MIN_HUMIDITY])) + if CONF_MAX_HUMIDITY in visual: + cg.add(var.set_visual_max_humidity_override(visual[CONF_MAX_HUMIDITY])) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) @@ -255,6 +276,12 @@ async def setup_climate_core_(var, config): config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] ) ) + if CONF_CURRENT_HUMIDITY_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_current_humidity_state_topic( + config[CONF_CURRENT_HUMIDITY_STATE_TOPIC] + ) + ) if CONF_FAN_MODE_COMMAND_TOPIC in config: cg.add( mqtt_.set_custom_fan_mode_command_topic( @@ -323,6 +350,18 @@ async def setup_climate_core_(var, config): config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] ) ) + if CONF_TARGET_HUMIDITY_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_target_humidity_command_topic( + config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC] + ) + ) + if CONF_TARGET_HUMIDITY_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_target_humidity_state_topic( + config[CONF_TARGET_HUMIDITY_STATE_TOPIC] + ) + ) for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -351,6 +390,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int), cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( validate_climate_fan_mode @@ -387,6 +427,9 @@ async def climate_control_to_code(config, action_id, template_arg, args): config[CONF_TARGET_TEMPERATURE_HIGH], args, float ) cg.add(var.set_target_temperature_high(template_)) + if CONF_TARGET_HUMIDITY in config: + template_ = await cg.templatable(config[CONF_TARGET_HUMIDITY], args, float) + cg.add(var.set_target_humidity(template_)) if CONF_FAN_MODE in config: template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 382871e1e7..a4d13ade58 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -14,6 +14,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_high) + TEMPLATABLE_VALUE(float, target_humidity) TEMPLATABLE_VALUE(bool, away) TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) TEMPLATABLE_VALUE(std::string, custom_fan_mode) @@ -27,6 +28,7 @@ template class ControlAction : public Action { call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); + call.set_target_humidity(this->target_humidity_.optional_value(x...)); if (away_.has_value()) { call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ea24cab954..1822707152 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -45,6 +45,9 @@ void ClimateCall::perform() { if (this->target_temperature_high_.has_value()) { ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } + if (this->target_humidity_.has_value()) { + ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_); + } this->parent_->control(*this); } void ClimateCall::validate_() { @@ -262,10 +265,16 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h this->target_temperature_high_ = target_temperature_high; return *this; } +ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { + this->target_humidity_ = target_humidity; + return *this; +} + const optional &ClimateCall::get_mode() const { return this->mode_; } const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } +const optional &ClimateCall::get_target_humidity() const { return this->target_humidity_; } const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } @@ -283,6 +292,10 @@ ClimateCall &ClimateCall::set_target_temperature(optional target_temperat this->target_temperature_ = target_temperature; return *this; } +ClimateCall &ClimateCall::set_target_humidity(optional target_humidity) { + this->target_humidity_ = target_humidity; + return *this; +} ClimateCall &ClimateCall::set_mode(optional mode) { this->mode_ = mode; return *this; @@ -343,6 +356,9 @@ void Climate::save_state_() { } else { state.target_temperature = this->target_temperature; } + if (traits.get_supports_target_humidity()) { + state.target_humidity = this->target_humidity; + } if (traits.get_supports_fan_modes() && fan_mode.has_value()) { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); @@ -408,6 +424,12 @@ void Climate::publish_state() { } else { ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); } + if (traits.get_supports_current_humidity()) { + ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); + } + if (traits.get_supports_target_humidity()) { + ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); + } // Send state to frontend this->state_callback_.call(*this); @@ -427,6 +449,12 @@ ClimateTraits Climate::get_traits() { traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); } + if (this->visual_min_humidity_override_.has_value()) { + traits.set_visual_min_humidity(*this->visual_min_humidity_override_); + } + if (this->visual_max_humidity_override_.has_value()) { + traits.set_visual_max_humidity(*this->visual_max_humidity_override_); + } return traits; } @@ -441,6 +469,12 @@ void Climate::set_visual_temperature_step_override(float target, float current) this->visual_target_temperature_step_override_ = target; this->visual_current_temperature_step_override_ = current; } +void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { + this->visual_min_humidity_override_ = visual_min_humidity_override; +} +void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { + this->visual_max_humidity_override_ = visual_max_humidity_override; +} ClimateCall Climate::make_call() { return ClimateCall(this); } @@ -454,6 +488,9 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { } else { call.set_target_temperature(this->target_temperature); } + if (traits.get_supports_target_humidity()) { + call.set_target_humidity(this->target_humidity); + } if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { call.set_fan_mode(this->fan_mode); } @@ -474,6 +511,9 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { } else { climate->target_temperature = this->target_temperature; } + if (traits.get_supports_target_humidity()) { + climate->target_humidity = this->target_humidity; + } if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { climate->fan_mode = this->fan_mode; } @@ -530,17 +570,25 @@ void Climate::dump_traits_(const char *tag) { auto traits = this->get_traits(); ESP_LOGCONFIG(tag, "ClimateTraits:"); ESP_LOGCONFIG(tag, " [x] Visual settings:"); - ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); - ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); - ESP_LOGCONFIG(tag, " - Step:"); + ESP_LOGCONFIG(tag, " - Min temperature: %.1f", traits.get_visual_min_temperature()); + ESP_LOGCONFIG(tag, " - Max temperature: %.1f", traits.get_visual_max_temperature()); + ESP_LOGCONFIG(tag, " - Temperature step:"); ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); + ESP_LOGCONFIG(tag, " - Min humidity: %.0f", traits.get_visual_min_humidity()); + ESP_LOGCONFIG(tag, " - Max humidity: %.0f", traits.get_visual_max_humidity()); if (traits.get_supports_current_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports current temperature"); } + if (traits.get_supports_current_humidity()) { + ESP_LOGCONFIG(tag, " [x] Supports current humidity"); + } if (traits.get_supports_two_point_target_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); } + if (traits.get_supports_target_humidity()) { + ESP_LOGCONFIG(tag, " [x] Supports target humidity"); + } if (traits.get_supports_action()) { ESP_LOGCONFIG(tag, " [x] Supports action"); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 1bbb17322d..7c2a0b1ed3 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,6 +64,10 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional target_temperature_high); + /// Set the target humidity of the climate device. + ClimateCall &set_target_humidity(float target_humidity); + /// Set the target humidity of the climate device. + ClimateCall &set_target_humidity(optional target_humidity); /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(ClimateFanMode fan_mode); /// Set the fan mode of the climate device. @@ -93,6 +97,7 @@ class ClimateCall { const optional &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; + const optional &get_target_humidity() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_custom_fan_mode() const; @@ -107,6 +112,7 @@ class ClimateCall { optional target_temperature_; optional target_temperature_low_; optional target_temperature_high_; + optional target_humidity_; optional fan_mode_; optional swing_mode_; optional custom_fan_mode_; @@ -136,6 +142,7 @@ struct ClimateDeviceRestoreState { float target_temperature_high; }; }; + float target_humidity; /// Convert this struct to a climate call that can be performed. ClimateCall to_call(Climate *climate); @@ -164,11 +171,16 @@ class Climate : public EntityBase { /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; + /// The active state of the climate device. ClimateAction action{CLIMATE_ACTION_OFF}; + /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; + /// The current humidity of the climate device, as reported from the integration. + float current_humidity{NAN}; + union { /// The target temperature of the climate device. float target_temperature; @@ -180,6 +192,9 @@ class Climate : public EntityBase { }; }; + /// The target humidity of the climate device. + float target_humidity; + /// The active fan mode of the climate device. optional fan_mode; @@ -233,6 +248,8 @@ class Climate : public EntityBase { void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override); void set_visual_temperature_step_override(float target, float current); + void set_visual_min_humidity_override(float visual_min_humidity_override); + void set_visual_max_humidity_override(float visual_max_humidity_override); protected: friend ClimateCall; @@ -282,6 +299,8 @@ class Climate : public EntityBase { optional visual_max_temperature_override_{}; optional visual_target_temperature_step_override_{}; optional visual_current_temperature_step_override_{}; + optional visual_min_humidity_override_{}; + optional visual_max_humidity_override_{}; }; } // namespace climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index e8c2db6c06..fd5b025a03 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -44,10 +44,18 @@ class ClimateTraits { void set_supports_current_temperature(bool supports_current_temperature) { supports_current_temperature_ = supports_current_temperature; } + bool get_supports_current_humidity() const { return supports_current_humidity_; } + void set_supports_current_humidity(bool supports_current_humidity) { + supports_current_humidity_ = supports_current_humidity; + } bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { supports_two_point_target_temperature_ = supports_two_point_target_temperature; } + bool get_supports_target_humidity() const { return supports_target_humidity_; } + void set_supports_target_humidity(bool supports_target_humidity) { + supports_target_humidity_ = supports_target_humidity; + } void set_supported_modes(std::set modes) { supported_modes_ = std::move(modes); } void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") @@ -153,6 +161,11 @@ class ClimateTraits { int8_t get_target_temperature_accuracy_decimals() const; int8_t get_current_temperature_accuracy_decimals() const; + float get_visual_min_humidity() const { return visual_min_humidity_; } + void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; } + float get_visual_max_humidity() const { return visual_max_humidity_; } + void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; } + protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { @@ -177,7 +190,9 @@ class ClimateTraits { } bool supports_current_temperature_{false}; + bool supports_current_humidity_{false}; bool supports_two_point_target_temperature_{false}; + bool supports_target_humidity_{false}; std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; bool supports_action_{false}; std::set supported_fan_modes_; @@ -190,6 +205,8 @@ class ClimateTraits { float visual_max_temperature_{30}; float visual_target_temperature_step_{0.1}; float visual_current_temperature_step_{0.1}; + float visual_min_humidity_{30}; + float visual_max_humidity_{99}; }; } // namespace climate diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 44c490c308..49a8f06734 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -17,9 +17,12 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { - // current_temperature_topic root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic(); } + // current_humidity_topic + if (traits.get_supports_current_humidity()) { + root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic(); + } // mode_command_topic root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic(); // mode_state_topic @@ -57,6 +60,13 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic(); } + if (traits.get_supports_target_humidity()) { + // target_humidity_command_topic + root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic(); + // target_humidity_state_topic + root[MQTT_TARGET_HUMIDITY_STATE_TOPIC] = this->get_target_humidity_state_topic(); + } + // min_temp root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature(); // max_temp @@ -66,6 +76,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; + // min_humidity + root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity(); + // max_humidity + root[MQTT_MAX_HUMIDITY] = traits.get_visual_max_humidity(); + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { // preset_mode_command_topic root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic(); @@ -192,6 +207,20 @@ void MQTTClimateComponent::setup() { }); } + if (traits.get_supports_target_humidity()) { + this->subscribe(this->get_target_humidity_command_topic(), + [this](const std::string &topic, const std::string &payload) { + auto val = parse_number(payload); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); + return; + } + auto call = this->device_->make_call(); + call.set_target_humidity(*val); + call.perform(); + }); + } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) { auto call = this->device_->make_call(); @@ -273,6 +302,17 @@ bool MQTTClimateComponent::publish_state_() { success = false; } + if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) { + std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0); + if (!this->publish(this->get_current_humidity_state_topic(), payload)) + success = false; + } + if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) { + std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0); + if (!this->publish(this->get_target_humidity_state_topic(), payload)) + success = false; + } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { std::string payload; if (this->device_->preset.has_value()) { diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index a93070fe66..4e54230e68 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -20,6 +20,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { void setup() override; MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state) + MQTT_COMPONENT_CUSTOM_TOPIC(current_humidity, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature, state) @@ -28,6 +29,8 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_low, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, state) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command) + MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, state) + MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, command) MQTT_COMPONENT_CUSTOM_TOPIC(away, state) MQTT_COMPONENT_CUSTOM_TOPIC(away, command) MQTT_COMPONENT_CUSTOM_TOPIC(action, state) diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 7f74197ab4..3d9e0b4c00 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -51,6 +51,8 @@ constexpr const char *const MQTT_CODE_ARM_REQUIRED = "cod_arm_req"; constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "cod_dis_req"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "curr_hum_t"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "curr_hum_tpl"; constexpr const char *const MQTT_DEVICE = "dev"; constexpr const char *const MQTT_DEVICE_CLASS = "dev_cla"; constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; @@ -305,6 +307,8 @@ constexpr const char *const MQTT_CODE_ARM_REQUIRED = "code_arm_required"; constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "code_disarm_required"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template"; constexpr const char *const MQTT_DEVICE = "device"; constexpr const char *const MQTT_DEVICE_CLASS = "device_class"; constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 7cd414f912..2c4ef688a5 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import climate, sensor, output -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.const import CONF_HUMIDITY_SENSOR, CONF_ID, CONF_SENSOR pid_ns = cg.esphome_ns.namespace("pid") PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component) @@ -45,6 +45,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PIDClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), @@ -86,6 +87,10 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + if CONF_COOL_OUTPUT in config: out = await cg.get_variable(config[CONF_COOL_OUTPUT]) cg.add(var.set_cool_output(out)) diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index dab4502d40..93b6999a00 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -14,6 +14,16 @@ void PIDClimate::setup() { this->update_pid_(); }); this->current_temperature = this->sensor_->state; + + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { @@ -47,6 +57,9 @@ climate::ClimateTraits PIDClimate::traits() { traits.set_supports_current_temperature(true); traits.set_supports_two_point_target_temperature(false); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); if (supports_cool_()) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index da57209a7e..5ae97ee10b 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -19,6 +19,7 @@ class PIDClimate : public climate::Climate, public Component { void dump_config() override; void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { humidity_sensor_ = sensor; } void set_cool_output(output::FloatOutput *cool_output) { cool_output_ = cool_output; } void set_heat_output(output::FloatOutput *heat_output) { heat_output_ = heat_output; } void set_kp(float kp) { controller_.kp_ = kp; } @@ -85,6 +86,8 @@ class PIDClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; output::FloatOutput *cool_output_{nullptr}; output::FloatOutput *heat_output_{nullptr}; PIDController controller_; diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index cca46609db..89d6b13376 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -35,6 +35,7 @@ from esphome.const import ( CONF_HEAT_DEADBAND, CONF_HEAT_MODE, CONF_HEAT_OVERRUN, + CONF_HUMIDITY_SENSOR, CONF_ID, CONF_IDLE_ACTION, CONF_MAX_COOLING_RUN_TIME, @@ -519,6 +520,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ThermostatClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), cv.Optional( @@ -658,6 +660,10 @@ async def to_code(config): ) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 73b061b07c..40a29295f1 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -27,6 +27,15 @@ void ThermostatClimate::setup() { }); this->current_temperature = this->sensor_->state; + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + auto use_default_preset = true; if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) { @@ -217,6 +226,9 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); + if (supports_auto_) traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); if (supports_heat_cool_) @@ -1169,6 +1181,9 @@ void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { + this->humidity_sensor_ = humidity_sensor; +} void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 677b4ad324..559812a94f 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -81,6 +81,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_heating_minimum_run_time_in_sec(uint32_t time); void set_idle_minimum_time_in_sec(uint32_t time); void set_sensor(sensor::Sensor *sensor); + void set_humidity_sensor(sensor::Sensor *humidity_sensor); void set_use_startup_delay(bool use_startup_delay); void set_supports_auto(bool supports_auto); void set_supports_heat_cool(bool supports_heat_cool); @@ -238,6 +239,8 @@ class ThermostatClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; /// Whether the controller supports auto/cooling/drying/fanning/heating. /// diff --git a/esphome/const.py b/esphome/const.py index 1aabe81db3..101a690db5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -157,6 +157,7 @@ CONF_CS_PIN = "cs_pin" CONF_CSS_INCLUDE = "css_include" CONF_CSS_URL = "css_url" CONF_CURRENT = "current" +CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" @@ -324,6 +325,7 @@ CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference" CONF_HOUR = "hour" CONF_HOURS = "hours" CONF_HUMIDITY = "humidity" +CONF_HUMIDITY_SENSOR = "humidity_sensor" CONF_HYSTERESIS = "hysteresis" CONF_I2C = "i2c" CONF_I2C_ID = "i2c_id" @@ -758,6 +760,8 @@ CONF_SYNC = "sync" CONF_TABLET = "tablet" CONF_TAG = "tag" CONF_TARGET = "target" +CONF_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" CONF_TARGET_TEMPERATURE = "target_temperature" CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action" CONF_TARGET_TEMPERATURE_COMMAND_TOPIC = "target_temperature_command_topic" diff --git a/tests/test3.yaml b/tests/test3.yaml index 6b1757c2ad..ab7f38d07f 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -899,6 +899,7 @@ climate: - platform: bang_bang name: Bang Bang Climate sensor: ha_hello_world + humidity_sensor: ha_hello_world default_target_temperature_low: 18°C default_target_temperature_high: 24°C idle_action: @@ -913,6 +914,7 @@ climate: - platform: thermostat name: Thermostat Climate sensor: ha_hello_world + humidity_sensor: ha_hello_world preset: - name: Default Preset default_target_temperature_low: 18°C @@ -1000,6 +1002,7 @@ climate: id: pid_climate name: PID Climate Controller sensor: ha_hello_world + humidity_sensor: ha_hello_world default_target_temperature: 21°C heat_output: my_slow_pwm control_parameters: From 2fcc5b3ef212c6be62a299b50274d8a48d169ae7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:46:11 +0900 Subject: [PATCH 23/35] Remove lingering note (#5916) --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec23656763..1c92d91159 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,5 +10,3 @@ Things to note when contributing: for more information. - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files which checks if your new feature compiles correctly. - - Sometimes I will let pull requests linger because I'm not 100% sure about them. Please feel free to ping - me after some time. From f1f8689462db94fb42e1440795d9bb68b7c2ceac Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:47:46 +0900 Subject: [PATCH 24/35] Fix typo added in esp32 post_build filename (#5918) --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index fd5e9377dd..307a3e76e5 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -462,7 +462,7 @@ async def to_code(config): add_extra_script( "post", - "post_build2.py", + "post_build.py", os.path.join(os.path.dirname(__file__), "post_build.py.script"), ) From 058c43e95319b2dcc544c807c786d859ed8384f7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:51:07 +0900 Subject: [PATCH 25/35] Copy esp32 custom partition files to build folder (#5919) --- esphome/components/esp32/__init__.py | 33 +++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 307a3e76e5..5d17633975 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -497,10 +497,11 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config: - cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) - else: - cg.add_platformio_option("board_build.partitions", "partitions.csv") + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -639,20 +640,22 @@ def _write_sdkconfig(): # Called by writer.py def copy_files(): if CORE.using_arduino: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) if CORE.using_esp_idf: _write_sdkconfig() - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_idf_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_idf_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, # and no version.txt file exists, the CMake script fails for some setups. From 9daaadb3b68aaa1fabe1ecf67acd394053da883e Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 13 Dec 2023 08:29:05 +0100 Subject: [PATCH 26/35] UART change at runtime (#5909) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../uart/uart_component_esp32_arduino.cpp | 9 +++++++++ .../uart/uart_component_esp32_arduino.h | 14 ++++++++++++++ .../components/uart/uart_component_esp_idf.cpp | 16 ++++++++++++++-- esphome/components/uart/uart_component_esp_idf.h | 14 ++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 7306dd2f31..75b67bf5c2 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -109,6 +109,11 @@ void ESP32ArduinoUARTComponent::setup() { this->number_ = next_uart_num; this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory) } + + this->load_settings(false); +} + +void ESP32ArduinoUARTComponent::load_settings(bool dump_config) { int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; bool invert = false; @@ -118,6 +123,10 @@ void ESP32ArduinoUARTComponent::setup() { invert = true; this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert); + if (dump_config) { + ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->number_); + this->dump_config(); + } } void ESP32ArduinoUARTComponent::dump_config() { diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h index 02dfd0531e..ec4953a598 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -32,6 +32,20 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component { HardwareSerial *get_hw_serial() { return this->hw_serial_; } uint8_t get_hw_serial_number() { return this->number_; } + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config = true); + protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index c78626fa26..2dd6ab105f 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -122,9 +122,21 @@ void IDFUARTComponent::setup() { xSemaphoreGive(this->lock_); } +void IDFUARTComponent::load_settings(bool dump_config) { + uart_config_t uart_config = this->get_config_(); + esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } else if (dump_config) { + ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); + this->dump_config(); + } +} + void IDFUARTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "UART Bus:"); - ESP_LOGCONFIG(TAG, " Number: %u", this->uart_num_); + ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); LOG_PIN(" TX Pin: ", tx_pin_); LOG_PIN(" RX Pin: ", rx_pin_); if (this->rx_pin_ != nullptr) { diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index fdaa4da9a7..068ebd32dc 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -26,6 +26,20 @@ class IDFUARTComponent : public UARTComponent, public Component { uint8_t get_hw_serial_number() { return this->uart_num_; } QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config = true); + protected: void check_logger_conflict() override; uart_port_t uart_num_; From f355972c9d8f43b3a64849d698da1163d41325dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:57:50 -1000 Subject: [PATCH 27/35] Bump aioesphomeapi from 20.1.0 to 21.0.0 (#5922) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0d08da2b45..f330ecbf3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==20.1.0 +aioesphomeapi==21.0.0 zeroconf==0.128.4 python-magic==0.4.27 From 9a8bc9484d99c47d207d1092d43ce81121c6e107 Mon Sep 17 00:00:00 2001 From: Jacob Masen-Smith Date: Wed, 13 Dec 2023 12:33:24 -0800 Subject: [PATCH 28/35] Fix the initial run of lambda light effects (#5921) The timer used for `millis()` is a monotonic timer based on the last start time of the device. If, for some reason, you pick a long `update_interval` and try to apply it as soon as you start the device, nothing happens because the device hasn't been on for longer than the `update_interval` --- esphome/components/light/addressable_light_effect.h | 2 +- esphome/components/light/base_light_effects.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 0482cf53b9..c2109b2d23 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -57,7 +57,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); - if (now - this->last_run_ >= this->update_interval_) { + if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) { this->last_run_ = now; this->f_(it, current_color, this->initial_run_); this->initial_run_ = false; diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index d126e4960c..c62ca43ca1 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -118,7 +118,7 @@ class LambdaLightEffect : public LightEffect { void start() override { this->initial_run_ = true; } void apply() override { const uint32_t now = millis(); - if (now - this->last_run_ >= this->update_interval_) { + if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) { this->last_run_ = now; this->f_(this->initial_run_); this->initial_run_ = false; From 81aa48a5f346f8127b6a50563e045ebd9af90eb3 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:45:08 +0100 Subject: [PATCH 29/35] Exposes `load_settings` to `UARTComponent` class (#5920) --- esphome/components/uart/uart_component.h | 4 ++++ esphome/components/uart/uart_component_esp32_arduino.h | 3 ++- esphome/components/uart/uart_component_esp_idf.h | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 42702cf5b8..34bda42bb5 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -62,6 +62,10 @@ class UARTComponent { UARTParityOptions get_parity() const { return this->parity_; } void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } uint32_t get_baud_rate() const { return baud_rate_; } +#ifdef USE_ESP32 + virtual void load_settings() = 0; + virtual void load_settings(bool dump_config) = 0; +#endif // USE_ESP32 #ifdef USE_UART_DEBUGGER void add_debug_callback(std::function &&callback) { diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h index ec4953a598..de17d9718b 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -44,7 +44,8 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - void load_settings(bool dump_config = true); + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 068ebd32dc..215641ebe2 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -38,7 +38,8 @@ class IDFUARTComponent : public UARTComponent, public Component { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - void load_settings(bool dump_config = true); + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } protected: void check_logger_conflict() override; From 6fd239362d768e4e9ec533e59ef453bc3cab1988 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 13 Dec 2023 16:54:55 -0600 Subject: [PATCH 30/35] Add support for PN7160 (#5486) --- CODEOWNERS | 3 + esphome/components/nfc/nci_core.h | 144 ++ esphome/components/nfc/nci_message.cpp | 166 +++ esphome/components/nfc/nci_message.h | 50 + esphome/components/nfc/nfc.cpp | 2 +- esphome/components/nfc/nfc_helpers.cpp | 47 + esphome/components/nfc/nfc_helpers.h | 17 + esphome/components/pn7160/__init__.py | 227 ++++ esphome/components/pn7160/automation.h | 82 ++ esphome/components/pn7160/pn7160.cpp | 1161 +++++++++++++++++ esphome/components/pn7160/pn7160.h | 315 +++++ .../pn7160/pn7160_mifare_classic.cpp | 322 +++++ .../pn7160/pn7160_mifare_ultralight.cpp | 186 +++ esphome/components/pn7160_i2c/__init__.py | 25 + esphome/components/pn7160_i2c/pn7160_i2c.cpp | 49 + esphome/components/pn7160_i2c/pn7160_i2c.h | 22 + esphome/components/pn7160_spi/__init__.py | 26 + esphome/components/pn7160_spi/pn7160_spi.cpp | 54 + esphome/components/pn7160_spi/pn7160_spi.h | 30 + tests/test1.yaml | 37 + 20 files changed, 2964 insertions(+), 1 deletion(-) create mode 100644 esphome/components/nfc/nci_core.h create mode 100644 esphome/components/nfc/nci_message.cpp create mode 100644 esphome/components/nfc/nci_message.h create mode 100644 esphome/components/nfc/nfc_helpers.cpp create mode 100644 esphome/components/nfc/nfc_helpers.h create mode 100644 esphome/components/pn7160/__init__.py create mode 100644 esphome/components/pn7160/automation.h create mode 100644 esphome/components/pn7160/pn7160.cpp create mode 100644 esphome/components/pn7160/pn7160.h create mode 100644 esphome/components/pn7160/pn7160_mifare_classic.cpp create mode 100644 esphome/components/pn7160/pn7160_mifare_ultralight.cpp create mode 100644 esphome/components/pn7160_i2c/__init__.py create mode 100644 esphome/components/pn7160_i2c/pn7160_i2c.cpp create mode 100644 esphome/components/pn7160_i2c/pn7160_i2c.h create mode 100644 esphome/components/pn7160_spi/__init__.py create mode 100644 esphome/components/pn7160_spi/pn7160_spi.cpp create mode 100644 esphome/components/pn7160_spi/pn7160_spi.h diff --git a/CODEOWNERS b/CODEOWNERS index d509c98433..2269d580e4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,9 @@ esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn7160/* @jesserockz @kbx81 +esphome/components/pn7160_i2c/* @jesserockz @kbx81 +esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h new file mode 100644 index 0000000000..fdaf6d0cc5 --- /dev/null +++ b/esphome/components/nfc/nci_core.h @@ -0,0 +1,144 @@ +#pragma once + +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace nfc { + +// Header info +static const uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes +static const uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets +static const uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset +static const uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset +static const uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset +// Important masks +static const uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask +static const uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit +static const uint8_t NCI_PKT_GID_MASK = 0x0F; +static const uint8_t NCI_PKT_OID_MASK = 0x3F; +// Message types +static const uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) +static const uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC +static const uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands +static const uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC +// GIDs +static const uint8_t NCI_CORE_GID = 0x0; +static const uint8_t RF_GID = 0x1; +static const uint8_t NFCEE_GID = 0x1; +static const uint8_t NCI_PROPRIETARY_GID = 0xF; +// OIDs +static const uint8_t NCI_CORE_RESET_OID = 0x00; +static const uint8_t NCI_CORE_INIT_OID = 0x01; +static const uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; +static const uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; +static const uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; +static const uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; +static const uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; +static const uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; +static const uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; + +static const uint8_t RF_DISCOVER_MAP_OID = 0x00; +static const uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; +static const uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; +static const uint8_t RF_DISCOVER_OID = 0x03; +static const uint8_t RF_DISCOVER_SELECT_OID = 0x04; +static const uint8_t RF_INTF_ACTIVATED_OID = 0x05; +static const uint8_t RF_DEACTIVATE_OID = 0x06; +static const uint8_t RF_FIELD_INFO_OID = 0x07; +static const uint8_t RF_T3T_POLLING_OID = 0x08; +static const uint8_t RF_NFCEE_ACTION_OID = 0x09; +static const uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; +static const uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; + +static const uint8_t NFCEE_DISCOVER_OID = 0x00; +static const uint8_t NFCEE_MODE_SET_OID = 0x01; +// Interfaces +static const uint8_t INTF_NFCEE_DIRECT = 0x00; +static const uint8_t INTF_FRAME = 0x01; +static const uint8_t INTF_ISODEP = 0x02; +static const uint8_t INTF_NFCDEP = 0x03; +static const uint8_t INTF_TAGCMD = 0x80; // NXP proprietary +// Bit rates +static const uint8_t NFC_BIT_RATE_106 = 0x00; +static const uint8_t NFC_BIT_RATE_212 = 0x01; +static const uint8_t NFC_BIT_RATE_424 = 0x02; +static const uint8_t NFC_BIT_RATE_848 = 0x03; +static const uint8_t NFC_BIT_RATE_1695 = 0x04; +static const uint8_t NFC_BIT_RATE_3390 = 0x05; +static const uint8_t NFC_BIT_RATE_6780 = 0x06; +// Protocols +static const uint8_t PROT_UNDETERMINED = 0x00; +static const uint8_t PROT_T1T = 0x01; +static const uint8_t PROT_T2T = 0x02; +static const uint8_t PROT_T3T = 0x03; +static const uint8_t PROT_ISODEP = 0x04; +static const uint8_t PROT_NFCDEP = 0x05; +static const uint8_t PROT_T5T = 0x06; +static const uint8_t PROT_MIFARE = 0x80; +// RF Technologies +static const uint8_t NFC_RF_TECH_A = 0x00; +static const uint8_t NFC_RF_TECH_B = 0x01; +static const uint8_t NFC_RF_TECH_F = 0x02; +static const uint8_t NFC_RF_TECH_15693 = 0x03; +// RF Technology & Modes +static const uint8_t MODE_MASK = 0xF0; +static const uint8_t MODE_LISTEN_MASK = 0x80; +static const uint8_t MODE_POLL = 0x00; + +static const uint8_t TECH_PASSIVE_NFCA = 0x00; +static const uint8_t TECH_PASSIVE_NFCB = 0x01; +static const uint8_t TECH_PASSIVE_NFCF = 0x02; +static const uint8_t TECH_ACTIVE_NFCA = 0x03; +static const uint8_t TECH_ACTIVE_NFCF = 0x05; +static const uint8_t TECH_PASSIVE_15693 = 0x06; +// Status codes +static const uint8_t STATUS_OK = 0x00; +static const uint8_t STATUS_REJECTED = 0x01; +static const uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; +static const uint8_t STATUS_FAILED = 0x03; +static const uint8_t STATUS_NOT_INITIALIZED = 0x04; +static const uint8_t STATUS_SYNTAX_ERROR = 0x05; +static const uint8_t STATUS_SEMANTIC_ERROR = 0x06; +static const uint8_t STATUS_INVALID_PARAM = 0x09; +static const uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; +static const uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; +static const uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; +static const uint8_t DISCOVERY_TEAR_DOWN = 0xA2; +static const uint8_t RF_TRANSMISSION_ERROR = 0xB0; +static const uint8_t RF_PROTOCOL_ERROR = 0xB1; +static const uint8_t RF_TIMEOUT_ERROR = 0xB2; +static const uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; +static const uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; +static const uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; +static const uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; +// Deactivation types/reasons +static const uint8_t DEACTIVATION_TYPE_IDLE = 0x00; +static const uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; +static const uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; +static const uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; +// RF discover map modes +static const uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; +static const uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; +// RF discover notification types +static const uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; +static const uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; +static const uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; +// Important message offsets +static const uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_message.cpp b/esphome/components/nfc/nci_message.cpp new file mode 100644 index 0000000000..c6b21f6ae0 --- /dev/null +++ b/esphome/components/nfc/nci_message.cpp @@ -0,0 +1,166 @@ +#include "nci_core.h" +#include "nci_message.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace nfc { + +static const char *const TAG = "NciMessage"; + +NciMessage::NciMessage(const uint8_t message_type, const std::vector &payload) { + this->set_message(message_type, payload); +} + +NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid) { + this->set_header(message_type, gid, oid); +} + +NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid, + const std::vector &payload) { + this->set_message(message_type, gid, oid, payload); +} + +NciMessage::NciMessage(const std::vector &raw_packet) { this->nci_message_ = raw_packet; }; + +std::vector NciMessage::encode() { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + std::vector message = this->nci_message_; + return message; +} + +void NciMessage::reset() { this->nci_message_ = {0, 0, 0}; } + +uint8_t NciMessage::get_message_type() const { + return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK; +} + +uint8_t NciMessage::get_gid() const { return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK; } + +uint8_t NciMessage::get_oid() const { return this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK; } + +uint8_t NciMessage::get_payload_size(const bool recompute) { + if (!this->nci_message_.empty()) { + if (recompute) { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + } + return this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET]; + } + return 0; +} + +uint8_t NciMessage::get_simple_status_response() const { + if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) { + return this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET]; + } + return STATUS_FAILED; +} + +uint8_t NciMessage::get_message_byte(const uint8_t offset) const { + if (this->nci_message_.size() > offset) { + return this->nci_message_[offset]; + } + return 0; +} + +std::vector &NciMessage::get_message() { return this->nci_message_; } + +bool NciMessage::has_payload() const { return this->nci_message_.size() > nfc::NCI_PKT_HEADER_SIZE; } + +bool NciMessage::message_type_is(const uint8_t message_type) const { + if (!this->nci_message_.empty()) { + return message_type == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK); + } + return false; +} + +bool NciMessage::message_length_is(const uint8_t message_length, const bool recompute) { + if (this->nci_message_.size() > nfc::NCI_PKT_LENGTH_OFFSET) { + if (recompute) { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + } + return message_length == this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET]; + } + return false; +} + +bool NciMessage::gid_is(const uint8_t gid) const { + if (this->nci_message_.size() > nfc::NCI_PKT_MT_GID_OFFSET) { + return gid == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK); + } + return false; +} + +bool NciMessage::oid_is(const uint8_t oid) const { + if (this->nci_message_.size() > nfc::NCI_PKT_OID_OFFSET) { + return oid == (this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK); + } + return false; +} + +bool NciMessage::simple_status_response_is(const uint8_t response) const { + if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) { + return response == this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET]; + } + return false; +} + +void NciMessage::set_header(const uint8_t message_type, const uint8_t gid, const uint8_t oid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = + (message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK); + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; +} + +void NciMessage::set_message(const uint8_t message_type, const std::vector &payload) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end()); +} + +void NciMessage::set_message(const uint8_t message_type, const uint8_t gid, const uint8_t oid, + const std::vector &payload) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = + (message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK); + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end()); +} + +void NciMessage::set_message_type(const uint8_t message_type) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + auto mt_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_MT_MASK; + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = mt_masked | (message_type & nfc::NCI_PKT_MT_MASK); +} + +void NciMessage::set_gid(const uint8_t gid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + auto gid_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_GID_MASK; + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = gid_masked | (gid & nfc::NCI_PKT_GID_MASK); +} + +void NciMessage::set_oid(const uint8_t oid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; +} + +void NciMessage::set_payload(const std::vector &payload) { + std::vector message(this->nci_message_.begin(), this->nci_message_.begin() + nfc::NCI_PKT_HEADER_SIZE); + + message.insert(message.end(), payload.begin(), payload.end()); + message[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_ = message; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_message.h b/esphome/components/nfc/nci_message.h new file mode 100644 index 0000000000..c6b8537402 --- /dev/null +++ b/esphome/components/nfc/nci_message.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace nfc { + +class NciMessage { + public: + NciMessage() {} + NciMessage(uint8_t message_type, const std::vector &payload); + NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid); + NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector &payload); + NciMessage(const std::vector &raw_packet); + + std::vector encode(); + void reset(); + + uint8_t get_message_type() const; + uint8_t get_gid() const; + uint8_t get_oid() const; + uint8_t get_payload_size(bool recompute = false); + uint8_t get_simple_status_response() const; + uint8_t get_message_byte(uint8_t offset) const; + std::vector &get_message(); + + bool has_payload() const; + bool message_type_is(uint8_t message_type) const; + bool message_length_is(uint8_t message_length, bool recompute = false); + bool gid_is(uint8_t gid) const; + bool oid_is(uint8_t oid) const; + bool simple_status_response_is(uint8_t response) const; + + void set_header(uint8_t message_type, uint8_t gid, uint8_t oid); + void set_message(uint8_t message_type, const std::vector &payload); + void set_message(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector &payload); + void set_message_type(uint8_t message_type); + void set_gid(uint8_t gid); + void set_oid(uint8_t oid); + void set_payload(const std::vector &payload); + + protected: + std::vector nci_message_{0, 0, 0}; // three bytes, MT/PBF/GID, OID, payload length/size +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 7225e373b3..cf5a7f5ef1 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -53,7 +53,7 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { } bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index) { - uint8_t i = get_mifare_classic_ndef_start_index(data); + auto i = get_mifare_classic_ndef_start_index(data); if (data[i] != 0x03) { ESP_LOGE(TAG, "Error, Can't decode message length."); return false; diff --git a/esphome/components/nfc/nfc_helpers.cpp b/esphome/components/nfc/nfc_helpers.cpp new file mode 100644 index 0000000000..bfaed6e486 --- /dev/null +++ b/esphome/components/nfc/nfc_helpers.cpp @@ -0,0 +1,47 @@ +#include "nfc_helpers.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.helpers"; + +bool has_ha_tag_ndef(NfcTag &tag) { return !get_ha_tag_ndef(tag).empty(); } + +std::string get_ha_tag_ndef(NfcTag &tag) { + if (!tag.has_ndef_message()) { + return std::string(); + } + auto message = tag.get_ndef_message(); + auto records = message->get_records(); + for (const auto &record : records) { + std::string payload = record->get_payload(); + size_t pos = payload.find(HA_TAG_ID_PREFIX); + if (pos != std::string::npos) { + return payload.substr(pos + sizeof(HA_TAG_ID_PREFIX) - 1); + } + } + return std::string(); +} + +std::string get_random_ha_tag_ndef() { + static const char ALPHANUM[] = "0123456789abcdef"; + std::string uri = HA_TAG_ID_PREFIX; + for (int i = 0; i < 8; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + uri += "-"; + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 4; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + uri += "-"; + } + for (int i = 0; i < 12; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + ESP_LOGD("pn7160", "Payload to be written: %s", uri.c_str()); + return uri; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc_helpers.h b/esphome/components/nfc/nfc_helpers.h new file mode 100644 index 0000000000..74f5beba13 --- /dev/null +++ b/esphome/components/nfc/nfc_helpers.h @@ -0,0 +1,17 @@ +#pragma once + +#include "nfc_tag.h" + +namespace esphome { +namespace nfc { + +static const char HA_TAG_ID_EXT_RECORD_TYPE[] = "android.com:pkg"; +static const char HA_TAG_ID_EXT_RECORD_PAYLOAD[] = "io.homeassistant.companion.android"; +static const char HA_TAG_ID_PREFIX[] = "https://www.home-assistant.io/tag/"; + +std::string get_ha_tag_ndef(NfcTag &tag); +std::string get_random_ha_tag_ndef(); +bool has_ha_tag_ndef(NfcTag &tag); + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py new file mode 100644 index 0000000000..c91ca78b03 --- /dev/null +++ b/esphome/components/pn7160/__init__.py @@ -0,0 +1,227 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import nfc +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["binary_sensor", "nfc"] +CODEOWNERS = ["@kbx81", "@jesserockz"] + +CONF_DWL_REQ_PIN = "dwl_req_pin" +CONF_EMULATION_MESSAGE = "emulation_message" +CONF_EMULATION_OFF = "emulation_off" +CONF_EMULATION_ON = "emulation_on" +CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" +CONF_MESSAGE = "message" +CONF_ON_FINISHED_WRITE = "on_finished_write" +CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan" +CONF_PN7160_ID = "pn7160_id" +CONF_POLLING_OFF = "polling_off" +CONF_POLLING_ON = "polling_on" +CONF_SET_CLEAN_MODE = "set_clean_mode" +CONF_SET_EMULATION_MESSAGE = "set_emulation_message" +CONF_SET_FORMAT_MODE = "set_format_mode" +CONF_SET_READ_MODE = "set_read_mode" +CONF_SET_WRITE_MESSAGE = "set_write_message" +CONF_SET_WRITE_MODE = "set_write_mode" +CONF_TAG_TTL = "tag_ttl" +CONF_VEN_PIN = "ven_pin" +CONF_WKUP_REQ_PIN = "wkup_req_pin" + +pn7160_ns = cg.esphome_ns.namespace("pn7160") +PN7160 = pn7160_ns.class_("PN7160", cg.Component) + +EmulationOffAction = pn7160_ns.class_("EmulationOffAction", automation.Action) +EmulationOnAction = pn7160_ns.class_("EmulationOnAction", automation.Action) +PollingOffAction = pn7160_ns.class_("PollingOffAction", automation.Action) +PollingOnAction = pn7160_ns.class_("PollingOnAction", automation.Action) +SetCleanModeAction = pn7160_ns.class_("SetCleanModeAction", automation.Action) +SetEmulationMessageAction = pn7160_ns.class_( + "SetEmulationMessageAction", automation.Action +) +SetFormatModeAction = pn7160_ns.class_("SetFormatModeAction", automation.Action) +SetReadModeAction = pn7160_ns.class_("SetReadModeAction", automation.Action) +SetWriteMessageAction = pn7160_ns.class_("SetWriteMessageAction", automation.Action) +SetWriteModeAction = pn7160_ns.class_("SetWriteModeAction", automation.Action) + + +PN7160OnEmulatedTagScanTrigger = pn7160_ns.class_( + "PN7160OnEmulatedTagScanTrigger", automation.Trigger.template() +) + +PN7160OnFinishedWriteTrigger = pn7160_ns.class_( + "PN7160OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN7160IsWritingCondition = pn7160_ns.class_( + "PN7160IsWritingCondition", automation.Condition +) + + +IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition) + + +SIMPLE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PN7160), + } +) + +SET_MESSAGE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7160), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean, + } +) + +PN7160_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN7160), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7160OnEmulatedTagScanTrigger + ), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7160OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_DWL_REQ_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_WKUP_REQ_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EMULATION_MESSAGE): cv.string, + cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "tag.set_emulation_message", + SetEmulationMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +@automation.register_action( + "tag.set_write_message", + SetWriteMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +async def pn7160_set_message_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + template_ = await cg.templatable( + config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_ + ) + cg.add(var.set_include_android_app_record(template_)) + return var + + +@automation.register_action( + "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action( + "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA +) +async def pn7160_simple_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def setup_pn7160(var, config): + await cg.register_component(var, config) + + if dwl_req_pin_config := config.get(CONF_DWL_REQ_PIN): + pin = await cg.gpio_pin_expression(dwl_req_pin_config) + cg.add(var.set_dwl_req_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN]) + cg.add(var.set_ven_pin(pin)) + + if wakeup_req_pin_config := config.get(CONF_WKUP_REQ_PIN): + pin = await cg.gpio_pin_expression(wakeup_req_pin_config) + cg.add(var.set_wkup_req_pin(pin)) + + if emulation_message_config := config.get(CONF_EMULATION_MESSAGE): + cg.add(var.set_tag_emulation_message(emulation_message_config)) + cg.add(var.set_tag_emulation_on()) + + if CONF_TAG_TTL in config: + cg.add(var.set_tag_ttl(config[CONF_TAG_TTL])) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn7160.is_writing", + PN7160IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7160), + } + ), +) +async def pn7160_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h new file mode 100644 index 0000000000..854fb11684 --- /dev/null +++ b/esphome/components/pn7160/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/pn7160/pn7160.h" + +namespace esphome { +namespace pn7160 { + +class PN7160OnEmulatedTagScanTrigger : public Trigger<> { + public: + explicit PN7160OnEmulatedTagScanTrigger(PN7160 *parent) { + parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); + } +}; + +class PN7160OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN7160OnFinishedWriteTrigger(PN7160 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN7160IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } +}; + +template class EmulationOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } +}; + +template class EmulationOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } +}; + +template class PollingOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_off(); } +}; + +template class PollingOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_on(); } +}; + +template class SetCleanModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->clean_mode(); } +}; + +template class SetFormatModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->format_mode(); } +}; + +template class SetReadModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->read_mode(); } +}; + +template class SetEmulationMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_write_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->write_mode(); } +}; + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp new file mode 100644 index 0000000000..ce5374d1d1 --- /dev/null +++ b/esphome/components/pn7160/pn7160.cpp @@ -0,0 +1,1161 @@ +#include + +#include "automation.h" +#include "pn7160.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160"; + +void PN7160::setup() { + this->irq_pin_->setup(); + this->ven_pin_->setup(); + if (this->dwl_req_pin_ != nullptr) { + this->dwl_req_pin_->setup(); + } + if (this->wkup_req_pin_ != nullptr) { + this->wkup_req_pin_->setup(); + } + + this->nci_fsm_transition_(); // kick off reset & init processes +} + +void PN7160::dump_config() { + ESP_LOGCONFIG(TAG, "PN7160:"); + if (this->dwl_req_pin_ != nullptr) { + LOG_PIN(" DWL_REQ pin: ", this->dwl_req_pin_); + } + LOG_PIN(" IRQ pin: ", this->irq_pin_); + LOG_PIN(" VEN pin: ", this->ven_pin_); + if (this->wkup_req_pin_ != nullptr) { + LOG_PIN(" WKUP_REQ pin: ", this->wkup_req_pin_); + } +} + +void PN7160::loop() { + this->nci_fsm_transition_(); + this->purge_old_tags_(); +} + +void PN7160::set_tag_emulation_message(std::shared_ptr message) { + this->card_emulation_message_ = std::move(message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7160::set_tag_emulation_message(const optional &message, + const optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->card_emulation_message_ = std::move(ndef_message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7160::set_tag_emulation_message(const char *message, const bool include_android_app_record) { + this->set_tag_emulation_message(std::string(message), include_android_app_record); +} + +void PN7160::set_tag_emulation_off() { + if (this->listening_enabled_) { + this->listening_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation disabled"); +} + +void PN7160::set_tag_emulation_on() { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation cannot be enabled"); + return; + } + if (!this->listening_enabled_) { + this->listening_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation enabled"); +} + +void PN7160::set_polling_off() { + if (this->polling_enabled_) { + this->polling_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling disabled"); +} + +void PN7160::set_polling_on() { + if (!this->polling_enabled_) { + this->polling_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling enabled"); +} + +void PN7160::read_mode() { + this->next_task_ = EP_READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} + +void PN7160::clean_mode() { + this->next_task_ = EP_CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} + +void PN7160::format_mode() { + this->next_task_ = EP_FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} + +void PN7160::write_mode() { + if (this->next_task_message_to_write_ == nullptr) { + ESP_LOGW(TAG, "Message to write must be set before setting write mode"); + return; + } + + this->next_task_ = EP_WRITE; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +void PN7160::set_tag_write_message(std::shared_ptr message) { + this->next_task_message_to_write_ = std::move(message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +void PN7160::set_tag_write_message(optional message, optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->next_task_message_to_write_ = std::move(ndef_message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +uint8_t PN7160::set_test_mode(const TestMode test_mode, const std::vector &data, + std::vector &result) { + auto test_oid = TEST_PRBS_OID; + + switch (test_mode) { + case TestMode::TEST_PRBS: + // test_oid = TEST_PRBS_OID; + break; + + case TestMode::TEST_ANTENNA: + test_oid = TEST_ANTENNA_OID; + break; + + case TestMode::TEST_GET_REGISTER: + test_oid = TEST_GET_REGISTER_OID; + break; + + case TestMode::TEST_NONE: + default: + ESP_LOGD(TAG, "Exiting test mode"); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_OK; + } + + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::TEST); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, test_oid, data); + + ESP_LOGW(TAG, "Starting test mode, OID 0x%02X", test_oid); + auto status = this->transceive_(tx, rx, NFCC_INIT_TIMEOUT); + + if (status != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to start test mode, OID 0x%02X", test_oid); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + result.clear(); + } else { + result = rx.get_message(); + result.erase(result.begin(), result.begin() + 4); // remove NCI header + if (!result.empty()) { + ESP_LOGW(TAG, "Test results: %s", nfc::format_bytes(result).c_str()); + } + } + return status; +} + +uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { + if (this->dwl_req_pin_ != nullptr) { + this->dwl_req_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + } + + if (power) { + this->ven_pin_->digital_write(true); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(true); + delay(NFCC_INIT_TIMEOUT); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_RESET_OID, + {(uint8_t) reset_config}); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending reset command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid reset response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return rx.get_simple_status_response(); + } + // read reset notification + if (this->read_nfcc(rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Reset notification was not received"); + return nfc::STATUS_FAILED; + } + // verify reset notification + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.message_length_is(9)) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET] != 0x02) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] != (uint8_t) reset_config)) { + ESP_LOGE(TAG, "Reset notification was malformed: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Configuration %s", rx.get_message()[4] ? "reset" : "retained"); + ESP_LOGD(TAG, "NCI version: %s", rx.get_message()[5] == 0x20 ? "2.0" : "1.0"); + ESP_LOGD(TAG, "Manufacturer ID: 0x%02X", rx.get_message()[6]); + rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); + ESP_LOGD(TAG, "Manufacturer info: %s", nfc::format_bytes(rx.get_message()).c_str()); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::init_core_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_INIT_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending initialise command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid initialise response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + uint8_t hw_version = rx.get_message()[17 + rx.get_message()[8]]; + uint8_t rom_code_version = rx.get_message()[18 + rx.get_message()[8]]; + uint8_t flash_major_version = rx.get_message()[19 + rx.get_message()[8]]; + uint8_t flash_minor_version = rx.get_message()[20 + rx.get_message()[8]]; + std::vector features(rx.get_message().begin() + 4, rx.get_message().begin() + 8); + + ESP_LOGD(TAG, "Hardware version: %u", hw_version); + ESP_LOGD(TAG, "ROM code version: %u", rom_code_version); + ESP_LOGD(TAG, "FLASH major version: %u", flash_major_version); + ESP_LOGD(TAG, "FLASH minor version: %u", flash_minor_version); + ESP_LOGD(TAG, "Features: %s", nfc::format_bytes(features).c_str()); + + return rx.get_simple_status_response(); +} + +uint8_t PN7160::send_init_config_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, nfc::NCI_CORE_SET_CONFIG_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error enabling proprietary extensions"); + return nfc::STATUS_FAILED; + } + + tx.set_message(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(std::begin(PMU_CFG), std::end(PMU_CFG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending PMU config"); + return nfc::STATUS_FAILED; + } + + return this->send_core_config_(); +} + +uint8_t PN7160::send_core_config_() { + const auto *core_config_begin = std::begin(CORE_CONFIG_SOLO); + const auto *core_config_end = std::end(CORE_CONFIG_SOLO); + this->core_config_is_solo_ = true; + + if (this->listening_enabled_ && this->polling_enabled_) { + core_config_begin = std::begin(CORE_CONFIG_RW_CE); + core_config_end = std::end(CORE_CONFIG_RW_CE); + this->core_config_is_solo_ = false; + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(core_config_begin, core_config_end)); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error sending core config"); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::refresh_core_config_() { + bool core_config_should_be_solo = !(this->listening_enabled_ && this->polling_enabled_); + + if (this->nci_state_ == NCIState::RFST_DISCOVERY) { + if (this->stop_discovery_() != nfc::STATUS_OK) { + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_FAILED; + } + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + + if (this->core_config_is_solo_ != core_config_should_be_solo) { + if (this->send_core_config_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to refresh core config"); + return nfc::STATUS_FAILED; + } + } + this->config_refresh_pending_ = false; + return nfc::STATUS_OK; +} + +uint8_t PN7160::set_discover_map_() { + std::vector discover_map = {sizeof(RF_DISCOVER_MAP_CONFIG) / 3}; + discover_map.insert(discover_map.end(), std::begin(RF_DISCOVER_MAP_CONFIG), std::end(RF_DISCOVER_MAP_CONFIG)); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_MAP_OID, discover_map); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending discover map poll config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::set_listen_mode_routing_() { + nfc::NciMessage rx; + nfc::NciMessage tx( + nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_SET_LISTEN_MODE_ROUTING_OID, + std::vector(std::begin(RF_LISTEN_MODE_ROUTING_CONFIG), std::end(RF_LISTEN_MODE_ROUTING_CONFIG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error setting listen mode routing config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::start_discovery_() { + const uint8_t *rf_discovery_config = RF_DISCOVERY_CONFIG; + uint8_t length = sizeof(RF_DISCOVERY_CONFIG); + + if (!this->listening_enabled_) { + length = sizeof(RF_DISCOVERY_POLL_CONFIG); + rf_discovery_config = RF_DISCOVERY_POLL_CONFIG; + } else if (!this->polling_enabled_) { + length = sizeof(RF_DISCOVERY_LISTEN_CONFIG); + rf_discovery_config = RF_DISCOVERY_LISTEN_CONFIG; + } + + std::vector discover_config = std::vector((length * 2) + 1); + + discover_config[0] = length; + for (uint8_t i = 0; i < length; i++) { + discover_config[(i * 2) + 1] = rf_discovery_config[i]; + discover_config[(i * 2) + 2] = 0x01; // RF Technology and Mode will be executed in every discovery period + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, discover_config); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + switch (rx.get_simple_status_response()) { + // in any of these cases, we are either already in or will remain in discovery, which satisfies the function call + case nfc::STATUS_OK: + case nfc::DISCOVERY_ALREADY_STARTED: + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + case nfc::DISCOVERY_TEAR_DOWN: + return nfc::STATUS_OK; + + default: + ESP_LOGE(TAG, "Error starting discovery"); + return nfc::STATUS_FAILED; + } + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::stop_discovery_() { return this->deactivate_(nfc::DEACTIVATION_TYPE_IDLE, NFCC_TAG_WRITE_TIMEOUT); } + +uint8_t PN7160::deactivate_(const uint8_t type, const uint16_t timeout) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {type}); + + auto status = this->transceive_(tx, rx, timeout); + // if (status != nfc::STATUS_OK) { + // ESP_LOGE(TAG, "Error sending deactivate type %u", type); + // return nfc::STATUS_FAILED; + // } + return status; +} + +void PN7160::select_endpoint_() { + if (this->discovered_endpoint_.empty()) { + ESP_LOGW(TAG, "No cached tags to select"); + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + return; + } + std::vector endpoint_data = {this->discovered_endpoint_[0].id, this->discovered_endpoint_[0].protocol, + 0x01}; // that last byte is the interface ID + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (!this->discovered_endpoint_[i].trig_called) { + endpoint_data = {this->discovered_endpoint_[i].id, this->discovered_endpoint_[i].protocol, + 0x01}; // that last byte is the interface ID + this->selecting_endpoint_ = i; + break; + } + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_SELECT_OID, endpoint_data); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error selecting endpoint"); + } else { + this->nci_fsm_set_state_(NCIState::EP_SELECTING); + } +} + +uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) { + uint8_t type = nfc::guess_tag_type(tag.get_uid().size()); + + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + ESP_LOGV(TAG, "Reading Mifare classic"); + return this->read_mifare_classic_tag_(tag); + + case nfc::TAG_TYPE_2: + ESP_LOGV(TAG, "Reading Mifare ultralight"); + return this->read_mifare_ultralight_tag_(tag); + + case nfc::TAG_TYPE_UNKNOWN: + default: + ESP_LOGV(TAG, "Cannot determine tag type"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::clean_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_mifare_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for cleaning"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::format_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_ndef_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for formatting"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::write_endpoint_(std::vector &uid, std::shared_ptr &message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->write_mifare_classic_tag_(message); + + case nfc::TAG_TYPE_2: + return this->write_mifare_ultralight_tag_(uid, message); + + default: + ESP_LOGE(TAG, "Unsupported tag for writing"); + break; + } + return nfc::STATUS_FAILED; +} + +std::unique_ptr PN7160::build_tag_(const uint8_t mode_tech, const std::vector &data) { + switch (mode_tech) { + case (nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA): { + uint8_t uid_length = data[2]; + if (!uid_length) { + ESP_LOGE(TAG, "UID length cannot be zero"); + return nullptr; + } + std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + const auto *tag_type_str = + nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; + return make_unique(uid, tag_type_str); + } + } + return nullptr; +} + +optional PN7160::find_tag_uid_(const std::vector &uid) { + if (!this->discovered_endpoint_.empty()) { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); + bool uid_match = (uid.size() == existing_tag_uid.size()); + + if (uid_match) { + for (size_t i = 0; i < uid.size(); i++) { + uid_match &= (uid[i] == existing_tag_uid[i]); + } + if (uid_match) { + return i; + } + } + } + } + return nullopt; +} + +void PN7160::purge_old_tags_() { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { + this->erase_tag_(i); + } + } +} + +void PN7160::erase_tag_(const uint8_t tag_index) { + if (tag_index < this->discovered_endpoint_.size()) { + for (auto *trigger : this->triggers_ontagremoved_) { + trigger->process(this->discovered_endpoint_[tag_index].tag); + } + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); + } +} + +void PN7160::nci_fsm_transition_() { + switch (this->nci_state_) { + case NCIState::NFCC_RESET: + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + // fall through + + case NCIState::NFCC_INIT: + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); + } + // fall through + + case NCIState::NFCC_CONFIG: + if (this->send_init_config_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to send initial config"); + this->nci_fsm_set_error_state_(NCIState::NFCC_CONFIG); + return; + } else { + this->config_refresh_pending_ = false; + this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); + } + // fall through + + case NCIState::NFCC_SET_DISCOVER_MAP: + if (this->set_discover_map_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set discover map"); + this->nci_fsm_set_error_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + } + // fall through + + case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: + if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set listen mode routing"); + this->nci_fsm_set_error_state_(NCIState::RFST_IDLE); + return; + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + // fall through + + case NCIState::RFST_IDLE: + if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { + this->stop_discovery_(); + } + + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + + if (!this->listening_enabled_ && !this->polling_enabled_) { + return; + } + + if (this->start_discovery_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to start discovery"); + this->nci_fsm_set_error_state_(NCIState::RFST_DISCOVERY); + } else { + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + } + return; + + case NCIState::RFST_W4_HOST_SELECT: + select_endpoint_(); + // fall through + + // All cases below are waiting for NOTIFICATION messages + case NCIState::RFST_DISCOVERY: + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + // fall through + + case NCIState::RFST_LISTEN_ACTIVE: + case NCIState::RFST_LISTEN_SLEEP: + case NCIState::RFST_POLL_ACTIVE: + case NCIState::EP_SELECTING: + case NCIState::EP_DEACTIVATING: + if (this->irq_pin_->digital_read()) { + this->process_message_(); + } + break; + + case NCIState::FAILED: + case NCIState::NONE: + default: + return; + } +} + +void PN7160::nci_fsm_set_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_state_(%u)", (uint8_t) new_state); + this->nci_state_ = new_state; + this->nci_state_error_ = NCIState::NONE; + this->error_count_ = 0; + this->last_nci_state_change_ = millis(); +} + +bool PN7160::nci_fsm_set_error_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_error_state_(%u); error_count_ = %u", (uint8_t) new_state, this->error_count_); + this->nci_state_error_ = new_state; + if (this->error_count_++ > NFCC_MAX_ERROR_COUNT) { + if ((this->nci_state_error_ == NCIState::NFCC_RESET) || (this->nci_state_error_ == NCIState::NFCC_INIT) || + (this->nci_state_error_ == NCIState::NFCC_CONFIG)) { + ESP_LOGE(TAG, "Too many initialization failures -- check device connections"); + this->mark_failed(); + this->nci_fsm_set_state_(NCIState::FAILED); + } else { + ESP_LOGW(TAG, "Too many errors transitioning to state %u; resetting NFCC", (uint8_t) this->nci_state_error_); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + } + } + return this->error_count_ > NFCC_MAX_ERROR_COUNT; +} + +void PN7160::process_message_() { + nfc::NciMessage rx; + if (this->read_nfcc(rx, NFCC_DEFAULT_TIMEOUT) != nfc::STATUS_OK) { + return; // No data + } + + switch (rx.get_message_type()) { + case nfc::NCI_PKT_MT_CTRL_NOTIFICATION: + if (rx.get_gid() == nfc::RF_GID) { + switch (rx.get_oid()) { + case nfc::RF_INTF_ACTIVATED_OID: + ESP_LOGVV(TAG, "RF_INTF_ACTIVATED_OID"); + this->process_rf_intf_activated_oid_(rx); + return; + + case nfc::RF_DISCOVER_OID: + ESP_LOGVV(TAG, "RF_DISCOVER_OID"); + this->process_rf_discover_oid_(rx); + return; + + case nfc::RF_DEACTIVATE_OID: + ESP_LOGVV(TAG, "RF_DEACTIVATE_OID: type: 0x%02X, reason: 0x%02X", rx.get_message()[3], rx.get_message()[4]); + this->process_rf_deactivate_oid_(rx); + return; + + default: + ESP_LOGV(TAG, "Unimplemented RF OID received: 0x%02X", rx.get_oid()); + } + } else if (rx.get_gid() == nfc::NCI_CORE_GID) { + switch (rx.get_oid()) { + case nfc::NCI_CORE_GENERIC_ERROR_OID: + ESP_LOGV(TAG, "NCI_CORE_GENERIC_ERROR_OID:"); + switch (rx.get_simple_status_response()) { + case nfc::DISCOVERY_ALREADY_STARTED: + ESP_LOGV(TAG, " DISCOVERY_ALREADY_STARTED"); + break; + + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + // Tag removed too soon + ESP_LOGV(TAG, " DISCOVERY_TARGET_ACTIVATION_FAILED"); + if (this->nci_state_ == NCIState::EP_SELECTING) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + if (!this->discovered_endpoint_.empty()) { + this->erase_tag_(this->selecting_endpoint_); + } + } else { + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + case nfc::DISCOVERY_TEAR_DOWN: + ESP_LOGV(TAG, " DISCOVERY_TEAR_DOWN"); + break; + + default: + ESP_LOGW(TAG, "Unknown error: 0x%02X", rx.get_simple_status_response()); + break; + } + break; + + default: + ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); + } + } else { + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + break; + + case nfc::NCI_PKT_MT_CTRL_RESPONSE: + ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), + nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_CTRL_COMMAND: + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_DATA: + this->process_data_message_(rx); + break; + + default: + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + } +} + +void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoint was activated + uint8_t discovery_id = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_DISCOVERY_ID); + uint8_t interface = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_INTERFACE); + uint8_t protocol = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_PROTOCOL); + uint8_t mode_tech = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MODE_TECH); + uint8_t max_size = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MAX_SIZE); + + ESP_LOGVV(TAG, "Endpoint activated -- interface: 0x%02X, protocol: 0x%02X, mode&tech: 0x%02X, max payload: %u", + interface, protocol, mode_tech, max_size); + + if (mode_tech & nfc::MODE_LISTEN_MASK) { + ESP_LOGVV(TAG, "Tag activated in listen mode"); + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_ACTIVE); + return; + } + + this->nci_fsm_set_state_(NCIState::RFST_POLL_ACTIVE); + auto incoming_tag = + this->build_tag_(mode_tech, std::vector(rx.get_message().begin() + 10, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = discovery_id; + this->discovered_endpoint_[tag_loc.value()].protocol = protocol; + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag cache updated"); + } else { + this->discovered_endpoint_.emplace_back( + DiscoveredEndpoint{discovery_id, protocol, millis(), std::move(incoming_tag), false}); + tag_loc = this->discovered_endpoint_.size() - 1; + ESP_LOGVV(TAG, "Tag added to cache"); + } + + auto &working_endpoint = this->discovered_endpoint_[tag_loc.value()]; + + switch (this->next_task_) { + case EP_CLEAN: + ESP_LOGD(TAG, " Tag cleaning..."); + if (this->clean_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag cleaning incomplete"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + break; + + case EP_FORMAT: + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + break; + + case EP_WRITE: + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag could not be formatted for writing"); + } else { + ESP_LOGD(TAG, " Writing NDEF data"); + if (this->write_endpoint_(working_endpoint.tag->get_uid(), this->next_task_message_to_write_) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + break; + + case EP_READ: + default: + if (!working_endpoint.trig_called) { + ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), + nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { + ESP_LOGW(TAG, " Unable to read NDEF record(s)"); + } else if (working_endpoint.tag->has_ndef_message()) { + const auto message = working_endpoint.tag->get_ndef_message(); + const auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF record(s):"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } else { + ESP_LOGW(TAG, " No NDEF records found"); + } + for (auto *trigger : this->triggers_ontag_) { + trigger->process(working_endpoint.tag); + } + working_endpoint.trig_called = true; + break; + } + } + if (working_endpoint.tag->get_tag_type() == nfc::MIFARE_CLASSIC) { + this->halt_mifare_classic_tag_(); + } + } + if (this->next_task_ != EP_READ) { + this->read_mode(); + } + + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::EP_DEACTIVATING); +} + +void PN7160::process_rf_discover_oid_(nfc::NciMessage &rx) { + auto incoming_tag = this->build_tag_(rx.get_message_byte(nfc::RF_DISCOVER_NTF_MODE_TECH), + std::vector(rx.get_message().begin() + 7, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag!"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID); + this->discovered_endpoint_[tag_loc.value()].protocol = rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL); + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag found & updated"); + } else { + this->discovered_endpoint_.emplace_back(DiscoveredEndpoint{rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID), + rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL), + millis(), std::move(incoming_tag), false}); + ESP_LOGVV(TAG, "Tag saved"); + } + } + + if (rx.get_message().back() != nfc::RF_DISCOVER_NTF_NT_MORE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + ESP_LOGVV(TAG, "Discovered %u endpoints", this->discovered_endpoint_.size()); + } +} + +void PN7160::process_rf_deactivate_oid_(nfc::NciMessage &rx) { + this->ce_state_ = CardEmulationState::CARD_EMU_IDLE; + + switch (rx.get_simple_status_response()) { + case nfc::DEACTIVATION_TYPE_DISCOVERY: + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + break; + + case nfc::DEACTIVATION_TYPE_IDLE: + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + break; + + case nfc::DEACTIVATION_TYPE_SLEEP: + case nfc::DEACTIVATION_TYPE_SLEEP_AF: + if (this->nci_state_ == NCIState::RFST_LISTEN_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_SLEEP); + } else if (this->nci_state_ == NCIState::RFST_POLL_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + default: + break; + } +} + +void PN7160::process_data_message_(nfc::NciMessage &rx) { + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + + std::vector ndef_response; + this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); + + uint16_t ndef_response_size = ndef_response.size(); + if (!ndef_response_size) { + return; // no message returned, we cannot respond + } + + std::vector tx_msg = {nfc::NCI_PKT_MT_DATA, uint8_t((ndef_response_size & 0xFF00) >> 8), + uint8_t(ndef_response_size & 0x00FF)}; + tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); + nfc::NciMessage tx(tx_msg); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending reply for card emulation failed"); + } +} + +void PN7160::card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response) { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation not possible"); + ndef_response.clear(); + return; + } + + if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_APP_SELECT))) { + // CARD_EMU_T4T_APP_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_APP_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_APP_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_CC_SELECT))) { + // CARD_EMU_T4T_CC_SELECT + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_APP_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_CC_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_CC_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_NDEF_SELECT))) { + // CARD_EMU_T4T_NDEF_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_READ), + std::begin(CARD_EMU_T4T_READ))) { + // CARD_EMU_T4T_READ + if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED"); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + if (length <= (sizeof(CARD_EMU_T4T_CC) + offset + 2)) { + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_CC) + offset, + std::begin(CARD_EMU_T4T_CC) + offset + length); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED"); + auto ndef_message = this->card_emulation_message_->encode(); + uint16_t ndef_msg_size = ndef_message.size(); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + + if (length <= (ndef_msg_size + offset + 2)) { + if (offset == 0) { + ndef_response.resize(2); + ndef_response[0] = (ndef_msg_size & 0xFF00) >> 8; + ndef_response[1] = (ndef_msg_size & 0x00FF); + if (length > 2) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 2); + } + } else if (offset == 1) { + ndef_response.resize(1); + ndef_response[0] = (ndef_msg_size & 0x00FF); + if (length > 1) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 1); + } + } else { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length); + } + + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + + if ((offset + length) >= (ndef_msg_size + 2)) { + ESP_LOGD(TAG, "NDEF message sent"); + this->on_emulated_tag_scan_callback_.call(); + } + } + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_WRITE), + std::begin(CARD_EMU_T4T_WRITE))) { + // CARD_EMU_T4T_WRITE + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_T4T_WRITE"); + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + std::vector ndef_msg_written; + + ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } +} + +uint8_t PN7160::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, + const bool expect_notification) { + uint8_t retries = NFCC_MAX_COMM_FAILS; + + while (retries) { + // first, send the message we need to send + if (this->write_nfcc(tx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + // next, the NFCC should send back a response + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error receiving message"); + if (!retries--) { + ESP_LOGE(TAG, " ...giving up"); + return nfc::STATUS_FAILED; + } + } else { + break; + } + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + // validate the response based on the message type that was sent (command vs. data) + if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { + // for commands, the GID and OID should match and the status should be OK + if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + return rx.get_simple_status_response(); + } else { + // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || + (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (expect_notification) { + // if the NFCC said "OK", there will be additional data to read; this comes back in a notification message + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error receiving data from endpoint"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + + return nfc::STATUS_OK; + } +} + +uint8_t PN7160::wait_for_irq_(uint16_t timeout, bool pin_state) { + auto start_time = millis(); + + while (millis() - start_time < timeout) { + if (this->irq_pin_->digital_read() == pin_state) { + return nfc::STATUS_OK; + } + } + ESP_LOGW(TAG, "Timed out waiting for IRQ state"); + return nfc::STATUS_FAILED; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h new file mode 100644 index 0000000000..2b3cb99453 --- /dev/null +++ b/esphome/components/pn7160/pn7160.h @@ -0,0 +1,315 @@ +#pragma once + +#include "esphome/components/nfc/automation.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/nfc/nci_message.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_helpers.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace pn7160 { + +static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static const uint16_t NFCC_INIT_TIMEOUT = 50; +static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; + +static const uint8_t NFCC_MAX_COMM_FAILS = 3; +static const uint8_t NFCC_MAX_ERROR_COUNT = 10; + +static const uint8_t XCHG_DATA_OID = 0x10; +static const uint8_t MF_SECTORSEL_OID = 0x32; +static const uint8_t MFC_AUTHENTICATE_OID = 0x40; +static const uint8_t TEST_PRBS_OID = 0x30; +static const uint8_t TEST_ANTENNA_OID = 0x3D; +static const uint8_t TEST_GET_REGISTER_OID = 0x33; + +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; + +static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; + +static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms + +static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms + +static const uint8_t PMU_CFG[] = { + 0x01, // Number of parameters + 0xA0, 0x0E, // ext. tag + 11, // length + 0x11, // IRQ Enable: PVDD + temp sensor IRQs + 0x01, // RFU + 0x01, // Power and Clock Configuration, device on (CFG1) + 0x01, // Power and Clock Configuration, device off (CFG1) + 0x00, // RFU + 0x00, // DC-DC 0 + 0x00, // DC-DC 1 + // 0x14, // TXLDO (3.3V / 4.75V) + // 0xBB, // TXLDO (4.7V / 4.7V) + 0xFF, // TXLDO (5.0V / 5.0V) + 0x00, // RFU + 0xD0, // TXLDO check + 0x0C, // RFU +}; + +static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes + nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN, + nfc::INTF_ISODEP, // poll & listen mode + nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_TAGCMD}; // poll mode + +static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode + +static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 2, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::PROT_ISODEP, // protocol + 0x00, // type = technology-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::TECH_PASSIVE_NFCA}; // technology + +enum class CardEmulationState : uint8_t { + CARD_EMU_IDLE, + CARD_EMU_NDEF_APP_SELECTED, + CARD_EMU_CC_SELECTED, + CARD_EMU_NDEF_SELECTED, + CARD_EMU_DESFIRE_PROD, +}; + +enum class NCIState : uint8_t { + NONE = 0x00, + NFCC_RESET, + NFCC_INIT, + NFCC_CONFIG, + NFCC_SET_DISCOVER_MAP, + NFCC_SET_LISTEN_MODE_ROUTING, + RFST_IDLE, + RFST_DISCOVERY, + RFST_W4_ALL_DISCOVERIES, + RFST_W4_HOST_SELECT, + RFST_LISTEN_ACTIVE, + RFST_LISTEN_SLEEP, + RFST_POLL_ACTIVE, + EP_DEACTIVATING, + EP_SELECTING, + TEST = 0XFE, + FAILED = 0XFF, +}; + +enum class TestMode : uint8_t { + TEST_NONE = 0x00, + TEST_PRBS, + TEST_ANTENNA, + TEST_GET_REGISTER, +}; + +struct DiscoveredEndpoint { + uint8_t id; + uint8_t protocol; + uint32_t last_seen; + std::unique_ptr tag; + bool trig_called; +}; + +class PN7160 : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; } + void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } + void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; } + void set_wkup_req_pin(GPIOPin *wkup_req_pin) { this->wkup_req_pin_ = wkup_req_pin; } + + void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; } + void set_tag_emulation_message(std::shared_ptr message); + void set_tag_emulation_message(const optional &message, optional include_android_app_record); + void set_tag_emulation_message(const char *message, bool include_android_app_record = true); + void set_tag_emulation_off(); + void set_tag_emulation_on(); + bool tag_emulation_enabled() { return this->listening_enabled_; } + + void set_polling_off(); + void set_polling_on(); + bool polling_enabled() { return this->polling_enabled_; } + + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_emulated_tag_scan_callback(std::function callback) { + this->on_emulated_tag_scan_callback_.add(std::move(callback)); + } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != EP_READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(); + void set_tag_write_message(std::shared_ptr message); + void set_tag_write_message(optional message, optional include_android_app_record); + + uint8_t set_test_mode(TestMode test_mode, const std::vector &data, std::vector &result); + + protected: + uint8_t reset_core_(bool reset_config, bool power); + uint8_t init_core_(); + uint8_t send_init_config_(); + uint8_t send_core_config_(); + uint8_t refresh_core_config_(); + + uint8_t set_discover_map_(); + + uint8_t set_listen_mode_routing_(); + + uint8_t start_discovery_(); + uint8_t stop_discovery_(); + uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT); + + void select_endpoint_(); + + uint8_t read_endpoint_data_(nfc::NfcTag &tag); + uint8_t clean_endpoint_(std::vector &uid); + uint8_t format_endpoint_(std::vector &uid); + uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + + std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); + optional find_tag_uid_(const std::vector &uid); + void purge_old_tags_(); + void erase_tag_(uint8_t tag_index); + + /// advance controller state as required + void nci_fsm_transition_(); + /// set new controller state + void nci_fsm_set_state_(NCIState new_state); + /// setting controller to this state caused an error; returns true if too many errors/failures + bool nci_fsm_set_error_state_(NCIState new_state); + /// parse & process incoming messages from the NFCC + void process_message_(); + void process_rf_intf_activated_oid_(nfc::NciMessage &rx); + void process_rf_discover_oid_(nfc::NciMessage &rx); + void process_rf_deactivate_oid_(nfc::NciMessage &rx); + void process_data_message_(nfc::NciMessage &rx); + + void card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response); + + uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT, + bool expect_notification = true); + virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0; + virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0; + + uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true); + + uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); + uint8_t sect_to_auth_(uint8_t block_num); + uint8_t format_mifare_classic_mifare_(); + uint8_t format_mifare_classic_ndef_(); + uint8_t write_mifare_classic_tag_(const std::shared_ptr &message); + uint8_t halt_mifare_classic_tag_(); + + uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); + uint16_t read_mifare_ultralight_capacity_(); + uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t clean_mifare_ultralight_(); + + enum NfcTask : uint8_t { + EP_READ = 0, + EP_CLEAN, + EP_FORMAT, + EP_WRITE, + } next_task_{EP_READ}; + + bool config_refresh_pending_{false}; + bool core_config_is_solo_{false}; + bool listening_enabled_{false}; + bool polling_enabled_{true}; + + uint8_t error_count_{0}; + uint8_t fail_count_{0}; + uint32_t last_nci_state_change_{0}; + uint8_t selecting_endpoint_{0}; + uint32_t tag_ttl_{250}; + + GPIOPin *dwl_req_pin_{nullptr}; + GPIOPin *irq_pin_{nullptr}; + GPIOPin *ven_pin_{nullptr}; + GPIOPin *wkup_req_pin_{nullptr}; + + CallbackManager on_emulated_tag_scan_callback_; + CallbackManager on_finished_write_callback_; + + std::vector discovered_endpoint_; + + CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE}; + NCIState nci_state_{NCIState::NFCC_RESET}; + NCIState nci_state_error_{NCIState::NONE}; + + std::shared_ptr card_emulation_message_; + std::shared_ptr next_task_message_to_write_; + + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; +}; + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp new file mode 100644 index 0000000000..fa63cc00d5 --- /dev/null +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -0,0 +1,322 @@ +#include + +#include "pn7160.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160.mifare_classic"; + +uint8_t PN7160::read_mifare_classic_tag_(nfc::NfcTag &tag) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data"); + return nfc::STATUS_FAILED; + } + std::vector data; + + if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return nfc::STATUS_FAILED; + } + } else { + ESP_LOGE(TAG, "Failed to read block %u", current_block); + return nfc::STATUS_FAILED; + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Block authentication failed for %u", current_block); + return nfc::STATUS_FAILED; + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading block %u", current_block); + return nfc::STATUS_FAILED; + } else { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + + if (buffer.begin() + message_start_index < buffer.end()) { + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + } else { + return nfc::STATUS_FAILED; + } + + tag.set_ndef_message(make_unique(buffer)); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Timeout reading tag data"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (!rx.message_length_is(18))) { + ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1); + + ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes(data).c_str()); + return nfc::STATUS_OK; +} + +uint8_t PN7160::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num}); + + switch (key_num) { + case nfc::MIFARE_CMD_AUTH_A: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A; + break; + + case nfc::MIFARE_CMD_AUTH_B: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B; + break; + + default: + break; + } + + if (key != nullptr) { + tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY; + tx.get_message().insert(tx.get_message().end(), key, key + 6); + } + + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); + return nfc::STATUS_FAILED; + } + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || + (rx.get_message()[4] != nfc::STATUS_OK)) { + ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num); + return nfc::STATUS_OK; +} + +uint8_t PN7160::sect_to_auth_(const uint8_t block_num) { + const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + if (block_num >= first_high_block) { + return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) + + nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + } + return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW; +} + +uint8_t PN7160::format_mifare_classic_mifare_() { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + auto status = nfc::STATUS_OK; + + for (int block = 0; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + continue; + } + if (block != 0) { + if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + + return status; +} + +uint8_t PN7160::format_mifare_classic_ndef_() { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Sector 0 formatted with NDEF"); + + auto status = nfc::STATUS_OK; + + for (int block = 4; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (block == 4) { + if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } else { + if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + return status; +} + +uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + // write command part two + tx.set_payload({XCHG_DATA_OID}); + tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) { + ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr &message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::halt_mifare_classic_tag_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0}); + + ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp new file mode 100644 index 0000000000..a74f23d4f2 --- /dev/null +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "pn7160.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160.mifare_ultralight"; + +uint8_t PN7160::read_mifare_ultralight_tag_(nfc::NfcTag &tag) { + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); + return nfc::STATUS_FAILED; + } + + uint8_t message_length; + uint8_t message_start_index; + if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); + + if (message_length == 0) { + return nfc::STATUS_FAILED; + } + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + + tag.set_ndef_message(make_unique(data)); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page}); + + for (size_t i = 0; i * read_increment < num_bytes; i++) { + tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page; + do { // loop because sometimes we struggle here...???... + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } while (rx.get_payload_size() < read_increment); + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1 + : rx.get_message().end() - (bytes_offset - num_bytes + 1); + + if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) { + data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr); + } + } + + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + + return nfc::STATUS_OK; +} + +bool PN7160::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); +} + +uint16_t PN7160::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); + return data[2] * 8U; + } + return 0; +} + +uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return nfc::STATUS_FAILED; + } + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; + message_start_index = 2; + return nfc::STATUS_OK; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; + message_start_index = 7; + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::write_mifare_ultralight_tag_(std::vector &uid, + const std::shared_ptr &message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); + return nfc::STATUS_FAILED; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; + payload.insert(payload.end(), write_data.begin(), write_data.end()); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); + + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error writing page %u", page_num); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160_i2c/__init__.py b/esphome/components/pn7160_i2c/__init__.py new file mode 100644 index 0000000000..87c4719ca8 --- /dev/null +++ b/esphome/components/pn7160_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn7160 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7160"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn7160_i2c_ns = cg.esphome_ns.namespace("pn7160_i2c") +PN7160I2C = pn7160_i2c_ns.class_("PN7160I2C", pn7160.PN7160, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn7160.PN7160_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7160I2C), + } + ).extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7160.setup_pn7160(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.cpp b/esphome/components/pn7160_i2c/pn7160_i2c.cpp new file mode 100644 index 0000000000..7c6da9dd06 --- /dev/null +++ b/esphome/components/pn7160_i2c/pn7160_i2c.cpp @@ -0,0 +1,49 @@ +#include "pn7160_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pn7160_i2c { + +static const char *const TAG = "pn7160_i2c"; + +uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE)) { + return nfc::STATUS_FAILED; + } + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length)) { + return nfc::STATUS_FAILED; + } + } + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160I2C::write_nfcc(nfc::NciMessage &tx) { + if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +void PN7160I2C::dump_config() { + PN7160::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn7160_i2c +} // namespace esphome diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.h b/esphome/components/pn7160_i2c/pn7160_i2c.h new file mode 100644 index 0000000000..eb253085eb --- /dev/null +++ b/esphome/components/pn7160_i2c/pn7160_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn7160/pn7160.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace pn7160_i2c { + +class PN7160I2C : public pn7160::PN7160, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7160_i2c +} // namespace esphome diff --git a/esphome/components/pn7160_spi/__init__.py b/esphome/components/pn7160_spi/__init__.py new file mode 100644 index 0000000000..ae1235655a --- /dev/null +++ b/esphome/components/pn7160_spi/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, pn7160 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7160"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +pn7160_spi_ns = cg.esphome_ns.namespace("pn7160_spi") +PN7160Spi = pn7160_spi_ns.class_("PN7160Spi", pn7160.PN7160, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + pn7160.PN7160_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7160Spi), + } + ).extend(spi.spi_device_schema(cs_pin_required=True)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7160.setup_pn7160(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/pn7160_spi/pn7160_spi.cpp b/esphome/components/pn7160_spi/pn7160_spi.cpp new file mode 100644 index 0000000000..09f673f700 --- /dev/null +++ b/esphome/components/pn7160_spi/pn7160_spi.cpp @@ -0,0 +1,54 @@ +#include "pn7160_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160_spi { + +static const char *const TAG = "pn7160_spi"; + +void PN7160Spi::setup() { + this->spi_setup(); + this->cs_->digital_write(false); + PN7160::setup(); +} + +uint8_t PN7160Spi::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + this->enable(); + this->write_byte(TDD_SPI_READ); // send "transfer direction detector" + this->read_array(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE); + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + this->read_array(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length); + } + this->disable(); + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160Spi::write_nfcc(nfc::NciMessage &tx) { + this->enable(); + this->write_byte(TDD_SPI_WRITE); // send "transfer direction detector" + this->write_array(tx.encode().data(), tx.encode().size()); + this->disable(); + return nfc::STATUS_OK; +} + +void PN7160Spi::dump_config() { + PN7160::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +} // namespace pn7160_spi +} // namespace esphome diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h new file mode 100644 index 0000000000..7d4460a76d --- /dev/null +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/pn7160/pn7160.h" +#include "esphome/components/spi/spi.h" + +#include + +namespace esphome { +namespace pn7160_spi { + +static const uint8_t TDD_SPI_READ = 0xFF; +static const uint8_t TDD_SPI_WRITE = 0x0A; + +class PN7160Spi : public pn7160::PN7160, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7160_spi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index b77cff7619..59818bbde5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3388,6 +3388,43 @@ pn532_spi: pn532_i2c: i2c_id: i2c_bus +pn7160_i2c: + id: nfcc_pn7160_i2c + i2c_id: i2c_bus + dwl_req_pin: + allow_other_uses: true + number: GPIO17 + irq_pin: + allow_other_uses: true + number: GPIO35 + ven_pin: + allow_other_uses: true + number: GPIO16 + wkup_req_pin: + allow_other_uses: true + number: GPIO21 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + +pn7160_spi: + id: nfcc_pn7160_spi + cs_pin: + number: GPIO15 + dwl_req_pin: + allow_other_uses: true + number: GPIO17 + irq_pin: + allow_other_uses: true + number: GPIO35 + ven_pin: + allow_other_uses: true + number: GPIO16 + wkup_req_pin: + allow_other_uses: true + number: GPIO21 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + rdm6300: uart_id: uart_0 From 76a6e288b6e7667f8620b85ea1160f9a89307c20 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 13 Dec 2023 17:27:35 -0600 Subject: [PATCH 31/35] Add support for PN7150 (#5487) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/pn7150/__init__.py | 215 ++++ esphome/components/pn7150/automation.h | 82 ++ esphome/components/pn7150/pn7150.cpp | 1137 +++++++++++++++++ esphome/components/pn7150/pn7150.h | 296 +++++ .../pn7150/pn7150_mifare_classic.cpp | 322 +++++ .../pn7150/pn7150_mifare_ultralight.cpp | 186 +++ esphome/components/pn7150_i2c/__init__.py | 25 + esphome/components/pn7150_i2c/pn7150_i2c.cpp | 49 + esphome/components/pn7150_i2c/pn7150_i2c.h | 22 + tests/test1.yaml | 9 + 11 files changed, 2345 insertions(+) create mode 100644 esphome/components/pn7150/__init__.py create mode 100644 esphome/components/pn7150/automation.h create mode 100644 esphome/components/pn7150/pn7150.cpp create mode 100644 esphome/components/pn7150/pn7150.h create mode 100644 esphome/components/pn7150/pn7150_mifare_classic.cpp create mode 100644 esphome/components/pn7150/pn7150_mifare_ultralight.cpp create mode 100644 esphome/components/pn7150_i2c/__init__.py create mode 100644 esphome/components/pn7150_i2c/pn7150_i2c.cpp create mode 100644 esphome/components/pn7150_i2c/pn7150_i2c.h diff --git a/CODEOWNERS b/CODEOWNERS index 2269d580e4..320a23ffaa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,8 @@ esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn7150/* @jesserockz @kbx81 +esphome/components/pn7150_i2c/* @jesserockz @kbx81 esphome/components/pn7160/* @jesserockz @kbx81 esphome/components/pn7160_i2c/* @jesserockz @kbx81 esphome/components/pn7160_spi/* @jesserockz @kbx81 diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py new file mode 100644 index 0000000000..3b80b574e9 --- /dev/null +++ b/esphome/components/pn7150/__init__.py @@ -0,0 +1,215 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import nfc +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["binary_sensor", "nfc"] +CODEOWNERS = ["@kbx81", "@jesserockz"] + +CONF_EMULATION_MESSAGE = "emulation_message" +CONF_EMULATION_OFF = "emulation_off" +CONF_EMULATION_ON = "emulation_on" +CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" +CONF_MESSAGE = "message" +CONF_ON_FINISHED_WRITE = "on_finished_write" +CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan" +CONF_PN7150_ID = "pn7150_id" +CONF_POLLING_OFF = "polling_off" +CONF_POLLING_ON = "polling_on" +CONF_SET_CLEAN_MODE = "set_clean_mode" +CONF_SET_EMULATION_MESSAGE = "set_emulation_message" +CONF_SET_FORMAT_MODE = "set_format_mode" +CONF_SET_READ_MODE = "set_read_mode" +CONF_SET_WRITE_MESSAGE = "set_write_message" +CONF_SET_WRITE_MODE = "set_write_mode" +CONF_TAG_TTL = "tag_ttl" +CONF_VEN_PIN = "ven_pin" + +pn7150_ns = cg.esphome_ns.namespace("pn7150") +PN7150 = pn7150_ns.class_("PN7150", cg.Component) + +EmulationOffAction = pn7150_ns.class_("EmulationOffAction", automation.Action) +EmulationOnAction = pn7150_ns.class_("EmulationOnAction", automation.Action) +PollingOffAction = pn7150_ns.class_("PollingOffAction", automation.Action) +PollingOnAction = pn7150_ns.class_("PollingOnAction", automation.Action) +SetCleanModeAction = pn7150_ns.class_("SetCleanModeAction", automation.Action) +SetEmulationMessageAction = pn7150_ns.class_( + "SetEmulationMessageAction", automation.Action +) +SetFormatModeAction = pn7150_ns.class_("SetFormatModeAction", automation.Action) +SetReadModeAction = pn7150_ns.class_("SetReadModeAction", automation.Action) +SetWriteMessageAction = pn7150_ns.class_("SetWriteMessageAction", automation.Action) +SetWriteModeAction = pn7150_ns.class_("SetWriteModeAction", automation.Action) + + +PN7150OnEmulatedTagScanTrigger = pn7150_ns.class_( + "PN7150OnEmulatedTagScanTrigger", automation.Trigger.template() +) + +PN7150OnFinishedWriteTrigger = pn7150_ns.class_( + "PN7150OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN7150IsWritingCondition = pn7150_ns.class_( + "PN7150IsWritingCondition", automation.Condition +) + + +IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition) + + +SIMPLE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PN7150), + } +) + +SET_MESSAGE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7150), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean, + } +) + +PN7150_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN7150), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7150OnEmulatedTagScanTrigger + ), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7150OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EMULATION_MESSAGE): cv.string, + cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "tag.set_emulation_message", + SetEmulationMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +@automation.register_action( + "tag.set_write_message", + SetWriteMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +async def pn7150_set_message_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + template_ = await cg.templatable( + config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_ + ) + cg.add(var.set_include_android_app_record(template_)) + return var + + +@automation.register_action( + "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action( + "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA +) +async def pn7150_simple_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def setup_pn7150(var, config): + await cg.register_component(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN]) + cg.add(var.set_ven_pin(pin)) + + if emulation_message_config := config.get(CONF_EMULATION_MESSAGE): + cg.add(var.set_tag_emulation_message(emulation_message_config)) + cg.add(var.set_tag_emulation_on()) + + if CONF_TAG_TTL in config: + cg.add(var.set_tag_ttl(config[CONF_TAG_TTL])) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn7150.is_writing", + PN7150IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7150), + } + ), +) +async def pn7150_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h new file mode 100644 index 0000000000..aebb1b7573 --- /dev/null +++ b/esphome/components/pn7150/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/pn7150/pn7150.h" + +namespace esphome { +namespace pn7150 { + +class PN7150OnEmulatedTagScanTrigger : public Trigger<> { + public: + explicit PN7150OnEmulatedTagScanTrigger(PN7150 *parent) { + parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); + } +}; + +class PN7150OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN7150OnFinishedWriteTrigger(PN7150 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN7150IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } +}; + +template class EmulationOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } +}; + +template class EmulationOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } +}; + +template class PollingOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_off(); } +}; + +template class PollingOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_on(); } +}; + +template class SetCleanModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->clean_mode(); } +}; + +template class SetFormatModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->format_mode(); } +}; + +template class SetReadModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->read_mode(); } +}; + +template class SetEmulationMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_write_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->write_mode(); } +}; + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp new file mode 100644 index 0000000000..6703ab6a12 --- /dev/null +++ b/esphome/components/pn7150/pn7150.cpp @@ -0,0 +1,1137 @@ +#include "automation.h" +#include "pn7150.h" + +#include + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150"; + +void PN7150::setup() { + this->irq_pin_->setup(); + this->ven_pin_->setup(); + + this->nci_fsm_transition_(); // kick off reset & init processes +} + +void PN7150::dump_config() { + ESP_LOGCONFIG(TAG, "PN7150:"); + LOG_PIN(" IRQ pin: ", this->irq_pin_); + LOG_PIN(" VEN pin: ", this->ven_pin_); +} + +void PN7150::loop() { + this->nci_fsm_transition_(); + this->purge_old_tags_(); +} + +void PN7150::set_tag_emulation_message(std::shared_ptr message) { + this->card_emulation_message_ = std::move(message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7150::set_tag_emulation_message(const optional &message, + const optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->card_emulation_message_ = std::move(ndef_message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7150::set_tag_emulation_message(const char *message, const bool include_android_app_record) { + this->set_tag_emulation_message(std::string(message), include_android_app_record); +} + +void PN7150::set_tag_emulation_off() { + if (this->listening_enabled_) { + this->listening_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation disabled"); +} + +void PN7150::set_tag_emulation_on() { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation cannot be enabled"); + return; + } + if (!this->listening_enabled_) { + this->listening_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation enabled"); +} + +void PN7150::set_polling_off() { + if (this->polling_enabled_) { + this->polling_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling disabled"); +} + +void PN7150::set_polling_on() { + if (!this->polling_enabled_) { + this->polling_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling enabled"); +} + +void PN7150::read_mode() { + this->next_task_ = EP_READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} + +void PN7150::clean_mode() { + this->next_task_ = EP_CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} + +void PN7150::format_mode() { + this->next_task_ = EP_FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} + +void PN7150::write_mode() { + if (this->next_task_message_to_write_ == nullptr) { + ESP_LOGW(TAG, "Message to write must be set before setting write mode"); + return; + } + + this->next_task_ = EP_WRITE; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +void PN7150::set_tag_write_message(std::shared_ptr message) { + this->next_task_message_to_write_ = std::move(message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +void PN7150::set_tag_write_message(optional message, optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->next_task_message_to_write_ = std::move(ndef_message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +uint8_t PN7150::set_test_mode(const TestMode test_mode, const std::vector &data, + std::vector &result) { + auto test_oid = TEST_PRBS_OID; + + switch (test_mode) { + case TestMode::TEST_PRBS: + // test_oid = TEST_PRBS_OID; + break; + + case TestMode::TEST_ANTENNA: + test_oid = TEST_ANTENNA_OID; + break; + + case TestMode::TEST_GET_REGISTER: + test_oid = TEST_GET_REGISTER_OID; + break; + + case TestMode::TEST_NONE: + default: + ESP_LOGD(TAG, "Exiting test mode"); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_OK; + } + + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::TEST); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, test_oid, data); + + ESP_LOGW(TAG, "Starting test mode, OID 0x%02X", test_oid); + auto status = this->transceive_(tx, rx, NFCC_INIT_TIMEOUT); + + if (status != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to start test mode, OID 0x%02X", test_oid); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + result.clear(); + } else { + result = rx.get_message(); + result.erase(result.begin(), result.begin() + 4); // remove NCI header + if (!result.empty()) { + ESP_LOGW(TAG, "Test results: %s", nfc::format_bytes(result).c_str()); + } + } + return status; +} + +uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { + if (power) { + this->ven_pin_->digital_write(true); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(true); + delay(NFCC_INIT_TIMEOUT); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_RESET_OID, + {(uint8_t) reset_config}); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending reset command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid reset response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return rx.get_simple_status_response(); + } + // verify reset response + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_RESPONSE)) || (!rx.message_length_is(3)) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] != 0x11) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] != (uint8_t) reset_config)) { + ESP_LOGE(TAG, "Reset response was malformed: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Configuration %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained"); + ESP_LOGD(TAG, "NCI version: %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::init_core_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_INIT_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending initialise command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid initialise response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + uint8_t manf_id = rx.get_message()[15 + rx.get_message()[8]]; + uint8_t hw_version = rx.get_message()[16 + rx.get_message()[8]]; + uint8_t rom_code_version = rx.get_message()[17 + rx.get_message()[8]]; + uint8_t flash_major_version = rx.get_message()[18 + rx.get_message()[8]]; + uint8_t flash_minor_version = rx.get_message()[19 + rx.get_message()[8]]; + + ESP_LOGD(TAG, "Manufacturer ID: 0x%02X", manf_id); + ESP_LOGD(TAG, "Hardware version: 0x%02X", hw_version); + ESP_LOGD(TAG, "ROM code version: 0x%02X", rom_code_version); + ESP_LOGD(TAG, "FLASH major version: 0x%02X", flash_major_version); + ESP_LOGD(TAG, "FLASH minor version: 0x%02X", flash_minor_version); + + return rx.get_simple_status_response(); +} + +uint8_t PN7150::send_init_config_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, nfc::NCI_CORE_SET_CONFIG_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error enabling proprietary extensions"); + return nfc::STATUS_FAILED; + } + + tx.set_message(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(std::begin(PMU_CFG), std::end(PMU_CFG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending PMU config"); + return nfc::STATUS_FAILED; + } + + return this->send_core_config_(); +} + +uint8_t PN7150::send_core_config_() { + const auto *core_config_begin = std::begin(CORE_CONFIG_SOLO); + const auto *core_config_end = std::end(CORE_CONFIG_SOLO); + this->core_config_is_solo_ = true; + + if (this->listening_enabled_ && this->polling_enabled_) { + core_config_begin = std::begin(CORE_CONFIG_RW_CE); + core_config_end = std::end(CORE_CONFIG_RW_CE); + this->core_config_is_solo_ = false; + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(core_config_begin, core_config_end)); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error sending core config"); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::refresh_core_config_() { + bool core_config_should_be_solo = !(this->listening_enabled_ && this->polling_enabled_); + + if (this->nci_state_ == NCIState::RFST_DISCOVERY) { + if (this->stop_discovery_() != nfc::STATUS_OK) { + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_FAILED; + } + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + + if (this->core_config_is_solo_ != core_config_should_be_solo) { + if (this->send_core_config_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to refresh core config"); + return nfc::STATUS_FAILED; + } + } + this->config_refresh_pending_ = false; + return nfc::STATUS_OK; +} + +uint8_t PN7150::set_discover_map_() { + std::vector discover_map = {sizeof(RF_DISCOVER_MAP_CONFIG) / 3}; + discover_map.insert(discover_map.end(), std::begin(RF_DISCOVER_MAP_CONFIG), std::end(RF_DISCOVER_MAP_CONFIG)); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_MAP_OID, discover_map); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending discover map poll config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::set_listen_mode_routing_() { + nfc::NciMessage rx; + nfc::NciMessage tx( + nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_SET_LISTEN_MODE_ROUTING_OID, + std::vector(std::begin(RF_LISTEN_MODE_ROUTING_CONFIG), std::end(RF_LISTEN_MODE_ROUTING_CONFIG))); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error setting listen mode routing config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::start_discovery_() { + const uint8_t *rf_discovery_config = RF_DISCOVERY_CONFIG; + uint8_t length = sizeof(RF_DISCOVERY_CONFIG); + + if (!this->listening_enabled_) { + length = sizeof(RF_DISCOVERY_POLL_CONFIG); + rf_discovery_config = RF_DISCOVERY_POLL_CONFIG; + } else if (!this->polling_enabled_) { + length = sizeof(RF_DISCOVERY_LISTEN_CONFIG); + rf_discovery_config = RF_DISCOVERY_LISTEN_CONFIG; + } + + std::vector discover_config = std::vector((length * 2) + 1); + + discover_config[0] = length; + for (uint8_t i = 0; i < length; i++) { + discover_config[(i * 2) + 1] = rf_discovery_config[i]; + discover_config[(i * 2) + 2] = 0x01; // RF Technology and Mode will be executed in every discovery period + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, discover_config); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + switch (rx.get_simple_status_response()) { + // in any of these cases, we are either already in or will remain in discovery, which satisfies the function call + case nfc::STATUS_OK: + case nfc::DISCOVERY_ALREADY_STARTED: + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + case nfc::DISCOVERY_TEAR_DOWN: + return nfc::STATUS_OK; + + default: + ESP_LOGE(TAG, "Error starting discovery"); + return nfc::STATUS_FAILED; + } + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::stop_discovery_() { return this->deactivate_(nfc::DEACTIVATION_TYPE_IDLE, NFCC_TAG_WRITE_TIMEOUT); } + +uint8_t PN7150::deactivate_(const uint8_t type, const uint16_t timeout) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {type}); + + auto status = this->transceive_(tx, rx, timeout); + // if (status != nfc::STATUS_OK) { + // ESP_LOGE(TAG, "Error sending deactivate type %u", type); + // return nfc::STATUS_FAILED; + // } + return status; +} + +void PN7150::select_endpoint_() { + if (this->discovered_endpoint_.empty()) { + ESP_LOGW(TAG, "No cached tags to select"); + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + return; + } + std::vector endpoint_data = {this->discovered_endpoint_[0].id, this->discovered_endpoint_[0].protocol, + 0x01}; // that last byte is the interface ID + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (!this->discovered_endpoint_[i].trig_called) { + endpoint_data = {this->discovered_endpoint_[i].id, this->discovered_endpoint_[i].protocol, + 0x01}; // that last byte is the interface ID + this->selecting_endpoint_ = i; + break; + } + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_SELECT_OID, endpoint_data); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error selecting endpoint"); + } else { + this->nci_fsm_set_state_(NCIState::EP_SELECTING); + } +} + +uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) { + uint8_t type = nfc::guess_tag_type(tag.get_uid().size()); + + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + ESP_LOGV(TAG, "Reading Mifare classic"); + return this->read_mifare_classic_tag_(tag); + + case nfc::TAG_TYPE_2: + ESP_LOGV(TAG, "Reading Mifare ultralight"); + return this->read_mifare_ultralight_tag_(tag); + + case nfc::TAG_TYPE_UNKNOWN: + default: + ESP_LOGV(TAG, "Cannot determine tag type"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::clean_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_mifare_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for cleaning"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::format_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_ndef_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for formatting"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::write_endpoint_(std::vector &uid, std::shared_ptr &message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->write_mifare_classic_tag_(message); + + case nfc::TAG_TYPE_2: + return this->write_mifare_ultralight_tag_(uid, message); + + default: + ESP_LOGE(TAG, "Unsupported tag for writing"); + break; + } + return nfc::STATUS_FAILED; +} + +std::unique_ptr PN7150::build_tag_(const uint8_t mode_tech, const std::vector &data) { + switch (mode_tech) { + case (nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA): { + uint8_t uid_length = data[2]; + if (!uid_length) { + ESP_LOGE(TAG, "UID length cannot be zero"); + return nullptr; + } + std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + const auto *tag_type_str = + nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; + return make_unique(uid, tag_type_str); + } + } + return nullptr; +} + +optional PN7150::find_tag_uid_(const std::vector &uid) { + if (!this->discovered_endpoint_.empty()) { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); + bool uid_match = (uid.size() == existing_tag_uid.size()); + + if (uid_match) { + for (size_t i = 0; i < uid.size(); i++) { + uid_match &= (uid[i] == existing_tag_uid[i]); + } + if (uid_match) { + return i; + } + } + } + } + return nullopt; +} + +void PN7150::purge_old_tags_() { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { + this->erase_tag_(i); + } + } +} + +void PN7150::erase_tag_(const uint8_t tag_index) { + if (tag_index < this->discovered_endpoint_.size()) { + for (auto *trigger : this->triggers_ontagremoved_) { + trigger->process(this->discovered_endpoint_[tag_index].tag); + } + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); + } +} + +void PN7150::nci_fsm_transition_() { + switch (this->nci_state_) { + case NCIState::NFCC_RESET: + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + // fall through + + case NCIState::NFCC_INIT: + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); + } + // fall through + + case NCIState::NFCC_CONFIG: + if (this->send_init_config_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to send initial config"); + this->nci_fsm_set_error_state_(NCIState::NFCC_CONFIG); + return; + } else { + this->config_refresh_pending_ = false; + this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); + } + // fall through + + case NCIState::NFCC_SET_DISCOVER_MAP: + if (this->set_discover_map_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set discover map"); + this->nci_fsm_set_error_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + } + // fall through + + case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: + if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set listen mode routing"); + this->nci_fsm_set_error_state_(NCIState::RFST_IDLE); + return; + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + // fall through + + case NCIState::RFST_IDLE: + if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { + this->stop_discovery_(); + } + + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + + if (!this->listening_enabled_ && !this->polling_enabled_) { + return; + } + + if (this->start_discovery_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to start discovery"); + this->nci_fsm_set_error_state_(NCIState::RFST_DISCOVERY); + } else { + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + } + return; + + case NCIState::RFST_W4_HOST_SELECT: + select_endpoint_(); + // fall through + + // All cases below are waiting for NOTIFICATION messages + case NCIState::RFST_DISCOVERY: + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + // fall through + + case NCIState::RFST_LISTEN_ACTIVE: + case NCIState::RFST_LISTEN_SLEEP: + case NCIState::RFST_POLL_ACTIVE: + case NCIState::EP_SELECTING: + case NCIState::EP_DEACTIVATING: + if (this->irq_pin_->digital_read()) { + this->process_message_(); + } + break; + + case NCIState::TEST: + case NCIState::FAILED: + case NCIState::NONE: + default: + return; + } +} + +void PN7150::nci_fsm_set_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_state_(%u)", (uint8_t) new_state); + this->nci_state_ = new_state; + this->nci_state_error_ = NCIState::NONE; + this->error_count_ = 0; + this->last_nci_state_change_ = millis(); +} + +bool PN7150::nci_fsm_set_error_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_error_state_(%u); error_count_ = %u", (uint8_t) new_state, this->error_count_); + this->nci_state_error_ = new_state; + if (this->error_count_++ > NFCC_MAX_ERROR_COUNT) { + if ((this->nci_state_error_ == NCIState::NFCC_RESET) || (this->nci_state_error_ == NCIState::NFCC_INIT) || + (this->nci_state_error_ == NCIState::NFCC_CONFIG)) { + ESP_LOGE(TAG, "Too many initialization failures -- check device connections"); + this->mark_failed(); + this->nci_fsm_set_state_(NCIState::FAILED); + } else { + ESP_LOGW(TAG, "Too many errors transitioning to state %u; resetting NFCC", (uint8_t) this->nci_state_error_); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + } + } + return this->error_count_ > NFCC_MAX_ERROR_COUNT; +} + +void PN7150::process_message_() { + nfc::NciMessage rx; + if (this->read_nfcc(rx, NFCC_DEFAULT_TIMEOUT) != nfc::STATUS_OK) { + return; // No data + } + + switch (rx.get_message_type()) { + case nfc::NCI_PKT_MT_CTRL_NOTIFICATION: + if (rx.get_gid() == nfc::RF_GID) { + switch (rx.get_oid()) { + case nfc::RF_INTF_ACTIVATED_OID: + ESP_LOGVV(TAG, "RF_INTF_ACTIVATED_OID"); + this->process_rf_intf_activated_oid_(rx); + return; + + case nfc::RF_DISCOVER_OID: + ESP_LOGVV(TAG, "RF_DISCOVER_OID"); + this->process_rf_discover_oid_(rx); + return; + + case nfc::RF_DEACTIVATE_OID: + ESP_LOGVV(TAG, "RF_DEACTIVATE_OID: type: 0x%02X, reason: 0x%02X", rx.get_message()[3], rx.get_message()[4]); + this->process_rf_deactivate_oid_(rx); + return; + + default: + ESP_LOGV(TAG, "Unimplemented RF OID received: 0x%02X", rx.get_oid()); + } + } else if (rx.get_gid() == nfc::NCI_CORE_GID) { + switch (rx.get_oid()) { + case nfc::NCI_CORE_GENERIC_ERROR_OID: + ESP_LOGV(TAG, "NCI_CORE_GENERIC_ERROR_OID:"); + switch (rx.get_simple_status_response()) { + case nfc::DISCOVERY_ALREADY_STARTED: + ESP_LOGV(TAG, " DISCOVERY_ALREADY_STARTED"); + break; + + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + // Tag removed too soon + ESP_LOGV(TAG, " DISCOVERY_TARGET_ACTIVATION_FAILED"); + if (this->nci_state_ == NCIState::EP_SELECTING) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + if (!this->discovered_endpoint_.empty()) { + this->erase_tag_(this->selecting_endpoint_); + } + } else { + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + case nfc::DISCOVERY_TEAR_DOWN: + ESP_LOGV(TAG, " DISCOVERY_TEAR_DOWN"); + break; + + default: + ESP_LOGW(TAG, "Unknown error: 0x%02X", rx.get_simple_status_response()); + break; + } + break; + + default: + ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); + } + } else { + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + break; + + case nfc::NCI_PKT_MT_CTRL_RESPONSE: + ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), + nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_CTRL_COMMAND: + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_DATA: + this->process_data_message_(rx); + break; + + default: + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + } +} + +void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoint was activated + uint8_t discovery_id = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_DISCOVERY_ID); + uint8_t interface = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_INTERFACE); + uint8_t protocol = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_PROTOCOL); + uint8_t mode_tech = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MODE_TECH); + uint8_t max_size = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MAX_SIZE); + + ESP_LOGVV(TAG, "Endpoint activated -- interface: 0x%02X, protocol: 0x%02X, mode&tech: 0x%02X, max payload: %u", + interface, protocol, mode_tech, max_size); + + if (mode_tech & nfc::MODE_LISTEN_MASK) { + ESP_LOGVV(TAG, "Tag activated in listen mode"); + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_ACTIVE); + return; + } + + this->nci_fsm_set_state_(NCIState::RFST_POLL_ACTIVE); + auto incoming_tag = + this->build_tag_(mode_tech, std::vector(rx.get_message().begin() + 10, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = discovery_id; + this->discovered_endpoint_[tag_loc.value()].protocol = protocol; + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag cache updated"); + } else { + this->discovered_endpoint_.emplace_back( + DiscoveredEndpoint{discovery_id, protocol, millis(), std::move(incoming_tag), false}); + tag_loc = this->discovered_endpoint_.size() - 1; + ESP_LOGVV(TAG, "Tag added to cache"); + } + + auto &working_endpoint = this->discovered_endpoint_[tag_loc.value()]; + + switch (this->next_task_) { + case EP_CLEAN: + ESP_LOGD(TAG, " Tag cleaning..."); + if (this->clean_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag cleaning incomplete"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + break; + + case EP_FORMAT: + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + break; + + case EP_WRITE: + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag could not be formatted for writing"); + } else { + ESP_LOGD(TAG, " Writing NDEF data"); + if (this->write_endpoint_(working_endpoint.tag->get_uid(), this->next_task_message_to_write_) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + break; + + case EP_READ: + default: + if (!working_endpoint.trig_called) { + ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), + nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { + ESP_LOGW(TAG, " Unable to read NDEF record(s)"); + } else if (working_endpoint.tag->has_ndef_message()) { + const auto message = working_endpoint.tag->get_ndef_message(); + const auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF record(s):"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } else { + ESP_LOGW(TAG, " No NDEF records found"); + } + for (auto *trigger : this->triggers_ontag_) { + trigger->process(working_endpoint.tag); + } + working_endpoint.trig_called = true; + break; + } + } + if (working_endpoint.tag->get_tag_type() == nfc::MIFARE_CLASSIC) { + this->halt_mifare_classic_tag_(); + } + } + if (this->next_task_ != EP_READ) { + this->read_mode(); + } + + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::EP_DEACTIVATING); +} + +void PN7150::process_rf_discover_oid_(nfc::NciMessage &rx) { + auto incoming_tag = this->build_tag_(rx.get_message_byte(nfc::RF_DISCOVER_NTF_MODE_TECH), + std::vector(rx.get_message().begin() + 7, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag!"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID); + this->discovered_endpoint_[tag_loc.value()].protocol = rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL); + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag found & updated"); + } else { + this->discovered_endpoint_.emplace_back(DiscoveredEndpoint{rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID), + rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL), + millis(), std::move(incoming_tag), false}); + ESP_LOGVV(TAG, "Tag saved"); + } + } + + if (rx.get_message().back() != nfc::RF_DISCOVER_NTF_NT_MORE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + ESP_LOGVV(TAG, "Discovered %u endpoints", this->discovered_endpoint_.size()); + } +} + +void PN7150::process_rf_deactivate_oid_(nfc::NciMessage &rx) { + this->ce_state_ = CardEmulationState::CARD_EMU_IDLE; + + switch (rx.get_simple_status_response()) { + case nfc::DEACTIVATION_TYPE_DISCOVERY: + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + break; + + case nfc::DEACTIVATION_TYPE_IDLE: + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + break; + + case nfc::DEACTIVATION_TYPE_SLEEP: + case nfc::DEACTIVATION_TYPE_SLEEP_AF: + if (this->nci_state_ == NCIState::RFST_LISTEN_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_SLEEP); + } else if (this->nci_state_ == NCIState::RFST_POLL_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + default: + break; + } +} + +void PN7150::process_data_message_(nfc::NciMessage &rx) { + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + + std::vector ndef_response; + this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); + + uint16_t ndef_response_size = ndef_response.size(); + if (!ndef_response_size) { + return; // no message returned, we cannot respond + } + + std::vector tx_msg = {nfc::NCI_PKT_MT_DATA, uint8_t((ndef_response_size & 0xFF00) >> 8), + uint8_t(ndef_response_size & 0x00FF)}; + tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); + nfc::NciMessage tx(tx_msg); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending reply for card emulation failed"); + } +} + +void PN7150::card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response) { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation not possible"); + ndef_response.clear(); + return; + } + + if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_APP_SELECT))) { + // CARD_EMU_T4T_APP_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_APP_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_APP_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_CC_SELECT))) { + // CARD_EMU_T4T_CC_SELECT + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_APP_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_CC_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_CC_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_NDEF_SELECT))) { + // CARD_EMU_T4T_NDEF_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_READ), + std::begin(CARD_EMU_T4T_READ))) { + // CARD_EMU_T4T_READ + if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED"); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + if (length <= (sizeof(CARD_EMU_T4T_CC) + offset + 2)) { + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_CC) + offset, + std::begin(CARD_EMU_T4T_CC) + offset + length); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED"); + auto ndef_message = this->card_emulation_message_->encode(); + uint16_t ndef_msg_size = ndef_message.size(); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + + if (length <= (ndef_msg_size + offset + 2)) { + if (offset == 0) { + ndef_response.resize(2); + ndef_response[0] = (ndef_msg_size & 0xFF00) >> 8; + ndef_response[1] = (ndef_msg_size & 0x00FF); + if (length > 2) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 2); + } + } else if (offset == 1) { + ndef_response.resize(1); + ndef_response[0] = (ndef_msg_size & 0x00FF); + if (length > 1) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 1); + } + } else { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length); + } + + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + + if ((offset + length) >= (ndef_msg_size + 2)) { + ESP_LOGD(TAG, "NDEF message sent"); + this->on_emulated_tag_scan_callback_.call(); + } + } + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_WRITE), + std::begin(CARD_EMU_T4T_WRITE))) { + // CARD_EMU_T4T_WRITE + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_T4T_WRITE"); + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + std::vector ndef_msg_written; + + ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } +} + +uint8_t PN7150::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, + const bool expect_notification) { + uint8_t retries = NFCC_MAX_COMM_FAILS; + + while (retries) { + // first, send the message we need to send + if (this->write_nfcc(tx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + // next, the NFCC should send back a response + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error receiving message"); + if (!retries--) { + ESP_LOGE(TAG, " ...giving up"); + return nfc::STATUS_FAILED; + } + } else { + break; + } + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + // validate the response based on the message type that was sent (command vs. data) + if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { + // for commands, the GID and OID should match and the status should be OK + if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + return rx.get_simple_status_response(); + } else { + // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || + (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (expect_notification) { + // if the NFCC said "OK", there will be additional data to read; this comes back in a notification message + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error receiving data from endpoint"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + + return nfc::STATUS_OK; + } +} + +uint8_t PN7150::wait_for_irq_(uint16_t timeout, bool pin_state) { + auto start_time = millis(); + + while (millis() - start_time < timeout) { + if (this->irq_pin_->digital_read() == pin_state) { + return nfc::STATUS_OK; + } + } + ESP_LOGW(TAG, "Timed out waiting for IRQ state"); + return nfc::STATUS_FAILED; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h new file mode 100644 index 0000000000..4aad4e1720 --- /dev/null +++ b/esphome/components/pn7150/pn7150.h @@ -0,0 +1,296 @@ +#pragma once + +#include "esphome/components/nfc/automation.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/nfc/nci_message.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_helpers.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace pn7150 { + +static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static const uint16_t NFCC_INIT_TIMEOUT = 50; +static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; + +static const uint8_t NFCC_MAX_COMM_FAILS = 3; +static const uint8_t NFCC_MAX_ERROR_COUNT = 10; + +static const uint8_t XCHG_DATA_OID = 0x10; +static const uint8_t MF_SECTORSEL_OID = 0x32; +static const uint8_t MFC_AUTHENTICATE_OID = 0x40; +static const uint8_t TEST_PRBS_OID = 0x30; +static const uint8_t TEST_ANTENNA_OID = 0x3D; +static const uint8_t TEST_GET_REGISTER_OID = 0x33; + +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; + +static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; + +static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms + +static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms + +static const uint8_t PMU_CFG[] = { + 0x01, // Number of parameters + 0xA0, 0x0E, // ext. tag + 3, // length + 0x06, // VBAT1 connected to 5V (CFG2) + 0x64, // TVDD monitoring threshold = 5.0V; TxLDO voltage = 4.7V (in reader & card modes) + 0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2 +}; + +static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes + nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN, + nfc::INTF_ISODEP, // poll & listen mode + nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_TAGCMD}; // poll mode + +static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode + +static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 1, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x01, // power state + nfc::PROT_ISODEP}; // protocol + +enum class CardEmulationState : uint8_t { + CARD_EMU_IDLE, + CARD_EMU_NDEF_APP_SELECTED, + CARD_EMU_CC_SELECTED, + CARD_EMU_NDEF_SELECTED, + CARD_EMU_DESFIRE_PROD, +}; + +enum class NCIState : uint8_t { + NONE = 0x00, + NFCC_RESET, + NFCC_INIT, + NFCC_CONFIG, + NFCC_SET_DISCOVER_MAP, + NFCC_SET_LISTEN_MODE_ROUTING, + RFST_IDLE, + RFST_DISCOVERY, + RFST_W4_ALL_DISCOVERIES, + RFST_W4_HOST_SELECT, + RFST_LISTEN_ACTIVE, + RFST_LISTEN_SLEEP, + RFST_POLL_ACTIVE, + EP_DEACTIVATING, + EP_SELECTING, + TEST = 0XFE, + FAILED = 0XFF, +}; + +enum class TestMode : uint8_t { + TEST_NONE = 0x00, + TEST_PRBS, + TEST_ANTENNA, + TEST_GET_REGISTER, +}; + +struct DiscoveredEndpoint { + uint8_t id; + uint8_t protocol; + uint32_t last_seen; + std::unique_ptr tag; + bool trig_called; +}; + +class PN7150 : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } + void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; } + + void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; } + void set_tag_emulation_message(std::shared_ptr message); + void set_tag_emulation_message(const optional &message, optional include_android_app_record); + void set_tag_emulation_message(const char *message, bool include_android_app_record = true); + void set_tag_emulation_off(); + void set_tag_emulation_on(); + bool tag_emulation_enabled() { return this->listening_enabled_; } + + void set_polling_off(); + void set_polling_on(); + bool polling_enabled() { return this->polling_enabled_; } + + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_emulated_tag_scan_callback(std::function callback) { + this->on_emulated_tag_scan_callback_.add(std::move(callback)); + } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != EP_READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(); + void set_tag_write_message(std::shared_ptr message); + void set_tag_write_message(optional message, optional include_android_app_record); + + uint8_t set_test_mode(TestMode test_mode, const std::vector &data, std::vector &result); + + protected: + uint8_t reset_core_(bool reset_config, bool power); + uint8_t init_core_(); + uint8_t send_init_config_(); + uint8_t send_core_config_(); + uint8_t refresh_core_config_(); + + uint8_t set_discover_map_(); + + uint8_t set_listen_mode_routing_(); + + uint8_t start_discovery_(); + uint8_t stop_discovery_(); + uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT); + + void select_endpoint_(); + + uint8_t read_endpoint_data_(nfc::NfcTag &tag); + uint8_t clean_endpoint_(std::vector &uid); + uint8_t format_endpoint_(std::vector &uid); + uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + + std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); + optional find_tag_uid_(const std::vector &uid); + void purge_old_tags_(); + void erase_tag_(uint8_t tag_index); + + /// advance controller state as required + void nci_fsm_transition_(); + /// set new controller state + void nci_fsm_set_state_(NCIState new_state); + /// setting controller to this state caused an error; returns true if too many errors/failures + bool nci_fsm_set_error_state_(NCIState new_state); + /// parse & process incoming messages from the NFCC + void process_message_(); + void process_rf_intf_activated_oid_(nfc::NciMessage &rx); + void process_rf_discover_oid_(nfc::NciMessage &rx); + void process_rf_deactivate_oid_(nfc::NciMessage &rx); + void process_data_message_(nfc::NciMessage &rx); + + void card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response); + + uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT, + bool expect_notification = true); + virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0; + virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0; + + uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true); + + uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); + uint8_t sect_to_auth_(uint8_t block_num); + uint8_t format_mifare_classic_mifare_(); + uint8_t format_mifare_classic_ndef_(); + uint8_t write_mifare_classic_tag_(const std::shared_ptr &message); + uint8_t halt_mifare_classic_tag_(); + + uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); + uint16_t read_mifare_ultralight_capacity_(); + uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t clean_mifare_ultralight_(); + + enum NfcTask : uint8_t { + EP_READ = 0, + EP_CLEAN, + EP_FORMAT, + EP_WRITE, + } next_task_{EP_READ}; + + bool config_refresh_pending_{false}; + bool core_config_is_solo_{false}; + bool listening_enabled_{false}; + bool polling_enabled_{true}; + + uint8_t error_count_{0}; + uint8_t fail_count_{0}; + uint32_t last_nci_state_change_{0}; + uint8_t selecting_endpoint_{0}; + uint32_t tag_ttl_{250}; + + GPIOPin *irq_pin_{nullptr}; + GPIOPin *ven_pin_{nullptr}; + + CallbackManager on_emulated_tag_scan_callback_; + CallbackManager on_finished_write_callback_; + + std::vector discovered_endpoint_; + + CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE}; + NCIState nci_state_{NCIState::NFCC_RESET}; + NCIState nci_state_error_{NCIState::NONE}; + + std::shared_ptr card_emulation_message_; + std::shared_ptr next_task_message_to_write_; + + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; +}; + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp new file mode 100644 index 0000000000..0443929f69 --- /dev/null +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -0,0 +1,322 @@ +#include + +#include "pn7150.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150.mifare_classic"; + +uint8_t PN7150::read_mifare_classic_tag_(nfc::NfcTag &tag) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data"); + return nfc::STATUS_FAILED; + } + std::vector data; + + if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return nfc::STATUS_FAILED; + } + } else { + ESP_LOGE(TAG, "Failed to read block %u", current_block); + return nfc::STATUS_FAILED; + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Block authentication failed for %u", current_block); + return nfc::STATUS_FAILED; + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading block %u", current_block); + return nfc::STATUS_FAILED; + } else { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + + if (buffer.begin() + message_start_index < buffer.end()) { + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + } else { + return nfc::STATUS_FAILED; + } + + tag.set_ndef_message(make_unique(buffer)); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Timeout reading tag data"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (!rx.message_length_is(18))) { + ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1); + + ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes(data).c_str()); + return nfc::STATUS_OK; +} + +uint8_t PN7150::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num}); + + switch (key_num) { + case nfc::MIFARE_CMD_AUTH_A: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A; + break; + + case nfc::MIFARE_CMD_AUTH_B: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B; + break; + + default: + break; + } + + if (key != nullptr) { + tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY; + tx.get_message().insert(tx.get_message().end(), key, key + 6); + } + + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); + return nfc::STATUS_FAILED; + } + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || + (rx.get_message()[4] != nfc::STATUS_OK)) { + ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num); + return nfc::STATUS_OK; +} + +uint8_t PN7150::sect_to_auth_(const uint8_t block_num) { + const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + if (block_num >= first_high_block) { + return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) + + nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + } + return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW; +} + +uint8_t PN7150::format_mifare_classic_mifare_() { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + auto status = nfc::STATUS_OK; + + for (int block = 0; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + continue; + } + if (block != 0) { + if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + + return status; +} + +uint8_t PN7150::format_mifare_classic_ndef_() { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Sector 0 formatted with NDEF"); + + auto status = nfc::STATUS_OK; + + for (int block = 4; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (block == 4) { + if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } else { + if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + return status; +} + +uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + // write command part two + tx.set_payload({XCHG_DATA_OID}); + tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) { + ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr &message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::halt_mifare_classic_tag_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0}); + + ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp new file mode 100644 index 0000000000..791b0634d6 --- /dev/null +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "pn7150.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150.mifare_ultralight"; + +uint8_t PN7150::read_mifare_ultralight_tag_(nfc::NfcTag &tag) { + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); + return nfc::STATUS_FAILED; + } + + uint8_t message_length; + uint8_t message_start_index; + if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); + + if (message_length == 0) { + return nfc::STATUS_FAILED; + } + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + + tag.set_ndef_message(make_unique(data)); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page}); + + for (size_t i = 0; i * read_increment < num_bytes; i++) { + tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page; + do { // loop because sometimes we struggle here...???... + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } while (rx.get_payload_size() < read_increment); + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1 + : rx.get_message().end() - (bytes_offset - num_bytes + 1); + + if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) { + data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr); + } + } + + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + + return nfc::STATUS_OK; +} + +bool PN7150::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); +} + +uint16_t PN7150::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); + return data[2] * 8U; + } + return 0; +} + +uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return nfc::STATUS_FAILED; + } + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; + message_start_index = 2; + return nfc::STATUS_OK; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; + message_start_index = 7; + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::write_mifare_ultralight_tag_(std::vector &uid, + const std::shared_ptr &message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); + return nfc::STATUS_FAILED; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; + payload.insert(payload.end(), write_data.begin(), write_data.end()); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); + + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error writing page %u", page_num); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150_i2c/__init__.py b/esphome/components/pn7150_i2c/__init__.py new file mode 100644 index 0000000000..5f48a0f3cb --- /dev/null +++ b/esphome/components/pn7150_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn7150 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7150"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn7150_i2c_ns = cg.esphome_ns.namespace("pn7150_i2c") +PN7150I2C = pn7150_i2c_ns.class_("PN7150I2C", pn7150.PN7150, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn7150.PN7150_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7150I2C), + } + ).extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7150.setup_pn7150(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.cpp b/esphome/components/pn7150_i2c/pn7150_i2c.cpp new file mode 100644 index 0000000000..38b3102b37 --- /dev/null +++ b/esphome/components/pn7150_i2c/pn7150_i2c.cpp @@ -0,0 +1,49 @@ +#include "pn7150_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pn7150_i2c { + +static const char *const TAG = "pn7150_i2c"; + +uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE)) { + return nfc::STATUS_FAILED; + } + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length)) { + return nfc::STATUS_FAILED; + } + } + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7150::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150I2C::write_nfcc(nfc::NciMessage &tx) { + if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +void PN7150I2C::dump_config() { + PN7150::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn7150_i2c +} // namespace esphome diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.h b/esphome/components/pn7150_i2c/pn7150_i2c.h new file mode 100644 index 0000000000..9308dddd26 --- /dev/null +++ b/esphome/components/pn7150_i2c/pn7150_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn7150/pn7150.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace pn7150_i2c { + +class PN7150I2C : public pn7150::PN7150, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7150_i2c +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 59818bbde5..f7b433cce2 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3388,6 +3388,15 @@ pn532_spi: pn532_i2c: i2c_id: i2c_bus +pn7150_i2c: + i2c_id: i2c_bus + irq_pin: + allow_other_uses: true + number: GPIO32 + ven_pin: + allow_other_uses: true + number: GPIO16 + pn7160_i2c: id: nfcc_pn7160_i2c i2c_id: i2c_bus From 9f27eadaee108cde1410c9de65fa5a82bd555e55 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 08:30:45 +0900 Subject: [PATCH 32/35] Bump version to 2024.1.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 101a690db5..5de34b86cd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0-dev" +__version__ = "2024.1.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 777cdb1c21ec3b467f47cf71ffdc53ee9112b69a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:24:16 +0900 Subject: [PATCH 33/35] Allow use of CDC/JTAG loggers on esp32 variants with Arduino (#4658) * Allow use of CDC/JTAG loggers on esp32 variants with Arduino * Only on s2/s3 * Separate C3 from S2/S3 * C code builds & runs correctly, still needs work though * Works on S2 * It works! * Remove unnecessary header --------- Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 2 -- esphome/components/logger/logger.cpp | 32 ++++++++++++++++++++++++--- esphome/components/logger/logger.h | 2 -- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index e431997276..11a7f996f0 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -124,8 +124,6 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: - if value.upper() in ESP_IDF_UARTS and not CORE.using_esp_idf: - raise cv.Invalid(f"Only esp-idf framework supports {value}.") variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 2d2524b5f4..e0ca0806cb 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -236,8 +236,13 @@ void Logger::pre_setup() { this->hw_serial_ = &Serial1; Serial1.begin(this->baud_rate_); #else +#if ARDUINO_USB_CDC_ON_BOOT this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif #endif #ifdef USE_ESP8266 if (this->uart_ == UART_SELECTION_UART0_SWAP) { @@ -265,12 +270,35 @@ void Logger::pre_setup() { Serial2.begin(this->baud_rate_); break; #endif +#if defined(USE_ESP32) && \ + (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3)) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_CDC: +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_SERIAL_JTAG: +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 +#ifdef USE_ESP32_VARIANT_ESP32C3 + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif // USE_ESP32_VARIANT_ESP32C3 +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif // ARDUINO_USB_CDC_ON_BOOT +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 + break; +#endif // USE_ESP32 && (USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3) #ifdef USE_RP2040 case UART_SELECTION_USB_CDC: this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); break; -#endif +#endif // USE_RP2040 } #endif // USE_ARDUINO #ifdef USE_ESP_IDF @@ -393,14 +421,12 @@ const char *const UART_SELECTIONS[] = { "UART2", #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARINT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#if defined(USE_ESP_IDF) #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) "USB_CDC", #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) "USB_SERIAL_JTAG", #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 -#endif // USE_ESP_IDF }; #endif // USE_ESP32 #ifdef USE_ESP8266 diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 3816b1dd14..68efc056df 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -45,7 +45,6 @@ enum UARTSelection { UART_SELECTION_UART2, #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#ifdef USE_ESP_IDF #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) UART_SELECTION_USB_CDC, #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 @@ -54,7 +53,6 @@ enum UARTSelection { UART_SELECTION_USB_SERIAL_JTAG, #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || // USE_ESP32_VARIANT_ESP32H2 -#endif // USE_ESP_IDF #endif // USE_ESP32 #ifdef USE_ESP8266 UART_SELECTION_UART0_SWAP, From 8c37066ed90a5eba7cf7ca95783c6ce4c09ace7b Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 14 Dec 2023 05:47:31 +0100 Subject: [PATCH 34/35] [Logger] ESP32 S3 serial logger (#4853) * Add support for ESP32 S3 logger. * fix default * Remove cpp & h changes to combine with PR #4658 * Not enough attention to details. * Add build flag * Validation fix * Fix validation for real this time --------- Co-authored-by: Your Name Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 10 ++++- esphome/config_validation.py | 53 +++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 11a7f996f0..be302bd489 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -97,7 +97,7 @@ UART_SELECTION_LIBRETINY = { COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], } -ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] +ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] @@ -124,6 +124,8 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: + if CORE.using_arduino and value.upper() in ESP_ARDUINO_UNSUPPORTED_USB_UARTS: + raise cv.Invalid(f"Arduino framework does not support {value}.") variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) @@ -169,6 +171,8 @@ CONFIG_SCHEMA = cv.All( CONF_HARDWARE_UART, esp8266=UART0, esp32=UART0, + esp32_s2=USB_CDC, + esp32_s3=USB_CDC, rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, @@ -256,6 +260,10 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") + if CORE.using_arduino: + if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") + if CORE.using_esp_idf: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ad2ee11512..fdbe7d6320 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1528,6 +1528,12 @@ class SplitDefault(Optional): esp32=vol.UNDEFINED, esp32_arduino=vol.UNDEFINED, esp32_idf=vol.UNDEFINED, + esp32_s2=vol.UNDEFINED, + esp32_s2_arduino=vol.UNDEFINED, + esp32_s2_idf=vol.UNDEFINED, + esp32_s3=vol.UNDEFINED, + esp32_s3_arduino=vol.UNDEFINED, + esp32_s3_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, @@ -1541,6 +1547,26 @@ class SplitDefault(Optional): self._esp32_idf_default = vol.default_factory( esp32_idf if esp32 is vol.UNDEFINED else esp32 ) + self._esp32_s2_arduino_default = vol.default_factory( + (esp32_s2_arduino if esp32 is vol.UNDEFINED else esp32) + if esp32_s2 is vol.UNDEFINED + else esp32_s2 + ) + self._esp32_s2_idf_default = vol.default_factory( + (esp32_s2_idf if esp32 is vol.UNDEFINED else esp32) + if esp32_s2 is vol.UNDEFINED + else esp32_s2 + ) + self._esp32_s3_arduino_default = vol.default_factory( + (esp32_s3_arduino if esp32 is vol.UNDEFINED else esp32) + if esp32_s3 is vol.UNDEFINED + else esp32_s3 + ) + self._esp32_s3_idf_default = vol.default_factory( + (esp32_s3_idf if esp32 is vol.UNDEFINED else esp32) + if esp32_s3 is vol.UNDEFINED + else esp32_s3 + ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) self._rtl87xx_default = vol.default_factory(rtl87xx) @@ -1550,10 +1576,29 @@ class SplitDefault(Optional): def default(self): if CORE.is_esp8266: return self._esp8266_default - if CORE.is_esp32 and CORE.using_arduino: - return self._esp32_arduino_default - if CORE.is_esp32 and CORE.using_esp_idf: - return self._esp32_idf_default + if CORE.is_esp32: + from esphome.components.esp32 import get_esp32_variant + from esphome.components.esp32.const import ( + VARIANT_ESP32S2, + VARIANT_ESP32S3, + ) + + variant = get_esp32_variant() + if variant == VARIANT_ESP32S2: + if CORE.using_arduino: + return self._esp32_s2_arduino_default + if CORE.using_esp_idf: + return self._esp32_s2_idf_default + elif variant == VARIANT_ESP32S3: + if CORE.using_arduino: + return self._esp32_s3_arduino_default + if CORE.using_esp_idf: + return self._esp32_s3_idf_default + else: + if CORE.using_arduino: + return self._esp32_arduino_default + if CORE.using_esp_idf: + return self._esp32_idf_default if CORE.is_rp2040: return self._rp2040_default if CORE.is_bk72xx: From a3cc5518565c56239808b34e3881090c5385632a Mon Sep 17 00:00:00 2001 From: jochenvg Date: Thu, 14 Dec 2023 05:01:01 +0000 Subject: [PATCH 35/35] Support toggle action for template cover (#5917) --- esphome/components/template/cover/__init__.py | 7 +++++++ esphome/components/template/cover/template_cover.cpp | 10 ++++++++++ esphome/components/template/cover/template_cover.h | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 8844ddd6ab..43d0be99b4 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -31,6 +31,7 @@ RESTORE_MODES = { } CONF_HAS_POSITION = "has_position" +CONF_TOGGLE_ACTION = "toggle_action" CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( { @@ -44,6 +45,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( RESTORE_MODES, upper=True @@ -74,6 +76,11 @@ async def to_code(config): var.get_stop_trigger(), [], config[CONF_STOP_ACTION] ) cg.add(var.set_has_stop(True)) + if CONF_TOGGLE_ACTION in config: + await automation.build_automation( + var.get_toggle_trigger(), [], config[CONF_TOGGLE_ACTION] + ) + cg.add(var.set_has_toggle(True)) if CONF_TILT_ACTION in config: await automation.build_automation( var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index b16e439943..2d6c3087ae 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -12,6 +12,7 @@ TemplateCover::TemplateCover() : open_trigger_(new Trigger<>()), close_trigger_(new Trigger<>), stop_trigger_(new Trigger<>()), + toggle_trigger_(new Trigger<>()), position_trigger_(new Trigger()), tilt_trigger_(new Trigger()) {} void TemplateCover::setup() { @@ -68,6 +69,7 @@ float TemplateCover::get_setup_priority() const { return setup_priority::HARDWAR Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } +Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } void TemplateCover::control(const CoverCall &call) { if (call.get_stop()) { @@ -76,6 +78,12 @@ void TemplateCover::control(const CoverCall &call) { this->prev_command_trigger_ = this->stop_trigger_; this->publish_state(); } + if (call.get_toggle().has_value()) { + this->stop_prev_trigger_(); + this->toggle_trigger_->trigger(); + this->prev_command_trigger_ = this->toggle_trigger_; + this->publish_state(); + } if (call.get_position().has_value()) { auto pos = *call.get_position(); this->stop_prev_trigger_(); @@ -110,6 +118,7 @@ CoverTraits TemplateCover::get_traits() { auto traits = CoverTraits(); traits.set_is_assumed_state(this->assumed_state_); traits.set_supports_stop(this->has_stop_); + traits.set_supports_toggle(this->has_toggle_); traits.set_supports_position(this->has_position_); traits.set_supports_tilt(this->has_tilt_); return traits; @@ -118,6 +127,7 @@ Trigger *TemplateCover::get_position_trigger() const { return this->posit Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } +void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 4ff5caf1db..958c94b0a6 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -21,6 +21,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; + Trigger<> *get_toggle_trigger() const; Trigger *get_position_trigger() const; Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); @@ -29,6 +30,7 @@ class TemplateCover : public cover::Cover, public Component { void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); + void set_has_toggle(bool has_toggle); void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } void setup() override; @@ -50,7 +52,9 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *open_trigger_; Trigger<> *close_trigger_; bool has_stop_{false}; + bool has_toggle_{false}; Trigger<> *stop_trigger_; + Trigger<> *toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; Trigger *position_trigger_; bool has_position_{false};