mirror of
https://github.com/esphome/esphome.git
synced 2026-10-28 00:42:11 +00:00
Merge branch 'dev' into ota-inflate-table-decode
This commit is contained in:
12 files changed
+545
-13
No files matched your search
@@ -1,6 +1,7 @@
|
||||
#include "image.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -165,6 +166,21 @@ Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off
|
||||
}
|
||||
#ifdef USE_LVGL
|
||||
lv_image_dsc_t *Image::get_lv_image_dsc() {
|
||||
// No pixels yet. A widget keeps this descriptor and reads it on every draw, and LVGL warns on every draw of an image
|
||||
// without data, so give it a transparent pixel instead
|
||||
static constexpr uint8_t TRANSPARENT_PIXEL = 0;
|
||||
if (this->data_start_ == nullptr) {
|
||||
if (this->dsc_.data != &TRANSPARENT_PIXEL) {
|
||||
memset(&this->dsc_, 0, sizeof(this->dsc_));
|
||||
this->dsc_.header.cf = LV_COLOR_FORMAT_A8;
|
||||
this->dsc_.header.w = 1;
|
||||
this->dsc_.header.h = 1;
|
||||
this->dsc_.header.stride = 1;
|
||||
this->dsc_.data_size = 1;
|
||||
this->dsc_.data = &TRANSPARENT_PIXEL;
|
||||
}
|
||||
return &this->dsc_;
|
||||
}
|
||||
// lazily construct lvgl image_dsc.
|
||||
if (this->dsc_.data != this->data_start_) {
|
||||
this->dsc_.data = this->data_start_;
|
||||
|
||||
@@ -43,7 +43,12 @@ class Image : public display::BaseImage {
|
||||
bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
|
||||
|
||||
#ifdef USE_LVGL
|
||||
lv_image_dsc_t *get_lv_image_dsc();
|
||||
virtual lv_image_dsc_t *get_lv_image_dsc();
|
||||
#endif
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
/// Called when an LVGL image widget is given this image. An image whose size can change keeps the widget, so it can
|
||||
/// give the widget its source again when the size changes: the widget keeps the size it read from the descriptor.
|
||||
virtual void add_lvgl_widget(lv_obj_t *obj) {}
|
||||
#endif
|
||||
protected:
|
||||
/// Per-pixel draw of the clipped range, rows (true) or columns (false) outer.
|
||||
|
||||
@@ -82,7 +82,10 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
#ifdef USE_IMAGE
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) {
|
||||
::lv_image_set_src(obj, image->get_lv_image_dsc());
|
||||
image->add_lvgl_widget(obj);
|
||||
}
|
||||
|
||||
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
|
||||
@@ -42,6 +42,7 @@ ONLINE_IMAGE_SCHEMA = (
|
||||
# AUTO (Content-Type detection) is online_image specific; not in the shared registry
|
||||
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, "AUTO", upper=True),
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
|
||||
cv.Optional(runtime_image.CONF_DOUBLE_BUFFER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
|
||||
cv.Schema({cv.string: cv.templatable(cv.string)})
|
||||
),
|
||||
@@ -141,6 +142,8 @@ async def setup_online_image(config: ConfigType) -> None:
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
if settings.double_buffer:
|
||||
cg.add(var.set_double_buffer(True))
|
||||
|
||||
for key, value in config.get(CONF_REQUEST_HEADERS, {}).items():
|
||||
if isinstance(value, Lambda):
|
||||
|
||||
@@ -428,9 +428,8 @@ void OnlineImage::finish_download_() {
|
||||
|
||||
void OnlineImage::end_connection_() {
|
||||
// Abort any in-progress decode; the decoder object is kept warm for the next decode.
|
||||
// Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release().
|
||||
if (this->is_decoding()) {
|
||||
RuntimeImage::release();
|
||||
this->abort_decode();
|
||||
}
|
||||
if (this->downloader_) {
|
||||
this->downloader_->end();
|
||||
|
||||
@@ -19,6 +19,7 @@ AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
|
||||
DOMAIN = "runtime_image"
|
||||
|
||||
CONF_DOUBLE_BUFFER = "double_buffer"
|
||||
CONF_PLACEHOLDER = "placeholder"
|
||||
CONF_TRANSPARENCY = "transparency"
|
||||
|
||||
@@ -212,6 +213,7 @@ class RuntimeImageSettings:
|
||||
transparent: cg.MockObj
|
||||
byte_order_big_endian: bool
|
||||
placeholder: cg.MockObj | None
|
||||
double_buffer: bool
|
||||
|
||||
|
||||
async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
|
||||
@@ -256,4 +258,5 @@ async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
|
||||
transparent=transparent,
|
||||
byte_order_big_endian=byte_order_big_endian,
|
||||
placeholder=placeholder,
|
||||
double_buffer=config.get(CONF_DOUBLE_BUFFER, False),
|
||||
)
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
@@ -48,7 +49,14 @@ RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Tra
|
||||
fixed_height_(fixed_height),
|
||||
placeholder_(placeholder) {}
|
||||
|
||||
RuntimeImage::~RuntimeImage() { this->release(); }
|
||||
RuntimeImage::~RuntimeImage() {
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
for (lv_obj_t *obj : this->lvgl_widgets_)
|
||||
lv_obj_remove_event_cb_with_user_data(obj, lvgl_widget_deleted, this);
|
||||
this->lvgl_widgets_.clear();
|
||||
#endif
|
||||
this->release();
|
||||
}
|
||||
|
||||
int RuntimeImage::resize(int width, int height) {
|
||||
// Use fixed dimensions if specified (0 means auto-resize)
|
||||
@@ -69,6 +77,7 @@ int RuntimeImage::resize(int width, int height) {
|
||||
this->width_ = this->buffer_width_;
|
||||
this->height_ = this->buffer_height_;
|
||||
this->data_start_ = this->buffer_;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -176,6 +185,52 @@ void RuntimeImage::map_chroma_key(Color &color) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL
|
||||
lv_image_dsc_t *RuntimeImage::get_lv_image_dsc() {
|
||||
if (this->data_start_ == nullptr && this->placeholder_ != nullptr) {
|
||||
this->dsc_ = *this->placeholder_->get_lv_image_dsc();
|
||||
return &this->dsc_;
|
||||
}
|
||||
return Image::get_lv_image_dsc();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
void RuntimeImage::add_lvgl_widget(lv_obj_t *obj) {
|
||||
if (std::find(this->lvgl_widgets_.begin(), this->lvgl_widgets_.end(), obj) != this->lvgl_widgets_.end())
|
||||
return;
|
||||
this->lvgl_widgets_.push_back(obj);
|
||||
lv_obj_add_event_cb(obj, lvgl_widget_deleted, LV_EVENT_DELETE, this);
|
||||
}
|
||||
|
||||
void RuntimeImage::lvgl_widget_deleted(lv_event_t *event) {
|
||||
auto *image = static_cast<RuntimeImage *>(lv_event_get_user_data(event));
|
||||
auto &widgets = image->lvgl_widgets_;
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), lv_event_get_target_obj(event)), widgets.end());
|
||||
}
|
||||
#endif
|
||||
|
||||
void RuntimeImage::refresh_lvgl_() {
|
||||
#ifdef USE_LVGL
|
||||
// Not a virtual call: this also runs from the destructor
|
||||
lv_image_dsc_t *dsc = this->RuntimeImage::get_lv_image_dsc();
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
auto &widgets = this->lvgl_widgets_;
|
||||
for (size_t i = 0; i < widgets.size();) {
|
||||
lv_obj_t *obj = widgets[i];
|
||||
if (lv_image_get_src(obj) == dsc) {
|
||||
// Setting the same source makes the widget read the size again, and redraws it
|
||||
::lv_image_set_src(obj, dsc);
|
||||
i++;
|
||||
} else {
|
||||
lv_obj_remove_event_cb_with_user_data(obj, lvgl_widget_deleted, this);
|
||||
widgets.erase(widgets.begin() + i);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||
if (this->data_start_) {
|
||||
// If we have a complete image, use the base class draw method
|
||||
@@ -263,11 +318,21 @@ bool RuntimeImage::end_decode() {
|
||||
}
|
||||
|
||||
// Finalize the image for display
|
||||
if (!this->progressive_display_) {
|
||||
if (this->is_double_buffered_()) {
|
||||
if (this->buffer_ != nullptr) {
|
||||
// Show the new image, and decode into the old one's buffer next time
|
||||
std::swap(this->front_buffer_, this->buffer_);
|
||||
std::swap(this->width_, this->buffer_width_);
|
||||
std::swap(this->height_, this->buffer_height_);
|
||||
this->data_start_ = this->front_buffer_;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
} else if (!this->progressive_display_) {
|
||||
// Only now make the image visible
|
||||
this->width_ = this->buffer_width_;
|
||||
this->height_ = this->buffer_height_;
|
||||
this->data_start_ = this->buffer_;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
|
||||
// End the session; the decoder object stays warm so the next decode can
|
||||
@@ -284,6 +349,15 @@ bool RuntimeImage::decoder_knows_end() const { return this->is_decoding() && thi
|
||||
|
||||
void RuntimeImage::release() {
|
||||
this->release_buffer_();
|
||||
if (this->front_buffer_) {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
allocator.deallocate(this->front_buffer_, this->get_buffer_size(this->width_, this->height_));
|
||||
this->front_buffer_ = nullptr;
|
||||
this->data_start_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
// End any active decode session; decoders free the format-specific working buffers
|
||||
// they can (PNG), while the decoder object itself is kept warm for the next decode.
|
||||
if (this->decoder_) {
|
||||
@@ -291,6 +365,16 @@ void RuntimeImage::release() {
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeImage::abort_decode() {
|
||||
if (!this->is_double_buffered_()) {
|
||||
this->release();
|
||||
return;
|
||||
}
|
||||
if (this->decoder_) {
|
||||
this->decoder_->reset();
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeImage::release_buffer_() {
|
||||
if (this->buffer_) {
|
||||
if (this->external_buffer_) {
|
||||
@@ -302,19 +386,25 @@ void RuntimeImage::release_buffer_() {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
allocator.deallocate(this->buffer_, this->get_buffer_size(this->buffer_width_, this->buffer_height_));
|
||||
}
|
||||
const bool shown = this->data_start_ == this->buffer_;
|
||||
this->buffer_ = nullptr;
|
||||
this->data_start_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->buffer_width_ = 0;
|
||||
this->buffer_height_ = 0;
|
||||
#ifdef USE_LVGL
|
||||
memset(&this->dsc_, 0, sizeof(this->dsc_));
|
||||
#endif
|
||||
// With double buffering the buffer decoded into is not the one on show
|
||||
if (shown) {
|
||||
this->data_start_ = nullptr;
|
||||
this->width_ = 0;
|
||||
this->height_ = 0;
|
||||
this->refresh_lvgl_();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeImage::set_external_buffer(uint8_t *buffer, int width, int height) {
|
||||
if (this->is_double_buffered_()) {
|
||||
ESP_LOGE(TAG, "An external buffer cannot be double buffered");
|
||||
return false;
|
||||
}
|
||||
this->release_buffer_();
|
||||
if (buffer == nullptr || this->get_buffer_size(width, height) == 0) {
|
||||
// Keep the released state rather than remembering a buffer that cannot be decoded into: an
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "esphome/components/image/image.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "image_decoder.h"
|
||||
#include "image_format.h"
|
||||
|
||||
@@ -54,6 +56,13 @@ class RuntimeImage : public image::Image {
|
||||
void map_chroma_key(Color &color);
|
||||
int get_buffer_width() const { return this->buffer_width_; }
|
||||
int get_buffer_height() const { return this->buffer_height_; }
|
||||
#ifdef USE_LVGL
|
||||
/// Until an image is loaded, the placeholder's descriptor if there is one
|
||||
lv_image_dsc_t *get_lv_image_dsc() override;
|
||||
#endif
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
void add_lvgl_widget(lv_obj_t *obj) override;
|
||||
#endif
|
||||
|
||||
// Image drawing interface
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
@@ -118,7 +127,9 @@ class RuntimeImage : public image::Image {
|
||||
/**
|
||||
* @brief Check if an image is currently loaded.
|
||||
*/
|
||||
bool is_loaded() const { return this->buffer_ != nullptr; }
|
||||
bool is_loaded() const {
|
||||
return this->is_double_buffered_() ? this->front_buffer_ != nullptr : this->buffer_ != nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the image format.
|
||||
@@ -134,6 +145,14 @@ class RuntimeImage : public image::Image {
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* @brief End a decode that will not be finished.
|
||||
*
|
||||
* With double buffering the image on show stays and the buffer decoded into is kept for the next
|
||||
* decode. Otherwise the same as release(), since the image on show was being decoded over.
|
||||
*/
|
||||
void abort_decode();
|
||||
|
||||
/**
|
||||
* @brief Decode into a buffer the caller owns, instead of one allocated here.
|
||||
*
|
||||
@@ -157,6 +176,8 @@ class RuntimeImage : public image::Image {
|
||||
* A null buffer or dimensions the image cannot decode at are refused, leaving the image with
|
||||
* no buffer at all.
|
||||
*
|
||||
* Refused when double buffering is on.
|
||||
*
|
||||
* @param buffer Memory for a picture of the given size, at least get_buffer_size() bytes.
|
||||
* @param width Width of the buffer in pixels.
|
||||
* @param height Height of the buffer in pixels.
|
||||
@@ -179,6 +200,13 @@ class RuntimeImage : public image::Image {
|
||||
*/
|
||||
void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; }
|
||||
|
||||
/**
|
||||
* @brief Set whether to decode into a second buffer, keeping the image on show until the new one is complete.
|
||||
*
|
||||
* Both buffers are kept, so a new image of the same size allocates nothing. Ignored with progressive display.
|
||||
*/
|
||||
void set_double_buffer(bool double_buffer) { this->double_buffer_ = double_buffer; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Resize the image buffer to the requested dimensions.
|
||||
@@ -196,6 +224,11 @@ class RuntimeImage : public image::Image {
|
||||
*/
|
||||
void release_buffer_();
|
||||
|
||||
bool is_double_buffered_() const { return this->double_buffer_ && !this->progressive_display_; }
|
||||
|
||||
/// Bring the LVGL descriptor up to date after the image shown changed, and give it again to the widgets showing it
|
||||
void refresh_lvgl_();
|
||||
|
||||
/**
|
||||
* @brief Get the position in the buffer for a pixel.
|
||||
*/
|
||||
@@ -210,6 +243,8 @@ class RuntimeImage : public image::Image {
|
||||
|
||||
// Memory management
|
||||
uint8_t *buffer_{nullptr};
|
||||
/// With double buffering, the buffer on show, width_ by height_. buffer_ is then the one decoded into
|
||||
uint8_t *front_buffer_{nullptr};
|
||||
|
||||
// Decoder management
|
||||
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
||||
@@ -246,8 +281,14 @@ class RuntimeImage : public image::Image {
|
||||
|
||||
// Configuration
|
||||
bool progressive_display_{false};
|
||||
bool double_buffer_{false};
|
||||
/** Whether buffer_ belongs to the caller, so it must not be freed or resized here. */
|
||||
bool external_buffer_{false};
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
static void lvgl_widget_deleted(lv_event_t *event);
|
||||
/// Widgets given this image. One moved to another image is dropped at the next refresh
|
||||
std::vector<lv_obj_t *> lvgl_widgets_;
|
||||
#endif
|
||||
|
||||
friend class ImageDecoder;
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ image:
|
||||
format: PNG
|
||||
type: RGB
|
||||
transparency: chroma_key
|
||||
double_buffer: true
|
||||
- platform: online_image
|
||||
id: online_rgb111_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.lvgl import LVGL_VERSION
|
||||
from esphome.components.runtime_image import enable_format
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
@@ -12,5 +14,11 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
enable_format("JPEG")
|
||||
enable_format("PNG")
|
||||
enable_format("QOI")
|
||||
# The LVGL image descriptor is tested too, so build with the LVGL library
|
||||
cg.add_library("lvgl/lvgl", LVGL_VERSION)
|
||||
cg.add_build_flag("-DLV_CONF_SKIP=1")
|
||||
cg.add_define("USE_LVGL")
|
||||
cg.add_define("USE_LVGL_IMAGE")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
manifest.dependencies = manifest.dependencies + ["lvgl"]
|
||||
@@ -0,0 +1,146 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
// 3x2 24bpp BMP (rows padded to 4 bytes)
|
||||
static const uint8_t BMP_3X2[] = {
|
||||
0x42, 0x4D, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x22, 0x11, 0x77, 0x88, 0x99, 0xEF, 0xCD, 0xAB, 0x00,
|
||||
0x00, 0x00, 0x20, 0x10, 0xE0, 0x40, 0xC0, 0x30, 0xA0, 0x60, 0x50, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// 2x1 24bpp BMP (row padded to 8 bytes)
|
||||
static const uint8_t BMP_2X1[] = {
|
||||
0x42, 0x4D, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x01, 0x06, 0x05, 0x04, 0x00, 0x00,
|
||||
};
|
||||
|
||||
template<size_t N> static void load(RuntimeImage &img, const uint8_t (&bmp)[N]) {
|
||||
std::vector<uint8_t> data(bmp, bmp + N);
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
ASSERT_EQ(img.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
}
|
||||
|
||||
class DoubleBufferedImage : public RuntimeImage {
|
||||
public:
|
||||
DoubleBufferedImage() : RuntimeImage(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE) {
|
||||
this->set_double_buffer(true);
|
||||
}
|
||||
};
|
||||
|
||||
// The image on show is not decoded over: the new one appears only when it is complete
|
||||
TEST(RuntimeImageDoubleBuffer, ImageOnShowStaysUntilTheNextIsDecoded) {
|
||||
DoubleBufferedImage img;
|
||||
load(img, BMP_3X2);
|
||||
const uint8_t *first = img.get_data_start();
|
||||
ASSERT_NE(first, nullptr);
|
||||
const std::vector<uint8_t> shown(first, first + 3 * 2 * 3);
|
||||
|
||||
std::vector<uint8_t> data(BMP_2X1, BMP_2X1 + sizeof(BMP_2X1));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
ASSERT_EQ(img.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
EXPECT_EQ(img.get_data_start(), first);
|
||||
EXPECT_EQ(img.get_width(), 3);
|
||||
EXPECT_EQ(img.get_height(), 2);
|
||||
EXPECT_EQ(std::vector<uint8_t>(first, first + shown.size()), shown);
|
||||
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
EXPECT_NE(img.get_data_start(), first);
|
||||
EXPECT_EQ(img.get_width(), 2);
|
||||
EXPECT_EQ(img.get_height(), 1);
|
||||
}
|
||||
|
||||
// Images of one size take turns in the same two buffers
|
||||
TEST(RuntimeImageDoubleBuffer, SameSizeImagesReuseBothBuffers) {
|
||||
DoubleBufferedImage img;
|
||||
load(img, BMP_3X2);
|
||||
const uint8_t *first = img.get_data_start();
|
||||
load(img, BMP_3X2);
|
||||
const uint8_t *second = img.get_data_start();
|
||||
EXPECT_NE(second, first);
|
||||
load(img, BMP_3X2);
|
||||
EXPECT_EQ(img.get_data_start(), first);
|
||||
load(img, BMP_3X2);
|
||||
EXPECT_EQ(img.get_data_start(), second);
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDoubleBuffer, AbortedDecodeLeavesTheImageOnShow) {
|
||||
DoubleBufferedImage img;
|
||||
load(img, BMP_3X2);
|
||||
const uint8_t *first = img.get_data_start();
|
||||
|
||||
std::vector<uint8_t> data(BMP_2X1, BMP_2X1 + sizeof(BMP_2X1));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
img.feed_data(data.data(), data.size() / 2 + 8);
|
||||
img.abort_decode();
|
||||
EXPECT_FALSE(img.is_decoding());
|
||||
EXPECT_EQ(img.get_data_start(), first);
|
||||
EXPECT_EQ(img.get_width(), 3);
|
||||
EXPECT_TRUE(img.is_loaded());
|
||||
|
||||
load(img, BMP_2X1);
|
||||
EXPECT_EQ(img.get_width(), 2);
|
||||
}
|
||||
|
||||
// An aborted first decode leaves nothing on show
|
||||
TEST(RuntimeImageDoubleBuffer, AbortedFirstDecodeIsNotLoaded) {
|
||||
DoubleBufferedImage img;
|
||||
std::vector<uint8_t> data(BMP_3X2, BMP_3X2 + sizeof(BMP_3X2));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
img.feed_data(data.data(), data.size() / 2 + 8);
|
||||
img.abort_decode();
|
||||
EXPECT_EQ(img.get_data_start(), nullptr);
|
||||
EXPECT_FALSE(img.is_loaded());
|
||||
}
|
||||
|
||||
// Without double buffering an aborted decode has drawn over the image, so it goes
|
||||
TEST(RuntimeImageDoubleBuffer, AbortedDecodeWithOneBufferReleasesTheImage) {
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
load(img, BMP_3X2);
|
||||
std::vector<uint8_t> data(BMP_3X2, BMP_3X2 + sizeof(BMP_3X2));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
img.feed_data(data.data(), data.size() / 2 + 8);
|
||||
img.abort_decode();
|
||||
EXPECT_EQ(img.get_data_start(), nullptr);
|
||||
EXPECT_FALSE(img.is_loaded());
|
||||
}
|
||||
|
||||
// Both buffers are freed; LeakSanitizer reports one that is not
|
||||
TEST(RuntimeImageDoubleBuffer, ReleaseFreesBothBuffers) {
|
||||
DoubleBufferedImage img;
|
||||
load(img, BMP_3X2);
|
||||
load(img, BMP_3X2);
|
||||
img.release();
|
||||
EXPECT_EQ(img.get_data_start(), nullptr);
|
||||
EXPECT_EQ(img.get_width(), 0);
|
||||
EXPECT_FALSE(img.is_loaded());
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDoubleBuffer, ExternalBufferIsRefused) {
|
||||
DoubleBufferedImage img;
|
||||
std::vector<uint8_t> buffer(img.get_buffer_size(3, 2));
|
||||
EXPECT_FALSE(img.set_external_buffer(buffer.data(), 3, 2));
|
||||
load(img, BMP_3X2);
|
||||
EXPECT_NE(img.get_data_start(), buffer.data());
|
||||
}
|
||||
|
||||
// Progressive display turns double buffering off, so an external buffer is allowed
|
||||
TEST(RuntimeImageDoubleBuffer, ExternalBufferIsAllowedWithProgressiveDisplay) {
|
||||
DoubleBufferedImage img;
|
||||
img.set_progressive_display(true);
|
||||
std::vector<uint8_t> buffer(img.get_buffer_size(3, 2));
|
||||
EXPECT_TRUE(img.set_external_buffer(buffer.data(), 3, 2));
|
||||
img.release();
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
@@ -0,0 +1,217 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
#ifdef USE_LVGL
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
// 3x2 24bpp BMP (rows padded to 4 bytes)
|
||||
static const uint8_t BMP_24BPP[] = {
|
||||
0x42, 0x4D, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x22, 0x11, 0x77, 0x88, 0x99, 0xEF, 0xCD, 0xAB, 0x00,
|
||||
0x00, 0x00, 0x20, 0x10, 0xE0, 0x40, 0xC0, 0x30, 0xA0, 0x60, 0x50, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
// A widget keeps the descriptor it was given and reads it on every draw. Once the image lets go of its pixels,
|
||||
// for example when it starts loading another image, the descriptor must still be one LVGL can draw.
|
||||
TEST(RuntimeImageLvgl, ReleasedImageLeavesADrawableDescriptor) {
|
||||
lv_init();
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
std::vector<uint8_t> data(BMP_24BPP, BMP_24BPP + sizeof(BMP_24BPP));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
ASSERT_EQ(img.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
|
||||
lv_image_dsc_t *dsc = img.get_lv_image_dsc();
|
||||
lv_image_header_t header;
|
||||
ASSERT_EQ(lv_image_decoder_get_info(dsc, &header), LV_RESULT_OK);
|
||||
EXPECT_EQ(header.w, 3);
|
||||
EXPECT_EQ(header.h, 2);
|
||||
|
||||
img.release();
|
||||
EXPECT_EQ(lv_image_decoder_get_info(dsc, &header), LV_RESULT_OK);
|
||||
EXPECT_EQ(header.w, 1);
|
||||
EXPECT_EQ(header.h, 1);
|
||||
lv_deinit();
|
||||
}
|
||||
|
||||
// Before the first image is loaded, a widget gets a descriptor it can draw too
|
||||
TEST(RuntimeImageLvgl, UnloadedImageHasADrawableDescriptor) {
|
||||
lv_init();
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
lv_image_header_t header;
|
||||
ASSERT_EQ(lv_image_decoder_get_info(img.get_lv_image_dsc(), &header), LV_RESULT_OK);
|
||||
EXPECT_EQ(header.w, 1);
|
||||
EXPECT_EQ(header.h, 1);
|
||||
lv_deinit();
|
||||
}
|
||||
|
||||
// With a placeholder, a widget shows it before the first image and while the next one loads
|
||||
TEST(RuntimeImageLvgl, PlaceholderIsShownWithoutAnImage) {
|
||||
lv_init();
|
||||
static constexpr uint8_t PLACEHOLDER_PIXELS[4 * 2 * 3] = {};
|
||||
image::Image placeholder(PLACEHOLDER_PIXELS, 4, 2, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE, &placeholder);
|
||||
lv_image_dsc_t *dsc = img.get_lv_image_dsc();
|
||||
EXPECT_EQ(dsc->data, PLACEHOLDER_PIXELS);
|
||||
lv_image_header_t header;
|
||||
ASSERT_EQ(lv_image_decoder_get_info(dsc, &header), LV_RESULT_OK);
|
||||
EXPECT_EQ(header.w, 4);
|
||||
EXPECT_EQ(header.h, 2);
|
||||
|
||||
std::vector<uint8_t> data(BMP_24BPP, BMP_24BPP + sizeof(BMP_24BPP));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
ASSERT_EQ(img.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
EXPECT_EQ(img.get_lv_image_dsc(), dsc);
|
||||
EXPECT_EQ(dsc->data, img.get_data_start());
|
||||
EXPECT_EQ(dsc->header.w, 3);
|
||||
|
||||
img.release();
|
||||
EXPECT_EQ(dsc->data, PLACEHOLDER_PIXELS);
|
||||
EXPECT_EQ(dsc->header.w, 4);
|
||||
EXPECT_EQ(dsc->header.h, 2);
|
||||
lv_deinit();
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
static constexpr int32_t SCREEN_WIDTH = 16;
|
||||
static constexpr int32_t SCREEN_HEIGHT = 8;
|
||||
|
||||
static void flush_ready(lv_display_t *display, const lv_area_t *area, uint8_t *px_map) {
|
||||
lv_display_flush_ready(display);
|
||||
}
|
||||
|
||||
// A display to draw on, so ASan sees any read LVGL makes past an image's pixels
|
||||
class RuntimeImageLvglWidget : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
lv_init();
|
||||
this->display_ = lv_display_create(SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
lv_display_set_flush_cb(this->display_, flush_ready);
|
||||
lv_display_set_buffers(this->display_, this->draw_buffer_.data(), nullptr, this->draw_buffer_.size(),
|
||||
LV_DISPLAY_RENDER_MODE_FULL);
|
||||
this->widget_ = lv_image_create(lv_screen_active());
|
||||
}
|
||||
void TearDown() override { lv_deinit(); }
|
||||
|
||||
// What the lvgl component's lv_image_set_src() overload does
|
||||
void set_src_(image::Image *image) {
|
||||
::lv_image_set_src(this->widget_, image->get_lv_image_dsc());
|
||||
image->add_lvgl_widget(this->widget_);
|
||||
}
|
||||
|
||||
static void load(RuntimeImage &img) {
|
||||
std::vector<uint8_t> data(BMP_24BPP, BMP_24BPP + sizeof(BMP_24BPP));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
ASSERT_EQ(img.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
}
|
||||
|
||||
// Redraw the widget, as something drawn over it would make LVGL do during a load
|
||||
void draw_() {
|
||||
lv_obj_invalidate(this->widget_);
|
||||
lv_refr_now(this->display_);
|
||||
}
|
||||
|
||||
lv_display_t *display_{};
|
||||
lv_obj_t *widget_{};
|
||||
std::vector<uint8_t> draw_buffer_ = std::vector<uint8_t>(SCREEN_WIDTH * SCREEN_HEIGHT * 4);
|
||||
};
|
||||
|
||||
// The widget keeps the size it read when given the descriptor, and draws that much of whatever the descriptor holds.
|
||||
// The image gives the widget its source again whenever what it shows changes size.
|
||||
TEST_F(RuntimeImageLvglWidget, WidgetFollowsTheImageSize) {
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
this->set_src_(&img);
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 1);
|
||||
|
||||
load(img);
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 3);
|
||||
EXPECT_EQ(lv_image_get_src_height(this->widget_), 2);
|
||||
this->draw_();
|
||||
|
||||
img.release();
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 1);
|
||||
EXPECT_EQ(lv_image_get_src_height(this->widget_), 1);
|
||||
this->draw_();
|
||||
|
||||
load(img);
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 3);
|
||||
this->draw_();
|
||||
}
|
||||
|
||||
// The placeholder is smaller than the image, and on the heap, so drawing it at the image's size trips ASan
|
||||
TEST_F(RuntimeImageLvglWidget, WidgetFollowsThePlaceholderSize) {
|
||||
std::vector<uint8_t> placeholder_pixels(2 * 1 * 3);
|
||||
image::Image placeholder(placeholder_pixels.data(), 2, 1, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE, &placeholder);
|
||||
load(img);
|
||||
this->set_src_(&img);
|
||||
this->draw_();
|
||||
|
||||
img.release();
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 2);
|
||||
EXPECT_EQ(lv_image_get_src_height(this->widget_), 1);
|
||||
this->draw_();
|
||||
}
|
||||
|
||||
// A widget given another image is left alone, however many times it was given this one
|
||||
TEST_F(RuntimeImageLvglWidget, WidgetGivenAnotherImageIsLeftAlone) {
|
||||
static constexpr uint8_t OTHER_PIXELS[4 * 5 * 3] = {};
|
||||
image::Image other(OTHER_PIXELS, 4, 5, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
load(img);
|
||||
this->set_src_(&img);
|
||||
this->set_src_(&img);
|
||||
this->set_src_(&other);
|
||||
|
||||
img.release();
|
||||
EXPECT_EQ(lv_image_get_src(this->widget_), other.get_lv_image_dsc());
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 4);
|
||||
|
||||
// Given back, it is followed again
|
||||
this->set_src_(&img);
|
||||
load(img);
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 3);
|
||||
}
|
||||
|
||||
// With double buffering the widget shows the old image while the new one decodes, then the new one at its size
|
||||
TEST_F(RuntimeImageLvglWidget, DoubleBufferedWidgetKeepsTheImageWhileDecoding) {
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
img.set_double_buffer(true);
|
||||
load(img);
|
||||
this->set_src_(&img);
|
||||
const void *first = static_cast<const lv_image_dsc_t *>(lv_image_get_src(this->widget_))->data;
|
||||
|
||||
std::vector<uint8_t> data(BMP_24BPP, BMP_24BPP + sizeof(BMP_24BPP));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
ASSERT_EQ(img.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
EXPECT_EQ(static_cast<const lv_image_dsc_t *>(lv_image_get_src(this->widget_))->data, first);
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 3);
|
||||
this->draw_();
|
||||
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
EXPECT_NE(static_cast<const lv_image_dsc_t *>(lv_image_get_src(this->widget_))->data, first);
|
||||
EXPECT_EQ(lv_image_get_src_width(this->widget_), 3);
|
||||
this->draw_();
|
||||
}
|
||||
|
||||
TEST_F(RuntimeImageLvglWidget, DeletedWidgetIsForgotten) {
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
load(img);
|
||||
this->set_src_(&img);
|
||||
lv_obj_delete(this->widget_);
|
||||
img.release();
|
||||
}
|
||||
#endif // USE_LVGL_IMAGE
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
|
||||
#endif // USE_LVGL
|
||||
Reference in new issue
Block a user