Files
homeassistant_espscreen/diagnostics/capture_ui.py
T
Xandrossian 9857cc3bda
CI / checks (push) Successful in 12m50s
CI / firmware (add-on) (push) Successful in 18m19s
CI / firmware (min_version) (push) Successful in 17m55s
Add the Elecrow CrowPanel 7.0" as a third board, and take the packages and the add-on from Gitea (0.2.93)
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.
2026-09-20 23:18:01 -07:00

55 lines
2.7 KiB
Python

"""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
import re
import struct
import zlib
import yaml
from aioesphomeapi import APIClient, LogLevel
def png(rows,width,height):
pixels=bytearray()
for y in range(height):
pixels.append(0)
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',width,height,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(pixels))+chunk(b'IEND',b'')
async def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--host',default='guition-wallbox.local')
p.add_argument('--name',default='guition-wallbox')
p.add_argument('--secrets',type=Path,default=Path(__file__).resolve().parents[1]/'secrets.yaml')
p.add_argument('--output',type=Path,required=True)
args=p.parse_args()
if args.output.exists():raise ValueError('Output already exists; choose a different name.')
client=APIClient(args.host,6053,noise_psk=yaml.safe_load(args.secrets.read_text())['api_encryption_key'],expected_name=args.name)
await client.connect(login=True)
try:
_,services=await client.list_entities_services()
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
# 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(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()
if __name__=='__main__':asyncio.run(main())