This commit is contained in:
guillempages 2024-05-02 13:54:25 +12:00 committed by GitHub
commit fb290e172a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1107 additions and 0 deletions

View File

@ -253,6 +253,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra
esphome/components/number/* @esphome/core
esphome/components/online_image/* @guillempages
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931

View File

@ -0,0 +1,184 @@
import logging
from esphome import automation
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_ESP8266_DISABLE_SSL_SUPPORT,
CONF_FORMAT,
CONF_ID,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_RESIZE,
CONF_URL,
)
from esphome.core import CORE
from esphome.components.image import (
Image_,
IMAGE_TYPE,
CONF_USE_TRANSPARENCY,
validate_cross_dependencies,
)
from esphome.components.http_request import (
CONF_FOLLOW_REDIRECTS,
CONF_REDIRECT_LIMIT,
CONF_TIMEOUT,
CONF_USERAGENT,
)
DEPENDENCIES = ["network", "display"]
AUTO_LOAD = ["image"]
CODEOWNERS = ["@guillempages"]
MULTI_CONF = True
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_ON_ERROR = "on_error"
_LOGGER = logging.getLogger(__name__)
online_image_ns = cg.esphome_ns.namespace("online_image")
ImageFormat = online_image_ns.enum("ImageFormat")
IMAGE_FORMAT = {"PNG": ImageFormat.PNG} # Add new supported formats here
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
# Actions
SetUrlAction = online_image_ns.class_(
"OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage)
)
ReleaseImageAction = online_image_ns.class_(
"OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage)
)
# Triggers
DownloadFinishedTrigger = online_image_ns.class_(
"DownloadFinishedTrigger", automation.Trigger.template()
)
DownloadErrorTrigger = online_image_ns.class_(
"DownloadErrorTrigger", automation.Trigger.template()
)
ONLINE_IMAGE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
#
# Image options
#
cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
# Not setting default here on purpose; the default depends on the image type,
# and thus will be set in the "validate_cross_dependencies" validator.
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
#
# Online Image specific options
#
cv.Optional(CONF_FORMAT, default="PNG"): cv.enum(IMAGE_FORMAT, upper=True),
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger),
}
),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
}
),
#
# HTTP Request options
#
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
cv.Optional(CONF_TIMEOUT, "5s"): cv.positive_time_period_milliseconds,
cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string,
cv.Optional(CONF_ESP8266_DISABLE_SSL_SUPPORT, False): cv.boolean,
}
).extend(cv.polling_component_schema("never"))
CONFIG_SCHEMA = cv.Schema(
cv.All(
ONLINE_IMAGE_SCHEMA,
validate_cross_dependencies,
cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0),
),
)
)
SET_URL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(OnlineImage),
cv.Required(CONF_URL): cv.templatable(cv.url),
}
)
RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(OnlineImage),
}
)
@automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA)
@automation.register_action(
"online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA
)
async def online_image_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_URL in config:
template_ = await cg.templatable(config[CONF_URL], args, cg.const_char_ptr)
cg.add(var.set_url(template_))
return var
async def to_code(config):
cg.add_define("USE_ONLINE_IMAGE")
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
if CORE.is_esp32:
cg.add_library("WiFiClientSecure", None)
cg.add_library("HTTPClient", None)
if CORE.is_esp8266:
cg.add_library("ESP8266HTTPClient", None)
if config[CONF_FORMAT] in ["PNG"]:
cg.add_define("ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", None)
url = config[CONF_URL]
width, height = config.get(CONF_RESIZE, (0, 0))
transparent = config[CONF_USE_TRANSPARENCY]
var = cg.new_Pvariable(
config[CONF_ID],
url,
width,
height,
config[CONF_FORMAT],
config[CONF_TYPE],
config[CONF_BUFFER_SIZE],
)
await cg.register_component(var, config)
cg.add(var.set_transparency(transparent))
cg.add(
var.set_follow_redirects(
config[CONF_FOLLOW_REDIRECTS], config[CONF_REDIRECT_LIMIT]
)
)
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
cg.add(var.set_useragent(config[CONF_USERAGENT]))
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ERROR, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@ -0,0 +1,46 @@
#ifdef USE_ARDUINO
#include "image_decoder.h"
#include "online_image.h"
namespace esphome {
namespace online_image {
static const char *const TAG = "online_image.decoder";
void ImageDecoder::set_size(int width, int height) {
image_->resize_(width, height);
x_scale_ = static_cast<double>(image_->buffer_width_) / width;
y_scale_ = static_cast<double>(image_->buffer_height_) / height;
}
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
auto width = std::min(image_->buffer_width_, static_cast<int>(std::ceil((x + w) * x_scale_)));
auto height = std::min(image_->buffer_height_, static_cast<int>(std::ceil((y + h) * y_scale_)));
for (int i = x * x_scale_; i < width; i++) {
for (int j = y * y_scale_; j < height; j++) {
image_->draw_pixel_(i, j, color);
}
}
}
uint8_t *DownloadBuffer::data(size_t offset) {
if (offset > buffer_.size()) {
ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!");
return buffer_.data();
}
return buffer_.data() + offset;
}
size_t DownloadBuffer::read(size_t len) {
unread_ -= len;
if (unread_ > 0) {
memmove(data(), data(len), unread_);
}
return unread_;
}
} // namespace online_image
} // namespace esphome
#endif // USE_ARDUINO

View File

@ -0,0 +1,121 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include <HTTPClient.h>
#endif
#ifdef USE_ESP8266
#include <ESP8266HTTPClient.h>
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
#include <WiFiClientSecure.h>
#endif
#endif
#include "esphome/core/color.h"
namespace esphome {
namespace online_image {
class OnlineImage;
/**
* @brief Class to abstract decoding different image formats.
*/
class ImageDecoder {
public:
/**
* @brief Construct a new Image Decoder object
*
* @param image The image to decode the stream into.
*/
ImageDecoder(OnlineImage *image) : image_(image) {}
virtual ~ImageDecoder() = default;
/**
* @brief Initialize the decoder.
*
* @param stream WiFiClient to read the data from, in case the decoder needs initial data to auto-configure itself.
* @param download_size The total number of bytes that need to be download for the image.
*/
virtual void prepare(uint32_t download_size) { download_size_ = download_size; }
/**
* @brief Decode a part of the image. It will try reading from the buffer.
* There is no guarantee that the whole available buffer will be read/decoded;
* the method will return the amount of bytes actually decoded, so that the
* unread content can be moved to the beginning.
*
* @param buffer The buffer to read from.
* @param size The maximum amount of bytes that can be read from the buffer.
* @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully
* decode anything, or negative in case of a decoding error.
*/
virtual int decode(uint8_t *buffer, size_t size);
/**
* @brief Request the image to be resized once the actual dimensions are known.
* Called by the callback functions, to be able to access the parent Image class.
*
* @param width The image's width.
* @param height The image's height.
*/
void set_size(int width, int height);
/**
* @brief Draw a rectangle on the display_buffer using the defined color.
* Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly.
* In case of binary displays, the color will be converted to binary as well.
* Called by the callback functions, to be able to access the parent Image class.
*
* @param x The left-most coordinate of the rectangle.
* @param y The top-most coordinate of the rectangle.
* @param w The width of the rectangle.
* @param h The height of the rectangle.
* @param color The color to draw the rectangle with.
*/
void draw(int x, int y, int w, int h, const Color &color);
bool is_finished() const { return decoded_bytes_ == download_size_; }
protected:
OnlineImage *image_;
// Initializing to 1, to ensure it is different than initial "decoded_bytes_".
// Will be overwritten anyway once the download size is known.
uint32_t download_size_ = 1;
uint32_t decoded_bytes_ = 0;
double x_scale_ = 1.0;
double y_scale_ = 1.0;
};
class DownloadBuffer {
public:
DownloadBuffer(size_t size) : buffer_(size) { reset(); }
uint8_t *data(size_t offset = 0);
uint8_t *append() { return data(unread_); }
size_t unread() const { return unread_; }
size_t size() const { return buffer_.size(); }
size_t free_capacity() const { return buffer_.size() - unread_; }
size_t read(size_t len);
size_t write(size_t len) {
unread_ += len;
return unread_;
}
void reset() { unread_ = 0; }
private:
std::vector<uint8_t> buffer_;
/** Total number of downloaded bytes not yet read. */
size_t unread_;
};
} // namespace online_image
} // namespace esphome
#endif // USE_ARDUINO

