mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 05:46:12 +00:00
[runtime_image] Let a source-reading decoder wait for data that has not arrived
A source that is still arriving, such as a download, may have nothing to give when the decoder asks for more. The pinned JPEGDEC now pauses before an MCU when less input is buffered than it needs and the source has no more yet, and decode_step() reports that as DECODE_STEP_NEED_DATA; the next call carries on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,13 +92,14 @@ class JPEGFormat(Format):
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
|
||||
# 1.8.4 plus decodeBegin()/decodeRows(), so an image can be decoded a few rows at a time,
|
||||
# 1.8.4 plus decodeBegin()/decodeRows(), so an image can be decoded a few rows at a time and
|
||||
# can wait for input that has not arrived yet, fixes for input that arrives in short reads,
|
||||
# and two ARM NEON fixes: swapped red and blue in the 4:2:0 RGB8888 path, and writes past
|
||||
# the row on clipped MCUs
|
||||
cg.add_library(
|
||||
"JPEGDEC",
|
||||
"1.8.4",
|
||||
"https://github.com/clydebarrow/JPEGDEC#047f891096c921fa08f987401fe0fbe0eb3bc6e5",
|
||||
"https://github.com/clydebarrow/JPEGDEC#0bf32c872d99dbd1fe133dd0441aaabaf1a85c77",
|
||||
)
|
||||
if CORE.is_host:
|
||||
# JPEGDEC's host detection checks __MACH__/__LINUX__, but gcc only
|
||||
|
||||
@@ -18,6 +18,7 @@ enum DecodeError : int {
|
||||
enum DecodeStep : int {
|
||||
DECODE_STEP_DONE = 0,
|
||||
DECODE_STEP_MORE = 1,
|
||||
DECODE_STEP_NEED_DATA = 2, // The source has nothing more yet; call again once more has arrived
|
||||
};
|
||||
|
||||
constexpr const char *decode_error_to_string(int error) {
|
||||
@@ -43,7 +44,10 @@ class RuntimeImage;
|
||||
class DecodeSource {
|
||||
public:
|
||||
virtual ~DecodeSource() = default;
|
||||
/// Read up to len bytes at the current position. Returns the number read, 0 at the end, or negative on error.
|
||||
/**
|
||||
* Read up to len bytes at the current position. Returns the number read, which is less than len, even 0, when
|
||||
* no more has arrived yet or at the end of the input, or negative on error.
|
||||
*/
|
||||
virtual int32_t read(uint8_t *buf, uint32_t len) = 0;
|
||||
/// Move to a position counted from the start. Returns false on error.
|
||||
virtual bool seek(uint32_t position) = 0;
|
||||
@@ -123,8 +127,9 @@ class ImageDecoder {
|
||||
/**
|
||||
* @brief Decode a little more of an image started with begin_source(), keeping each call short.
|
||||
*
|
||||
* @return int DECODE_STEP_MORE while there is more to do, DECODE_STEP_DONE when the image is complete,
|
||||
* or a {@see DecodeError} value.
|
||||
* @return int DECODE_STEP_MORE while there is more to do, DECODE_STEP_NEED_DATA when the source has not
|
||||
* delivered enough to go on, DECODE_STEP_DONE when the image is complete, or a {@see DecodeError}
|
||||
* value.
|
||||
*/
|
||||
virtual int decode_step() { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
|
||||
|
||||
|
||||
@@ -256,6 +256,8 @@ int HOT JpegDecoder::decode_step() {
|
||||
}
|
||||
if (result == JPEG_DECODE_MORE)
|
||||
return DECODE_STEP_MORE;
|
||||
if (result == JPEG_DECODE_NEED_DATA)
|
||||
return DECODE_STEP_NEED_DATA;
|
||||
if (result == 0)
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = this->expected_size_;
|
||||
|
||||
@@ -27,7 +27,7 @@ class JpegDecoder : public ImageDecoder {
|
||||
/// The source is read as the decode needs it, through JPEGDEC's own 2 KB buffer
|
||||
bool supports_source() const override { return true; }
|
||||
int begin_source(DecodeSource *source) override;
|
||||
/// Decodes one row of MCUs (8 or 16 pixel rows)
|
||||
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
|
||||
int HOT decode_step() override;
|
||||
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ lib_deps =
|
||||
improv/Improv@1.2.7 ; improv_serial / improv_ble
|
||||
kikuchan98/pngle@1.1.0 ; runtime_image
|
||||
; Using the repository directly, otherwise ESP-IDF can't use the library
|
||||
https://github.com/clydebarrow/JPEGDEC.git#047f891096c921fa08f987401fe0fbe0eb3bc6e5 ; runtime_image
|
||||
https://github.com/clydebarrow/JPEGDEC.git#0bf32c872d99dbd1fe133dd0441aaabaf1a85c77 ; runtime_image
|
||||
; This dependency is used only in unit tests.
|
||||
; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py
|
||||
; See scripts/cpp_unit_test.py and tests/components/README.md
|
||||
@@ -253,7 +253,7 @@ lib_deps =
|
||||
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
|
||||
improv/Improv@1.2.7 ; improv_serial
|
||||
kikuchan98/pngle@1.1.0 ; runtime_image
|
||||
https://github.com/clydebarrow/JPEGDEC.git#047f891096c921fa08f987401fe0fbe0eb3bc6e5 ; runtime_image
|
||||
https://github.com/clydebarrow/JPEGDEC.git#0bf32c872d99dbd1fe133dd0441aaabaf1a85c77 ; runtime_image
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
-DUSE_LIBRETINY
|
||||
|
||||
@@ -176,6 +176,31 @@ class MemorySource : public DecodeSource {
|
||||
size_t fail_at_;
|
||||
};
|
||||
|
||||
/// Hands out only the bytes delivered so far, like a download that is still arriving
|
||||
class TrickleSource : public DecodeSource {
|
||||
public:
|
||||
TrickleSource(const uint8_t *data, size_t size, size_t delivered) : data_(data), size_(size), delivered_(delivered) {}
|
||||
int32_t read(uint8_t *buf, uint32_t len) override {
|
||||
const size_t n = std::min<size_t>(len, this->delivered_ - this->pos_);
|
||||
memcpy(buf, this->data_ + this->pos_, n);
|
||||
this->pos_ += n;
|
||||
return static_cast<int32_t>(n);
|
||||
}
|
||||
bool seek(uint32_t position) override {
|
||||
if (position > this->delivered_)
|
||||
return false;
|
||||
this->pos_ = position;
|
||||
return true;
|
||||
}
|
||||
void deliver(size_t len) { this->delivered_ = std::min(this->size_, this->delivered_ + len); }
|
||||
|
||||
protected:
|
||||
const uint8_t *data_;
|
||||
size_t size_;
|
||||
size_t delivered_;
|
||||
size_t pos_{0};
|
||||
};
|
||||
|
||||
static bool decode_fed(RuntimeImage &img) {
|
||||
std::vector<uint8_t> buffer(JPEG_ROWS, JPEG_ROWS + sizeof(JPEG_ROWS));
|
||||
if (!img.begin_decode(buffer.size()))
|
||||
@@ -233,6 +258,29 @@ TEST(JpegSourceDecode, SteppedDirectRgb565MatchesFedDecode) {
|
||||
EXPECT_EQ(pixels(stepped), pixels(fed));
|
||||
}
|
||||
|
||||
TEST(JpegSourceDecode, WaitsForDataThatHasNotArrived) {
|
||||
SourceImage fed;
|
||||
ASSERT_TRUE(decode_fed(fed));
|
||||
|
||||
// Enough for the headers (the scan starts at byte 609), then 50 bytes between steps
|
||||
SourceImage img;
|
||||
TrickleSource source(JPEG_ROWS, sizeof(JPEG_ROWS), 700);
|
||||
ASSERT_TRUE(img.begin_decode(sizeof(JPEG_ROWS)));
|
||||
ASSERT_EQ(img.begin_source(&source), 0);
|
||||
int waits = 0;
|
||||
int result;
|
||||
while ((result = img.decode_step()) == DECODE_STEP_MORE || result == DECODE_STEP_NEED_DATA) {
|
||||
if (result == DECODE_STEP_NEED_DATA)
|
||||
waits++;
|
||||
source.deliver(50);
|
||||
ASSERT_LT(waits, 1000);
|
||||
}
|
||||
ASSERT_EQ(result, DECODE_STEP_DONE);
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
EXPECT_GT(waits, 0);
|
||||
EXPECT_EQ(pixels(img), pixels(fed));
|
||||
}
|
||||
|
||||
TEST(JpegSourceDecode, ImageIsHiddenUntilTheLastStep) {
|
||||
SourceImage img;
|
||||
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
|
||||
|
||||
Reference in New Issue
Block a user