9857cc3bda
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.
149 lines
8.9 KiB
Python
149 lines
8.9 KiB
Python
"""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()
|