View File

@ -0,0 +1,428 @@
#ifdef USE_ARDUINO
#include "online_image.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
#include <HTTPClient.h>
#endif
#ifdef USE_ESP8266
#include <ESP8266HTTPClient.h>
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
#include <WiFiClientSecure.h>
#include <utility>
#endif
#endif
static const char *const TAG = "online_image";
#include "image_decoder.h"
#ifdef ONLINE_IMAGE_PNG_SUPPORT
#include "png_image.h"
#endif
namespace esphome {
namespace online_image {
using display::Display;
using image::ImageType;
inline bool is_color_on(const Color &color) {
// This produces the most accurate monochrome conversion, but is slightly slower.
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
// Approximation using fast integer computations; produces acceptable results
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
}
OnlineImage::OnlineImage(const char *url, int width, int height, ImageFormat format, ImageType type,
uint32_t download_buffer_size)
: Image(nullptr, 0, 0, type),
buffer_(nullptr),
url_(url),
download_buffer_(download_buffer_size),
format_(format),
fixed_width_(width),
fixed_height_(height) {}
void OnlineImage::draw(int x, int y, Display *display, Color color_on, Color color_off) {
switch (type_) {
case ImageType::IMAGE_TYPE_BINARY: {
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!this->transparent_) {
display->draw_pixel_at(x + img_x, y + img_y, color_off);
}
}
}
break;
}
case ImageType::IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_grayscale_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case ImageType::IMAGE_TYPE_RGB565:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb565_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case ImageType::IMAGE_TYPE_RGB24:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb24_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case ImageType::IMAGE_TYPE_RGBA:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgba_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
}
}
void OnlineImage::set_follow_redirects(bool follow, int limit) {
#if defined(USE_ESP32) || defined(USE_ESP8266)
if (follow) {
http_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
} else {
http_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
}
http_.setRedirectLimit(limit);
#endif
}
void OnlineImage::set_useragent(const char *useragent) {
if (useragent != nullptr) {
http_.setUserAgent(useragent);
}
}
void OnlineImage::release() {
if (buffer_) {
ESP_LOGD(TAG, "Deallocating old buffer...");
allocator_.deallocate(buffer_, get_buffer_size_());
buffer_ = nullptr;
width_ = 0;
height_ = 0;
buffer_width_ = 0;
buffer_height_ = 0;
etag_ = "";
end_connection_();
}
}
bool OnlineImage::resize_(int width_in, int height_in) {
int width = fixed_width_;
int height = fixed_height_;
if (auto_resize_()) {
width = width_in;
height = height_in;
if (width_ != width && height_ != height) {
release();
}
}
if (buffer_) {
return false;
}
auto new_size = get_buffer_size_(width, height);
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size);
delay_microseconds_safe(2000);
buffer_ = allocator_.allocate(new_size);
if (buffer_) {
buffer_width_ = width;
buffer_height_ = height;
width_ = width;
ESP_LOGD(TAG, "New size: (%d, %d)", width, height);
} else {
#if defined(USE_ESP8266)
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
int max_block = ESP.getMaxFreeBlockSize();
#elif defined(USE_ESP32)
int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
#endif
ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block);
end_connection_();
return false;
}
return true;
}
void OnlineImage::loop() {
PollingComponent::loop();
if (!decoder_) {
// Not decoding at the moment => nothing to do.
return;
}
if (!http_.connected() || decoder_->is_finished()) {
ESP_LOGD(TAG, "Image fully downloaded");
data_start_ = buffer_;
width_ = buffer_width_;
height_ = buffer_height_;
end_connection_();
download_finished_callback_.call();
return;
}
WiFiClient *stream = http_.getStreamPtr();
size_t available = stream->available();
if (available) {
if (available > download_buffer_.free_capacity()) {
available = download_buffer_.free_capacity();
}
auto len = stream->readBytes(download_buffer_.append(), available);
if (len > 0) {
download_buffer_.write(len);
auto fed = decoder_->decode(download_buffer_.data(), download_buffer_.unread());
if (fed < 0) {
ESP_LOGE(TAG, "Error when decoding image.");
etag_ = ""; // Need to re-download
end_connection_();
download_error_callback_.call();
return;
}
download_buffer_.read(fed);
}
}
}
void OnlineImage::draw_pixel_(int x, int y, Color color) {
if (!buffer_) {
ESP_LOGE(TAG, "Buffer not allocated!");
return;
}
if (x < 0 || y < 0 || x >= buffer_width_ || y >= buffer_height_) {
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
return;
}
uint32_t pos = get_position_(x, y);
switch (type_) {
case ImageType::IMAGE_TYPE_BINARY: {
uint32_t byte_num = pos;
uint8_t bit_num = (x + y * buffer_width_) % 8;
if ((has_transparency() && color.w > 127) || is_color_on(color)) {
buffer_[byte_num] |= 1 << bit_num;
} else {
buffer_[byte_num] &= ~(1 << bit_num);
}
break;
}
case ImageType::IMAGE_TYPE_GRAYSCALE: {
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
if (has_transparency()) {
if (gray == 1) {
gray = 0;
}
if (color.w < 0x80) {
gray = 1;
}
}
buffer_[pos] = gray;
break;
}
case ImageType::IMAGE_TYPE_RGB565: {
uint16_t col565 = display::ColorUtil::color_to_565(color);
if (has_transparency()) {
if (col565 == 0x0020) {
col565 = 0;
}
if (color.w < 0x80) {
col565 = 0x0020;
}
}
buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
break;
}
case ImageType::IMAGE_TYPE_RGBA: {
buffer_[pos + 0] = color.r;
buffer_[pos + 1] = color.g;
buffer_[pos + 2] = color.b;
buffer_[pos + 3] = color.w;
break;
}
case ImageType::IMAGE_TYPE_RGB24:
default: {
if (has_transparency()) {
if (color.b == 1 && color.r == 0 && color.g == 0) {
color.b = 0;
}
if (color.w < 0x80) {
color.r = 0;
color.g = 0;
color.b = 1;
}
}
buffer_[pos + 0] = color.r;
buffer_[pos + 1] = color.g;
buffer_[pos + 2] = color.b;
break;
}
}
}
bool OnlineImage::get_binary_pixel_(int x, int y) const {
uint32_t pos = x + y * width_;
uint8_t position_offset = pos % 8;
uint8_t mask = 1 << position_offset;
return buffer_[pos / 8] & mask;
}
Color OnlineImage::get_grayscale_pixel_(int x, int y) const {
auto pos = get_position_(x, y);
uint8_t grey = buffer_[pos];
uint8_t alpha;
if (grey == 1 && has_transparency()) {
alpha = 0;
} else {
alpha = 0xFF;
}
return Color(grey, grey, grey, alpha);
}
Color OnlineImage::get_rgb565_pixel_(int x, int y) const {
auto pos = get_position_(x, y);
uint16_t col565 = encode_uint16(buffer_[pos], buffer_[pos + 1]);
uint8_t alpha;
if (col565 == 0x0020 && has_transparency()) {
alpha = 0;
} else {
alpha = 0xFF;
}
return Color(static_cast<uint8_t>((col565 >> 8) & 0xF8), static_cast<uint8_t>((col565 & 0x7E0) >> 3),
static_cast<uint8_t>((col565 & 0x1F) << 3), alpha);
}
Color OnlineImage::get_color_pixel_(int x, int y) const {
auto pos = get_position_(x, y);
auto r = buffer_[pos + 0];
auto g = buffer_[pos + 1];
auto b = buffer_[pos + 2];
auto a = (b == 1 && r == 0 && g == 0 && has_transparency()) ? 0 : 0xFF;
return Color(r, g, b, a);
}
Color OnlineImage::get_rgba_pixel_(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) {
return Color(0);
}
auto pos = get_position_(x, y);
auto r = buffer_[pos + 0];
auto g = buffer_[pos + 1];
auto b = buffer_[pos + 2];
auto a = buffer_[pos + 3];
return Color(r, g, b, a);
}
void OnlineImage::end_connection_() {
http_.end();
decoder_.reset();
download_buffer_.reset();
}
void OnlineImage::update() {
if (decoder_) {
ESP_LOGW(TAG, "Image already being updated.");
return;
} else {
ESP_LOGI(TAG, "Updating image");
}
int begin_status = http_.begin(url_);
if (!begin_status) {
ESP_LOGE(TAG, "Could not download image from %s. Connection failed: %i", url_, begin_status);
return;
}
http_.setTimeout(timeout_);
#if defined(USE_ESP32)
http_.setConnectTimeout(timeout_);
#endif
if (etag_ != "") {
http_.addHeader("If-None-Match", etag_, false, true);
}
const char *header_keys[] = {"Content-Type", "Content-Length", "ETag"};
http_.collectHeaders(header_keys, 3);
int http_code = http_.GET();
if (http_code == HTTP_CODE_NOT_MODIFIED) {
ESP_LOGI(TAG, "Image hasn't changed on server. Skipping download.");
end_connection_();
return;
}
if (http_code != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Could not download image from %s. Error code: %i", url_, http_code);
end_connection_();
download_error_callback_.call();
return;
}
String content_type = http_.header("Content-Type");
String etag = http_.header("ETag");
uint32_t total_size = http_.header("Content-Length").toInt();
ESP_LOGD(TAG, "Content Type: %s", content_type.c_str());
ESP_LOGD(TAG, "Content Length: %d", total_size);
ESP_LOGD(TAG, "ETag: %s", etag.c_str());
if (etag != "" && etag == etag_) {
ESP_LOGI(TAG, "Image hasn't changed on server. Skipping download.");
end_connection_();
return;
}
etag_ = etag;
#ifdef ONLINE_IMAGE_PNG_SUPPORT
if (format_ == ImageFormat::PNG) {
decoder_ = esphome::make_unique<PngDecoder>(this);
}
#endif // ONLINE_IMAGE_PNG_SUPPORT
if (!decoder_) {
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported.");
end_connection_();
download_error_callback_.call();
return;
}
decoder_->prepare(total_size);
ESP_LOGI(TAG, "Downloading image from %s", url_);
}
void OnlineImage::add_on_finished_callback(std::function<void()> &&callback) {
download_finished_callback_.add(std::move(callback));
}
void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
download_error_callback_.add(std::move(callback));
}
} // namespace online_image
} // namespace esphome
#endif // USE_ARDUINO

