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.
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user