first commit

This commit is contained in:
2026-08-20 21:26:05 +02:00
commit 81717cb9a8
10 changed files with 777 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
.git
.gitignore
.DS_Store
__pycache__
*.pyc
+4
View File
@@ -0,0 +1,4 @@
.DS_Store
.env
__pycache__/
*.pyc
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.12-slim-bookworm
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py /app/app.py
EXPOSE 80
CMD ["python", "-u", "app.py"]
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Doorbell JPEG proxy: prefetch from Frigate and serve a baseline 480px-wide JPEG."""
from __future__ import annotations
import hashlib
import io
import os
import threading
import time
import requests
from flask import Flask, request
from PIL import Image
FRIGATE_URL = os.environ.get(
"FRIGATE_URL", "http://10.0.1.197:5050/api/doorbell/latest.jpg?h=270"
)
TARGET_WIDTH = int(os.environ.get("TARGET_WIDTH", "480"))
JPEG_QUALITY = int(os.environ.get("JPEG_QUALITY", "40"))
FETCH_TIMEOUT = float(os.environ.get("FETCH_TIMEOUT", "5"))
PREFETCH_HZ = float(os.environ.get("PREFETCH_HZ", "10"))
app = Flask(__name__)
_lock = threading.Lock()
_jpeg = b""
_etag = ""
_width = 0
_height = 0
def _encode(frame: Image.Image) -> tuple[bytes, int, int]:
frame = frame.convert("RGB")
if frame.width != TARGET_WIDTH:
height = max(1, round(frame.height * TARGET_WIDTH / frame.width))
frame = frame.resize((TARGET_WIDTH, height), Image.BILINEAR)
buf = io.BytesIO()
frame.save(
buf,
format="JPEG",
quality=JPEG_QUALITY,
subsampling=2,
optimize=False,
progressive=False,
)
return buf.getvalue(), frame.width, frame.height
def _prefetch_loop() -> None:
session = requests.Session()
interval = 1.0 / max(PREFETCH_HZ, 0.5)
while True:
started = time.monotonic()
try:
response = session.get(FRIGATE_URL, timeout=FETCH_TIMEOUT)
response.raise_for_status()
with Image.open(io.BytesIO(response.content)) as image:
encoded, width, height = _encode(image)
etag = '"' + hashlib.md5(encoded).hexdigest() + '"'
with _lock:
global _jpeg, _etag, _width, _height
_jpeg = encoded
_etag = etag
_width = width
_height = height
except Exception as exc: # noqa: BLE001 — keep last good frame
print(f"prefetch failed: {exc}", flush=True)
delay = interval - (time.monotonic() - started)
if delay > 0:
time.sleep(delay)
def _snapshot() -> tuple[bytes, str, int, int]:
with _lock:
return _jpeg, _etag, _width, _height
@app.get("/")
@app.get("/latest.jpg")
def latest():
body, etag, width, height = _snapshot()
if not body:
return "Image not ready", 503, {"Content-Type": "text/plain"}
if request.headers.get("If-None-Match") == etag:
return ("", 304, {"ETag": etag, "Cache-Control": "no-cache"})
return (
body,
200,
{
"Content-Type": "image/jpeg",
"Content-Length": str(len(body)),
"ETag": etag,
"Last-Modified": time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()),
"Cache-Control": "no-cache",
"Accept-Ranges": "none",
"X-Image-Size": f"{width}x{height}",
},
)
@app.get("/health")
def health():
body, _, width, height = _snapshot()
status = 200 if body else 503
return {"ready": bool(body), "bytes": len(body), "size": f"{width}x{height}"}, status
if __name__ == "__main__":
threading.Thread(target=_prefetch_loop, name="prefetch", daemon=True).start()
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "80")), threaded=True)
+45
View File
@@ -0,0 +1,45 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import display
from esphome.const import CONF_DISPLAY_ID, CONF_ID, CONF_URL, CONF_X, CONF_Y
from esphome.core import CORE
DEPENDENCIES = ["display", "esp32"]
fast_jpeg_ns = cg.esphome_ns.namespace("fast_jpeg")
FastJpeg = fast_jpeg_ns.class_("FastJpeg", cg.PollingComponent)
CONF_BIG_ENDIAN = "big_endian"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(FastJpeg),
cv.Required(CONF_URL): cv.url,
cv.Required(CONF_DISPLAY_ID): cv.use_id(display.Display),
cv.Optional(CONF_X, default=0): cv.int_,
cv.Optional(CONF_Y): cv.int_,
cv.Optional(CONF_BIG_ENDIAN, default=True): cv.boolean,
}
)
.extend(cv.polling_component_schema("200ms"))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_url(config[CONF_URL]))
disp = await cg.get_variable(config[CONF_DISPLAY_ID])
cg.add(var.set_display(disp))
cg.add(var.set_x(config[CONF_X]))
if CONF_Y in config:
cg.add(var.set_y(config[CONF_Y]))
cg.add(var.set_big_endian(config[CONF_BIG_ENDIAN]))
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
if CORE.is_esp32:
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
include_builtin_idf_component("esp_http_client")
add_idf_component(name="espressif/esp-dsp", ref="1.8.2")
+292
View File
@@ -0,0 +1,292 @@
#include "fast_jpeg.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <JPEGDEC.h>
#include "esp_heap_caps.h"
#include "esp_http_client.h"
#include <algorithm>
#include <cstring>
#include <new>
#include <strings.h>
#include <utility>
namespace esphome {
namespace fast_jpeg {
static const char *const TAG = "fast_jpeg";
static const int kMaxWidth = 480;
static const int kMaxHeight = 480;
static const size_t kJpegCap = 48 * 1024;
static int align16_(int value) { return (value + 15) & ~15; }
static void *psram_alloc_(size_t size) {
void *ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (ptr == nullptr)
ptr = heap_caps_malloc(size, MALLOC_CAP_8BIT);
return ptr;
}
int jpeg_draw_cb_(JPEGDRAW *draw) {
auto *self = static_cast<FastJpeg *>(draw->pUser);
if (self == nullptr || self->back_buffer() == nullptr)
return 1;
const int img_w = self->pending_width();
const int img_h = self->pending_height();
const int stride = self->pending_stride();
if (img_w <= 0 || img_h <= 0 || stride <= 0)
return 1;
// JPEGDEC emits MCU tiles that can extend past the true image edge (270 is not
// 16-pixel aligned). Clip or we write off the end of the framebuffer and reboot.
const int x0 = draw->x;
const int y0 = draw->y;
if (x0 >= img_w || y0 >= img_h)
return 1;
const int copy_w = std::min(draw->iWidthUsed > 0 ? draw->iWidthUsed : draw->iWidth, img_w - x0);
const int copy_h = std::min(draw->iHeight, img_h - y0);
if (copy_w <= 0 || copy_h <= 0)
return 1;
auto *dst = reinterpret_cast<uint16_t *>(self->back_buffer());
const auto *src = reinterpret_cast<const uint16_t *>(draw->pPixels);
for (int row = 0; row < copy_h; row++) {
memcpy(dst + (y0 + row) * stride + x0, src + row * draw->iWidth, static_cast<size_t>(copy_w) * sizeof(uint16_t));
}
return 1;
}
esp_err_t http_event_cb_(esp_http_client_event_t *evt) {
auto *self = static_cast<FastJpeg *>(evt->user_data);
if (self == nullptr)
return ESP_OK;
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER:
if (evt->header_key != nullptr && strcasecmp(evt->header_key, "ETag") == 0 && evt->header_value != nullptr) {
strncpy(self->etag(), evt->header_value, 79);
self->etag()[79] = '\0';
}
break;
case HTTP_EVENT_ON_DATA:
if (evt->data_len <= 0)
break;
if (*self->http_rx_len() + evt->data_len > self->jpeg_cap()) {
ESP_LOGE(TAG, "JPEG larger than %u bytes", (unsigned) self->jpeg_cap());
return ESP_FAIL;
}
memcpy(self->jpeg_buf() + *self->http_rx_len(), evt->data, evt->data_len);
*self->http_rx_len() += evt->data_len;
break;
default:
break;
}
return ESP_OK;
}
void FastJpeg::setup() {
ESP_LOGI(TAG, "Setup");
this->jpeg_cap_ = kJpegCap;
this->jpeg_buf_ = static_cast<uint8_t *>(psram_alloc_(this->jpeg_cap_));
this->decoder_ = new (std::nothrow) ::JPEGDEC();
if (this->jpeg_buf_ == nullptr || this->decoder_ == nullptr) {
this->mark_failed();
ESP_LOGE(TAG, "Failed to allocate JPEG buffers");
return;
}
BaseType_t ok = xTaskCreate(task_fn_, "fast_jpeg", 8192, this, 5, &this->task_);
if (ok != pdPASS) {
this->mark_failed();
ESP_LOGE(TAG, "Failed to create fetch task");
return;
}
this->last_fps_log_ = millis();
}
void FastJpeg::dump_config() {
ESP_LOGCONFIG(TAG, "Fast JPEG stream");
ESP_LOGCONFIG(TAG, " URL: %s", this->url_.c_str());
ESP_LOGCONFIG(TAG, " Interval: %u ms", this->get_update_interval());
}
void FastJpeg::update() {
if (this->is_failed() || this->task_ == nullptr)
return;
if (this->busy_.load() || this->decoded_ready_.load())
return;
xTaskNotifyGive(this->task_);
}
void FastJpeg::loop() {
if (!this->decoded_ready_.exchange(false))
return;
std::swap(this->front_, this->back_);
this->w_ = this->pending_w_;
this->h_ = this->pending_h_;
this->stride_ = this->pending_stride_;
this->clear_bars_once_();
this->paint_();
this->frames_++;
uint32_t now = millis();
if (now - this->last_fps_log_ >= 5000) {
ESP_LOGI(TAG, "%u frames in 5s (%.1f fps), last %dx%d", this->frames_, this->frames_ / 5.0f, this->w_, this->h_);
this->frames_ = 0;
this->last_fps_log_ = now;
}
}
void FastJpeg::task_fn_(void *param) {
auto *self = static_cast<FastJpeg *>(param);
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
self->busy_.store(true);
self->fetch_once_();
self->busy_.store(false);
}
}
void FastJpeg::fetch_once_() {
if (this->url_.empty() || this->jpeg_buf_ == nullptr)
return;
esp_http_client_config_t config = {};
config.url = this->url_.c_str();
config.timeout_ms = 2000;
config.event_handler = http_event_cb_;
config.user_data = this;
config.keep_alive_enable = true;
config.method = HTTP_METHOD_GET;
if (this->http_ == nullptr) {
this->http_ = esp_http_client_init(&config);
if (this->http_ == nullptr) {
ESP_LOGE(TAG, "HTTP init failed");
return;
}
}
auto *client = static_cast<esp_http_client_handle_t>(this->http_);
esp_http_client_set_url(client, this->url_.c_str());
esp_http_client_set_header(client, "Accept", "image/jpeg");
if (this->etag_[0] != '\0')
esp_http_client_set_header(client, "If-None-Match", this->etag_);
this->http_rx_len_ = 0;
uint32_t t0 = millis();
esp_err_t err = esp_http_client_perform(client);
if (err != ESP_OK) {
ESP_LOGW(TAG, "HTTP failed: %s", esp_err_to_name(err));
return;
}
int status = esp_http_client_get_status_code(client);
if (status == 304)
return;
if (status != 200) {
ESP_LOGW(TAG, "HTTP status %d", status);
return;
}
uint32_t download_ms = millis() - t0;
ESP_LOGD(TAG, "downloaded %u bytes in %ums", (unsigned) this->http_rx_len_, (unsigned) download_ms);
if (!this->decode_jpeg_(this->http_rx_len_))
return;
}
bool FastJpeg::ensure_framebuffers_(int width, int height) {
if (width <= 0 || height <= 0 || width > kMaxWidth || height > kMaxHeight) {
ESP_LOGW(TAG, "Unsupported JPEG size %dx%d", width, height);
return false;
}
const int stride = align16_(width);
const int rows = align16_(height);
size_t need = static_cast<size_t>(stride) * static_cast<size_t>(rows) * 2;
this->pending_stride_ = stride;
if (need <= this->fb_cap_ && this->front_ != nullptr && this->back_ != nullptr)
return true;
heap_caps_free(this->front_);
heap_caps_free(this->back_);
this->front_ = nullptr;
this->back_ = nullptr;
this->front_ = static_cast<uint8_t *>(psram_alloc_(need));
this->back_ = static_cast<uint8_t *>(psram_alloc_(need));
this->fb_cap_ = need;
if (this->front_ == nullptr || this->back_ == nullptr) {
ESP_LOGE(TAG, "Framebuffer alloc failed (%u bytes)", (unsigned) need);
return false;
}
memset(this->front_, 0, need);
memset(this->back_, 0, need);
return true;
}
bool FastJpeg::decode_jpeg_(size_t len) {
if (len < 16 || this->decoder_ == nullptr)
return false;
::JPEGDEC *jpeg = this->decoder_;
if (!jpeg->openRAM(this->jpeg_buf_, static_cast<int>(len), jpeg_draw_cb_)) {
ESP_LOGW(TAG, "JPEG open failed: %d", jpeg->getLastError());
return false;
}
if (jpeg->getJPEGType() == JPEG_MODE_PROGRESSIVE) {
ESP_LOGW(TAG, "Progressive JPEG is not supported");
jpeg->close();
return false;
}
int width = jpeg->getWidth();
int height = jpeg->getHeight();
jpeg->setUserPointer(this);
if (!this->ensure_framebuffers_(width, height)) {
jpeg->close();
return false;
}
this->pending_w_ = width;
this->pending_h_ = height;
jpeg->setPixelType(this->big_endian_ ? ::RGB565_BIG_ENDIAN : ::RGB565_LITTLE_ENDIAN);
uint32_t t0 = millis();
if (!jpeg->decode(0, 0, 0)) {
ESP_LOGW(TAG, "JPEG decode failed: %d", jpeg->getLastError());
jpeg->close();
return false;
}
jpeg->close();
ESP_LOGI(TAG, "decoded %dx%d in %ums", width, height, (unsigned) (millis() - t0));
this->decoded_ready_.store(true);
return true;
}
void FastJpeg::clear_bars_once_() {
if (this->bars_cleared_ || this->display_ == nullptr || this->display_->is_failed())
return;
const int canvas_w = this->display_->get_width();
const int canvas_h = this->display_->get_height();
if (canvas_w <= 0 || canvas_h <= 0)
return;
const int y = this->y_ >= 0 ? this->y_ : (canvas_h - this->h_) / 2;
uint16_t line[kMaxWidth] = {};
const int line_w = std::min(canvas_w, kMaxWidth);
for (int row = 0; row < y; row++) {
this->display_->draw_pixels_at(0, row, line_w, 1, reinterpret_cast<const uint8_t *>(line), display::COLOR_ORDER_RGB,
display::COLOR_BITNESS_565, this->big_endian_);
}
for (int row = y + this->h_; row < canvas_h; row++) {
this->display_->draw_pixels_at(0, row, line_w, 1, reinterpret_cast<const uint8_t *>(line), display::COLOR_ORDER_RGB,
display::COLOR_BITNESS_565, this->big_endian_);
}
this->bars_cleared_ = true;
}
void FastJpeg::paint_() {
if (this->display_ == nullptr || this->display_->is_failed() || this->front_ == nullptr || this->w_ <= 0)
return;
const int canvas_h = this->display_->get_height();
const int y = this->y_ >= 0 ? this->y_ : (canvas_h - this->h_) / 2;
const int x_pad = this->stride_ > this->w_ ? this->stride_ - this->w_ : 0;
this->display_->draw_pixels_at(this->x_, y, this->w_, this->h_, this->front_, display::COLOR_ORDER_RGB,
display::COLOR_BITNESS_565, this->big_endian_, 0, 0, x_pad);
}
} // namespace fast_jpeg
} // namespace esphome
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/display/display.h"
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <string>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
class JPEGDEC;
namespace esphome {
namespace fast_jpeg {
class FastJpeg : public PollingComponent {
public:
void set_url(const std::string &url) { this->url_ = url; }
void set_display(display::Display *display) { this->display_ = display; }
void set_x(int x) { this->x_ = x; }
void set_y(int y) { this->y_ = y; }
void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; }
void setup() override;
void loop() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
uint8_t *jpeg_buf() { return this->jpeg_buf_; }
size_t jpeg_cap() const { return this->jpeg_cap_; }
size_t *http_rx_len() { return &this->http_rx_len_; }
char *etag() { return this->etag_; }
uint8_t *back_buffer() { return this->back_; }
int pending_width() const { return this->pending_w_; }
int pending_height() const { return this->pending_h_; }
int pending_stride() const { return this->pending_stride_; }
protected:
static void task_fn_(void *param);
void fetch_once_();
bool decode_jpeg_(size_t len);
bool ensure_framebuffers_(int width, int height);
void paint_();
void clear_bars_once_();
std::string url_;
display::Display *display_{nullptr};
int x_{0};
int y_{-1};
bool big_endian_{true};
uint8_t *front_{nullptr};
uint8_t *back_{nullptr};
uint8_t *jpeg_buf_{nullptr};
::JPEGDEC *decoder_{nullptr};
size_t fb_cap_{0};
size_t jpeg_cap_{0};
int pending_w_{0};
int pending_h_{0};
int pending_stride_{0};
int w_{0};
int h_{0};
int stride_{0};
TaskHandle_t task_{nullptr};
void *http_{nullptr};
size_t http_rx_len_{0};
std::atomic<bool> busy_{false};
std::atomic<bool> decoded_ready_{false};
bool bars_cleared_{false};
char etag_[80]{};
uint32_t frames_{0};
uint32_t last_fps_log_{0};
};
} // namespace fast_jpeg
} // namespace esphome
+23
View File
@@ -0,0 +1,23 @@
# Portainer: Stacks → Add stack → upload this file + Dockerfile/app.py/requirements.txt
# (Web editor alone can't build; use Git stack or pre-built image — see notes below.)
services:
resizer:
build: .
image: doorbell-resizer:latest
container_name: doorbell-resizer
ports:
- "8080:80"
environment:
# Reachable from the Portainer host (LAN IP or Docker DNS if same compose network)
FRIGATE_URL: http://10.0.1.197:5050/api/doorbell/latest.jpg?h=270
TARGET_WIDTH: "480"
JPEG_QUALITY: "40"
FETCH_TIMEOUT: "5"
PREFETCH_HZ: "10"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1/health')"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped
+202
View File
@@ -0,0 +1,202 @@
# Waveshare ESP32-S3-Touch-LCD-4B — doorbell JPEG viewer
# Copy this file AND the components/ folder into your ESPHome config directory.
# Pair with the resizer proxy in this repo.
#
# Uses a custom fast_jpeg component (RGB565 block decode + DMA blit).
# Stock online_image JPEG is too slow for ~5 fps.
substitutions:
name: doorbell-display
friendly_name: Doorbell Display
image_url: "http://10.0.1.70:8080/"
external_components:
- source:
type: local
path: components
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: "2025.2.0"
on_boot:
- priority: 800
then:
- lambda: |-
auto *io = id(ext_io);
io->pin_mode(0, esphome::gpio::FLAG_OUTPUT);
io->pin_mode(1, esphome::gpio::FLAG_OUTPUT);
io->pin_mode(2, esphome::gpio::FLAG_OUTPUT);
io->pin_mode(7, esphome::gpio::FLAG_OUTPUT);
io->digital_write(0, true);
io->digital_write(1, false);
io->digital_write(2, false);
io->digital_write(7, false);
- delay: 120ms
- lambda: |-
id(ext_io)->digital_write(7, true);
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y"
CONFIG_SPIRAM_RODATA: "y"
advanced:
include_builtin_idf_components:
- esp_http_client
psram:
mode: octal
speed: 80MHz
logger:
logs:
fast_jpeg: INFO
api:
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
i2c:
sda: GPIO47
scl: GPIO48
frequency: 400kHz
scan: true
pca9554:
- id: ext_io
address: 0x20
pin_count: 8
spi:
- id: disp_spi
interface: software
clk_pin:
pca9554: ext_io
number: 2
mode:
output: true
mosi_pin:
pca9554: ext_io
number: 1
mode:
output: true
output:
- platform: ledc
id: backlight_pwm
inverted: true
pin: GPIO4
light:
- platform: monochromatic
id: backlight
name: "Backlight"
output: backlight_pwm
restore_mode: ALWAYS_ON
default_transition_length: 0s
# Simple on/off for Home Assistant automations (light still exposes brightness).
switch:
- platform: template
name: "Screen"
id: screen_power
icon: mdi:monitor
lambda: |-
return id(backlight).current_values.is_on();
turn_on_action:
- light.turn_on:
id: backlight
brightness: 100%
turn_off_action:
- light.turn_off: backlight
display:
- platform: mipi_rgb
id: lcd
model: WAVESHARE-4-480x480
dimensions: 480x480
color_order: RGB
auto_clear_enabled: false
update_interval: never
spi_id: disp_spi
cs_pin:
pca9554: ext_io
number: 0
mode:
output: true
reset_pin:
pca9554: ext_io
number: 7
mode:
output: true
de_pin: GPIO17
hsync_pin:
number: GPIO46
ignore_strapping_warning: true
vsync_pin:
number: GPIO3
ignore_strapping_warning: true
pclk_pin: GPIO9
pclk_inverted: false
data_pins:
red: [GPIO10, GPIO11, GPIO12, GPIO13, GPIO14]
green: [GPIO21, GPIO8, GPIO18, GPIO45, GPIO38, GPIO39]
blue: [GPIO40, GPIO41, GPIO42, GPIO2, GPIO1]
init_sequence:
- [0xFF, 0x77, 0x01, 0x00, 0x00, 0x10]
- [0xC0, 0x3B, 0x00]
- [0xC1, 0x0D, 0x02]
- [0xC2, 0x21, 0x08]
- [0xCD, 0x08]
- [0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18]
- [0xB1, 0x00, 0x11, 0x19, 0x0E, 0x12, 0x07, 0x08, 0x08, 0x08, 0x22, 0x04, 0x11, 0x11, 0xA9, 0x32, 0x18]
- [0xFF, 0x77, 0x01, 0x00, 0x00, 0x11]
- [0xB0, 0x60]
- [0xB1, 0x30]
- [0xB2, 0x87]
- [0xB3, 0x80]
- [0xB5, 0x49]
- [0xB7, 0x85]
- [0xB8, 0x21]
- [0xC1, 0x78]
- [0xC2, 0x78]
- delay 20ms
- [0xE0, 0x00, 0x1B, 0x02]
- [0xE1, 0x08, 0xA0, 0x00, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x44, 0x44]
- [0xE2, 0x11, 0x11, 0x44, 0x44, 0xED, 0xA0, 0x00, 0x00, 0xEC, 0xA0, 0x00, 0x00]
- [0xE3, 0x00, 0x00, 0x11, 0x11]
- [0xE4, 0x44, 0x44]
- [0xE5, 0x0A, 0xE9, 0xD8, 0xA0, 0x0C, 0xEB, 0xD8, 0xA0, 0x0E, 0xED, 0xD8, 0xA0, 0x10, 0xEF, 0xD8, 0xA0]
- [0xE6, 0x00, 0x00, 0x11, 0x11]
- [0xE7, 0x44, 0x44]
- [0xE8, 0x09, 0xE8, 0xD8, 0xA0, 0x0B, 0xEA, 0xD8, 0xA0, 0x0D, 0xEC, 0xD8, 0xA0, 0x0F, 0xEE, 0xD8, 0xA0]
- [0xEB, 0x02, 0x00, 0xE4, 0xE4, 0x88, 0x00, 0x40]
- [0xEC, 0x3C, 0x00]
- [0xED, 0xAB, 0x89, 0x76, 0x54, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x45, 0x67, 0x98, 0xBA]
- [0xFF, 0x77, 0x01, 0x00, 0x00, 0x00]
- [0x36, 0x00]
- [0x3A, 0x66]
fast_jpeg:
id: doorbell_jpg
url: ${image_url}
display_id: lcd
update_interval: 200ms
# If colours look swapped, set big_endian: false
big_endian: true
+3
View File
@@ -0,0 +1,3 @@
flask==3.1.2
pillow==11.3.0
requests==2.32.5