View File

@ -0,0 +1,168 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/components/image/image.h"
#include "esphome/core/helpers.h"
#include "image_decoder.h"
namespace esphome {
namespace online_image {
/**
* @brief Format that the image is encoded with.
*/
enum ImageFormat {
/** Automatically detect from MIME type. */
AUTO,
/** JPEG format. Not supported yet. */
JPEG,
/** PNG format. */
PNG,
};
/**
* @brief Download an image from a given URL, and decode it using the specified decoder.
* The image will then be stored in a buffer, so that it can be displayed.
*/
class OnlineImage : public PollingComponent, public image::Image {
public:
/**
* @brief Construct a new Online Image object.
*
* @param url URL to download the image from.
* @param width Desired width of the target image area.
* @param height Desired height of the target image area.
* @param format Format that the image is encoded in (@see ImageFormat).
* @param buffer_size Size of the buffer used to download the image.
*/
OnlineImage(const char *url, int width, int height, ImageFormat format, image::ImageType type, uint32_t buffer_size);
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
void update() override;
void loop() override;
/** Set the URL to download the image from. */
void set_url(const char *url) { url_ = url; }
/**
* Release the buffer storing the image. The image will need to be downloaded again
* to be able to be displayed.
*/
void release();
void set_follow_redirects(bool follow, int limit);
void set_useragent(const char *useragent);
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
void add_on_finished_callback(std::function<void()> &&callback);
void add_on_error_callback(std::function<void()> &&callback);
protected:
bool get_binary_pixel_(int x, int y) const;
Color get_rgba_pixel_(int x, int y) const;
Color get_color_pixel_(int x, int y) const;
Color get_rgb565_pixel_(int x, int y) const;
Color get_grayscale_pixel_(int x, int y) const;
using Allocator = ExternalRAMAllocator<uint8_t>;
Allocator allocator_{Allocator::Flags::ALLOW_FAILURE};
uint32_t get_buffer_size_() const { return get_buffer_size_(buffer_width_, buffer_height_); }
int get_buffer_size_(int width, int height) const {
return std::ceil(image::image_type_to_bpp(type_) * width * height / 8.0);
}
int get_position_(int x, int y) const { return ((x + y * buffer_width_) * image::image_type_to_bpp(type_)) / 8; }
ALWAYS_INLINE bool auto_resize_() const { return fixed_width_ == 0 || fixed_height_ == 0; }
bool resize_(int width, int height);
void draw_pixel_(int x, int y, Color color);
void end_connection_();
CallbackManager<void()> download_finished_callback_{};
CallbackManager<void()> download_error_callback_{};
HTTPClient http_;
std::unique_ptr<ImageDecoder> decoder_;
uint8_t *buffer_;
const char *url_;
String etag_ = "";
DownloadBuffer download_buffer_;
const ImageFormat format_;
/** width requested on configuration, or 0 if non specified. */
const int fixed_width_;
/** height requested on configuration, or 0 if non specified. */
const int fixed_height_;
/**
* Actual width of the current image. If fixed_width_ is specified,
* this will be equal to it; otherwise it will be set once the decoding
* starts and the original size is known.
* This needs to be separate from "BaseImage::get_width()" because the latter
* must return 0 until the image has been decoded (to avoid showing partially
* decoded images).
*/
int buffer_width_;
/**
* Actual height of the current image. If fixed_height_ is specified,
* this will be equal to it; otherwise it will be set once the decoding
* starts and the original size is known.
* This needs to be separate from "BaseImage::get_height()" because the latter
* must return 0 until the image has been decoded (to avoid showing partially
* decoded images).
*/
int buffer_height_;
uint16_t timeout_;
friend void ImageDecoder::set_size(int width, int height);
friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color);
};
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
public:
OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(const char *, url)
void play(Ts... x) override {
this->parent_->set_url(this->url_.value(x...));
this->parent_->update();
}
protected:
OnlineImage *parent_;
};
template<typename... Ts> class OnlineImageReleaseAction : public Action<Ts...> {
public:
OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(const char *, url)
void play(Ts... x) override { this->parent_->release(); }
protected:
OnlineImage *parent_;
};
class DownloadFinishedTrigger : public Trigger<> {
public:
explicit DownloadFinishedTrigger(OnlineImage *parent) {
parent->add_on_finished_callback([this]() { this->trigger(); });
}
};
class DownloadErrorTrigger : public Trigger<> {
public:
explicit DownloadErrorTrigger(OnlineImage *parent) {
parent->add_on_error_callback([this]() { this->trigger(); });
}
};
} // namespace online_image
} // namespace esphome
#endif // USE_ARDUINO

