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:
@@ -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},
|
||||
|
||||
Reference in New Issue
Block a user