diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index a2a80832..1b7ff9d8 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -131,6 +131,7 @@ jobs: id: extract_version run: | echo "ubo_app_version=$(poetry run python scripts/print_version.py)" >> "$GITHUB_OUTPUT" + echo "ubo_app_version=$(poetry run python scripts/print_version.py)" - name: Upload wheel uses: actions/upload-artifact@v4 @@ -209,6 +210,9 @@ jobs: name: binary path: /build/dist + - run: | + ls -l /build/dist + - name: Generate Image URL and Checksum id: generate_image_url run: | diff --git a/.gitignore b/.gitignore index 38f98321..6dc4c81a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -### Python ### +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -21,9 +22,11 @@ parts/ sdist/ var/ wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -38,77 +41,46 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover +*.py,cover .hypothesis/ +.pytest_cache/ +cover/ # Translations *.mo *.pot -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - # pyenv .python-version -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - # Environments .env .venv env/ venv/ ENV/ +env.bak/ +venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject +# pyright +/typings/ -# mkdocs documentation -/site - -# mypy -.mypy_cache/ +# logs +*.log # headless-kivy-pi headless_kivy_pi_buffer.raw -# stubs -stubs/ - # packer -*/packer/output-arm-image/ */packer/packer_cache/ +*/packer/output-* # local development scripts/toggle_local_dependencies.py -scripts/packer/output-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c2f41f..329228ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.10.6 + +- fix: wireless module now has sufficient privileges + ## Version 0.10.5 - chore: setup git-lfs for audio files diff --git a/poetry.lock b/poetry.lock index 66fc3827..35bc9fb7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -141,13 +141,13 @@ adafruit-circuitpython-busdevice = "*" [[package]] name = "adafruit-circuitpython-typing" -version = "1.10.0" +version = "1.10.2" description = "Types needed for type annotation that are not in `typing`" optional = false python-versions = "*" files = [ - {file = "adafruit-circuitpython-typing-1.10.0.tar.gz", hash = "sha256:6246893becc914e5cced3fcc204a70928c957934f28546a1d786713bbbbd42cb"}, - {file = "adafruit_circuitpython_typing-1.10.0-py3-none-any.whl", hash = "sha256:d110dcfe6a58cdda12cfcc9987a8c244af642d87dec4b7fa305cc157734f0cae"}, + {file = "adafruit-circuitpython-typing-1.10.2.tar.gz", hash = "sha256:ecfbaae7ac0f41b202aa3ed98cbd0f696b17e83b7e7f3fac6dc6a228cbc1c6c6"}, + {file = "adafruit_circuitpython_typing-1.10.2-py3-none-any.whl", hash = "sha256:e64bbf2f7d4a4bdf0f9be8e039cfe2ed199aa7d51aa44a090d50be4f0917d24f"}, ] [package.dependencies] @@ -1174,13 +1174,13 @@ files = [ [[package]] name = "python-redux" -version = "0.9.23" +version = "0.9.24" description = "Redux implementation for Python" optional = false python-versions = ">=3.9,<4.0" files = [ - {file = "python_redux-0.9.23-py3-none-any.whl", hash = "sha256:19d54def7ecce881b937fb03fcdab12bf8acf7941dad469e8c724cb3c1f5500c"}, - {file = "python_redux-0.9.23.tar.gz", hash = "sha256:9c9eaf466f9c30f9276d98d7d5b0f52b18d41ba2a0886201034622733a51e4b3"}, + {file = "python_redux-0.9.24-py3-none-any.whl", hash = "sha256:b5ebc975e5e3921bdc84c2b6998fc17c9fa17f9b879b1e07bf3db5395ba1c633"}, + {file = "python_redux-0.9.24.tar.gz", hash = "sha256:7eeb64a6d2b53e33203ce61ec5859c3c44f6bf3cf13464c432385e8f058095ef"}, ] [package.dependencies] @@ -1347,18 +1347,18 @@ files = [ [[package]] name = "setuptools" -version = "69.0.3" +version = "69.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, + {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, + {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1396,13 +1396,13 @@ files = [ [[package]] name = "ubo-gui" -version = "0.9.2" +version = "0.9.5a0" description = "GUI sdk for Ubo Pod" optional = true python-versions = ">=3.11,<4.0" files = [ - {file = "ubo_gui-0.9.2-py3-none-any.whl", hash = "sha256:bef2999b4f4a54c15a6109961bec02058ea7d201b4d00580ca6e9a6c5a5850bb"}, - {file = "ubo_gui-0.9.2.tar.gz", hash = "sha256:e179a8fea549e975556d1b2066e14fdbf7e33f61d6f857476cbd7beea3d7dbc2"}, + {file = "ubo_gui-0.9.5a0-py3-none-any.whl", hash = "sha256:794751c8ceaa74150074855d467933578748f821a47ea1c07675a92f9e8b5086"}, + {file = "ubo_gui-0.9.5a0.tar.gz", hash = "sha256:75aaf3e517a486e221a1905c3d7415cc38d2a3a48c8fee083ef9b5b04178ab08"}, ] [package.dependencies] @@ -1543,4 +1543,4 @@ dev = ["ubo-gui", "ubo-gui"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "8493f5afa39ef46eb3415d30c4ed573e857eed927e4e33a2f98b193ac67743b4" +content-hash = "9f9bcb709e9068455112fcd7debe8ccefca812136ddc0e6d55dadf5784302372" diff --git a/pyproject.toml b/pyproject.toml index b914ba42..4d477a76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ubo-app" -version = "0.10.5" +version = "0.10.6" description = "Ubo main app, running on device initialization. A platform for running other apps." authors = ["Sassan Haradji "] license = "Apache-2.0" @@ -18,14 +18,14 @@ priority = "primary" python = "^3.11" psutil = "^5.9.8" ubo-gui = [ - { version = "^0.9.2", markers = "extra=='default'", extras = [ + { version = "^0.9.5a0", markers = "extra=='default'", extras = [ 'default', ] }, - { version = "^0.9.2", markers = "extra=='dev'", extras = [ + { version = "^0.9.5a0", markers = "extra=='dev'", extras = [ 'dev', ] }, ] -python-redux = "^0.9.23" +python-redux = "^0.9.24" pyzbar = "^0.1.9" sdbus-networkmanager = { version = "^2.0.0", markers = "platform_machine=='aarch64'" } rpi_ws281x = { version = "^5.0.0", markers = "platform_machine=='aarch64'" } @@ -75,7 +75,6 @@ lint.select = ['ALL'] lint.ignore = ['INP001', 'PLR0911', 'D203', 'D213'] lint.fixable = ['ALL'] lint.unfixable = [] -exclude = ['stubs'] [tool.ruff.lint.flake8-builtins] builtins-ignorelist = ['type', 'id'] @@ -92,5 +91,4 @@ quote-style = 'single' profile = "black" [tool.pyright] -stubPath = "ubo_app/stubs" -exclude = ['ubo_app/stubs'] +exclude = ['typings'] diff --git a/scripts/packer/image.pkr.hcl b/scripts/packer/image.pkr.hcl index d298b245..1142590d 100644 --- a/scripts/packer/image.pkr.hcl +++ b/scripts/packer/image.pkr.hcl @@ -40,10 +40,7 @@ build { "chmod +x /install.sh", "/install.sh --for-packer --with-docker --source=/ubo_app-${var.ubo_app_version}-py3-none-any.whl", "rm /install.sh /ubo_app-${var.ubo_app_version}-py3-none-any.whl", - "apt clean", - "df -h", - "echo 'DU GITHUB'", - "du -hs /github/*" + "apt clean" ] } } diff --git a/ubo_app/__init__.py b/ubo_app/__init__.py index 20f335a3..b429e42f 100644 --- a/ubo_app/__init__.py +++ b/ubo_app/__init__.py @@ -41,13 +41,13 @@ def main() -> None: sys.exit(0) def global_exception_handler( - _: type, - value: Exception, + exception_type: type, + value: int, tb: TracebackType, ) -> None: from ubo_app.logging import logger - logger.error(f'Uncaught exception: {value}') + logger.error(f'Uncaught exception: {exception_type}: {value}') logger.error(''.join(traceback.format_tb(tb))) # Set the global exception handler diff --git a/ubo_app/services/000-sound/audio_manager.py b/ubo_app/services/000-sound/audio_manager.py index 4a16fced..d3c5b06a 100644 --- a/ubo_app/services/000-sound/audio_manager.py +++ b/ubo_app/services/000-sound/audio_manager.py @@ -71,7 +71,7 @@ def find_respeaker_index(self: AudioManager) -> int: """Find the index of the ReSpeaker device.""" for index in range(self.pyaudio.get_device_count()): info = self.pyaudio.get_device_info_by_index(index) - if isinstance(info['name'], str) and 'wm8960' in info['name']: + if not isinstance(info['name'], (int, float)) and 'wm8960' in info['name']: logger.debug(f'ReSpeaker found at index: {index}') logger.debug(f'Device Info: {info}') return index diff --git a/ubo_app/services/010-wifi/pages/create_wireless_connection.py b/ubo_app/services/010-wifi/pages/create_wireless_connection.py index 6580f14d..a737b211 100644 --- a/ubo_app/services/010-wifi/pages/create_wireless_connection.py +++ b/ubo_app/services/010-wifi/pages/create_wireless_connection.py @@ -4,6 +4,7 @@ import pathlib from typing import Sequence +from kivy.clock import Clock from kivy.lang.builder import Builder from kivy.properties import BooleanProperty from ubo_gui.constants import SUCCESS_COLOR @@ -49,6 +50,7 @@ def __init__( self.create_wireless_connection, ) dispatch(SoundPlayChimeAction(name='scan')) + self.bind(on_close=lambda *_: self.unsubscribe) def create_wireless_connection( self: CreateWirelessConnectionPage, @@ -93,15 +95,11 @@ async def act() -> None: ), ), ) - self.dispatch('on_close') + Clock.schedule_once(lambda _: self.dispatch('on_close'), 0) self.creating = True create_task(act()) - def on_close(self: CreateWirelessConnectionPage) -> None: - self.unsubscribe() - return super().on_close() - def get_item(self: CreateWirelessConnectionPage, index: int) -> ActionItem | None: if index == 2: # noqa: PLR2004 return ActionItem( diff --git a/ubo_app/services/010-wifi/wifi_manager.py b/ubo_app/services/010-wifi/wifi_manager.py index e837370c..17faf8c8 100644 --- a/ubo_app/services/010-wifi/wifi_manager.py +++ b/ubo_app/services/010-wifi/wifi_manager.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import contextlib import uuid from typing import TYPE_CHECKING, Any, Coroutine, TypeVar, cast @@ -28,6 +29,7 @@ if TYPE_CHECKING: from asyncio.tasks import _FutureLike +RETRIES = 3 T = TypeVar('T') @@ -376,41 +378,48 @@ async def forget_wireless_connection(ssid: str) -> None: async def get_connections() -> list[WiFiConnection]: - active_connection = await get_active_connection() - active_connection_ssid = await get_active_connection_ssid() - saved_ssids = await get_saved_ssids() - access_point_ssids = { - ( - await wait_for( - i.ssid, + # It is need as this action is not atomic and the active_connection may not be + # available when active_connection.state is queried + for i in range(RETRIES): + with contextlib.suppress(Exception): + active_connection = await get_active_connection() + active_connection_ssid = await get_active_connection_ssid() + saved_ssids = await get_saved_ssids() + access_point_ssids = { + ( + await wait_for( + i.ssid, + ) + ).decode('utf-8'): i + for i in await get_access_points() + } + + active_connection_state = cast( + SdBusConnectionState, + await active_connection.state if active_connection else None, ) - ).decode('utf-8'): i - for i in await get_access_points() - } - active_connection_state = cast( - SdBusConnectionState, - await active_connection.state if active_connection else None, - ) - state_map = { - SdBusConnectionState.ACTIVATED: ConnectionState.CONNECTED, - SdBusConnectionState.ACTIVATING: ConnectionState.CONNECTING, - SdBusConnectionState.DEACTIVATED: ConnectionState.DISCONNECTED, - SdBusConnectionState.DEACTIVATING: ConnectionState.DISCONNECTED, - SdBusConnectionState.UNKNOWN: ConnectionState.UNKNOWN, - } - - return [ - WiFiConnection( - ssid=ssid, - signal_strength=await wait_for( - access_point_ssids[ssid].strength, - ) - if ssid in access_point_ssids - else 0, - state=state_map[active_connection_state] - if active_connection_ssid == ssid - else ConnectionState.DISCONNECTED, - ) - for ssid in saved_ssids - ] + state_map = { + SdBusConnectionState.ACTIVATED: ConnectionState.CONNECTED, + SdBusConnectionState.ACTIVATING: ConnectionState.CONNECTING, + SdBusConnectionState.DEACTIVATED: ConnectionState.DISCONNECTED, + SdBusConnectionState.DEACTIVATING: ConnectionState.DISCONNECTED, + SdBusConnectionState.UNKNOWN: ConnectionState.UNKNOWN, + } + + return [ + WiFiConnection( + ssid=ssid, + signal_strength=await wait_for( + access_point_ssids[ssid].strength, + ) + if ssid in access_point_ssids + else 0, + state=state_map[active_connection_state] + if active_connection_ssid == ssid + else ConnectionState.DISCONNECTED, + ) + for ssid in saved_ssids + ] + await asyncio.sleep(0.5) + return [] diff --git a/ubo_app/services/040-camera/setup.py b/ubo_app/services/040-camera/setup.py index db88b9c4..c447fb34 100644 --- a/ubo_app/services/040-camera/setup.py +++ b/ubo_app/services/040-camera/setup.py @@ -38,13 +38,11 @@ def resize_image( return resized[: new_size[0], : new_size[1]] -def check_image(regex: re.Pattern, barcodes: list, last_match: float) -> None: +def check_codes(regex: re.Pattern, codes: list[str], last_match: float) -> float: if time.time() - last_match < THROTTL_TIME: - return - last_match = time.time() + return last_match - for barcode in barcodes: - code = barcode.data.decode() + for code in codes: logger.info( 'Read barcode, decoded value', extra={'decoded_value': code}, @@ -59,14 +57,23 @@ def check_image(regex: re.Pattern, barcodes: list, last_match: float) -> None: 'decoded_value': code, }, ) - dispatch( - CameraBarcodeAction(code=code, match=match.groupdict()), - ) + dispatch(CameraBarcodeAction(code=code, match=match.groupdict())) + + return time.time() def init_service() -> None: if not IS_RPI: + subscribe_event( + CameraStartViewfinderEvent, + lambda event: check_codes( + re.compile(event.barcode_pattern or ''), + ['WIFI:S:SSID;T:WPA;P:password;;'], + 0, + ), + ) return + from picamera2 import Picamera2 # pyright: ignore [reportMissingImports] from pyzbar.pyzbar import decode @@ -95,7 +102,11 @@ def feed_viewfinder(_: object) -> None: barcodes = decode(data) if len(barcodes) > 0 and regex is not None: nonlocal last_match - last_match = check_image(regex, barcodes, last_match) + last_match = check_codes( + regex, + [barcode.data.decode() for barcode in barcodes], + last_match, + ) data = resize_image(data) data = np.rot90(data, 2) diff --git a/ubo_app/services/040-camera/ubo_handle.py b/ubo_app/services/040-camera/ubo_handle.py index 2d3a9fbb..928f5fad 100644 --- a/ubo_app/services/040-camera/ubo_handle.py +++ b/ubo_app/services/040-camera/ubo_handle.py @@ -3,7 +3,6 @@ from setup import init_service from ubo_app.load_services import register_service -from ubo_app.utils import IS_RPI register_service( service_id='camera', @@ -11,5 +10,4 @@ reducer=reducer, ) -if IS_RPI: - init_service() +init_service() diff --git a/ubo_app/side_effects.py b/ubo_app/side_effects.py index 594d8989..ffe4d109 100644 --- a/ubo_app/side_effects.py +++ b/ubo_app/side_effects.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Sequence from debouncer import DebounceOptions, debounce +from kivy.clock import Clock from redux import FinishAction, FinishEvent from ubo_app.store import autorun, dispatch, subscribe_event @@ -37,7 +38,7 @@ def setup(app: MenuApp) -> None: initialize_board() subscribe_event(PowerOffEvent, power_off) - subscribe_event(FinishEvent, app.stop) + subscribe_event(FinishEvent, lambda *_: Clock.schedule_once(app.stop)) subscribe_event(UpdateManagerUpdateEvent, lambda: create_task(update())) subscribe_event(UpdateManagerCheckEvent, lambda: create_task(check_version())) diff --git a/ubo_app/system/bootstrap.py b/ubo_app/system/bootstrap.py index d0becc32..b31506fb 100644 --- a/ubo_app/system/bootstrap.py +++ b/ubo_app/system/bootstrap.py @@ -165,7 +165,7 @@ def setup_polkit() -> None: with Path('/etc/polkit-1/rules.d/50-ubo.rules').open('w') as file: file.write( Path(__file__) - .parent.joinpath('polkit-reboot.rules') + .parent.joinpath('polkit.rules') .open() .read() .replace('{{INSTALLATION_PATH}}', INSTALLATION_PATH) diff --git a/ubo_app/system/install.sh b/ubo_app/system/install.sh index 4d50a0ff..769ab2fc 100755 --- a/ubo_app/system/install.sh +++ b/ubo_app/system/install.sh @@ -60,7 +60,7 @@ fi # Create the user adduser --disabled-password --gecos "" $USERNAME || true -usermod -a -G audio,video,netdev,gpio,i2c,spi,kmem,render $USERNAME +usermod -a -G audio,video,gpio,i2c,spi,kmem,render $USERNAME echo "User $USERNAME created successfully." diff --git a/ubo_app/system/polkit-reboot.rules b/ubo_app/system/polkit.rules similarity index 100% rename from ubo_app/system/polkit-reboot.rules rename to ubo_app/system/polkit.rules diff --git a/ubo_app/utils/fake.py b/ubo_app/utils/fake.py index 80fcee86..4ef47862 100644 --- a/ubo_app/utils/fake.py +++ b/ubo_app/utils/fake.py @@ -10,6 +10,7 @@ class Fake(ModuleType): def __init__(self: Fake, *args: object, **kwargs: object) -> None: logger.verbose('Initializing `Fake`', extra={'args_': args, 'kwargs': kwargs}) + self.iterated = False super().__init__('') def __init_subclass__(cls: type[Fake], **kwargs: dict[str, Any]) -> None: @@ -39,20 +40,26 @@ def __call__(self: Fake, *args: object, **kwargs: dict[str, Any]) -> Fake: return self def __await__(self: Fake) -> Generator[Fake | None, Any, Any]: - yield None - return self + yield + return Fake() def __next__(self: Fake) -> Fake: - raise StopIteration + if self.iterated: + raise StopIteration + self.iterated = True + return self def __anext__(self: Fake) -> Fake: - raise StopAsyncIteration + if self.iterated: + raise StopAsyncIteration + self.iterated = True + return self def __iter__(self: Fake) -> Iterator[Fake]: - return self + return Fake() def __aiter__(self: Fake) -> Iterator[Fake]: - return self + return Fake() def __enter__(self: Fake) -> Fake: # noqa: PYI034 return self @@ -66,3 +73,15 @@ def __mro_entries__(self: Fake, bases: tuple[type[Fake]]) -> tuple[type[Fake]]: extra={'bases': bases}, ) return (cast(type, self),) + + def __index__(self: Fake) -> int: + return 1 + + def __contains__(self: Fake, _: object) -> bool: + return True + + def __eq__(self: Fake, _: object) -> bool: + return True + + def __repr__(self: Fake) -> str: + return 'Fake'