View File

@ -0,0 +1,71 @@
#ifdef USE_ARDUINO
#include "png_image.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#ifdef ONLINE_IMAGE_PNG_SUPPORT
static const char *const TAG = "online_image.png";
namespace esphome {
namespace online_image {
/**
* @brief Callback method that will be called by the PNGLE engine when the basic
* data of the image is received (i.e. width and height);
*
* @param pngle The PNGLE object, including the context data.
* @param w The width of the image.
* @param h The height of the image.
*/
static void initCallback(pngle_t *pngle, uint32_t w, uint32_t h) {
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
decoder->set_size(w, h);
}
/**
* @brief Callback method that will be called by the PNGLE engine when a chunk
* of the image is decoded.
*
* @param pngle The PNGLE object, including the context data.
* @param x The X coordinate to draw the rectangle on.
* @param y The Y coordinate to draw the rectangle on.
* @param w The width of the rectangle to draw.
* @param h The height of the rectangle to draw.
* @param rgba The color to paint the rectangle in.
*/
static void drawCallback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) {
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
decoder->draw(x, y, w, h, color);
}
void PngDecoder::prepare(uint32_t download_size) {
ImageDecoder::prepare(download_size);
pngle_set_user_data(pngle, this);
pngle_set_init_callback(pngle, initCallback);
pngle_set_draw_callback(pngle, drawCallback);
}
int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
if (size < 256 && size < download_size_ - decoded_bytes_) {
ESP_LOGD(TAG, "Waiting for data");
return 0;
}
auto fed = pngle_feed(pngle, buffer, size);
if (fed < 0) {
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(pngle));
} else {
decoded_bytes_ += fed;
}
return fed;
}
} // namespace online_image
} // namespace esphome
#endif // ONLINE_IMAGE_PNG_SUPPORT
#endif // USE_ARDUINO

