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
+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" });
});
});