Show a spinner while a screen starts and while a camera loads, bring a camera's first picture sooner, and round the alert's camera picture (0.2.87)
Starting screen: until the first layout, render() keeps the status text
("Connecting to Home Assistant", "Waiting for ESP Screens") and a spinner
as one block in the middle of the page; the first layout deletes both.
The room label stays empty meanwhile, and ROOM_NAME defaults to "".
Camera: the full screen shows a spinner instead of "Loading image". The
screen asks when the camera opens and loads a link as soon as it comes
(camera_load, never under a finger) instead of waiting for the 250 ms
tick twice, and online_image reads 16 KB per loop instead of 4 KB. On the
bench Guition with the EZVIZ camera: cold 5.8 -> 4.55 s, warm 3.4 -> 2.06 s.
Alert picture: the image takes the frame's radius (18, the alert card's);
LVGL's software renderer clips it row by row, without a layer.
One spinner_create() makes the busy sheet's, the starting screen's and the
camera's spinner; theme role CAMERA_TRACK colours the camera's ring. The
README gets a row of camera pictures; docs/CAMERA.md, RELEASING.md and
TEST_RESULTS_0287.md describe the measurements.
Checked with tools/check.sh (473 Python tests, 19/19 C++, editor) and
--firmware: CYD 1,678,816 B (91.5 %, +1,312 B), Guition 2,055,280 B
(25.3 %). Host renders of both boards in both looks; four OTA builds on
the bench Guition Wallbox, which runs this firmware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ lights, the heating or the vacuum.
|
||||
|
||||
**What.** Firmware for two affordable panels, the 2.8-inch CYD and the 4-inch Guition, with up to
|
||||
48 tiles over eight pages: lights, climate, blinds and curtains, the vacuum, media, the weather,
|
||||
history graphs, clocks and timers. A tile can take the whole page, one big switch you push without
|
||||
history graphs, clocks and timers, and on a Guition your cameras. A tile can take the whole page, one big switch you push without
|
||||
looking, and a tile can go to another page. An automation can put an alert on every screen when someone rings
|
||||
the bell, and a Guition shows who is there with the doorbell camera's picture. A tap can run any
|
||||
action Home Assistant has for a tile, and Dark mode suits a screen beside the bed.
|
||||
@@ -62,6 +62,12 @@ also be changed on the screen or by an automation, and your own YAML for one scr
|
||||
<img src="docs/images/guition-dark-media.png" width="32%" alt="The media card in dark mode: the same cover and keys on a black page">
|
||||
</p>
|
||||
<p align="center"><sub>Tap a tile to switch it, hold it for the full card. Keys and sliders right on the tile, pastel colors, a clock, the weather and history you can read with a finger. A lamp with modes (a WLED) gets an effects page with a drum picker, named and filled from what Home Assistant lists for it. The media card shows what plays with its album cover, fetched by ESP Screens like a camera picture (<a href="docs/CAMERA.md">how</a>). Rendered from the firmware's own LVGL code with a demo home.</sub></p>
|
||||
<p align="center">
|
||||
<img src="docs/images/guition-camera-tiles.png" width="32%" alt="Cameras as tiles on the Guition: the front door camera, the garden camera live, the doorbell's last ring, next to the door, the porch light and the garage door">
|
||||
<img src="docs/images/guition-camera.png" width="32%" alt="A camera tile tapped: the front door camera full screen, with the round back key and the camera's name at the top">
|
||||
<img src="docs/images/guition-alert-camera.png" width="32%" alt="The same camera in an alert: its picture across the top of the card, with the card's rounded corners">
|
||||
</p>
|
||||
<p align="center"><sub>Cameras are tiles too, not only part of an alert: any camera or snapshot in Home Assistant (a doorbell's last ring, for example) goes on a Guition like any other tile. Tap it for the picture full screen, refreshed every four seconds; an alert can carry the same picture. The CYD has no memory for pictures (<a href="docs/CAMERA.md">how it works</a>).</sub></p>
|
||||
<p align="center">
|
||||
<img src="docs/images/cyd-home.png" width="32%" alt="CYD 2.8-inch screen: the weather forecast, a kitchen timer, the coffee machine, a lamp and power usage as a large value">
|
||||
<img src="docs/images/cyd-page-2.png" width="32%" alt="Second CYD page: Sonos volume, presence, a scene and an energy graph">
|
||||
|
||||
@@ -117,6 +117,11 @@ on the screen itself, and how updates work.
|
||||
off shows a power key. A Guition fetches the cover through ESP Screens, like a camera picture
|
||||
([docs/CAMERA.md](docs/CAMERA.md)); the CYD shows the player's icon in its place. A media tile of
|
||||
size **Full page** is the same card on the page, with the keys and the volume working on the tile.
|
||||
- **Cameras** on a Guition (app 0.2.66 / firmware 0.2.57): a `camera.*` entity, or an `image.*` one such
|
||||
as a doorbell's last ring, is a tile like any other. A tap opens the picture full screen, refreshed every
|
||||
four seconds, with the round back key; standby and **Back to page 1** close it. The same camera can bring its
|
||||
picture to an alert ([With a camera picture](#with-a-camera-picture)). The CYD has no memory for pictures and
|
||||
the editor doesn't offer it camera tiles. How the picture travels: [docs/CAMERA.md](docs/CAMERA.md).
|
||||
- **History card** for sensors, numbers, binary sensors, people, and switches, the way Home
|
||||
Assistant shows history: a line with an axis in round steps and clock times for numbers,
|
||||
with the highest and lowest moment, and a timeline with the time in each state for on/off,
|
||||
@@ -297,6 +302,10 @@ actions:
|
||||
camera: camera.front_door
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/guition-camera-tiles.png" width="41%" alt="Cameras as tiles on the Guition: the front door camera, the garden camera live, the doorbell's last ring, next to the door, the porch light and the garage door">
|
||||
</p>
|
||||
|
||||
A camera or image entity also works as a **tile** on a Guition: a tap opens it full screen, refreshed
|
||||
every four seconds. How the image travels (port 8098 of the app, no token on the screen) is in
|
||||
[docs/CAMERA.md](docs/CAMERA.md).
|
||||
|
||||
@@ -2875,6 +2875,26 @@ inline void control_event(lv_event_t *e) {
|
||||
auto a=tile_controls::press_key(t,command,w.key_args[n]);
|
||||
if(a.valid())action(a.service,t.entity,a.key,a.value);
|
||||
}
|
||||
// The one spinner of the firmware: a busy card, the starting screen and a camera that loads (firmware 0.2.73+). The
|
||||
// ring in the spinner paint, the arc in Home Assistant's blue. Null where the board builds no spinner.
|
||||
inline lv_obj_t *spinner_create(lv_obj_t *parent, int size, int arc) {
|
||||
#if LV_USE_SPINNER
|
||||
auto *spinner = lv_spinner_create(parent);
|
||||
lv_spinner_set_anim_params(spinner, 900, 200);
|
||||
lv_obj_set_size(spinner, size, size);
|
||||
lv_obj_remove_flag(spinner, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_style_arc_width(spinner, arc, LV_PART_MAIN);
|
||||
lv_obj_set_style_arc_width(spinner, arc, LV_PART_INDICATOR);
|
||||
lv_obj_add_style(spinner, theme::style(theme::Paint::spinner), LV_PART_MAIN);
|
||||
lv_obj_set_style_arc_color(spinner, lv_color_hex(theme::ha::RAIN), LV_PART_INDICATOR);
|
||||
lv_obj_set_style_bg_opa(spinner, LV_OPA_TRANSP, LV_PART_KNOB);
|
||||
lv_obj_set_style_pad_all(spinner, 0, LV_PART_KNOB);
|
||||
return spinner;
|
||||
#else
|
||||
(void) parent; (void) size; (void) arc;
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
// A busy card is covered by a translucent white sheet with a small spinner until
|
||||
// Home Assistant confirms; the sheet also swallows taps meanwhile.
|
||||
inline void set_busy(Widgets &w,bool busy,bool large){
|
||||
@@ -2883,13 +2903,8 @@ inline void set_busy(Widgets &w,bool busy,bool large){
|
||||
w.busy=lv_obj_create(w.tile);lv_obj_remove_style_all(w.busy);lv_obj_remove_flag(w.busy,LV_OBJ_FLAG_SCROLLABLE);lv_obj_add_flag(w.busy,LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_style(w.busy,theme::style(theme::Paint::veil),0);lv_obj_set_style_bg_opa(w.busy,LV_OPA_60,0);
|
||||
lv_obj_set_style_radius(w.busy,lv_obj_get_style_radius(w.tile,LV_PART_MAIN),0);
|
||||
#if LV_USE_SPINNER
|
||||
w.spinner=lv_spinner_create(w.busy);lv_spinner_set_anim_params(w.spinner,900,200);
|
||||
int size=large?30:20;lv_obj_set_size(w.spinner,size,size);lv_obj_center(w.spinner);lv_obj_remove_flag(w.spinner,LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_style_arc_width(w.spinner,large?4:3,LV_PART_MAIN);lv_obj_set_style_arc_width(w.spinner,large?4:3,LV_PART_INDICATOR);
|
||||
lv_obj_add_style(w.spinner,theme::style(theme::Paint::spinner),LV_PART_MAIN);lv_obj_set_style_arc_color(w.spinner,lv_color_hex(theme::ha::RAIN),LV_PART_INDICATOR);
|
||||
lv_obj_set_style_bg_opa(w.spinner,LV_OPA_TRANSP,LV_PART_KNOB);lv_obj_set_style_pad_all(w.spinner,0,LV_PART_KNOB);
|
||||
#endif
|
||||
w.spinner=spinner_create(w.busy,large?30:20,large?4:3);
|
||||
if(w.spinner)lv_obj_center(w.spinner);
|
||||
}
|
||||
// Cover the whole card, padding included, at the width the card asks for: a slot that just turned
|
||||
// wide is still single width in LVGL's own coordinates. Unchanged sizes cost LVGL nothing.
|
||||
@@ -3297,11 +3312,45 @@ inline lv_timer_t *fill_timer=nullptr;
|
||||
inline bool fill_refreshed=false;
|
||||
inline uint32_t fill_step_ms=0;
|
||||
inline bool fill_cards(size_t cards);
|
||||
// The starting screen (firmware 0.2.73+): what the screen waits for in the middle of the page with a spinner under it,
|
||||
// until the first layout arrives. The first render() makes it and the first layout deletes it, spinner and all.
|
||||
inline lv_obj_t *boot_panel = nullptr, *boot_text = nullptr, *boot_spinner = nullptr;
|
||||
inline void boot_status(lv_obj_t *page, const char *text) {
|
||||
const int width = lv_display_get_horizontal_resolution(lv_obj_get_display(page));
|
||||
const bool large = width >= 480;
|
||||
const int ring = large ? 48 : 32, gap = large ? 24 : 16, text_width = width - 2 * lv_obj_get_style_x(room_label, LV_PART_MAIN);
|
||||
const lv_font_t *font = watch_font ? watch_font : lv_obj_get_style_text_font(room_label, LV_PART_MAIN);
|
||||
if (!boot_panel) {
|
||||
boot_panel = lv_obj_create(page);
|
||||
lv_obj_remove_style_all(boot_panel);
|
||||
lv_obj_remove_flag(boot_panel, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(boot_panel, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(boot_panel, lv_pct(100), lv_pct(100));
|
||||
// The name's place in the drawing order: under the tiles, the cards and an alert.
|
||||
lv_obj_move_to_index(boot_panel, lv_obj_get_index(room_label));
|
||||
boot_text = lv_label_create(boot_panel);
|
||||
lv_obj_add_style(boot_text, theme::style(theme::Paint::ink), 0);
|
||||
lv_obj_set_style_text_font(boot_text, font, 0);
|
||||
lv_obj_set_style_text_align(boot_text, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_label_set_long_mode(boot_text, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(boot_text, text_width);
|
||||
boot_spinner = spinner_create(boot_panel, ring, large ? 5 : 4);
|
||||
}
|
||||
if (strcmp(lv_label_get_text(boot_text), text) == 0) return;
|
||||
lv_label_set_text(boot_text, text);
|
||||
// The text and the spinner as one block in the middle of the page.
|
||||
lv_point_t size;
|
||||
lv_text_get_size(&size, text, font, 0, 0, text_width, LV_TEXT_FLAG_NONE);
|
||||
lv_obj_align(boot_text, LV_ALIGN_CENTER, 0, -(ring + gap) / 2);
|
||||
if (boot_spinner) lv_obj_align(boot_spinner, LV_ALIGN_CENTER, 0, (size.y + gap) / 2);
|
||||
}
|
||||
// Before the first layout the screen is starting: HA connects, then ESP Screens sends the tiles.
|
||||
inline void render(lv_obj_t *room) {
|
||||
if (!enabled) return;
|
||||
room_label=room; swipe_profile::Lap lap;
|
||||
label(room, !model.configured ? (!ha_connected() ? "Connecting to Home Assistant..." : "Waiting for ESP Screens...") : !model.ready() ? "Loading tiles..." : !ha_connected() ? "HA not connected" : !feed_alive() ? "ESP Screens not active" : model.title);
|
||||
if (!model.configured) boot_status(lv_obj_get_parent(room), !ha_connected() ? "Connecting to Home Assistant" : "Waiting for ESP Screens");
|
||||
else if (boot_panel) { lv_obj_delete(boot_panel); boot_panel = boot_text = boot_spinner = nullptr; }
|
||||
label(room, !model.configured ? "" : !model.ready() ? "Loading tiles..." : !ha_connected() ? "HA not connected" : !feed_alive() ? "ESP Screens not active" : model.title);
|
||||
render_header();
|
||||
lap(swipe_profile::HEADER);
|
||||
bool all=dirty_all || (!dirty_tiles && !dirty_header);
|
||||
@@ -3927,6 +3976,8 @@ inline camera_view::Feed camera;
|
||||
// show the page below at once.
|
||||
inline bool camera_release_due = false;
|
||||
inline lv_obj_t *camera_root = nullptr, *camera_picture = nullptr, *camera_note = nullptr, *camera_back = nullptr, *camera_title = nullptr;
|
||||
// Turns in the middle until the first image is there (firmware 0.2.73+); a note (no image) takes its place.
|
||||
inline lv_obj_t *camera_spinner = nullptr;
|
||||
// The alert's image: the board's frame at the bottom left of the card, the camera the app announced for the next alert
|
||||
// and the one the card on screen shows.
|
||||
inline lv_obj_t *alert_frame = nullptr, *alert_picture = nullptr, *alert_frame_icon = nullptr;
|
||||
@@ -4061,6 +4112,7 @@ inline void camera_request(const std::string &entity, int size, uint32_t backgro
|
||||
|
||||
inline void camera_note_text(const char *text) {
|
||||
if (!camera_note) return;
|
||||
if (camera_spinner) { lv_obj_delete(camera_spinner); camera_spinner = nullptr; }
|
||||
lv_label_set_text(camera_note, text);
|
||||
if (text[0]) lv_obj_remove_flag(camera_note, LV_OBJ_FLAG_HIDDEN);
|
||||
else lv_obj_add_flag(camera_note, LV_OBJ_FLAG_HIDDEN);
|
||||
@@ -4074,7 +4126,7 @@ inline void camera_release() {
|
||||
inline void camera_close() {
|
||||
if (!camera_root) return;
|
||||
lv_obj_delete(camera_root);
|
||||
camera_root = camera_picture = camera_note = camera_back = camera_title = nullptr;
|
||||
camera_root = camera_picture = camera_note = camera_back = camera_title = camera_spinner = nullptr;
|
||||
camera_release_due = true;
|
||||
ESP_LOGI("camera", "closed %s", camera.entity.c_str());
|
||||
camera = camera_view::Feed{};
|
||||
@@ -4102,7 +4154,13 @@ inline void camera_open(const std::string &entity, const std::string &name) {
|
||||
if (detail_font) lv_obj_set_style_text_font(camera_note, detail_font, 0);
|
||||
lv_obj_set_style_text_color(camera_note, theme::color(theme::CAMERA_NOTE), 0);
|
||||
lv_obj_center(camera_note);
|
||||
camera_note_text("Loading image");
|
||||
camera_note_text("");
|
||||
// The starting screen's spinner, its ring dark on the black page in both looks.
|
||||
camera_spinner = spinner_create(camera_root, large ? 48 : 32, large ? 5 : 4);
|
||||
if (camera_spinner) {
|
||||
lv_obj_set_style_arc_color(camera_spinner, theme::color(theme::CAMERA_TRACK), LV_PART_MAIN);
|
||||
lv_obj_center(camera_spinner);
|
||||
}
|
||||
// The same top bar as a tile's card: a round back arrow at the left, the name centred.
|
||||
const int bar = large ? 60 : 40, bar_x = large ? 16 : 10, bar_y = large ? 16 : 8;
|
||||
camera_back = lv_obj_create(camera_root);
|
||||
@@ -4133,6 +4191,20 @@ inline void camera_open(const std::string &entity, const std::string &name) {
|
||||
lv_obj_set_size(camera_title, width - 2 * (bar_x + bar + 8), title_font ? lv_font_get_line_height(title_font) : 20);
|
||||
lv_label_set_text(camera_title, name.c_str());
|
||||
ESP_LOGI("camera", "open %s", entity.c_str());
|
||||
// Asked for now rather than on the next tick (firmware 0.2.73+): the answer is most of the wait.
|
||||
const uint32_t now = esphome::millis();
|
||||
if (awake() && fresh() && camera.should_ask(now)) {
|
||||
camera.ask(now);
|
||||
camera_request(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// The next image, never under a finger: a load that starts now would hold up the tap on its way.
|
||||
inline void camera_load(uint32_t now) {
|
||||
auto *input = lv_indev_get_next(nullptr);
|
||||
if (input && lv_indev_get_state(input) == LV_INDEV_STATE_PRESSED) return;
|
||||
camera.start(now);
|
||||
camera_full.load(camera.url);
|
||||
}
|
||||
|
||||
// The board's interval (250 ms): ask for a link, or load the image again when it is time. Loading happens here, in
|
||||
@@ -4156,11 +4228,7 @@ inline void camera_tick() {
|
||||
camera.ask(now);
|
||||
camera_request(camera.entity);
|
||||
} else if (camera.should_load(now)) {
|
||||
// Not under a finger: a load that starts now would hold up the tap that is on its way.
|
||||
auto *input = lv_indev_get_next(nullptr);
|
||||
if (input && lv_indev_get_state(input) == LV_INDEV_STATE_PRESSED) return;
|
||||
camera.start(now);
|
||||
camera_full.load(camera.url);
|
||||
camera_load(now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4202,7 +4270,13 @@ inline void camera_answer(const std::string &view, const std::string &entity, co
|
||||
if (view == "full") {
|
||||
if (!camera_root || camera.entity != entity) return;
|
||||
camera.link(url);
|
||||
if (url.empty() && !camera.shown) camera_note_text("No image from this camera");
|
||||
if (url.empty()) {
|
||||
if (!camera.shown) camera_note_text("No image from this camera");
|
||||
return;
|
||||
}
|
||||
// Loaded now rather than on the next tick (firmware 0.2.73+), as an alert's image is.
|
||||
const uint32_t now = esphome::millis();
|
||||
if (awake() && camera.should_load(now)) camera_load(now);
|
||||
return;
|
||||
}
|
||||
if (view == "cover") { // the media card's album cover (firmware 0.2.64+)
|
||||
@@ -4233,12 +4307,16 @@ inline void camera_answer(const std::string &view, const std::string &entity, co
|
||||
}
|
||||
|
||||
// LVGL's image widget is only built for a board whose profile draws images (the Guition's hidden seed); the CYD's has none.
|
||||
inline void camera_show(lv_obj_t *parent, lv_obj_t *&picture, lv_image_dsc_t *source, bool fresh_pixels) {
|
||||
// `radius` rounds the picture's corners (the alert's, firmware 0.2.73+): LVGL 9.5's software renderer clips an image
|
||||
// to its own radius row by row with a one-row mask (radius_only in lv_draw_sw_img.c), without a layer; clip_corner on
|
||||
// the frame would draw the frame into a layer of its size instead.
|
||||
inline void camera_show(lv_obj_t *parent, lv_obj_t *&picture, lv_image_dsc_t *source, bool fresh_pixels, int32_t radius = 0) {
|
||||
#if LV_USE_IMAGE
|
||||
if (!source || !source->data) return;
|
||||
if (!picture) {
|
||||
picture = lv_image_create(parent);
|
||||
lv_obj_remove_flag(picture, LV_OBJ_FLAG_CLICKABLE);
|
||||
if (radius > 0) lv_obj_set_style_radius(picture, radius, LV_PART_MAIN);
|
||||
lv_image_set_src(picture, source);
|
||||
lv_obj_center(picture);
|
||||
return;
|
||||
@@ -4249,7 +4327,7 @@ inline void camera_show(lv_obj_t *parent, lv_obj_t *&picture, lv_image_dsc_t *so
|
||||
lv_image_set_src(picture, source);
|
||||
lv_obj_invalidate(picture);
|
||||
#else
|
||||
(void) parent; (void) picture; (void) source; (void) fresh_pixels;
|
||||
(void) parent; (void) picture; (void) source; (void) fresh_pixels; (void) radius;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -4258,7 +4336,8 @@ inline void camera_loaded(bool thumb, bool cached) {
|
||||
if (thumb) {
|
||||
alert_thumb_loading = false;
|
||||
if (alert_camera.empty() || !alert_frame) return;
|
||||
camera_show(alert_frame, alert_picture, camera_thumb.source(), !cached);
|
||||
// The picture takes the frame's radius, the alert card's own (the board profile sets it on the frame).
|
||||
camera_show(alert_frame, alert_picture, camera_thumb.source(), !cached, lv_obj_get_style_radius(alert_frame, LV_PART_MAIN));
|
||||
if (alert_picture && alert_frame_icon) lv_obj_add_flag(alert_frame_icon, LV_OBJ_FLAG_HIDDEN);
|
||||
ESP_LOGI("camera", "alert picture shown");
|
||||
return;
|
||||
|
||||
@@ -76,7 +76,8 @@ enum Role : uint8_t {
|
||||
// ---- a camera image: dark in both looks, like a photo viewer (firmware 0.2.57+)
|
||||
CAMERA_PAGE, // behind the image full screen and in the alert's image frame
|
||||
CAMERA_INK, // the camera's name over the image
|
||||
CAMERA_NOTE, // "Loading image", "No image from this camera"
|
||||
CAMERA_NOTE, // "No image from this camera"
|
||||
CAMERA_TRACK, // the ring of the spinner while the first image loads (firmware 0.2.73+)
|
||||
// ---- the CYD's touch calibration: dark in both looks, a yellow crosshair
|
||||
CALIBRATION_PAGE,
|
||||
CALIBRATION_INK,
|
||||
@@ -144,6 +145,7 @@ inline constexpr Pair ROLES[ROLE_COUNT] = {
|
||||
/* CAMERA_PAGE */ {0x000000, 0x000000},
|
||||
/* CAMERA_INK */ {0xF2F2F2, 0xDADADA},
|
||||
/* CAMERA_NOTE */ {0x9E9E9E, 0x8A8A8A},
|
||||
/* CAMERA_TRACK */ {0x393D42, 0x393D42},
|
||||
/* CALIBRATION_PAGE */ {0x101820, 0x101820},
|
||||
/* CALIBRATION_INK */ {0xFFFFFF, 0xFFFFFF},
|
||||
/* CALIBRATION_MARK */ {0xFFD34D, 0xFFD34D},
|
||||
|
||||
+17
-7
@@ -3,11 +3,13 @@
|
||||
App 0.2.66 with firmware 0.2.57 shows camera images on a Guition 4848S040:
|
||||
|
||||
- **A camera tile.** Add a `camera.*` or `image.*` entity as a tile. A tap opens the image full
|
||||
screen, with the round back key at the top left like every card. The image is refreshed every
|
||||
four seconds while it is open. It is not video: ESPHome has no video decoder.
|
||||
screen, with the round back key at the top left like every card, and a spinner until the first
|
||||
image is there (firmware 0.2.73). The image is refreshed every four seconds while it is open. It
|
||||
is not video: ESPHome has no video decoder.
|
||||
- **An alert with a picture.** Add `camera: camera.front_door` to the `esp_screens_show_alert`
|
||||
event. The card shows the picture of that moment across its top (it stays that picture); a tap
|
||||
on it opens the camera full screen over the alert, and Back returns to the alert.
|
||||
event. The card shows the picture of that moment across its top (it stays that picture), with
|
||||
the card's round corners (firmware 0.2.73); a tap on it opens the camera full screen over the
|
||||
alert, and Back returns to the alert.
|
||||
|
||||
The CYD has no memory for images (a 320×180 image needs 115 KB in one piece, the CYD's largest
|
||||
free block is about 45 KB). It shows the alert without the picture, and the editor doesn't offer
|
||||
@@ -70,9 +72,10 @@ screen's made the picture change after 1.5 s one time and 6 s the next.) A slow
|
||||
images older, never the screen slower, and nothing queues up. Nobody loading means nothing fetched.
|
||||
A link that nobody loads for two minutes stops working; an alert's picture stays for half an hour.
|
||||
|
||||
**Why BMP.** ESPHome decodes a BMP piece by piece while it downloads (4 KB per round of its main
|
||||
loop). A JPEG of the full screen took 0.6 s in one piece on the Guition, during which the screen
|
||||
missed taps. A full-screen BMP is about 390 KB; on the LAN that takes one to three seconds.
|
||||
**Why BMP.** ESPHome decodes a BMP piece by piece while it downloads (16 KB per round of its main
|
||||
loop since firmware 0.2.73, 4 KB before). A JPEG of the full screen took 0.6 s in one piece on the
|
||||
Guition, during which the screen missed taps. A full-screen BMP is about 390 KB; on the bench
|
||||
Guition it comes in about 1.8 s (2.8 s with 4 KB).
|
||||
|
||||
## Network
|
||||
|
||||
@@ -91,6 +94,13 @@ missed taps. A full-screen BMP is about 390 KB; on the LAN that takes one to thr
|
||||
- The internal heap stays level while a camera refreshes: 77.2 KB free after 91 images in six minutes,
|
||||
and PSRAM unchanged.
|
||||
- Standby, **Back to page 1** and a new layout close the camera; nothing loads in standby.
|
||||
- The first image (firmware 0.2.73, measured 2026-09-19 with an EZVIZ camera): the screen asks when
|
||||
the camera opens and loads the link as soon as it comes. About 2 s when the app still has the
|
||||
camera's last snapshot (it keeps one for 30 s after the last load), 4.5 s when it must ask Home
|
||||
Assistant first, of which 2.4 s is the camera's own snapshot. Firmware 0.2.72 took 3.4 s and
|
||||
5.8 s. Details in docs/TEST_RESULTS_0287.md.
|
||||
- `power_save_mode: none` under `wifi:` (the YAML ESP Screens writes has it since app 0.2.24) keeps
|
||||
the screen from waiting on its Wi-Fi now and then while an image comes in.
|
||||
|
||||
## For developers
|
||||
|
||||
|
||||
@@ -219,6 +219,32 @@ icons sit off-center in the browser).
|
||||
The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual
|
||||
profiles must come from the set. No changed preferences or keys.
|
||||
|
||||
### Compatibility 0.2.87 / firmware 0.2.73
|
||||
|
||||
Firmware only, plus README pictures and docs/CAMERA.md. No protocol, storage, preference or key change: an older app
|
||||
drives this firmware as before, and the camera messages are the same; only when the screen sends and loads changed.
|
||||
|
||||
- Starting screen: while `!model.configured`, `render()` calls `boot_status()`, which keeps a text in `watch_font`
|
||||
(paint `ink`) and a spinner as one block in the middle of the page, at the room label's index in the drawing order;
|
||||
the room label is empty meanwhile. The first layout deletes the block (`boot_panel`), spinner and all.
|
||||
`ROOM_NAME` defaults to "" (was "Choose tiles in HA").
|
||||
- `spinner_create(parent, size, arc)` makes every spinner: the busy sheet (30/20 px, unchanged), the starting screen
|
||||
and the camera (48/32 px).
|
||||
- Camera full screen: `camera_open()` asks at once (awake and fresh); `camera_answer()` loads a new link at once
|
||||
through `camera_load()`, which like `camera_tick()` never starts under a finger. `camera_tick()` still does the
|
||||
4 s refresh and the retries. `camera_spinner` turns until the first picture or a note; its ring is the new theme
|
||||
role `CAMERA_TRACK` (0x393D42 in both looks). "Loading image" is gone.
|
||||
- Guition `online_image` `buffer_size` 16384 (was 4096) for `camera_image` and `alert_image`: the full picture in
|
||||
about 1.8 s instead of 2.8 s on the bench; without Wi-Fi power save no loop over 50 ms. 32 KB held a loop 0.1 s.
|
||||
- Alert picture: `alert_image_frame` has the alert card's radius (18) and `camera_show()` gives the alert's picture the
|
||||
frame's radius. LVGL 9.5 clips an image to its own radius row by row (`radius_only` in `lv_draw_sw_img.c`) without a
|
||||
layer, so `clip_corner` stays out (`tests/test_layer_free.py`). About 5 % more render time for the picture on the
|
||||
host, only when it is drawn.
|
||||
- Tests: `test_camera` (the first picture waits for no tick), `test_header_bar` (the starting screen;
|
||||
`render_header()` anywhere in `render()`), `test_theme` (the spinner paint in `spinner_create()`).
|
||||
|
||||
Details: docs/TEST_RESULTS_0287.md.
|
||||
|
||||
### Compatibility 0.2.85 / firmware 0.2.71
|
||||
|
||||
Tile colours as Home Assistant draws them. No protocol, storage or editor change: the state messages are the same as
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Test results app 0.2.87 / firmware 0.2.73 (2026-09-19)
|
||||
|
||||
After the README picture refresh Max asked for four things:
|
||||
|
||||
- cameras in the README as tiles of their own ("zodat mensen snappen dat camera niet alleen tijdens alert is");
|
||||
- the alert card's rounded corners on its camera picture, if that stays light;
|
||||
- the spinner instead of "Loading image" on a camera that opens, and a look at why its first picture takes so long;
|
||||
- a nicer starting screen: the text in the middle, a spinner under it.
|
||||
|
||||
Performance comes first for him, and the add-on often runs on a Raspberry Pi 4 class machine (his Home Assistant
|
||||
Yellow is one), so work moved to the add-on is not free either. Everything below stays on ESPHome's and LVGL's own
|
||||
means: `online_image`'s `buffer_size`, LVGL's spinner and image radius, no ESP-IDF or lwIP settings.
|
||||
|
||||
## Round corners on the alert's picture
|
||||
|
||||
### Three ways to round the picture
|
||||
|
||||
- **`clip_corner` on the frame.** LVGL 9.5 then draws the frame and its picture into a layer of the frame's size to
|
||||
clip them (392 × 220 ARGB8888, about 345 KB, on every redraw). The project keeps layers out:
|
||||
`tests/test_layer_free.py` refuses `clip_corner` (see the CYD memory research of 0.2.50).
|
||||
- **Corners baked in by the add-on**, as for the media covers. The screen would copy a square picture as it does now,
|
||||
but the add-on would have to know the exact colour behind each corner: the alert's swatch in the light or the dark
|
||||
look, which only `theme.h` on the screen knows, and it would have to make the picture again (Pillow, on a Pi) when
|
||||
the look changes while the alert is up. A wrong guess shows as coloured corners.
|
||||
- **LVGL's own image radius (chosen).** `lv_image` passes its `radius` style to the draw as `clip_radius`, and the
|
||||
software renderer's `radius_only()` (`lv_draw_sw_img.c`) blends the picture row by row with a mask of one row
|
||||
(392 bytes). Rows between the corners come back from the mask as fully covered and are copied as before; only the
|
||||
rows of the corners are blended through the mask. No layer and no allocation beyond that row.
|
||||
`LV_DRAW_SW_COMPLEX` is 1 in ESPHome's `lv_conf.h`, so this path is compiled in; rounded rectangles already use it.
|
||||
|
||||
### What changed
|
||||
|
||||
- `packages/boards/guition-4848s040.yaml`: `alert_image_frame` has radius 18, the radius of the alert card
|
||||
(`packages/core.yaml`), instead of 0, so the frame behind a picture that doesn't fill it is rounded too.
|
||||
- `components/smart_display/runtime_tiles.h`: `camera_show(..., radius)` gives the picture that radius when it
|
||||
creates it; `camera_loaded()` passes the frame's radius for the alert's picture. The camera full screen and the
|
||||
media cover pass none and stay square.
|
||||
- README.md: a camera row under "On the screen" (`guition-camera-tiles.png` new: a page with a camera, a live camera
|
||||
and a doorbell's last ring; the camera full screen; the alert with its round picture). README_EXTENDED.md: a
|
||||
Cameras bullet under "What you can configure" and the tiles picture beside "With a camera picture".
|
||||
|
||||
### Measured (host build on the Mac, `.esphome/readme-render/readme-0919/time_radius.py`)
|
||||
|
||||
The render time of the picture's frame after invalidating it (LVGL's render events minus the flushes to the SDL
|
||||
window, which are the same for both), 40 redraws per run, four runs alternating between the two radii:
|
||||
|
||||
| Radius | Fastest | Median of the runs' medians |
|
||||
|---|---|---|
|
||||
| 0 (square, as before) | 1269 µs | 1340 µs |
|
||||
| 18 (the card's) | 1352 µs | 1413 µs |
|
||||
|
||||
About 5 % more, and only when the picture is drawn: when the alert appears, when the camera full screen closes over
|
||||
it, when something next to it is redrawn. A picture that stays up costs nothing afterwards. These are host numbers;
|
||||
the Guition is slower in absolute terms, and per row the extra work stays one 392-byte memset and one blend call.
|
||||
|
||||
## The starting screen
|
||||
|
||||
Until the first layout the top bar read "Connecting to Home Assistant..." and then "Waiting for ESP Screens..." where
|
||||
the screen's name goes (0.2.43). Now `render()` calls `boot_status()`, which puts the text in the middle of the page in
|
||||
the name's font (`watch_font`, 27 px Guition, 18 px CYD) with a spinner under it (48/32 px, 24/16 px apart), the two
|
||||
as one block in the middle; the name stays empty. The first layout deletes the block, spinner and all, so nothing
|
||||
turns behind the tiles and no animation runs for the rest of the day. The block sits in the name's place in the
|
||||
drawing order, under the tiles, the cards and an alert (an alert can come before the tiles). It is drawn in the
|
||||
shared paints (`ink`, `spinner`), so Dark mode colours it like the rest. `ROOM_NAME`, the name in the profile until the
|
||||
first `render()`, is empty instead of "Choose tiles in HA", which showed for a moment at every start. The text has no
|
||||
"..." any more; the spinner says it is busy.
|
||||
|
||||
The three spinners of the firmware (a busy card, the starting screen, a camera that loads) come from one
|
||||
`spinner_create()` in `runtime_tiles.h`; the busy card's looks the same as before.
|
||||
|
||||
## A camera that opens
|
||||
|
||||
### The spinner
|
||||
|
||||
The camera full screen shows the spinner in the middle until the first picture is there, instead of "Loading image".
|
||||
Its ring uses the new theme role `CAMERA_TRACK` (0x393D42, the dark look's spinner ring), since the camera page is
|
||||
black in both looks. A note ("No image from this camera") takes the spinner's place; the first picture ends both.
|
||||
|
||||
### Where the wait went
|
||||
|
||||
A camera opened full screen on the bench Guition (fw 0.2.72, 4 KB chunks), timed from the screen's own log
|
||||
(`.esphome/camera-speed/timeline.py`), with Max's EZVIZ camera `camera.max`:
|
||||
|
||||
1. 0.2-0.35 s until the screen asked: `camera_tick()`, every 250 ms, sent the request, not `camera_open()`.
|
||||
2. 2.6 s until the link came: Home Assistant's snapshot of the EZVIZ takes 2.4 s (the camera itself; measured the
|
||||
same in 0.2.66); the add-on's encoding is small next to it (a 1080p JPEG to the 480 × 270 BMP takes 10 ms on the
|
||||
Mac, several times that on a Pi 4 class machine).
|
||||
3. Up to 0.25 s until the load started: the next tick again.
|
||||
4. 2.8 s download: 388,854 bytes at 136 KB/s.
|
||||
|
||||
Step 4 is ESPHome's `online_image`: every round of the main loop it reads one `buffer_size` (then 4 KB) from the
|
||||
connection and decodes it, and `esp_http_client_read` waits until that chunk is complete. 4 KB a round came to about
|
||||
136 KB/s. A bigger chunk a round is faster (the table below), up to what one TCP connection with lwIP's 5,760-byte
|
||||
window gets over this Wi-Fi, roughly 300 KB/s. The same 2.7-3.3 s was measured in 0.2.66 from the Mac and from the
|
||||
Yellow: the limit was on the screen's side, not the sender's.
|
||||
|
||||
### What changed
|
||||
|
||||
- `camera_open()` asks right away; `camera_answer()` starts the load as soon as the link comes, as an alert's
|
||||
picture already did. Both through `camera_load()`, which, like the tick, never starts under a finger.
|
||||
`camera_tick()` keeps doing the refresh every 4 s and the retries.
|
||||
- `online_image` `buffer_size` 16384 instead of 4096, for both images.
|
||||
|
||||
### Measured on the bench Guition
|
||||
|
||||
`.esphome/camera-speed/speed.py`: per open, from the `preview_camera` call (the same path as a tap on the tile) until
|
||||
`online_image` reports the picture complete. Cold: 35 s after the last close, so the add-on has forgotten the snapshot
|
||||
and asks Home Assistant for a new one. Warm: 5 s after the last close, so the add-on answers with the snapshot it has
|
||||
and fetches the next one meanwhile. Three opens each (two cold for 32 KB); medians. The 0.2.73 builds went over the
|
||||
air from `.esphome/camera-speed/guition-wallbox*.yaml` (the Device Builder's API, OTA and Wi-Fi settings, the packages
|
||||
of this branch). One "cold" open of the power-save-off build found the snapshot still there and is left out.
|
||||
|
||||
| Build | Wi-Fi power save | Cold open | Warm open | Download | Longest loop wait during the opens |
|
||||
|---|---|---|---|---|---|
|
||||
| fw 0.2.72, 4 KB, tick | light (ESPHome's default) | 5.80 s | 3.40 s | 2.86 s (136 KB/s) | 131 ms (lvgl), 65 ms |
|
||||
| 0.2.73, 16 KB | light | 4.36 s | 2.09 s | 1.74-1.91 s (216-223 KB/s) | 58, 61, 77 ms |
|
||||
| 0.2.73, 16 KB | none | 4.24 s | 1.99 s | 1.73-1.80 s (223 KB/s) | none over 50 ms |
|
||||
| 0.2.73, 32 KB | none | 3.94 s | 1.62 s | 1.42-1.45 s (271 KB/s) | 101 ms |
|
||||
| 0.2.73, 16 KB (this release) | light | 4.55 s | 2.06 s | 1.75-1.90 s (208-218 KB/s) | 93, 107 ms |
|
||||
|
||||
The screen now asks 20-150 ms after the call (the API call and the log line's way back included) instead of
|
||||
210-345 ms, and loads about 150 ms after asking when the add-on has the snapshot. "Longest loop wait" is ESPHome's own
|
||||
"took a long time" warning (over 50 ms). With Wi-Fi power save, `online_image` now and then held a loop up to 107 ms
|
||||
(65 ms with 4 KB, next to a 131 ms LVGL frame in the same opens); without power save nothing went over 50 ms.
|
||||
|
||||
- **16 KB, not 32 KB.** 32 KB saves 0.3 s more, but a read of 32 KB waits for several TCP windows (lwIP's is 5,760
|
||||
bytes in ESPHome's build) and held a loop 101 ms even without power save: a tap in that moment waits as long.
|
||||
- **Wi-Fi power save.** The bench Guition's Device Builder YAML dates from before 0.2.24 and has no
|
||||
`power_save_mode: none`, so it runs ESPHome's default `light`: pings 103-191 ms (117 ms average) against 5-94 ms
|
||||
(21 ms) without it. With 16 KB chunks it made no difference to the download, but without it no loop waited over
|
||||
50 ms. The YAML the app writes has had `power_save_mode: none` since 0.2.24 (EASY_SETUP.md says why); the package
|
||||
doesn't set it, since it is the owner's Wi-Fi block.
|
||||
- **What remains** of a cold open is the camera: 2.4 s of the 4.5 s is Home Assistant's snapshot of the EZVIZ. Only
|
||||
fetching before the tap would hide it (when a camera's page shows, or when the screen wakes). That polls cameras
|
||||
nobody looks at, and a battery camera wakes for it; it is left for Max to decide.
|
||||
- Not changed: lwIP's TCP window and other ESP-IDF settings (`sdkconfig_options`). They would speed up the reads
|
||||
further but are memory and platform tuning outside ESPHome's defaults.
|
||||
|
||||
## Automated
|
||||
|
||||
- `tools/check.sh` (worktree, after the rebase on 0.2.86): Python 473 tests OK (two new: `test_camera` "the first
|
||||
picture waits for no tick", `test_header_bar` "a starting screen says what it waits for in the middle"; the
|
||||
position check of `render_header()` now looks in `render()` as a whole, and the spinner paint check in
|
||||
`spinner_create()`), C++ 19/19, packages, icons, editor tests, types, build and bundle PASS.
|
||||
- `tools/check.sh --firmware --baseline 1677504`: CYD 1,678,816 B of 1,835,008 B = 91.5 % (156,192 B free),
|
||||
+1,312 B against 0.2.86 (WARN: the 90-93 % band, under the 8 KB that needs Max's OK); Guition 2,055,280 B = 25.3 %.
|
||||
|
||||
## Renders
|
||||
|
||||
- Host builds from this branch (`.esphome/readme-render/readme-0919/boot_camera_host.py`, both boards, light and
|
||||
dark): the starting screen with both texts, page 1 after the layout without it, and on the Guition the camera page
|
||||
with the spinner, with the picture and with the note.
|
||||
- `guition-alert-camera.png` from the new firmware on the host: only the four corners of the picture changed
|
||||
(1,400 pixels of the 960 × 960 README image).
|
||||
- `guition-camera-tiles.png`: the camera tiles page of the README demo home (Home Assistant's icons and words), the
|
||||
front door camera idle and grey, the garden camera live and amber, since 0.2.85's colours.
|
||||
|
||||
## Hardware
|
||||
|
||||
- Bench Guition Wallbox (192.168.146.136): four builds of this branch over the air for the table above (its first
|
||||
row is the toggle-key session's 0.2.72 bench build that was on it); it runs this release's firmware (16 KB, its
|
||||
own `light` Wi-Fi). An LVGL snapshot during an open shows the spinner on the black camera page. After a
|
||||
restart the add-on's layout was there before my own API connection (6.8 s), and the snapshot showed "Loading tiles..."
|
||||
with the tiles in place: the starting screen had gone as it should.
|
||||
- The CYD: compiled only.
|
||||
|
||||
## Not tested
|
||||
|
||||
- The round alert picture on the panel: the add-on sends an alert picture only with the `esp_screens_show_alert`
|
||||
event, which goes to every screen, Studio 1 included. It shows with the next doorbell or camera alert.
|
||||
- The starting screen on the panels themselves: it is up for the few seconds between the start and ESP Screens'
|
||||
layout. Max sees it at the next update or restart.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -371,9 +371,11 @@ api:
|
||||
|
||||
# Camera images (firmware 0.2.57+): ESP Screen Manager serves each image already sized for this screen, as an uncompressed
|
||||
# BMP on its own port on the LAN (screen_manager/app/camera_feed.py). ESPHome decodes a BMP piece by piece as it arrives,
|
||||
# 4 KB per loop, where a JPEG of the full screen held the loop for 0.6 s; touch and drawing keep their turn. One image full
|
||||
# screen and one in an alert's frame; runtime_tiles.h decides when they load and frees them when they close. Plain HTTP
|
||||
# on the LAN, no certificates.
|
||||
# one `buffer_size` per loop, where a JPEG of the full screen held the loop for 0.6 s; touch and drawing keep their turn.
|
||||
# 16 KB a loop (firmware 0.2.73+): the full picture came in 1.8 s instead of 2.8 s with 4 KB. Without Wi-Fi power save no
|
||||
# loop waited over 50 ms for it (with power save now and then 0.1 s, against 65 ms with 4 KB); 32 KB saved 0.3 s more but
|
||||
# held a loop 0.1 s even without power save (docs/TEST_RESULTS_0287.md). One image full screen and one in an alert's
|
||||
# frame; runtime_tiles.h decides when they load and frees them when they close. Plain HTTP on the LAN, no certificates.
|
||||
http_request:
|
||||
timeout: 4s
|
||||
verify_ssl: false
|
||||
@@ -385,7 +387,7 @@ online_image:
|
||||
url: "http://127.0.0.1/camera.bmp" # set when a camera opens
|
||||
format: BMP
|
||||
type: RGB565
|
||||
buffer_size: 4096
|
||||
buffer_size: 16384
|
||||
on_download_finished:
|
||||
- lambda: 'runtime_tiles::camera_loaded(false, cached);'
|
||||
on_error:
|
||||
@@ -394,7 +396,7 @@ online_image:
|
||||
url: "http://127.0.0.1/camera.bmp" # set when an alert brings an image
|
||||
format: BMP
|
||||
type: RGB565
|
||||
buffer_size: 4096
|
||||
buffer_size: 16384
|
||||
on_download_finished:
|
||||
- lambda: 'runtime_tiles::camera_loaded(true, cached);'
|
||||
on_error:
|
||||
@@ -574,6 +576,7 @@ lvgl:
|
||||
widgets:
|
||||
# The alert's camera image (firmware 0.2.57+) across the top of the card, shown when the app announced
|
||||
# one; a tap opens the camera full screen over the alert. runtime_tiles.h puts the image in it.
|
||||
# Rounded like the alert card itself (firmware 0.2.73+); the image takes this radius (camera_loaded).
|
||||
- obj:
|
||||
id: alert_image_frame
|
||||
x: ${ALERT_IMAGE_INSET}
|
||||
@@ -583,7 +586,7 @@ lvgl:
|
||||
hidden: true
|
||||
styles: paint_camera
|
||||
bg_opa: COVER
|
||||
radius: 0
|
||||
radius: 18
|
||||
border_width: 0
|
||||
shadow_width: 0
|
||||
pad_all: 0
|
||||
|
||||
+2
-2
@@ -14,10 +14,10 @@
|
||||
# fonts/materialdesignicons-webfont.ttf (Apache 2.0, github.com/Templarian/MaterialDesign-Webfont).
|
||||
############################################################
|
||||
substitutions:
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.72"
|
||||
SCREEN_FIRMWARE_VERSION: "0.2.73"
|
||||
DASHBOARD_INBOX_INTERNAL: "false"
|
||||
CALIBRATION_ON_BOOT: "false" # a bench build: start on the touch test page
|
||||
ROOM_NAME: "Choose tiles in HA" # shown top left until ESP Screens sends the screen name
|
||||
ROOM_NAME: "" # top left until ESP Screens sends the screen name
|
||||
|
||||
## DIM settings
|
||||
AUTO_DIM: "true" # true = enable auto dimming
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
## 0.2.87 (firmware 0.2.73)
|
||||
|
||||
A starting screen with a spinner, a camera that opens sooner with a spinner, and round corners on an alert's camera picture.
|
||||
|
||||
- **A starting screen with a spinner.** Until the tiles arrive, a screen says in the middle what it waits for, *Connecting to Home Assistant* and then *Waiting for ESP Screens*, with a turning spinner under it. Before, that was a line of text in the top bar. The text and the spinner go as soon as the tiles come, so nothing keeps turning behind them.
|
||||
- **A camera opens sooner, with a spinner.** The camera full screen shows the spinner until the first picture is there, instead of the words *Loading image*; a camera without a picture still says so. The first picture also comes sooner on a Guition. The screen asks for it the moment the camera opens and starts loading it the moment ESP Screens answers, where it used to wait for its next quarter-second tick each time. It also loads 16 KB of the picture at a time instead of 4 KB. On the bench Guition a camera the add-on had just shown opened in 2.1 s instead of 3.4 s, and a camera it had to ask Home Assistant for first in 4.6 s instead of 5.8 s. The rest of that wait is the camera itself: the EZVIZ answers in 2.4 s.
|
||||
- **The picture in an alert has round corners**, the same as the alert card around it, on a Guition. LVGL rounds the picture itself while it draws it, row by row, without an extra buffer. Drawing the picture takes about 5 % longer than a square one, and only when it is drawn (the alert appearing, or coming back after the camera full screen); a picture that stays up costs nothing more. The camera full screen keeps its square picture from edge to edge.
|
||||
- **Cameras are in the README**, as tiles of their own and not only in an alert: a page with a camera, a live camera and a doorbell's last ring, the picture full screen, and the same camera in an alert.
|
||||
- Needs firmware 0.2.73: press **Update** on the screen. The starting screen is on both boards; the camera changes are the Guition's. Includes everything from 0.2.86. CYD firmware: 1,678,816 bytes, 91.5 % of the update slot (1,312 bytes more than 0.2.86).
|
||||
|
||||
## 0.2.86 (firmware 0.2.72)
|
||||
|
||||
The on/off switch on a wide tile switches again.
|
||||
|
||||
@@ -37,7 +37,7 @@ WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
|
||||
REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
|
||||
REFS = {'cyd': 'main', 'guition': 'main'}
|
||||
# Firmware shipped with this app release; screens below it get an update offer.
|
||||
FIRMWARE_VERSION = '0.2.72'
|
||||
FIRMWARE_VERSION = '0.2.73'
|
||||
# The Auto standby switch a screen offers Home Assistant automations.
|
||||
AUTO_STANDBY_MIN_FIRMWARE = '0.2.41'
|
||||
# The settings page the screen opens itself, and the screen.settings tile that opens it.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: ESP Screen Manager
|
||||
version: "0.2.86"
|
||||
version: "0.2.87"
|
||||
slug: esp_screen_manager
|
||||
description: Choose tiles for your CYD and Guition touchscreen, without reflashing.
|
||||
url: https://github.com/MaxGramser/homeassistant_espscreen
|
||||
|
||||
@@ -96,6 +96,21 @@ class Rules(unittest.TestCase):
|
||||
config = (ROOT / 'screen_manager/config.yaml').read_text()
|
||||
self.assertIn(f'{camera_feed.PORT}/tcp: {camera_feed.PORT}', config)
|
||||
|
||||
def test_the_first_picture_waits_for_no_tick(self):
|
||||
# Firmware 0.2.73+: the camera asks when it opens and loads the link when it comes, not on the next 250 ms tick;
|
||||
# the spinner turns until the first picture or a note is there.
|
||||
opened = TILES.split('inline void camera_open(const std::string &entity, const std::string &name) {', 1)[1].split('\n}\n', 1)[0]
|
||||
self.assertIn('camera_spinner = spinner_create(camera_root,', opened)
|
||||
self.assertIn('if (awake() && fresh() && camera.should_ask(now)) {', opened)
|
||||
answer = TILES.split('inline void camera_answer(const std::string &view, const std::string &entity, const std::string &url) {', 1)[1].split('\n}\n', 1)[0]
|
||||
self.assertIn('if (awake() && camera.should_load(now)) camera_load(now);', answer)
|
||||
# Never under a finger, from the answer or from the tick.
|
||||
load = TILES.split('inline void camera_load(uint32_t now) {', 1)[1].split('\n}\n', 1)[0]
|
||||
self.assertIn('lv_indev_get_state(input) == LV_INDEV_STATE_PRESSED) return;', load)
|
||||
note = TILES.split('inline void camera_note_text(const char *text) {', 1)[1].split('\n}\n', 1)[0]
|
||||
self.assertIn('lv_obj_delete(camera_spinner);', note)
|
||||
self.assertNotIn('Loading image', TILES)
|
||||
|
||||
|
||||
@unittest.skipUnless(HAS_PIL, 'Pillow comes with ESPHome in the add-on image')
|
||||
class Encoding(unittest.TestCase):
|
||||
|
||||
@@ -221,9 +221,24 @@ class ParityTests(unittest.TestCase):
|
||||
self.assertIn('runtime_tiles::time_label = id(lbl_time);', text, name)
|
||||
refresh = text.split(' - id: ui_refresh', 1)[1].split('return;', 1)[0]
|
||||
self.assertNotIn('lbl_time', refresh, name)
|
||||
self.assertIn('render_header();', TILES.split('inline void render(lv_obj_t *room) {', 1)[1].split('\n', 5)[4])
|
||||
self.assertIn('render_header();', TILES.split('inline void render(lv_obj_t *room) {', 1)[1].split('\n}', 1)[0])
|
||||
self.assertLessEqual(HEADER_MIN_FIRMWARE, tuple(int(p) for p in FIRMWARE_VERSION.split('.')))
|
||||
|
||||
def test_a_starting_screen_says_what_it_waits_for_in_the_middle(self):
|
||||
# Firmware 0.2.73+: until the first layout the text and a spinner stand in the middle and the name stays empty;
|
||||
# the first layout deletes them, so no spinner turns behind the tiles.
|
||||
render = TILES.split('inline void render(lv_obj_t *room) {', 1)[1].split('\n}', 1)[0]
|
||||
self.assertIn('if (!model.configured) boot_status(lv_obj_get_parent(room), !ha_connected() ? '
|
||||
'"Connecting to Home Assistant" : "Waiting for ESP Screens");', render)
|
||||
self.assertIn('else if (boot_panel) { lv_obj_delete(boot_panel);', render)
|
||||
self.assertIn('label(room, !model.configured ? "" :', render)
|
||||
boot = TILES.split('inline void boot_status(', 1)[1].split('\n}', 1)[0]
|
||||
self.assertIn('boot_spinner = spinner_create(boot_panel,', boot)
|
||||
# Under the tiles, the cards and an alert, in the name's place.
|
||||
self.assertIn('lv_obj_move_to_index(boot_panel, lv_obj_get_index(room_label));', boot)
|
||||
for name in PROFILES:
|
||||
self.assertNotIn('Choose tiles in HA', profiles.text(name), name)
|
||||
|
||||
def test_second_hand_runs_only_while_the_screen_is_awake(self):
|
||||
for name in PROFILES:
|
||||
text = profiles.text(name)
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ class Paints(unittest.TestCase):
|
||||
tiles = (COMPONENT / 'runtime_tiles.h').read_text()
|
||||
for needle in ('lv_obj_add_style(w.slider,theme::style(theme::Paint::knob),LV_PART_KNOB);',
|
||||
'lv_obj_add_style(w.busy,theme::style(theme::Paint::veil),0);',
|
||||
'lv_obj_add_style(w.spinner,theme::style(theme::Paint::spinner),LV_PART_MAIN);',
|
||||
'lv_obj_add_style(spinner, theme::style(theme::Paint::spinner), LV_PART_MAIN);',
|
||||
'lv_obj_add_style(part, theme::style(theme::Paint::slate), 0);',
|
||||
'inline void restyle() {'):
|
||||
self.assertIn(needle, tiles)
|
||||
|
||||
Reference in New Issue
Block a user