View File

@ -0,0 +1,37 @@
#pragma once
#ifdef USE_ARDUINO
#include "image_decoder.h"
#ifdef ONLINE_IMAGE_PNG_SUPPORT
#include <pngle.h>
namespace esphome {
namespace online_image {
/**
* @brief Image decoder specialization for PNG images.
*/
class PngDecoder : public ImageDecoder {
public:
/**
* @brief Construct a new PNG Decoder object.
*
* @param display The image to decode the stream into.
*/
PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle(pngle_new()) {}
virtual ~PngDecoder() { pngle_destroy(pngle); }
void prepare(uint32_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
private:
pngle_t *pngle;
};
} // namespace online_image
} // namespace esphome
#endif // ONLINE_IMAGE_PNG_SUPPORT
#endif // USE_ARDUINO

View File

@ -0,0 +1,51 @@
wifi:
ssid: MySSID
password: password1
spi:
- id: spi_main_lcd
clk_pin: 16
mosi_pin: 17
miso_pin: 15
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 12
dc_pin: 13
reset_pin: 21
lambda: |-
it.fill(Color(0, 0, 0));
it.image(0, 0, id(online_rgba_image));
# Purposely test that `online_image:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
online_image:
- id: online_binary_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
type: BINARY
resize: 50x50
- id: online_binary_transparent_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
type: TRANSPARENT_BINARY
format: png
- id: online_rgba_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
type: RGBA
- id: online_rgb24_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
type: RGB24
use_transparency: true
# Check the set_url action
time:
- platform: sntp
on_time:
- at: "13:37:42"
then:
- online_image.set_url:
id: online_rgba_image
url: http://www.example.org/example.png