From ce72c6cd541119a8bb73ec92234cb0ea89ea2b21 Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Fri, 19 Jul 2024 21:33:19 +0400 Subject: [PATCH] refactor(core): make the power-off menu, a sub-menu with power-off and reboot action items - closes #123 --- CHANGELOG.md | 1 + poetry.lock | 21 +++------- pyproject.toml | 6 +-- .../store-desktop-000.jsonc | 41 +++++++++++++++++-- .../app_runs_and_exits/store-rpi-000.jsonc | 41 +++++++++++++++++-- .../store-desktop-000.jsonc | 41 +++++++++++++++++-- .../all_services_register/store-rpi-000.jsonc | 41 +++++++++++++++++-- ubo_app/display.py | 7 +++- ubo_app/menu_app/menu_central.py | 2 +- ubo_app/services/040-camera/setup.py | 4 +- ubo_app/side_effects.py | 41 ++++++++++++------- ubo_app/store/core/__init__.py | 16 +++++++- ubo_app/store/core/_menus.py | 27 ++++++++++-- ubo_app/store/core/reducer.py | 11 ++++- ubo_app/store/update_manager/reducer.py | 4 +- ubo_app/utils/hardware.py | 8 ---- 16 files changed, 245 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 138038d2..273effe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - feat(notifications): add `progress` and `progress_weight` properties to `Notification` object and show the progress on the header of the app - feat(core): show the progress of the update using the new `progress` property of the `Notification` object - fix(camera): render the viewfinder on the display even if the display is paused - closes #78 +- refactor(core): make the power-off menu, a sub-menu with power-off and reboot action items - closes #123 ## Version 0.15.4 diff --git a/poetry.lock b/poetry.lock index 314014af..79190e84 100644 --- a/poetry.lock +++ b/poetry.lock @@ -991,16 +991,6 @@ files = [ {file = "lgpio-0.2.2.0.tar.gz", hash = "sha256:11372e653b200f76a0b3ef8a23a0735c85ec678a9f8550b9893151ed0f863fff"}, ] -[[package]] -name = "lock" -version = "2018.3.25.2110" -description = "module for enabling file locks" -optional = false -python-versions = "*" -files = [ - {file = "lock-2018.3.25.2110.tar.gz", hash = "sha256:cc5ac770930493eed7a8cfd0cf2568a125faf112eb8aa6b6149b3e581523d0c7"}, -] - [[package]] name = "matplotlib-inline" version = "0.1.7" @@ -1593,17 +1583,16 @@ testing = ["filelock"] [[package]] name = "python-debouncer" -version = "0.1.4" +version = "0.1.5" description = "Debouncer and friends for Python" optional = false -python-versions = ">=3.9,<4.0" +python-versions = "<4.0,>=3.9" files = [ - {file = "python_debouncer-0.1.4-py3-none-any.whl", hash = "sha256:fbab13f8b1639144d99184836d169b22cd4bc54339e331f83cd45e581234d074"}, - {file = "python_debouncer-0.1.4.tar.gz", hash = "sha256:a053a4b35108a72c2c4af6fddb685626487b012d21aa16e4b1d4437b48484b05"}, + {file = "python_debouncer-0.1.5-py3-none-any.whl", hash = "sha256:d92bef943f169f93b5be79bc5c71e315f155ec7090e95c02a14c420ebdd4765d"}, + {file = "python_debouncer-0.1.5.tar.gz", hash = "sha256:ad4bc0229334f12cf0268a55c76ea98cb89a28f933c12fb556f92623e0c72b04"}, ] [package.dependencies] -lock = ">=2018.3.25.2110,<2019.0.0.0" python-immutable = ">=1.0.4,<2.0.0" [[package]] @@ -2152,4 +2141,4 @@ dev = ["headless-kivy", "headless-kivy"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "3a37c1b9c59c2e633053f2b0c925a601a87da9d348c3ccefb4fc19e91dda973d" +content-hash = "6a39da6bb5d8d280c9bb23b0a1d5e42e627d0f897d3f30b85744643ed2d95056" diff --git a/pyproject.toml b/pyproject.toml index 668d15e2..960cd386 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ python-redux = "^0.15.9" pyzbar = "^0.1.9" sdbus-networkmanager = { version = "^2.0.0", markers = "platform_machine=='aarch64'" } rpi_ws281x = { version = "^5.0.0", markers = "platform_machine=='aarch64'" } -python-debouncer = "^0.1.4" +python-debouncer = "^0.1.5" pulsectl = "^23.5.2" aiohttp = "^3.9.1" semver = "^3.0.2" @@ -55,13 +55,13 @@ optional = true [tool.poetry.group.dev.dependencies] poethepoet = "^0.24.4" -pyright = "^1.1.371" +pyright = "^1.1.372" pytest = "^8.0.0" pytest-asyncio = "^0.23.5.post1" pytest-cov = "^4.1.0" pytest-timeout = "^2.3.1" pytest-xdist = "^3.5.0" -ruff = "^0.5.1" +ruff = "^0.5.3" tenacity = "^8.2.3" toml = "^0.10.2" pytest-mock = "^3.14.0" diff --git a/tests/integration/results/test_core/app_runs_and_exits/store-desktop-000.jsonc b/tests/integration/results/test_core/app_runs_and_exits/store-desktop-000.jsonc index 17d9907a..710578de 100644 --- a/tests/integration/results/test_core/app_runs_and_exits/store-desktop-000.jsonc +++ b/tests/integration/results/test_core/app_runs_and_exits/store-desktop-000.jsonc @@ -204,7 +204,6 @@ } }, { - "action": ">", "background_color": "#68B7FF", "color": [ 1, @@ -214,9 +213,45 @@ ], "icon": "󰐥", "is_short": true, - "label": "Turn off", + "label": "", "opacity": null, - "progress": null + "progress": null, + "sub_menu": { + "items": [ + { + "action": ">", + "background_color": "#68B7FF", + "color": [ + 1, + 1, + 1, + 1 + ], + "icon": "󰜉", + "is_short": false, + "label": "Reboot", + "opacity": null, + "progress": null + }, + { + "action": ">", + "background_color": "#68B7FF", + "color": [ + 1, + 1, + 1, + 1 + ], + "icon": "󰤂", + "is_short": false, + "label": "Power off", + "opacity": null, + "progress": null + } + ], + "placeholder": null, + "title": "󰐥Power" + } } ], "placeholder": null, diff --git a/tests/integration/results/test_core/app_runs_and_exits/store-rpi-000.jsonc b/tests/integration/results/test_core/app_runs_and_exits/store-rpi-000.jsonc index b21aa984..ee1b72a3 100644 --- a/tests/integration/results/test_core/app_runs_and_exits/store-rpi-000.jsonc +++ b/tests/integration/results/test_core/app_runs_and_exits/store-rpi-000.jsonc @@ -204,7 +204,6 @@ } }, { - "action": ">", "background_color": "#68B7FF", "color": [ 1, @@ -214,9 +213,45 @@ ], "icon": "󰐥", "is_short": true, - "label": "Turn off", + "label": "", "opacity": null, - "progress": null + "progress": null, + "sub_menu": { + "items": [ + { + "action": ">", + "background_color": "#68B7FF", + "color": [ + 1, + 1, + 1, + 1 + ], + "icon": "󰜉", + "is_short": false, + "label": "Reboot", + "opacity": null, + "progress": null + }, + { + "action": ">", + "background_color": "#68B7FF", + "color": [ + 1, + 1, + 1, + 1 + ], + "icon": "󰤂", + "is_short": false, + "label": "Power off", + "opacity": null, + "progress": null + } + ], + "placeholder": null, + "title": "󰐥Power" + } } ], "placeholder": null, diff --git a/tests/integration/results/test_services/all_services_register/store-desktop-000.jsonc b/tests/integration/results/test_services/all_services_register/store-desktop-000.jsonc index db70edc3..a6d9f5d6 100644 --- a/tests/integration/results/test_services/all_services_register/store-desktop-000.jsonc +++ b/tests/integration/results/test_services/all_services_register/store-desktop-000.jsonc @@ -509,7 +509,6 @@ } }, { - "action": ">", "background_color": "#68B7FF", "color": [ 1, @@ -519,9 +518,45 @@ ], "icon": "󰐥", "is_short": true, - "label": "Turn off", + "label": "", "opacity": null, - "progress": null + "progress": null, + "sub_menu": { + "items": [ + { + "action": ">", + "background_color": "#68B7FF", + "color": [ + 1, + 1, + 1, + 1 + ], + "icon": "󰜉", + "is_short": false, + "label": "Reboot", + "opacity": null, + "progress": null + }, + { + "action": ">", + "background_color": "#68B7FF", + "color": [ + 1, + 1, + 1, + 1 + ], + "icon": "󰤂", + "is_short": false, + "label": "Power off", + "opacity": null, + "progress": null + } + ], + "placeholder": null, + "title": "󰐥Power" + } } ], "placeholder": null, diff --git a/tests/integration/results/test_services/all_services_register/store-rpi-000.jsonc b/tests/integration/results/test_services/all_services_register/store-rpi-000.jsonc index 323eb5ca..92385eff 100644 --- a/tests/integration/results/test_services/all_services_register/store-rpi-000.jsonc +++ b/tests/integration/results/test_services/all_services_register/store-rpi-000.jsonc @@ -509,7 +509,6 @@ } }, { - "action": ">", "background_color": "#68B7FF", "color": [ 1, @@ -519,9 +518,45 @@ ], "icon": "󰐥", "is_short": true, - "label": "Turn off", + "label": "", "opacity": null, - "progress": null + "progress": null, + "sub_menu": { + "items": [ + { + "action": ">", + "background_color": "#68B7FF", + "color": [ + 1, + 1, + 1, + 1 + ], + "icon": "󰜉", + "is_short": false, + "label": "Reboot", + "opacity": null, + "progress": null + }, + { + "action": ">", + "background_color": "#68B7FF", + "color": [ + 1, + 1, + 1, + 1 + ], + "icon": "󰤂", + "is_short": false, + "label": "Power off", + "opacity": null, + "progress": null + } + ], + "placeholder": null, + "title": "󰐥Power" + } } ], "placeholder": null, diff --git a/ubo_app/display.py b/ubo_app/display.py index b382bd18..5df567f7 100644 --- a/ubo_app/display.py +++ b/ubo_app/display.py @@ -14,6 +14,7 @@ from numpy._typing import NDArray +from ubo_app.constants import BYTES_PER_PIXEL from ubo_app.utils import IS_RPI from ubo_app.utils.fake import Fake @@ -101,12 +102,14 @@ def __init__(self: _State, splash_screen: bytes | None = None) -> None: self.block( (0, 0, WIDTH - 1, HEIGHT - 1), - bytes(WIDTH * HEIGHT * 2) if splash_screen is None else splash_screen, + bytes(WIDTH * HEIGHT * BYTES_PER_PIXEL) + if splash_screen is None + else splash_screen, ) atexit.register( lambda: self.block( (0, 0, WIDTH - 1, HEIGHT - 1), - bytes(WIDTH * HEIGHT * 2), + np.zeros((WIDTH, HEIGHT, BYTES_PER_PIXEL), dtype=np.uint8).tobytes(), ), ) diff --git a/ubo_app/menu_app/menu_central.py b/ubo_app/menu_app/menu_central.py index 142f6fcd..12d56791 100644 --- a/ubo_app/menu_app/menu_central.py +++ b/ubo_app/menu_app/menu_central.py @@ -69,7 +69,7 @@ def __init__(self: MenuAppCentral, **kwargs: object) -> None: @autorun(lambda state: state.main.menu) @debounce(0.1, DebounceOptions(leading=True, trailing=True, time_window=0.1)) - async def _(menu: Menu | None) -> None: + def _(menu: Menu | None) -> None: self = _self() if not self or not menu: return diff --git a/ubo_app/services/040-camera/setup.py b/ubo_app/services/040-camera/setup.py index 80144d80..3be6292b 100644 --- a/ubo_app/services/040-camera/setup.py +++ b/ubo_app/services/040-camera/setup.py @@ -13,6 +13,7 @@ from debouncer import DebounceOptions, debounce from kivy.clock import Clock, mainthread from pyzbar.pyzbar import decode +from typing_extensions import override from ubo_gui.page import PageWidget from ubo_app import display @@ -59,11 +60,12 @@ def resize_image( wait=THROTTL_TIME, options=DebounceOptions(leading=True, trailing=False, time_window=THROTTL_TIME), ) -async def check_codes(codes: list[str]) -> None: +def check_codes(codes: list[str]) -> None: dispatch(CameraReportBarcodeAction(codes=codes)) class CameraApplication(PageWidget): + @override def go_back(self: CameraApplication) -> bool: return True diff --git a/ubo_app/side_effects.py b/ubo_app/side_effects.py index 5ceffd9b..c0edba89 100644 --- a/ubo_app/side_effects.py +++ b/ubo_app/side_effects.py @@ -8,10 +8,10 @@ from pathlib import Path from typing import TYPE_CHECKING -from debouncer import DebounceOptions, debounce +from kivy.clock import Clock from redux import FinishAction -from ubo_app.store.core import PowerOffEvent +from ubo_app.store.core import PowerOffEvent, RebootEvent from ubo_app.store.main import ( ScreenshotEvent, SnapshotEvent, @@ -28,7 +28,6 @@ UpdateStatus, ) from ubo_app.store.update_manager.utils import check_version, update -from ubo_app.utils.async_ import create_task from ubo_app.utils.hardware import ( IS_RPI, initialize_board, @@ -44,12 +43,30 @@ def power_off() -> None: """Power off the device.""" dispatch(SoundPlayChimeAction(name=Chime.FAILURE), FinishAction()) if IS_RPI: - atexit.register( - lambda: subprocess.run( # noqa: S603 + + def power_off_system(*_: list[object]) -> None: + subprocess.run( # noqa: S603 ['/usr/bin/env', 'systemctl', 'poweroff', '-i'], check=True, - ), - ) + ) + + Clock.schedule_once(power_off_system, 5) + atexit.register(power_off_system) + + +def reboot() -> None: + """Reboot the device.""" + dispatch(SoundPlayChimeAction(name=Chime.FAILURE), FinishAction()) + if IS_RPI: + + def reboot_system(*_: list[object]) -> None: + subprocess.run( # noqa: S603 + ['/usr/bin/env', 'systemctl', 'reboot', '-i'], + check=True, + ) + + Clock.schedule_once(reboot_system, 5) + atexit.register(reboot_system) def write_image(image_path: Path, array: NDArray) -> None: @@ -91,18 +108,12 @@ def setup_side_effects() -> None: initialize_board() subscribe_event(PowerOffEvent, power_off) + subscribe_event(RebootEvent, reboot) subscribe_event(UpdateManagerUpdateEvent, update) subscribe_event(UpdateManagerCheckEvent, check_version) subscribe_event(ScreenshotEvent, take_screenshot) subscribe_event(SnapshotEvent, take_snapshot) - @debounce( - wait=10, - options=DebounceOptions(leading=True, trailing=False, time_window=10), - ) - async def request_check_version() -> None: - dispatch(UpdateManagerSetStatusAction(status=UpdateStatus.CHECKING)) - - create_task(request_check_version()) + dispatch(UpdateManagerSetStatusAction(status=UpdateStatus.CHECKING)) atexit.register(turn_off_screen) diff --git a/ubo_app/store/core/__init__.py b/ubo_app/store/core/__init__.py index fa240cba..1b932bab 100644 --- a/ubo_app/store/core/__init__.py +++ b/ubo_app/store/core/__init__.py @@ -53,7 +53,13 @@ class RegisterSettingAppAction(RegisterAppAction): priority: int | None = None -class PowerOffAction(MainAction): ... +class PowerAction(MainAction): ... + + +class PowerOffAction(PowerAction): ... + + +class RebootAction(PowerAction): ... class SetMenuPathAction(MainAction): @@ -91,7 +97,13 @@ class CloseApplicationEvent(MainEvent): application: PageWidget -class PowerOffEvent(MainEvent): ... +class PowerEvent(MainEvent): ... + + +class PowerOffEvent(PowerEvent): ... + + +class RebootEvent(PowerEvent): ... class MainState(Immutable): diff --git a/ubo_app/store/core/_menus.py b/ubo_app/store/core/_menus.py index adc0f717..6c6f927f 100644 --- a/ubo_app/store/core/_menus.py +++ b/ubo_app/store/core/_menus.py @@ -13,7 +13,12 @@ SubMenuItem, ) -from ubo_app.store.core import SETTINGS_ICONS, PowerOffAction, SettingsCategory +from ubo_app.store.core import ( + SETTINGS_ICONS, + PowerOffAction, + RebootAction, + SettingsCategory, +) from ubo_app.store.main import autorun, dispatch from ubo_app.store.services.notifications import ( Notification, @@ -145,9 +150,23 @@ def notifications_color(unread_count: int) -> str: icon='', is_short=True, ), - ActionItem( - label='Turn off', - action=lambda: dispatch(PowerOffAction()), + SubMenuItem( + label='', + sub_menu=HeadlessMenu( + title='󰐥Power', + items=[ + ActionItem( + label='Reboot', + action=lambda: dispatch(RebootAction()), + icon='󰜉', + ), + ActionItem( + label='Power off', + action=lambda: dispatch(PowerOffAction()), + icon='󰤂', + ), + ], + ), icon='󰐥', is_short=True, ), diff --git a/ubo_app/store/core/reducer.py b/ubo_app/store/core/reducer.py index 795e1af0..72851d78 100644 --- a/ubo_app/store/core/reducer.py +++ b/ubo_app/store/core/reducer.py @@ -16,8 +16,11 @@ InitEvent, MainAction, MainState, + PowerEvent, PowerOffAction, PowerOffEvent, + RebootAction, + RebootEvent, RegisterRegularAppAction, RegisterSettingAppAction, SetMenuPathAction, @@ -39,7 +42,7 @@ def reducer( ) -> ReducerResult[ MainState, SoundChangeVolumeAction, - KeypadEvent | InitEvent | PowerOffEvent, + KeypadEvent | InitEvent | PowerEvent, ]: from ubo_gui.menu.types import Item, Menu, SubMenuItem, menu_items @@ -228,4 +231,10 @@ def sort_key(item: Item) -> tuple[int, str]: events=[PowerOffEvent()], ) + if isinstance(action, RebootAction): + return CompleteReducerResult( + state=state, + events=[RebootEvent()], + ) + return state diff --git a/ubo_app/store/update_manager/reducer.py b/ubo_app/store/update_manager/reducer.py index 74571480..13d8012f 100644 --- a/ubo_app/store/update_manager/reducer.py +++ b/ubo_app/store/update_manager/reducer.py @@ -75,8 +75,8 @@ def reducer( notification=Notification( id=UPDATE_MANAGER_NOTIFICATION_ID, title='Update available!', - content=f"""Ubo v{action.latest_version - } is available. Go to the About menu to update.""", + content=f"""Ubo v{action.latest_version} is available. Go to + the About menu to update.""", display_type=NotificationDisplayType.FLASH if action.flash_notification else NotificationDisplayType.BACKGROUND, diff --git a/ubo_app/utils/hardware.py b/ubo_app/utils/hardware.py index c90087ed..9950b05c 100644 --- a/ubo_app/utils/hardware.py +++ b/ubo_app/utils/hardware.py @@ -1,7 +1,5 @@ # pyright: reportMissingModuleSource=false # ruff: noqa: D100, D101, D102, D103, D104, D107 -import numpy as np - from ubo_app.utils import IS_RPI @@ -20,15 +18,9 @@ def turn_off_screen() -> None: return from RPi import GPIO - from ubo_app.constants import BYTES_PER_PIXEL, HEIGHT, WIDTH - from ubo_app.display import state - GPIO.setup(26, GPIO.OUT) GPIO.output(26, GPIO.LOW) - data = np.zeros((WIDTH, HEIGHT, BYTES_PER_PIXEL), dtype=np.uint8) - state.block((0, 0, WIDTH - 1, HEIGHT - 1), data.tobytes()) - def turn_on_screen() -> None: if not IS_RPI: