diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index d55f1449..9024ba21 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -2,6 +2,10 @@ name: CI/CD on: push: + branches: + - main + tags: + - 'v*' pull_request: workflow_dispatch: @@ -373,17 +377,19 @@ jobs: name: Publish if: >- github.event_name == 'push' && github.ref == 'refs/heads/main' || - github.event_name == 'pull_request' && github.head_ref == 'main' + github.event_name == 'pull_request' && github.event.pull_request.base.ref + == 'main' needs: - type-check - lint - test - build - - images runs-on: ubuntu-latest environment: name: pypi - url: https://pypi.org/p/${{ needs.build.outputs.name }} + url: + https://pypi.org/project/${{ needs.build.outputs.name }}/${{ + needs.build.outputs.version }} permissions: id-token: write steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a4ac65..59dd9158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - chore: remove what has remained from poetry in the codebase - refactor(core): avoid truncating or coloring logs in log files - feat(web-ui): add web-ui service +- feat(web-ui): process input demands, dispatched on the bus ## Version 1.0.0 diff --git a/README.md b/README.md index be4bfdc9..89811966 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ This is the fastest, easiest, and recommended way to get started with Ubo App. If you want to install the image on an existing operating system, then read on. Otherwise, skip this section. ---- +______________________________________________________________________ ⚠️ **Executing scripts directly from the internet with root privileges poses a significant security risk. It's generally a good practice to ensure you understand the script's content before running it. You can check the content of this particular script [here](https://raw.githubusercontent.com/ubopod/ubo-app/main/ubo_app/system/install.sh) before running it.** ---- +______________________________________________________________________ To install ubo, run this command in a terminal shell: @@ -177,9 +177,11 @@ uv run poe test #### QR code -In development environment, the camera is probably not working as it is relying, on `picamera2`, so it may become challenging to test the flows relying on QR code input. +In development environment, the camera is probably not working, as it is relying on `picamera2`, so it may become challenging to test the flows relying on QR code input. -To address this, the `qrcode_input` method, in not-RPi environments, will try to get its input from `/tmp/qrcode_input.txt`. So, whenever you encounter a QR code input, you can write the content of the QR code in that file and the application will read it from there and continue the flow. +To address this, the camera module, in not-RPi environments, will try reading from `/tmp/qrcode_input.txt` and `/tmp/qrcode_input.png` too. So, whenever you encounter a QR code input, you can write the content of the QR code in the text file path or put the qrcode image itself in the image file path and the application will read it from there and continue the flow. + +Alternatively you may be able to provide the input in the web-ui (needs refresh at the moment) or provide it by `InputProvideAction` in grpc channel. ## 🔒 License diff --git a/pyproject.toml b/pyproject.toml index 0bc9d97c..5988c0c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "platformdirs >=4.2.0", "dill >=0.3.8", "simpleaudio >=1.0.4", - "python-redux >=0.17.1", + "python-redux >=0.17.2", "python-debouncer >=0.1.5", "python-strtobool >=1.0.0", "python-fake >=0.1.3", diff --git a/scripts/test_on_device.sh b/scripts/test_on_device.sh index 97dd1f51..6429ed07 100755 --- a/scripts/test_on_device.sh +++ b/scripts/test_on_device.sh @@ -6,7 +6,7 @@ set -o nounset # Signal handler function cleanup() { - run_on_pod "killall -9 pytest" + run_on_pod killall -9 pytest } trap cleanup ERR trap cleanup SIGINT @@ -17,16 +17,18 @@ run=${run:-"False"} results=${results:-"False"} function run_on_pod() { - echo $1 if [ $# -lt 1 ]; then - echo "Usage: run_on_pod_out_of_env " + echo "Usage: run_on_pod " return 1 fi - if [ $# -eq 1 ]; then - ssh ubo-development-pod "sudo XDG_RUNTIME_DIR=/run/user/\$(id -u ubo) -u ubo bash -c 'cd; source /etc/profile && source \$HOME/.profile && ($1)'" - return 0 - fi - return 1 + + # Use SSH to execute commands read from stdin + ssh ubo-development-pod "sudo XDG_RUNTIME_DIR=/run/user/\$(id -u ubo) -u ubo bash -s" <[^'\'']+\)'\''\"|" ~/test-runner/pyproject.toml && cd ~/test-runner && uv python pin python3.11 && uv venv --system-site-packages && true') + fi + + if [ "$run" == "True" ]; then + cmd_list+=("killall -9 pytest || true && systemctl --user stop ubo-app || true &&") + fi + + # Common commands + cmd_list+=("cd ~/test-runner &&") + cmd_list+=("uv venv --system-site-packages &&") + cmd_list+=("uv python pin python3.11 &&") + + if [ "$deps" == "True" ]; then + cmd_list+=('SETUPTOOLS_SCM_PRETEND_VERSION=$(uvx hatch version) uv sync --frozen &&') + fi + + if [ "$run" == "True" ]; then + cmd_list+=("uv run poe test --verbosity=2 --capture=no --make-screenshots -n1 $* || true &&") + fi + + # Add a final true to ensure the command exits successfully + cmd_list+=("true") + + # Combine the commands into a single string + cmd="${cmd_list[*]}" + + # Execute the command on the pod + run_on_pod $cmd fi if [ "$run" == "True" ] || [ "$results" == True ]; then diff --git a/setup_scm_schemes.py b/setup_scm_schemes.py index 923f54c6..c3f3044d 100644 --- a/setup_scm_schemes.py +++ b/setup_scm_schemes.py @@ -1,6 +1,6 @@ -from setuptools_scm.version import get_local_node_and_date +from setuptools_scm.version import get_local_node_and_date # pyright: ignore import re -from datetime import datetime +from datetime import datetime, timezone def local_scheme(version): @@ -11,4 +11,4 @@ def local_scheme(version): ) original_local_version = get_local_node_and_date(version) numeric_version = original_local_version.replace('+', '').replace('.d', '') - return datetime.utcnow().strftime('%y%m%d') + numeric_version + return datetime.now(timezone.utc).strftime('%y%m%d') + numeric_version diff --git a/tests/flows/results/test_wireless/wireless_flow/window-rpi-001.hash b/tests/flows/results/test_wireless/wireless_flow/window-rpi-001.hash index e6f622ae..2836abc5 100644 --- a/tests/flows/results/test_wireless/wireless_flow/window-rpi-001.hash +++ b/tests/flows/results/test_wireless/wireless_flow/window-rpi-001.hash @@ -1,2 +1,2 @@ // window-rpi-001 -37ce32a12a80915276ecbf3176fdf9162f152f74a9f67e34e1df640f48e3f741 +92b0e1d02d380eb831bcafb57b27c2f2afa023a52a95af44765c17754ca3de38 diff --git a/tests/flows/results/test_wireless/wireless_flow/window-rpi-002.hash b/tests/flows/results/test_wireless/wireless_flow/window-rpi-002.hash index ab41966a..ce58dfb7 100644 --- a/tests/flows/results/test_wireless/wireless_flow/window-rpi-002.hash +++ b/tests/flows/results/test_wireless/wireless_flow/window-rpi-002.hash @@ -1,2 +1,2 @@ // window-rpi-002 -37ce32a12a80915276ecbf3176fdf9162f152f74a9f67e34e1df640f48e3f741 +92b0e1d02d380eb831bcafb57b27c2f2afa023a52a95af44765c17754ca3de38 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 939c10d1..f16ecddf 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 @@ -2,6 +2,7 @@ { "_id": "e3e70682c2094cac629f6fbed82c07cd", "_type": "RootState", + "input": null, "main": { "_type": "MainState", "depth": 1, 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 bc886710..d2e7a979 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 @@ -2,6 +2,7 @@ { "_id": "e3e70682c2094cac629f6fbed82c07cd", "_type": "RootState", + "input": null, "main": { "_type": "MainState", "depth": 1, 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 78c02de4..42161ee0 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 @@ -84,6 +84,7 @@ "usernames": {} } }, + "input": null, "ip": { "_type": "IpState", "interfaces": [ @@ -1106,7 +1107,10 @@ "is_pending": false, "status": null }, - "web_ui": null, + "web_ui": { + "_type": "WebUIState", + "active_inputs": [] + }, "wifi": { "_type": "WiFiState", "connections": [ 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 8a190040..cd3e98f4 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 @@ -84,6 +84,7 @@ "usernames": {} } }, + "input": null, "ip": { "_type": "IpState", "interfaces": [ @@ -1176,7 +1177,10 @@ "is_pending": false, "status": null }, - "web_ui": null, + "web_ui": { + "_type": "WebUIState", + "active_inputs": [] + }, "wifi": { "_type": "WiFiState", "connections": [], diff --git a/ubo_app/constants.py b/ubo_app/constants.py index 2326f551..c797f903 100644 --- a/ubo_app/constants.py +++ b/ubo_app/constants.py @@ -32,7 +32,7 @@ GRPC_LISTEN_HOST = os.environ.get('UBO_GRPC_LISTEN_HOST', '127.0.0.1') GRPC_LISTEN_PORT = int(os.environ.get('UBO_GRPC_LISTEN_PORT', '50051')) -WEB_UI_LISTEN_HOST = os.environ.get('UBO_WEB_UI_LISTEN_HOST', '127.0.0.1') +WEB_UI_LISTEN_HOST = os.environ.get('UBO_WEB_UI_LISTEN_HOST', '0.0.0.0') # noqa: S104 WEB_UI_LISTEN_PORT = int(os.environ.get('UBO_WEB_UI_LISTEN_PORT', '21215')) WEB_UI_DEBUG_MODE = str_to_bool(os.environ.get('UBO_WEB_UI_DEBUG_MODE', 'False')) == 1 diff --git a/ubo_app/load_services.py b/ubo_app/load_services.py index cd4841aa..0d9c5dbb 100644 --- a/ubo_app/load_services.py +++ b/ubo_app/load_services.py @@ -180,7 +180,7 @@ def __init__( def register_reducer(self: UboServiceThread, reducer: ReducerType) -> None: from ubo_app.store.main import root_reducer_id, store - logger.info( + logger.debug( 'Registering ubo service reducer', extra={ 'service_id': self.service_id, @@ -205,7 +205,7 @@ def register( setup: SetupFunction, ) -> None: if service_id in DISABLED_SERVICES: - logger.info( + logger.debug( 'Skipping disabled ubo service', extra={ 'service_id': service_id, @@ -216,7 +216,7 @@ def register( return if WHITE_LIST and service_id not in WHITE_LIST: - logger.info( + logger.debug( 'Service is not in services white list', extra={ 'service_id': service_id, @@ -230,7 +230,7 @@ def register( self.service_id = service_id self.setup = setup - logger.info( + logger.debug( 'Ubo service registered!', extra={ 'service_id': self.service_id, @@ -306,6 +306,15 @@ def run(self: UboServiceThread) -> None: self.loop.run_forever() + logger.info( + 'Ubo service thread stopped', + extra={ + 'thread_native_id': self.native_id, + 'service_label': self.label, + 'service_id': self.service_id, + }, + ) + def __repr__(self: UboServiceThread) -> str: return ( f' None: ) await asyncio.sleep(0.1) - logger.info('Stopping event loop', extra={'thread_': self}) self.loop.stop() def stop(self: UboServiceThread) -> None: diff --git a/ubo_app/rpc/generate_proto.py b/ubo_app/rpc/generate_proto.py index ea195f4c..6da45382 100644 --- a/ubo_app/rpc/generate_proto.py +++ b/ubo_app/rpc/generate_proto.py @@ -77,9 +77,9 @@ def get_proto( current_package: str | None, ) -> str: _ = name, current_package - if self.type == 'UboAction': # Assuming it is kivy color + if self.type == 'UboAction': return 'Action' - if self.type == 'UboEvent': # Assuming it is kivy color + if self.type == 'UboEvent': return 'Event' if self.type == 'Color': # Assuming it is kivy color return 'string' @@ -122,7 +122,7 @@ def package(self: Self) -> str | None: return global_enums[self.type] if self.type in global_types: return global_types[self.type] - if self.type == 'Color': + if self.type in ('Color', 'UboAction', 'UboEvent'): return None msg = f'Unknown type "{self.type}"' raise TypeError(msg) @@ -250,7 +250,7 @@ def get_embedded_definitions( if len(self.types) > 0: definitions += f' oneof {betterproto.casing.snake_case(name)} {{\n' index = 1 - for item in self.types: + for item in sorted(self.types, key=lambda x: x.local_name): try: definitions += f""" { item.get_embedded_proto(f"{name}_{index}")} { @@ -599,7 +599,7 @@ def _generate_operations_proto(output_directory: Path) -> None: if actions: proto += 'message Action {\n' proto += ' oneof action {\n' - for i, (action, _) in enumerate(actions, 1): + for i, (action, _) in enumerate(sorted(actions), 1): # proto += f""" {package_name}.v1.{action} { proto += f""" {action} { betterproto.casing.snake_case(action)} = {i};\n""" @@ -608,7 +608,7 @@ def _generate_operations_proto(output_directory: Path) -> None: if events: proto += 'message Event {\n' proto += ' oneof event {\n' - for i, (event, _) in enumerate(events, 1): + for i, (event, _) in enumerate(sorted(events), 1): # proto += f""" {package_name}.v1.{event} { proto += f""" {event} { betterproto.casing.snake_case(event)} = {i};\n""" diff --git a/ubo_app/rpc/generated/ubo/v1/__init__.py b/ubo_app/rpc/generated/ubo/v1/__init__.py index a87e42a9..f3e0d1a0 100644 --- a/ubo_app/rpc/generated/ubo/v1/__init__.py +++ b/ubo_app/rpc/generated/ubo/v1/__init__.py @@ -237,45 +237,88 @@ class SnapshotEvent(betterproto.Message): @dataclass(eq=False, repr=False) -class UboAction(betterproto.Message): - audio_action: 'AudioAction' = betterproto.message_field(1, group='ubo_action') - camera_action: 'CameraAction' = betterproto.message_field(2, group='ubo_action') - display_action: 'DisplayAction' = betterproto.message_field(3, group='ubo_action') - docker_action: 'DockerAction' = betterproto.message_field(4, group='ubo_action') - ip_action: 'IpAction' = betterproto.message_field(5, group='ubo_action') - keypad_action: 'KeypadAction' = betterproto.message_field(6, group='ubo_action') - light_dm_action: 'LightDmAction' = betterproto.message_field(7, group='ubo_action') - notifications_action: 'NotificationsAction' = betterproto.message_field( - 8, group='ubo_action', +class InputDescription(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) ) - rgb_ring_action: 'RgbRingAction' = betterproto.message_field(9, group='ubo_action') - r_pi_connect_action: 'RPiConnectAction' = betterproto.message_field( - 10, group='ubo_action', + title: str = betterproto.string_field(2) + prompt: str = betterproto.string_field(3) + extra_information: 'NotificationExtraInformation | None' = ( + betterproto.message_field(4, optional=True) ) - sensors_action: 'SensorsAction' = betterproto.message_field(11, group='ubo_action') - ssh_action: 'SshAction' = betterproto.message_field(12, group='ubo_action') - users_action: 'UsersAction' = betterproto.message_field(13, group='ubo_action') - voice_action: 'VoiceAction' = betterproto.message_field(14, group='ubo_action') - vs_code_action: 'VsCodeAction' = betterproto.message_field(15, group='ubo_action') - wi_fi_action: 'WiFiAction' = betterproto.message_field(16, group='ubo_action') + id: str = betterproto.string_field(5) + pattern: str = betterproto.string_field(6) @dataclass(eq=False, repr=False) -class UboEvent(betterproto.Message): - screenshot_event: 'ScreenshotEvent' = betterproto.message_field( - 1, group='ubo_event', +class InputAction(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) ) - audio_event: 'AudioEvent' = betterproto.message_field(2, group='ubo_event') - camera_event: 'CameraEvent' = betterproto.message_field(3, group='ubo_event') - display_event: 'DisplayEvent' = betterproto.message_field(4, group='ubo_event') - ip_event: 'IpEvent' = betterproto.message_field(5, group='ubo_event') - keypad_event: 'KeypadEvent' = betterproto.message_field(6, group='ubo_event') - notifications_event: 'NotificationsEvent' = betterproto.message_field( - 7, group='ubo_event', + + +@dataclass(eq=False, repr=False) +class InputDemandAction(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) + ) + description: 'InputDescription' = betterproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class InputResolveAction(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) + ) + id: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class InputCancelAction(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) + ) + id: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class InputProvideAction(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) + ) + value: str = betterproto.string_field(2) + data: 'dict[str, str]' = betterproto.map_field( + 3, betterproto.TYPE_STRING, betterproto.TYPE_STRING, + ) + id: str = betterproto.string_field(4) + + +@dataclass(eq=False, repr=False) +class InputResolveEvent(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) ) - snapshot_event: 'SnapshotEvent' = betterproto.message_field(8, group='ubo_event') - users_event: 'UsersEvent' = betterproto.message_field(9, group='ubo_event') - wi_fi_event: 'WiFiEvent' = betterproto.message_field(10, group='ubo_event') + id: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class InputCancelEvent(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) + ) + id: str = betterproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class InputProvideEvent(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_operations: 'str | None' = ( + betterproto.string_field(1000, optional=True) + ) + value: str = betterproto.string_field(2) + data: 'dict[str, str]' = betterproto.map_field( + 3, betterproto.TYPE_STRING, betterproto.TYPE_STRING, + ) + id: str = betterproto.string_field(4) @dataclass(eq=False, repr=False) @@ -417,22 +460,6 @@ class CameraAction(betterproto.Message): @dataclass(eq=False, repr=False) class CameraStartViewfinderAction(betterproto.Message): - meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera: 'str | None' = ( - betterproto.string_field(1000, optional=True) - ) - id: str = betterproto.string_field(2) - pattern: str = betterproto.string_field(3) - - -@dataclass(eq=False, repr=False) -class CameraEvent(betterproto.Message): - meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera: 'str | None' = ( - betterproto.string_field(1000, optional=True) - ) - - -@dataclass(eq=False, repr=False) -class CameraStartViewfinderEvent(betterproto.Message): meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera: 'str | None' = ( betterproto.string_field(1000, optional=True) ) @@ -440,40 +467,34 @@ class CameraStartViewfinderEvent(betterproto.Message): @dataclass(eq=False, repr=False) -class CameraStopViewfinderEvent(betterproto.Message): +class CameraReportBarcodeAction(betterproto.Message): meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera: 'str | None' = ( betterproto.string_field(1000, optional=True) ) - id: str = betterproto.string_field(2) + codes: 'list[str]' = betterproto.string_field(2) @dataclass(eq=False, repr=False) -class CameraReportBarcodeAction(betterproto.Message): +class CameraEvent(betterproto.Message): meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera: 'str | None' = ( betterproto.string_field(1000, optional=True) ) - codes: 'list[str]' = betterproto.string_field(2) @dataclass(eq=False, repr=False) -class CameraBarcodeEvent(betterproto.Message): +class CameraStartViewfinderEvent(betterproto.Message): meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera: 'str | None' = ( betterproto.string_field(1000, optional=True) ) - id: str = betterproto.string_field(2) - code: str = betterproto.string_field(3) - group_dict: 'dict[str, str]' = betterproto.map_field( - 4, betterproto.TYPE_STRING, betterproto.TYPE_STRING, - ) + pattern: str = betterproto.string_field(2) @dataclass(eq=False, repr=False) -class InputDescription(betterproto.Message): +class CameraStopViewfinderEvent(betterproto.Message): meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera: 'str | None' = ( betterproto.string_field(1000, optional=True) ) id: str = betterproto.string_field(2) - pattern: str = betterproto.string_field(3) @dataclass(eq=False, repr=False) @@ -1576,6 +1597,14 @@ class VsCodeState(betterproto.Message): status: 'VsCodeStatus | None' = betterproto.message_field(6, optional=True) +@dataclass(eq=False, repr=False) +class WebUiState(betterproto.Message): + meta_field_package_name_ubo_app_dot_store_dot_services_dot_web_ui: 'str | None' = ( + betterproto.string_field(1000, optional=True) + ) + active_inputs: 'list[InputDescription]' = betterproto.message_field(2) + + @dataclass(eq=False, repr=False) class WiFiConnection(betterproto.Message): meta_field_package_name_ubo_app_dot_store_dot_services_dot_wifi: 'str | None' = ( @@ -1650,29 +1679,29 @@ class WiFiState(betterproto.Message): @dataclass(eq=False, repr=False) class Action(betterproto.Message): audio_action: 'AudioAction' = betterproto.message_field(1, group='action') - audio_set_volume_action: 'AudioSetVolumeAction' = betterproto.message_field( + audio_change_volume_action: 'AudioChangeVolumeAction' = betterproto.message_field( 2, group='action', ) - audio_change_volume_action: 'AudioChangeVolumeAction' = betterproto.message_field( + audio_play_audio_action: 'AudioPlayAudioAction' = betterproto.message_field( 3, group='action', ) - audio_set_mute_status_action: 'AudioSetMuteStatusAction' = ( - betterproto.message_field(4, group='action') + audio_play_chime_action: 'AudioPlayChimeAction' = betterproto.message_field( + 4, group='action', ) - audio_toggle_mute_status_action: 'AudioToggleMuteStatusAction' = ( + audio_set_mute_status_action: 'AudioSetMuteStatusAction' = ( betterproto.message_field(5, group='action') ) - audio_play_chime_action: 'AudioPlayChimeAction' = betterproto.message_field( + audio_set_volume_action: 'AudioSetVolumeAction' = betterproto.message_field( 6, group='action', ) - audio_play_audio_action: 'AudioPlayAudioAction' = betterproto.message_field( - 7, group='action', + audio_toggle_mute_status_action: 'AudioToggleMuteStatusAction' = ( + betterproto.message_field(7, group='action') ) camera_action: 'CameraAction' = betterproto.message_field(8, group='action') - camera_start_viewfinder_action: 'CameraStartViewfinderAction' = ( + camera_report_barcode_action: 'CameraReportBarcodeAction' = ( betterproto.message_field(9, group='action') ) - camera_report_barcode_action: 'CameraReportBarcodeAction' = ( + camera_start_viewfinder_action: 'CameraStartViewfinderAction' = ( betterproto.message_field(10, group='action') ) display_action: 'DisplayAction' = betterproto.message_field(11, group='action') @@ -1683,229 +1712,246 @@ class Action(betterproto.Message): 13, group='action', ) docker_action: 'DockerAction' = betterproto.message_field(14, group='action') - docker_set_status_action: 'DockerSetStatusAction' = betterproto.message_field( + docker_image_action: 'DockerImageAction' = betterproto.message_field( 15, group='action', ) - docker_store_username_action: 'DockerStoreUsernameAction' = ( + docker_image_set_docker_id_action: 'DockerImageSetDockerIdAction' = ( betterproto.message_field(16, group='action') ) - docker_remove_username_action: 'DockerRemoveUsernameAction' = ( + docker_image_set_status_action: 'DockerImageSetStatusAction' = ( betterproto.message_field(17, group='action') ) - docker_image_action: 'DockerImageAction' = betterproto.message_field( - 18, group='action', + docker_remove_username_action: 'DockerRemoveUsernameAction' = ( + betterproto.message_field(18, group='action') ) - docker_image_set_status_action: 'DockerImageSetStatusAction' = ( - betterproto.message_field(19, group='action') + docker_set_status_action: 'DockerSetStatusAction' = betterproto.message_field( + 19, group='action', ) - docker_image_set_docker_id_action: 'DockerImageSetDockerIdAction' = ( + docker_store_username_action: 'DockerStoreUsernameAction' = ( betterproto.message_field(20, group='action') ) - ip_action: 'IpAction' = betterproto.message_field(21, group='action') - ip_update_interfaces_action: 'IpUpdateInterfacesAction' = betterproto.message_field( + input_action: 'InputAction' = betterproto.message_field(21, group='action') + input_cancel_action: 'InputCancelAction' = betterproto.message_field( 22, group='action', ) - ip_set_is_connected_action: 'IpSetIsConnectedAction' = betterproto.message_field( + input_demand_action: 'InputDemandAction' = betterproto.message_field( 23, group='action', ) - keypad_action: 'KeypadAction' = betterproto.message_field(24, group='action') - keypad_key_up_action: 'KeypadKeyUpAction' = betterproto.message_field( - 25, group='action', + input_provide_action: 'InputProvideAction' = betterproto.message_field( + 24, group='action', ) - keypad_key_down_action: 'KeypadKeyDownAction' = betterproto.message_field( - 26, group='action', + input_resolve_action: 'InputResolveAction' = betterproto.message_field( + 25, group='action', ) - keypad_key_press_action: 'KeypadKeyPressAction' = betterproto.message_field( + ip_action: 'IpAction' = betterproto.message_field(26, group='action') + ip_set_is_connected_action: 'IpSetIsConnectedAction' = betterproto.message_field( 27, group='action', ) - keypad_key_release_action: 'KeypadKeyReleaseAction' = betterproto.message_field( + ip_update_interfaces_action: 'IpUpdateInterfacesAction' = betterproto.message_field( 28, group='action', ) - light_dm_action: 'LightDmAction' = betterproto.message_field(29, group='action') - light_dm_update_state_action: 'LightDmUpdateStateAction' = ( - betterproto.message_field(30, group='action') + keypad_action: 'KeypadAction' = betterproto.message_field(29, group='action') + keypad_key_down_action: 'KeypadKeyDownAction' = betterproto.message_field( + 30, group='action', ) - light_dm_clear_enabled_state_action: 'LightDmClearEnabledStateAction' = ( - betterproto.message_field(31, group='action') + keypad_key_press_action: 'KeypadKeyPressAction' = betterproto.message_field( + 31, group='action', ) - notifications_action: 'NotificationsAction' = betterproto.message_field( + keypad_key_release_action: 'KeypadKeyReleaseAction' = betterproto.message_field( 32, group='action', ) - notifications_add_action: 'NotificationsAddAction' = betterproto.message_field( + keypad_key_up_action: 'KeypadKeyUpAction' = betterproto.message_field( 33, group='action', ) - notifications_clear_action: 'NotificationsClearAction' = betterproto.message_field( - 34, group='action', - ) - notifications_clear_by_id_action: 'NotificationsClearByIdAction' = ( + light_dm_action: 'LightDmAction' = betterproto.message_field(34, group='action') + light_dm_clear_enabled_state_action: 'LightDmClearEnabledStateAction' = ( betterproto.message_field(35, group='action') ) - notifications_clear_all_action: 'NotificationsClearAllAction' = ( + light_dm_update_state_action: 'LightDmUpdateStateAction' = ( betterproto.message_field(36, group='action') ) - rgb_ring_action: 'RgbRingAction' = betterproto.message_field(37, group='action') - rgb_ring_set_is_connected_action: 'RgbRingSetIsConnectedAction' = ( - betterproto.message_field(38, group='action') + notifications_action: 'NotificationsAction' = betterproto.message_field( + 37, group='action', ) - rgb_ring_set_is_busy_action: 'RgbRingSetIsBusyAction' = betterproto.message_field( + notifications_add_action: 'NotificationsAddAction' = betterproto.message_field( + 38, group='action', + ) + notifications_clear_action: 'NotificationsClearAction' = betterproto.message_field( 39, group='action', ) - rgb_ring_command_action: 'RgbRingCommandAction' = betterproto.message_field( - 40, group='action', + notifications_clear_all_action: 'NotificationsClearAllAction' = ( + betterproto.message_field(40, group='action') ) - rgb_ring_waitable_command_action: 'RgbRingWaitableCommandAction' = ( + notifications_clear_by_id_action: 'NotificationsClearByIdAction' = ( betterproto.message_field(41, group='action') ) - rgb_ring_colorful_command_action: 'RgbRingColorfulCommandAction' = ( - betterproto.message_field(42, group='action') + r_pi_connect_action: 'RPiConnectAction' = betterproto.message_field( + 42, group='action', ) - rgb_ring_set_enabled_action: 'RgbRingSetEnabledAction' = betterproto.message_field( - 43, group='action', + r_pi_connect_done_downloading_action: 'RPiConnectDoneDownloadingAction' = ( + betterproto.message_field(43, group='action') ) - rgb_ring_set_all_action: 'RgbRingSetAllAction' = betterproto.message_field( - 44, group='action', + r_pi_connect_set_pending_action: 'RPiConnectSetPendingAction' = ( + betterproto.message_field(44, group='action') ) - rgb_ring_set_brightness_action: 'RgbRingSetBrightnessAction' = ( + r_pi_connect_set_status_action: 'RPiConnectSetStatusAction' = ( betterproto.message_field(45, group='action') ) - rgb_ring_blank_action: 'RgbRingBlankAction' = betterproto.message_field( - 46, group='action', - ) - rgb_ring_rainbow_action: 'RgbRingRainbowAction' = betterproto.message_field( - 47, group='action', + r_pi_connect_start_downloading_action: 'RPiConnectStartDownloadingAction' = ( + betterproto.message_field(46, group='action') ) - rgb_ring_progress_wheel_step_action: 'RgbRingProgressWheelStepAction' = ( - betterproto.message_field(48, group='action') + r_pi_connect_update_service_state_action: 'RPiConnectUpdateServiceStateAction' = ( + betterproto.message_field(47, group='action') ) - rgb_ring_pulse_action: 'RgbRingPulseAction' = betterproto.message_field( + rgb_ring_action: 'RgbRingAction' = betterproto.message_field(48, group='action') + rgb_ring_blank_action: 'RgbRingBlankAction' = betterproto.message_field( 49, group='action', ) rgb_ring_blink_action: 'RgbRingBlinkAction' = betterproto.message_field( 50, group='action', ) - rgb_ring_spinning_wheel_action: 'RgbRingSpinningWheelAction' = ( + rgb_ring_colorful_command_action: 'RgbRingColorfulCommandAction' = ( betterproto.message_field(51, group='action') ) - rgb_ring_progress_wheel_action: 'RgbRingProgressWheelAction' = ( - betterproto.message_field(52, group='action') - ) - rgb_ring_fill_upto_action: 'RgbRingFillUptoAction' = betterproto.message_field( - 53, group='action', + rgb_ring_command_action: 'RgbRingCommandAction' = betterproto.message_field( + 52, group='action', ) rgb_ring_fill_downfrom_action: 'RgbRingFillDownfromAction' = ( - betterproto.message_field(54, group='action') + betterproto.message_field(53, group='action') ) - r_pi_connect_action: 'RPiConnectAction' = betterproto.message_field( - 55, group='action', + rgb_ring_fill_upto_action: 'RgbRingFillUptoAction' = betterproto.message_field( + 54, group='action', ) - r_pi_connect_start_downloading_action: 'RPiConnectStartDownloadingAction' = ( + rgb_ring_progress_wheel_action: 'RgbRingProgressWheelAction' = ( + betterproto.message_field(55, group='action') + ) + rgb_ring_progress_wheel_step_action: 'RgbRingProgressWheelStepAction' = ( betterproto.message_field(56, group='action') ) - r_pi_connect_done_downloading_action: 'RPiConnectDoneDownloadingAction' = ( - betterproto.message_field(57, group='action') + rgb_ring_pulse_action: 'RgbRingPulseAction' = betterproto.message_field( + 57, group='action', ) - r_pi_connect_set_pending_action: 'RPiConnectSetPendingAction' = ( - betterproto.message_field(58, group='action') + rgb_ring_rainbow_action: 'RgbRingRainbowAction' = betterproto.message_field( + 58, group='action', ) - r_pi_connect_set_status_action: 'RPiConnectSetStatusAction' = ( - betterproto.message_field(59, group='action') + rgb_ring_set_all_action: 'RgbRingSetAllAction' = betterproto.message_field( + 59, group='action', ) - r_pi_connect_update_service_state_action: 'RPiConnectUpdateServiceStateAction' = ( + rgb_ring_set_brightness_action: 'RgbRingSetBrightnessAction' = ( betterproto.message_field(60, group='action') ) - sensors_action: 'SensorsAction' = betterproto.message_field(61, group='action') - sensors_report_reading_action: 'SensorsReportReadingAction' = ( - betterproto.message_field(62, group='action') + rgb_ring_set_enabled_action: 'RgbRingSetEnabledAction' = betterproto.message_field( + 61, group='action', ) - ssh_action: 'SshAction' = betterproto.message_field(63, group='action') - ssh_update_state_action: 'SshUpdateStateAction' = betterproto.message_field( - 64, group='action', + rgb_ring_set_is_busy_action: 'RgbRingSetIsBusyAction' = betterproto.message_field( + 62, group='action', ) - ssh_clear_enabled_state_action: 'SshClearEnabledStateAction' = ( + rgb_ring_set_is_connected_action: 'RgbRingSetIsConnectedAction' = ( + betterproto.message_field(63, group='action') + ) + rgb_ring_spinning_wheel_action: 'RgbRingSpinningWheelAction' = ( + betterproto.message_field(64, group='action') + ) + rgb_ring_waitable_command_action: 'RgbRingWaitableCommandAction' = ( betterproto.message_field(65, group='action') ) - users_action: 'UsersAction' = betterproto.message_field(66, group='action') - users_set_users_action: 'UsersSetUsersAction' = betterproto.message_field( - 67, group='action', + ssh_action: 'SshAction' = betterproto.message_field(66, group='action') + ssh_clear_enabled_state_action: 'SshClearEnabledStateAction' = ( + betterproto.message_field(67, group='action') ) - users_create_user_action: 'UsersCreateUserAction' = betterproto.message_field( + ssh_update_state_action: 'SshUpdateStateAction' = betterproto.message_field( 68, group='action', ) - users_delete_user_action: 'UsersDeleteUserAction' = betterproto.message_field( - 69, group='action', - ) - users_reset_password_action: 'UsersResetPasswordAction' = betterproto.message_field( - 70, group='action', + sensors_action: 'SensorsAction' = betterproto.message_field(69, group='action') + sensors_report_reading_action: 'SensorsReportReadingAction' = ( + betterproto.message_field(70, group='action') ) - voice_action: 'VoiceAction' = betterproto.message_field(71, group='action') - voice_set_engine_action: 'VoiceSetEngineAction' = betterproto.message_field( + users_action: 'UsersAction' = betterproto.message_field(71, group='action') + users_create_user_action: 'UsersCreateUserAction' = betterproto.message_field( 72, group='action', ) - voice_read_text_action: 'VoiceReadTextAction' = betterproto.message_field( + users_delete_user_action: 'UsersDeleteUserAction' = betterproto.message_field( 73, group='action', ) - vs_code_action: 'VsCodeAction' = betterproto.message_field(74, group='action') - vs_code_start_downloading_action: 'VsCodeStartDownloadingAction' = ( - betterproto.message_field(75, group='action') + users_reset_password_action: 'UsersResetPasswordAction' = betterproto.message_field( + 74, group='action', + ) + users_set_users_action: 'UsersSetUsersAction' = betterproto.message_field( + 75, group='action', ) + vs_code_action: 'VsCodeAction' = betterproto.message_field(76, group='action') vs_code_done_downloading_action: 'VsCodeDoneDownloadingAction' = ( - betterproto.message_field(76, group='action') + betterproto.message_field(77, group='action') ) vs_code_set_pending_action: 'VsCodeSetPendingAction' = betterproto.message_field( - 77, group='action', + 78, group='action', ) vs_code_set_status_action: 'VsCodeSetStatusAction' = betterproto.message_field( - 78, group='action', + 79, group='action', ) - wi_fi_action: 'WiFiAction' = betterproto.message_field(79, group='action') - wi_fi_set_has_visited_onboarding_action: 'WiFiSetHasVisitedOnboardingAction' = ( + vs_code_start_downloading_action: 'VsCodeStartDownloadingAction' = ( betterproto.message_field(80, group='action') ) + voice_action: 'VoiceAction' = betterproto.message_field(81, group='action') + voice_read_text_action: 'VoiceReadTextAction' = betterproto.message_field( + 82, group='action', + ) + voice_set_engine_action: 'VoiceSetEngineAction' = betterproto.message_field( + 83, group='action', + ) + wi_fi_action: 'WiFiAction' = betterproto.message_field(84, group='action') + wi_fi_set_has_visited_onboarding_action: 'WiFiSetHasVisitedOnboardingAction' = ( + betterproto.message_field(85, group='action') + ) wi_fi_update_action: 'WiFiUpdateAction' = betterproto.message_field( - 81, group='action', + 86, group='action', ) wi_fi_update_request_action: 'WiFiUpdateRequestAction' = betterproto.message_field( - 82, group='action', + 87, group='action', ) @dataclass(eq=False, repr=False) class Event(betterproto.Message): - screenshot_event: 'ScreenshotEvent' = betterproto.message_field(1, group='event') - snapshot_event: 'SnapshotEvent' = betterproto.message_field(2, group='event') - audio_event: 'AudioEvent' = betterproto.message_field(3, group='event') - audio_play_chime_event: 'AudioPlayChimeEvent' = betterproto.message_field( - 4, group='event', - ) + audio_event: 'AudioEvent' = betterproto.message_field(1, group='event') audio_play_audio_event: 'AudioPlayAudioEvent' = betterproto.message_field( - 5, group='event', + 2, group='event', + ) + audio_play_chime_event: 'AudioPlayChimeEvent' = betterproto.message_field( + 3, group='event', ) audio_playback_done_event: 'AudioPlaybackDoneEvent' = betterproto.message_field( - 6, group='event', + 4, group='event', ) - camera_event: 'CameraEvent' = betterproto.message_field(7, group='event') + camera_event: 'CameraEvent' = betterproto.message_field(5, group='event') camera_start_viewfinder_event: 'CameraStartViewfinderEvent' = ( - betterproto.message_field(8, group='event') + betterproto.message_field(6, group='event') ) camera_stop_viewfinder_event: 'CameraStopViewfinderEvent' = ( - betterproto.message_field(9, group='event') + betterproto.message_field(7, group='event') ) - camera_barcode_event: 'CameraBarcodeEvent' = betterproto.message_field( - 10, group='event', + display_compressed_render_event: 'DisplayCompressedRenderEvent' = ( + betterproto.message_field(8, group='event') ) - display_event: 'DisplayEvent' = betterproto.message_field(11, group='event') + display_event: 'DisplayEvent' = betterproto.message_field(9, group='event') display_render_event: 'DisplayRenderEvent' = betterproto.message_field( + 10, group='event', + ) + docker_event: 'DockerEvent' = betterproto.message_field(11, group='event') + docker_image_event: 'DockerImageEvent' = betterproto.message_field( 12, group='event', ) - display_compressed_render_event: 'DisplayCompressedRenderEvent' = ( + docker_image_register_app_event: 'DockerImageRegisterAppEvent' = ( betterproto.message_field(13, group='event') ) - docker_event: 'DockerEvent' = betterproto.message_field(14, group='event') - docker_image_event: 'DockerImageEvent' = betterproto.message_field( + input_cancel_event: 'InputCancelEvent' = betterproto.message_field( + 14, group='event', + ) + input_provide_event: 'InputProvideEvent' = betterproto.message_field( 15, group='event', ) - docker_image_register_app_event: 'DockerImageRegisterAppEvent' = ( - betterproto.message_field(16, group='event') + input_resolve_event: 'InputResolveEvent' = betterproto.message_field( + 16, group='event', ) ip_event: 'IpEvent' = betterproto.message_field(17, group='event') keypad_event: 'KeypadEvent' = betterproto.message_field(18, group='event') @@ -1915,35 +1961,33 @@ class Event(betterproto.Message): keypad_key_release_event: 'KeypadKeyReleaseEvent' = betterproto.message_field( 20, group='event', ) - notifications_event: 'NotificationsEvent' = betterproto.message_field( - 21, group='event', - ) notifications_clear_event: 'NotificationsClearEvent' = betterproto.message_field( - 22, group='event', + 21, group='event', ) notifications_display_event: 'NotificationsDisplayEvent' = ( - betterproto.message_field(23, group='event') + betterproto.message_field(22, group='event') ) - rgb_ring_event: 'RgbRingEvent' = betterproto.message_field(24, group='event') - rgb_ring_command_event: 'RgbRingCommandEvent' = betterproto.message_field( - 25, group='event', + notifications_event: 'NotificationsEvent' = betterproto.message_field( + 23, group='event', ) - r_pi_connect_event: 'RPiConnectEvent' = betterproto.message_field(26, group='event') + r_pi_connect_event: 'RPiConnectEvent' = betterproto.message_field(24, group='event') r_pi_connect_login_event: 'RPiConnectLoginEvent' = betterproto.message_field( - 27, group='event', + 25, group='event', ) - users_event: 'UsersEvent' = betterproto.message_field(28, group='event') - users_create_user_event: 'UsersCreateUserEvent' = betterproto.message_field( - 29, group='event', + rgb_ring_command_event: 'RgbRingCommandEvent' = betterproto.message_field( + 26, group='event', ) - users_delete_user_event: 'UsersDeleteUserEvent' = betterproto.message_field( + rgb_ring_event: 'RgbRingEvent' = betterproto.message_field(27, group='event') + screenshot_event: 'ScreenshotEvent' = betterproto.message_field(28, group='event') + snapshot_event: 'SnapshotEvent' = betterproto.message_field(29, group='event') + users_create_user_event: 'UsersCreateUserEvent' = betterproto.message_field( 30, group='event', ) - users_reset_password_event: 'UsersResetPasswordEvent' = betterproto.message_field( + users_delete_user_event: 'UsersDeleteUserEvent' = betterproto.message_field( 31, group='event', ) - voice_event: 'VoiceEvent' = betterproto.message_field(32, group='event') - voice_synthesize_text_event: 'VoiceSynthesizeTextEvent' = betterproto.message_field( + users_event: 'UsersEvent' = betterproto.message_field(32, group='event') + users_reset_password_event: 'UsersResetPasswordEvent' = betterproto.message_field( 33, group='event', ) vs_code_event: 'VsCodeEvent' = betterproto.message_field(34, group='event') @@ -1953,7 +1997,11 @@ class Event(betterproto.Message): vs_code_restart_event: 'VsCodeRestartEvent' = betterproto.message_field( 36, group='event', ) - wi_fi_event: 'WiFiEvent' = betterproto.message_field(37, group='event') - wi_fi_update_request_event: 'WiFiUpdateRequestEvent' = betterproto.message_field( + voice_event: 'VoiceEvent' = betterproto.message_field(37, group='event') + voice_synthesize_text_event: 'VoiceSynthesizeTextEvent' = betterproto.message_field( 38, group='event', ) + wi_fi_event: 'WiFiEvent' = betterproto.message_field(39, group='event') + wi_fi_update_request_event: 'WiFiUpdateRequestEvent' = betterproto.message_field( + 40, group='event', + ) diff --git a/ubo_app/rpc/proto/ubo/v1/ubo.proto b/ubo_app/rpc/proto/ubo/v1/ubo.proto index abd4dfdd..4c7e34a6 100644 --- a/ubo_app/rpc/proto/ubo/v1/ubo.proto +++ b/ubo_app/rpc/proto/ubo/v1/ubo.proto @@ -105,41 +105,69 @@ message SnapshotEvent { optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; } -message UboAction { - oneof ubo_action { - AudioAction audio_action = 1; - CameraAction camera_action = 2; - DisplayAction display_action = 3; - DockerAction docker_action = 4; - IpAction ip_action = 5; - KeypadAction keypad_action = 6; - LightDMAction light_dm_action = 7; - NotificationsAction notifications_action = 8; - RgbRingAction rgb_ring_action = 9; - RPiConnectAction r_pi_connect_action = 10; - SensorsAction sensors_action = 11; - SSHAction ssh_action = 12; - UsersAction users_action = 13; - VoiceAction voice_action = 14; - VSCodeAction vs_code_action = 15; - WiFiAction wi_fi_action = 16; - } +message InputDescription { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; + string title = 2; + string prompt = 3; + optional NotificationExtraInformation extra_information = 4; + string id = 5; + string pattern = 6; } -message UboEvent { - oneof ubo_event { - ScreenshotEvent screenshot_event = 1; - AudioEvent audio_event = 2; - CameraEvent camera_event = 3; - DisplayEvent display_event = 4; - IpEvent ip_event = 5; - KeypadEvent keypad_event = 6; - NotificationsEvent notifications_event = 7; - SnapshotEvent snapshot_event = 8; - UsersEvent users_event = 9; - WiFiEvent wi_fi_event = 10; - } +message InputAction { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; +} + +message InputDemandAction { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; + InputDescription description = 2; +} + +message InputResolveAction { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; + string id = 2; +} + +message InputCancelAction { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; + string id = 2; +} + +message InputProvideAction { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; + + string value = 2; + map data = 3; + string id = 4; +} + +message InputResolveEvent { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; + string id = 2; +} + +message InputCancelEvent { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; + string id = 2; } + +message InputProvideEvent { + option (package_info.v1.package_name) = "ubo_app.store.operations"; + optional string meta_field_package_name_ubo_app_dot_store_dot_operations = 1000; + + string value = 2; + map data = 3; + string id = 4; +} + message DispatchItem { option (package_info.v1.package_name) = "ubo_app.store.dispatch_action"; optional string meta_field_package_name_ubo_app_dot_store_dot_dispatch_action = 1000; @@ -257,49 +285,32 @@ message CameraAction { } message CameraStartViewfinderAction { - option (package_info.v1.package_name) = "ubo_app.store.services.camera"; - optional string meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera = 1000; - string id = 2; - string pattern = 3; -} - -message CameraEvent { - option (package_info.v1.package_name) = "ubo_app.store.services.camera"; - optional string meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera = 1000; -} - -message CameraStartViewfinderEvent { option (package_info.v1.package_name) = "ubo_app.store.services.camera"; optional string meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera = 1000; string pattern = 2; } -message CameraStopViewfinderEvent { +message CameraReportBarcodeAction { option (package_info.v1.package_name) = "ubo_app.store.services.camera"; optional string meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera = 1000; - string id = 2; + repeated string codes = 2; } -message CameraReportBarcodeAction { +message CameraEvent { option (package_info.v1.package_name) = "ubo_app.store.services.camera"; optional string meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera = 1000; - repeated string codes = 2; } -message CameraBarcodeEvent { +message CameraStartViewfinderEvent { option (package_info.v1.package_name) = "ubo_app.store.services.camera"; optional string meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera = 1000; - - string id = 2; - string code = 3; - map group_dict = 4; + string pattern = 2; } -message InputDescription { +message CameraStopViewfinderEvent { option (package_info.v1.package_name) = "ubo_app.store.services.camera"; optional string meta_field_package_name_ubo_app_dot_store_dot_services_dot_camera = 1000; string id = 2; - string pattern = 3; } message CameraState { @@ -1217,6 +1228,12 @@ message VSCodeState { optional VSCodeStatus status = 6; } +message WebUIState { + option (package_info.v1.package_name) = "ubo_app.store.services.web_ui"; + optional string meta_field_package_name_ubo_app_dot_store_dot_services_dot_web_ui = 1000; + repeated InputDescription active_inputs = 2; +} + enum WiFiType { WI_FI_TYPE_UBO_APP_DOT_STORE_DOT_SERVICES_DOT_WIFI_UNSPECIFIED = 0; WI_FI_TYPE_WEP = 1; @@ -1300,129 +1317,136 @@ message WiFiState { message Action { oneof action { AudioAction audio_action = 1; - AudioSetVolumeAction audio_set_volume_action = 2; - AudioChangeVolumeAction audio_change_volume_action = 3; - AudioSetMuteStatusAction audio_set_mute_status_action = 4; - AudioToggleMuteStatusAction audio_toggle_mute_status_action = 5; - AudioPlayChimeAction audio_play_chime_action = 6; - AudioPlayAudioAction audio_play_audio_action = 7; + AudioChangeVolumeAction audio_change_volume_action = 2; + AudioPlayAudioAction audio_play_audio_action = 3; + AudioPlayChimeAction audio_play_chime_action = 4; + AudioSetMuteStatusAction audio_set_mute_status_action = 5; + AudioSetVolumeAction audio_set_volume_action = 6; + AudioToggleMuteStatusAction audio_toggle_mute_status_action = 7; CameraAction camera_action = 8; - CameraStartViewfinderAction camera_start_viewfinder_action = 9; - CameraReportBarcodeAction camera_report_barcode_action = 10; + CameraReportBarcodeAction camera_report_barcode_action = 9; + CameraStartViewfinderAction camera_start_viewfinder_action = 10; DisplayAction display_action = 11; DisplayPauseAction display_pause_action = 12; DisplayResumeAction display_resume_action = 13; DockerAction docker_action = 14; - DockerSetStatusAction docker_set_status_action = 15; - DockerStoreUsernameAction docker_store_username_action = 16; - DockerRemoveUsernameAction docker_remove_username_action = 17; - DockerImageAction docker_image_action = 18; - DockerImageSetStatusAction docker_image_set_status_action = 19; - DockerImageSetDockerIdAction docker_image_set_docker_id_action = 20; - IpAction ip_action = 21; - IpUpdateInterfacesAction ip_update_interfaces_action = 22; - IpSetIsConnectedAction ip_set_is_connected_action = 23; - KeypadAction keypad_action = 24; - KeypadKeyUpAction keypad_key_up_action = 25; - KeypadKeyDownAction keypad_key_down_action = 26; - KeypadKeyPressAction keypad_key_press_action = 27; - KeypadKeyReleaseAction keypad_key_release_action = 28; - LightDMAction light_dm_action = 29; - LightDMUpdateStateAction light_dm_update_state_action = 30; - LightDMClearEnabledStateAction light_dm_clear_enabled_state_action = 31; - NotificationsAction notifications_action = 32; - NotificationsAddAction notifications_add_action = 33; - NotificationsClearAction notifications_clear_action = 34; - NotificationsClearByIdAction notifications_clear_by_id_action = 35; - NotificationsClearAllAction notifications_clear_all_action = 36; - RgbRingAction rgb_ring_action = 37; - RgbRingSetIsConnectedAction rgb_ring_set_is_connected_action = 38; - RgbRingSetIsBusyAction rgb_ring_set_is_busy_action = 39; - RgbRingCommandAction rgb_ring_command_action = 40; - RgbRingWaitableCommandAction rgb_ring_waitable_command_action = 41; - RgbRingColorfulCommandAction rgb_ring_colorful_command_action = 42; - RgbRingSetEnabledAction rgb_ring_set_enabled_action = 43; - RgbRingSetAllAction rgb_ring_set_all_action = 44; - RgbRingSetBrightnessAction rgb_ring_set_brightness_action = 45; - RgbRingBlankAction rgb_ring_blank_action = 46; - RgbRingRainbowAction rgb_ring_rainbow_action = 47; - RgbRingProgressWheelStepAction rgb_ring_progress_wheel_step_action = 48; - RgbRingPulseAction rgb_ring_pulse_action = 49; + DockerImageAction docker_image_action = 15; + DockerImageSetDockerIdAction docker_image_set_docker_id_action = 16; + DockerImageSetStatusAction docker_image_set_status_action = 17; + DockerRemoveUsernameAction docker_remove_username_action = 18; + DockerSetStatusAction docker_set_status_action = 19; + DockerStoreUsernameAction docker_store_username_action = 20; + InputAction input_action = 21; + InputCancelAction input_cancel_action = 22; + InputDemandAction input_demand_action = 23; + InputProvideAction input_provide_action = 24; + InputResolveAction input_resolve_action = 25; + IpAction ip_action = 26; + IpSetIsConnectedAction ip_set_is_connected_action = 27; + IpUpdateInterfacesAction ip_update_interfaces_action = 28; + KeypadAction keypad_action = 29; + KeypadKeyDownAction keypad_key_down_action = 30; + KeypadKeyPressAction keypad_key_press_action = 31; + KeypadKeyReleaseAction keypad_key_release_action = 32; + KeypadKeyUpAction keypad_key_up_action = 33; + LightDMAction light_dm_action = 34; + LightDMClearEnabledStateAction light_dm_clear_enabled_state_action = 35; + LightDMUpdateStateAction light_dm_update_state_action = 36; + NotificationsAction notifications_action = 37; + NotificationsAddAction notifications_add_action = 38; + NotificationsClearAction notifications_clear_action = 39; + NotificationsClearAllAction notifications_clear_all_action = 40; + NotificationsClearByIdAction notifications_clear_by_id_action = 41; + RPiConnectAction r_pi_connect_action = 42; + RPiConnectDoneDownloadingAction r_pi_connect_done_downloading_action = 43; + RPiConnectSetPendingAction r_pi_connect_set_pending_action = 44; + RPiConnectSetStatusAction r_pi_connect_set_status_action = 45; + RPiConnectStartDownloadingAction r_pi_connect_start_downloading_action = 46; + RPiConnectUpdateServiceStateAction r_pi_connect_update_service_state_action = 47; + RgbRingAction rgb_ring_action = 48; + RgbRingBlankAction rgb_ring_blank_action = 49; RgbRingBlinkAction rgb_ring_blink_action = 50; - RgbRingSpinningWheelAction rgb_ring_spinning_wheel_action = 51; - RgbRingProgressWheelAction rgb_ring_progress_wheel_action = 52; - RgbRingFillUptoAction rgb_ring_fill_upto_action = 53; - RgbRingFillDownfromAction rgb_ring_fill_downfrom_action = 54; - RPiConnectAction r_pi_connect_action = 55; - RPiConnectStartDownloadingAction r_pi_connect_start_downloading_action = 56; - RPiConnectDoneDownloadingAction r_pi_connect_done_downloading_action = 57; - RPiConnectSetPendingAction r_pi_connect_set_pending_action = 58; - RPiConnectSetStatusAction r_pi_connect_set_status_action = 59; - RPiConnectUpdateServiceStateAction r_pi_connect_update_service_state_action = 60; - SensorsAction sensors_action = 61; - SensorsReportReadingAction sensors_report_reading_action = 62; - SSHAction ssh_action = 63; - SSHUpdateStateAction ssh_update_state_action = 64; - SSHClearEnabledStateAction ssh_clear_enabled_state_action = 65; - UsersAction users_action = 66; - UsersSetUsersAction users_set_users_action = 67; - UsersCreateUserAction users_create_user_action = 68; - UsersDeleteUserAction users_delete_user_action = 69; - UsersResetPasswordAction users_reset_password_action = 70; - VoiceAction voice_action = 71; - VoiceSetEngineAction voice_set_engine_action = 72; - VoiceReadTextAction voice_read_text_action = 73; - VSCodeAction vs_code_action = 74; - VSCodeStartDownloadingAction vs_code_start_downloading_action = 75; - VSCodeDoneDownloadingAction vs_code_done_downloading_action = 76; - VSCodeSetPendingAction vs_code_set_pending_action = 77; - VSCodeSetStatusAction vs_code_set_status_action = 78; - WiFiAction wi_fi_action = 79; - WiFiSetHasVisitedOnboardingAction wi_fi_set_has_visited_onboarding_action = 80; - WiFiUpdateAction wi_fi_update_action = 81; - WiFiUpdateRequestAction wi_fi_update_request_action = 82; + RgbRingColorfulCommandAction rgb_ring_colorful_command_action = 51; + RgbRingCommandAction rgb_ring_command_action = 52; + RgbRingFillDownfromAction rgb_ring_fill_downfrom_action = 53; + RgbRingFillUptoAction rgb_ring_fill_upto_action = 54; + RgbRingProgressWheelAction rgb_ring_progress_wheel_action = 55; + RgbRingProgressWheelStepAction rgb_ring_progress_wheel_step_action = 56; + RgbRingPulseAction rgb_ring_pulse_action = 57; + RgbRingRainbowAction rgb_ring_rainbow_action = 58; + RgbRingSetAllAction rgb_ring_set_all_action = 59; + RgbRingSetBrightnessAction rgb_ring_set_brightness_action = 60; + RgbRingSetEnabledAction rgb_ring_set_enabled_action = 61; + RgbRingSetIsBusyAction rgb_ring_set_is_busy_action = 62; + RgbRingSetIsConnectedAction rgb_ring_set_is_connected_action = 63; + RgbRingSpinningWheelAction rgb_ring_spinning_wheel_action = 64; + RgbRingWaitableCommandAction rgb_ring_waitable_command_action = 65; + SSHAction ssh_action = 66; + SSHClearEnabledStateAction ssh_clear_enabled_state_action = 67; + SSHUpdateStateAction ssh_update_state_action = 68; + SensorsAction sensors_action = 69; + SensorsReportReadingAction sensors_report_reading_action = 70; + UsersAction users_action = 71; + UsersCreateUserAction users_create_user_action = 72; + UsersDeleteUserAction users_delete_user_action = 73; + UsersResetPasswordAction users_reset_password_action = 74; + UsersSetUsersAction users_set_users_action = 75; + VSCodeAction vs_code_action = 76; + VSCodeDoneDownloadingAction vs_code_done_downloading_action = 77; + VSCodeSetPendingAction vs_code_set_pending_action = 78; + VSCodeSetStatusAction vs_code_set_status_action = 79; + VSCodeStartDownloadingAction vs_code_start_downloading_action = 80; + VoiceAction voice_action = 81; + VoiceReadTextAction voice_read_text_action = 82; + VoiceSetEngineAction voice_set_engine_action = 83; + WiFiAction wi_fi_action = 84; + WiFiSetHasVisitedOnboardingAction wi_fi_set_has_visited_onboarding_action = 85; + WiFiUpdateAction wi_fi_update_action = 86; + WiFiUpdateRequestAction wi_fi_update_request_action = 87; } } message Event { oneof event { - ScreenshotEvent screenshot_event = 1; - SnapshotEvent snapshot_event = 2; - AudioEvent audio_event = 3; - AudioPlayChimeEvent audio_play_chime_event = 4; - AudioPlayAudioEvent audio_play_audio_event = 5; - AudioPlaybackDoneEvent audio_playback_done_event = 6; - CameraEvent camera_event = 7; - CameraStartViewfinderEvent camera_start_viewfinder_event = 8; - CameraStopViewfinderEvent camera_stop_viewfinder_event = 9; - CameraBarcodeEvent camera_barcode_event = 10; - DisplayEvent display_event = 11; - DisplayRenderEvent display_render_event = 12; - DisplayCompressedRenderEvent display_compressed_render_event = 13; - DockerEvent docker_event = 14; - DockerImageEvent docker_image_event = 15; - DockerImageRegisterAppEvent docker_image_register_app_event = 16; + AudioEvent audio_event = 1; + AudioPlayAudioEvent audio_play_audio_event = 2; + AudioPlayChimeEvent audio_play_chime_event = 3; + AudioPlaybackDoneEvent audio_playback_done_event = 4; + CameraEvent camera_event = 5; + CameraStartViewfinderEvent camera_start_viewfinder_event = 6; + CameraStopViewfinderEvent camera_stop_viewfinder_event = 7; + DisplayCompressedRenderEvent display_compressed_render_event = 8; + DisplayEvent display_event = 9; + DisplayRenderEvent display_render_event = 10; + DockerEvent docker_event = 11; + DockerImageEvent docker_image_event = 12; + DockerImageRegisterAppEvent docker_image_register_app_event = 13; + InputCancelEvent input_cancel_event = 14; + InputProvideEvent input_provide_event = 15; + InputResolveEvent input_resolve_event = 16; IpEvent ip_event = 17; KeypadEvent keypad_event = 18; KeypadKeyPressEvent keypad_key_press_event = 19; KeypadKeyReleaseEvent keypad_key_release_event = 20; - NotificationsEvent notifications_event = 21; - NotificationsClearEvent notifications_clear_event = 22; - NotificationsDisplayEvent notifications_display_event = 23; - RgbRingEvent rgb_ring_event = 24; - RgbRingCommandEvent rgb_ring_command_event = 25; - RPiConnectEvent r_pi_connect_event = 26; - RPiConnectLoginEvent r_pi_connect_login_event = 27; - UsersEvent users_event = 28; - UsersCreateUserEvent users_create_user_event = 29; - UsersDeleteUserEvent users_delete_user_event = 30; - UsersResetPasswordEvent users_reset_password_event = 31; - VoiceEvent voice_event = 32; - VoiceSynthesizeTextEvent voice_synthesize_text_event = 33; + NotificationsClearEvent notifications_clear_event = 21; + NotificationsDisplayEvent notifications_display_event = 22; + NotificationsEvent notifications_event = 23; + RPiConnectEvent r_pi_connect_event = 24; + RPiConnectLoginEvent r_pi_connect_login_event = 25; + RgbRingCommandEvent rgb_ring_command_event = 26; + RgbRingEvent rgb_ring_event = 27; + ScreenshotEvent screenshot_event = 28; + SnapshotEvent snapshot_event = 29; + UsersCreateUserEvent users_create_user_event = 30; + UsersDeleteUserEvent users_delete_user_event = 31; + UsersEvent users_event = 32; + UsersResetPasswordEvent users_reset_password_event = 33; VSCodeEvent vs_code_event = 34; VSCodeLoginEvent vs_code_login_event = 35; VSCodeRestartEvent vs_code_restart_event = 36; - WiFiEvent wi_fi_event = 37; - WiFiUpdateRequestEvent wi_fi_update_request_event = 38; + VoiceEvent voice_event = 37; + VoiceSynthesizeTextEvent voice_synthesize_text_event = 38; + WiFiEvent wi_fi_event = 39; + WiFiUpdateRequestEvent wi_fi_update_request_event = 40; } } diff --git a/ubo_app/rpc/service.py b/ubo_app/rpc/service.py index 4bc906b4..e031bef5 100644 --- a/ubo_app/rpc/service.py +++ b/ubo_app/rpc/service.py @@ -20,8 +20,7 @@ from ubo_app.rpc.generated.ubo.v1 import Event from ubo_app.rpc.message_to_object import get_class, rebuild_object, reduce_group from ubo_app.rpc.object_to_message import build_message -from ubo_app.store.main import store -from ubo_app.store.operations import UboAction, UboEvent +from ubo_app.store.main import UboAction, UboEvent, store if TYPE_CHECKING: from collections.abc import AsyncIterator diff --git a/ubo_app/service.py b/ubo_app/service.py index 84454c01..2b9be39c 100644 --- a/ubo_app/service.py +++ b/ubo_app/service.py @@ -64,7 +64,7 @@ async def shutdown(self: WorkerThread) -> None: from ubo_app.constants import MAIN_LOOP_GRACE_PERIOD from ubo_app.logging import logger - logger.info('Shutting down worker thread') + logger.info('Stopping worker thread') while True: tasks = [ @@ -93,7 +93,7 @@ async def shutdown(self: WorkerThread) -> None: ) await asyncio.sleep(0.1) - logger.info('Stopping event loop', extra={'thread_': self}) + logger.debug('Stopping event loop', extra={'thread_': self}) self.loop.stop() self.is_finished.set() diff --git a/ubo_app/services/010-voice/setup.py b/ubo_app/services/010-voice/setup.py index ac722d32..ee69bf01 100644 --- a/ubo_app/services/010-voice/setup.py +++ b/ubo_app/services/010-voice/setup.py @@ -28,8 +28,8 @@ ) from ubo_app.utils import secrets from ubo_app.utils.async_ import create_task, to_thread +from ubo_app.utils.input import ubo_input from ubo_app.utils.persistent_store import register_persistent_store -from ubo_app.utils.qrcode import qrcode_input if TYPE_CHECKING: from collections.abc import Callable, Sequence @@ -73,10 +73,11 @@ def input_access_key() -> None: async def act() -> None: try: access_key = ( - await qrcode_input( + await ubo_input( '.*', prompt='Convert the Picovoice access key to a QR code and ' 'scan it.', + title='Picovoice Access Key', ) )[0] secrets.write_secret(key=PICOVOICE_ACCESS_KEY, value=access_key) diff --git a/ubo_app/services/030-wifi/pages/create_wireless_connection.py b/ubo_app/services/030-wifi/pages/create_wireless_connection.py index ba02b547..af6a1aea 100644 --- a/ubo_app/services/030-wifi/pages/create_wireless_connection.py +++ b/ubo_app/services/030-wifi/pages/create_wireless_connection.py @@ -24,7 +24,7 @@ ) from ubo_app.store.services.wifi import WiFiType, WiFiUpdateRequestAction from ubo_app.utils.async_ import create_task -from ubo_app.utils.qrcode import qrcode_input +from ubo_app.utils.input import ubo_input if TYPE_CHECKING: from collections.abc import Sequence @@ -55,9 +55,9 @@ def __init__( async def create_wireless_connection(self: CreateWirelessConnectionPage) -> None: try: - _, match = await qrcode_input( + _, match = await ubo_input( BARCODE_PATTERN, - prompt='Scan WiFi QR-Code With Front Camera', + prompt='Enter WiFi connection', extra_information=NotificationExtraInformation( text='Go to your phone settings, choose QR code and hold it in ' 'front of the camera to scan it.', diff --git a/ubo_app/services/040-camera/reducer.py b/ubo_app/services/040-camera/reducer.py index be5123bc..00abf089 100644 --- a/ubo_app/services/040-camera/reducer.py +++ b/ubo_app/services/040-camera/reducer.py @@ -1,6 +1,7 @@ # ruff: noqa: D100, D101, D102, D103, D104, D107 from __future__ import annotations +import datetime import re from dataclasses import replace @@ -11,20 +12,34 @@ ReducerResult, ) +from ubo_app.store.operations import ( + InputCancelAction, + InputDemandAction, + InputProvideAction, + InputResolveAction, +) from ubo_app.store.services.camera import ( CameraAction, - CameraBarcodeEvent, CameraEvent, CameraReportBarcodeAction, CameraStartViewfinderAction, CameraStartViewfinderEvent, CameraState, CameraStopViewfinderEvent, - InputDescription, ) from ubo_app.store.services.keypad import Key, KeypadKeyPressAction +from ubo_app.store.services.notifications import ( + Notification, + NotificationDispatchItem, + NotificationDisplayType, + NotificationsAddAction, + NotificationsClearByIdAction, +) -Action = InitAction | CameraAction +Action = InitAction | CameraAction | InputDemandAction | KeypadKeyPressAction +DispatchAction = ( + NotificationsAddAction | NotificationsClearByIdAction | InputResolveAction +) def pop_queue(state: CameraState) -> CameraState: @@ -41,26 +56,79 @@ def pop_queue(state: CameraState) -> CameraState: def reducer( state: CameraState | None, action: Action, -) -> ReducerResult[CameraState, Action, CameraEvent]: +) -> ReducerResult[ + CameraState, + DispatchAction, + CameraEvent, +]: if state is None: if isinstance(action, InitAction): return CameraState(is_viewfinder_active=False, queue=[]) raise InitializationActionError(action) - if isinstance(action, CameraStartViewfinderAction): + if isinstance(action, InputDemandAction): if state.is_viewfinder_active: return replace( state, queue=[ *state.queue, - InputDescription(id=action.id, pattern=action.pattern), + action.description, ], ) + return CompleteReducerResult( + state=replace( + state, + current=action.description, + ), + actions=[ + NotificationsAddAction( + notification=Notification( + id='camera:qrcode', + icon='󰄀󰐲', + title='QR Code', + content=f'[size=18dp]{action.description.prompt}[/size]', + display_type=NotificationDisplayType.STICKY, + is_read=True, + extra_information=action.description.extra_information, + expiration_timestamp=datetime.datetime.now(tz=datetime.UTC), + color='#ffffff', + actions=[ + NotificationDispatchItem( + operation=CameraStartViewfinderAction( + pattern=action.description.pattern, + ), + icon='󰄀', + dismiss_notification=True, + ), + ], + dismissable=False, + dismiss_on_close=True, + ), + ), + ], + ) + + if isinstance(action, InputResolveAction): + if state.current and state.current.id == action.id: + return CompleteReducerResult( + state=pop_queue(state), + actions=[NotificationsClearByIdAction(id='camera:qrcode')], + events=[CameraStopViewfinderEvent(id=state.current.id)], + ) + return replace( + state, + queue=[ + description + for description in state.queue + if description.id != action.id + ], + ) + + if isinstance(action, CameraStartViewfinderAction): return CompleteReducerResult( state=replace( state, is_viewfinder_active=True, - current=InputDescription(id=action.id, pattern=action.pattern), ), events=[CameraStartViewfinderEvent(pattern=action.pattern)], ) @@ -72,24 +140,28 @@ def reducer( if match: return CompleteReducerResult( state=pop_queue(state), - events=[ - CameraBarcodeEvent( + actions=[ + InputProvideAction( id=state.current.id, - code=code, - group_dict=match.groupdict(), + value=code, + data=match.groupdict(), ), + ], + events=[ CameraStopViewfinderEvent(id=None), ], ) else: return CompleteReducerResult( state=pop_queue(state), - events=[ - CameraBarcodeEvent( + actions=[ + InputProvideAction( id=state.current.id, - code=code, - group_dict=None, + value=code, + data=None, ), + ], + events=[ CameraStopViewfinderEvent(id=None), ], ) @@ -97,14 +169,29 @@ def reducer( return state if isinstance(action, KeypadKeyPressAction): # noqa: SIM102 - if action.key == Key.BACK and state.is_viewfinder_active: - return CompleteReducerResult( - state=pop_queue(state), - events=[ + if action.key in [Key.BACK, Key.HOME]: + actions: list[DispatchAction] = [] + events: list[CameraEvent] = [] + + if state.current: + actions.extend( + [ + InputCancelAction(id=state.current.id), + NotificationsClearByIdAction(id='camera:qrcode'), + ], + ) + + if state.is_viewfinder_active: + events.append( CameraStopViewfinderEvent( id=state.current.id if state.current else None, ), - ], + ) + + return CompleteReducerResult( + state=pop_queue(state), + actions=actions, + events=events, ) return state diff --git a/ubo_app/services/040-camera/setup.py b/ubo_app/services/040-camera/setup.py index ad6f4c36..23e58599 100644 --- a/ubo_app/services/040-camera/setup.py +++ b/ubo_app/services/040-camera/setup.py @@ -12,7 +12,6 @@ import png 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 @@ -115,6 +114,8 @@ def feed_viewfinder(picamera2: Picamera2 | None) -> None: data = None if data is not None: + from pyzbar.pyzbar import decode + barcodes = decode(data) if len(barcodes) > 0: create_task( @@ -192,6 +193,7 @@ def feed_viewfinder_locked(_: object) -> None: store.dispatch(DisplayPauseAction()) def handle_stop_viewfinder() -> None: + unsubscribe() with fs_lock: nonlocal is_running is_running = False @@ -200,12 +202,11 @@ def handle_stop_viewfinder() -> None: CloseApplicationEvent(application=application), DisplayResumeAction(), ) - cancel_subscription() if picamera2: picamera2.stop() picamera2.close() - cancel_subscription = store.subscribe_event( + unsubscribe = store.subscribe_event( CameraStopViewfinderEvent, handle_stop_viewfinder, ) diff --git a/ubo_app/services/050-vscode/commands.py b/ubo_app/services/050-vscode/commands.py index acd51c49..016b2cb4 100644 --- a/ubo_app/services/050-vscode/commands.py +++ b/ubo_app/services/050-vscode/commands.py @@ -102,7 +102,7 @@ async def check_status() -> None: ), ), ) - logger.info( + logger.debug( 'Checked VSCode Tunnel Status', extra={ 'status': status_data, diff --git a/ubo_app/services/080-docker/image_.py b/ubo_app/services/080-docker/image_.py index fbb397ca..9da91224 100644 --- a/ubo_app/services/080-docker/image_.py +++ b/ubo_app/services/080-docker/image_.py @@ -41,7 +41,7 @@ def find_container(client: docker.DockerClient, *, image: str) -> Container | No def update_container(image_id: str, container: Container) -> None: """Update a container's state in store based on its real state.""" if container.status == 'running': - logger.info( + logger.debug( 'Container running image found', extra={'image': image_id, 'path': IMAGES[image_id].path}, ) @@ -61,7 +61,7 @@ def update_container(image_id: str, container: Container) -> None: ), ) return - logger.info( + logger.debug( "Container for the image found, but it's not running", extra={'image': image_id, 'path': IMAGES[image_id].path}, ) @@ -140,7 +140,7 @@ def check_container(image_id: str) -> None: path = IMAGES[image_id].path def act() -> None: - logger.info('Checking image', extra={'image': image_id, 'path': path}) + logger.debug('Checking image', extra={'image': image_id, 'path': path}) docker_client = docker.from_env() try: image = docker_client.images.get(path) @@ -154,14 +154,14 @@ def act() -> None: docker_id=image.id, ), ) - logger.info('Image found', extra={'image': image_id, 'path': path}) + logger.debug('Image found', extra={'image': image_id, 'path': path}) container = find_container(docker_client, image=path) if container: update_container(image_id, container) return - logger.info( + logger.debug( 'Container running image not found', extra={'image': image_id, 'path': path}, ) @@ -172,9 +172,10 @@ def act() -> None: ), ) except docker.errors.ImageNotFound: - logger.exception( + logger.debug( 'Image not found', extra={'image': image_id, 'path': path}, + exc_info=True, ) store.dispatch( DockerImageSetStatusAction( diff --git a/ubo_app/services/080-docker/image_menus.py b/ubo_app/services/080-docker/image_menus.py index 66f13b43..7c7c0aa4 100644 --- a/ubo_app/services/080-docker/image_menus.py +++ b/ubo_app/services/080-docker/image_menus.py @@ -3,7 +3,7 @@ from __future__ import annotations from asyncio import iscoroutine -from typing import TYPE_CHECKING, Any, cast, overload +from typing import TYPE_CHECKING, Any, overload import docker import docker.errors @@ -75,7 +75,7 @@ def act() -> None: except docker.errors.DockerException: logger.exception( 'Image error', - extra={'image': IMAGES[image.id].path}, + extra={'image': image.id, 'path': IMAGES[image.id].path}, ) store.dispatch( DockerImageSetStatusAction( @@ -104,8 +104,6 @@ async def _process_str( | Callable[[], str | Coroutine[Any, Any, str]] | Coroutine[Any, Any, str], ) -> str: ... - - @overload async def _process_str( value: str @@ -113,8 +111,6 @@ async def _process_str( | Coroutine[Any, Any, str | None] | None, ) -> str | None: ... - - async def _process_str( value: str | Callable[[], str | Coroutine[Any, Any, str | None] | None] @@ -124,7 +120,7 @@ async def _process_str( if callable(value): value = value() if iscoroutine(value): - value = cast(str, await value) + value = await value return value @@ -309,17 +305,19 @@ def action() -> PageWidget: ), ) + messages = { + ImageStatus.NOT_AVAILABLE: 'Image needs to be fetched', + ImageStatus.FETCHING: 'Image is being fetched', + ImageStatus.AVAILABLE: 'Image is ready but container is not running', + ImageStatus.CREATED: 'Container is created but not running', + ImageStatus.RUNNING: IMAGES[image.id].note or 'Container is running', + ImageStatus.ERROR: 'Image has an error, please check the logs', + } + return HeadedMenu( title=f'Docker - {IMAGES[image.id].label}', heading=IMAGES[image.id].label, - sub_heading={ - ImageStatus.NOT_AVAILABLE: 'Image needs to be fetched', - ImageStatus.FETCHING: 'Image is being fetched', - ImageStatus.AVAILABLE: 'Image is ready but container is not running', - ImageStatus.CREATED: 'Container is created but not running', - ImageStatus.RUNNING: IMAGES[image.id].note or 'Container is running', - ImageStatus.ERROR: 'Image has an error, please check the logs', - }[image.status], + sub_heading=messages[image.status], items=items, placeholder='Waiting...', ) diff --git a/ubo_app/services/080-docker/images_.py b/ubo_app/services/080-docker/images_.py index a6b618f7..93de7ff1 100644 --- a/ubo_app/services/080-docker/images_.py +++ b/ubo_app/services/080-docker/images_.py @@ -15,7 +15,7 @@ from ubo_app.constants import DEBUG_MODE_DOCKER, DOCKER_PREFIX from ubo_app.store.services.notifications import NotificationExtraInformation -from ubo_app.utils.qrcode import qrcode_input +from ubo_app.utils.input import ubo_input if TYPE_CHECKING: from collections.abc import Callable, Coroutine @@ -115,7 +115,7 @@ class ImageEntry(Immutable): path=DOCKER_PREFIX + 'ngrok/ngrok:latest', registry='docker.io', environment_vairables={ - 'NGROK_AUTHTOKEN': lambda: qrcode_input( + 'NGROK_AUTHTOKEN': lambda: ubo_input( r'^[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{20,30}$', resolver=lambda code, _: code, prompt='Enter the Ngrok Auth Token', @@ -137,7 +137,7 @@ class ImageEntry(Immutable): ), ), }, - command=lambda: qrcode_input( + command=lambda: ubo_input( '', resolver=lambda code, _: code, prompt='Enter the command, for example: `http 80` or `tcp 22`', diff --git a/ubo_app/services/080-docker/setup.py b/ubo_app/services/080-docker/setup.py index 3ca7ce87..ae6bd204 100644 --- a/ubo_app/services/080-docker/setup.py +++ b/ubo_app/services/080-docker/setup.py @@ -5,7 +5,7 @@ import asyncio import contextlib import functools -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import docker import docker.errors @@ -41,9 +41,9 @@ from ubo_app.utils import secrets from ubo_app.utils.apt import is_package_installed from ubo_app.utils.async_ import create_task +from ubo_app.utils.input import ubo_input from ubo_app.utils.monitor_unit import monitor_unit from ubo_app.utils.persistent_store import register_persistent_store -from ubo_app.utils.qrcode import qrcode_input from ubo_app.utils.server import send_command if TYPE_CHECKING: @@ -209,9 +209,11 @@ def input_credentials() -> None: async def act() -> None: try: credentials = ( - await qrcode_input( - r'^[^|]*\|[^|]*\|[^|]*$|^[^|]*|[^|]*$', + await ubo_input( + r'^(?P[^|]*)\|(?P[^|]*)\|(?P[^|]*)$|' + r'(?P^[^|]*)|(?P[^|]*)$', prompt='Format: [i]SERVICE|USERNAME|PASSWORD[/i]', + title='Enter Docker Credentials', extra_information=NotificationExtraInformation( text="""To generate your QR code for login, format your \ details by separating your service, username, and password with the pipe symbol. For \ @@ -231,15 +233,15 @@ async def act() -> None: default.""", ), ) - )[0] - if credentials.count('|') == 1: - username, password = credentials.split('|') - registry = 'docker.io' - else: - registry, username, password = credentials.split('|') - registry = registry.strip() - username = username.strip() - password = password.strip() + )[1] + if not credentials: + return + username = credentials.get('Username', credentials.get('Username_', '')) + password = credentials.get('Password', credentials.get('Password_', '')) + registry = credentials.get('Service', 'docker.io') + username = cast(str, username).strip() + password = cast(str, password).strip() + registry = cast(str, registry).strip() docker_client = docker.from_env() docker_client.login( username=username, diff --git a/ubo_app/services/090-web-ui/reducer.py b/ubo_app/services/090-web-ui/reducer.py index 657b9dd8..ae369b59 100644 --- a/ubo_app/services/090-web-ui/reducer.py +++ b/ubo_app/services/090-web-ui/reducer.py @@ -1,15 +1,45 @@ # ruff: noqa: D100, D101, D102, D103, D104, D107 from __future__ import annotations +from dataclasses import replace from typing import TYPE_CHECKING +from redux import InitAction, InitializationActionError + +from ubo_app.store.operations import ( + InputAction, + InputDemandAction, + InputResolveAction, +) +from ubo_app.store.services.web_ui import WebUIState + if TYPE_CHECKING: - from redux import BaseAction, ReducerResult + from redux import ReducerResult def reducer( - state: None, - action: BaseAction, -) -> ReducerResult[None, None, None]: - _ = action + state: WebUIState | None, + action: InputAction, +) -> WebUIState | ReducerResult[WebUIState, None, None]: + if state is None: + if isinstance(action, InitAction): + return WebUIState(active_inputs=[]) + raise InitializationActionError(action) + + if isinstance(action, InputDemandAction): + return replace( + state, + active_inputs=[*state.active_inputs, action.description], + ) + + if isinstance(action, InputResolveAction): + return replace( + state, + active_inputs=[ + description + for description in state.active_inputs + if description.id != action.id + ], + ) + return state diff --git a/ubo_app/services/090-web-ui/setup.py b/ubo_app/services/090-web-ui/setup.py index a632f372..292778bf 100644 --- a/ubo_app/services/090-web-ui/setup.py +++ b/ubo_app/services/090-web-ui/setup.py @@ -1,24 +1,60 @@ """Implementation of the web-ui service.""" import asyncio +import re from pathlib import Path -from quart import Quart +from quart import Quart, render_template, request from redux import FinishEvent from ubo_app.constants import WEB_UI_DEBUG_MODE, WEB_UI_LISTEN_HOST, WEB_UI_LISTEN_PORT from ubo_app.store.main import store +from ubo_app.store.operations import ( + InputCancelAction, + InputDescription, + InputProvideAction, +) async def init_service() -> None: """Initialize the web-ui service.""" - app = Quart('ubo-app') - app.debug = False + app = Quart( + 'ubo-app', + template_folder=(Path(__file__).parent / 'templates').absolute().as_posix(), + ) + app.debug = WEB_UI_DEBUG_MODE shutdown_event: asyncio.Event = asyncio.Event() - @app.get('/') - async def hello_world() -> str: - return (Path(__file__).parent / 'static' / 'index.html').read_text() + @store.view(lambda state: state.web_ui.active_inputs) + def inputs(inputs: list[InputDescription]) -> list[InputDescription]: + return inputs + + @app.route('/', methods=['GET', 'POST']) + async def inputs_form() -> str: + if request.method == 'POST': + data = dict(await request.form) + if data['action'] == 'cancel': + store.dispatch(InputCancelAction(id=data['id'])) + elif data['action'] == 'provide': + id = data.pop('id') + value = data.pop('value', '') + store.dispatch( + InputProvideAction( + id=id, + value=value, + data=data, + ), + ) + await asyncio.sleep(0.2) + return await render_template('index.jinja2', inputs=inputs(), re=re) + + if WEB_UI_DEBUG_MODE: + + @app.errorhandler(Exception) + async def handle_error(_: Exception) -> str: + import traceback + + return f'
{traceback.format_exc()}
' store.subscribe_event(FinishEvent, shutdown_event.set) diff --git a/ubo_app/services/090-web-ui/static/index.html b/ubo_app/services/090-web-ui/static/index.html deleted file mode 100644 index 5c511233..00000000 --- a/ubo_app/services/090-web-ui/static/index.html +++ /dev/null @@ -1,8 +0,0 @@ - -ubo - web-ui - - -

