diff --git a/CODEOWNERS b/CODEOWNERS index efc00db94d..6a46732dc1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -137,6 +137,7 @@ esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz +esphome/components/qr_code/* @wjtje esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index ac878b01e1..4ad353a254 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -252,6 +252,12 @@ void DisplayBuffer::legend(int x, int y, graph::Graph *graph, Color color_on) { } #endif // USE_GRAPH +#ifdef USE_QR_CODE +void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) { + qr_code->draw(this, x, y, color_on, scale); +} +#endif // USE_QR_CODE + void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width, int *height) { int x_offset, baseline; diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 096aaace4a..8ee1cd8779 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -14,6 +14,10 @@ #include "esphome/components/graph/graph.h" #endif +#ifdef USE_QR_CODE +#include "esphome/components/qr_code/qr_code.h" +#endif + namespace esphome { namespace display { @@ -307,6 +311,17 @@ class DisplayBuffer { void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON); #endif // USE_GRAPH +#ifdef USE_QR_CODE + /** Draw the `qr_code` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param qr_code The qr_code to draw + * @param color_on The color to replace in binary images for the on bits. + */ + void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); +#endif + /** 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/qr_code/__init__.py b/esphome/components/qr_code/__init__.py new file mode 100644 index 0000000000..855db86335 --- /dev/null +++ b/esphome/components/qr_code/__init__.py @@ -0,0 +1,41 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_VALUE + +CONF_SCALE = "scale" +CONF_ECC = "ecc" + +CODEOWNERS = ["@wjtje"] + +DEPENDENCIES = ["display"] +MULTI_CONF = True + +qr_code_ns = cg.esphome_ns.namespace("qr_code") +QRCode = qr_code_ns.class_("QrCode", cg.Component) + +qrcodegen_Ecc = cg.esphome_ns.enum("qrcodegen_Ecc") +ECC = { + "LOW": qrcodegen_Ecc.qrcodegen_Ecc_LOW, + "MEDIUM": qrcodegen_Ecc.qrcodegen_Ecc_MEDIUM, + "QUARTILE": qrcodegen_Ecc.qrcodegen_Ecc_QUARTILE, + "HIGH": qrcodegen_Ecc.qrcodegen_Ecc_HIGH, +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(QRCode), + cv.Required(CONF_VALUE): cv.string, + cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True), + } +) + + +async def to_code(config): + cg.add_library("wjtje/qr-code-generator-library", "^1.7.0") + + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_value(config[CONF_VALUE])) + cg.add(var.set_ecc(ECC[config[CONF_ECC]])) + await cg.register_component(var, config) + + cg.add_define("USE_QR_CODE") diff --git a/esphome/components/qr_code/qr_code.cpp b/esphome/components/qr_code/qr_code.cpp new file mode 100644 index 0000000000..a2efbdb804 --- /dev/null +++ b/esphome/components/qr_code/qr_code.cpp @@ -0,0 +1,55 @@ +#include "qr_code.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/core/color.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace qr_code { + +static const char *const TAG = "qr_code"; + +void QrCode::dump_config() { + ESP_LOGCONFIG(TAG, "QR code:"); + ESP_LOGCONFIG(TAG, " Value: '%s'", this->value_.c_str()); +} + +void QrCode::set_value(const std::string &value) { + this->value_ = value; + this->needs_update_ = true; +} + +void QrCode::set_ecc(qrcodegen_Ecc ecc) { + this->ecc_ = ecc; + this->needs_update_ = true; +} + +void QrCode::generate_qr_code() { + ESP_LOGV(TAG, "Generating QR code..."); + uint8_t tempbuffer[qrcodegen_BUFFER_LEN_MAX]; + + if (!qrcodegen_encodeText(this->value_.c_str(), tempbuffer, this->qr_, this->ecc_, qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true)) { + ESP_LOGE(TAG, "Failed to generate QR code"); + } +} + +void QrCode::draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale) { + ESP_LOGV(TAG, "Drawing QR code at (%d, %d)", x_offset, y_offset); + + if (this->needs_update_) { + this->generate_qr_code(); + this->needs_update_ = false; + } + + uint8_t qrcode_width = qrcodegen_getSize(this->qr_); + + for (int y = 0; y < qrcode_width * scale; y++) { + for (int x = 0; x < qrcode_width * scale; x++) { + if (qrcodegen_getModule(this->qr_, x / scale, y / scale)) { + buff->draw_pixel_at(x_offset + x, y_offset + y, color); + } + } + } +} +} // namespace qr_code +} // namespace esphome diff --git a/esphome/components/qr_code/qr_code.h b/esphome/components/qr_code/qr_code.h new file mode 100644 index 0000000000..58f3a70321 --- /dev/null +++ b/esphome/components/qr_code/qr_code.h @@ -0,0 +1,34 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/color.h" + +#include + +#include "qrcodegen.h" + +namespace esphome { +// forward declare DisplayBuffer +namespace display { +class DisplayBuffer; +} // namespace display + +namespace qr_code { +class QrCode : public Component { + public: + void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color, int scale); + + void dump_config() override; + + void set_value(const std::string &value); + void set_ecc(qrcodegen_Ecc ecc); + + void generate_qr_code(); + + protected: + std::string value_; + qrcodegen_Ecc ecc_; + bool needs_update_ = true; + uint8_t qr_[qrcodegen_BUFFER_LEN_MAX]; +}; +} // namespace qr_code +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ded98054b6..acdc5df815 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -32,6 +32,7 @@ #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK #define USE_POWER_SUPPLY +#define USE_QR_CODE #define USE_SELECT #define USE_SENSOR #define USE_STATUS_LED diff --git a/platformio.ini b/platformio.ini index e2081a3ff6..e7bf848f1b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -33,10 +33,11 @@ build_flags = ; This are common settings for all environments. [common] lib_deps = - esphome/noise-c@0.1.4 ; api - makuna/NeoPixelBus@2.6.9 ; neopixelbus - esphome/Improv@1.1.0 ; improv_serial / esp32_improv - bblanchon/ArduinoJson@6.18.5 ; json + esphome/noise-c@0.1.4 ; api + makuna/NeoPixelBus@2.6.9 ; neopixelbus + esphome/Improv@1.1.0 ; improv_serial / esp32_improv + bblanchon/ArduinoJson@6.18.5 ; json + wjtje/qr-code-generator-library@1.7.0 ; qr_code build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = diff --git a/tests/test1.yaml b/tests/test1.yaml index 0ded069638..40cd0d4827 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2154,6 +2154,7 @@ display: pages: - id: page1 lambda: |- + it.qr_code(0, 0, id(homepage_qr)); it.rectangle(0, 0, it.get_width(), it.get_height()); - id: page2 lambda: |- @@ -2577,3 +2578,7 @@ select: - one - two optimistic: true + +qr_code: + - id: homepage_qr + value: https://esphome.io/index.html diff --git a/tests/test3.yaml b/tests/test3.yaml index 2b00a3612e..32a3b1be3d 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1351,6 +1351,10 @@ daly_bms: update_interval: 20s uart_id: uart1 +qr_code: + - id: homepage_qr + value: https://esphome.io/index.html + button: - platform: output id: output_button