Add the Elecrow CrowPanel 7.0" as a third board, and take the packages and the add-on from Gitea (0.2.93)
CI / checks (push) Successful in 12m50s
CI / firmware (add-on) (push) Successful in 18m19s
CI / firmware (min_version) (push) Successful in 17m55s

A board file for the Elecrow CrowPanel 7.0" (DIS08070H, ESP32-S3-WROOM-1-N4R8): 800x480 through mipi_rgb, GT911 on the
ESP32-S3's USB pins (elecrow_usb_phy.h frees them, behind a flag only this board sets), the Guition's vertical sizes
across a wider page, no rotation. Its 4 MB of flash take the CYD's budget: a partition table of the component's
(smart_display: partitions: 4mb_wide_slots, 1,984 KB slots), the icon fonts at 2 bits a pixel and the TLS client only
bring the firmware to 1,865,440 bytes, 91.8 % of a slot, with camera images kept. docs/ELECROW.md has the numbers and
the trade.

The add-on reads a large board's name from the type sensor's value, the editor offers the board under New screen and
draws the wide preview, tools/check.sh builds it with the budget, render_topbar and capture_ui take the panel's size.
Firmware 0.2.79; the CYD image is unchanged (1,633,760 bytes).

This fork builds from gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen: the packages, the fonts, the
component and the add-on repository point there instead of GitHub.
This commit is contained in:
Xandrossian
2026-09-20 23:18:01 -07:00
parent efbab04b39
commit 9857cc3bda
64 changed files with 1664 additions and 145 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ jobs:
~/.cache/esphome
~/.platformio
.esphome/check
key: firmware-${{ runner.os }}-esphome-${{ steps.esphome.outputs.version }}-${{ hashFiles('home-like-2432s028.yaml', 'guition-4848s040.yaml', 'packages/**', 'components/**') }}
key: firmware-${{ runner.os }}-esphome-${{ steps.esphome.outputs.version }}-${{ hashFiles('home-like-2432s028.yaml', 'guition-4848s040.yaml', 'elecrow-crowpanel7.yaml', 'packages/**', 'components/**') }}
restore-keys: |
firmware-${{ runner.os }}-esphome-${{ steps.esphome.outputs.version }}-
- name: tools/check.sh --firmware
+17 -9
View File
@@ -3,11 +3,11 @@
The preferred route for new users is docs/EASY_SETUP.md: ESP Screen Manager
plus remote ESPHome packages. No token or blueprint needed. Tiles live in the
persistent add-on data; Wi-Fi/API/OTA stay in the device's own ESPHome YAML. Read
docs/RELEASING.md before publishing updates. Main distributes both boards.
docs/RELEASING.md before publishing updates. Main distributes every board.
Every push to GitHub is a release: always also bump the add-on version in
screen_manager/config.yaml (with a CHANGELOG line), otherwise HA won't see an update.
A screen's YAML is packages/core.yaml (shared by every board) plus one file under packages/boards/;
packages/<board>.yaml and the two profiles in the root only include them. Read docs/PROFILES.md before
packages/<board>.yaml and the profiles in the root only include them. Read docs/PROFILES.md before
touching them: a board-dependent number is a `${NAME}` in the board file, board-only code inside a shared
lambda is a hook substitution there, and `tools/check_packages.py` (run by tools/check.sh) keeps the boards
complete.
@@ -26,10 +26,18 @@ docs/GUITION.md. Don't carry over the CYD layout or the XPT2046 calibration.
Verify touch with tools/verify_gt911.py and keep the CYD regressions green.
Don't configure wallbox relays as part of display support.
## Elecrow board
The Elecrow CrowPanel 7.0" (DIS08070H, ESP32-S3-WROOM-1-N4R8) has `packages/boards/elecrow-crowpanel7.yaml`:
800×480 through `mipi_rgb`, GT911 on the ESP32-S3's USB pins (`elecrow_usb_phy.h` frees them; no interrupt pin, no
PCA9557 writes, `scan: false`), the Guition's vertical sizes across a wider page, no rotation. It has 4 MB of flash: the
CYD's budget applies (`tools/check.sh --firmware`), and the board file's partition table, icon fonts at 2 bits a pixel
and the notes in docs/ELECROW.md are what keep it inside. Read docs/ELECROW.md before touching it.
# Working instructions for LLMs and developers
This project drives two boards: the CYD ESP32-2432S028 with ILI9341 + XPT2046 (320×240,
LVGL 90°) and the Guition above. Read README.md, README_EXTENDED.md and docs/ before installing. The owner can
This project drives three boards: the CYD ESP32-2432S028 with ILI9341 + XPT2046 (320×240,
LVGL 90°), the Guition above and the Elecrow above. Read README.md, README_EXTENDED.md and docs/ before installing. The owner can
physically tap; an agent cannot replace that with software coordinates.
## Installing a new screen
@@ -44,7 +52,7 @@ physically tap; an agent cannot replace that with software coordinates.
crosshairs. `tools/calibrate.py` with docs/CALIBRATING.md is the USB route for a
panel that needs measuring; ask for physical taps per target, and wait for
confirmation that the measurement screen is visible. A successful build is not a flash.
Guition: GT911 reports pixels; verify with tools/verify_gt911.py, no ADC calibration.
Guition and Elecrow: GT911 reports pixels; verify with tools/verify_gt911.py, no ADC calibration.
3. Pair with the owner's own Home Assistant through the ESPHome integration.
Read real entity IDs and supported attributes; don't make up entities.
4. Choose the tiles in ESP Screens. Don't test real device actions without the
@@ -83,15 +91,15 @@ and screens for lookups and tests. If that file is missing you are not on his ma
and LVGL 90°. A changed orientation also requires a new projection/tests.
- Run `tools/check.sh` on code changes (the Python tests, every C++ test, the package check, the icon
generator's check, the editor's tests, types and build); on a firmware change also `tools/check.sh --firmware`, which
compiles both boards and applies the CYD's flash budget (docs/RELEASING.md step 2). CI runs the
compiles every board and applies the flash budget to the 4 MB boards, the CYD and the Elecrow (docs/RELEASING.md step 2). CI runs the
same script. Firmware tests and hardware acceptance are different checks.
- `diagnostics/run_ui_test.py` renders without HA actions; don't touch the screen
during that test. Use `--name` for the expected device identity.
`diagnostics/send_layout.py` pushes a demo layout with every card type to a
screen via the API inbox (no HA actions); the manager restores the real
layout within ~25 s. Guition: `capture_ui.py` saves the LVGL render as a PNG.
Without a screen: `tools/render_topbar.py` renders the real top-bar code for both
boards via the ESPHome host + SDL2 to `.esphome/render-topbar/out/sheet.png`.
layout within ~25 s. Guition and Elecrow: `capture_ui.py` saves the LVGL render as a PNG.
Without a screen: `tools/render_topbar.py` renders the real top-bar code for every
board via the ESPHome host + SDL2 to `.esphome/render-topbar/out/sheet.png`.
- Share through Git. Don't stage secrets, measurements, binaries, logs, build
caches, or local device profiles.
- No automatic firmware upload to an arbitrary connected port.
+3 -3
View File
@@ -15,7 +15,7 @@ Thank you! I work on this project with a lot of love, and every bit of support h
panel costs a fraction of that, sits on a table or in a wall box, and is always at hand for the
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
**What.** Firmware for three affordable panels, the 2.8-inch CYD, the 4-inch Guition and the 7-inch Elecrow CrowPanel, with up to
48 tiles over eight pages: lights, climate, blinds and curtains, the vacuum, media, the weather,
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
@@ -227,7 +227,7 @@ For Home Assistant OS with Apps/Add-ons on **aarch64 or amd64**:
2. Install **ESP Screen Manager**, start the app, and open **ESP Screens**.
ESPHome Device Builder is optional: the ESPHome CLI is already in this app.
3. Connect the screen with a USB data cable to the **Home Assistant machine**
and choose **New screen** in the sidebar: CYD or Guition, a name, the USB port, and
and choose **New screen** in the sidebar: CYD, Guition or Elecrow CrowPanel 7.0", a name, the USB port, and
**Install**. If Wi-Fi is missing from the ESPHome `secrets.yaml`, the window
asks for it once and ESP Screens only adds the missing lines. The profile
with unique API and OTA keys goes into the ESPHome folder; the build
@@ -237,7 +237,7 @@ For Home Assistant OS with Apps/Add-ons on **aarch64 or amd64**:
Choose **Download** under **Install via**: ESP Screens builds the firmware, and you put it
on the screen from your own computer with [ESPHome Web](https://web.esphome.io) in Chrome
or Edge. After that, updates go over Wi-Fi as usual.
4. **CYD:** go through the calibration on the screen. **Guition:** uses GT911
4. **CYD:** go through the calibration on the screen. **Guition and Elecrow:** use GT911
without resistive calibration. Then pair the discovered ESPHome device in
**Settings → Devices & services** using the API key the window
shows after installation (copy button). Grant the device permission to
+1
View File
@@ -550,6 +550,7 @@ take it from, and the same `FIRMWARE_VERSION` in `screen_manager/app/core.py`.
- [Complete installation from ESP Screens](docs/EASY_SETUP.md)
- [ESP Screens with Home Assistant Container (Docker)](docs/DOCKER.md)
- [Guition hardware, mounting, and rotation](docs/GUITION.md)
- [Elecrow CrowPanel 7.0" hardware, touch and flash](docs/ELECROW.md)
- [CYD calibration and USB diagnostics](docs/CALIBRATING.md)
- [Camera images and album covers](docs/CAMERA.md)
- [Troubleshooting](docs/TROUBLESHOOTING.md)
+17 -2
View File
@@ -4,19 +4,26 @@
screen_manager/translations/nl.json, with English for any key it lacks, becomes one table in main.cpp
(screen_text_gen.py, screen_text.h). The shared core sets it from the LANGUAGE substitution, which ESP Screens writes
into a screen's YAML.
`smart_display: partitions: 4mb_wide_slots` (firmware 0.2.79) builds with one of the partition tables of partitions.py
instead of ESPHome's own: two update slots of 1,984 KB on a 4 MB ESP32-S3, which the Elecrow CrowPanel 7.0" needs.
"""
import logging
import esphome.codegen as cg
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.core import CORE
from esphome.helpers import write_file_if_changed
from . import screen_text_gen
from . import partitions, screen_text_gen
CODEOWNERS = []
AUTO_LOAD = ["json"]
_LOGGER = logging.getLogger(__name__)
CONF_LANGUAGE = "language"
CONF_PARTITIONS = "partitions"
def _language(value):
@@ -26,7 +33,10 @@ def _language(value):
return value
CONFIG_SCHEMA = cv.Schema({cv.Optional(CONF_LANGUAGE, default="en"): _language})
CONFIG_SCHEMA = cv.Schema({
cv.Optional(CONF_LANGUAGE, default="en"): _language,
cv.Optional(CONF_PARTITIONS): cv.one_of(*partitions.TABLES, lower=True),
})
async def to_code(config):
@@ -36,3 +46,8 @@ async def to_code(config):
# ESP Screens only writes languages it has; a hand-written one it lacks builds in the nearest it does have.
_LOGGER.warning("No translation for %s yet; the screen's texts are in %s", wanted, language)
cg.add_global(cg.RawStatement(code))
if CONF_PARTITIONS in config:
# ESPHome writes no partitions.csv of its own when one is among the extra build files (esp32.copy_files).
table = CORE.relative_build_path("smart_display_partitions.csv")
write_file_if_changed(table, partitions.TABLES[config[CONF_PARTITIONS]])
esp32.add_extra_build_file("partitions.csv", table)
@@ -0,0 +1,30 @@
#pragma once
// The Elecrow CrowPanel 7.0" wires its GT911 touch I2C to GPIO19 and GPIO20, the ESP32-S3's USB D- and D+. The ROM
// leaves the USB_SERIAL_JTAG PHY on those pads, and ESP-IDF's I2C driver cannot take them back: the bus reads as
// "I2C software timeout" and the touch never answers (esphome/esphome#15356, ESPHome 2026.3+). This runs before any
// ESPHome component (a static constructor at priority 101): the PHY lets the pads go, and both become plain GPIOs
// with their pull-ups, as the I2C driver expects to find them. The board's USB-C port is a CH340 on UART0, so nothing
// is lost.
//
// ESPHome includes every header of the component in every build, so the code is behind a flag only the Elecrow's board
// file sets (`-DESP_SCREENS_FREE_USB_PADS=1`, packages/boards/elecrow-crowpanel7.yaml): the CYD has no such peripheral,
// and the Guition, an ESP32-S3 too, keeps its USB.
#if defined(ESP_SCREENS_FREE_USB_PADS) && ESP_SCREENS_FREE_USB_PADS
#include "hal/usb_serial_jtag_ll.h"
#include "soc/io_mux_reg.h"
#include "soc/usb_serial_jtag_reg.h"
namespace elecrow {
static __attribute__((constructor(101))) void release_usb_pads() {
usb_serial_jtag_ll_phy_enable_pad(false);
CLEAR_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_USB_PAD_ENABLE);
CLEAR_PERI_REG_MASK(USB_SERIAL_JTAG_CONF0_REG, USB_SERIAL_JTAG_DP_PULLUP);
for (uint32_t reg : {IO_MUX_GPIO19_REG, IO_MUX_GPIO20_REG}) {
PIN_FUNC_SELECT(reg, PIN_FUNC_GPIO);
PIN_INPUT_ENABLE(reg);
REG_SET_BIT(reg, FUN_PU);
REG_CLR_BIT(reg, FUN_PD);
}
}
} // namespace elecrow
#endif
+17 -11
View File
@@ -1,18 +1,22 @@
#pragma once
#include <algorithm>
#include "lvgl.h"
#include "esphome/core/log.h"
// On-demand 240x240 diagnostic image of LVGL's rendered screen. Nothing is
// allocated or streamed during normal use. The RGB display signal itself
// still needs a physical visual check.
// On-demand diagnostic image of LVGL's rendered screen at half its resolution (240x240 on the Guition, 400x240 on
// the Elecrow): every second pixel of every second row. Nothing is allocated or streamed during normal use. The RGB
// display signal itself still needs a physical visual check. The geometry check takes the panel's own size.
namespace guition {
inline int screen_w() { return lv_display_get_horizontal_resolution(lv_display_get_default()); }
inline int screen_h() { return lv_display_get_vertical_resolution(lv_display_get_default()); }
inline bool check_geometry(lv_obj_t *obj) {
if (lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return true;
bool ok = true;
lv_area_t area;
lv_obj_get_coords(obj, &area);
if (lv_obj_has_flag(obj, LV_OBJ_FLAG_CLICKABLE) &&
(area.x1 < 0 || area.y1 < 0 || area.x2 >= 480 || area.y2 >= 480)) {
(area.x1 < 0 || area.y1 < 0 || area.x2 >= screen_w() || area.y2 >= screen_h())) {
ESP_LOGE("ui_test", "GEOMETRY FAIL clickable=(%d,%d)-(%d,%d)",
area.x1, area.y1, area.x2, area.y2);
ok = false;
@@ -32,7 +36,7 @@ inline void snapshot_begin() {
overlay = nullptr;
snapshot = lv_snapshot_take(lv_screen_active(), LV_COLOR_FORMAT_RGB565);
snapshot_row = 0;
if (!snapshot || snapshot->header.w != 480 || snapshot->header.h != 480) {
if (!snapshot || (int) snapshot->header.w != screen_w() || (int) snapshot->header.h != screen_h()) {
if (snapshot) lv_draw_buf_destroy(snapshot);
snapshot = nullptr;
ESP_LOGE("ui_image", "IMAGE FAIL allocation/dimensions");
@@ -41,10 +45,10 @@ inline void snapshot_begin() {
for (uint32_t i = 0; i < lv_obj_get_child_count(lv_layer_top()); ++i)
if (!lv_obj_has_flag(lv_obj_get_child(lv_layer_top(), i), LV_OBJ_FLAG_HIDDEN)) {
overlay = lv_snapshot_take(lv_layer_top(), LV_COLOR_FORMAT_ARGB8888);
if (overlay && (overlay->header.w != 480 || overlay->header.h != 480)) { lv_draw_buf_destroy(overlay); overlay = nullptr; }
if (overlay && ((int) overlay->header.w != screen_w() || (int) overlay->header.h != screen_h())) { lv_draw_buf_destroy(overlay); overlay = nullptr; }
break;
}
ESP_LOGI("ui_image", "IMAGE BEGIN 240 240%s", overlay ? " with top layer" : "");
ESP_LOGI("ui_image", "IMAGE BEGIN %d %d%s", screen_w() / 2, screen_h() / 2, overlay ? " with top layer" : "");
}
// The top layer's pixel over the page's, as the panel shows them.
inline uint16_t layered(uint16_t pixel, int x, int y) {
@@ -62,15 +66,17 @@ inline void snapshot_next_row() {
const auto *row = reinterpret_cast<const uint16_t *>(
snapshot->data + snapshot_row * 2 * snapshot->header.stride);
static constexpr char hex[] = "0123456789abcdef";
char line[961];
for (int x = 0; x < 240; ++x) {
// Four hex digits a pixel; the widest panel is 800 px, 400 in the image.
static char line[1601];
const int columns = std::min(screen_w() / 2, 400);
for (int x = 0; x < columns; ++x) {
uint16_t pixel = layered(row[x * 2], x * 2, snapshot_row * 2);
for (int digit = 0; digit < 4; ++digit)
line[x * 4 + digit] = hex[(pixel >> (12 - digit * 4)) & 15];
}
line[960] = 0;
line[columns * 4] = 0;
ESP_LOGI("ui_image", "ROW %03d %s", snapshot_row, line);
if (++snapshot_row == 240) {
if (++snapshot_row == screen_h() / 2) {
lv_draw_buf_destroy(snapshot);
snapshot = nullptr;
if (overlay) lv_draw_buf_destroy(overlay);
+23
View File
@@ -0,0 +1,23 @@
"""Partition tables a board file can ask for with `smart_display: partitions:` (firmware 0.2.79).
ESPHome's own table for 4 MB of flash keeps 448 KB for NVS and gives the two update slots 1,792 KB each. The screen's
preferences take a few KB of NVS (ESP-IDF's own two-slot example keeps 16 KB), and the Elecrow CrowPanel 7.0" needs
the room: its firmware is the Guition's, which does not fit 1,792 KB. The tables live here, as text, because a board
file over GitHub cannot carry a file of its own; __init__.py hands the chosen one to ESPHome as the build's
partitions.csv. A table never changes once a screen is flashed with it: an update over Wi-Fi writes into the other slot
of the table the screen has, and only a USB install lays the flash out anew.
"""
# 4 MB as two update slots of 0x1F0000 bytes, the largest 64 KB-aligned pair that fits, and 64 KB of NVS at the end;
# otadata and phy_init where ESPHome puts them. Offsets are explicit so the table is the same on every build.
TABLES = {
'4mb_wide_slots': """\
# ESP Screens: 4 MB ESP32-S3 with two update slots of 1,984 KB (components/smart_display/partitions.py).
# Name, Type, SubType, Offset, Size
otadata, data, ota, 0x9000, 0x2000
phy_init, data, phy, 0xB000, 0x1000
app0, app, ota_0, 0x10000, 0x1F0000
app1, app, ota_1, 0x200000, 0x1F0000
nvs, data, nvs, 0x3F0000, 0x10000
""",
}
+13 -10
View File
@@ -1,4 +1,4 @@
"""Save a Guition LVGL diagnostic snapshot as PNG through its encrypted API."""
"""Save a Guition or Elecrow LVGL diagnostic snapshot as PNG through its encrypted API (half the panel's size)."""
import argparse
import asyncio
from pathlib import Path
@@ -9,16 +9,16 @@ import yaml
from aioesphomeapi import APIClient, LogLevel
def png(rows):
def png(rows,width,height):
pixels=bytearray()
for y in range(240):
for y in range(height):
pixels.append(0)
for x in range(240):
for x in range(width):
p=int(rows[y][x*4:x*4+4],16)
pixels.extend((((p>>11)&31)*255//31,((p>>5)&63)*255//63,(p&31)*255//31))
def chunk(name,data):
return struct.pack('>I',len(data))+name+data+struct.pack('>I',zlib.crc32(name+data)&0xffffffff)
return b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>2I5B',240,240,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(pixels))+chunk(b'IEND',b'')
return b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>2I5B',width,height,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(pixels))+chunk(b'IEND',b'')
async def main():
@@ -33,18 +33,21 @@ async def main():
await client.connect(login=True)
try:
_,services=await client.list_entities_services()
done=asyncio.Event();rows={};errors=[]
done=asyncio.Event();rows={};errors=[];size=[240,240]
def log(msg):
text=msg.message.decode(errors='replace') if isinstance(msg.message,bytes) else msg.message
m=re.search(r'ROW (\d{3}) ([0-9a-f]{960})',text)
if m:rows[int(m[1])]=m[2]
# The firmware says the image's size first: 240x240 on a Guition, 400x240 on an Elecrow.
b=re.search(r'IMAGE BEGIN (\d+) (\d+)',text)
if b:size[:]=[int(b[1]),int(b[2])]
m=re.search(r'ROW (\d{3}) ([0-9a-f]{960,1600})',text)
if m and len(m[2])==size[0]*4:rows[int(m[1])]=m[2]
if 'IMAGE FAIL' in text:errors.append(text);done.set()
if 'IMAGE COMPLETE' in text:done.set()
client.subscribe_logs(log,log_level=LogLevel.LOG_LEVEL_INFO,dump_config=False)
await client.execute_service(next(s for s in services if s.name=='ui_snapshot'),{})
await asyncio.wait_for(done.wait(),timeout=60)
if errors or set(rows)!=set(range(240)):raise ValueError(f'Incomplete image: {len(rows)}/240 rows; {errors}')
with args.output.open('xb') as f:f.write(png(rows))
if errors or set(rows)!=set(range(size[1])):raise ValueError(f'Incomplete image: {len(rows)}/{size[1]} rows; {errors}')
with args.output.open('xb') as f:f.write(png(rows,*size))
print(f'PASS: LVGL render saved to {args.output}')
finally:await client.disconnect()
+2 -1
View File
@@ -1,6 +1,7 @@
# Camera images on a Guition
App 0.2.66 with firmware 0.2.57 shows camera images on a Guition 4848S040:
App 0.2.66 with firmware 0.2.57 shows camera images on a Guition 4848S040 (and, from app 0.2.93 with firmware
0.2.79, on an Elecrow CrowPanel 7.0" at its own 800×480 size, docs/ELECROW.md; "Guition" below reads for both):
- **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, and a spinner until the first
+4 -3
View File
@@ -15,6 +15,7 @@ ESPHome Device Builder is optional:
| --- | --- |
| CYD | ESP32-2432S028, 320×240, ILI9341 and XPT2046 |
| Guition | ESP32-S3-4848S040, 480×480, ST7701S and GT911 |
| Elecrow CrowPanel 7.0" | DIS08070H, ESP32-S3-WROOM-1-N4R8, 800×480 RGB and GT911 ([ELECROW.md](ELECROW.md)) |
Other screens with roughly the same name can have different pins. Use
the board profile that matches the hardware. Use a USB cable that supports data.
@@ -38,8 +39,8 @@ Use the GitHub version for updates; a local test add-on is a separate app.
1. Connect the screen with a **USB data cable** to the machine running Home
Assistant. With multiple boards: connect them one at a time for the first
installation, or check which port belongs to this screen.
2. Click **New screen** in the sidebar of ESP Screens, under your screens. Choose CYD
or Guition and give the screen a name, for example `Kitchen`. The device name
2. Click **New screen** in the sidebar of ESP Screens, under your screens. Choose CYD,
Guition or Elecrow CrowPanel and give the screen a name, for example `Kitchen`. The device name
(`kitchen`) follows from that; use **customize** to choose a different one.
3. Wi-Fi: if `wifi_ssid` and `wifi_password` are already in the ESPHome `secrets.yaml`,
the screen uses them automatically. If they're missing, or the file doesn't
@@ -88,7 +89,7 @@ and survives OTA updates. Use the HA device button **Calibrate touch** to
measure again later. For a panel that stays off, or for a measurement report over
USB, see [CALIBRATING.md](CALIBRATING.md).
**Guition:** the GT911 touch mapping is baked into the board profile; there's no ADC calibration.
**Guition and Elecrow:** the GT911 touch mapping is baked into the board profile; there's no ADC calibration.
## 3. Pair the screen with Home Assistant
+107
View File
@@ -0,0 +1,107 @@
# Elecrow CrowPanel 7.0" — 800 × 480
The **Elecrow CrowPanel ESP32 HMI 7.0-inch** (product code **DIS08070H**, hardware V2.0 and V3.0) has a board file of
its own, next to the CYD's and the Guition's: an 800 × 480 IPS panel on the ESP32-S3's own RGB bus, **GT911
capacitive touch**, an **ESP32-S3-WROOM-1-N4R8** with 4 MB of flash and 8 MB of octal PSRAM, and a backlight on
GPIO 2. It is the Guition's kind of screen, a big RGB panel with capacitive touch, in a wider, landscape form: every
card, the camera images and album covers, the page swipe from the glass edge, the settings on the screen. Two things
differ in kind, and both are below: the **4 MB of flash** (the CYD's budget, not the Guition's 16 MB) and a panel
that is not square, so it **does not turn** (no Rotation entity).
Hardware source: [Elecrow's wiki](https://www.elecrow.com/wiki/esp32-display-702727-intelligent-touch-screen-wi-fi26ble-800480-hmi-display.html)
and the [ESPHome devices page for the CrowPanel 7.0"](https://github.com/esphome/esphome-devices/pull/1494), whose
panel timings, pins and pitfalls this board file follows. The **CrowPanel *Advance* 7.0"** is a different board with
other pins (ESPHome's `CROWPANEL-ADVANCE-7` model); this file is for the plain CrowPanel 7.0".
## Files
- `packages/boards/elecrow-crowpanel7.yaml`: the board file (hardware, the 800 × 480 sizes, the flash measures); the
interface itself is `packages/core.yaml`, shared with both other boards (docs/PROFILES.md).
- `packages/elecrow.yaml`: what a screen builds from over GitHub; `elecrow-crowpanel7.yaml` in the root is the same for
a build from a checkout; `installers/elecrow.yaml` the block for a hand-written ESPHome configuration.
- `components/smart_display/elecrow_usb_phy.h`: frees the USB pins for the touch I2C (below).
- `components/smart_display/partitions.py`: the partition table that makes the firmware fit (below).
- `tools/verify_gt911.py`: physical pixel check, as on the Guition; no resistive calibration.
Choose **Elecrow CrowPanel · 7 inch** under **New screen** in ESP Screens (app 0.2.93+). The Guition's own parts (the
Rotation choice) stay off for it; everything the Guition can show, it shows.
## Interface
Six tiles of **378 × 108 pixels** per page in two columns, the Guition's rows and fonts across a wider page: the
vertical sizes are the Guition's to the pixel, the tiles and the cards take the extra width. The top bar, the tile
labels, the alert card (560 wide, its camera picture 532 × 220), the colour and climate titles and the page keys are
sized for 800 px; the cards that are a share of the page (the setpoint card, the mode keys) simply widen. A camera
opens full screen at 800 × 480; ESP Screens serves the picture in that box (`camera_feed.BOXES['elecrow']`).
At 133 pixels an inch the Guition's 42 px icons are a shade larger on this panel than on the 4-inch (170 ppi), which
suits a screen that stands further away.
### Touch
The GT911 is polled every 20 ms over I2C at 400 kHz, with no interrupt pin and no reset: the **V3.0** has a PCA9557
I/O expander (I2C 0x18) on the GT911's reset and interrupt lines, and writing it during boot is what breaks the touch
on that revision. It powers up with every pin as an input, a pull-up brings the GT911 out of reset before the firmware
starts, and the driver finds it ready. A bus scan at boot upsets the GT911 too, so `scan: false`.
The GT911 takes I2C address **0x5D** when GPIO 38 reads low at power-on and **0x14** when it reads high; on this
board it is 0x5D on most units, and a unit that never reports a touch may be a 0x14. Give that screen an Override YAML
in ESP Screens (Screen settings → Override YAML) with:
```yaml
touchscreen:
address: 0x14
```
**GPIO 19 and 20, the touch I2C, are the ESP32-S3's USB D- and D+.** The ROM leaves the USB PHY on those pads and
ESP-IDF's I2C driver cannot take them back: the bus reads as `I2C software timeout` and the touch never answers
(esphome/esphome#15356, ESPHome 2026.3+). `elecrow_usb_phy.h` runs before any component, as a static constructor,
and lets the pads go; it is behind the build flag `ESP_SCREENS_FREE_USB_PADS`, which only this board file sets, because
ESPHome includes every header of the component in every build and the Guition, an ESP32-S3 too, keeps its USB. The board's USB-C port is a CH340 on UART0, so flashing and the log are unaffected; the
`GPIO19 is used by the USB-Serial-JTAG interface` warnings at build time are expected.
### Rotation
A landscape panel with a layout 800 px wide: it does not turn. `ROTATION_SUPPORTED` is `false`, the settings page
shows no rotation row, and the app offers none for this board.
## Flash
The ESP32-S3-WROOM-1-**N4**R8 has **4 MB** of flash (the N4; 16 MB in the YAML boot-loops with no log). The firmware
is the Guition's, with camera images; with the Guition's fonts it is **2,028 KB**, and ESPHome's own partition table for
4 MB, two update slots of 1,792 KB with 448 KB kept for NVS, does not hold it. Three measures make it fit with room for
updates, all in the board file, measured with ESPHome 2026.9.0 (the add-on's):
| Measure | Saves | Where |
|---|---|---|
| Two update slots of **1,984 KB** and 64 KB of NVS instead of 448 KB (the preferences take a few KB; ESP-IDF's own two-slot example keeps 16 KB) | 192 KB more room a slot | `smart_display: partitions: 4mb_wide_slots`, `components/smart_display/partitions.py` |
| The four icon fonts (the 314 tile icons at 42, 26 and 18 px, the 31 big icons at 64 px) at **2 bits a pixel** instead of 4; the text fonts keep their 4 | 156 KB | `font:` in the board file (`!extend` on the core's fonts) |
| The TLS client only; nothing serves TLS | 7 KB | `sdkconfig_options` in the board file |
That is **1,865,456 bytes, 91.8 %** of a slot: within the CYD's rules (docs/RELEASING.md step 2), in the *tight* band,
and `tools/check.sh --firmware` applies the same budget to this board. Without camera images it is about 1,754 KB,
86 %: drop the `online_image`, `http_request` and `preview_camera` sections, the alert's picture frame and the
`camera_image_seed` image, and empty the camera hooks (`BOOT_CAMERA_HOOKS`, `BOARD_PAINT_FILL`, `CLOSE_CARDS_HOOK`,
`MAY_OPEN_EXTRA`, `TICK_HOOK`, `AWAY_EXTRA`, `ALERT_SHOW_HOOK`) as the CYD's board file has them. The icons at 4 bits
a pixel are the Guition's 156 KB back; they fit only without the camera images, and then at 94 %.
The partition table is part of the firmware and **never changes once a screen is flashed**: an update over Wi-Fi
writes into the other slot of the table the screen has, and only a USB install (ESP Screens' New screen, or ESPHome
Web with the factory image) lays the flash out anew. A screen flashed with this table keeps it through every update.
## Panel
ESPHome's `mipi_rgb` driver with the `RPI` model: the panel takes the pixels straight from the ESP32-S3's LCD
peripheral and needs no initialisation, so the timings are the whole configuration: 15 MHz pixel clock, inverted
(`pclk_inverted: true`, without it the picture tears), horizontal porches 40/48/13 and vertical 1/31/13, the 16 data
pins of Elecrow's schematic. The frame buffer is in PSRAM; LVGL draws in a buffer of 15 % of the screen, the same
number of pixels as the Guition's 25 % of 480 × 480.
## Checks
- `python3 tools/check_packages.py`, `esphome config elecrow-crowpanel7.yaml`, `tools/check.sh --firmware`.
- The touch test page and `tools/verify_gt911.py` for the corners; the ghost-touch filter at (0, 0) of the Guition's
GT911 applies here too.
- `diagnostics/capture_ui.py` saves the LVGL render at 400 × 240 (half the panel) as a PNG; `ui_self_test` renders
every card without touching a device.
- A build is not a flash, and a render is not a touch: the owner checks the picture and taps the corners.
+10 -6
View File
@@ -10,8 +10,9 @@ already drifted apart in small ways.
| `packages/core.yaml` | Everything every board shares: the LVGL tree (tiles, cards, overlays, the touch test page), the scripts, the API actions, the entities, the fonts, the globals, and Wi-Fi that never dozes (`power_save_mode: none`, firmware 0.2.74+; the network itself stays in the screen's own YAML). It contains no number that depends on the board: every such value is a `${NAME}` the board file defines. |
| `packages/boards/cyd-2432s028.yaml` | The CYD: its hardware (ILI9341 over SPI, XPT2046 resistive touch, backlight, LED), its sizes and font sizes, the code the shared lambdas take from it (the *hooks*, below), and what only a CYD has: the calibration wizard, a page swipe by LVGL's gesture, `ui_self_test`, the two scripts that drive the backlight as a light action, its climate card layout. |
| `packages/boards/guition-4848s040.yaml` | The Guition: its hardware (ST7701S RGB, GT911, PSRAM), its sizes, its hooks, and what only a Guition has: camera images (`online_image`, the alert's picture frame), the Rotation entity, the page swipe from the glass edge, the snapshot diagnostics, the climate card with its fan and swing card, the wider tile labels (`!extend` on each tile). |
| `packages/cyd.yaml`, `packages/guition.yaml` | What a screen installed from ESP Screens builds from over GitHub, unchanged in name and place: ESP Screen Manager writes every screen's YAML with `files: [packages/<board>.yaml]`. Each is a few lines: where the fonts are, the components from GitHub, and `packages: {core: !include core.yaml, board: !include boards/<board>.yaml}`. |
| `home-like-2432s028.yaml`, `guition-4848s040.yaml` | The same two files for a build from a checkout (the bench, the manual route of README_EXTENDED.md): the components of the checkout, the fonts from `fonts/`, and the secrets from `secrets.yaml`. |
| `packages/boards/elecrow-crowpanel7.yaml` | The Elecrow CrowPanel 7.0" (app 0.2.93): the Guition's kind at 800×480 on 4 MB of flash. Its hardware (the RGB panel through `mipi_rgb`, GT911, the USB pins freed for the touch I2C), the Guition's vertical sizes across a wider page, the same hooks and sections as the Guition's file except the Rotation (a landscape panel does not turn), and what the flash asks for: a partition table of the component's (`smart_display: partitions`), the icon fonts at 2 bits a pixel (`font:` with `!extend`). docs/ELECROW.md has the numbers. |
| `packages/cyd.yaml`, `packages/guition.yaml`, `packages/elecrow.yaml` | What a screen installed from ESP Screens builds from over GitHub, unchanged in name and place: ESP Screen Manager writes every screen's YAML with `files: [packages/<board>.yaml]`. Each is a few lines: where the fonts are, the components from GitHub, and `packages: {core: !include core.yaml, board: !include boards/<board>.yaml}`. |
| `home-like-2432s028.yaml`, `guition-4848s040.yaml`, `elecrow-crowpanel7.yaml` | The same files for a build from a checkout (the bench, the manual route of README_EXTENDED.md): the components of the checkout, the fonts from `fonts/`, and the secrets from `secrets.yaml`. |
ESPHome merges them in this order: the core, then the board file on top of it, then the entry file on top of both.
A mapping merges key by key, a list of components with ids merges by id (`!extend` adds to an existing widget or
@@ -41,11 +42,14 @@ the board file replaces the core's. `esphome config home-like-2432s028.yaml` sho
`assertion_level: SILENT` in its `esp32:` block (firmware 0.2.75+): every board builds its firmware the same way,
and `tests/test_easy_package.py` checks it.
2. Fill the sizes table for the new resolution, and go through the hooks: keep, change or empty each one.
3. Add `packages/<board>.yaml` and `<board>.yaml` after the existing entries, `<board>` to `ENTRIES` in
3. Add `packages/<board>.yaml` and `<board>.yaml` after the existing entries, `<board>` to `BOARDS` and `ENTRIES` in
`tools/profiles.py` and to `REFS` in `screen_manager/app/core.py` (the boards `installation_yaml()` writes a
profile for), and a `BANDS` entry to `tools/render_topbar.py`. The add-on and the editor still know two boards,
a Guition or not: the board a screen is (`core.py`), `ALERT_LIMITS`, the camera sizes in `camera_feed.py`, and
the editor's New screen choices and `BAR_METRICS` (`web/src`) need the new board too.
profile for), and a `BANDS` entry to `tools/render_topbar.py`. The add-on and the editor know a board as small (the
CYD) or large (`LARGE_BOARDS` in `core.py` and `web/src/store.ts`: a Guition or an Elecrow, the ones that draw
images and take the large cards); a large board reports its name in the value of the "Guition screen type" sensor
(`discover_screens`). `ALERT_LIMITS`, the camera sizes in `camera_feed.py`, the editor's New screen choices
(`InstallerView.vue` and `board_<name>` in every translation) and `BAR_METRICS` need the new board too, and
`tools/check.sh` builds it (with the flash budget when it has 4 MB). The Elecrow (app 0.2.93) is the worked example.
4. `python3 tools/check_packages.py`, `esphome config <board>.yaml`, `tools/check.sh --firmware`.
The tiles per page (two columns of three) are still fixed in the firmware, the add-on and the editor
+40
View File
@@ -0,0 +1,40 @@
# ESP Screens - an Elecrow CrowPanel 7.0" (DIS08070H, 7-inch 800x480) built from this checkout: the bench and the
# manual route (README_EXTENDED.md, docs/ELECROW.md).
#
# The screen is two packages: packages/core.yaml, which every board shares (the LVGL tree, the cards, the
# scripts, the entities), and packages/boards/elecrow-crowpanel7.yaml with this board's hardware and sizes.
# docs/PROFILES.md says how the files fit together. A screen installed from ESP Screens builds the same two
# files over GitHub through packages/elecrow.yaml; this file only adds what a build from a checkout needs:
# the secrets (api_encryption_key, ota_password, Wi-Fi) from secrets.yaml and the components of this checkout.
substitutions:
FONT_DIR: "fonts" # the fonts next to this file
packages:
core: !include packages/core.yaml
board: !include packages/boards/elecrow-crowpanel7.yaml
# The components of this checkout; packages/elecrow.yaml takes them from GitHub.
external_components:
- source:
type: local
path: components
components: [smart_display]
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Mains-powered panel: no modem sleep, so HA updates and OTA arrive without beacon latency.
power_save_mode: none
ap:
ssid: "Smartdisplay Fallback Hotspot"
password: !secret ap_password
captive_portal:
+1 -1
View File
@@ -3,7 +3,7 @@
# The easy route generates a complete file of its own: ESP Screen Manager.
packages:
screen:
url: https://github.com/MaxGramser/homeassistant_espscreen
url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen
ref: main
files: [packages/cyd.yaml]
refresh: 0s
+9
View File
@@ -0,0 +1,9 @@
# Manual alternative: add this block to the ESPHome wizard configuration.
# Remove the old esp32: block; keep your own esphome:, api:, ota: and wifi:.
# The easy route generates a complete file of its own: ESP Screen Manager.
packages:
screen:
url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen
ref: main
files: [packages/elecrow.yaml]
refresh: 0s
+1 -1
View File
@@ -3,7 +3,7 @@
# The easy route generates a complete file of its own: ESP Screen Manager.
packages:
screen:
url: https://github.com/MaxGramser/homeassistant_espscreen
url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen
ref: main
files: [packages/guition.yaml]
refresh: 0s
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,7 +14,7 @@
# fonts/materialdesignicons-webfont.ttf (Apache 2.0, github.com/Templarian/MaterialDesign-Webfont).
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.78"
SCREEN_FIRMWARE_VERSION: "0.2.79"
DASHBOARD_INBOX_INTERNAL: "false"
CALIBRATION_ON_BOOT: "false" # a bench build: start on the touch test page
ROOM_NAME: "" # top left until ESP Screens sends the screen name
+2 -2
View File
@@ -6,7 +6,7 @@
# entities), and boards/cyd-2432s028.yaml with this board's hardware and sizes. docs/PROFILES.md says how the
# files fit together. home-like-2432s028.yaml builds the same two files from a checkout.
substitutions:
FONT_DIR: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts"
FONT_DIR: "https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen/raw/branch/main/fonts"
packages:
core: !include core.yaml
@@ -15,7 +15,7 @@ packages:
external_components:
- source:
type: git
url: https://github.com/MaxGramser/homeassistant_espscreen.git
url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen.git
ref: main
path: components
refresh: 0s
+22
View File
@@ -0,0 +1,22 @@
# ESP Screens - the package an Elecrow CrowPanel 7.0" builds from over GitHub. ESP Screen Manager writes every
# screen's own YAML with `packages: display: {url: <this repository>, files: [packages/elecrow.yaml]}` and adds
# the name, the Wi-Fi reference and the unique keys (docs/EASY_SETUP.md); nothing of that lives here.
#
# The screen is two packages: core.yaml, which every board shares (the LVGL tree, the cards, the scripts, the
# entities), and boards/elecrow-crowpanel7.yaml with this board's hardware and sizes. docs/PROFILES.md says how
# the files fit together. elecrow-crowpanel7.yaml builds the same two files from a checkout.
substitutions:
FONT_DIR: "https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen/raw/branch/main/fonts"
packages:
core: !include core.yaml
board: !include boards/elecrow-crowpanel7.yaml
external_components:
- source:
type: git
url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen.git
ref: main
path: components
refresh: 0s
components: [smart_display]
+2 -2
View File
@@ -6,7 +6,7 @@
# entities), and boards/guition-4848s040.yaml with this board's hardware and sizes. docs/PROFILES.md says how
# the files fit together. guition-4848s040.yaml builds the same two files from a checkout.
substitutions:
FONT_DIR: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts"
FONT_DIR: "https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen/raw/branch/main/fonts"
packages:
core: !include core.yaml
@@ -15,7 +15,7 @@ packages:
external_components:
- source:
type: git
url: https://github.com/MaxGramser/homeassistant_espscreen.git
url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen.git
ref: main
path: components
refresh: 0s
+2 -2
View File
@@ -1,3 +1,3 @@
name: Home Assistant ESP Screens
url: https://github.com/MaxGramser/homeassistant_espscreen
maintainer: MaxGramser
url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen
maintainer: Xandrossian (fork of MaxGramser/homeassistant_espscreen with the Elecrow CrowPanel 7.0")
+9
View File
@@ -1,3 +1,12 @@
## 0.2.93 (firmware 0.2.79)
A third screen: the 7-inch Elecrow CrowPanel, with everything the Guition shows across a wider page.
- **Elecrow CrowPanel 7.0" (DIS08070H).** Choose **Elecrow CrowPanel · 7 inch** under **New screen**. An 800 × 480 panel with GT911 touch on an ESP32-S3-WROOM-1-N4R8: every card, the camera images and album covers, the live pictures on camera tiles, the page swipe from the glass edge and the settings on the screen, the Guition's rows and fonts across six tiles of 378 × 108. It does not turn (a landscape panel), so it has no Rotation. docs/ELECROW.md has the hardware, the touch notes (the GT911 sits on the ESP32-S3's USB pins, which the firmware frees; a unit at I2C 0x14 gets one line of Override YAML) and the flash.
- **4 MB of flash, like the CYD.** The firmware is the Guition's, which does not fit ESPHome's own partition table for 4 MB. Three measures in the board file make it fit with room for updates: two update slots of 1,984 KB instead of 1,792 (a partition table of the component's, `smart_display: partitions`), the icon fonts at 2 bits a pixel where the text keeps its 4, and the TLS client only. Elecrow firmware: 1,865,456 bytes, 91.8 % of its update slot (without camera images it would be 86 %; the trade is in docs/ELECROW.md). `tools/check.sh --firmware` applies the CYD's budget to this board too.
- The app reads a large board's name from its "Guition screen type" sensor (an older Guition, which never publishes a value, stays a Guition); the editor's preview takes the panel's shape. The snapshot diagnostics (`ui_snapshot`, `capture_ui.py`) take the panel's own size, half of it, 400 × 240 on the Elecrow.
- Needs firmware 0.2.79 for the Elecrow; a CYD or Guition on 0.2.78 loses nothing by waiting. Includes everything from 0.2.92.
## 0.2.92 (firmware 0.2.78)
The album cover on a media tile, the way Home Assistant's own tile shows it.
+1 -1
View File
@@ -5,7 +5,7 @@ Thank you! I work on this project with a lot of love, and every bit of support h
<a href="https://buymeacoffee.com/f5j9jnkmhpv"><img src="https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=f5j9jnkmhpv&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" alt="Buy me a coffee" height="42"></a>
Find your Home Assistant entities, arrange up to 48 tiles, and push changes
to a CYD or Guition screen without reflashing. Includes installation help,
to a CYD, Guition or Elecrow CrowPanel screen without reflashing. Includes installation help,
a unique per-device YAML, and support for multiple screens.
<p align="center">
+3 -3
View File
@@ -1,4 +1,4 @@
"""Camera images on a Guition screen (app 0.2.66, firmware 0.2.57).
"""Camera images on a Guition screen (app 0.2.66, firmware 0.2.57), and on an Elecrow CrowPanel 7.0" (app 0.2.93).
A screen never asks Home Assistant for a camera itself. This app fetches the snapshot with its own token (a
camera's access token changes every five minutes), makes it exactly as large as the screen shows it and serves
@@ -62,8 +62,8 @@ MAX_LINKS = 64
# What the screens load (online_image `format: BMP`).
CONTENT_TYPE = 'image/bmp'
# The pixel box per board and view; the image keeps its proportions inside it. Equal to the CAMERA_*
# substitutions of packages/guition.yaml (tests/test_camera.py).
BOXES = {'guition': {'full': (480, 480), 'thumb': (392, 220)}}
# substitutions of the board's file under packages/boards/ (tests/test_camera.py).
BOXES = {'guition': {'full': (480, 480), 'thumb': (392, 220)}, 'elecrow': {'full': (800, 480), 'thumb': (532, 220)}}
def supported(entity):
+1 -1
View File
@@ -27,7 +27,7 @@ PAGE_TILE_REPEAT_VERSION = '.'.join(str(part) for part in PAGE_TILE_REPEAT_MIN_F
NAME = 'esp-screens'
# claude.ai accepts at most 200 characters; Claude Code picks the skill by this sentence.
DESCRIPTION = ('ESP Screens (CYD and Guition touchscreens run from Home Assistant): put a tile on a screen, move or '
DESCRIPTION = ('ESP Screens (CYD, Guition and Elecrow screens run from Home Assistant): put a tile on a screen, move or '
'order tiles, show an alert on one screen or all of them, and wake, sleep or keep a screen awake.')
TYPES = {'string': 'text', 'int': 'number', 'bool': 'on/off'}
+17 -6
View File
@@ -48,10 +48,13 @@ FULL_PAGE_MIN_FIRMWARE = (0, 2, 62)
# Twenty tiles from firmware 0.2.7, ten before.
TWENTY_TILES_MIN_FIRMWARE = (0, 2, 7)
FIRST_MAX_TILES = 10
REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
REFS = {'cyd': 'main', 'guition': 'main'}
REPO = 'https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen'
REFS = {'cyd': 'main', 'guition': 'main', 'elecrow': 'main'}
# The boards that draw camera images and album covers and take the large layouts (a big RGB panel with PSRAM); the CYD
# is the small one. The Elecrow CrowPanel 7.0" (app 0.2.93) is the Guition's kind at 800x480, without the Rotation.
LARGE_BOARDS = ('guition', 'elecrow')
# Firmware shipped with this app release; screens below it get an update offer.
FIRMWARE_VERSION = '0.2.78'
FIRMWARE_VERSION = '0.2.79'
# 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.
@@ -202,6 +205,8 @@ def resolve_controls(tile):
# forms are recognised until every board has been reflashed with an English `name:`.
NAME_TILE_SETTINGS = ('Tile settings', 'Tegelinstellingen')
NAME_SCREEN_FIRMWARE = ('Screen firmware', 'Schermfirmware')
# Only the large boards have this sensor; its value names the board ("guition", or "elecrow" from firmware 0.2.79),
# an older Guition never publishes one.
NAME_GUITION_TYPE = ('Guition screen type', 'Guition schermtype')
NAME_DEVICE_NAME = ('Device name', 'Apparaatnaam')
NAME_IP_ADDRESS = ('IP address', 'IP-adres')
@@ -1408,7 +1413,8 @@ ALERT_FIELDS = (
# is a value to type as it is.
ALERT_TEXT_EXAMPLES = frozenset(('title', 'subtitle', 'button_text'))
# Bytes per field the firmware keeps (the profiles' ALERT_*_MAX); an accented letter takes two.
ALERT_LIMITS = {'cyd': {'title': 48, 'subtitle': 160, 'button_text': 12}, 'guition': {'title': 64, 'subtitle': 240, 'button_text': 16}}
ALERT_LIMITS = {'cyd': {'title': 48, 'subtitle': 160, 'button_text': 12}, 'guition': {'title': 64, 'subtitle': 240, 'button_text': 16},
'elecrow': {'title': 64, 'subtitle': 240, 'button_text': 16}}
ALERT_SUGGESTED_ICONS = ('doorbell', 'bell', 'bell-ring', 'alert-outline', 'alarm-light', 'lock', 'lock-open-variant', 'door-open',
'window-closed-variant', 'motion-sensor', 'cctv', 'smoke-detector', 'water-alert', 'fire', 'mailbox', 'car',
'account', 'account-group', 'washing-machine', 'robot-vacuum', 'timer-outline', 'check')
@@ -1559,8 +1565,13 @@ def discover_screens(registry, states, devices, areas):
area_map = {a['area_id']: a['name'] for a in areas}
versions = {item.get('device_id'): states.get(item['entity_id'], {}).get('state', 'unknown') for item in registry
if item.get('platform') == 'esphome' and item.get('original_name') in NAME_SCREEN_FIRMWARE}
boards = {item.get("device_id"): "guition" for item in registry
if item.get("platform") == "esphome" and item.get("original_name") in NAME_GUITION_TYPE}
# A screen with the type sensor is a large board: the one its value names, a Guition when the value is not a board
# (older firmware never publishes one, and a screen that is restarting reports "unavailable").
boards = {}
for item in registry:
if item.get("platform") == "esphome" and item.get("original_name") in NAME_GUITION_TYPE:
value = states.get(item['entity_id'], {}).get('state')
boards[item.get("device_id")] = value if value in LARGE_BOARDS else "guition"
def diagnostic(names, pattern):
found = {}
for item in registry:
+2 -2
View File
@@ -171,10 +171,10 @@ def capabilities(entity_id, actions, state, services):
elif domain == 'sun':
displays.append('sunpath')
elif domain in ('camera', 'image'):
# A live picture on the tile (app 0.2.91): the editor offers it on a Guition, the only board that draws images.
# A live picture on the tile (app 0.2.91): the editor offers it on a large board (a Guition, an Elecrow), the ones that draw images.
displays.append('live')
elif domain == 'media_player':
# The album cover in the icon's place (app 0.2.92), a Guition again.
# The album cover in the icon's place (app 0.2.92), a large board again.
displays.append('cover')
return {
'toggle': TOGGLE.format(domain=domain) in actions,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>ESP Screens</title>
<script type="module" crossorigin src="./assets/index-plSjhIie.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DQxVdm_5.css">
<script type="module" crossorigin src="./assets/index-C4Uqwn3z.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DGMtqWTE.css">
</head>
<body>
<div id="app"></div>
+2 -2
View File
@@ -1,8 +1,8 @@
name: ESP Screen Manager
version: "0.2.92"
version: "0.2.93"
slug: esp_screen_manager
description: Choose tiles for your CYD and Guition touchscreen, without reflashing.
url: https://github.com/MaxGramser/homeassistant_espscreen
url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen
arch: [aarch64, amd64]
startup: application
boot: auto
+1
View File
@@ -1593,6 +1593,7 @@
"board": "Welchen Bildschirm hast du?",
"board_cyd": "CYD · 2,8 Zoll",
"board_guition": "Guition · 4 Zoll",
"board_elecrow": "Elecrow CrowPanel · 7 Zoll",
"name": "Name",
"name_placeholder": "Bildschirm Wohnzimmer",
"device_name": "Gerätename",
+1
View File
@@ -1593,6 +1593,7 @@
"board": "Which screen do you have?",
"board_cyd": "CYD · 2.8 inch",
"board_guition": "Guition · 4 inch",
"board_elecrow": "Elecrow CrowPanel · 7 inch",
"name": "Name",
"name_placeholder": "Living room screen",
"device_name": "Device name",
+1
View File
@@ -1593,6 +1593,7 @@
"board": "¿Qué pantalla tienes?",
"board_cyd": "CYD · 2,8 pulgadas",
"board_guition": "Guition · 4 pulgadas",
"board_elecrow": "Elecrow CrowPanel · 7 pulgadas",
"name": "Nombre",
"name_placeholder": "Pantalla del salón",
"device_name": "Nombre del dispositivo",
+1
View File
@@ -1593,6 +1593,7 @@
"board": "Quel écran avez-vous ?",
"board_cyd": "CYD · 2,8 pouces",
"board_guition": "Guition · 4 pouces",
"board_elecrow": "Elecrow CrowPanel · 7 pouces",
"name": "Nom",
"name_placeholder": "Écran du salon",
"device_name": "Nom de l'appareil",
+1
View File
@@ -1593,6 +1593,7 @@
"board": "Quale schermo hai?",
"board_cyd": "CYD · 2,8 pollici",
"board_guition": "Guition · 4 pollici",
"board_elecrow": "Elecrow CrowPanel · 7 pollici",
"name": "Nome",
"name_placeholder": "Schermo del soggiorno",
"device_name": "Nome dispositivo",
+1
View File
@@ -1593,6 +1593,7 @@
"board": "Welk scherm heb je?",
"board_cyd": "CYD · 2,8 inch",
"board_guition": "Guition · 4 inch",
"board_elecrow": "Elecrow CrowPanel · 7 inch",
"name": "Naam",
"name_placeholder": "Scherm woonkamer",
"device_name": "Apparaatnaam",
+1
View File
@@ -1593,6 +1593,7 @@
"board": "Jaki masz ekran?",
"board_cyd": "CYD · 2,8 cala",
"board_guition": "Guition · 4 cale",
"board_elecrow": "Elecrow CrowPanel · 7 cali",
"name": "Nazwa",
"name_placeholder": "Ekran w salonie",
"device_name": "Nazwa urządzenia",
+1
View File
@@ -1593,6 +1593,7 @@
"board": "Que ecrã tem?",
"board_cyd": "CYD · 2,8 polegadas",
"board_guition": "Guition · 4 polegadas",
"board_elecrow": "Elecrow CrowPanel · 7 polegadas",
"name": "Nome",
"name_placeholder": "Ecrã da sala",
"device_name": "Nome do dispositivo",
+3 -3
View File
@@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import profiles # noqa: E402
BOARDS = {'cyd': 'home-like-2432s028.yaml', 'guition': 'guition-4848s040.yaml'}
BOARDS = {'cyd': 'home-like-2432s028.yaml', 'guition': 'guition-4848s040.yaml', 'elecrow': 'elecrow-crowpanel7.yaml'}
# The old manual profile bound tiles to fixed entities in the YAML. ESP Screens sends the tiles now, so
# none of this may come back: the fixed subscriptions, its tap handlers and scripts, its own vacuum card.
GONE = ['ha_state_tile1', 'tile1_brightness', 'tile6_climate_humidity', 'tile6_cover_state', 'smartdisplay_action',
@@ -42,8 +42,8 @@ class PackageTests(unittest.TestCase):
package = (ROOT / 'packages' / f'{board}.yaml').read_text()
self.assertNotIn('!secret', package)
self.assertNotIn('type: local', package)
self.assertIn('url: https://github.com/MaxGramser/homeassistant_espscreen.git', package)
self.assertIn('FONT_DIR: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts"', package)
self.assertIn('url: https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen.git', package)
self.assertIn('FONT_DIR: "https://gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen/raw/branch/main/fonts"', package)
# The fonts of the shared core come from that place; a checkout takes them from fonts/.
self.assertIn('file: "${FONT_DIR}/Roboto-500.ttf"', profiles.CORE.read_text())
self.assertIn('FONT_DIR: "fonts"', (ROOT / BOARDS[board]).read_text())
+148
View File
@@ -0,0 +1,148 @@
"""The Elecrow CrowPanel 7.0" (app 0.2.93, firmware 0.2.79): a third board, the Guition's kind at 800x480 on 4 MB of flash."""
import re
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import profiles # noqa: E402
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
import camera_feed # noqa: E402
import core # noqa: E402
sys.path.insert(0, str(ROOT / 'components/smart_display'))
import partitions # noqa: E402
BOARD = profiles.BOARDS['elecrow']
TEXT = BOARD.read_text()
SUBS = profiles.substitutions_of(BOARD)
class BoardFile(unittest.TestCase):
def test_it_is_a_landscape_guition(self):
self.assertEqual((SUBS['DISPLAY_W'], SUBS['DISPLAY_H']), ('800', '480'))
guition = profiles.substitutions_of(profiles.BOARDS['guition'])
# The vertical sizes are the Guition's: the rows, the tile height, the fonts.
for name in ('GRID_ROW1_Y', 'GRID_ROW2_Y', 'GRID_ROW3_Y', 'TILE_H', 'SCROLL_Y', 'SCROLL_H', 'HEADER_Y',
'FONT_LABEL_SIZE', 'FONT_ICON_SIZE', 'FONT_HEADLINE_SIZE', 'TILE_ICON_SIZE', 'PAGE_BAR_H'):
self.assertEqual(SUBS[name], guition[name], name)
# Two columns of tiles fill the width with the Guition's margins and gap.
col1, col2, tile = int(SUBS['GRID_COL1_X']), int(SUBS['GRID_COL2_X']), int(SUBS['TILE_W'])
self.assertEqual(col1, 16)
self.assertEqual(col2, col1 + tile + 12)
self.assertEqual(col2 + tile + 16, 800)
self.assertEqual(int(SUBS['PAGE_KEY_W']), 400)
self.assertEqual(int(SUBS['TOUCH_TEST_FAR_X']), 800 - 12 - 24 - 1)
self.assertEqual(int(SUBS['TOUCH_TEST_MID_X']), 400 - 12)
self.assertEqual(int(SUBS['SETTINGS_HOLD_W']), 800 - 2 * int(SUBS['EDGE_SWIPE_BAND_PX']))
def test_it_does_not_turn(self):
self.assertEqual(SUBS['ROTATION_SUPPORTED'], 'false')
self.assertEqual(SUBS['APPLY_ROTATION'], '')
self.assertEqual(SUBS['APPLY_ROTATION_ENTITY'], '')
self.assertNotIn('id: setting_rotation', TEXT)
def test_the_hardware_of_the_crowpanel(self):
self.assertIn('flash_size: 4MB', TEXT)
self.assertIn('platform: mipi_rgb', TEXT)
self.assertIn('model: RPI', TEXT)
self.assertIn('pclk_inverted: true', TEXT)
self.assertIn('red: [14, 21, 47, 48, 45]', TEXT)
self.assertIn('green: [9, 46, 3, 8, 16, 1]', TEXT)
self.assertIn('blue: [15, 7, 6, 5, 4]', TEXT)
self.assertIn('sda: GPIO19', TEXT)
self.assertIn('scl: GPIO20', TEXT)
self.assertIn('scan: false', TEXT)
self.assertIn('address: 0x5D', TEXT)
self.assertIn('pin: GPIO2', TEXT)
# The USB pins are the touch I2C: the header that frees them, and no interrupt pin or I/O expander.
self.assertIn('<esphome/components/smart_display/elecrow_usb_phy.h>', TEXT)
code = '\n'.join(line for line in TEXT.split('\n') if not line.lstrip().startswith('#'))
self.assertNotIn('interrupt_pin', code)
self.assertNotIn('pca9557', code.lower())
self.assertIn('assertion_level: SILENT', TEXT)
def test_the_flash_fits_by_three_measures(self):
self.assertIn('partitions: 4mb_wide_slots', TEXT)
self.assertIn('CONFIG_MBEDTLS_TLS_CLIENT_ONLY: y', TEXT)
fonts = re.search(r'^font:\n(.*?)(?=^[a-z_]+:)', TEXT, re.M | re.S)[1]
for font in ('materialdesign_icons', 'materialdesign_icons_mini', 'materialdesign_icons_big', 'watch_icon'):
self.assertIn(f'- id: !extend {font}\n bpp: 2', fonts)
self.assertNotIn('!extend headline', fonts)
def test_the_usb_phy_header_runs_before_every_component(self):
header = (ROOT / 'components/smart_display/elecrow_usb_phy.h').read_text()
self.assertIn('__attribute__((constructor(101)))', header)
self.assertIn('usb_serial_jtag_ll_phy_enable_pad(false)', header)
for reg in ('IO_MUX_GPIO19_REG', 'IO_MUX_GPIO20_REG'):
self.assertIn(reg, header)
# ESPHome includes every header of the component in every build: the code is behind a flag only this board sets.
self.assertIn('#if defined(ESP_SCREENS_FREE_USB_PADS) && ESP_SCREENS_FREE_USB_PADS', header)
for board, path in profiles.BOARDS.items():
self.assertEqual('-DESP_SCREENS_FREE_USB_PADS=1' in path.read_text(), board == 'elecrow', board)
def test_the_partition_table(self):
table = partitions.TABLES['4mb_wide_slots']
rows = [[cell.strip() for cell in line.split(',')] for line in table.splitlines() if line.strip() and not line.startswith('#')]
by_name = {row[0]: row for row in rows}
self.assertEqual(by_name['app0'][1:], ['app', 'ota_0', '0x10000', '0x1F0000'])
self.assertEqual(by_name['app1'][1:], ['app', 'ota_1', '0x200000', '0x1F0000'])
self.assertEqual(int(by_name['nvs'][3], 16) + int(by_name['nvs'][4], 16), 4 * 1024 * 1024)
# The slots touch neither each other nor the table before them.
self.assertEqual(int(by_name['app0'][3], 16) + int(by_name['app0'][4], 16), int(by_name['app1'][3], 16))
self.assertEqual(int(by_name['app1'][3], 16) + int(by_name['app1'][4], 16), int(by_name['nvs'][3], 16))
self.assertGreaterEqual(int(by_name['app0'][3], 16), int(by_name['phy_init'][3], 16) + int(by_name['phy_init'][4], 16))
for name in ('app0', 'app1'):
self.assertEqual(int(by_name[name][3], 16) % 0x10000, 0, f'{name} is 64 KB aligned')
def test_the_entry_files(self):
for name in ('elecrow-crowpanel7.yaml', 'packages/elecrow.yaml'):
self.assertIn('boards/elecrow-crowpanel7.yaml', (ROOT / name).read_text(), name)
self.assertIn('files: [packages/elecrow.yaml]', (ROOT / 'installers/elecrow.yaml').read_text())
self.assertEqual(profiles.board_of('packages/elecrow.yaml'), 'elecrow')
class TheApp(unittest.TestCase):
def test_the_board_is_read_from_the_type_sensor(self):
registry = []
states = {}
for device, value in (('d1', 'elecrow'), ('d2', 'guition'), ('d3', 'unavailable'), ('d4', None), ('d5', 'cyd')):
registry += [{'entity_id': f'text.{device}_tiles', 'platform': 'esphome', 'original_name': 'Tile settings', 'device_id': device},
{'entity_id': f'sensor.{device}_type', 'platform': 'esphome', 'original_name': 'Guition screen type', 'device_id': device}]
states[f'text.{device}_tiles'] = {'state': 'Synced'}
if value is not None:
states[f'sensor.{device}_type'] = {'state': value}
registry.append({'entity_id': 'text.d6_tiles', 'platform': 'esphome', 'original_name': 'Tile settings', 'device_id': 'd6'})
states['text.d6_tiles'] = {'state': 'Synced'}
screens = {s['device_id']: s['board'] for s in core.discover_screens(registry, states, [{'id': f'd{n}', 'name': f'D{n}'} for n in range(1, 7)], [])}
# The value names the board; a sensor without a board's name (older firmware, a restart) is a Guition; no sensor is no large board.
self.assertEqual(screens, {'d1': 'elecrow', 'd2': 'guition', 'd3': 'guition', 'd4': 'guition', 'd5': 'guition', 'd6': 'unknown'})
def test_the_app_knows_the_board(self):
self.assertIn('elecrow', core.REFS)
self.assertEqual(core.ALERT_LIMITS['elecrow'], {'title': int(SUBS['ALERT_TITLE_MAX']), 'subtitle': int(SUBS['ALERT_SUBTITLE_MAX']),
'button_text': int(SUBS['ALERT_BUTTON_MAX'])})
self.assertIn('files: [packages/elecrow.yaml]', core.installation_yaml({'board': 'elecrow', 'name': 'hall', 'friendly_name': 'Hall'}))
def test_the_camera_boxes_are_the_profile_sizes(self):
self.assertEqual(camera_feed.BOXES['elecrow'], {'full': (int(SUBS['CAMERA_FULL_W']), int(SUBS['CAMERA_FULL_H'])),
'thumb': (int(SUBS['CAMERA_THUMB_W']), int(SUBS['CAMERA_THUMB_H']))})
self.assertTrue(camera_feed.can_show({'board': 'elecrow', 'firmware': '0.2.79'}))
self.assertTrue(camera_feed.can_show_live({'board': 'elecrow', 'firmware': '0.2.79'}))
self.assertTrue(camera_feed.can_show_cover({'board': 'elecrow', 'firmware': '0.2.79'}))
# The alert's picture fits the wider card.
self.assertEqual(int(SUBS['CAMERA_THUMB_W']), int(SUBS['ALERT_CARD_W']) - 2 * int(SUBS['ALERT_IMAGE_INSET']))
def test_the_editor_offers_the_board(self):
installer = (ROOT / 'web/src/components/InstallerView.vue').read_text()
self.assertIn('value="elecrow"', installer)
for path in sorted((ROOT / 'screen_manager/translations').glob('*.json')):
if path.stem != 'en-GB':
self.assertIn('"board_elecrow"', path.read_text(), path.name)
store = (ROOT / 'web/src/store.ts').read_text()
self.assertIn('LARGE_BOARDS = ["guition", "elecrow"]', store)
self.assertIn('elecrow: { width: 768', (ROOT / 'web/src/model/topbar.ts').read_text())
if __name__ == '__main__':
unittest.main()
+1 -1
View File
@@ -20,7 +20,7 @@ CHANGELOG = ROOT / 'screen_manager/CHANGELOG.md'
APP_HEADING = re.compile(r'^## (\d+)\.(\d+)\.(\d+)\b(.*)$', re.M)
FIRMWARE_IN_HEADING = re.compile(r'\(firmware (\d+\.\d+\.\d+)\)')
# The published entries (packages/<board>.yaml) point ${FONT_DIR} at the raw GitHub URL of fonts/ on main.
FONT_URL = re.compile(r'https://raw\.githubusercontent\.com/MaxGramser/homeassistant_espscreen/[^/\s"\']+/(fonts/[^"\'\s]+)')
FONT_URL = re.compile(r'https://gittea\.trippedonmy\.name/Xandrossian/homeassistant_espscreen/raw/branch/[^/\s"\']+/(fonts/[^"\'\s]+)')
def app_headings():
+10 -7
View File
@@ -4,7 +4,7 @@
#
# tools/check.sh the Python tests, every tests/*.cpp, the package check, the icon generator's --check, the editor's
# tests, types and build, and whether the editor bundle in Git equals that build
# tools/check.sh --firmware compiles both board profiles and applies the CYD's flash budget
# tools/check.sh --firmware compiles every board profile and applies the flash budget to the 4 MB boards (CYD, Elecrow)
# tools/check.sh --all both
# --baseline BYTES with --firmware: the CYD image of the last release, to print the growth
#
@@ -175,8 +175,8 @@ api_encryption_key: "Y2hlY2stYnVpbGQtcGxhY2Vob2xkZXIta2V5LTMyYnk="
ota_password: "check-build-ota-password-0000000"
ap_password: "check-ap-passwd0"
EOF
for board in cyd guition; do
file=$([[ $board == cyd ]] && echo home-like-2432s028.yaml || echo guition-4848s040.yaml)
for board in cyd guition elecrow; do
case $board in cyd) file=home-like-2432s028.yaml ;; guition) file=guition-4848s040.yaml ;; elecrow) file=elecrow-crowpanel7.yaml ;; esac
cp "$ROOT/$file" "$config/check-$board.yaml" || return 1
if ! grep -q '^captive_portal:' "$config/check-$board.yaml" || ! grep -q '^ ap:' "$config/check-$board.yaml"; then
echo "$file has no captive_portal: or wifi ap: any more, unlike the YAML users get; the flash figures would read low."
@@ -186,16 +186,16 @@ EOF
echo "Check profiles in $config"
}
compile_board() { # compile_board cyd|guition
compile_board() { # compile_board cyd|guition|elecrow
local board=$1
cd "$WORK/config" || return 1
"${ESPHOME_CMD[@]}" -s DEVICE_NAME "check-$board" -s DEVICE_FRIENDLY_NAME "Check $board" compile "check-$board.yaml" || return 1
flash_report "$board"
}
# flash_report BOARD [budget]: the image against its update slot, both read from the build, and on the CYD the growth
# against --baseline and the budget of docs/RELEASING.md step 2. Exit 3 means over 90 %: it passes, but the release has
# to say why.
# flash_report BOARD [budget]: the image against its update slot, both read from the build, and on the 4 MB boards (the
# CYD, the Elecrow) the growth against --baseline (the CYD only) and the budget of docs/RELEASING.md step 2. Exit 3 means
# over 90 %: it passes, but the release has to say why.
flash_report() {
local board=$1 mode=${2:-} status=0 out="$WORK/flash-$1.txt" against=""
[[ $board == cyd ]] && against=$baseline
@@ -251,6 +251,7 @@ EOF
}
cyd_budget() { flash_report cyd budget; }
elecrow_budget() { flash_report elecrow budget; }
# ---- Run ----
@@ -282,6 +283,8 @@ if ((want_firmware)); then
run "Firmware: CYD" compile_board cyd
if ((last_ok)); then run "CYD flash budget" cyd_budget; else skip "CYD flash budget" "no CYD build"; fi
run "Firmware: Guition" compile_board guition
run "Firmware: Elecrow" compile_board elecrow
if ((last_ok)); then run "Elecrow flash budget" elecrow_budget; else skip "Elecrow flash budget" "no Elecrow build"; fi
else
skip "Firmware builds" "ESPHome or the check profiles are missing"
fi
+1 -1
View File
@@ -200,7 +200,7 @@ LINT_KEEP = {
'None', 'Auto', 'Wh',
# Placeholders of the YAML tree that the runtime tiles replace before a screen shows them, and profile defaults.
'Lamp', 'Plug', 'Evening', 'All off', 'AC', 'Vacuum', 'Tile 7', 'Tile 8', 'Tile 9', 'Tile 10', 'Light', 'Light Color',
'Climate', 'Example lamp', 'My CYD', 'My Guition',
'Climate', 'Example lamp', 'My CYD', 'My Guition', 'My CrowPanel',
}
LINT_FILES = ('components/smart_display/*.h', 'packages/core.yaml', 'packages/boards/*.yaml')
+9 -7
View File
@@ -1,7 +1,8 @@
"""The board profiles as the tests and tools read them (app 0.2.84+).
A screen is three files: an entry (`home-like-2432s028.yaml` or `packages/cyd.yaml` for a CYD, `guition-4848s040.yaml`
or `packages/guition.yaml` for a Guition), `packages/core.yaml` that every board shares, and the board's own file under
or `packages/guition.yaml` for a Guition, `elecrow-crowpanel7.yaml` or `packages/elecrow.yaml` for an Elecrow CrowPanel 7.0"),
`packages/core.yaml` that every board shares, and the board's own file under
`packages/boards/`. `text(name)` is the three of them one after the other, so a check that looks for a line finds it
wherever it lives; `resolved(name)` also fills in the `${NAME}` substitutions from the three files (the sizes, the fonts
and the hooks with the board's code), for a check that needs the numbers or the whole lambda. Neither is what ESPHome
@@ -15,12 +16,13 @@ import yaml
ROOT = Path(__file__).resolve().parents[1]
CORE = ROOT / 'packages/core.yaml'
BOARDS = {'cyd': ROOT / 'packages/boards/cyd-2432s028.yaml', 'guition': ROOT / 'packages/boards/guition-4848s040.yaml'}
ENTRIES = {'home-like-2432s028.yaml': 'cyd', 'guition-4848s040.yaml': 'guition',
'packages/cyd.yaml': 'cyd', 'packages/guition.yaml': 'guition'}
# The names the four entry files are known by, in the order the older tests listed them.
PROFILES = ('home-like-2432s028.yaml', 'guition-4848s040.yaml')
PACKAGES = ('packages/cyd.yaml', 'packages/guition.yaml')
BOARDS = {'cyd': ROOT / 'packages/boards/cyd-2432s028.yaml', 'guition': ROOT / 'packages/boards/guition-4848s040.yaml',
'elecrow': ROOT / 'packages/boards/elecrow-crowpanel7.yaml'}
ENTRIES = {'home-like-2432s028.yaml': 'cyd', 'guition-4848s040.yaml': 'guition', 'elecrow-crowpanel7.yaml': 'elecrow',
'packages/cyd.yaml': 'cyd', 'packages/guition.yaml': 'guition', 'packages/elecrow.yaml': 'elecrow'}
# The names the six entry files are known by, in the order the older tests listed them.
PROFILES = ('home-like-2432s028.yaml', 'guition-4848s040.yaml', 'elecrow-crowpanel7.yaml')
PACKAGES = ('packages/cyd.yaml', 'packages/guition.yaml', 'packages/elecrow.yaml')
NAMES = PROFILES + PACKAGES
+18 -12
View File
@@ -20,14 +20,16 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WORK = ROOT / '.esphome' / 'render-topbar'
PROFILES = {'guition': 'guition-4848s040.yaml', 'cyd': 'home-like-2432s028.yaml'}
PROFILES = {'guition': 'guition-4848s040.yaml', 'cyd': 'home-like-2432s028.yaml', 'elecrow': 'elecrow-crowpanel7.yaml'}
sys.path.insert(0, str(ROOT / 'tools'))
import profiles # noqa: E402
FONTS = ('headline', 'time_label', 'sublabel_big', 'label', 'materialdesign_icons', 'materialdesign_icons_mini')
# Hardware headers the host cannot compile; the top bar needs none of them.
SKIP = {'guition_diagnostics.h', '__pycache__'}
# Header band per board: page width and height above the tiles.
BANDS = {'guition': (480, 72), 'cyd': (320, 40)}
# Header band per board: page width and height above the tiles. The Elecrow's bar is the Guition's across 800 px
# (the same fonts and insets), so it renders with the Guition's fonts.
BANDS = {'guition': (480, 72), 'cyd': (320, 40), 'elecrow': (800, 72)}
LARGE = ('guition', 'elecrow')
# (slug, title, items) with items as C++ expressions of the render lambda's helpers.
SCENARIOS = (
@@ -56,8 +58,8 @@ def font_blocks(board):
def band(board, y):
width, height = BANDS[board]
margin, top = (16, 22) if board == 'guition' else (11, 14)
tile_y, tile_w, tile_h, col2, radius = (72, 218, 108, 246, 22) if board == 'guition' else (40, 147, 52, 164, 18)
margin, top = (16, 22) if board in LARGE else (11, 14)
tile_y, tile_w, tile_h, col2, radius = {'guition': (72, 218, 108, 246, 22), 'elecrow': (72, 378, 108, 406, 22), 'cyd': (40, 147, 52, 164, 18)}[board]
return f''' - obj:
id: {board}_page
x: 0
@@ -85,7 +87,7 @@ def band(board, y):
text: ""
text_font: time_label_{board}
- obj:
x: {margin if board == 'guition' else 9}
x: {margin if board in LARGE else 9}
y: {tile_y}
width: {tile_w}
height: {tile_h}
@@ -128,6 +130,7 @@ struct Board {{ const char *name; lv_obj_t *page, *room, *time; const lv_font_t
std::vector<Board> boards = {{
{{"guition", id(guition_page), id(guition_room), id(guition_time), id(sublabel_big_guition)->get_lv_font(), id(materialdesign_icons_mini_guition)->get_lv_font()}},
{{"cyd", id(cyd_page), id(cyd_room), id(cyd_time), id(sublabel_big_cyd)->get_lv_font(), id(materialdesign_icons_mini_cyd)->get_lv_font()}},
{{"elecrow", id(elecrow_page), id(elecrow_room), id(elecrow_time), id(sublabel_big_guition)->get_lv_font(), id(materialdesign_icons_mini_guition)->get_lv_font()}},
}};
for (auto &scenario : scenarios) {{
for (auto &board : boards) {{
@@ -200,8 +203,8 @@ display:
- platform: sdl
id: sdl_display
dimensions:
width: 480
height: 480
width: 800
height: 540
auto_clear_enabled: false
update_interval: never
@@ -227,7 +230,7 @@ lvgl:
points:
- 0, 0
- 1, 1
{band('guition', 0)}{band('cyd', 220)}
{band('guition', 0)}{band('cyd', 220)}{band('elecrow', 340).replace('headline_elecrow', 'headline_guition').replace('time_label_elecrow', 'time_label_guition')}
script:
- id: action_seed
then:
@@ -254,15 +257,18 @@ def crop_scale(image, width, height, scale):
return bytes(out)
def sheet(out):
"""Header bands of every scenario: Guition 2x above CYD 3x, 12 px white between."""
"""Header bands of every scenario: Guition 2x above CYD 3x above the Elecrow 1x, 12 px white between."""
width, gap = 960, 12
rows = []
for slug, _, _ in SCENARIOS:
for board, scale in (('guition', 2), ('cyd', 3)):
for board, scale in (('guition', 2), ('cyd', 3), ('elecrow', 1)):
image = read_ppm(out / f'{slug}-{board}.ppm')
band_w, band_h = BANDS[board]
png(out / f'{slug}-{board}.png', image[0], image[1], image[2])
rows.append((crop_scale(image, band_w, band_h, scale), band_h * scale))
band = crop_scale(image, band_w, band_h, scale)
# A band narrower than the sheet (the Elecrow's 800) gets white at its right.
pad = bytes([255]) * (width - band_w * scale) * 3
rows.append((b''.join(band[y * band_w * scale * 3:(y + 1) * band_w * scale * 3] + pad for y in range(band_h * scale)), band_h * scale))
rows.append((bytes([255]) * width * 3 * gap * 2, gap * 2))
height = sum(h for _, h in rows)
png(out / 'sheet.png', width, height, b''.join(data for data, _ in rows))
+2 -2
View File
@@ -3,7 +3,7 @@
import { computed } from "vue";
import { t } from "../i18n";
import { cellsOf, pageOf, sizeOf, SLOTS_PER_PAGE, spanOf } from "../model/layout";
import { isGuition, openBar, removePage, state } from "../store";
import { isGuition, isWideScreen, openBar, removePage, state } from "../store";
import type { Tile } from "../types";
import TileCard from "./TileCard.vue";
import TopbarSvg from "./TopbarSvg.vue";
@@ -29,7 +29,7 @@ function pickCell(slot: number) {
<button v-if="empty" type="button" class="btn mini" :title="t('editor.page.remove_title')" @click="removePage(page)">{{ t("editor.page.remove") }}</button>
<span v-else>{{ filled }} / {{ SLOTS_PER_PAGE }}</span>
</div>
<div class="device" :class="{ cyd: !isGuition }">
<div class="device" :class="{ cyd: !isGuition, wide: isWideScreen }">
<div class="bar-wrap" :class="{ selected: barSelected }" :title="t('editor.page.edit_bar')" role="button" tabindex="0"
@click="openBar(0)" @keydown.enter.prevent="openBar(0)">
<TopbarSvg />
+1
View File
@@ -189,6 +189,7 @@ onBeforeUnmount(() => clearInterval(poll));
<div class="boards">
<label class="board"><input type="radio" name="board" value="cyd" v-model="form.board" /><span><b>{{ t("editor.installer.board_cyd") }}</b><small>ESP32-2432S028 · 320 × 240</small></span></label>
<label class="board"><input type="radio" name="board" value="guition" v-model="form.board" /><span><b>{{ t("editor.installer.board_guition") }}</b><small>ESP32-S3-4848S040 · 480 × 480 · GT911</small></span></label>
<label class="board"><input type="radio" name="board" value="elecrow" v-model="form.board" /><span><b>{{ t("editor.installer.board_elecrow") }}</b><small>DIS08070H · ESP32-S3 N4R8 · 800 × 480 · GT911</small></span></label>
</div>
</fieldset>
<div class="field">
+2 -2
View File
@@ -3,7 +3,7 @@
import { computed } from "vue";
import { t } from "../i18n";
import { entriesOf, hasGaps, MAX_PAGES, pageCount } from "../model/layout";
import { addPage, closeInspector, currentScreen, pageReachWarning, pagesShown, state, supports, tileLimit } from "../store";
import { addPage, closeInspector, currentScreen, isGuition, isWideScreen, pageReachWarning, pagesShown, state, supports, tileLimit } from "../store";
import DevicePage from "./DevicePage.vue";
import Library from "./Library.vue";
@@ -37,7 +37,7 @@ function onCanvasClick(e: MouseEvent) {
<DevicePage v-for="page in shown" :key="page" :page="page - 1" :entries="entries" :pages="pages" :moving="state.drag.moving" />
<div class="page ghost" :class="{ disabled: !canAdd }">
<div class="page-label"><span>{{ t("editor.page.label", { page: shown + 1 }) }}</span></div>
<div class="device" :class="{ cyd: currentScreen?.board !== 'guition' }" id="add-page" role="button" :tabindex="canAdd ? 0 : -1" @click="canAdd && addPage()" @keydown.enter.prevent="canAdd && addPage()">
<div class="device" :class="{ cyd: !isGuition, wide: isWideScreen }" id="add-page" role="button" :tabindex="canAdd ? 0 : -1" @click="canAdd && addPage()" @keydown.enter.prevent="canAdd && addPage()">
{{ canAdd ? t("editor.layout.add_page") : t("editor.layout.max_pages", MAX_PAGES) }}
</div>
</div>
+2
View File
@@ -50,6 +50,8 @@ export function agoText(then: number, now = Math.floor(Date.now() / 1000), local
export type BarMetrics = { width: number; top: number; name: number; text: number; icon: number };
export const BAR_METRICS: Record<string, BarMetrics> = {
guition: { width: 448, top: 36, name: 27, text: 21, icon: 26 },
// The Elecrow CrowPanel 7.0" (app 0.2.93): the Guition's bar across 800 px, the same insets and fonts.
elecrow: { width: 768, top: 36, name: 27, text: 21, icon: 26 },
cyd: { width: 298, top: 24, name: 18, text: 14, icon: 18 },
};
export type ItemView = { icon?: string | null; text?: string; color?: string | null; shown: boolean; analog?: boolean; loading?: boolean };
+6 -2
View File
@@ -86,8 +86,12 @@ export const pageTilesRepeat = computed(() => {
return typeof repeat === "boolean" ? repeat : supports(0, 2, 65);
});
export const repeatable = (id: string) => pageTilesRepeat.value && pageTarget(id) > 0;
export const isGuition = computed(() => currentScreen.value?.board === "guition");
export const barMetrics = computed(() => BAR_METRICS[isGuition.value ? "guition" : "cyd"]);
// The large boards (app 0.2.93): a Guition or an Elecrow CrowPanel 7.0", the ones that draw camera images and album
// covers and take the large cards; `isGuition` keeps its name where it means that. The CYD is the small one.
export const LARGE_BOARDS = ["guition", "elecrow"];
export const isGuition = computed(() => LARGE_BOARDS.includes(currentScreen.value?.board ?? ""));
export const isWideScreen = computed(() => currentScreen.value?.board === "elecrow");
export const barMetrics = computed(() => BAR_METRICS[currentScreen.value?.board ?? ""] ?? BAR_METRICS.cyd);
export const currentTile = computed<Tile | undefined>(() =>
state.selectedTile && state.layout?.tiles.includes(state.selectedTile) ? state.selectedTile : undefined);
// The reactive copy and the plain object are the same tile.
+1
View File
@@ -137,6 +137,7 @@ a:hover { text-decoration: underline; }
/* The device mock keeps the Guition light palette on purpose: it is the screen, not the page. */
.device { background: #e7e7e7; border-radius: 18px; padding: 10px; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 8px; color: #1b1b1b; aspect-ratio: 1 / 1; }
.device.cyd { aspect-ratio: 4 / 3; }
.device.wide { aspect-ratio: 5 / 3; }
.bar-wrap { cursor: pointer; border-radius: 8px; padding: 0 2px; }
.bar-wrap:hover { background: #dedede; }
.bar-wrap.selected { outline: 2px solid var(--accent); outline-offset: 1px; }
+5
View File
@@ -237,6 +237,11 @@ describe("TileInspector: a live picture on a camera tile (app 0.2.91)", () => {
Object.assign(state.inventory.screens[0], { board: "guition" });
tile.options = { size: "full" };
expect(choices(mount(TileInspector, { props: { tile } }), "Display")).toEqual(["Name and status", "Large value"]);
// An Elecrow CrowPanel 7.0" (app 0.2.93) is a large board like the Guition: the cover is on offer again.
Object.assign(state.inventory.screens[0], { board: "elecrow" });
tile.options = { size: "single" };
expect(choices(mount(TileInspector, { props: { tile } }), "Display")).toContain("Album cover");
Object.assign(state.inventory.screens[0], { board: "guition" });
});
});