Hello World!

- - - diff --git a/ubo_app/services/090-web-ui/templates/index.jinja2 b/ubo_app/services/090-web-ui/templates/index.jinja2 new file mode 100644 index 00000000..b472f2a7 --- /dev/null +++ b/ubo_app/services/090-web-ui/templates/index.jinja2 @@ -0,0 +1,40 @@ + + + + + ubo - web-ui + + + + {% for input in inputs %} +
+ +

+

+ +

+ {% endfor %} + {% else %} + + {% endif %} + +

+ + + {% if not loop.last %} +
+ {% endif %} +
+ {% endfor %} + + diff --git a/ubo_app/store/dispatch_action.py b/ubo_app/store/dispatch_action.py index c6afda6a..5a037a13 100644 --- a/ubo_app/store/dispatch_action.py +++ b/ubo_app/store/dispatch_action.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from collections.abc import Callable - from ubo_app.store.operations import UboAction, UboEvent + from ubo_app.store.main import UboAction, UboEvent def _default_action() -> Callable[[], None]: diff --git a/ubo_app/store/input/reducer.py b/ubo_app/store/input/reducer.py new file mode 100644 index 00000000..126484e4 --- /dev/null +++ b/ubo_app/store/input/reducer.py @@ -0,0 +1,40 @@ +"""Input reducer.""" + +from __future__ import annotations + +from redux import CompleteReducerResult, ReducerResult + +from ubo_app.store.operations import ( + InputAction, + InputCancelAction, + InputCancelEvent, + InputProvideAction, + InputProvideEvent, + InputResolveEvent, +) + + +def reducer( + state: None, + action: InputAction, +) -> ReducerResult[None, None, InputResolveEvent]: + """Input reducer.""" + if isinstance(action, InputProvideAction): + return CompleteReducerResult( + state=state, + events=[ + InputProvideEvent( + id=action.id, + value=action.value, + data=action.data, + ), + ], + ) + + if isinstance(action, InputCancelAction): + return CompleteReducerResult( + state=state, + events=[InputCancelEvent(id=action.id)], + ) + + return None diff --git a/ubo_app/store/main.py b/ubo_app/store/main.py index 539f5e6a..03ff3de6 100644 --- a/ubo_app/store/main.py +++ b/ubo_app/store/main.py @@ -9,14 +9,16 @@ from datetime import datetime from pathlib import Path from types import GenericAlias -from typing import TYPE_CHECKING, Any, TypeVar, cast, get_origin, overload +from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar, cast, get_origin, overload import dill from fake import Fake from immutable import Immutable from redux import ( BaseCombineReducerState, + CombineReducerAction, CreateStoreOptions, + FinishAction, FinishEvent, InitAction, Store, @@ -25,9 +27,37 @@ from ubo_app.constants import DEBUG_MODE, STORE_GRACE_PERIOD from ubo_app.logging import logger +from ubo_app.store.core import MainAction, MainEvent from ubo_app.store.core.reducer import reducer as main_reducer -from ubo_app.store.operations import UboAction, UboEvent +from ubo_app.store.input.reducer import reducer as input_reducer +from ubo_app.store.operations import ( + InputAction, + InputProvideEvent, + ScreenshotEvent, + SnapshotEvent, +) +from ubo_app.store.services.audio import AudioAction, AudioEvent +from ubo_app.store.services.camera import CameraAction, CameraEvent +from ubo_app.store.services.display import DisplayAction, DisplayEvent +from ubo_app.store.services.docker import DockerAction +from ubo_app.store.services.ip import IpAction, IpEvent +from ubo_app.store.services.keypad import KeypadAction, KeypadEvent +from ubo_app.store.services.lightdm import LightDMAction +from ubo_app.store.services.notifications import ( + NotificationsAction, + NotificationsEvent, +) +from ubo_app.store.services.rgb_ring import RgbRingAction +from ubo_app.store.services.rpi_connect import RPiConnectAction +from ubo_app.store.services.sensors import SensorsAction +from ubo_app.store.services.ssh import SSHAction +from ubo_app.store.services.users import UsersAction, UsersEvent +from ubo_app.store.services.voice import VoiceAction +from ubo_app.store.services.vscode import VSCodeAction +from ubo_app.store.services.wifi import WiFiAction, WiFiEvent +from ubo_app.store.status_icons import StatusIconsAction from ubo_app.store.status_icons.reducer import reducer as status_icons_reducer +from ubo_app.store.update_manager import UpdateManagerAction from ubo_app.store.update_manager.reducer import reducer as update_manager_reducer from ubo_app.utils.serializer import add_type_field @@ -51,10 +81,55 @@ from ubo_app.store.services.users import UsersState from ubo_app.store.services.voice import VoiceState from ubo_app.store.services.vscode import VSCodeState + from ubo_app.store.services.web_ui import WebUIState from ubo_app.store.services.wifi import WiFiState from ubo_app.store.status_icons import StatusIconsState from ubo_app.store.update_manager import UpdateManagerState +UboAction: TypeAlias = ( + # Core Actions + CombineReducerAction + | StatusIconsAction + | UpdateManagerAction + | MainAction + | InputAction + | InitAction + | FinishAction + # Services Actions + | AudioAction + | CameraAction + | DisplayAction + | DockerAction + | IpAction + | KeypadAction + | LightDMAction + | NotificationsAction + | RgbRingAction + | RPiConnectAction + | SensorsAction + | SSHAction + | UsersAction + | VoiceAction + | VSCodeAction + | WiFiAction +) +UboEvent: TypeAlias = ( + # Core Events + MainEvent + | ScreenshotEvent + | InputProvideEvent + # Services Events + | AudioEvent + | CameraEvent + | DisplayEvent + | IpEvent + | KeypadEvent + | NotificationsEvent + | SnapshotEvent + | UsersEvent + | WiFiEvent +) + if threading.current_thread() is not threading.main_thread(): msg = 'Store should be created in the main thread' raise RuntimeError(msg) @@ -89,6 +164,7 @@ class RootState(BaseCombineReducerState): users: UsersState voice: VoiceState vscode: VSCodeState + web_ui: WebUIState wifi: WiFiState @@ -99,6 +175,7 @@ class RootState(BaseCombineReducerState): main=main_reducer, status_icons=status_icons_reducer, update_manager=update_manager_reducer, + input=input_reducer, ) T = TypeVar('T') diff --git a/ubo_app/store/operations.py b/ubo_app/store/operations.py index fdb426b6..ecea0ca8 100644 --- a/ubo_app/store/operations.py +++ b/ubo_app/store/operations.py @@ -2,37 +2,13 @@ from __future__ import annotations -from typing import TypeAlias - -from redux import ( - BaseEvent, - CombineReducerAction, - FinishAction, - InitAction, -) - -from ubo_app.store.core import MainAction, MainEvent -from ubo_app.store.services.audio import AudioAction, AudioEvent -from ubo_app.store.services.camera import CameraAction, CameraEvent -from ubo_app.store.services.display import DisplayAction, DisplayEvent -from ubo_app.store.services.docker import DockerAction -from ubo_app.store.services.ip import IpAction, IpEvent -from ubo_app.store.services.keypad import KeypadAction, KeypadEvent -from ubo_app.store.services.lightdm import LightDMAction -from ubo_app.store.services.notifications import ( - NotificationsAction, - NotificationsEvent, -) -from ubo_app.store.services.rgb_ring import RgbRingAction -from ubo_app.store.services.rpi_connect import RPiConnectAction -from ubo_app.store.services.sensors import SensorsAction -from ubo_app.store.services.ssh import SSHAction -from ubo_app.store.services.users import UsersAction, UsersEvent -from ubo_app.store.services.voice import VoiceAction -from ubo_app.store.services.vscode import VSCodeAction -from ubo_app.store.services.wifi import WiFiAction, WiFiEvent -from ubo_app.store.status_icons import StatusIconsAction -from ubo_app.store.update_manager import UpdateManagerAction +from typing import TYPE_CHECKING + +from immutable import Immutable +from redux import BaseAction, BaseEvent + +if TYPE_CHECKING: + from ubo_app.store.services.notifications import NotificationExtraInformation class ScreenshotEvent(BaseEvent): @@ -43,44 +19,55 @@ class SnapshotEvent(BaseEvent): """Event for taking a snapshot of the store.""" -UboAction: TypeAlias = ( - # Core Actions - CombineReducerAction - | StatusIconsAction - | UpdateManagerAction - | MainAction - | InitAction - | FinishAction - # Services Actions - | AudioAction - | CameraAction - | DisplayAction - | DockerAction - | IpAction - | KeypadAction - | LightDMAction - | NotificationsAction - | RgbRingAction - | RPiConnectAction - | SensorsAction - | SSHAction - | UsersAction - | VoiceAction - | VSCodeAction - | WiFiAction -) -UboEvent: TypeAlias = ( - # Core Events - MainEvent - | ScreenshotEvent - # Services Events - | AudioEvent - | CameraEvent - | DisplayEvent - | IpEvent - | KeypadEvent - | NotificationsEvent - | SnapshotEvent - | UsersEvent - | WiFiEvent -) +class InputDescription(Immutable): + """Description of an input demand.""" + + title: str + prompt: str | None + extra_information: NotificationExtraInformation | None = None + id: str + pattern: str | None + + +class InputAction(BaseAction): + """Base class for input actions.""" + + +class InputDemandAction(InputAction): + """Action for demanding input from the user.""" + + description: InputDescription + + +class InputResolveAction(InputAction): + """Base class for resolving input demands.""" + + id: str + + +class InputCancelAction(InputResolveAction): + """Action for cancelling an input demand.""" + + +class InputProvideAction(InputResolveAction): + """Action for reporting input from the user.""" + + value: str + data: dict[str, str | None] | None + + +class InputResolveEvent(BaseEvent): + """Base class for resolving input demands.""" + + id: str + + +class InputCancelEvent(InputResolveEvent): + """Event for cancelling an input demand.""" + + +class InputProvideEvent(InputResolveEvent): + """Event for reporting input from the user.""" + + value: str + data: dict[str, str | None] | None diff --git a/ubo_app/store/services/camera.py b/ubo_app/store/services/camera.py index ab710b2f..167618bb 100644 --- a/ubo_app/store/services/camera.py +++ b/ubo_app/store/services/camera.py @@ -1,18 +1,26 @@ # ruff: noqa: D100, D101, D102, D103, D104, D107, N999 from __future__ import annotations +from typing import TYPE_CHECKING + from immutable import Immutable from redux import BaseAction, BaseEvent +if TYPE_CHECKING: + from ubo_app.store.operations import InputDescription + class CameraAction(BaseAction): ... class CameraStartViewfinderAction(CameraAction): - id: str pattern: str | None +class CameraReportBarcodeAction(CameraAction): + codes: list[str] + + class CameraEvent(BaseEvent): ... @@ -24,21 +32,6 @@ class CameraStopViewfinderEvent(CameraEvent): id: str | None -class CameraReportBarcodeAction(CameraAction): - codes: list[str] - - -class CameraBarcodeEvent(CameraEvent): - id: str | None - code: str - group_dict: dict[str, str | None] | None - - -class InputDescription(Immutable): - id: str - pattern: str | None - - class CameraState(Immutable): current: InputDescription | None = None is_viewfinder_active: bool diff --git a/ubo_app/store/services/web-ui.py b/ubo_app/store/services/web-ui.py deleted file mode 100644 index f97a28c0..00000000 --- a/ubo_app/store/services/web-ui.py +++ /dev/null @@ -1,7 +0,0 @@ -# ruff: noqa: D100, D101, D102, D103, D104, D107, N999 -from __future__ import annotations - -from immutable import Immutable - - -class WebUIState(Immutable): ... diff --git a/ubo_app/store/services/web_ui.py b/ubo_app/store/services/web_ui.py new file mode 100644 index 00000000..ac3dff71 --- /dev/null +++ b/ubo_app/store/services/web_ui.py @@ -0,0 +1,13 @@ +# ruff: noqa: D100, D101, D102, D103, D104, D107, N999 +from __future__ import annotations + +from typing import TYPE_CHECKING + +from immutable import Immutable + +if TYPE_CHECKING: + from ubo_app.store.operations import InputDescription + + +class WebUIState(Immutable): + active_inputs: list[InputDescription] diff --git a/ubo_app/utils/input.py b/ubo_app/utils/input.py new file mode 100644 index 00000000..68bffd00 --- /dev/null +++ b/ubo_app/utils/input.py @@ -0,0 +1,143 @@ +"""Imperative input.""" + +from __future__ import annotations + +import asyncio +import uuid +from asyncio import Future +from typing import TYPE_CHECKING, TypeAlias, overload + +from typing_extensions import TypeVar + +from ubo_app.store.main import store +from ubo_app.store.operations import ( + InputCancelEvent, + InputDemandAction, + InputDescription, + InputProvideEvent, +) +from ubo_app.store.services.camera import CameraStopViewfinderEvent +from ubo_app.store.services.rgb_ring import RgbRingBlinkAction + +if TYPE_CHECKING: + from collections.abc import Callable + + from ubo_app.store.services.notifications import NotificationExtraInformation + +InputResultGroupDict: TypeAlias = dict[str, str | None] | None + + +ReturnType = TypeVar('ReturnType', infer_variance=True) + + +@overload +async def ubo_input( + pattern: str, + *, + prompt: str | None = None, + extra_information: NotificationExtraInformation | None = None, + title: str | None = None, +) -> tuple[str, InputResultGroupDict]: ... +@overload +async def ubo_input( + pattern: str, + *, + prompt: str | None = None, + extra_information: NotificationExtraInformation | None = None, + title: str | None = None, + resolver: Callable[[str, InputResultGroupDict], ReturnType], +) -> ReturnType: ... +async def ubo_input( + pattern: str, + *, + prompt: str | None = None, + extra_information: NotificationExtraInformation | None = None, + title: str | None = None, + resolver: Callable[[str, InputResultGroupDict], ReturnType] | None = None, +) -> tuple[str, InputResultGroupDict] | ReturnType: + """Input the user in an imperative way.""" + prompt_id = uuid.uuid4().hex + loop = asyncio.get_running_loop() + + subscriptions: set[Callable[[], None]] = set() + future: Future[tuple[str, InputResultGroupDict]] = loop.create_future() + + def unsubscribe() -> None: + for subscription in subscriptions: + subscription() + + def handle_input_cancel_event(event: InputCancelEvent) -> None: + if event.id == prompt_id: + unsubscribe() + loop.call_soon_threadsafe(future.cancel) + + def handle_input_provide_event(event: InputProvideEvent) -> None: + if event.id == prompt_id: + unsubscribe() + from kivy.utils import get_color_from_hex + + loop.call_soon_threadsafe( + future.set_result, + (event.value, event.data), + ) + kivy_color = get_color_from_hex('#21E693') + color = tuple(round(c * 255) for c in kivy_color[:3]) + store.dispatch( + RgbRingBlinkAction( + color=color, + repetitions=1, + wait=200, + ), + ) + + def handle_cancel(event: CameraStopViewfinderEvent) -> None: + if event.id == prompt_id: + unsubscribe() + loop.call_soon_threadsafe(future.cancel) + + subscriptions.add( + store.subscribe_event( + InputProvideEvent, + handle_input_provide_event, + keep_ref=False, + ), + ) + subscriptions.add( + store.subscribe_event( + InputCancelEvent, + handle_input_cancel_event, + keep_ref=False, + ), + ) + subscriptions.add( + store.subscribe_event( + CameraStopViewfinderEvent, + handle_cancel, + keep_ref=False, + ), + ) + subscriptions.add( + store.subscribe_event( + CameraStopViewfinderEvent, + handle_cancel, + keep_ref=False, + ), + ) + store.dispatch( + InputDemandAction( + description=InputDescription( + title=title or 'Untitled input', + prompt=prompt, + extra_information=extra_information, + id=prompt_id, + pattern=pattern, + ), + ), + ) + + result = await future + + if not resolver: + return result + + return resolver(*result) diff --git a/ubo_app/utils/qrcode.py b/ubo_app/utils/qrcode.py deleted file mode 100644 index 5f5c8edb..00000000 --- a/ubo_app/utils/qrcode.py +++ /dev/null @@ -1,142 +0,0 @@ -"""Module for scanning QR codes using the camera.""" - -from __future__ import annotations - -import asyncio -import datetime -import uuid -from asyncio import Future -from typing import TYPE_CHECKING, TypeAlias, overload - -from typing_extensions import TypeVar - -from ubo_app.store.main import store -from ubo_app.store.services.camera import ( - CameraBarcodeEvent, - CameraStartViewfinderAction, - CameraStopViewfinderEvent, -) -from ubo_app.store.services.notifications import ( - Notification, - NotificationActionItem, - NotificationDisplayType, - NotificationExtraInformation, - NotificationsAddAction, -) -from ubo_app.store.services.rgb_ring import RgbRingBlinkAction - -if TYPE_CHECKING: - from collections.abc import Callable - -QrCodeGroupDict: TypeAlias = dict[str, str | None] | None - - -ReturnType = TypeVar('ReturnType', infer_variance=True) - - -@overload -async def qrcode_input( - pattern: str, - *, - prompt: str | None = None, - extra_information: NotificationExtraInformation | None = None, - title: str | None = None, -) -> tuple[str, QrCodeGroupDict]: ... -@overload -async def qrcode_input( - pattern: str, - *, - prompt: str | None = None, - extra_information: NotificationExtraInformation | None = None, - title: str | None = None, - resolver: Callable[[str, QrCodeGroupDict], ReturnType], -) -> ReturnType: ... -async def qrcode_input( - pattern: str, - *, - prompt: str | None = None, - extra_information: NotificationExtraInformation | None = None, - title: str | None = None, - resolver: Callable[[str, QrCodeGroupDict], ReturnType] | None = None, -) -> tuple[str, QrCodeGroupDict] | ReturnType: - """Use the camera to scan a QR code.""" - prompt_id = uuid.uuid4().hex - loop = asyncio.get_running_loop() - - if prompt: - notification_future: Future[None] = loop.create_future() - store.dispatch( - NotificationsAddAction( - notification=Notification( - id='qrcode', - icon='󰄀󰐲', - title='QR Code' if title is None else title, - content=f'[size=18dp]{prompt}[/size]', - display_type=NotificationDisplayType.STICKY, - is_read=True, - extra_information=extra_information, - expiration_timestamp=datetime.datetime.now(tz=datetime.UTC), - color='#ffffff', - actions=[ - NotificationActionItem( - action=lambda: loop.call_soon_threadsafe( - notification_future.set_result, - None, - ) - and None, - icon='󰄀', - dismiss_notification=True, - ), - ], - dismissable=False, - dismiss_on_close=True, - on_close=lambda: loop.call_soon_threadsafe( - notification_future.cancel, - ), - ), - ), - ) - - await notification_future - - future: Future[tuple[str, QrCodeGroupDict]] = loop.create_future() - - def handle_barcode_event(event: CameraBarcodeEvent) -> None: - if event.id == prompt_id: - from kivy.utils import get_color_from_hex - - loop.call_soon_threadsafe(future.set_result, (event.code, event.group_dict)) - kivy_color = get_color_from_hex('#21E693') - store.dispatch( - RgbRingBlinkAction( - color=( - round(kivy_color[0] * 255), - round(kivy_color[1] * 255), - round(kivy_color[2] * 255), - ), - repetitions=1, - wait=200, - ), - ) - - def handle_cancel(event: CameraStopViewfinderEvent) -> None: - if event.id == prompt_id: - loop.call_soon_threadsafe(future.cancel) - - store.subscribe_event( - CameraBarcodeEvent, - handle_barcode_event, - keep_ref=False, - ) - store.subscribe_event( - CameraStopViewfinderEvent, - handle_cancel, - ) - store.dispatch(CameraStartViewfinderAction(id=prompt_id, pattern=pattern)) - - result = await future - - if not resolver: - return result - - return resolver(*result) diff --git a/uv.lock b/uv.lock index ae1acfa9..93d2e266 100644 --- a/uv.lock +++ b/uv.lock @@ -938,6 +938,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/b1/d1ca22a7b18e7b2b90152a78a0c2d09a96fdb924f87be1914d70d9bee543/kivy_deps.angle-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:574381d4e66f3198bc48aa10f238e7a3816ad56b80ec939f5d56fb33a378d0b1", size = 5130936 }, { url = "https://files.pythonhosted.org/packages/c1/89/bb8b9a0fee422972fcf38a406ee9d0b1636968d7d2b5e97aafea8fdec251/kivy_deps.angle-0.4.0-cp312-cp312-win32.whl", hash = "sha256:4fa7a6366899fba13f7624baf4645787165f45731db08d14557da29c12ee48f0", size = 4588969 }, { url = "https://files.pythonhosted.org/packages/c7/f2/d1500b880d3079454af0f935408ddd37cfce4fd11f53d0917e169d478869/kivy_deps.angle-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:668e670d4afd2551af0af2c627ceb0feac884bd799fb6a3dff78fdbfa2ea0451", size = 5130935 }, + { url = "https://files.pythonhosted.org/packages/47/7e/ad805773fb76f07cb1bdf5147e66ba264a94f5ac54553cd9dee809a161bb/kivy_deps.angle-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:9afbf702f8bb9a993c48f39c018ca3b4d2ec381a5d3f82fe65bdaa6af0bba29b", size = 5133260 }, ] [[package]] @@ -949,6 +950,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/37/884034260818569547347cc2ba89780ff3f83a9ce6b9a894360c1d86e82c/kivy_deps.glew-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:22e155ec59ce717387f5d8804811206d200a023ba3d0bc9bbf1393ee28d0053e", size = 123574 }, { url = "https://files.pythonhosted.org/packages/2b/3b/a960053dccd627e4483db4765fa84318a831cbf3af648aee20297ae56815/kivy_deps.glew-0.3.1-cp312-cp312-win32.whl", hash = "sha256:b64ee4e445a04bc7c848c0261a6045fc2f0944cc05d7f953e3860b49f2703424", size = 126458 }, { url = "https://files.pythonhosted.org/packages/ad/3a/37a0a051dd3c7298d9e149a489457a6196665444c1a1473ad4fa617e05af/kivy_deps.glew-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:3acbbd30da05fc10c185b5d4bb75fbbc882a6ef2192963050c1c94d60a6e795a", size = 123573 }, + { url = "https://files.pythonhosted.org/packages/21/99/e3478c34afed7a820b3348ce7fefc53f2034fa340348dca57162695e69d9/kivy_deps.glew-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:f4aa8322078359862ccd9e16e5cea61976d75fb43125d87922e20c916fa31a11", size = 123595 }, ] [[package]] @@ -1542,7 +1544,7 @@ wheels = [ [[package]] name = "python-redux" -version = "0.17.1" +version = "0.17.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyright" }, @@ -1550,9 +1552,9 @@ dependencies = [ { name = "python-strtobool" }, { name = "ruff" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/80/f8f508233455dad8edc239dd37fc65d0587f3a8203d4147e7c03119c90a9/python_redux-0.17.1.tar.gz", hash = "sha256:e16aaa434f09a1b0e064958458a0f7376f2e0c306718d8f79df38cb8286db5b3", size = 22677 } +sdist = { url = "https://files.pythonhosted.org/packages/26/e0/476b776d5410d97bf57d069240385b4ce0ec9e6e1c972ef6946cd3a46b27/python_redux-0.17.2.tar.gz", hash = "sha256:185e491390c6ae4eecf1e6f3c34cab25bed21667bc4ca49e02a94494ae497bf3", size = 22940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/ff/305aaeb1ef73cf25861a16428db9e83112886193d174806dc55e16d21298/python_redux-0.17.1-py3-none-any.whl", hash = "sha256:f98f9c483172786f7a8e762f98f63d0afaa7466025a8f7aab7762d76f174eedd", size = 25059 }, + { url = "https://files.pythonhosted.org/packages/49/47/a9de36c30dfc0aa8e30d5fbd58aa9152fba47dd0a49f17193068fa1bf50f/python_redux-0.17.2-py3-none-any.whl", hash = "sha256:c10d505ac1861e0e221cf694268f207f7fd15f1d0d73d1d2b25e31425f7fe880", size = 25390 }, ] [[package]] @@ -1873,7 +1875,7 @@ wheels = [ [[package]] name = "ubo-app" -version = "0.17.2.dev4+unknown" +version = "1.0.1.dev6+unknown" source = { editable = "." } dependencies = [ { name = "adafruit-circuitpython-aw9523" }, @@ -1950,7 +1952,7 @@ requires-dist = [ { name = "python-debouncer", specifier = ">=0.1.5" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-fake", specifier = ">=0.1.3" }, - { name = "python-redux", specifier = ">=0.17.1" }, + { name = "python-redux", specifier = ">=0.17.2" }, { name = "python-strtobool", specifier = ">=1.0.0" }, { name = "pyzbar", specifier = ">=0.1.9" }, { name = "quart", specifier = ">=0.19.6" },