mirror of
https://github.com/esphome/esphome.git
synced 2024-12-21 16:27:44 +01:00
Add runtime online image support (#4710)
This commit is contained in:
parent
b0d9800817
commit
9188836f70
@ -276,6 +276,7 @@ esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/one_wire/* @ssieb
|
||||
esphome/components/online_image/* @guillempages
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
|
161
esphome/components/online_image/__init__.py
Normal file
161
esphome/components/online_image/__init__.py
Normal file
@ -0,0 +1,161 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||
from esphome.components.image import (
|
||||
CONF_USE_TRANSPARENCY,
|
||||
IMAGE_TYPE,
|
||||
Image_,
|
||||
validate_cross_dependencies,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_FORMAT,
|
||||
CONF_ID,
|
||||
CONF_ON_ERROR,
|
||||
CONF_RESIZE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
DEPENDENCIES = ["display", "http_request"]
|
||||
CODEOWNERS = ["@guillempages"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||
|
||||
ImageFormat = online_image_ns.enum("ImageFormat")
|
||||
|
||||
FORMAT_PNG = "PNG"
|
||||
|
||||
IMAGE_FORMAT = {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),
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
#
|
||||
# Common image options
|
||||
#
|
||||
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.Required(CONF_URL): cv.url,
|
||||
cv.Required(CONF_FORMAT): 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),
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(cv.polling_component_schema("never"))
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
cv.All(
|
||||
ONLINE_IMAGE_SCHEMA,
|
||||
validate_cross_dependencies,
|
||||
cv.require_framework_version(
|
||||
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
|
||||
# esp8266_arduino=cv.Version(2, 7, 0),
|
||||
esp32_arduino=cv.Version(0, 0, 0),
|
||||
esp_idf=cv.Version(4, 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):
|
||||
format = config[CONF_FORMAT]
|
||||
if format in [FORMAT_PNG]:
|
||||
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
|
||||
cg.add_library("pngle", "1.0.2")
|
||||
|
||||
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,
|
||||
format,
|
||||
config[CONF_TYPE],
|
||||
config[CONF_BUFFER_SIZE],
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
|
||||
cg.add(var.set_transparency(transparent))
|
||||
|
||||
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)
|
44
esphome/components/online_image/image_decoder.cpp
Normal file
44
esphome/components/online_image/image_decoder.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
#include "image_decoder.h"
|
||||
#include "online_image.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
static const char *const TAG = "online_image.decoder";
|
||||
|
||||
void ImageDecoder::set_size(int width, int height) {
|
||||
this->image_->resize_(width, height);
|
||||
this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width;
|
||||
this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height;
|
||||
}
|
||||
|
||||
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
auto width = std::min(this->image_->buffer_width_, static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||
auto height = std::min(this->image_->buffer_height_, static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||
for (int i = x * this->x_scale_; i < width; i++) {
|
||||
for (int j = y * this->y_scale_; j < height; j++) {
|
||||
this->image_->draw_pixel_(i, j, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *DownloadBuffer::data(size_t offset) {
|
||||
if (offset > this->size_) {
|
||||
ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!");
|
||||
return this->buffer_;
|
||||
}
|
||||
return this->buffer_ + offset;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::read(size_t len) {
|
||||
this->unread_ -= len;
|
||||
if (this->unread_ > 0) {
|
||||
memmove(this->data(), this->data(len), this->unread_);
|
||||
}
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
112
esphome/components/online_image/image_decoder.h
Normal file
112
esphome/components/online_image/image_decoder.h
Normal file
@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#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 download_size The total number of bytes that need to be download for the image.
|
||||
*/
|
||||
virtual void prepare(uint32_t download_size) { this->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 this->decoded_bytes_ == this->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) : size_(size) {
|
||||
this->buffer_ = this->allocator_.allocate(size);
|
||||
this->reset();
|
||||
}
|
||||
|
||||
virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }
|
||||
|
||||
uint8_t *data(size_t offset = 0);
|
||||
|
||||
uint8_t *append() { return this->data(this->unread_); }
|
||||
|
||||
size_t unread() const { return this->unread_; }
|
||||
size_t size() const { return this->size_; }
|
||||
size_t free_capacity() const { return this->size_ - this->unread_; }
|
||||
|
||||
size_t read(size_t len);
|
||||
size_t write(size_t len) {
|
||||
this->unread_ += len;
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
void reset() { this->unread_ = 0; }
|
||||
|
||||
protected:
|
||||
ExternalRAMAllocator<uint8_t> allocator_;
|
||||
uint8_t *buffer_;
|
||||
size_t size_;
|
||||
/** Total number of downloaded bytes not yet read. */
|
||||
size_t unread_;
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
275
esphome/components/online_image/online_image.cpp
Normal file
275
esphome/components/online_image/online_image.cpp
Normal file
@ -0,0 +1,275 @@
|
||||
#include "online_image.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
static const char *const TAG = "online_image";
|
||||
|
||||
#include "image_decoder.h"
|
||||
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#include "png_image.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
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 std::string &url, int width, int height, ImageFormat format, ImageType type,
|
||||
uint32_t download_buffer_size)
|
||||
: Image(nullptr, 0, 0, type),
|
||||
buffer_(nullptr),
|
||||
download_buffer_(download_buffer_size),
|
||||
format_(format),
|
||||
fixed_width_(width),
|
||||
fixed_height_(height) {
|
||||
this->set_url(url);
|
||||
}
|
||||
|
||||
void OnlineImage::release() {
|
||||
if (this->buffer_) {
|
||||
ESP_LOGD(TAG, "Deallocating old buffer...");
|
||||
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
|
||||
this->data_start_ = nullptr;
|
||||
this->buffer_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->buffer_width_ = 0;
|
||||
this->buffer_height_ = 0;
|
||||
this->end_connection_();
|
||||
}
|
||||
}
|
||||
|
||||
bool OnlineImage::resize_(int width_in, int height_in) {
|
||||
int width = this->fixed_width_;
|
||||
int height = this->fixed_height_;
|
||||
if (this->auto_resize_()) {
|
||||
width = width_in;
|
||||
height = height_in;
|
||||
if (this->width_ != width && this->height_ != height) {
|
||||
this->release();
|
||||
}
|
||||
}
|
||||
if (this->buffer_) {
|
||||
return false;
|
||||
}
|
||||
auto new_size = this->get_buffer_size_(width, height);
|
||||
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size);
|
||||
delay_microseconds_safe(2000);
|
||||
this->buffer_ = this->allocator_.allocate(new_size);
|
||||
if (this->buffer_) {
|
||||
this->buffer_width_ = width;
|
||||
this->buffer_height_ = height;
|
||||
this->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);
|
||||
#else
|
||||
int max_block = -1;
|
||||
#endif
|
||||
ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block);
|
||||
this->end_connection_();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnlineImage::update() {
|
||||
if (this->decoder_) {
|
||||
ESP_LOGW(TAG, "Image already being updated.");
|
||||
return;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Updating image");
|
||||
}
|
||||
|
||||
this->downloader_ = this->parent_->get(this->url_);
|
||||
|
||||
if (this->downloader_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Download failed.");
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
|
||||
int http_code = this->downloader_->status_code;
|
||||
if (http_code == HTTP_CODE_NOT_MODIFIED) {
|
||||
// Image hasn't changed on server. Skip download.
|
||||
this->end_connection_();
|
||||
return;
|
||||
}
|
||||
if (http_code != HTTP_CODE_OK) {
|
||||
ESP_LOGE(TAG, "HTTP result: %d", http_code);
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Starting download");
|
||||
size_t total_size = this->downloader_->content_length;
|
||||
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
if (this->format_ == ImageFormat::PNG) {
|
||||
this->decoder_ = esphome::make_unique<PngDecoder>(this);
|
||||
}
|
||||
#endif // ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
if (!this->decoder_) {
|
||||
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported.");
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
this->decoder_->prepare(total_size);
|
||||
ESP_LOGI(TAG, "Downloading image");
|
||||
}
|
||||
|
||||
void OnlineImage::loop() {
|
||||
if (!this->decoder_) {
|
||||
// Not decoding at the moment => nothing to do.
|
||||
return;
|
||||
}
|
||||
if (!this->downloader_ || this->decoder_->is_finished()) {
|
||||
ESP_LOGD(TAG, "Image fully downloaded");
|
||||
this->data_start_ = buffer_;
|
||||
this->width_ = buffer_width_;
|
||||
this->height_ = buffer_height_;
|
||||
this->end_connection_();
|
||||
this->download_finished_callback_.call();
|
||||
return;
|
||||
}
|
||||
if (this->downloader_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
|
||||
return;
|
||||
}
|
||||
size_t available = this->download_buffer_.free_capacity();
|
||||
if (available) {
|
||||
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||
if (len > 0) {
|
||||
this->download_buffer_.write(len);
|
||||
auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error when decoding image.");
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
this->download_buffer_.read(fed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
if (!this->buffer_) {
|
||||
ESP_LOGE(TAG, "Buffer not allocated!");
|
||||
return;
|
||||
}
|
||||
if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
|
||||
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
|
||||
return;
|
||||
}
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
switch (this->type_) {
|
||||
case ImageType::IMAGE_TYPE_BINARY: {
|
||||
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
|
||||
const uint32_t pos = x + y * width_8;
|
||||
if ((this->has_transparency() && color.w > 127) || is_color_on(color)) {
|
||||
this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u));
|
||||
} else {
|
||||
this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u));
|
||||
}
|
||||
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 (this->has_transparency()) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
gray = 1;
|
||||
}
|
||||
}
|
||||
this->buffer_[pos] = gray;
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGB565: {
|
||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||
if (this->has_transparency()) {
|
||||
if (col565 == 0x0020) {
|
||||
col565 = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
col565 = 0x0020;
|
||||
}
|
||||
}
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGBA: {
|
||||
this->buffer_[pos + 0] = color.r;
|
||||
this->buffer_[pos + 1] = color.g;
|
||||
this->buffer_[pos + 2] = color.b;
|
||||
this->buffer_[pos + 3] = color.w;
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGB24:
|
||||
default: {
|
||||
if (this->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;
|
||||
}
|
||||
}
|
||||
this->buffer_[pos + 0] = color.r;
|
||||
this->buffer_[pos + 1] = color.g;
|
||||
this->buffer_[pos + 2] = color.b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::end_connection_() {
|
||||
if (this->downloader_) {
|
||||
this->downloader_->end();
|
||||
this->downloader_ = nullptr;
|
||||
}
|
||||
this->decoder_.reset();
|
||||
this->download_buffer_.reset();
|
||||
}
|
||||
|
||||
bool OnlineImage::validate_url_(const std::string &url) {
|
||||
if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) {
|
||||
ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnlineImage::add_on_finished_callback(std::function<void()> &&callback) {
|
||||
this->download_finished_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
|
||||
this->download_error_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
184
esphome/components/online_image/online_image.h
Normal file
184
esphome/components/online_image/online_image.h
Normal file
@ -0,0 +1,184 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/components/image/image.h"
|
||||
|
||||
#include "image_decoder.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
using t_http_codes = enum {
|
||||
HTTP_CODE_OK = 200,
|
||||
HTTP_CODE_NOT_MODIFIED = 304,
|
||||
HTTP_CODE_NOT_FOUND = 404,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Format that the image is encoded with.
|
||||
*/
|
||||
enum ImageFormat {
|
||||
/** Automatically detect from MIME type. Not supported yet. */
|
||||
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 re-displayed without the
|
||||
* need to re-download or re-decode.
|
||||
*/
|
||||
class OnlineImage : public PollingComponent,
|
||||
public image::Image,
|
||||
public Parented<esphome::http_request::HttpRequestComponent> {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new OnlineImage 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 std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
||||
uint32_t buffer_size);
|
||||
|
||||
void update() override;
|
||||
void loop() override;
|
||||
|
||||
/** Set the URL to download the image from. */
|
||||
void set_url(const std::string &url) {
|
||||
if (this->validate_url_(url)) {
|
||||
this->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 add_on_finished_callback(std::function<void()> &&callback);
|
||||
void add_on_error_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
bool validate_url_(const std::string &url);
|
||||
|
||||
using Allocator = ExternalRAMAllocator<uint8_t>;
|
||||
Allocator allocator_{Allocator::Flags::ALLOW_FAILURE};
|
||||
|
||||
uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); }
|
||||
int get_buffer_size_(int width, int height) const {
|
||||
return std::ceil(image::image_type_to_bpp(this->type_) * width * height / 8.0);
|
||||
}
|
||||
|
||||
int get_position_(int x, int y) const {
|
||||
return ((x + y * this->buffer_width_) * image::image_type_to_bpp(this->type_)) / 8;
|
||||
}
|
||||
|
||||
ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }
|
||||
|
||||
bool resize_(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief Draw a pixel into the buffer.
|
||||
*
|
||||
* This is used by the decoder to fill the buffer that will later be displayed
|
||||
* by the `draw` method. This will internally convert the supplied 32 bit RGBA
|
||||
* color into the requested image storage format.
|
||||
*
|
||||
* @param x Horizontal pixel position.
|
||||
* @param y Vertical pixel position.
|
||||
* @param color 32 bit color to put into the pixel.
|
||||
*/
|
||||
void draw_pixel_(int x, int y, Color color);
|
||||
|
||||
void end_connection_();
|
||||
|
||||
CallbackManager<void()> download_finished_callback_{};
|
||||
CallbackManager<void()> download_error_callback_{};
|
||||
|
||||
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
||||
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
||||
|
||||
uint8_t *buffer_;
|
||||
DownloadBuffer download_buffer_;
|
||||
|
||||
const ImageFormat format_;
|
||||
|
||||
std::string url_{""};
|
||||
|
||||
/** 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_;
|
||||
|
||||
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
|
68
esphome/components/online_image/png_image.cpp
Normal file
68
esphome/components/online_image/png_image.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
#include "png_image.h"
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
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 init_callback(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 draw_callback(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(this->pngle_, this);
|
||||
pngle_set_init_callback(this->pngle_, init_callback);
|
||||
pngle_set_draw_callback(this->pngle_, draw_callback);
|
||||
}
|
||||
|
||||
int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
if (size < 256 && size < this->download_size_ - this->decoded_bytes_) {
|
||||
ESP_LOGD(TAG, "Waiting for data");
|
||||
return 0;
|
||||
}
|
||||
auto fed = pngle_feed(this->pngle_, buffer, size);
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
|
||||
} else {
|
||||
this->decoded_bytes_ += fed;
|
||||
}
|
||||
return fed;
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
33
esphome/components/online_image/png_image.h
Normal file
33
esphome/components/online_image/png_image.h
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "image_decoder.h"
|
||||
#ifdef USE_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()) {}
|
||||
~PngDecoder() override { pngle_destroy(this->pngle_); }
|
||||
|
||||
void prepare(uint32_t download_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
protected:
|
||||
pngle_t *pngle_;
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
@ -53,6 +53,7 @@
|
||||
#define USE_MQTT
|
||||
#define USE_NEXTION_TFT_UPLOAD
|
||||
#define USE_NUMBER
|
||||
#define USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#define USE_OTA
|
||||
#define USE_OTA_PASSWORD
|
||||
#define USE_OTA_STATE_CALLBACK
|
||||
|
@ -40,6 +40,7 @@ lib_deps =
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
|
||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||
kikuchan98/pngle@1.0.2 ; online_image
|
||||
; This is using the repository until a new release is published to PlatformIO
|
||||
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
|
||||
lvgl/lvgl@8.4.0 ; lvgl
|
||||
|
18
tests/components/online_image/common-esp32.yaml
Normal file
18
tests/components/online_image/common-esp32.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
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));
|
18
tests/components/online_image/common-esp8266.yaml
Normal file
18
tests/components/online_image/common-esp8266.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
spi:
|
||||
- id: spi_main_lcd
|
||||
clk_pin: 14
|
||||
mosi_pin: 13
|
||||
miso_pin: 12
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
id: main_lcd
|
||||
model: ili9342
|
||||
cs_pin: 15
|
||||
dc_pin: 3
|
||||
reset_pin: 1
|
||||
lambda: |-
|
||||
it.fill(Color(0, 0, 0));
|
||||
it.image(0, 0, id(online_rgba_image));
|
37
tests/components/online_image/common.yaml
Normal file
37
tests/components/online_image/common.yaml
Normal file
@ -0,0 +1,37 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
# 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
|
||||
format: 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
|
||||
format: PNG
|
||||
type: RGBA
|
||||
- id: online_rgb24_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
format: 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
|
||||
|
4
tests/components/online_image/test.esp32-ard.yaml
Normal file
4
tests/components/online_image/test.esp32-ard.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
<<: !include common-esp32.yaml
|
||||
|
||||
http_request:
|
||||
verify_ssl: false
|
4
tests/components/online_image/test.esp32-idf.yaml
Normal file
4
tests/components/online_image/test.esp32-idf.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
<<: !include common-esp32.yaml
|
||||
|
||||
http_request:
|
||||
|
Loading…
Reference in New Issue
Block a user