From 924fa6dde22315b1aec602f07a0063a1776f0ac5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 13 Sep 2023 18:31:53 +0000 Subject: [PATCH] Hero 12 Support --- .github/workflows/github-pages.yml | 10 +- .github/workflows/python_sdk_test.yml | 6 +- .../sdk_wireless_camera_control/.gitignore | 1 + .../.python-version | 2 +- .../sdk_wireless_camera_control/README.md | 31 +- .../sdk_wireless_camera_control/TODO.md | 4 +- .../docs/_static/coverage.svg | 4 +- .../sdk_wireless_camera_control/docs/api.rst | 71 +- .../docs/changelog.rst | 1 + .../sdk_wireless_camera_control/docs/conf.py | 35 +- .../docs/contributing.rst | 6 +- .../docs/index.rst | 2 +- .../docs/troubleshooting.rst | 2 +- .../docs/usage.rst | 287 +- .../sdk_wireless_camera_control/noxfile.py | 13 +- .../open_gopro/__init__.py | 13 +- .../open_gopro/api/__init__.py | 14 +- .../open_gopro/api/api.py | 5 +- .../open_gopro/api/ble_commands.py | 772 ++- .../open_gopro/api/builders.py | 577 +- .../open_gopro/api/http_commands.py | 365 +- .../open_gopro/api/params.py | 241 +- .../open_gopro/api/parsers.py | 381 ++ .../open_gopro/ble/__init__.py | 6 +- .../open_gopro/ble/adapters/__init__.py | 2 +- .../open_gopro/ble/adapters/bleak_wrapper.py | 437 +- .../open_gopro/ble/client.py | 46 +- .../open_gopro/ble/controller.py | 22 +- .../open_gopro/ble/services.py | 26 +- ...interface.py => communicator_interface.py} | 181 +- .../open_gopro/constants.py | 196 +- .../open_gopro/demos/connect_wifi.py | 22 +- .../open_gopro/demos/gui/__init__.py | 3 +- .../demos/gui/components/__init__.py | 8 +- .../demos/gui/components/controllers.py | 45 +- .../open_gopro/demos/gui/components/models.py | 162 +- .../open_gopro/demos/gui/components/util.py | 42 +- .../open_gopro/demos/gui/components/views.py | 62 +- .../open_gopro/demos/gui/gui_demo.py | 9 +- .../open_gopro/demos/gui/livestream.py | 145 +- .../open_gopro/demos/gui/preview_stream.py | 45 + .../open_gopro/demos/gui/webcam.py | 116 +- .../open_gopro/demos/log_battery.py | 112 +- .../open_gopro/demos/photo.py | 38 +- .../open_gopro/demos/preset_control.py | 932 ---- .../open_gopro/demos/video.py | 45 +- .../open_gopro/enum.py | 128 + .../open_gopro/exceptions.py | 4 +- .../open_gopro/gopro_base.py | 145 +- .../open_gopro/gopro_wired.py | 198 +- .../open_gopro/gopro_wireless.py | 565 +- .../open_gopro/logger.py | 267 + .../open_gopro/models/__init__.py | 15 + .../open_gopro/models/bases.py | 20 + .../open_gopro/models/general.py | 58 + .../open_gopro/models/media_list.py | 150 + .../open_gopro/models/response.py | 521 ++ .../open_gopro/parser_interface.py | 262 + .../open_gopro/proto/__init__.py | 56 +- .../open_gopro/proto/live_streaming_pb2.py | 4 +- .../open_gopro/proto/live_streaming_pb2.pyi | 44 +- .../proto/network_management_pb2.py | 4 +- .../proto/network_management_pb2.pyi | 11 +- .../open_gopro/proto/preset_status_pb2.py | 4 +- .../open_gopro/proto/preset_status_pb2.pyi | 14 +- .../proto/request_get_preset_status_pb2.py | 4 +- .../proto/request_get_preset_status_pb2.pyi | 22 +- .../open_gopro/proto/response_generic_pb2.py | 4 +- .../proto/set_camera_control_status_pb2.py | 4 +- .../proto/set_camera_control_status_pb2.pyi | 4 +- .../open_gopro/proto/turbo_transfer_pb2.py | 4 +- .../open_gopro/responses.py | 777 --- .../open_gopro/types.py | 38 + .../open_gopro/util.py | 464 +- .../open_gopro/wifi/__init__.py | 6 +- .../open_gopro/wifi/adapters/__init__.py | 2 +- .../open_gopro/wifi/adapters/wireless.py | 29 +- .../open_gopro/wifi/client.py | 3 +- .../open_gopro/wifi/mdns_scanner.py | 88 + .../sdk_wireless_camera_control/poetry.lock | 4118 +++++++------- .../pyproject.toml | 51 +- .../tests/__init__.py | 8 + .../tests/conftest.py | 322 +- .../tests/unit/test_ble_commands.py | 166 +- .../tests/unit/test_bleak_wrapper.py | 215 +- .../tests/unit/test_enums.py | 48 + .../tests/unit/test_gopro.py | 110 - .../tests/unit/test_gopro_ble.py | 68 +- .../tests/unit/test_gopro_wifi.py | 26 +- .../tests/unit/test_http_commands.py | 31 +- .../tests/unit/test_models.py | 296 + .../tests/unit/test_parsers.py | 32 + .../tests/unit/test_responses.py | 125 +- .../tests/unit/test_services.py | 64 +- ..._wireless_wifi.py => test_wifi_adapter.py} | 19 +- .../tests/unit/test_wired_gopro.py | 91 + .../tests/unit/test_wireless_gopro.py | 171 + .../tools/start_rtmp_server.sh | 11 - docs/index.md | 4 +- docs/specs/ble_versions/ble_2_0.md | 1678 +++++- docs/specs/capabilities.json | 4910 +++++++++++++++-- docs/specs/capabilities.xlsx | 4 +- docs/specs/http_versions/http_2_0.md | 2808 ++++++++-- protobuf/preset_status.proto | 264 +- tools/test_media_server/.dockerignore | 10 + tools/test_media_server/.gitignore | 3 + tools/test_media_server/Dockerfile | 120 + tools/test_media_server/README.md | 81 + tools/test_media_server/cert_request.ext | 19 + tools/test_media_server/cert_request.ini | 17 + tools/test_media_server/docker-compose.yml | 15 + tools/test_media_server/entrypoint.sh | 67 + tools/test_media_server/hls.html | 59 + tools/test_media_server/nginx.conf | 122 + 114 files changed, 17012 insertions(+), 8605 deletions(-) create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/api/parsers.py rename demos/python/sdk_wireless_camera_control/open_gopro/{interface.py => communicator_interface.py} (71%) create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/preview_stream.py delete mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/demos/preset_control.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/enum.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/logger.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/models/__init__.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/models/bases.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/models/general.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/models/media_list.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/models/response.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/parser_interface.py delete mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/responses.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/types.py create mode 100644 demos/python/sdk_wireless_camera_control/open_gopro/wifi/mdns_scanner.py create mode 100644 demos/python/sdk_wireless_camera_control/tests/unit/test_enums.py delete mode 100644 demos/python/sdk_wireless_camera_control/tests/unit/test_gopro.py create mode 100644 demos/python/sdk_wireless_camera_control/tests/unit/test_models.py create mode 100644 demos/python/sdk_wireless_camera_control/tests/unit/test_parsers.py rename demos/python/sdk_wireless_camera_control/tests/unit/{test_wireless_wifi.py => test_wifi_adapter.py} (94%) create mode 100644 demos/python/sdk_wireless_camera_control/tests/unit/test_wired_gopro.py create mode 100644 demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_gopro.py delete mode 100755 demos/python/sdk_wireless_camera_control/tools/start_rtmp_server.sh create mode 100644 tools/test_media_server/.dockerignore create mode 100644 tools/test_media_server/.gitignore create mode 100644 tools/test_media_server/Dockerfile create mode 100644 tools/test_media_server/README.md create mode 100644 tools/test_media_server/cert_request.ext create mode 100644 tools/test_media_server/cert_request.ini create mode 100644 tools/test_media_server/docker-compose.yml create mode 100644 tools/test_media_server/entrypoint.sh create mode 100644 tools/test_media_server/hls.html create mode 100644 tools/test_media_server/nginx.conf diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index 80729709..ea98ca5f 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -59,10 +59,10 @@ jobs: shell: bash run: sudo apt-get update && sudo apt install -y graphviz - - name: Set up Python 3.10.6 + - name: Set up Python 3.11.4 uses: actions/setup-python@v2 with: - python-version: 3.10.6 + python-version: 3.11.4 - name: Restore cached pip environment uses: actions/cache@v2 @@ -76,8 +76,8 @@ jobs: working-directory: ./source/demos/python/sdk_wireless_camera_control/ run: | python -m pip install --upgrade pip wheel - pip install nox==2022.8.7 - pip install nox-poetry==1.0.1 + pip install nox==2023.4.22 + pip install nox-poetry==1.0.3 pip install poetry - name: Build Sphinx Documentation @@ -133,7 +133,7 @@ jobs: if [[ ${{ github.repository }} == gopro/OpenGoPro ]]; then make build else - make build BUILD_HOST_URL=${{ secrets.STAGING_GH_PAGES_SITE }} BUILD_BASE_URL='\"\"' + make build BUILD_HOST_URL=${{ secrets.STAGING_GH_PAGES_SITE }} BUILD_BASE_URL="\'\'" fi - name: Deploy to Github Pages diff --git a/.github/workflows/python_sdk_test.yml b/.github/workflows/python_sdk_test.yml index 111ae810..d462f59f 100644 --- a/.github/workflows/python_sdk_test.yml +++ b/.github/workflows/python_sdk_test.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] - python-version: ["3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11"] include: - os: ubuntu-latest path: ~/.cache/pip @@ -49,8 +49,8 @@ jobs: working-directory: ./demos/python/sdk_wireless_camera_control/ run: | python -m pip install --upgrade pip wheel - pip install nox==2022.8.7 - pip install nox-poetry==1.0.1 + pip install nox==2023.4.22 + pip install nox-poetry==1.0.3 pip install poetry - name: Perform checks (format, types, lint, docstrings, unit tests, docs, safety) diff --git a/demos/python/sdk_wireless_camera_control/.gitignore b/demos/python/sdk_wireless_camera_control/.gitignore index 62d73497..bfbe3baf 100644 --- a/demos/python/sdk_wireless_camera_control/.gitignore +++ b/demos/python/sdk_wireless_camera_control/.gitignore @@ -1,3 +1,4 @@ +**/*.crt **/temp .reports/ diff --git a/demos/python/sdk_wireless_camera_control/.python-version b/demos/python/sdk_wireless_camera_control/.python-version index 36435ac6..0c7d5f5f 100644 --- a/demos/python/sdk_wireless_camera_control/.python-version +++ b/demos/python/sdk_wireless_camera_control/.python-version @@ -1 +1 @@ -3.10.8 +3.11.4 diff --git a/demos/python/sdk_wireless_camera_control/README.md b/demos/python/sdk_wireless_camera_control/README.md index aa1d7a23..7235ed6e 100644 --- a/demos/python/sdk_wireless_camera_control/README.md +++ b/demos/python/sdk_wireless_camera_control/README.md @@ -28,6 +28,7 @@ Complete documentation can be found on [Open GoPro](https://gopro.github.io/Open - BLE implemented using [bleak](https://pypi.org/project/bleak/) - Wi-Fi controller provided in the Open GoPro package (loosely based on the [Wireless Library](https://pypi.org/project/wireless/) - Supports all commands, settings, and statuses from the [Open GoPro API](https://gopro.github.io/OpenGoPro/) +- [Asyncio](https://docs.python.org/3/library/asyncio.html) based - Automatically handles connection maintenance: - manage camera ready / encoding - periodically sends keep alive signals @@ -41,7 +42,7 @@ Complete documentation can be found on [Open GoPro](https://gopro.github.io/Open ## Installation -> Note! This package requires Python >= 3.8 and < 3.11 +> Note! This package requires Python >= 3.8 and < 3.12 The minimal install to use the Open GoPro library and the CLI demos is: @@ -61,21 +62,23 @@ To automatically connect to GoPro device via BLE and WiFI, set the preset, set v video, and download all files: ```python -import time +import asyncio from open_gopro import WirelessGoPro, Params -with WirelessGoPro() as gopro: - gopro.ble_command.load_preset(Params.Preset.CINEMATIC) - gopro.ble_setting.resolution.set(Params.Resolution.RES_4K) - gopro.ble_setting.fps.set(Params.FPS.FPS_30) - gopro.ble_command.set_shutter(Params.Shutter.ON) - time.sleep(2) # Record for 2 seconds - gopro.ble_command.set_shutter(Params.Shutter.OFF) - - # Download all of the files from the camera - media_list = [x["n"] for x in gopro.wifi_command.get_media_list().flatten - for file in media_list: - gopro.wifi_command.download_file(camera_file=file) +async def main(): + async with WirelessGoPro() as gopro: + await gopro.ble_setting.resolution.set(Params.Resolution.RES_4K) + await gopro.ble_setting.fps.set(Params.FPS.FPS_30) + await gopro.ble_command.set_shutter(shutter=Params.Toggle.ENABLE) + await asyncio.sleep(2) # Record for 2 seconds + await gopro.ble_command.set_shutter(shutter=Params.Toggle.DISABLE) + + # Download all of the files from the camera + media_list = (await gopro.http_command.get_media_list()).data.files + for item in media_list: + await gopro.http_command.download_file(camera_file=item.filename) + +asyncio.run(main()) ``` And much more! diff --git a/demos/python/sdk_wireless_camera_control/TODO.md b/demos/python/sdk_wireless_camera_control/TODO.md index c04f65ac..0ba98ab0 100644 --- a/demos/python/sdk_wireless_camera_control/TODO.md +++ b/demos/python/sdk_wireless_camera_control/TODO.md @@ -6,9 +6,8 @@ - [ ] Refactor commands / parsers so that setting / status parsers (i.e. enums) can be accessed from class, not instance - This is already done for commands but needs to be for others - [ ] Better handle kwargs that match base dict args in command as_dict methods -- [ ] Investigate worthiness of move to asyncio - [ ] More test coverage -- [ ] Clean up artifacts after testing +- [ ] Clean up artifacts after testing. Or use temp directory. - [ ] Make scalable for multiple simultaneous cameras - [ ] Allow encoding = False for Set Livestream Mode. Requires tracking livestream state to not pend on encoding started after sending Set Shutter On @@ -36,5 +35,6 @@ - [ ] Investigate MacOS delay after connecting WiFi - [ ] More Linux testing - [ ] Use descriptors for main driver access to OS-driver implementation +- [ ] Move to program language (C?) instead of EN-US diff --git a/demos/python/sdk_wireless_camera_control/docs/_static/coverage.svg b/demos/python/sdk_wireless_camera_control/docs/_static/coverage.svg index 321ee2ee..75518b71 100644 --- a/demos/python/sdk_wireless_camera_control/docs/_static/coverage.svg +++ b/demos/python/sdk_wireless_camera_control/docs/_static/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 76% - 76% + 77% + 77% diff --git a/demos/python/sdk_wireless_camera_control/docs/api.rst b/demos/python/sdk_wireless_camera_control/docs/api.rst index d8511beb..4b57944d 100644 --- a/demos/python/sdk_wireless_camera_control/docs/api.rst +++ b/demos/python/sdk_wireless_camera_control/docs/api.rst @@ -76,6 +76,11 @@ These should not be imported directly and instead should be accessed using the r Base Types ---------- +GoPro Enum +^^^^^^^^^^ + +.. autoclass:: open_gopro.enum.GoProEnum + BLE Setting ^^^^^^^^^^^ @@ -107,27 +112,27 @@ Message Bases These are the base types that are used to implement version-specific API's. These are published for reference but the end user should never need to use these directly. -.. autoclass:: open_gopro.interface.Message +.. autoclass:: open_gopro.communicator_interface.Message :show-inheritance: -.. autoclass:: open_gopro.interface.HttpMessage +.. autoclass:: open_gopro.communicator_interface.HttpMessage :show-inheritance: -.. autoclass:: open_gopro.interface.BleMessage +.. autoclass:: open_gopro.communicator_interface.BleMessage :show-inheritance: -.. autoclass:: open_gopro.interface.Messages +.. autoclass:: open_gopro.communicator_interface.Messages :show-inheritance: -.. autoclass:: open_gopro.interface.BleMessages +.. autoclass:: open_gopro.communicator_interface.BleMessages :show-inheritance: -.. autoclass:: open_gopro.interface.HttpMessages +.. autoclass:: open_gopro.communicator_interface.HttpMessages :show-inheritance: -.. autoclass:: open_gopro.interface.MessageRules +.. autoclass:: open_gopro.communicator_interface.MessageRules -.. autoclass:: open_gopro.interface.RuleSignature +.. autoclass:: open_gopro.communicator_interface.RuleSignature Parameters ---------- @@ -141,18 +146,52 @@ All of these parameters can be accessed via: .. automodule:: open_gopro.api.params :undoc-members: - Responses ========= +Generic common response container: + This can be imported via: .. code-block:: python from open_gopro import GoProResp -.. autoclass:: open_gopro.responses.GoProResp +.. autoclass:: open_gopro.models.response.GoProResp + +Data Models +----------- +These are the various models that are returned in responses, used in commands, etc. They can be imported with: + +.. code-block:: python + + from open_gopro.models import *** + +.. autopydantic_model:: open_gopro.models.media_list.MediaMetadata + +.. autopydantic_model:: open_gopro.models.media_list.PhotoMetadata + :show-inheritance: + +.. autopydantic_model:: open_gopro.models.media_list.VideoMetadata + :show-inheritance: + +.. autopydantic_model:: open_gopro.models.media_list.MediaItem + +.. autopydantic_model:: open_gopro.models.media_list.GroupedMediaItem + :show-inheritance: + +.. autopydantic_model:: open_gopro.models.media_list.MediaFileSystem + +.. autopydantic_model:: open_gopro.models.media_list.MediaList + +.. autopydantic_model:: open_gopro.models.general.TzDstDateTime + +.. autopydantic_model:: open_gopro.models.general.CameraInfo + +.. autopydantic_model:: open_gopro.models.general.WebcamResponse + +.. autopydantic_model:: open_gopro.models.general.SupportedOption Constants ========= @@ -179,15 +218,17 @@ Common Interface .. autoclass:: open_gopro.gopro_base.GoProBase -.. autoclass:: open_gopro.interface.GoProBle +.. autoclass:: open_gopro.communicator_interface.GoProBle + +.. autoclass:: open_gopro.communicator_interface.GoProHttp -.. autoclass:: open_gopro.interface.GoProHttp +.. autoclass:: open_gopro.communicator_interface.GoProWifi -.. autoclass:: open_gopro.interface.GoProWifi +.. autoclass:: open_gopro.communicator_interface.GoProWiredInterface -.. autoclass:: open_gopro.interface.GoProWiredInterface +.. autoclass:: open_gopro.communicator_interface.GoProWirelessInterface -.. autoclass:: open_gopro.interface.GoProWirelessInterface +.. autoclass:: open_gopro.communicator_interface.BaseGoProCommunicator BLE Interface diff --git a/demos/python/sdk_wireless_camera_control/docs/changelog.rst b/demos/python/sdk_wireless_camera_control/docs/changelog.rst index 0702f405..54d419fc 100644 --- a/demos/python/sdk_wireless_camera_control/docs/changelog.rst +++ b/demos/python/sdk_wireless_camera_control/docs/changelog.rst @@ -13,6 +13,7 @@ Unreleased ---------- * Improve video viewer latency * Improve BLE and HTTP setting documentation +* Add media list and metadata pydantic models 0.13.0 (February-24-2023) ------------------------- diff --git a/demos/python/sdk_wireless_camera_control/docs/conf.py b/demos/python/sdk_wireless_camera_control/docs/conf.py index d939b973..ab6330bb 100644 --- a/demos/python/sdk_wireless_camera_control/docs/conf.py +++ b/demos/python/sdk_wireless_camera_control/docs/conf.py @@ -2,24 +2,9 @@ # This copyright was auto-generated on Wed, Sep 1, 2021 5:05:41 PM from __future__ import annotations -import re from datetime import date -from typing import Union, Optional - -from darglint.docstring.docstring import Docstring -from darglint.docstring.sections import Sections -from sphinx.ext.napoleon.docstring import GoogleDocstring from open_gopro import WirelessGoPro -from open_gopro.api.builders import ( - HttpGetBinary, - HttpGetJsonCommand, - BleProtoCommand, - BleWriteCommand, - BleReadCommand, - RegisterUnregisterAll, -) -from open_gopro.interface import BleMessage, HttpMessage gopro = WirelessGoPro(enable_wifi=False) @@ -38,7 +23,8 @@ "sphinx.ext.autosectionlabel", "sphinx.ext.graphviz", "sphinx.ext.inheritance_diagram", - "sphinxemoji.sphinxemoji", + # "sphinxemoji.sphinxemoji", # https://github.com/sphinx-contrib/emojicodes/issues/42 + "sphinxcontrib.autodoc_pydantic", ] html_theme = "sphinx_rtd_theme" html_context = { @@ -50,6 +36,9 @@ autodoc_default_options = { "members": True, } +# https://autodoc-pydantic.readthedocs.io/en/stable/users/installation.html#configuration +autodoc_pydantic_model_show_json = True +autodoc_pydantic_settings_show_json = False # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout @@ -80,21 +69,25 @@ ("py:class", "T_co"), ("py:class", "ExceptionHandler"), ("py:class", "datetime.datetime"), - ("py:class", "open_gopro.responses.Parser"), - ("py:class", "InitVar"), + ("py:class", "open_gopro.models.parsers.Parser"), ("py:class", "abc.ABC"), ("py:class", "collections.abc.Iterable"), ] nitpick_ignore_regex = [ + (r"py:class", r".*proto\..+"), # TODO how should / can we handle protobuf documenting? + (r"py:class", r".*_pb2\..+"), (r"py:class", r".+Type"), (r"py:obj", r".+Type"), (r"py:class", r".*Path"), + (r"py:class", r".*InitVar"), + (r"py:class", r".*UpdateCb"), + (r"py:class", r".*JsonDict"), (r"py:class", r".*BleDevice"), (r"py:class", r".*BleHandle"), - (r"py:class", r".*JsonParser"), - (r"py:class", r".*BytesParserBuilder"), - (r"py:class", r".*BytesParser"), + (r"py:class", r".*Parser"), + (r"py:class", r".*Builder"), (r".*", r".*construct.*"), + (r".*", r".*response.T*"), ] # This is the expected signature of the handler for this event, cf doc diff --git a/demos/python/sdk_wireless_camera_control/docs/contributing.rst b/demos/python/sdk_wireless_camera_control/docs/contributing.rst index fcadf335..92d7faef 100644 --- a/demos/python/sdk_wireless_camera_control/docs/contributing.rst +++ b/demos/python/sdk_wireless_camera_control/docs/contributing.rst @@ -58,7 +58,7 @@ Ready to contribute? Here's how to set up Open GoPro for local development. Minimal Requirements ~~~~~~~~~~~~~~~~~~~~ -* Python (>= 3.9, < 3.11) +* Python (>= 3.9, < 3.12) * `Poetry `_ : Needed to install dependencies / development tasks Additional Optional Requirements: @@ -92,7 +92,7 @@ Steps .. code-block:: console - $ poetry install --extras gui + $ poetry install --all-extras #. Make your changes locally. When you're done making changes, check that your changes are: @@ -127,7 +127,7 @@ Before you submit a pull request, check that it meets these guidelines: $ poetry run poe docs #. Modify the ``CHANGELOG.rst``. -#. The pull request should work for Python 3.8 - 3.10 on the following platforms: +#. The pull request should work for Python 3.8 - 3.12 on the following platforms: - Windows 10, version 16299 (Fall Creators Update) and greater - Linux distributions with BlueZ >= 5.43 diff --git a/demos/python/sdk_wireless_camera_control/docs/index.rst b/demos/python/sdk_wireless_camera_control/docs/index.rst index 26fcd2aa..21302fdc 100644 --- a/demos/python/sdk_wireless_camera_control/docs/index.rst +++ b/demos/python/sdk_wireless_camera_control/docs/index.rst @@ -40,7 +40,7 @@ For more information on the API, see the relevant documentation: - `HTTP API `_ .. warning:: - This package requires Python >= version 3.9 and < 3.11 + This package requires Python >= version 3.9 and < 3.12 Features -------- diff --git a/demos/python/sdk_wireless_camera_control/docs/troubleshooting.rst b/demos/python/sdk_wireless_camera_control/docs/troubleshooting.rst index 7d9dbe1c..ad835647 100644 --- a/demos/python/sdk_wireless_camera_control/docs/troubleshooting.rst +++ b/demos/python/sdk_wireless_camera_control/docs/troubleshooting.rst @@ -23,7 +23,7 @@ All of the demos use this and here is an example: logger = setup_logging(__name__, Path("my_log.log")) - with WirelessGoPro() as gopro: + async with WirelessGoPro() as gopro: logger.info("I'm logged!") There are several other logging-related functions that extend and / or offer finer logging control. diff --git a/demos/python/sdk_wireless_camera_control/docs/usage.rst b/demos/python/sdk_wireless_camera_control/docs/usage.rst index 91859520..94fd9152 100644 --- a/demos/python/sdk_wireless_camera_control/docs/usage.rst +++ b/demos/python/sdk_wireless_camera_control/docs/usage.rst @@ -22,9 +22,26 @@ camera resource. The general procedure to communicate with the GoPro is: 2. :ref:`Send Messages` and :ref:`Receive Responses` via BLE / HTTP 3. Gracefully :ref:`close` the connection with the GoPro -.. tip:: There is a lot of logging throughout the Open GoPro package. See +.. tip:: There is a lot of useful logging throughout the Open GoPro package. See :ref:`troubleshooting ` for more info. +Asyncio +======= + +This package is `asyncio `_-based which means that its awaitable +methods need to be called from an async coroutine. For the code snippets throughout this documentation, assume that this +is accomplished in the same manner as the demo scripts provided: + +.. code-block:: python + + import asyncio + + async def main() -> None: + # Put our code here + + if __name__ == "__main__": + asyncio.run(main()) + Opening ======= @@ -40,7 +57,7 @@ The Wireless GoPro client can be opened either with the context manager: from open_gopro import WirelessGoPro - with WirelessGoPro() as gopro: + async with WirelessGoPro() as gopro: print("Yay! I'm connected via BLE, Wifi, opened, and ready to send / get data now!") # Send some messages now @@ -51,7 +68,7 @@ The Wireless GoPro client can be opened either with the context manager: from open_gopro import WirelessGoPro gopro = WirelessGoPro() - gopro.open() + await gopro.open() print("Yay! I'm connected via BLE, Wifi, opened, and ready to send / get data now!") # Send some messages now @@ -78,7 +95,7 @@ The Wired GoPro client can be opened either with the context manager: from open_gopro import WiredGoPro - with WiredGoPro() as gopro: + async with WiredGoPro() as gopro: print("Yay! I'm connected via USB, opened, and ready to send / get data now!") # Send some messages now @@ -89,18 +106,17 @@ The Wired GoPro client can be opened either with the context manager: from open_gopro import WiredGoPro gopro = WiredGoPro() - gopro.open() + await gopro.open() print("Yay! I'm connected via USB, opened, and ready to send / get data now!") # Send some messages now -If an identifier is not passed to the `WiredGoPro`, the mDNS server will be queried during opening to search +If, as above, an identifier is not passed to the `WiredGoPro`, the mDNS server will be queried during opening to search for a connected GoPro. Common Opening -------------- -The GoPro's state can be checked via several properties. All of the following will return True after a -successful opening: +The GoPro's state can be checked via several properties. - :meth:`~open_gopro.gopro_base.GoProBase.is_ble_connected` - :meth:`~open_gopro.gopro_base.GoProBase.is_http_connected` @@ -120,22 +136,21 @@ Camera Readiness A message can not be sent to the camera if it is not ready where "ready" is defined as not encoding and not busy. These two states are managed automatically by the `WirelessGoPro` instance such that a call to any -message will block until the camera is ready. It is possible to check these from the application via: +message will block until the camera is ready. They are combined into the following ready state: -- :meth:`~open_gopro.gopro_base.GoProBase.is_encoding` -- :meth:`~open_gopro.gopro_base.GoProBase.is_busy` +- :meth:`~open_gopro.gopro_base.GoProBase.is_ready` For example, .. code-block:: python - with GoPro() as gopro: + async with WirelessGoPro() as gopro: # A naive check for it to be ready - while gopro.is_encoding or gopro.is_ready: + while not await gopro.is_ready: pass To reiterate...it is not needed or recommended to worry about this as the internal state is managed automatically -by the `WirelessGoPro` instance. +by the `WirelessGoPro` instance. Just know that most commands will be (asynchronously) blocked until the camera is ready. Sending Messages ================ @@ -153,25 +168,25 @@ by transport protocol where the superset of message groups are: - WirelessGoPro (WiFi Enabled) - WirelessGoPro (WiFi Disabled) * - :meth:`~open_gopro.gopro_base.GoProBase.http_command` - - |:heavy_check_mark:| - - |:heavy_check_mark:| - - |:x:| + - ✔️ + - ✔️ + - ❌ * - :meth:`~open_gopro.gopro_base.GoProBase.http_setting` - - |:heavy_check_mark:| - - |:heavy_check_mark:| - - |:x:| + - ✔️ + - ✔️ + - ❌ * - :meth:`~open_gopro.gopro_base.GoProBase.ble_command` - - |:x:| - - |:heavy_check_mark:| - - |:heavy_check_mark:| + - ❌ + - ✔️ + - ✔️ * - :meth:`~open_gopro.gopro_base.GoProBase.ble_setting` - - |:x:| - - |:heavy_check_mark:| - - |:heavy_check_mark:| + - ❌ + - ✔️ + - ✔️ * - :meth:`~open_gopro.gopro_base.GoProBase.ble_status` - - |:x:| - - |:heavy_check_mark:| - - |:heavy_check_mark:| + - ❌ + - ✔️ + - ✔️ In the case where a given group of messages is not supported, a `NotImplementedError` will be returned when the relevant property is accessed. @@ -181,6 +196,9 @@ All messages are communicated via one of two strategies: - Performing synchronous :ref:`data operations` to send a message and receive a GoPro Response - Registering for :ref:`asynchronous push notifications` and getting these after they are enqueued +.. note:: For the remainder of this document, the term (a)synchronous is in the context of communication with the camera. + Do not confuse this with `asyncio`: all operations from the user's perspective are awaitable. + Both of these patterns will be expanded upon below. But first, a note on selecting parameters for use with messages... Selecting Parameters @@ -209,7 +227,7 @@ Synchronous Data Operations all messages are synchronous messages. This section refers to sending commands, getting settings / statuses, and setting settings. In all cases here, -the method will block until a :ref:`response` is received. +the method will await until a :ref:`response` is received. Commands ^^^^^^^^ @@ -220,9 +238,12 @@ Commands are callable instance attributes of a Messages class instance .. code-block:: python - with GoPro() as gopro: - gopro.ble_command.set_shutter(Params.Toggle.ENABLE) - gopro.http_command.set_shutter(Params.Toggle.DISABLE) + async with WirelessGoPro() as gopro: + await gopro.ble_command.set_shutter(shutter=Params.Toggle.ENABLE) + await gopro.http_command.set_shutter(shutter=Params.Toggle.DISABLE) + +.. warning:: Most commands specifically require `keyword-only arguments `_. You can + not optionally use positional arguments in such cases as this will affect functionality. Statuses ^^^^^^^^ @@ -232,16 +253,16 @@ synchronously using their `get_value` method as such: .. code-block:: python - with GoPro() as gopro: - gopro.ble_status.encoding_active.get_value() - gopro.ble_status.int_batt_per.get_value() + async with WirelessGoPro() as gopro: + is_encoding = await gopro.ble_status.encoding_active.get_value() + battery = await gopro.ble_status.int_batt_per.get_value() It is also possible to read all statuses at once via: .. code-block:: python - with GoPro() as gopro: - gopro.ble_command.get_camera_statuses() + async with WirelessGoPro() as gopro: + statuses = await gopro.ble_command.get_camera_statuses() .. note:: HTTP can not access individual statuses. Instead it can use @@ -259,16 +280,16 @@ Their values can be read (via BLE only) using the `get_value` method as such: .. code-block:: python - with GoPro() as gopro: - gopro.ble_setting.resolution.get_value() - gopro.ble_setting.video_field_of_view.get_value() + async with WirelessGoPro() as gopro: + resolution = await gopro.ble_setting.resolution.get_value() + fov = await gopro.ble_setting.video_field_of_view.get_value() It is also possible to read all settings at once via: .. code-block:: python - with GoPro() as gopro: - gopro.ble_command.get_camera_settings() + async with WirelessGoPro() as gopro: + settings = await gopro.ble_command.get_camera_settings() .. note:: HTTP can not access individual settings. Instead it can use @@ -280,16 +301,16 @@ the current capabilities for a given setting (via BLE only) using the `get_capab .. code-block:: python - with GoPro() as gopro: - gopro.ble_setting.resolution.get_capabilities_values() + async with WirelessGoPro() as gopro: + capabilities = await gopro.ble_setting.resolution.get_capabilities_values() Settings' values can be set (via either BLE or WiFI) using the `set` method as such: .. code-block:: python - with GoPro() as gopro: - gopro.ble_setting.resolution.set(Params.Resolution.RES_4K) - gopro.http_setting.fps.set(Params.FPS.FPS_30) + async with WirelessGoPro() as gopro: + await gopro.ble_setting.resolution.set(Params.Resolution.RES_4K) + await gopro.http_setting.fps.set(Params.FPS.FPS_30) Asynchronous Push Notifications ------------------------------- @@ -302,11 +323,12 @@ It is possible to enable push notifications for any of the following: - setting capabilities via :meth:`~open_gopro.api.builders.BleSetting.register_capability_update` - status values via :meth:`~open_gopro.api.builders.BleStatus.register_value_update` -Firstly, the desired settings / id must be registered for. +Firstly, the desired settings / ID must be registered for and given a callback to handle received notifications. + +:meth:`~open_gopro.communicator_interface.BaseGoProCommunicator.register_update`. Once registered, the camera will send a push notification when the relevant setting / status changes. These -responses are added to an internal queue of the `GoProBase` instance and can be retrieved via -:meth:`~open_gopro.gopro_wireless.WirelessGoPro.get_notification`. +responses will then be sent to the registered callback for handling. It is possible to stop receiving notifications by issuing the relevant unregister command, i.e.: @@ -318,25 +340,20 @@ Here is an example of registering for and receiving FOV updates: .. code-block:: python - from open_gopro import WirelessGoPro - from open_gopro.constants import SettingId + async def process_battery_notifications(update: types.UpdateType, value: int) -> None: + if update == constants.StatusId.INT_BATT_PER: + ... + elif update == constants.StatusId.BATT_LEVEL: + ... - with WirelessGoPro() as gopro: - current_fov = gopro.ble_setting.video_field_of_view.register_value_update().flatten - print(f"Current FOV is {current_fov}") - # Get updates until we get a FOV update - while True: - update = gopro.get_notification() # Block until update is received - if SettingId.VIDEO_FOV in update: - print(f"New resolution is {update[SettingId.VIDEO_FOV]}") - break - # We don't care about FOV anymore so let's stop receiving notifications - gopro.ble_setting.video_field_of_view.unregister_value_update() - -.. note:: The `register_XXX_update` methods will return the current value / capabilities. That is why we are - printing the current value in the example above. - -.. tip:: It is probably desirable to have a separate thread to retrieve these updates as the demo examples do. + async with WirelessGoPro() as gopro: + await gopro.ble_status.int_batt_per.register_value_update(process_battery_notifications) + await gopro.ble_status.batt_level.register_value_update(process_battery_notifications) + +.. note:: The `register_XXX_update` methods also return the current value / capabilities. + +.. warning:: The coupling of command ID to command is not obvious. This is a temporary solution and will be improved upon + in a future release. It is also possible to register / unregister for **all** settings, statuses, and / or capabilities via one API call using the following commands: @@ -352,34 +369,24 @@ Handling Responses ================== Unless otherwise stated, all commands, settings, and status operations return a `GoProResp` -(:class:`~open_gopro.responses.GoProResp`) which is a container around a JSON serializable dict with some helper +(:class:`~open_gopro.models.response.GoProResp`) which is a container around a JSON serializable dict with some helper functions. Response Structure ------------------ -A `GoProResp` has 3 relevant attributes for the end user: +A `GoProResp` has the following relevant attributes / properties for the end user: -- | :meth:`~open_gopro.responses.GoProResp.identifier`: identifier of the completed operation. +- | :meth:`~open_gopro.models.response.GoProResp.identifier`: identifier of the completed operation. | This will vary based on what type the response is and will also contain the most specific identification information. - UUID if a direct BLE characteristic read - CmdId if an Open GoPro BLE Operation - endpoint string if a Wifi HTTP operation -- :meth:`~open_gopro.responses.GoProResp.status`: the status returned from the camera -- :meth:`~open_gopro.responses.GoProResp.data`: JSON serializable dict containing the responded data - -Besides the `identifier` attribute which always contains the most specific identification information, there are properties -to attempt to access other identification information. If the property is not valid for the given response, -it will return `None`. - -- :meth:`~open_gopro.responses.GoProResp.cmd`. Relevant for any BLE operation. -- :meth:`~open_gopro.responses.GoProResp.uuid`. Relevant for any BLE operation. -- :meth:`~open_gopro.responses.GoProResp.endpoint`. Relevant for any Wifi operation. - -There is also a property to check that the `status` is Success: - -- :meth:`~open_gopro.responses.GoProResp.is_ok` +- :meth:`~open_gopro.models.response.GoProResp.protocol`: the communication protocol where the response was received +- :meth:`~open_gopro.models.response.GoProResp.status`: the status returned from the camera +- :meth:`~open_gopro.models.response.GoProResp.data`: JSON serializable dict containing the responded data +- :meth:`~open_gopro.models.response.GoProResp.ok`: Is this a successful response? The response object can be serialized to a JSON string with the default Python `str()` function. Note that the `identifier` and `status` attributes are appended to the JSON. @@ -388,10 +395,10 @@ For example, first let's connect, send a command, and then store the response: .. code-block:: console - >>> from open_gopro import WirelessGoPro >>> gopro = WirelessGoPro() - >>> gopro.open() - >>> response = gopro.ble_setting.resolution.get_value() + >>> await gopro.open() + >>> response = await (gopro.ble_setting.resolution).get_value() + >>> print(response) Now let's print the response (as JSON): @@ -399,9 +406,12 @@ Now let's print the response (as JSON): >>> print(response) { - "status": "SUCCESS", - "identifier": "UUID.CQ_QUERY_RESP::QueryCmdId.GET_SETTING_VAL", - "SettingId.RESOLUTION": "RES_5_3_K" + "id" : "QueryCmdId.GET_SETTING_VAL", + "status" : "ErrorCode.SUCCESS", + "protocol" : "Protocol.BLE", + "data" : { + "SettingId.RESOLUTION" : "Resolution.RES_4K_16_9", + }, } Now let's inspect the responses various attributes / properties: @@ -410,87 +420,44 @@ Now let's inspect the responses various attributes / properties: >>> print(response.status) ErrorCode.SUCCESS - >>> print(response.is_ok) + >>> print(response.ok) True >>> print(response.identifier) QueryCmdId.GET_SETTING_VAL - >>> print(response.cmd) - QueryCmdId.GET_SETTING_VAL - >>> print(response.uuid) - UUID.CQ_QUERY_RESP - + >>> print(response.protocol) + Protocol.BLE Data Access ----------- -The response data is stored in the `data` attribute (:meth:`~open_gopro.responses.GoProResp.data`) but can also -be accessed via dict access on the instance since `__getitem__` has been overridden. For example, the must -succinct way to access the current resolution from the response is: - -.. code-block:: console +The response data is stored in the `data` attribute (:meth:`~open_gopro.models.response.GoProResp.data`) and its type +is specified via the Generic type specified in the corresponding command signature where the response is defined. - >>> print(response[SettingId.RESOLUTION]) - RES_5_3_K +For example, consider :meth:`~open_gopro.api.ble_commands.BleCommands.get_hardware_info`. It's signature is: -However, it is also possible to this as: - -.. code-block:: console - - >>> print(response.data[SettingId.RESOLUTION]) - RES_5_3_K - -Similarly, `__contains__`, `__keys__`, `__values__`, and `__items__` and `__iter__` have also been overridden to operate on the `data` attribute: - -.. code-block:: console - - >>> SettingId.RESOLUTION in response - True - >>> [str(x) for x in response] - ['SettingId.RESOLUTION'] - -.. note:: The `Open GoPro Documentation `_ should be referenced in regards - to how to access the JSON for each response. - -Value Flattening ----------------- - -For short responses, it is rather unwieldy to access the JSON dict as described above. Therefore, you can attempt to use the -`flatten` property (:meth:`~open_gopro.responses.GoProResp.flatten`) to flatten the data: - -Continuing with our example above, where previously we accessed the responded resolution as: +.. code-block:: python -.. code-block:: console + async def get_hardware_info(self) -> GoProResp[CameraInfo]: + ... - >>> print(response[SettingId.RESOLUTION]) - RES_5_3_K +Therefore, its response's `data` property is of type :meth:`~open_gopro.models.general.CameraInfo`. Continuing the +example from above: -We can also do it as: .. code-block:: console - >>> print(response.flatten) - RES_5_3_K - -For example, we can get and print all resolution capabilities on one line via: - - >>> print(", ".join(gopro.ble_setting.resolution.get_capabilities_values().flatten)) - RES_4K, RES_2_7K, RES_2_7K_4_3, RES_1080, RES_4K_4_3, RES_5_K_4_3, RES_5_3_K - -If the response data is anything other than a single value or a list, it can't be flattened and so the entire -data structure will be returned. - -Flattening works well when getting a single value (from a get status / value) or a list of values (from a get -capabilities). This won't work for many cases. - -For complex JSON structures, you will need to read through the -`Open GoPro API Documentation `_ for -parsing it. There will be some future work to turn these (at least the media list) into nice Python classes. But -for now, it will look ugly like this: - -.. code-block:: python - - # Get list of media - gopro.media_list = http_command.get_media_list().data["media"][0]["fs"] + >>> gopro = WirelessGoPro(enable_wifi=False) + >>> await gopro.open() + >>> response = await gopro.ble_command.get_hardware_info() + >>> print(response.data) + { + "model_number" : "62", + "model_name" : "HERO12 Black", + "firmware_version" : "H23.01.01.09.67", + "serial_number" : "C3501324500702", + "ap_mac_addr" : "2674f7f66104", + "ap_ssid" : "GP24500702", + } Closing ======= @@ -511,10 +478,10 @@ Otherwise, you will need to manually call the `close` method, i.e.: .. code-block:: python gopro = WirelessGoPro() - gopro.open() + await gopro.open() print("Yay! I'm connected via BLE, Wifi, opened, and ready to send / get data now!") # When we're done... - gopro.close() + await gopro.close() # The camera resource is closed now!! The `close` method will handle gracefully disconnecting BLE and Wifi. diff --git a/demos/python/sdk_wireless_camera_control/noxfile.py b/demos/python/sdk_wireless_camera_control/noxfile.py index 8061ac83..73b1e430 100644 --- a/demos/python/sdk_wireless_camera_control/noxfile.py +++ b/demos/python/sdk_wireless_camera_control/noxfile.py @@ -11,7 +11,7 @@ SUPPORTED_VERSIONS = [ "3.9", "3.10", - # "3.11", + "3.11", ] @@ -22,7 +22,8 @@ def format(session) -> None: session.run("black", "--check", "open_gopro", "tests", "noxfile.py", "docs/conf.py") -@session(python=SUPPORTED_VERSIONS) +# Mypy is changing too much between versions. Let's only lint on the latest version +@session(python=SUPPORTED_VERSIONS[-1]) def lint(session) -> None: """Lint using pylint and check types with mypy.""" session.install(".[gui]") @@ -33,6 +34,8 @@ def lint(session) -> None: "mypy-protobuf", "types-requests", "types-attrs", + "types-pytz", + "types-tzlocal", ) session.run("mypy", "open_gopro") session.run("pylint", "--no-docstring-rgx=__|main|parse_arguments|entrypoint", "open_gopro") @@ -72,11 +75,9 @@ def docs(session) -> None: "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-napoleon", + "autodoc-pydantic", "darglint", - "sphinxemoji", ) session.run("sphinx-build", "docs", "docs/build") # Clean up for Jekyll consumption - session.run( - "rm", "-rf", "docs/build/.doctrees", "/docs/build/_sources", "/docs/build/_static/fonts", external=True - ) + session.run("rm", "-rf", "docs/build/.doctrees", "/docs/build/_sources", "/docs/build/_static/fonts", external=True) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/__init__.py index 9b1838b7..a3cc6bb7 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/__init__.py @@ -8,16 +8,17 @@ import sys # Validate python version -if sys.version_info.major != 3 or not 9 <= sys.version_info.minor < 11: - raise RuntimeError("Python >= 3.9 and < 3.11 must be used") +# This is to make it painfully clear so that people hopefully stop trying invalid versions +if sys.version_info.major != 3 or not 9 <= sys.version_info.minor < 12: + raise RuntimeError("Python >= 3.9 and < 3.12 must be used") import logging -from open_gopro.util import Logger +from open_gopro.logger import Logger Logger.addLoggingLevel("TRACE", logging.DEBUG - 5) -from open_gopro.gopro_wireless import WirelessGoPro -from open_gopro.gopro_wired import WiredGoPro from open_gopro.api import Params -from open_gopro.responses import GoProResp +from open_gopro.gopro_wired import WiredGoPro +from open_gopro.gopro_wireless import WirelessGoPro +from open_gopro.models import GoProResp diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/api/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/api/__init__.py index b61121af..863a6ce2 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/api/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/__init__.py @@ -3,22 +3,22 @@ """Top level API module definition""" +from . import params as Params from .api import WiredApi, WirelessApi from .ble_commands import BleCommands, BleSettings, BleStatuses -from .http_commands import HttpCommands, HttpSettings from .builders import ( - BleSetting, - BleStatus, - BleReadCommand, BleAsyncResponse, BleProtoCommand, + BleReadCommand, + BleSetting, + BleStatus, BleWriteCommand, - RegisterUnregisterAll, - HttpSetting, HttpGetBinary, HttpGetJsonCommand, + HttpSetting, + RegisterUnregisterAll, ) -from . import params as Params +from .http_commands import HttpCommands, HttpSettings # TODO find a better way to set up parsers, etc besides instantiating diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/api/api.py b/demos/python/sdk_wireless_camera_control/open_gopro/api/api.py index 702d7673..563ee681 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/api/api.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/api.py @@ -5,8 +5,9 @@ from typing import Final -from open_gopro.interface import GoProWirelessInterface, GoProHttp -from .ble_commands import BleCommands, BleSettings, BleStatuses, BleAsyncResponses +from open_gopro.communicator_interface import GoProHttp, GoProWirelessInterface + +from .ble_commands import BleAsyncResponses, BleCommands, BleSettings, BleStatuses from .http_commands import HttpCommands, HttpSettings diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/api/ble_commands.py b/demos/python/sdk_wireless_camera_control/open_gopro/api/ble_commands.py index b021f72c..73311fe5 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/api/ble_commands.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/ble_commands.py @@ -6,111 +6,63 @@ # mypy: disable-error-code=empty-body from __future__ import annotations -import logging + import datetime -from typing import Optional, Final, Callable +import logging +from pathlib import Path +from typing import Any, Final from construct import ( - Int8ub, - Int32ub, - Int64ub, Flag, GreedyBytes, GreedyString, - Struct, - Padding, - PaddedString, - this, Hex, + Int8ub, Int16ub, - Int16sb, + Int32ub, + Int64ub, + PaddedString, + Padding, + Struct, + this, ) -from open_gopro import proto -from open_gopro.responses import GoProResp, BytesParser, BytesBuilder, JsonParser -from open_gopro.interface import GoProBle, BleMessage, BleMessages, MessageRules +from open_gopro import proto, types from open_gopro.api.builders import ( - DeprecatedAdapter, - BleStatus, - BleSetting, BleAsyncResponse, + BleSetting, + BleStatus, RegisterUnregisterAll, + ble_proto_command, + ble_read_command, ble_register_command, ble_write_command, - ble_read_command, - ble_proto_command, ) -from open_gopro.constants import ActionId, FeatureId, CmdId, QueryCmdId, SettingId, StatusId, GoProUUIDs -from open_gopro.util import map_keys +from open_gopro.api.parsers import ByteParserBuilders, JsonParsers +from open_gopro.communicator_interface import ( + BleMessage, + BleMessages, + GoProBle, + MessageRules, +) +from open_gopro.constants import ( + ActionId, + CmdId, + FeatureId, + GoProUUIDs, + QueryCmdId, + SettingId, + StatusId, +) +from open_gopro.models import CameraInfo, TzDstDateTime +from open_gopro.models.response import GlobalParsers, GoProResp +from open_gopro.parser_interface import Parser +from open_gopro.types import CameraState + from . import params as Params logger = logging.getLogger(__name__) -# pylint: disable = arguments-renamed -class BleParserBuilders: - """The collection of custom (i.e. not-construct) parsers and / or builders""" - - class DateTime(BytesParser, BytesBuilder): - """Handle local and non-local datetime parsing / building""" - - def build( - self, dt: datetime.datetime, tzone: Optional[int] = None, is_dst: Optional[bool] = None - ) -> bytes: - """Build bytestream from datetime and optional local arguments - - Args: - dt (datetime.datetime): date and time - tzone (Optional[int], optional): timezone (as UTC offset). Defaults to None. - is_dst (Optional[bool], optional): is daylight savings time?. Defaults to None. - - Returns: - bytes: bytestream built from datetime - """ - byte_data = [*Int16ub.build(dt.year), dt.month, dt.day, dt.hour, dt.minute, dt.second] - if tzone and is_dst: - byte_data.extend([*Int16sb.build(tzone), *Flag.build(is_dst)]) - return bytes(byte_data) - - def parse(self, data: bytes) -> dict: - """Parse bytestream into dict of datetime and potential timezone / dst - - Args: - data (bytes): bytestream to parse - - Returns: - dict: dict containing datetime - """ - is_dst_tz = len(data) == 9 - buf = data[1:] - year = Int16ub.parse(buf[0:2]) - - dt = datetime.datetime(year, *[int(x) for x in buf[2:7]]) # type: ignore - return ( - {"datetime": dt} - if is_dst_tz - else {"datetime": dt, "tzone": Int16sb.parse(buf[7:9]), "dst": bool(buf[9])} - ) - - class MapKey(JsonParser): - """Map all matching keys using the input function""" - - def __init__(self, key: str, func: Callable) -> None: - self.key = key - self.func = func - super().__init__() - - def parse(self, data: dict) -> dict: - """Use the map key function to transform the dict - - Args: - data (dict): input json - - Returns: - dict: json with keys mapped - """ - map_keys(data, self.key, self.func) - return data - class BleCommands(BleMessages[BleMessage, CmdId]): """All of the BLE commands. @@ -123,15 +75,15 @@ class BleCommands(BleMessages[BleMessage, CmdId]): ###################################################################################################### @ble_write_command( - GoProUUIDs.CQ_COMMAND, - CmdId.SET_SHUTTER, - Int8ub, + uuid=GoProUUIDs.CQ_COMMAND, + cmd=CmdId.SET_SHUTTER, + param_builder=Int8ub, rules={ MessageRules.FASTPASS: lambda **kwargs: kwargs["shutter"] == Params.Toggle.DISABLE, MessageRules.WAIT_FOR_ENCODING_START: lambda **kwargs: kwargs["shutter"] == Params.Toggle.ENABLE, }, ) - def set_shutter(self, *, shutter: Params.Toggle) -> GoProResp: + async def set_shutter(self, *, shutter: Params.Toggle) -> GoProResp[None]: """Set the Shutter to start / stop encoding Args: @@ -142,50 +94,55 @@ def set_shutter(self, *, shutter: Params.Toggle) -> GoProResp: """ @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.TAG_HILIGHT) - def tag_hilight(self) -> GoProResp: + async def tag_hilight(self) -> GoProResp[None]: """Tag a highlight during encoding Returns: - GoProResp: response as JSON + GoProResp: status of command """ @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.POWER_DOWN) - def power_down(self) -> GoProResp: + async def power_down(self) -> GoProResp[None]: """Power Down the camera Returns: - GoProResp: response as JSON + GoProResp: status of command """ @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.SLEEP) - def sleep(self) -> GoProResp: + async def sleep(self) -> GoProResp[None]: """Put the camera in standby Returns: - GoProResp: response as JSON + GoProResp: status of command """ @ble_write_command( - GoProUUIDs.CQ_COMMAND, - CmdId.GET_HW_INFO, - parser=Struct( - Padding(1), - "model_number" / Int32ub, - "model_name_len" / Int8ub, - "model_name" / PaddedString(this.model_name_len, "utf-8"), - Padding(1), - "board_type" / Hex(Int32ub), - "firmware_version_len" / Int8ub, - "firmware_version" / PaddedString(this.firmware_version_len, "utf-8"), - "serial_number_len" / Int8ub, - "serial_number" / PaddedString(this.serial_number_len, "utf-8"), - "ap_ssid_len" / Int8ub, - "ap_ssid" / PaddedString(this.ap_ssid_len, "utf-8"), - "ap_mac_len" / Int8ub, - "ap_mac" / PaddedString(this.ap_mac_len, "utf-8"), + uuid=GoProUUIDs.CQ_COMMAND, + cmd=CmdId.GET_HW_INFO, + parser=Parser( + byte_json_adapter=ByteParserBuilders.Construct( + Struct( + Padding(1), + "model_number" / Int32ub, + "model_name_len" / Int8ub, + "model_name" / PaddedString(this.model_name_len, "utf-8"), + Padding(1), + "board_type" / Hex(Int32ub), + "firmware_version_len" / Int8ub, + "firmware_version" / PaddedString(this.firmware_version_len, "utf-8"), + "serial_number_len" / Int8ub, + "serial_number" / PaddedString(this.serial_number_len, "utf-8"), + "ap_ssid_len" / Int8ub, + "ap_ssid" / PaddedString(this.ap_ssid_len, "utf-8"), + "ap_mac_len" / Int8ub, + "ap_mac_addr" / PaddedString(this.ap_mac_len, "utf-8"), + ) + ), + json_parser=JsonParsers.PydanticAdapter(CameraInfo), ), ) - def get_hardware_info(self) -> GoProResp: + async def get_hardware_info(self) -> GoProResp[CameraInfo]: """Get the model number, board, type, firmware version, serial number, and AP info Returns: @@ -193,7 +150,7 @@ def get_hardware_info(self) -> GoProResp: """ @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.SET_WIFI, Int8ub) - def enable_wifi_ap(self, *, enable: bool) -> GoProResp: + async def enable_wifi_ap(self, *, enable: bool) -> GoProResp[None]: """Enable / disable the Wi-Fi Access Point. Args: @@ -204,20 +161,20 @@ def enable_wifi_ap(self, *, enable: bool) -> GoProResp: """ @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.LOAD_PRESET_GROUP, Int16ub) - def load_preset_group(self, *, group: Params.PresetGroup) -> GoProResp: + async def load_preset_group(self, *, group: proto.EnumPresetGroup) -> GoProResp[None]: """Load a Preset Group. Once complete, the most recently used preset in this group will be active. Args: - group (open_gopro.api.params.PresetGroup): preset group to load + group (open_gopro.api.proto.EnumPresetGroup): preset group to load Returns: GoProResp: response as JSON """ @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.LOAD_PRESET, Int32ub) - def load_preset(self, *, preset: int) -> GoProResp: + async def load_preset(self, *, preset: int) -> GoProResp[None]: """Load a Preset The integer preset value can be found from the get_preset_status command @@ -230,7 +187,7 @@ def load_preset(self, *, preset: int) -> GoProResp: """ @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.SET_THIRD_PARTY_CLIENT_INFO) - def set_third_party_client_info(self) -> GoProResp: + async def set_third_party_client_info(self) -> GoProResp[None]: """Flag as third party app Returns: @@ -238,43 +195,60 @@ def set_third_party_client_info(self) -> GoProResp: """ @ble_write_command( - GoProUUIDs.CQ_COMMAND, - CmdId.GET_THIRD_PARTY_API_VERSION, - parser=Struct(Padding(1), "major" / Int8ub, Padding(1), "minor" / Int8ub), + uuid=GoProUUIDs.CQ_COMMAND, + cmd=CmdId.GET_THIRD_PARTY_API_VERSION, + parser=Parser( + byte_json_adapter=ByteParserBuilders.Construct( + Struct(Padding(1), "major" / Int8ub, Padding(1), "minor" / Int8ub) + ), + json_parser=JsonParsers.LambdaParser(lambda data: f"{data['major']}.{data['minor']}"), + ), ) - def get_open_gopro_api_version(self) -> GoProResp: + async def get_open_gopro_api_version(self) -> GoProResp[str]: """Get Open GoPro API Version Returns: GoProResp: response as JSON """ - @ble_write_command(GoProUUIDs.CQ_QUERY, CmdId.GET_CAMERA_STATUSES) - def get_camera_statuses(self) -> GoProResp: + @ble_write_command( + GoProUUIDs.CQ_QUERY, + CmdId.GET_CAMERA_STATUSES, + parser=Parser(json_parser=JsonParsers.CameraStateParser()), + ) + async def get_camera_statuses(self) -> GoProResp[CameraState]: """Get all of the camera's statuses Returns: GoProResp: response as JSON """ - @ble_write_command(GoProUUIDs.CQ_QUERY, CmdId.GET_CAMERA_SETTINGS) - def get_camera_settings(self) -> GoProResp: + @ble_write_command( + GoProUUIDs.CQ_QUERY, + CmdId.GET_CAMERA_SETTINGS, + parser=Parser(json_parser=JsonParsers.CameraStateParser()), + ) + async def get_camera_settings(self) -> GoProResp[CameraState]: """Get all of the camera's settings Returns: GoProResp: response as JSON """ - @ble_write_command(GoProUUIDs.CQ_QUERY, CmdId.GET_CAMERA_CAPABILITIES) - def get_camera_capabilities(self) -> GoProResp: + @ble_write_command( + GoProUUIDs.CQ_QUERY, + CmdId.GET_CAMERA_CAPABILITIES, + parser=Parser(json_parser=JsonParsers.CameraStateParser()), + ) + async def get_camera_capabilities(self) -> GoProResp[CameraState]: """Get the current capabilities of each camera setting Returns: GoProResp: response as JSON """ - @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.SET_DATE_TIME, param_builder=BleParserBuilders.DateTime()) - def set_date_time(self, *, date_time: datetime.datetime) -> GoProResp: + @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.SET_DATE_TIME, param_builder=ByteParserBuilders.DateTime()) + async def set_date_time(self, *, date_time: datetime.datetime) -> GoProResp[None]: """Set the camera's date and time (non timezone / DST version) Args: @@ -284,18 +258,25 @@ def set_date_time(self, *, date_time: datetime.datetime) -> GoProResp: GoProResp: command status """ - @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.GET_DATE_TIME, parser=BleParserBuilders.DateTime()) - def get_date_time(self) -> GoProResp: + @ble_write_command( + GoProUUIDs.CQ_COMMAND, + CmdId.GET_DATE_TIME, + parser=Parser( + byte_json_adapter=ByteParserBuilders.DateTime(), + json_parser=JsonParsers.LambdaParser(lambda data: data["datetime"]), + ), + ) + async def get_date_time(self) -> GoProResp[datetime.datetime]: """Get the camera's date and time (non timezone / DST version) Returns: GoProResp: response as JSON """ - @ble_write_command( - GoProUUIDs.CQ_COMMAND, CmdId.SET_DATE_TIME_DST, param_builder=BleParserBuilders.DateTime() - ) - def set_date_time_tz_dst(self, *, date_time: datetime.datetime, tz_offset: int, is_dst: bool) -> GoProResp: + @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.SET_DATE_TIME_DST, param_builder=ByteParserBuilders.DateTime()) + async def set_date_time_tz_dst( + self, *, date_time: datetime.datetime, tz_offset: int, is_dst: bool + ) -> GoProResp[None]: """Set the camera's date and time with timezone and DST Args: @@ -307,8 +288,15 @@ def set_date_time_tz_dst(self, *, date_time: datetime.datetime, tz_offset: int, GoProResp: command status """ - @ble_write_command(GoProUUIDs.CQ_COMMAND, CmdId.GET_DATE_TIME_DST, parser=BleParserBuilders.DateTime()) - def get_date_time_tz_dst(self) -> GoProResp: + @ble_write_command( + GoProUUIDs.CQ_COMMAND, + CmdId.GET_DATE_TIME_DST, + parser=Parser( + byte_json_adapter=ByteParserBuilders.DateTime(), + json_parser=JsonParsers.PydanticAdapter(TzDstDateTime), + ), + ) + async def get_date_time_tz_dst(self) -> GoProResp[TzDstDateTime]: """Get the camera's date and time with timezone / DST Returns: @@ -319,16 +307,28 @@ def get_date_time_tz_dst(self) -> GoProResp: # BLE DIRECT CHARACTERISTIC READ COMMANDS ###################################################################################################### - @ble_read_command(GoProUUIDs.WAP_SSID, Struct("ssid" / GreedyString("utf-8"))) - def get_wifi_ssid(self) -> GoProResp: + @ble_read_command( + uuid=GoProUUIDs.WAP_SSID, + parser=Parser( + byte_json_adapter=ByteParserBuilders.Construct(Struct("ssid" / GreedyString("utf-8"))), + json_parser=JsonParsers.LambdaParser(lambda data: data["ssid"]), + ), + ) + async def get_wifi_ssid(self) -> GoProResp[str]: """Get the Wifi SSID. Returns: GoProResp: command status and SSID """ - @ble_read_command(GoProUUIDs.WAP_PASSWORD, Struct("password" / GreedyString("utf-8"))) - def get_wifi_password(self) -> GoProResp: + @ble_read_command( + uuid=GoProUUIDs.WAP_PASSWORD, + parser=Parser( + byte_json_adapter=ByteParserBuilders.Construct(Struct("password" / GreedyString("utf-8"))), + json_parser=JsonParsers.LambdaParser(lambda data: data["password"]), + ), + ) + async def get_wifi_password(self) -> GoProResp[str]: """Get the Wifi password. Returns: @@ -342,12 +342,16 @@ def get_wifi_password(self) -> GoProResp: @ble_register_command( GoProUUIDs.CQ_QUERY, CmdId.REGISTER_ALL_STATUSES, - producer=(StatusId, QueryCmdId.STATUS_VAL_PUSH), + update_set=StatusId, + responded_cmd=QueryCmdId.STATUS_VAL_PUSH, # TODO probably remove this action=RegisterUnregisterAll.Action.REGISTER, ) - def register_for_all_statuses(self) -> GoProResp: + async def register_for_all_statuses(self, callback: types.UpdateCb) -> GoProResp[None]: """Register push notifications for all statuses + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: command status and current value of all statuses """ @@ -355,12 +359,16 @@ def register_for_all_statuses(self) -> GoProResp: @ble_register_command( GoProUUIDs.CQ_QUERY, CmdId.UNREGISTER_ALL_STATUSES, - producer=(StatusId, QueryCmdId.STATUS_VAL_PUSH), + update_set=StatusId, + responded_cmd=QueryCmdId.STATUS_VAL_PUSH, action=RegisterUnregisterAll.Action.UNREGISTER, ) - def unregister_for_all_statuses(self) -> GoProResp: + async def unregister_for_all_statuses(self, callback: types.UpdateCb) -> GoProResp[None]: """Unregister push notifications for all statuses + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: command status """ @@ -368,12 +376,16 @@ def unregister_for_all_statuses(self) -> GoProResp: @ble_register_command( GoProUUIDs.CQ_QUERY, CmdId.REGISTER_ALL_SETTINGS, - producer=(SettingId, QueryCmdId.SETTING_VAL_PUSH), + update_set=SettingId, + responded_cmd=QueryCmdId.SETTING_VAL_PUSH, action=RegisterUnregisterAll.Action.REGISTER, ) - def register_for_all_settings(self) -> GoProResp: + async def register_for_all_settings(self, callback: types.UpdateCb) -> GoProResp[None]: """Register push notifications for all settings + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: command status and current value of all settings """ @@ -381,12 +393,16 @@ def register_for_all_settings(self) -> GoProResp: @ble_register_command( GoProUUIDs.CQ_QUERY, CmdId.UNREGISTER_ALL_SETTINGS, - producer=(SettingId, QueryCmdId.SETTING_VAL_PUSH), + update_set=SettingId, + responded_cmd=QueryCmdId.SETTING_VAL_PUSH, action=RegisterUnregisterAll.Action.UNREGISTER, ) - def unregister_for_all_settings(self) -> GoProResp: + async def unregister_for_all_settings(self, callback: types.UpdateCb) -> GoProResp[None]: """Unregister push notifications for all settings + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: command status """ @@ -394,12 +410,16 @@ def unregister_for_all_settings(self) -> GoProResp: @ble_register_command( GoProUUIDs.CQ_QUERY, CmdId.REGISTER_ALL_CAPABILITIES, - producer=(SettingId, QueryCmdId.SETTING_CAPABILITY_PUSH), + update_set=SettingId, + responded_cmd=QueryCmdId.SETTING_CAPABILITY_PUSH, action=RegisterUnregisterAll.Action.REGISTER, ) - def register_for_all_capabilities(self) -> GoProResp: + async def register_for_all_capabilities(self, callback: types.UpdateCb) -> GoProResp[None]: """Register push notifications for all capabilities + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: command status and current value of all capabilities """ @@ -407,12 +427,16 @@ def register_for_all_capabilities(self) -> GoProResp: @ble_register_command( GoProUUIDs.CQ_QUERY, CmdId.UNREGISTER_ALL_CAPABILITIES, - producer=(SettingId, QueryCmdId.SETTING_CAPABILITY_PUSH), + update_set=SettingId, + responded_cmd=QueryCmdId.SETTING_CAPABILITY_PUSH, action=RegisterUnregisterAll.Action.UNREGISTER, ) - def unregister_for_all_capabilities(self) -> GoProResp: + async def unregister_for_all_capabilities(self, callback: types.UpdateCb) -> GoProResp[None]: """Unregister push notifications for all capabilities + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: command status """ @@ -429,11 +453,11 @@ def unregister_for_all_capabilities(self) -> GoProResp: request_proto=proto.RequestSetCameraControlStatus, response_proto=proto.ResponseGeneric, ) - def set_camera_control(self, *, camera_control_status: Params.CameraControlStatus) -> GoProResp: + async def set_camera_control(self, *, camera_control_status: proto.EnumCameraControlStatus) -> GoProResp[None]: """Tell the camera that the app (i.e. External Control) wishes to claim control of the camera. Args: - camera_control_status (open_gopro.api.params.CameraControlStatus): Desired camera control. + camera_control_status (open_gopro.api.proto.EnumCameraControlStatus): Desired camera control. Returns: GoProResp: command status of request @@ -447,7 +471,7 @@ def set_camera_control(self, *, camera_control_status: Params.CameraControlStatu request_proto=proto.RequestSetTurboActive, response_proto=proto.ResponseGeneric, ) - def set_turbo_mode(self, *, mode: Params.Toggle) -> GoProResp: + async def set_turbo_mode(self, *, mode: Params.Toggle) -> GoProResp[None]: """Enable / disable turbo mode. Args: @@ -467,21 +491,21 @@ def set_turbo_mode(self, *, mode: Params.Toggle) -> GoProResp: response_proto=proto.NotifyPresetStatus, additional_matching_ids={ActionId.PRESET_MODIFIED_NOTIFICATION}, ) - def get_preset_status( + async def get_preset_status( self, *, - register: Optional[list[Params.RegisterPreset]] = None, - unregister: Optional[list[Params.RegisterPreset]] = None, - ) -> GoProResp: + register: list[proto.EnumRegisterPresetStatus] | None = None, + unregister: list[proto.EnumRegisterPresetStatus] | None = None, + ) -> GoProResp[proto.NotifyPresetStatus]: """Get information about what Preset Groups and Presets the camera supports in its current state Also optionally (un)register for preset / group preset modified notifications which will be sent asynchronously as :py:attr:`open_gopro.constants.ActionId.PRESET_MODIFIED_NOTIFICATION` Args: - register (list[open_gopro.api.params.RegisterPreset], Optional): Types of preset modified + register (list[open_gopro.api.proto.EnumRegisterPresetStatus], Optional): Types of preset modified updates to register for. Defaults to None. - unregister (list[open_gopro.api.params.RegisterPreset], Optional): Types of preset modified + unregister (list[open_gopro.api.proto.EnumRegisterPresetStatus], Optional): Types of preset modified updates to unregister for. Defaults to None. Returns: @@ -500,7 +524,7 @@ def get_preset_status( request_proto=proto.RequestStartScan, response_proto=proto.ResponseStartScanning, ) - def scan_wifi_networks(self) -> GoProResp: + async def scan_wifi_networks(self) -> GoProResp[proto.ResponseStartScanning]: """Scan for Wifi networks Returns: @@ -514,9 +538,10 @@ def scan_wifi_networks(self) -> GoProResp: response_action_id=ActionId.GET_AP_ENTRIES_RSP, request_proto=proto.RequestGetApEntries, response_proto=proto.ResponseGetApEntries, - additional_parsers=[BleParserBuilders.MapKey("scan_entry_flags", lambda x: Params.ScanEntry(x))], ) - def get_ap_entries(self, *, scan_id: int, start_index: int = 0, max_entries: int = 100) -> GoProResp: + async def get_ap_entries( + self, *, scan_id: int, start_index: int = 0, max_entries: int = 100 + ) -> GoProResp[proto.ResponseGetApEntries]: """Get the results of a scan for wifi networks Args: @@ -538,7 +563,7 @@ def get_ap_entries(self, *, scan_id: int, start_index: int = 0, max_entries: int response_proto=proto.ResponseConnect, additional_matching_ids={ActionId.REQUEST_WIFI_CONNECT_RSP}, ) - def request_wifi_connect(self, *, ssid: str) -> GoProResp: + async def request_wifi_connect(self, *, ssid: str) -> GoProResp[proto.ResponseConnect]: """Request the camera to connect to a WiFi network that is already provisioned. Updates will be sent as :py:attr:`open_gopro.constants.ActionId.NOTIF_PROVIS_STATE` @@ -559,7 +584,7 @@ def request_wifi_connect(self, *, ssid: str) -> GoProResp: response_proto=proto.ResponseConnectNew, additional_matching_ids={ActionId.REQUEST_WIFI_CONNECT_NEW_RSP}, ) - def request_wifi_connect_new(self, *, ssid: str, password: str) -> GoProResp: + async def request_wifi_connect_new(self, *, ssid: str, password: str) -> GoProResp[proto.ResponseConnectNew]: """Request the camera to connect to a WiFi network that is not already provisioned. Updates will be sent as :py:attr:`open_gopro.constants.ActionId.NOTIF_PROVIS_STATE` @@ -580,41 +605,49 @@ def request_wifi_connect_new(self, *, ssid: str, password: str) -> GoProResp: request_proto=proto.RequestSetLiveStreamMode, response_proto=proto.ResponseGeneric, ) - def set_livestream_mode( + async def set_livestream_mode( self, *, url: str, - window_size: Params.WindowSize, - cert: bytes, + window_size: proto.EnumWindowSize, minimum_bitrate: int, maximum_bitrate: int, starting_bitrate: int, - lens: Params.LensType, - ) -> GoProResp: + lens: proto.EnumLens, + certs: list[Path] | None = None, + ) -> GoProResp[None]: """Initiate livestream to any site that accepts an RTMP URL and simultaneously encode to camera. Args: url (str): url used to stream. Set to empty string to invalidate/cancel stream - window_size (open_gopro.api.params.WindowSize): Streaming video resolution - cert (bytes): Certificate from a trusted root for streaming services that use encryption + window_size (open_gopro.api.proto.EnumWindowSize): Streaming video resolution minimum_bitrate (int): Desired minimum streaming bitrate (>= 800) maximum_bitrate (int): Desired maximum streaming bitrate (<= 8000) starting_bitrate (int): Initial streaming bitrate (honored if 800 <= value <= 8000) - lens (open_gopro.api.params.LensType): Streaming Field of View + lens (open_gopro.api.proto.EnumLens): Streaming Field of View + certs (list[Path] | None): list of certificates to use. Defaults to None. Returns: GoProResp: command status of request """ - return { # type: ignore + d = { "url": url, "encode": True, "window_size": window_size, - "cert": cert, "minimum_bitrate": minimum_bitrate, "maximum_bitrate": maximum_bitrate, "starting_bitrate": starting_bitrate, "lens": lens, } + if certs: + cert_buf = bytearray() + for cert in certs: + with open(cert, "rb") as fp: + cert_buf += bytearray(fp.read()) + "\n".encode() + + cert_buf.pop() + d["cert"] = bytes(cert_buf) + return d # type: ignore @ble_proto_command( uuid=GoProUUIDs.CQ_QUERY, @@ -625,18 +658,18 @@ def set_livestream_mode( response_proto=proto.NotifyLiveStreamStatus, additional_matching_ids={ActionId.LIVESTREAM_STATUS_NOTIF}, ) - def register_livestream_status( + async def register_livestream_status( self, *, - register: Optional[list[Params.RegisterLiveStream]] = None, - unregister: Optional[list[Params.RegisterLiveStream]] = None, - ) -> GoProResp: + register: list[proto.EnumRegisterLiveStreamStatus] | None = None, + unregister: list[proto.EnumRegisterLiveStreamStatus] | None = None, + ) -> GoProResp[proto.NotifyLiveStreamStatus]: """Register / unregister to receive asynchronous livestream statuses Args: - register (Optional[list[open_gopro.api.params.RegisterLiveStream]]): Statuses to register + register (Optional[list[open_gopro.api.proto.EnumRegisterLiveStreamStatus]]): Statuses to register for. Defaults to None (don't register for any). - unregister (Optional[list[open_gopro.api.params.RegisterLiveStream]]): Statues to + unregister (Optional[list[open_gopro.api.proto.EnumRegisterLiveStreamStatus]]): Statues to unregister for. Defaults to None (don't unregister for any). Returns: @@ -644,21 +677,6 @@ def register_livestream_status( """ return {"register_live_stream_status": register or [], "unregister_live_stream_status": unregister or []} # type: ignore - @ble_proto_command( - uuid=GoProUUIDs.CQ_COMMAND, - feature_id=FeatureId.COMMAND, - action_id=ActionId.RELEASE_NETWORK, - response_action_id=ActionId.RELEASE_NETWORK_RSP, - request_proto=proto.RequestReleaseNetwork, - response_proto=proto.ResponseGeneric, - ) - def release_network(self) -> GoProResp: - """Disconnect the camera Wifi network in STA mode so that it returns to AP mode. - - Returns: - GoProResp: status of release request - """ - class BleSettings(BleMessages[BleSetting, SettingId]): # pylint: disable=missing-class-docstring, unused-argument @@ -789,28 +807,152 @@ def __init__(self, communicator: GoProBle): ) """Lock / unlock horizon leveling for photo.""" + self.bit_rate: BleSetting[Params.BitRate] = BleSetting[Params.BitRate]( + communicator, + SettingId.BIT_RATE, + Params.BitRate, + ) + """System Video Bit Rate.""" + + self.bit_depth: BleSetting[Params.BitDepth] = BleSetting[Params.BitDepth]( + communicator, + SettingId.BIT_DEPTH, + Params.BitDepth, + ) + """System Video Bit depth.""" + + self.video_profile: BleSetting[Params.VideoProfile] = BleSetting[Params.VideoProfile]( + communicator, + SettingId.VIDEO_PROFILE, + Params.VideoProfile, + ) + """Video Profile (hdr, etc.)""" + + self.video_aspect_ratio: BleSetting[Params.VideoAspectRatio] = BleSetting[Params.VideoAspectRatio]( + communicator, + SettingId.VIDEO_ASPECT_RATIO, + Params.VideoAspectRatio, + ) + """Video aspect ratio""" + + self.video_easy_aspect_ratio: BleSetting[Params.EasyAspectRatio] = BleSetting[Params.EasyAspectRatio]( + communicator, + SettingId.VIDEO_EASY_ASPECT_RATIO, + Params.EasyAspectRatio, + ) + """Video easy aspect ratio""" + + self.multi_shot_easy_aspect_ratio: BleSetting[Params.EasyAspectRatio] = BleSetting[Params.EasyAspectRatio]( + communicator, + SettingId.MULTI_SHOT_EASY_ASPECT_RATIO, + Params.EasyAspectRatio, + ) + """Multi shot easy aspect ratio""" + + self.multi_shot_nlv_aspect_ratio: BleSetting[Params.EasyAspectRatio] = BleSetting[Params.EasyAspectRatio]( + communicator, + SettingId.MULTI_SHOT_NLV_ASPECT_RATIO, + Params.EasyAspectRatio, + ) + """Multi shot NLV aspect ratio""" + + self.video_mode: BleSetting[Params.VideoMode] = BleSetting[Params.VideoMode]( + communicator, + SettingId.VIDEO_MODE, + Params.VideoMode, + ) + """Video Mode (i.e. quality)""" + + self.timelapse_mode: BleSetting[Params.TimelapseMode] = BleSetting[Params.TimelapseMode]( + communicator, + SettingId.TIMELAPSE_MODE, + Params.TimelapseMode, + ) + """Timelapse Mode""" + + self.maxlens_mod_type: BleSetting[Params.MaxLensModType] = BleSetting[Params.MaxLensModType]( + communicator, + SettingId.ADDON_MAX_LENS_MOD, + Params.MaxLensModType, + ) + """Max lens mod? If so, what type?""" + + self.maxlens_status: BleSetting[Params.Toggle] = BleSetting[Params.Toggle]( + communicator, + SettingId.ADDON_MAX_LENS_MOD_ENABLE, + Params.Toggle, + ) + """Enable / disable max lens mod""" + + self.photo_mode: BleSetting[Params.PhotoMode] = BleSetting[Params.PhotoMode]( + communicator, + SettingId.PHOTO_MODE, + Params.PhotoMode, + ) + """Photo Mode""" + + self.framing: BleSetting[Params.Framing] = BleSetting[Params.Framing]( + communicator, + SettingId.FRAMING, + Params.Framing, + ) + """Video Framing Mode""" + + self.hindsight: BleSetting[Params.Hindsight] = BleSetting[Params.Hindsight]( + communicator, + SettingId.HINDSIGHT, + Params.Hindsight, + ) + """Hindsight time / disable""" + + self.photo_interval: BleSetting[Params.PhotoInterval] = BleSetting[Params.PhotoInterval]( + communicator, + SettingId.PHOTO_INTERVAL, + Params.PhotoInterval, + ) + """Interval between photo captures""" + + self.photo_duration: BleSetting[Params.PhotoDuration] = BleSetting[Params.PhotoDuration]( + communicator, + SettingId.PHOTO_INTERVAL_DURATION, + Params.PhotoDuration, + ) + """Interval between photo captures""" + super().__init__(communicator) class BleAsyncResponses: """These are responses whose ID's are not associated with any messages""" - generic_response: Final = Struct("unparsed" / GreedyBytes) + generic_parser: Final = Parser[bytes]( + byte_json_adapter=ByteParserBuilders.Construct(Struct("unparsed" / GreedyBytes)) + ) - responses = [ + responses: list[BleAsyncResponse] = [ + BleAsyncResponse( + FeatureId.NETWORK_MANAGEMENT, + ActionId.NOTIF_PROVIS_STATE, + Parser(byte_json_adapter=ByteParserBuilders.Protobuf(proto.NotifProvisioningState)), + ), + BleAsyncResponse( + FeatureId.NETWORK_MANAGEMENT, + ActionId.NOTIF_START_SCAN, + Parser(byte_json_adapter=ByteParserBuilders.Protobuf(proto.NotifStartScanning)), + ), BleAsyncResponse( - FeatureId.NETWORK_MANAGEMENT, ActionId.NOTIF_PROVIS_STATE, proto.NotifProvisioningState + FeatureId.QUERY, + ActionId.INTERNAL_FF, + generic_parser, ), - BleAsyncResponse(FeatureId.NETWORK_MANAGEMENT, ActionId.NOTIF_START_SCAN, proto.NotifStartScanning), - BleAsyncResponse(FeatureId.QUERY, ActionId.INTERNAL_FF, generic_response), ] @classmethod def add_parsers(cls) -> None: """Add all of the defined asynchronous responses to the global parser map""" for response in cls.responses: - GoProResp._add_global_parser(response.action_id, response.parser) - GoProResp._add_feature_action_id_mapping(response.feature_id, response.action_id) + GlobalParsers.add(response.action_id, response.parser) + GlobalParsers.add_feature_action_id_mapping(response.feature_id, response.action_id) class BleStatuses(BleMessages[BleStatus, StatusId]): @@ -824,304 +966,328 @@ class BleStatuses(BleMessages[BleStatus, StatusId]): """ def __init__(self, communicator: GoProBle) -> None: - self.batt_present: BleStatus = BleStatus(communicator, StatusId.BATT_PRESENT, Flag) + self.batt_present: BleStatus[bool] = BleStatus(communicator, StatusId.BATT_PRESENT, Flag) """Is the system's internal battery present?""" - self.batt_level: BleStatus = BleStatus(communicator, StatusId.BATT_LEVEL, Int8ub) + self.batt_level: BleStatus[int] = BleStatus(communicator, StatusId.BATT_LEVEL, Int8ub) """Rough approximation of internal battery level in bars.""" # TODO can we just not define deprecated statuses? - self.deprecated_3: BleStatus = BleStatus(communicator, StatusId.DEPRECATED_3, DeprecatedAdapter()) + self.deprecated_3: BleStatus[Any] = BleStatus( + communicator, StatusId.DEPRECATED_3, ByteParserBuilders.DeprecatedMarker() + ) """This status is deprecated.""" - self.deprecated_4: BleStatus = BleStatus(communicator, StatusId.DEPRECATED_4, DeprecatedAdapter()) + self.deprecated_4: BleStatus[Any] = BleStatus( + communicator, StatusId.DEPRECATED_4, ByteParserBuilders.DeprecatedMarker() + ) """This status is deprecated.""" - self.system_hot: BleStatus = BleStatus(communicator, StatusId.SYSTEM_HOT, Flag) + self.system_hot: BleStatus[bool] = BleStatus(communicator, StatusId.SYSTEM_HOT, Flag) """Is the system currently overheating?""" - self.system_busy: BleStatus = BleStatus(communicator, StatusId.SYSTEM_BUSY, Flag) + self.system_busy: BleStatus[bool] = BleStatus(communicator, StatusId.SYSTEM_BUSY, Flag) """Is the camera busy?""" - self.quick_capture: BleStatus = BleStatus(communicator, StatusId.QUICK_CAPTURE, Flag) + self.quick_capture: BleStatus[bool] = BleStatus(communicator, StatusId.QUICK_CAPTURE, Flag) """Is quick capture feature enabled?""" - self.encoding_active: BleStatus = BleStatus(communicator, StatusId.ENCODING, Flag) + self.encoding_active: BleStatus[bool] = BleStatus(communicator, StatusId.ENCODING, Flag) """Is the camera currently encoding (i.e. capturing photo / video)?""" - self.lcd_lock_active: BleStatus = BleStatus(communicator, StatusId.LCD_LOCK_ACTIVE, Flag) + self.lcd_lock_active: BleStatus[bool] = BleStatus(communicator, StatusId.LCD_LOCK_ACTIVE, Flag) """Is the LCD lock currently active?""" - self.video_progress: BleStatus = BleStatus(communicator, StatusId.VIDEO_PROGRESS, Int32ub) + self.video_progress: BleStatus[int] = BleStatus(communicator, StatusId.VIDEO_PROGRESS, Int32ub) """When encoding video, this is the duration (seconds) of the video so far; 0 otherwise.""" - self.wireless_enabled: BleStatus = BleStatus(communicator, StatusId.WIRELESS_ENABLED, Flag) + self.wireless_enabled: BleStatus[bool] = BleStatus(communicator, StatusId.WIRELESS_ENABLED, Flag) """Are Wireless Connections enabled?""" - self.pair_state: BleStatus = BleStatus(communicator, StatusId.PAIR_STATE, Params.PairState) + self.pair_state: BleStatus[Params.PairState] = BleStatus(communicator, StatusId.PAIR_STATE, Params.PairState) """What is the pair state?""" - self.pair_type: BleStatus = BleStatus(communicator, StatusId.PAIR_TYPE, Params.PairType) + self.pair_type: BleStatus[Params.PairType] = BleStatus(communicator, StatusId.PAIR_TYPE, Params.PairType) """The last type of pairing that the camera was engaged in.""" - self.pair_time: BleStatus = BleStatus(communicator, StatusId.PAIR_TIME, Int32ub) + self.pair_time: BleStatus[int] = BleStatus(communicator, StatusId.PAIR_TIME, Int32ub) """ Time (milliseconds) since boot of last successful pairing complete action.""" - self.wap_scan_state: BleStatus = BleStatus(communicator, StatusId.WAP_SCAN_STATE, Params.WAPState) + self.wap_scan_state: BleStatus[Params.PairType] = BleStatus( + communicator, StatusId.WAP_SCAN_STATE, Params.WAPState + ) """State of current scan for Wifi Access Points. Appears to only change for CAH-related scans.""" # TODO this is returning different sized data in BLE vs WiFi - # self.wap_scan_time: BleStatus = BleStatus(communicator, StatusId.WAP_SCAN_TIME, Int8ub) + # self.wap_scan_time:[int] BleStatus = BleStatus(communicator, StatusId.WAP_SCAN_TIME, Int8ub) # """The time, in milliseconds since boot that the Wifi Access Point scan completed.""" - self.wap_prov_stat: BleStatus = BleStatus(communicator, StatusId.WAP_PROV_STAT, Params.WAPState) + self.wap_prov_stat: BleStatus[Params.PairType] = BleStatus( + communicator, StatusId.WAP_PROV_STAT, Params.WAPState + ) """Wifi AP provisioning state.""" - self.remote_ctrl_ver: BleStatus = BleStatus(communicator, StatusId.REMOTE_CTRL_VER, Int8ub) + self.remote_ctrl_ver: BleStatus[int] = BleStatus(communicator, StatusId.REMOTE_CTRL_VER, Int8ub) """What is the remote control version?""" - self.remote_ctrl_conn: BleStatus = BleStatus(communicator, StatusId.REMOTE_CTRL_CONN, Flag) + self.remote_ctrl_conn: BleStatus[bool] = BleStatus(communicator, StatusId.REMOTE_CTRL_CONN, Flag) """Is the remote control connected?""" - self.pair_state2: BleStatus = BleStatus(communicator, StatusId.PAIR_STATE2, Int8ub) + self.pair_state2: BleStatus[int] = BleStatus(communicator, StatusId.PAIR_STATE2, Int8ub) """Wireless Pairing State.""" - self.wlan_ssid: BleStatus = BleStatus(communicator, StatusId.WLAN_SSID, GreedyString(encoding="utf-8")) + self.wlan_ssid: BleStatus[str] = BleStatus(communicator, StatusId.WLAN_SSID, GreedyString(encoding="utf-8")) """Provisioned WIFI AP SSID. On BLE connection, value is big-endian byte-encoded int.""" - self.ap_ssid: BleStatus = BleStatus(communicator, StatusId.AP_SSID, GreedyString(encoding="utf-8")) + self.ap_ssid: BleStatus[str] = BleStatus(communicator, StatusId.AP_SSID, GreedyString(encoding="utf-8")) """Camera's WIFI SSID. On BLE connection, value is big-endian byte-encoded int.""" - self.app_count: BleStatus = BleStatus(communicator, StatusId.APP_COUNT, Int8ub) + self.app_count: BleStatus[int] = BleStatus(communicator, StatusId.APP_COUNT, Int8ub) """The number of wireless devices connected to the camera.""" - self.preview_enabled: BleStatus = BleStatus(communicator, StatusId.PREVIEW_ENABLED, Flag) + self.preview_enabled: BleStatus[bool] = BleStatus(communicator, StatusId.PREVIEW_ENABLED, Flag) """Is preview stream enabled?""" - self.sd_status: BleStatus = BleStatus(communicator, StatusId.SD_STATUS, Params.SDStatus) + self.sd_status: BleStatus[Params.SDStatus] = BleStatus(communicator, StatusId.SD_STATUS, Params.SDStatus) """Primary Storage Status.""" - self.photos_rem: BleStatus = BleStatus(communicator, StatusId.PHOTOS_REM, Int32ub) + self.photos_rem: BleStatus[int] = BleStatus(communicator, StatusId.PHOTOS_REM, Int32ub) """How many photos can be taken before sdcard is full?""" - self.video_rem: BleStatus = BleStatus(communicator, StatusId.VIDEO_REM, Int32ub) + self.video_rem: BleStatus[int] = BleStatus(communicator, StatusId.VIDEO_REM, Int32ub) """How many minutes of video can be captured with current settings before sdcard is full?""" - self.num_group_photo: BleStatus = BleStatus(communicator, StatusId.NUM_GROUP_PHOTO, Int32ub) + self.num_group_photo: BleStatus[int] = BleStatus(communicator, StatusId.NUM_GROUP_PHOTO, Int32ub) """How many group photos can be taken with current settings before sdcard is full?""" - self.num_group_video: BleStatus = BleStatus(communicator, StatusId.NUM_GROUP_VIDEO, Int32ub) + self.num_group_video: BleStatus[int] = BleStatus(communicator, StatusId.NUM_GROUP_VIDEO, Int32ub) """Total number of group videos on sdcard.""" - self.num_total_photo: BleStatus = BleStatus(communicator, StatusId.NUM_TOTAL_PHOTO, Int32ub) + self.num_total_photo: BleStatus[int] = BleStatus(communicator, StatusId.NUM_TOTAL_PHOTO, Int32ub) """Total number of photos on sdcard.""" - self.num_total_video: BleStatus = BleStatus(communicator, StatusId.NUM_TOTAL_VIDEO, Int32ub) + self.num_total_video: BleStatus[int] = BleStatus(communicator, StatusId.NUM_TOTAL_VIDEO, Int32ub) """Total number of videos on sdcard.""" - self.deprecated_40: BleStatus = BleStatus(communicator, StatusId.DEPRECATED_40, DeprecatedAdapter()) + self.deprecated_40: BleStatus[Any] = BleStatus( + communicator, StatusId.DEPRECATED_40, ByteParserBuilders.DeprecatedMarker() + ) """This status is deprecated.""" - self.ota_stat: BleStatus = BleStatus(communicator, StatusId.OTA_STAT, Params.OTAStatus) + self.ota_stat: BleStatus[Params.OTAStatus] = BleStatus(communicator, StatusId.OTA_STAT, Params.OTAStatus) """The current status of Over The Air (OTA) update.""" - self.download_cancel_pend: BleStatus = BleStatus(communicator, StatusId.DOWNLOAD_CANCEL_PEND, Flag) + self.download_cancel_pend: BleStatus[bool] = BleStatus(communicator, StatusId.DOWNLOAD_CANCEL_PEND, Flag) """Is download firmware update cancel request pending?""" - self.mode_group: BleStatus = BleStatus(communicator, StatusId.MODE_GROUP, Int8ub) + self.mode_group: BleStatus[int] = BleStatus(communicator, StatusId.MODE_GROUP, Int8ub) """Current mode group (deprecated in HERO8).""" - self.locate_active: BleStatus = BleStatus(communicator, StatusId.LOCATE_ACTIVE, Flag) + self.locate_active: BleStatus[bool] = BleStatus(communicator, StatusId.LOCATE_ACTIVE, Flag) """Is locate camera feature active?""" - self.multi_count_down: BleStatus = BleStatus(communicator, StatusId.MULTI_COUNT_DOWN, Int32ub) + self.multi_count_down: BleStatus[int] = BleStatus(communicator, StatusId.MULTI_COUNT_DOWN, Int32ub) """The current timelapse interval countdown value (e.g. 5...4...3...2...1...).""" - self.space_rem: BleStatus = BleStatus(communicator, StatusId.SPACE_REM, Int64ub) + self.space_rem: BleStatus[int] = BleStatus(communicator, StatusId.SPACE_REM, Int64ub) """Remaining space on the sdcard in Kilobytes.""" - self.streaming_supp: BleStatus = BleStatus(communicator, StatusId.STREAMING_SUPP, Flag) + self.streaming_supp: BleStatus[bool] = BleStatus(communicator, StatusId.STREAMING_SUPP, Flag) """Is streaming supports in current recording/flatmode/secondary-stream?""" - self.wifi_bars: BleStatus = BleStatus(communicator, StatusId.WIFI_BARS, Int8ub) + self.wifi_bars: BleStatus[int] = BleStatus(communicator, StatusId.WIFI_BARS, Int8ub) """Wifi signal strength in bars.""" - self.current_time_ms: BleStatus = BleStatus(communicator, StatusId.CURRENT_TIME_MS, Int32ub) + self.current_time_ms: BleStatus[int] = BleStatus(communicator, StatusId.CURRENT_TIME_MS, Int32ub) """System time in milliseconds since system was booted.""" - self.num_hilights: BleStatus = BleStatus(communicator, StatusId.NUM_HILIGHTS, Int8ub) + self.num_hilights: BleStatus[int] = BleStatus(communicator, StatusId.NUM_HILIGHTS, Int8ub) """The number of hilights in encoding video (set to 0 when encoding stops).""" - self.last_hilight: BleStatus = BleStatus(communicator, StatusId.LAST_HILIGHT, Int32ub) + self.last_hilight: BleStatus[int] = BleStatus(communicator, StatusId.LAST_HILIGHT, Int32ub) """Time since boot (msec) of most recent hilight in encoding video (set to 0 when encoding stops).""" - self.next_poll: BleStatus = BleStatus(communicator, StatusId.NEXT_POLL, Int32ub) + self.next_poll: BleStatus[int] = BleStatus(communicator, StatusId.NEXT_POLL, Int32ub) """The min time between camera status updates (msec). Do not poll for status more often than this.""" - self.analytics_rdy: BleStatus = BleStatus(communicator, StatusId.ANALYTICS_RDY, Params.AnalyticsState) + self.analytics_rdy: BleStatus[Params.AnalyticsState] = BleStatus( + communicator, StatusId.ANALYTICS_RDY, Params.AnalyticsState + ) """The current state of camera analytics.""" - self.analytics_size: BleStatus = BleStatus(communicator, StatusId.ANALYTICS_SIZE, Int32ub) + self.analytics_size: BleStatus[int] = BleStatus(communicator, StatusId.ANALYTICS_SIZE, Int32ub) """The size (units??) of the analytics file.""" - self.in_context_menu: BleStatus = BleStatus(communicator, StatusId.IN_CONTEXT_MENU, Flag) + self.in_context_menu: BleStatus[bool] = BleStatus(communicator, StatusId.IN_CONTEXT_MENU, Flag) """Is the camera currently in a contextual menu (e.g. Preferences)?""" - self.timelapse_rem: BleStatus = BleStatus(communicator, StatusId.TIMELAPSE_REM, Int32ub) + self.timelapse_rem: BleStatus[int] = BleStatus(communicator, StatusId.TIMELAPSE_REM, Int32ub) """How many min of Timelapse video can be captured with current settings before sdcard is full?""" - self.exposure_type: BleStatus = BleStatus(communicator, StatusId.EXPOSURE_TYPE, Params.ExposureMode) + self.exposure_type: BleStatus[Params.ExposureMode] = BleStatus( + communicator, StatusId.EXPOSURE_TYPE, Params.ExposureMode + ) """Liveview Exposure Select Mode.""" - self.exposure_x: BleStatus = BleStatus(communicator, StatusId.EXPOSURE_X, Int8ub) + self.exposure_x: BleStatus[int] = BleStatus(communicator, StatusId.EXPOSURE_X, Int8ub) """Liveview Exposure Select for y-coordinate (percent).""" - self.exposure_y: BleStatus = BleStatus(communicator, StatusId.EXPOSURE_Y, Int8ub) + self.exposure_y: BleStatus[int] = BleStatus(communicator, StatusId.EXPOSURE_Y, Int8ub) """Liveview Exposure Select for y-coordinate (percent).""" - self.gps_stat: BleStatus = BleStatus(communicator, StatusId.GPS_STAT, Flag) + self.gps_stat: BleStatus[bool] = BleStatus(communicator, StatusId.GPS_STAT, Flag) """Does the camera currently have a GPS lock?""" - self.ap_state: BleStatus = BleStatus(communicator, StatusId.AP_STATE, Flag) + self.ap_state: BleStatus[bool] = BleStatus(communicator, StatusId.AP_STATE, Flag) """Is the Wifi radio enabled?""" - self.int_batt_per: BleStatus = BleStatus(communicator, StatusId.INT_BATT_PER, Int8ub) + self.int_batt_per: BleStatus[int] = BleStatus(communicator, StatusId.INT_BATT_PER, Int8ub) """Internal battery level (percent).""" - self.acc_mic_stat: BleStatus = BleStatus(communicator, StatusId.ACC_MIC_STAT, Params.ExposureMode) + self.acc_mic_stat: BleStatus[Params.ExposureMode] = BleStatus( + communicator, StatusId.ACC_MIC_STAT, Params.ExposureMode + ) """Microphone Accessory status.""" - self.digital_zoom: BleStatus = BleStatus(communicator, StatusId.DIGITAL_ZOOM, Int8ub) + self.digital_zoom: BleStatus[int] = BleStatus(communicator, StatusId.DIGITAL_ZOOM, Int8ub) """ Digital Zoom level (percent).""" - self.wireless_band: BleStatus = BleStatus(communicator, StatusId.WIRELESS_BAND, Params.WifiBand) + self.wireless_band: BleStatus[Params.WifiBand] = BleStatus( + communicator, StatusId.WIRELESS_BAND, Params.WifiBand + ) """Wireless Band.""" - self.dig_zoom_active: BleStatus = BleStatus(communicator, StatusId.DIG_ZOOM_ACTIVE, Flag) + self.dig_zoom_active: BleStatus[bool] = BleStatus(communicator, StatusId.DIG_ZOOM_ACTIVE, Flag) """Is Digital Zoom feature available?""" - self.mobile_video: BleStatus = BleStatus(communicator, StatusId.MOBILE_VIDEO, Flag) + self.mobile_video: BleStatus[bool] = BleStatus(communicator, StatusId.MOBILE_VIDEO, Flag) """Are current video settings mobile friendly? (related to video compression and frame rate).""" - self.first_time: BleStatus = BleStatus(communicator, StatusId.FIRST_TIME, Flag) + self.first_time: BleStatus[bool] = BleStatus(communicator, StatusId.FIRST_TIME, Flag) """Is the camera currently in First Time Use (FTU) UI flow?""" - self.sec_sd_stat: BleStatus = BleStatus(communicator, StatusId.SEC_SD_STAT, Params.SDStatus) + self.sec_sd_stat: BleStatus[Params.SDStatus] = BleStatus(communicator, StatusId.SEC_SD_STAT, Params.SDStatus) """Secondary Storage Status (exclusive to Superbank).""" - self.band_5ghz_avail: BleStatus = BleStatus(communicator, StatusId.BAND_5GHZ_AVAIL, Flag) + self.band_5ghz_avail: BleStatus[bool] = BleStatus(communicator, StatusId.BAND_5GHZ_AVAIL, Flag) """Is 5GHz wireless band available?""" - self.system_ready: BleStatus = BleStatus(communicator, StatusId.SYSTEM_READY, Flag) + self.system_ready: BleStatus[bool] = BleStatus(communicator, StatusId.SYSTEM_READY, Flag) """Is the system ready to accept messages?""" - self.batt_ok_ota: BleStatus = BleStatus(communicator, StatusId.BATT_OK_OTA, Flag) + self.batt_ok_ota: BleStatus[bool] = BleStatus(communicator, StatusId.BATT_OK_OTA, Flag) """Is the internal battery charged sufficiently to start Over The Air (OTA) update?""" - self.video_low_temp: BleStatus = BleStatus(communicator, StatusId.VIDEO_LOW_TEMP, Flag) + self.video_low_temp: BleStatus[bool] = BleStatus(communicator, StatusId.VIDEO_LOW_TEMP, Flag) """Is the camera getting too cold to continue recording?""" - self.orientation: BleStatus = BleStatus(communicator, StatusId.ORIENTATION, Params.Orientation) + self.orientation: BleStatus[Params.Orientation] = BleStatus( + communicator, StatusId.ORIENTATION, Params.Orientation + ) """The rotational orientation of the camera.""" - self.deprecated_87: BleStatus = BleStatus(communicator, StatusId.DEPRECATED_87, DeprecatedAdapter()) + self.deprecated_87: BleStatus[Any] = BleStatus( + communicator, StatusId.DEPRECATED_87, ByteParserBuilders.DeprecatedMarker() + ) """This status is deprecated.""" - self.zoom_encoding: BleStatus = BleStatus(communicator, StatusId.ZOOM_ENCODING, Flag) + self.zoom_encoding: BleStatus[bool] = BleStatus(communicator, StatusId.ZOOM_ENCODING, Flag) """Is this camera capable of zooming while encoding (static value based on model, not settings)?""" - self.flatmode_id: BleStatus = BleStatus(communicator, StatusId.FLATMODE_ID, Params.Flatmode) + self.flatmode_id: BleStatus[Params.Flatmode] = BleStatus(communicator, StatusId.FLATMODE_ID, Params.Flatmode) """Current flatmode ID.""" - self.logs_ready: BleStatus = BleStatus(communicator, StatusId.LOGS_READY, Flag) + self.logs_ready: BleStatus[bool] = BleStatus(communicator, StatusId.LOGS_READY, Flag) """ Are system logs ready to be downloaded?""" - self.deprecated_92: BleStatus = BleStatus(communicator, StatusId.DEPRECATED_92, DeprecatedAdapter()) + self.deprecated_92: BleStatus[Any] = BleStatus( + communicator, StatusId.DEPRECATED_92, ByteParserBuilders.DeprecatedMarker() + ) """This status is deprecated.""" - self.video_presets: BleStatus = BleStatus(communicator, StatusId.VIDEO_PRESETS, Int32ub) + self.video_presets: BleStatus[int] = BleStatus(communicator, StatusId.VIDEO_PRESETS, Int32ub) """Current Video Preset (ID).""" - self.photo_presets: BleStatus = BleStatus(communicator, StatusId.PHOTO_PRESETS, Int32ub) + self.photo_presets: BleStatus[int] = BleStatus(communicator, StatusId.PHOTO_PRESETS, Int32ub) """Current Photo Preset (ID).""" - self.timelapse_presets: BleStatus = BleStatus(communicator, StatusId.TIMELAPSE_PRESETS, Int32ub) + self.timelapse_presets: BleStatus[int] = BleStatus(communicator, StatusId.TIMELAPSE_PRESETS, Int32ub) """ Current Timelapse Preset (ID).""" - self.presets_group: BleStatus = BleStatus(communicator, StatusId.PRESETS_GROUP, Int32ub) + self.presets_group: BleStatus[int] = BleStatus(communicator, StatusId.PRESETS_GROUP, Int32ub) """Current Preset Group (ID).""" - self.active_preset: BleStatus = BleStatus(communicator, StatusId.ACTIVE_PRESET, Int32ub) + self.active_preset: BleStatus[int] = BleStatus(communicator, StatusId.ACTIVE_PRESET, Int32ub) """Currently Preset (ID).""" - self.preset_modified: BleStatus = BleStatus(communicator, StatusId.PRESET_MODIFIED, Int32ub) + self.preset_modified: BleStatus[int] = BleStatus(communicator, StatusId.PRESET_MODIFIED, Int32ub) """Preset Modified Status, which contains an event ID and a preset (group) ID.""" - self.live_burst_rem: BleStatus = BleStatus(communicator, StatusId.LIVE_BURST_REM, Int32ub) + self.live_burst_rem: BleStatus[int] = BleStatus(communicator, StatusId.LIVE_BURST_REM, Int32ub) """How many Live Bursts can be captured before sdcard is full?""" - self.live_burst_total: BleStatus = BleStatus(communicator, StatusId.LIVE_BURST_TOTAL, Int32ub) + self.live_burst_total: BleStatus[int] = BleStatus(communicator, StatusId.LIVE_BURST_TOTAL, Int32ub) """Total number of Live Bursts on sdcard.""" - self.capt_delay_active: BleStatus = BleStatus(communicator, StatusId.CAPT_DELAY_ACTIVE, Flag) + self.capt_delay_active: BleStatus[bool] = BleStatus(communicator, StatusId.CAPT_DELAY_ACTIVE, Flag) """Is Capture Delay currently active (i.e. counting down)?""" - self.media_mod_mic_stat: BleStatus = BleStatus( + self.media_mod_mic_stat: BleStatus[Params.MediaModMicStatus] = BleStatus( communicator, StatusId.MEDIA_MOD_MIC_STAT, Params.MediaModMicStatus ) """Media mod State.""" - self.timewarp_speed_ramp: BleStatus = BleStatus( + self.timewarp_speed_ramp: BleStatus[Params.TimeWarpSpeed] = BleStatus( communicator, StatusId.TIMEWARP_SPEED_RAMP, Params.TimeWarpSpeed ) """Time Warp Speed.""" - self.linux_core_active: BleStatus = BleStatus(communicator, StatusId.LINUX_CORE_ACTIVE, Flag) + self.linux_core_active: BleStatus[bool] = BleStatus(communicator, StatusId.LINUX_CORE_ACTIVE, Flag) """Is the system's Linux core active?""" - self.camera_lens_type: BleStatus = BleStatus( + self.camera_lens_type: BleStatus[Params.MaxLensMode] = BleStatus( communicator, StatusId.CAMERA_LENS_TYPE, Params.MaxLensMode ) """Camera lens type (reflects changes to setting 162).""" - self.video_hindsight: BleStatus = BleStatus(communicator, StatusId.VIDEO_HINDSIGHT, Flag) + self.video_hindsight: BleStatus[bool] = BleStatus(communicator, StatusId.VIDEO_HINDSIGHT, Flag) """Is Video Hindsight Capture Active?""" - self.scheduled_preset: BleStatus = BleStatus(communicator, StatusId.SCHEDULED_PRESET, Int32ub) + self.scheduled_preset: BleStatus[int] = BleStatus(communicator, StatusId.SCHEDULED_PRESET, Int32ub) """Scheduled Capture Preset ID.""" - self.scheduled_capture: BleStatus = BleStatus(communicator, StatusId.SCHEDULED_CAPTURE, Flag) + self.scheduled_capture: BleStatus[bool] = BleStatus(communicator, StatusId.SCHEDULED_CAPTURE, Flag) """Is Scheduled Capture set?""" - self.creating_preset: BleStatus = BleStatus(communicator, StatusId.CREATING_PRESET, Flag) + self.creating_preset: BleStatus[bool] = BleStatus(communicator, StatusId.CREATING_PRESET, Flag) """Is the camera in the process of creating a custom preset?""" - self.media_mod_stat: BleStatus = BleStatus( + self.media_mod_stat: BleStatus[Params.MediaModStatus] = BleStatus( communicator, StatusId.MEDIA_MOD_STAT, Params.MediaModStatus ) """Media Mode Status (bitmasked).""" - self.turbo_mode: BleStatus = BleStatus(communicator, StatusId.TURBO_MODE, Flag) + self.turbo_mode: BleStatus[bool] = BleStatus(communicator, StatusId.TURBO_MODE, Flag) """Is Turbo Transfer active?""" - self.sd_rating_check_error: BleStatus = BleStatus(communicator, StatusId.SD_RATING_CHECK_ERROR, Flag) + self.sd_rating_check_error: BleStatus[bool] = BleStatus(communicator, StatusId.SD_RATING_CHECK_ERROR, Flag) """Does sdcard meet specified minimum write speed?""" - self.sd_write_speed_error: BleStatus = BleStatus(communicator, StatusId.SD_WRITE_SPEED_ERROR, Int8ub) + self.sd_write_speed_error: BleStatus[int] = BleStatus(communicator, StatusId.SD_WRITE_SPEED_ERROR, Int8ub) """Number of sdcard write speed errors since device booted""" - self.camera_control: BleStatus = BleStatus( - communicator, StatusId.CAMERA_CONTROL, Params.CameraControlStatus + self.camera_control: BleStatus[Params.CameraControl] = BleStatus( + communicator, StatusId.CAMERA_CONTROL, Params.CameraControl ) """Camera control status ID""" - self.usb_connected: BleStatus = BleStatus(communicator, StatusId.USB_CONNECTED, Flag) + self.usb_connected: BleStatus[bool] = BleStatus(communicator, StatusId.USB_CONNECTED, Flag) """Is the camera connected to a PC via USB?""" - self.control_allowed_over_usb: BleStatus = BleStatus(communicator, StatusId.CONTROL_OVER_USB, Flag) + self.control_allowed_over_usb: BleStatus[bool] = BleStatus(communicator, StatusId.CONTROL_OVER_USB, Flag) """Is control allowed over USB?""" - self.total_sd_space_kb: BleStatus = BleStatus(communicator, StatusId.TOTAL_SD_SPACE_KB, Int32ub) + self.total_sd_space_kb: BleStatus[int] = BleStatus(communicator, StatusId.TOTAL_SD_SPACE_KB, Int32ub) """Total space taken up on the SD card in kilobytes""" super().__init__(communicator) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/api/builders.py b/demos/python/sdk_wireless_camera_control/open_gopro/api/builders.py index 056abe16..db97e118 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/api/builders.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/builders.py @@ -4,192 +4,82 @@ """Common functionality across API versions to build commands, settings, and statuses""" from __future__ import annotations + import enum import logging +from collections.abc import Iterable +from dataclasses import dataclass from pathlib import Path +from typing import Any, Callable, Final, Generic, TypeVar, Union from urllib.parse import urlencode -from collections.abc import Iterable -from dataclasses import dataclass, InitVar, field -from typing import Any, TypeVar, Generic, Union, Optional, Final, Callable -import wrapt -import google.protobuf.json_format -from google.protobuf import descriptor -from google.protobuf.message import Message as Protobuf -from google.protobuf.json_format import MessageToDict as ProtobufToDict import construct +import wrapt -from open_gopro.responses import BytesBuilder, BytesParser, GoProResp, JsonParser, BytesParserBuilder, Parser -from open_gopro.constants import ( - ActionId, - FeatureId, - BleUUID, - CmdId, - SettingId, - QueryCmdId, - StatusId, - GoProUUIDs, - enum_factory, - GoProEnum, -) -from open_gopro.interface import ( +from open_gopro import types +from open_gopro.api.parsers import ByteParserBuilders, JsonParsers +from open_gopro.communicator_interface import ( + BleMessage, + BleMessages, GoProBle, GoProHttp, - BleMessage, HttpMessage, - BleMessages, HttpMessages, MessageRules, RuleSignature, ) -from open_gopro.util import Logger, jsonify +from open_gopro.constants import ( + ActionId, + BleUUID, + CmdId, + FeatureId, + GoProUUIDs, + QueryCmdId, + SettingId, + StatusId, +) +from open_gopro.enum import GoProEnum +from open_gopro.logger import Logger +from open_gopro.models.general import HttpInvalidSettingResponse +from open_gopro.models.response import GlobalParsers, GoProResp +from open_gopro.parser_interface import BytesBuilder, BytesParserBuilder, Parser +from open_gopro.util import pretty_print logger = logging.getLogger(__name__) ValueType = TypeVar("ValueType") IdType = TypeVar("IdType") -ProtobufProducerType = tuple[Union[type[SettingId], type[StatusId]], QueryCmdId] - -ProtobufPrinter = google.protobuf.json_format._Printer # type: ignore # noqa -original_field_to_json = ProtobufPrinter._FieldToJsonObject QueryParserType = Union[construct.Construct, type[GoProEnum], BytesParserBuilder] -AsyncParserType = Union[construct.Construct, BytesParser[dict], type[Protobuf]] ######################################################## BLE ################################################# - -def enum_parser_factory(target: type[GoProEnum]) -> BytesParserBuilder: - """Build an Enum ParserBuilder - - Args: - target (type[GoProEnum]): enum to use for parsing and building - - Returns: - BytesParserBuilder: instance of generated class - """ - - class ParserBuilder(BytesParserBuilder[GoProEnum]): - """Adapt enums to / from a one byte value""" - - container = target - - def parse(self, data: bytes) -> GoProEnum: - return self.container(data[0]) - - def build(self, *args: Any, **_: Any) -> bytes: - return bytes([int(args[0])]) - - return ParserBuilder() - - -def construct_adapter_factory(target: construct.Construct) -> BytesParserBuilder: - """Build a construct parser adapter from a construct - - Args: - target (construct.Construct): construct to use for parsing and building - - Returns: - BytesParserBuilder: instance of generated class - """ - - class ParserBuilder(BytesParserBuilder): - """Adapt the construct for our interface""" - - container = target - - def parse(self, data: bytes) -> Any: - return self.container.parse(data) - - def build(self, *args: Any, **kwargs: Any) -> bytes: - return self.container.build(*args, **kwargs) - - return ParserBuilder() - - -def protobuf_parser_factory(proto: type[Protobuf]) -> BytesParser[dict]: - """Build a BytesParser from a protobuf definition - - Args: - proto (type[Protobuf]): protobuf definition to build class from - - Returns: - BytesParser[dict]: instance of generated class - """ - - class ProtobufByteParser(BytesParser[dict]): - """Parse bytes into a dict using the protobuf""" - - protobuf = proto - - def parse(self, data: bytes) -> dict: - response: Protobuf = self.protobuf().FromString(bytes(data)) - - # Monkey patch the field-to-json function to use our enum translation - ProtobufPrinter._FieldToJsonObject = ( - lambda self, field, value: enum_factory(field.enum_type)(value) # pylint: disable=not-callable - if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM - else original_field_to_json(self, field, value) - ) - return ProtobufToDict(response, preserving_proto_field_name=True) - - return ProtobufByteParser() - - -class DeprecatedAdapter(BytesParserBuilder[str]): - """Used to return "DEPRECATED" when a deprecated setting / status is attempted to be parsed / built""" - - def parse(self, data: bytes) -> str: - """Return string indicating this ID is deprecated - - Args: - data (bytes): ignored - - Returns: - str: "DEPRECATED" - """ - return "DEPRECATED" - - def build(self, obj: Any) -> bytes: - """Return empty bytes since this ID is deprecated - - Args: - obj (Any): ignored - - Returns: - bytes: empty - """ - return bytes() +T = TypeVar("T") class BleReadCommand(BleMessage[BleUUID]): """A BLE command that reads data from a BleUUID""" - def __init__(self, uuid: BleUUID, parser: Optional[Union[construct.Construct, BytesParser[dict]]]) -> None: + def __init__(self, uuid: BleUUID, parser: Parser) -> None: """Constructor Args: uuid (BleUUID): BleUUID to read from - parser (Optional[Union[construct.Construct, BytesParser[dict]]]): the parser that will parse the - received bytestream into a JSON dict + parser (Parser): the parser that will parse the received bytestream into a JSON dict """ - super().__init__( - uuid=uuid, - parser=construct_adapter_factory(parser) if isinstance(parser, construct.Construct) else parser, - identifier=uuid, - ) + super().__init__(uuid=uuid, parser=parser, identifier=uuid) - def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: # noqa: D102 - logger.info(Logger.build_log_tx_str(jsonify(self._as_dict()))) - response = __communicator__._read_characteristic(self._uuid) + async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: # noqa: D102 + logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict()))) + response = await __communicator__._read_characteristic(self._uuid) logger.info(Logger.build_log_rx_str(response)) return response def __str__(self) -> str: return f"Read {self._uuid.name.lower().replace('_', ' ').title()}" - def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: + def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: """Return the attributes of the command as a dict Args: @@ -197,9 +87,9 @@ def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: **kwargs (Any): additional entries for the dict Returns: - dict[str, Any]: command as dict + types.JsonDict: command as dict """ - return {"id": "Read " + self._uuid.name, **self._base_dict} | kwargs # type: ignore + return {"id": "Read " + self._uuid.name, **self._base_dict} | kwargs class BleWriteCommand(BleMessage[CmdId]): @@ -209,9 +99,9 @@ def __init__( self, uuid: BleUUID, cmd: CmdId, - param_builder: Optional[BytesBuilder] = None, - parser: Optional[Union[construct.Construct, BytesParser[dict]]] = None, - rules: Optional[dict[MessageRules, RuleSignature]] = None, + param_builder: BytesBuilder | None = None, + parser: Parser | None = None, + rules: dict[MessageRules, RuleSignature] | None = None, ) -> None: """Constructor @@ -225,14 +115,9 @@ def __init__( """ self.param_builder = param_builder self.cmd = cmd - super().__init__( - uuid, - construct_adapter_factory(parser) if isinstance(parser, construct.Construct) else parser, - cmd, - rules, - ) + super().__init__(uuid, parser, cmd, rules) - def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: + async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: """Execute the command by sending it via BLE Args: @@ -242,7 +127,7 @@ def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: Returns: GoProResp: Response received via BLE """ - logger.info(Logger.build_log_tx_str(jsonify(self._as_dict(**kwargs)))) + logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(**kwargs)))) data = bytearray([self.cmd.value]) params = bytearray() @@ -254,7 +139,7 @@ def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: if params: data.append(len(params)) data.extend(params) - response = __communicator__._send_ble_message( + response = await __communicator__._send_ble_message( self._uuid, data, self._identifier, rules=self._evaluate_rules(**kwargs) ) logger.info(Logger.build_log_rx_str(response)) @@ -263,7 +148,7 @@ def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: def __str__(self) -> str: return self.cmd.name.lower().replace("_", " ").removeprefix("cmdid").title() - def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: + def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: """Return the attributes of the command as a dict Args: @@ -271,9 +156,9 @@ def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: **kwargs (Any): additional entries for the dict Returns: - dict[str, Any]: command as dict + types.JsonDict: command as dict """ - return {"id": self.cmd, **self._base_dict} | kwargs # type: ignore + return {"id": self.cmd, **self._base_dict} | kwargs class RegisterUnregisterAll(BleWriteCommand): @@ -294,37 +179,35 @@ def __init__( self, uuid: BleUUID, cmd: CmdId, - producer: ProtobufProducerType, + update_set: type[SettingId] | type[StatusId], + responded_cmd: QueryCmdId, action: Action, - parser: Optional[BytesParser] = None, + parser: Parser | None = None, ) -> None: """Constructor Args: uuid (BleUUID): UUID to write to cmd (CmdId): Command ID that is being sent - producer (ProtobufProducerType): Tuple of (element_set, query command) where element_set is the GoProEnum - that this command relates to, i.e. SettingId for settings, StatusId for Statuses + update_set (type[SettingId] | type[StatusId]): what are registering / unregistering for? + responded_cmd (QueryCmdId): not used currently action (Action): whether to register or unregister parser (Optional[BytesParser], optional): Optional response parser. Defaults to None. """ self.action = action - self.producer = producer + self.update_set = update_set + self.responded_cmd = responded_cmd super().__init__(uuid=uuid, cmd=cmd, parser=parser) - def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: # noqa: D102 - element_set = self.producer[0] - responded_command = self.producer[1] - response = super().__call__(__communicator__) - if response.is_ok: - for element in element_set: - ( - __communicator__._register_listener + async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: # noqa: D102 + response = await super().__call__(__communicator__) + if response.ok: + for update in self.update_set: + ( # type: ignore + __communicator__.register_update if self.action is RegisterUnregisterAll.Action.REGISTER - else __communicator__._unregister_listener - )( - (responded_command, element) # type: ignore - ) + else __communicator__.unregister_update + )(kwargs["callback"], update) return response @@ -337,10 +220,10 @@ def __init__( feature_id: FeatureId, action_id: ActionId, response_action_id: ActionId, - request_proto: type[Protobuf], - response_proto: type[Protobuf], - additional_matching_ids: Optional[set[Union[ActionId, CmdId]]] = None, - additional_parsers: Optional[list[JsonParser]] = None, + request_proto: type[types.Protobuf], + response_proto: type[types.Protobuf], + parser: Parser | None, + additional_matching_ids: set[ActionId | CmdId] | None = None, ) -> None: """Constructor @@ -349,28 +232,26 @@ def __init__( feature_id (FeatureId): Feature ID that is being executed action_id (ActionId): protobuf specific action ID that is being executed response_action_id (ActionId): the action ID that will be in the response to this command - request_proto (type[Protobuf]): the action ID that will be in the response - response_proto (type[Protobuf]): protobuf used to parse received bytestream + request_proto (type[types.Protobuf]): the action ID that will be in the response + response_proto (type[types.Protobuf]): protobuf used to parse received bytestream + parser (Optional[BytesParser], optional): Optional response parser. Defaults to None. additional_matching_ids (Optional[set[Union[ActionId, CmdId]]], optional): Other action ID's to share this parser. This is used, for example, if a notification shares the same ID as the synchronous response. Defaults to None.. Defaults to None. - additional_parsers (Optional[list[JsonParser]], optional): Any additional JSON parsers to apply - after normal protobuf response parsing. Defaults to None. """ - parser = protobuf_parser_factory(response_proto) - for p in additional_parsers or []: - parser += p # type: ignore - super().__init__(uuid=uuid, parser=parser, identifier=action_id) + p = parser or Parser() + p.byte_json_adapter = ByteParserBuilders.Protobuf(response_proto) + super().__init__(uuid=uuid, parser=p, identifier=action_id) self.feature_id = feature_id self.action_id = action_id self.response_action_id = response_action_id self.request_proto = request_proto self.response_proto = response_proto - self.additional_matching_ids: set[Union[ActionId, CmdId]] = additional_matching_ids or set() + self.additional_matching_ids: set[ActionId | CmdId] = additional_matching_ids or set() assert self._parser for matching_id in [*self.additional_matching_ids, response_action_id]: - GoProResp._add_global_parser(matching_id, self._parser) - GoProResp._add_feature_action_id_mapping(self.feature_id, self.response_action_id) + GlobalParsers.add(matching_id, self._parser) + GlobalParsers.add_feature_action_id_mapping(self.feature_id, self.response_action_id) def build_data(self, **kwargs: Any) -> bytearray: """Build the byte data to prepare for command sending @@ -397,19 +278,19 @@ def build_data(self, **kwargs: Any) -> bytearray: # Prepend headers and serialize return bytearray([self.feature_id.value, self.action_id.value, *proto.SerializeToString()]) - def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: # noqa: D102 + async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: # noqa: D102 # The method that will actually build and send the protobuf command - logger.info(Logger.build_log_tx_str(jsonify(self._as_dict(**kwargs)))) + logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(**kwargs)))) data = self.build_data(**kwargs) # Allow exception to pass through if protobuf not completely initialized - response = __communicator__._send_ble_message(self._uuid, data, self.response_action_id) + response = await __communicator__._send_ble_message(self._uuid, data, self.response_action_id) logger.info(Logger.build_log_rx_str(response)) return response def __str__(self) -> str: return self.action_id.name.lower().replace("_", " ").removeprefix("actionid").title() - def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: + def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: """Return the attributes of the command as a dict Args: @@ -417,17 +298,17 @@ def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: **kwargs (Any): additional entries for the dict Returns: - dict[str, Any]: command as dict + types.JsonDict: command as dict """ - return {"id": self.action_id, "feature_id": self.feature_id, **self._base_dict} | kwargs # type: ignore + return {"id": self.action_id, "feature_id": self.feature_id, **self._base_dict} | kwargs def ble_write_command( uuid: BleUUID, cmd: CmdId, - param_builder: Optional[BytesBuilder] = None, - parser: Optional[Union[construct.Construct, BytesParser[dict]]] = None, - rules: Optional[dict[MessageRules, RuleSignature]] = None, + param_builder: BytesBuilder | None = None, + parser: Parser | None = None, + rules: dict[MessageRules, RuleSignature] | None = None, ) -> Callable: """Factory to build a BleWriteCommand and wrapper to execute it @@ -435,7 +316,7 @@ def ble_write_command( uuid (BleUUID): BleUUID to write to cmd (CmdId): Command ID that is being sent param_builder (BytesBuilder, optional): is responsible for building the bytestream to send from the input params - parser (BytesParser. optional): the parser that will parse the received bytestream into a JSON dict + parser (Parser, optional): the parser that will parse the received bytestream into a JSON dict rules (dict[MessageRules, RuleSignature], optional): Rules to be applied to message execution Returns: @@ -444,21 +325,18 @@ def ble_write_command( message = BleWriteCommand(uuid, cmd, param_builder, parser, rules=rules) @wrapt.decorator - def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: - return message(instance._communicator, **(wrapped(**kwargs) or kwargs)) + async def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: + return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) return wrapper -def ble_read_command( - uuid: BleUUID, parser: Optional[Union[construct.Construct, BytesParser[dict]]] -) -> Callable: +def ble_read_command(uuid: BleUUID, parser: Parser) -> Callable: """Factory to build a BleReadCommand and wrapper to execute it Args: uuid (BleUUID): BleUUID to read from - parser (Optional[Union[construct.Construct, BytesParser[dict]]]): the parser that will parse the - received bytestream into a JSON dict + parser (Parser): the parser that will parse the received bytestream into a JSON dict Returns: Callable: Generated method to perform command @@ -466,8 +344,8 @@ def ble_read_command( message = BleReadCommand(uuid, parser) @wrapt.decorator - def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: - return message(instance._communicator, **(wrapped(**kwargs) or kwargs)) + async def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: + return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) return wrapper @@ -475,28 +353,29 @@ def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> Go def ble_register_command( uuid: BleUUID, cmd: CmdId, - producer: ProtobufProducerType, + update_set: type[SettingId] | type[StatusId], + responded_cmd: QueryCmdId, action: RegisterUnregisterAll.Action, - parser: Optional[BytesParser] = None, + parser: Parser | None = None, ) -> Callable: """Factory to build a RegisterUnregisterAll command and wrapper to execute it Args: uuid (BleUUID): UUID to write to cmd (CmdId): Command ID that is being sent - producer (ProtobufProducerType): Tuple of (element_set, query command) where element_set is the GoProEnum - that this command relates to, i.e. SettingId for settings, StatusId for Statuses + update_set (type[SettingId] | type[StatusId]): set of ID's being registered for + responded_cmd (QueryCmdId): not currently used action (Action): whether to register or unregister - parser (Optional[BytesParser], optional): Optional response parser. Defaults to None. + parser (Parser, optional): Optional response parser. Defaults to None. Returns: Callable: Generated method to perform command """ - message = RegisterUnregisterAll(uuid, cmd, producer, action, parser) + message = RegisterUnregisterAll(uuid, cmd, update_set, responded_cmd, action, parser) @wrapt.decorator - def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: - return message(instance._communicator, **(wrapped(**kwargs) or kwargs)) + async def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: + return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) return wrapper @@ -506,10 +385,10 @@ def ble_proto_command( feature_id: FeatureId, action_id: ActionId, response_action_id: ActionId, - request_proto: type[Protobuf], - response_proto: type[Protobuf], - additional_matching_ids: Optional[set[Union[ActionId, CmdId]]] = None, - additional_parsers: Optional[list[JsonParser]] = None, + request_proto: type[types.Protobuf], + response_proto: type[types.Protobuf], + parser: Parser | None = None, + additional_matching_ids: set[ActionId | CmdId] | None = None, ) -> Callable: """Factory to build a BLE Protobuf command and wrapper to execute it @@ -518,13 +397,12 @@ def ble_proto_command( feature_id (FeatureId): Feature ID that is being executed action_id (ActionId): protobuf specific action ID that is being executed response_action_id (ActionId): the action ID that will be in the response to this command - request_proto (type[Protobuf]): the action ID that will be in the response - response_proto (type[Protobuf]): protobuf used to parse received bytestream + request_proto (type[types.Protobuf]): the action ID that will be in the response + response_proto (type[types.Protobuf]): protobuf used to parse received bytestream + parser (Parser | None, optional): _description_. Defaults to None. additional_matching_ids (Optional[set[Union[ActionId, CmdId]]], optional): Other action ID's to share this parser. This is used, for example, if a notification shares the same ID as the synchronous response. Defaults to None.. Defaults to None. - additional_parsers (Optional[list[JsonParser]], optional): Any additional JSON parsers to apply - after normal protobuf response parsing. Defaults to None. Returns: Callable: Generated method to perform command @@ -536,13 +414,13 @@ def ble_proto_command( response_action_id, request_proto, response_proto, + parser, additional_matching_ids, - additional_parsers, ) @wrapt.decorator - def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: - return message(instance._communicator, **(wrapped(**kwargs) or kwargs)) + async def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: + return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) return wrapper @@ -554,23 +432,11 @@ class BleAsyncResponse: Args: feature_id (FeatureId): Feature ID that response corresponds to action_id (ActionId): Action ID that response corresponds to - parser_type (AsyncParserType): how to parse the response """ feature_id: FeatureId action_id: ActionId - parser_type: InitVar[AsyncParserType] - parser: Parser = field(init=False) - - def __post_init__(self, parser_type: AsyncParserType) -> None: - if isinstance(parser_type, construct.Construct): - self.parser = construct_adapter_factory(parser_type) - elif isinstance(parser_type, BytesParser): - self.parser = parser_type - elif issubclass(parser_type, Protobuf): - self.parser = protobuf_parser_factory(parser_type) - else: - raise TypeError(f"Unexpected {parser_type=}") + parser: Parser def __str__(self) -> str: return self.action_id.name.lower().replace("_", " ").removeprefix("actionid").title() @@ -593,23 +459,25 @@ def __init__(self, communicator: GoProBle, identifier: SettingId, parser_builder Raises: TypeError: Invalid parser_builder type """ + # TODO abstract this + parser = Parser[types.CameraState]() if isinstance(parser_builder, construct.Construct): - parser = construct_adapter_factory(parser_builder) + parser.byte_json_adapter = ByteParserBuilders.Construct(parser_builder) elif isinstance(parser_builder, BytesParserBuilder): - parser = parser_builder + parser.byte_json_adapter = parser_builder elif issubclass(parser_builder, GoProEnum): - parser = enum_parser_factory(parser_builder) + parser.byte_json_adapter = ByteParserBuilders.GoProEnum(parser_builder) else: raise TypeError(f"Unexpected {parser_builder=}") self._identifier = identifier - self._builder = parser + self._builder = parser.byte_json_adapter self._communicator = communicator BleMessage.__init__(self, uuid=self.SETTER_UUID, parser=parser, identifier=identifier) def __str__(self) -> str: return str(self._identifier).lower().replace("_", " ").title() - def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> Any: + async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> Any: """Not applicable for a BLE setting Args: @@ -622,8 +490,8 @@ def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> Any: raise NotImplementedError def _as_dict( # pylint: disable = arguments-differ - self, identifier: Union[QueryCmdId, SettingId, str], *_: Any, **kwargs: Any - ) -> dict[str, Any]: + self, identifier: QueryCmdId | SettingId | str, *_: Any, **kwargs: Any + ) -> types.JsonDict: """Return the attributes of the message as a dict Args: @@ -632,11 +500,11 @@ def _as_dict( # pylint: disable = arguments-differ **kwargs (Any): additional entries for the dict Returns: - dict[str, Any]: setting as dict + types.JsonDict: setting as dict """ - return {"id": identifier, **self._base_dict} | kwargs # type: ignore + return {"id": identifier, **self._base_dict} | kwargs - def set(self, value: ValueType) -> GoProResp: + async def set(self, value: ValueType) -> GoProResp[None]: """Set the value of the setting. Args: @@ -645,9 +513,7 @@ def set(self, value: ValueType) -> GoProResp: Returns: GoProResp: Status of set """ - logger.info( - Logger.build_log_tx_str(jsonify(self._as_dict(f"Set {str(self._identifier)}", value=value))) - ) + logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(f"Set {str(self._identifier)}", value=value)))) # Special case. Can't use _send_query data = bytearray([int(self._identifier)]) try: @@ -656,11 +522,11 @@ def set(self, value: ValueType) -> GoProResp: except IndexError: pass - response = self._communicator._send_ble_message(self.SETTER_UUID, data, self._identifier) + response = await self._communicator._send_ble_message(self.SETTER_UUID, data, self._identifier) logger.info(Logger.build_log_rx_str(response)) return response - def _send_query(self, response_id: QueryCmdId) -> GoProResp: + async def _send_query(self, response_id: QueryCmdId) -> GoProResp[types.CameraState | None]: """Build the byte data and query setting information Args: @@ -670,22 +536,20 @@ def _send_query(self, response_id: QueryCmdId) -> GoProResp: GoProResp: query response """ data = self._build_cmd(response_id) - logger.info( - Logger.build_log_tx_str(jsonify(self._as_dict(f"{str(response_id)}.{str(self._identifier)}"))) - ) - response = self._communicator._send_ble_message(self.READER_UUID, data, response_id) + logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(f"{str(response_id)}.{str(self._identifier)}")))) + response = await self._communicator._send_ble_message(self.READER_UUID, data, response_id) logger.info(Logger.build_log_rx_str(response)) return response - def get_value(self) -> GoProResp: + async def get_value(self) -> GoProResp[ValueType]: """Get the settings value. Returns: GoProResp: settings value """ - return self._send_query(QueryCmdId.GET_SETTING_VAL) + return await self._send_query(QueryCmdId.GET_SETTING_VAL) # type: ignore - def get_name(self) -> GoProResp: + async def get_name(self) -> GoProResp[str]: """Get the settings name. Raises: @@ -693,15 +557,15 @@ def get_name(self) -> GoProResp: """ raise NotImplementedError("Not implemented on camera!") - def get_capabilities_values(self) -> GoProResp: + async def get_capabilities_values(self) -> GoProResp[list[ValueType]]: """Get currently supported settings capabilities values. Returns: GoProResp: settings capabilities values """ - return self._send_query(QueryCmdId.GET_CAPABILITIES_VAL) + return await self._send_query(QueryCmdId.GET_CAPABILITIES_VAL) # type: ignore - def get_capabilities_names(self) -> GoProResp: + async def get_capabilities_names(self) -> GoProResp[list[str]]: """Get currently supported settings capabilities names. Raises: @@ -709,45 +573,57 @@ def get_capabilities_names(self) -> GoProResp: """ raise NotImplementedError("Not implemented on camera!") - def register_value_update(self) -> GoProResp: + async def register_value_update(self, callback: types.UpdateCb) -> GoProResp[None]: """Register for asynchronous notifications when a given setting ID's value updates. + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: Current value of respective setting ID """ - if (response := self._send_query(QueryCmdId.REG_SETTING_VAL_UPDATE)).is_ok: - self._communicator._register_listener((QueryCmdId.SETTING_VAL_PUSH, self._identifier)) - return response + if (response := await self._send_query(QueryCmdId.REG_SETTING_VAL_UPDATE)).ok: + self._communicator.register_update(callback, self._identifier) + return response # type: ignore - def unregister_value_update(self) -> GoProResp: + async def unregister_value_update(self, callback: types.UpdateCb) -> GoProResp[None]: """Stop receiving notifications when a given setting ID's value updates. + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: Status of unregister """ - if (response := self._send_query(QueryCmdId.UNREG_SETTING_VAL_UPDATE)).is_ok: - self._communicator._unregister_listener((QueryCmdId.SETTING_VAL_PUSH, self._identifier)) - return response + if (response := await self._send_query(QueryCmdId.UNREG_SETTING_VAL_UPDATE)).ok: + self._communicator.unregister_update(callback, self._identifier) + return response # type: ignore - def register_capability_update(self) -> GoProResp: + async def register_capability_update(self, callback: types.UpdateCb) -> GoProResp[None]: """Register for asynchronous notifications when a given setting ID's capabilities update. + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: Current capabilities of respective setting ID """ - if (response := self._send_query(QueryCmdId.REG_CAPABILITIES_UPDATE)).is_ok: - self._communicator._register_listener((QueryCmdId.SETTING_CAPABILITY_PUSH, self._identifier)) - return response + if (response := await self._send_query(QueryCmdId.REG_CAPABILITIES_UPDATE)).ok: + self._communicator.register_update(callback, self._identifier) + return response # type: ignore - def unregister_capability_update(self) -> GoProResp: + async def unregister_capability_update(self, callback: types.UpdateCb) -> GoProResp[None]: """Stop receiving notifications when a given setting ID's capabilities change. + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: Status of unregister """ - if (response := self._send_query(QueryCmdId.UNREG_CAPABILITIES_UPDATE)).is_ok: - self._communicator._unregister_listener((QueryCmdId.SETTING_CAPABILITY_PUSH, self._identifier)) - return response + if (response := await self._send_query(QueryCmdId.UNREG_CAPABILITIES_UPDATE)).ok: + self._communicator.unregister_update(callback, self._identifier) + return response # type: ignore def _build_cmd(self, cmd: QueryCmdId) -> bytearray: """Build the data to send a settings query over-the-air. @@ -762,7 +638,7 @@ def _build_cmd(self, cmd: QueryCmdId) -> bytearray: return ret -class BleStatus(BleMessage[StatusId]): +class BleStatus(BleMessage[StatusId], Generic[ValueType]): """An individual camera status that is interacted with via BLE.""" UUID: Final[BleUUID] = GoProUUIDs.CQ_QUERY @@ -778,19 +654,23 @@ def __init__(self, communicator: GoProBle, identifier: StatusId, parser: QueryPa Raises: TypeError: Invalid parser type """ + # TODO abstract this + parser_builder = Parser[types.CameraState]() + # Is it a protobuf enum? if isinstance(parser, construct.Construct): - parser_builder = construct_adapter_factory(parser) + parser_builder.byte_json_adapter = ByteParserBuilders.Construct(parser) elif isinstance(parser, BytesParserBuilder): - parser_builder = parser + parser_builder.byte_json_adapter = parser elif issubclass(parser, GoProEnum): - parser_builder = enum_parser_factory(parser) + parser_builder.byte_json_adapter = ByteParserBuilders.GoProEnum(parser) else: - raise TypeError(f"Unexpected {parser=}") + raise TypeError(f"Unexpected {parser_builder=}") + self._communicator = communicator BleMessage.__init__(self, uuid=self.UUID, parser=parser_builder, identifier=identifier) self._identifier = identifier - def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> Any: + async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> Any: """Not applicable for a BLE status Args: @@ -805,7 +685,7 @@ def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> Any: def __str__(self) -> str: return str(self._identifier).lower().replace("_", " ").title() - def _send_query(self, response_id: QueryCmdId) -> GoProResp: + async def _send_query(self, response_id: QueryCmdId) -> GoProResp: """Build the byte data and query setting information Args: @@ -815,19 +695,17 @@ def _send_query(self, response_id: QueryCmdId) -> GoProResp: GoProResp: query response """ data = self._build_cmd(response_id) - logger.info( - Logger.build_log_tx_str(jsonify(self._as_dict(f"{response_id.name}.{str(self._identifier)}"))) - ) - response = self._communicator._send_ble_message(self.UUID, data, response_id) + logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(f"{response_id.name}.{str(self._identifier)}")))) + response = await self._communicator._send_ble_message(self.UUID, data, response_id) logger.info(Logger.build_log_rx_str(response)) return response def _as_dict( # pylint: disable = arguments-differ self, - identifier: Union[QueryCmdId, SettingId, str], + identifier: QueryCmdId | SettingId | str, *_: Any, **kwargs: Any, - ) -> dict[str, Any]: + ) -> types.JsonDict: """Return the attributes of the command as a dict Args: @@ -836,36 +714,42 @@ def _as_dict( # pylint: disable = arguments-differ **kwargs (Any): additional entries for the dict Returns: - dict[str, Any]: command as dict + types.JsonDict: command as dict """ - return {"id": identifier, **self._base_dict} | kwargs # type: ignore + return {"id": identifier, **self._base_dict} | kwargs - def get_value(self) -> GoProResp: + async def get_value(self) -> GoProResp[ValueType]: """Get the current value of a status. Returns: GoProResp: current status value """ - return self._send_query(QueryCmdId.GET_STATUS_VAL) + return await self._send_query(QueryCmdId.GET_STATUS_VAL) - def register_value_update(self) -> GoProResp: + async def register_value_update(self, callback: types.UpdateCb) -> GoProResp[ValueType]: """Register for asynchronous notifications when a status changes. + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: current status value """ - if (response := self._send_query(QueryCmdId.REG_STATUS_VAL_UPDATE)).is_ok: - self._communicator._register_listener((QueryCmdId.STATUS_VAL_PUSH, self._identifier)) + if (response := await self._send_query(QueryCmdId.REG_STATUS_VAL_UPDATE)).ok: + self._communicator.register_update(callback, self._identifier) return response - def unregister_value_update(self) -> GoProResp: + async def unregister_value_update(self, callback: types.UpdateCb) -> GoProResp: """Stop receiving notifications when status changes. + Args: + callback (types.UpdateCb): callback to be notified with + Returns: GoProResp: Status of unregister """ - if (response := self._send_query(QueryCmdId.UNREG_STATUS_VAL_UPDATE)).is_ok: - self._communicator._unregister_listener((QueryCmdId.STATUS_VAL_PUSH, self._identifier)) + if (response := await self._send_query(QueryCmdId.UNREG_STATUS_VAL_UPDATE)).ok: + self._communicator.unregister_update(callback, self._identifier) return response def _build_cmd(self, cmd: QueryCmdId) -> bytearray: @@ -890,11 +774,11 @@ class HttpCommand(HttpMessage[str]): def __init__( self, endpoint: str, - components: Optional[list[str]] = None, - arguments: Optional[list[str]] = None, - parser: Optional[JsonParser] = None, - identifier: Optional[str] = None, - rules: Optional[dict[MessageRules, RuleSignature]] = None, + components: list[str] | None = None, + arguments: list[str] | None = None, + parser: Parser | None = None, + identifier: str | None = None, + rules: dict[MessageRules, RuleSignature] | None = None, ) -> None: """Constructor @@ -921,8 +805,8 @@ def __init__( class HttpGetJsonCommand(HttpCommand): """An HTTP command that performs a GET operation and receives JSON as response""" - def __call__( - self, __communicator__: GoProHttp, rules: Optional[list[MessageRules]] = None, **kwargs: Any + async def __call__( + self, __communicator__: GoProHttp, rules: list[MessageRules] | None = None, **kwargs: Any ) -> GoProResp: """Execute the command by sending it via HTTP @@ -953,9 +837,9 @@ def __call__( url += "?" + arg_part # Send to camera - logger.info(Logger.build_log_tx_str(jsonify(self._as_dict(**kwargs, endpoint=url)))) - response = __communicator__._get(url, self._parser, rules=rules) - response._meta.append(self._identifier) + logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(**kwargs, endpoint=url)))) + response = await __communicator__._http_get(url, self._parser, rules=rules) + response.identifier = self._identifier logger.info(Logger.build_log_rx_str(response)) return response @@ -964,8 +848,8 @@ def __call__( class HttpGetBinary(HttpCommand): """An HTTP command that performs a GET operation and receives a binary stream as response""" - def __call__( # type: ignore - self, __communicator__: GoProHttp, *, camera_file: str, local_file: Optional[Path] = None + async def __call__( # type: ignore + self, __communicator__: GoProHttp, *, camera_file: str, local_file: Path | None = None ) -> GoProResp: """Execute the command by getting the binary data from the communicator @@ -983,26 +867,24 @@ def __call__( # type: ignore url = self._endpoint + "/" + camera_file logger.info( Logger.build_log_tx_str( - jsonify(self._as_dict(endpoint=url, camera_file=camera_file, local_file=local_file)) + pretty_print(self._as_dict(endpoint=url, camera_file=camera_file, local_file=local_file)) ) ) # Send to camera - response = __communicator__._stream_to_file(url, local_file) + response = await __communicator__._stream_to_file(url, local_file) logger.info( - Logger.build_log_rx_str( - jsonify(self._as_dict(status="SUCCESS", endpoint=url, local_file=local_file)) - ) + Logger.build_log_rx_str(pretty_print(self._as_dict(status="SUCCESS", endpoint=url, local_file=local_file))) ) return response def http_get_json_command( endpoint: str, - components: Optional[list[str]] = None, - arguments: Optional[list[str]] = None, - parser: Optional[JsonParser] = None, - identifier: Optional[str] = None, - rules: Optional[dict[MessageRules, RuleSignature]] = None, + components: list[str] | None = None, + arguments: list[str] | None = None, + parser: Parser | None = None, + identifier: str | None = None, + rules: dict[MessageRules, RuleSignature] | None = None, ) -> Callable: """Factory to build an HttpGetJson command and wrapper to execute it @@ -1020,9 +902,9 @@ def http_get_json_command( message = HttpGetJsonCommand(endpoint, components, arguments, parser, identifier, rules=rules) @wrapt.decorator - def wrapper(wrapped: Callable, instance: HttpMessages, _: Any, kwargs: Any) -> GoProResp: - return message( - instance._communicator, message._evaluate_rules(**kwargs), **(wrapped(**kwargs) or kwargs) + async def wrapper(wrapped: Callable, instance: HttpMessages, _: Any, kwargs: Any) -> GoProResp: + return await message( + instance._communicator, message._evaluate_rules(**kwargs), **(await wrapped(**kwargs) or kwargs) ) return wrapper @@ -1030,12 +912,12 @@ def wrapper(wrapped: Callable, instance: HttpMessages, _: Any, kwargs: Any) -> G def http_get_binary_command( endpoint: str, - components: Optional[list[str]] = None, - arguments: Optional[list[str]] = None, - parser: Optional[JsonParser] = None, - identifier: Optional[str] = None, + components: list[str] | None = None, + arguments: list[str] | None = None, + parser: Parser | None = None, + identifier: str | None = None, ) -> Callable: - """Factory to build am HttpGetBinary command and wrapper to execute it + """Factory to build an HttpGetBinary command and wrapper to execute it Args: endpoint (str): base endpoint @@ -1050,8 +932,8 @@ def http_get_binary_command( message = HttpGetBinary(endpoint, components, arguments, parser, identifier) @wrapt.decorator - def wrapper(wrapped: Callable, instance: HttpMessages, _: Any, kwargs: Any) -> GoProResp: - return message(instance._communicator, **(wrapped(**kwargs) or kwargs)) + async def wrapper(wrapped: Callable, instance: HttpMessages, _: Any, kwargs: Any) -> GoProResp: + return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) return wrapper @@ -1068,7 +950,7 @@ def __init__(self, communicator: GoProHttp, identifier: SettingId) -> None: # Note! It is assumed that BLE and HTTP settings are symmetric so we only add to the communicator's # parser in the BLE Setting. - def __call__(self, __communicator__: GoProHttp, **kwargs: Any) -> Any: + async def __call__(self, __communicator__: GoProHttp, **kwargs: Any) -> Any: """Not applicable for settings Args: @@ -1083,7 +965,7 @@ def __call__(self, __communicator__: GoProHttp, **kwargs: Any) -> Any: def __str__(self) -> str: return str(self._identifier).lower().replace("_", " ").title() - def set(self, value: ValueType) -> GoProResp: + async def set(self, value: ValueType) -> GoProResp: """Set the value of the setting. Args: @@ -1093,10 +975,15 @@ def set(self, value: ValueType) -> GoProResp: GoProResp: Status of set """ url = self._endpoint.format(int(self._identifier), value) - logger.info(Logger.build_log_tx_str(jsonify(self._as_dict(value=value, endpoint=url)))) + logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(value=value, endpoint=url)))) value = value.value if isinstance(value, enum.Enum) else value # Send to camera - if response := self._communicator._get(url): - response._meta.append(self._identifier) + if response := await self._communicator._http_get( + url, + parser=Parser( + json_parser=JsonParsers.LambdaParser(lambda data: HttpInvalidSettingResponse(**data) if data else data) + ), + ): + response.identifier = self._identifier logger.info(Logger.build_log_rx_str(response)) return response diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/api/http_commands.py b/demos/python/sdk_wireless_camera_control/open_gopro/api/http_commands.py index 145a7d0f..61b4ea6c 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/api/http_commands.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/http_commands.py @@ -6,66 +6,34 @@ # mypy: disable-error-code=empty-body from __future__ import annotations -import logging + import datetime +import logging from pathlib import Path -from typing import Any, Optional -from open_gopro.interface import GoProHttp, HttpMessage, HttpMessages, MessageRules -from open_gopro.constants import SettingId, StatusId, CmdId -from open_gopro.responses import GoProResp, JsonParser -from open_gopro.api.builders import HttpSetting, http_get_binary_command, http_get_json_command +from open_gopro import proto, types +from open_gopro.api.builders import ( + HttpSetting, + http_get_binary_command, + http_get_json_command, +) +from open_gopro.api.parsers import JsonParsers +from open_gopro.communicator_interface import ( + GoProHttp, + HttpMessage, + HttpMessages, + MessageRules, +) +from open_gopro.constants import CmdId, SettingId +from open_gopro.models import CameraInfo, MediaList, MediaMetadata +from open_gopro.models.general import WebcamResponse +from open_gopro.models.response import GoProResp +from open_gopro.parser_interface import Parser + from . import params as Params logger = logging.getLogger(__name__) -# pylint: disable = missing-class-docstring -class HttpParsers: - """The collection of parsers used for additional JSON parsing""" - - class CameraStateParser(JsonParser): - """Parse integer numbers into Enums""" - - def parse(self, data: dict) -> dict: - """Parse dict of integer values into human readable (i.e. enum'ed) setting / status map - - Args: - data (dict): input dict to parse - - Returns: - dict: output human readable dict - """ - parsed: dict[Any, Any] = {} - # Parse status and settings values into nice human readable things - for (name, id_map) in [("status", StatusId), ("settings", SettingId)]: - for k, v in data[name].items(): - identifier = id_map(int(k)) - try: - parsed[identifier] = ( - container(v) if (container := GoProResp._get_query_container(identifier)) else v # type: ignore - ) - except ValueError: - # This is the case where we receive a value that is not defined in our params. - # This shouldn't happen and is either a firmware bug or means the documentation needs to - # be updated. However, it isn't functionally critical. - logger.warning(f"{identifier.name} does not contain a value {v}") - parsed[identifier] = v - return parsed - - class MediaListParser(JsonParser): - """Extract the list of files from the media list JSON""" - - def parse(self, data: dict) -> dict: - """Get the list of files from the media list - - Args: - data (dict): media list response - - Returns: - dict: list of files - """ - return {"files": data["media"][0]["fs"] if data["media"] else []} - class HttpCommands(HttpMessages[HttpMessage, CmdId]): """All of the HTTP commands. @@ -78,7 +46,7 @@ class HttpCommands(HttpMessages[HttpMessage, CmdId]): ##################################################################################################### @http_get_json_command(endpoint="gopro/camera/digital_zoom", arguments=["percent"]) - def set_digital_zoom(self, *, percent: int) -> GoProResp: + async def set_digital_zoom(self, *, percent: int) -> GoProResp[None]: """Set digital zoom in percent. Args: @@ -90,38 +58,55 @@ def set_digital_zoom(self, *, percent: int) -> GoProResp: @http_get_json_command( endpoint="gopro/camera/state", - parser=HttpParsers.CameraStateParser(), + parser=Parser(json_parser=JsonParsers.CameraStateParser()), rules={MessageRules.FASTPASS: lambda **kwargs: True}, ) - def get_camera_state(self) -> GoProResp: + async def get_camera_state(self) -> GoProResp[types.CameraState]: """Get all camera statuses and settings Returns: GoProResp: status and settings as JSON """ + @http_get_json_command( + endpoint="gopro/camera/info", parser=Parser(json_parser=JsonParsers.PydanticAdapter(CameraInfo)) + ) + async def get_camera_info(self) -> GoProResp[CameraInfo]: + """Get general information about the camera such as firmware version + + Returns: + GoProResp: status and settings as JSON + """ + @http_get_json_command(endpoint="gopro/camera/keep_alive") - def set_keep_alive(self) -> GoProResp: + async def set_keep_alive(self) -> GoProResp[None]: """Send the keep alive signal to maintain the connection. Returns: GoProResp: command status """ - @http_get_json_command(endpoint="gopro/media/info", arguments=["path"]) - def get_media_info(self, *, file: str) -> GoProResp: - """Get media info for a file. + @http_get_json_command( + endpoint="gopro/media/info", + arguments=["path"], + parser=Parser(json_parser=JsonParsers.PydanticAdapter(MediaMetadata)), + ) + async def get_media_metadata(self, *, file: str) -> GoProResp[MediaMetadata]: + """Get media metadata for a file. Args: - file (str): Media file to get info for + file (str): Media file to get metadata for Returns: - GoProResp: Media info as JSON + GoProResp: Media metadata JSON structure """ return {"path": f"100GOPRO/{file}"} # type: ignore - @http_get_json_command(endpoint="gopro/media/list", parser=HttpParsers.MediaListParser()) - def get_media_list(self) -> GoProResp: + @http_get_json_command( + endpoint="gopro/media/list", + parser=Parser(json_parser=JsonParsers.PydanticAdapter(MediaList)), + ) + async def get_media_list(self) -> GoProResp[MediaList]: """Get a list of media on the camera. Returns: @@ -129,7 +114,7 @@ def get_media_list(self) -> GoProResp: """ @http_get_json_command(endpoint="gopro/media/turbo_transfer", arguments=["p"]) - def set_turbo_mode(self, *, mode: Params.Toggle) -> GoProResp: + async def set_turbo_mode(self, *, mode: Params.Toggle) -> GoProResp[None]: """Enable or disable Turbo transfer mode. Args: @@ -140,16 +125,20 @@ def set_turbo_mode(self, *, mode: Params.Toggle) -> GoProResp: """ return {"p": mode} # type: ignore - @http_get_json_command(endpoint="gopro/version") - def get_open_gopro_api_version(self) -> GoProResp: + @http_get_json_command( + endpoint="gopro/version", + parser=Parser(json_parser=JsonParsers.LambdaParser(lambda data: f"{data['version']}")), + ) + async def get_open_gopro_api_version(self) -> GoProResp[str]: """Get Open GoPro API version Returns: GoProResp: Open GoPro Version """ + # TODO make pydantic @http_get_json_command(endpoint="gopro/camera/presets/get") - def get_preset_status(self) -> GoProResp: + async def get_preset_status(self) -> GoProResp[types.JsonDict]: """Get status of current presets Returns: @@ -157,7 +146,7 @@ def get_preset_status(self) -> GoProResp: """ @http_get_json_command(endpoint="gopro/camera/presets/load", arguments=["id"]) - def load_preset(self, *, preset: int) -> GoProResp: + async def load_preset(self, *, preset: int) -> GoProResp[None]: """Set camera to a given preset The preset ID can be found from :py:class:`open_gopro.api.http_commands.HttpCommands.get_preset_status` @@ -171,13 +160,13 @@ def load_preset(self, *, preset: int) -> GoProResp: return {"id": preset} # type: ignore @http_get_json_command(endpoint="gopro/camera/presets/set_group", arguments=["id"]) - def load_preset_group(self, *, group: Params.PresetGroup) -> GoProResp: + async def load_preset_group(self, *, group: proto.EnumPresetGroup) -> GoProResp[None]: """Set the active preset group. The most recently used Preset in this group will be set. Args: - group (open_gopro.api.params.PresetGroup): desired Preset Group + group (open_gopro.proto.EnumPresetGroup): desired Preset Group Returns: GoProResp: command status @@ -185,7 +174,7 @@ def load_preset_group(self, *, group: Params.PresetGroup) -> GoProResp: return {"id": group} # type: ignore @http_get_json_command(endpoint="gopro/camera/stream", components=["mode"], identifier="Preview Stream") - def set_preview_stream(self, *, mode: Params.Toggle) -> GoProResp: + async def set_preview_stream(self, *, mode: Params.Toggle) -> GoProResp[None]: """Start or stop the preview stream Args: @@ -197,7 +186,7 @@ def set_preview_stream(self, *, mode: Params.Toggle) -> GoProResp: return {"mode": "start" if mode is Params.Toggle.ENABLE else "stop"} # type: ignore @http_get_json_command(endpoint="gopro/camera/analytics/set_client_info") - def set_third_party_client_info(self) -> GoProResp: + async def set_third_party_client_info(self) -> GoProResp[None]: """Flag as third party app Returns: @@ -212,7 +201,7 @@ def set_third_party_client_info(self) -> GoProResp: MessageRules.WAIT_FOR_ENCODING_START: lambda **kwargs: kwargs["shutter"] == Params.Toggle.ENABLE, }, ) - def set_shutter(self, *, shutter: Params.Toggle) -> GoProResp: + async def set_shutter(self, *, shutter: Params.Toggle) -> GoProResp[None]: """Set the shutter on or off Args: @@ -224,7 +213,7 @@ def set_shutter(self, *, shutter: Params.Toggle) -> GoProResp: return {"mode": "start" if shutter is Params.Toggle.ENABLE else "stop"} # type: ignore @http_get_json_command(endpoint="gopro/camera/control/set_ui_controller", arguments=["p"]) - def set_camera_control(self, *, mode: Params.CameraControl) -> GoProResp: + async def set_camera_control(self, *, mode: Params.CameraControl) -> GoProResp[None]: """Configure global behaviors by setting camera control (to i.e. Idle, External) Args: @@ -236,9 +225,13 @@ def set_camera_control(self, *, mode: Params.CameraControl) -> GoProResp: return {"p": mode} # type: ignore @http_get_json_command(endpoint="gopro/camera/set_date_time", arguments=["date", "time", "tzone", "dst"]) - def set_date_time( - self, *, date_time: datetime.datetime, tz_offset: int = 0, is_dst: bool = False - ) -> GoProResp: + async def set_date_time( + self, + *, + date_time: datetime.datetime, + tz_offset: int = 0, + is_dst: bool = False, + ) -> GoProResp[None]: """Update the date and time of the camera Args: @@ -256,76 +249,98 @@ def set_date_time( "dst": int(is_dst), } + # TODO @http_get_json_command(endpoint="gopro/camera/get_date_time") - def get_date_time(self) -> GoProResp: + async def get_date_time(self) -> GoProResp[datetime.datetime]: """Get the date and time of the camera (Non timezone / DST aware) Returns: GoProResp: current date and time on camera """ - @http_get_json_command(endpoint="gopro/webcam/status") - def get_webcam_status(self) -> GoProResp: - """Get the status of the webcam endpoint - - Returns: - GoProResp: webcam status - """ - @http_get_json_command(endpoint="gopro/webcam/version") - def get_webcam_version(self) -> GoProResp: + async def get_webcam_version(self) -> GoProResp[str]: """Get the version of the webcam implementation Returns: GoProResp: version """ - @http_get_json_command(endpoint="gopro/media/hilight/file", arguments=["path", "ms"]) - def add_file_hilight(self, *, file: str, offset: Optional[int] = None) -> GoProResp: + @http_get_json_command( + endpoint="gopro/media/hilight/file", + arguments=["path", "ms"], + ) + async def add_file_hilight( + self, + *, + file: str, + offset: int | None = None, + ) -> GoProResp[None]: """Add a hilight to a media file (.mp4) Args: file (str): the media to add the hilight to - offset (Optional[int]): offset in ms from start of media + offset (int | None): offset in ms from start of media Returns: GoProResp: command status """ return {"path": f"100GOPRO/{file}", "ms": offset or None} # type: ignore - @http_get_json_command(endpoint="gopro/media/hilight/remove", arguments=["path", "ms"]) - def remove_file_hilight(self, *, file: str, offset: Optional[int] = None) -> GoProResp: + @http_get_json_command( + endpoint="gopro/media/hilight/remove", + arguments=["path", "ms"], + ) + async def remove_file_hilight( + self, + *, + file: str, + offset: int | None = None, + ) -> GoProResp[None]: """Remove a hilight from a media file (.mp4) Args: file (str): the media to remove the hilight from - offset (Optional[int]): offset in ms from start of media + offset (int | None): offset in ms from start of media Returns: GoProResp: command status """ return {"path": f"100GOPRO/{file}", "ms": offset} # type: ignore - @http_get_json_command(endpoint="gopro/webcam/exit") - def webcam_exit(self) -> GoProResp: + @http_get_json_command( + endpoint="gopro/webcam/exit", + parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + ) + async def webcam_exit(self) -> GoProResp[WebcamResponse]: """Exit the webcam. Returns: GoProResp: command status """ - @http_get_json_command(endpoint="gopro/webcam/preview") - def webcam_preview(self) -> GoProResp: + @http_get_json_command( + endpoint="gopro/webcam/preview", + parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + ) + async def webcam_preview(self) -> GoProResp[WebcamResponse]: """Start the webcam preview. Returns: GoProResp: command status """ - @http_get_json_command(endpoint="gopro/webcam/start", arguments=["res", "fov"]) - def webcam_start( - self, *, resolution: Optional[Params.WebcamResolution] = None, fov: Optional[Params.WebcamFOV] = None - ) -> GoProResp: + @http_get_json_command( + endpoint="gopro/webcam/start", + arguments=["res", "fov"], + parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + ) + async def webcam_start( + self, + *, + resolution: Params.WebcamResolution | None = None, + fov: Params.WebcamFOV | None = None, + ) -> GoProResp[WebcamResponse]: """Start the webcam. Args: @@ -339,24 +354,34 @@ def webcam_start( """ return {"res": resolution, "fov": fov} # type: ignore - @http_get_json_command(endpoint="gopro/webcam/stop", rules={MessageRules.FASTPASS: lambda **kwargs: True}) - def webcam_stop(self) -> GoProResp: + @http_get_json_command( + endpoint="gopro/webcam/stop", + rules={MessageRules.FASTPASS: lambda **kwargs: True}, + parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + ) + async def webcam_stop(self) -> GoProResp[WebcamResponse]: """Stop the webcam. Returns: GoProResp: command status """ - @http_get_json_command(endpoint="gopro/webcam/status") - def webcam_status(self) -> GoProResp: + @http_get_json_command( + endpoint="gopro/webcam/status", + parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + ) + async def webcam_status(self) -> GoProResp[WebcamResponse]: """Get the current status of the webcam Returns: GoProResp: command status including the webcam status """ - @http_get_json_command(endpoint="gopro/camera/control/wired_usb", arguments=["p"]) - def wired_usb_control(self, *, control: Params.Toggle) -> GoProResp: + @http_get_json_command( + endpoint="gopro/camera/control/wired_usb", + arguments=["p"], + ) + async def wired_usb_control(self, *, control: Params.Toggle) -> GoProResp[None]: """Enable / disable wired usb control Args: @@ -372,70 +397,70 @@ def wired_usb_control(self, *, control: Params.Toggle) -> GoProResp: ###################################################################################################### @http_get_binary_command(endpoint="gopro/media/gpmf?path=100GOPRO") - def get_gpmf_data(self, *, camera_file: str, local_file: Optional[Path] = None) -> Path: + async def get_gpmf_data(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Get GPMF data for a file. If local_file is none, the output location will be the same name as the camera_file. Args: camera_file (str): filename on camera to operate on - local_file (Optional[Path]): Location on computer to write output. Defaults to None. + local_file (Path | None): Location on computer to write output. Defaults to None. Returns: Path: Path to local_file that output was written to """ @http_get_binary_command(endpoint="gopro/media/screennail?path=100GOPRO") - def get_screennail__call__(self, *, camera_file: str, local_file: Optional[Path] = None) -> Path: + async def get_screennail__call__(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Get screennail for a file. If local_file is none, the output location will be the same name as the camera_file. Args: camera_file (str): filename on camera to operate on - local_file (Optional[Path]): Location on computer to write output. Defaults to None. + local_file (Path | None): Location on computer to write output. Defaults to None. Returns: Path: Path to local_file that output was written to """ @http_get_binary_command(endpoint="gopro/media/thumbnail?path=100GOPRO") - def get_thumbnail(self, *, camera_file: str, local_file: Optional[Path] = None) -> Path: + async def get_thumbnail(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Get thumbnail for a file. If local_file is none, the output location will be the same name as the camera_file. Args: camera_file (str): filename on camera to operate on - local_file (Optional[Path]): Location on computer to write output. Defaults to None. + local_file (Path | None): Location on computer to write output. Defaults to None. Returns: Path: Path to local_file that output was written to """ @http_get_binary_command(endpoint="gopro/media/telemetry?path=100GOPRO") - def get_telemetry(self, *, camera_file: str, local_file: Optional[Path] = None) -> Path: + async def get_telemetry(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Download the telemetry data for a camera file and store in a local file. If local_file is none, the output location will be the same name as the camera_file. Args: camera_file (str): filename on camera to operate on - local_file (Optional[Path]): Location on computer to write output. Defaults to None. + local_file (Path | None): Location on computer to write output. Defaults to None. Returns: Path: Path to local_file that output was written to """ @http_get_binary_command(endpoint="videos/DCIM/100GOPRO", identifier="Download File") - def download_file(self, *, camera_file: str, local_file: Optional[Path] = None) -> Path: + async def download_file(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Download a video from the camera to a local file. If local_file is none, the output location will be the same name as the camera_file. Args: camera_file (str): filename on camera to operate on - local_file (Optional[Path]): Location on computer to write output. Defaults to None. + local_file (Path | None): Location on computer to write output. Defaults to None. Returns: Path: Path to local_file that output was written to @@ -459,9 +484,7 @@ def __init__(self, communicator: GoProHttp): self.fps: HttpSetting[Params.FPS] = HttpSetting[Params.FPS](communicator, SettingId.FPS) """Frames per second.""" - self.auto_off: HttpSetting[Params.AutoOff] = HttpSetting[Params.AutoOff]( - communicator, SettingId.AUTO_OFF - ) + self.auto_off: HttpSetting[Params.AutoOff] = HttpSetting[Params.AutoOff](communicator, SettingId.AUTO_OFF) """Set the auto off time.""" self.video_field_of_view: HttpSetting[Params.VideoFOV] = HttpSetting[Params.VideoFOV]( @@ -519,9 +542,7 @@ def __init__(self, communicator: GoProHttp): ) """Night Photo easy mode.""" - self.wifi_band: HttpSetting[Params.WifiBand] = HttpSetting[Params.WifiBand]( - communicator, SettingId.WIFI_BAND - ) + self.wifi_band: HttpSetting[Params.WifiBand] = HttpSetting[Params.WifiBand](communicator, SettingId.WIFI_BAND) """Current WiFi band being used.""" self.star_trail_length: HttpSetting[Params.StarTrailLength] = HttpSetting[Params.StarTrailLength]( @@ -544,4 +565,100 @@ def __init__(self, communicator: GoProHttp): ) """Lock / unlock horizon leveling for photo.""" + self.bit_rate: HttpSetting[Params.BitRate] = HttpSetting[Params.BitRate]( + communicator, + SettingId.BIT_RATE, + ) + """System Video Bit Rate.""" + + self.bit_depth: HttpSetting[Params.BitDepth] = HttpSetting[Params.BitDepth]( + communicator, + SettingId.BIT_DEPTH, + ) + """System Video Bit depth.""" + + self.video_profile: HttpSetting[Params.VideoProfile] = HttpSetting[Params.VideoProfile]( + communicator, + SettingId.VIDEO_PROFILE, + ) + """Video Profile (hdr, etc.)""" + + self.video_aspect_ratio: HttpSetting[Params.VideoAspectRatio] = HttpSetting[Params.VideoAspectRatio]( + communicator, + SettingId.VIDEO_ASPECT_RATIO, + ) + """Video aspect ratio""" + + self.video_easy_aspect_ratio: HttpSetting[Params.EasyAspectRatio] = HttpSetting[Params.EasyAspectRatio]( + communicator, + SettingId.VIDEO_EASY_ASPECT_RATIO, + ) + """Video easy aspect ratio""" + + self.multi_shot_easy_aspect_ratio: HttpSetting[Params.EasyAspectRatio] = HttpSetting[Params.EasyAspectRatio]( + communicator, + SettingId.MULTI_SHOT_EASY_ASPECT_RATIO, + ) + """Multi shot easy aspect ratio""" + + self.multi_shot_nlv_aspect_ratio: HttpSetting[Params.EasyAspectRatio] = HttpSetting[Params.EasyAspectRatio]( + communicator, + SettingId.MULTI_SHOT_NLV_ASPECT_RATIO, + ) + """Multi shot NLV aspect ratio""" + + self.video_mode: HttpSetting[Params.VideoMode] = HttpSetting[Params.VideoMode]( + communicator, + SettingId.VIDEO_MODE, + ) + """Video Mode (i.e. quality)""" + + self.timelapse_mode: HttpSetting[Params.TimelapseMode] = HttpSetting[Params.TimelapseMode]( + communicator, + SettingId.TIMELAPSE_MODE, + ) + """Timelapse Mode""" + + self.maxlens_mod_type: HttpSetting[Params.MaxLensModType] = HttpSetting[Params.MaxLensModType]( + communicator, + SettingId.ADDON_MAX_LENS_MOD, + ) + """Max lens mod? If so, what type?""" + + self.maxlens_status: HttpSetting[Params.Toggle] = HttpSetting[Params.Toggle]( + communicator, + SettingId.ADDON_MAX_LENS_MOD_ENABLE, + ) + """Enable / disable max lens mod""" + + self.photo_mode: HttpSetting[Params.PhotoMode] = HttpSetting[Params.PhotoMode]( + communicator, + SettingId.PHOTO_MODE, + ) + """Photo Mode""" + + self.framing: HttpSetting[Params.Framing] = HttpSetting[Params.Framing]( + communicator, + SettingId.FRAMING, + ) + """Video Framing Mode""" + + self.hindsight: HttpSetting[Params.Hindsight] = HttpSetting[Params.Hindsight]( + communicator, + SettingId.HINDSIGHT, + ) + """Hindsight time / disable""" + + self.photo_interval: HttpSetting[Params.PhotoInterval] = HttpSetting[Params.PhotoInterval]( + communicator, + SettingId.PHOTO_INTERVAL, + ) + """Interval between photo captures""" + + self.photo_duration: HttpSetting[Params.PhotoDuration] = HttpSetting[Params.PhotoDuration]( + communicator, + SettingId.PHOTO_INTERVAL_DURATION, + ) + """Interval between photo captures""" + super().__init__(communicator) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/api/params.py b/demos/python/sdk_wireless_camera_control/open_gopro/api/params.py index b4a05f57..eba5d28d 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/api/params.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/params.py @@ -7,108 +7,7 @@ from __future__ import annotations -from open_gopro.constants import GoProEnum, GoProFlagEnum - -# Import required parameters from protobuf -import open_gopro.proto.live_streaming_pb2 -import open_gopro.proto.network_management_pb2 -import open_gopro.proto.request_get_preset_status_pb2 -import open_gopro.proto.set_camera_control_status_pb2 - - -class ScanEntry(GoProFlagEnum): - AUTHENTICATED = open_gopro.proto.network_management_pb2.SCAN_FLAG_AUTHENTICATED - CONFIGURED = open_gopro.proto.network_management_pb2.SCAN_FLAG_CONFIGURED - BEST_SSID = open_gopro.proto.network_management_pb2.SCAN_FLAG_BEST_SSID - ASSOCIATED = open_gopro.proto.network_management_pb2.SCAN_FLAG_ASSOCIATED - UNSUPPORTED_TYPE = open_gopro.proto.network_management_pb2.SCAN_FLAG_UNSUPPORTED_TYPE - - -class ScanState(GoProEnum): - UNKNOWN = open_gopro.proto.network_management_pb2.SCANNING_UNKNOWN - NEVER_STARTED = open_gopro.proto.network_management_pb2.SCANNING_NEVER_STARTED - STARTED = open_gopro.proto.network_management_pb2.SCANNING_STARTED - ABORTED_BY_SYSTEM = open_gopro.proto.network_management_pb2.SCANNING_ABORTED_BY_SYSTEM - CANCELLED_BY_USER = open_gopro.proto.network_management_pb2.SCANNING_CANCELLED_BY_USER - SUCCESS = open_gopro.proto.network_management_pb2.SCANNING_SUCCESS - - -class LiveStreamStatus(GoProEnum): - IDLE = open_gopro.proto.live_streaming_pb2.EnumLiveStreamStatus.LIVE_STREAM_STATE_IDLE - CONFIG = open_gopro.proto.live_streaming_pb2.EnumLiveStreamStatus.LIVE_STREAM_STATE_CONFIG - READY = open_gopro.proto.live_streaming_pb2.EnumLiveStreamStatus.LIVE_STREAM_STATE_READY - STREAMING = open_gopro.proto.live_streaming_pb2.EnumLiveStreamStatus.LIVE_STREAM_STATE_STREAMING - STAY_ON_COMPLETE = ( - open_gopro.proto.live_streaming_pb2.EnumLiveStreamStatus.LIVE_STREAM_STATE_COMPLETE_STAY_ON - ) - STAY_ON_FAILED = open_gopro.proto.live_streaming_pb2.EnumLiveStreamStatus.LIVE_STREAM_STATE_FAILED_STAY_ON - RECONNECTING = open_gopro.proto.live_streaming_pb2.EnumLiveStreamStatus.LIVE_STREAM_STATE_RECONNECTING - - -class LensType(GoProEnum): - WIDE = open_gopro.proto.live_streaming_pb2.EnumLens.LENS_WIDE - LINEAR = open_gopro.proto.live_streaming_pb2.EnumLens.LENS_LINEAR - SUPERVIEW = open_gopro.proto.live_streaming_pb2.EnumLens.LENS_SUPERVIEW - - -class WindowSize(GoProEnum): - SIZE_480 = open_gopro.proto.live_streaming_pb2.EnumWindowSize.WINDOW_SIZE_480 - SIZE_720 = open_gopro.proto.live_streaming_pb2.EnumWindowSize.WINDOW_SIZE_720 - SIZE_1080 = open_gopro.proto.live_streaming_pb2.EnumWindowSize.WINDOW_SIZE_1080 - - -class ProvisioningState(GoProEnum): - UNKNOWN = open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_UNKNOWN - NEVER_STARTED = open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_NEVER_STARTED - STARTED = open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_STARTED - ABORTED_BY_SYSTEM = open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_ABORTED_BY_SYSTEM - CANCELLED_BY_USER = open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_CANCELLED_BY_USER - SUCCESS_NEW_AP = open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_SUCCESS_NEW_AP - SUCCESS_OLD_AP = open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_SUCCESS_OLD_AP - ERROR_FAILED_TO_ASSOCIATE = ( - open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_ERROR_FAILED_TO_ASSOCIATE - ) - ERROR_PASSWORD_AUTH = ( - open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_ERROR_PASSWORD_AUTH - ) - ERROR_EULA_BLOCKING = ( - open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_ERROR_EULA_BLOCKING - ) - ERROR_NO_INTERNET = open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_ERROR_NO_INTERNET - ERROR_UNSUPPORTED_TYPE = ( - open_gopro.proto.network_management_pb2.EnumProvisioning.PROVISIONING_ERROR_UNSUPPORTED_TYPE - ) - - -class RegisterPreset(GoProEnum): - PRESET = ( - open_gopro.proto.request_get_preset_status_pb2.EnumRegisterPresetStatus.REGISTER_PRESET_STATUS_PRESET - ) - PRESET_GROUP_ARRAY = ( - open_gopro.proto.request_get_preset_status_pb2.EnumRegisterPresetStatus.REGISTER_PRESET_STATUS_PRESET_GROUP_ARRAY - ) - - -class RegisterLiveStream(GoProEnum): - MODE = open_gopro.proto.live_streaming_pb2.EnumRegisterLiveStreamStatus.REGISTER_LIVE_STREAM_STATUS_MODE - ERROR = open_gopro.proto.live_streaming_pb2.EnumRegisterLiveStreamStatus.REGISTER_LIVE_STREAM_STATUS_ERROR - STATUS = ( - open_gopro.proto.live_streaming_pb2.EnumRegisterLiveStreamStatus.REGISTER_LIVE_STREAM_STATUS_STATUS - ) - - -class CameraControlStatus(GoProEnum): - IDLE = open_gopro.proto.set_camera_control_status_pb2.EnumCameraControlStatus.CAMERA_IDLE - CONTROL = open_gopro.proto.set_camera_control_status_pb2.EnumCameraControlStatus.CAMERA_CONTROL - EXTERNAL_CONTROL = ( - open_gopro.proto.set_camera_control_status_pb2.EnumCameraControlStatus.CAMERA_EXTERNAL_CONTROL - ) - - -class PresetGroup(GoProEnum): - VIDEO = 1000 - PHOTO = 1001 - TIMELAPSE = 1002 +from open_gopro.constants import GoProEnum class Resolution(GoProEnum): @@ -120,11 +19,19 @@ class Resolution(GoProEnum): RES_1080 = 9 RES_4K_4_3 = 18 RES_5K = 24 - RES_5_K_4_3 = 25 - RES_5_3_K_8_7 = 26 - RES_5_3_K_4_3 = 27 - RES_4_K_8_7 = 28 - RES_5_3_K = 100 + RES_5K_4_3 = 25 + RES_5_3K_8_7 = 26 + RES_5_3K_4_3 = 27 + RES_4K_8_7 = 28 + RES_4K_9_16 = 29 + RES_1080_9_16 = 30 + RES_5_3K = 100 + RES_5_3K_16_9 = 101 + RES_4K_16_9 = 102 + RES_4K_4_3_TODO = 103 + RES_2_7K_16_9 = 104 + RES_2_7K_4_3_TODO = 105 + RES_1080_16_9 = 106 class WebcamResolution(GoProEnum): @@ -152,6 +59,8 @@ class AutoOff(GoProEnum): MIN_5 = 4 MIN_15 = 6 MIN_30 = 7 + SEC_8 = 11 + SEC_30 = 12 class LensMode(GoProEnum): @@ -168,6 +77,7 @@ class VideoFOV(GoProEnum): LINEAR_HORIZON_LEVELING = 8 HYPERVIEW = 9 LINEAR_HORIZON_LOCK = 10 + MAX_HYPERVIEW = 11 class WebcamFOV(GoProEnum): @@ -335,6 +245,7 @@ class Flatmode(GoProEnum): LIVE_BURST = 25 NIGHT_LAPSE_VIDEO = 26 SLO_MO = 27 + UNKNOWN = 28 class Toggle(GoProEnum): @@ -415,6 +326,27 @@ class Speed(GoProEnum): SUPER_SLO_MO_4X_2_7_K = 25 SLO_MO_2X_4K_50_HZ = 26 SUPER_SLO_MO_4X_2_7_K_50_HZ = 27 + SUPER_SLO_MO_4X_2_7K_50HZ = 27 + SPEED_1X_LOW_LIGHT = 28 + SPEED_1X_LOW_LIGHT_2 = 29 + SLO_MO_2X_2 = 30 + SLO_MO_2X_3 = 31 + SPEED_1X_LOW_LIGHT_3 = 32 + SPEED_1X_LOW_LIGHT_4 = 33 + SLO_MO_2X_4 = 34 + SLO_MO_2X_5 = 35 + SPEED_1X_LOW_LIGHT_5 = 36 + SPEED_1X_LOW_LIGHT_6 = 37 + SPEED_1X_LOW_LIGHT_7 = 38 + SPEED_1X_LOW_LIGHT_8 = 39 + SLO_MO_2X_6 = 40 + SLO_MO_2X_7 = 41 + SLO_MO_2X_8 = 42 + SLO_MO_2X_9 = 43 + SPEED_1X_LOW_LIGHT_9 = 44 + SPEED_1X_LOW_LIGHT_10 = 45 + SPEED_1X_LOW_LIGHT_11 = 46 + SPEED_1X_LOW_LIGHT_12 = 47 class PhotoEasyMode(GoProEnum): @@ -436,3 +368,98 @@ class SystemVideoMode(GoProEnum): EXTENDED_BATTERY = 1 EXTENDED_BATTERY_GREEN_ICON = 101 LONGEST_BATTERY_GREEN_ICON = 102 + + +class BitRate(GoProEnum): + STANDARD = 0 + HIGH = 1 + + +class BitDepth(GoProEnum): + BIT_8 = 0 + BIT_10 = 2 + + +class VideoProfile(GoProEnum): + STANDARD = 0 + HDR = 1 + LOG = 2 + + +class VideoAspectRatio(GoProEnum): + RATIO_4_3 = 0 + RATIO_16_9 = 1 + RATIO_8_7 = 3 + RATIO_9_16 = 4 + + +class EasyAspectRatio(GoProEnum): + WIDESCREEN = 0 + MOBILE = 1 + UNIVERSAL = 2 + + +class VideoMode(GoProEnum): + HIGHEST = 0 + STANDARD = 1 + BASIC = 2 + + +class TimelapseMode(GoProEnum): + TIMEWARP = 0 + STAR_TRAILS = 1 + LIGHT_PAINTING = 2 + VEHICLE_LIGHTS = 3 + MAX_TIMEWARP = 4 + MAX_STAR_TRAILS = 5 + MAX_LIGHT_PAINTING = 6 + MAX_VEHICLE_LIGHTS = 7 + + +class PhotoMode(GoProEnum): + SUPER = 0 + NIGHT = 1 + + +class Framing(GoProEnum): + WIDESCREEN = 0 + VERTICAL = 1 + FULL = 2 + + +class MaxLensModType(GoProEnum): + NONE = 0 + V1 = 1 + V2 = 2 + + +class Hindsight(GoProEnum): + SEC_15 = 2 + SEC_30 = 3 + OFF = 4 + + +class PhotoInterval(GoProEnum): + OFF = 0 + SEC_0_5 = 2 + SEC_1 = 3 + SEC_2 = 4 + SEC_5 = 5 + SEC_10 = 6 + SEC_30 = 7 + SEC_60 = 8 + SEC_120 = 9 + SEC_3 = 10 + + +class PhotoDuration(GoProEnum): + OFF = 0 + SEC_15 = 1 + SEC_30 = 2 + MIN_1 = 3 + MIN_5 = 4 + MIN_15 = 5 + MIN_30 = 6 + HOUR_1 = 7 + HOUR_2 = 8 + HOUR_3 = 9 diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/api/parsers.py b/demos/python/sdk_wireless_camera_control/open_gopro/api/parsers.py new file mode 100644 index 00000000..344eba2d --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/parsers.py @@ -0,0 +1,381 @@ +# parsers.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 + +"""Parser implementations""" + +from __future__ import annotations + +import datetime +import logging +from typing import Any, Callable, TypeVar, cast + +import google.protobuf.json_format +from construct import Construct, Flag, Int16sb, Int16ub +from google.protobuf import descriptor +from google.protobuf.json_format import MessageToDict as ProtobufToDict +from pydantic import BaseModel + +from open_gopro import types +from open_gopro.constants import SettingId, StatusId +from open_gopro.enum import GoProEnum, enum_factory +from open_gopro.parser_interface import ( + BytesBuilder, + BytesParser, + BytesParserBuilder, + GlobalParsers, + JsonParser, + JsonTransformer, +) +from open_gopro.util import map_keys, pretty_print + +logger = logging.getLogger(__name__) + +ProtobufPrinter = google.protobuf.json_format._Printer # type: ignore # noqa +original_field_to_json = ProtobufPrinter._FieldToJsonObject + +# TODO move into below class +def construct_adapter_factory(target: Construct) -> BytesParserBuilder: + """Build a construct parser adapter from a construct + + Args: + target (Construct): construct to use for parsing and building + + Returns: + BytesParserBuilder: instance of generated class + """ + + class ParserBuilder(BytesParserBuilder): + """Adapt the construct for our interface""" + + container = target + + def parse(self, data: bytes) -> Any: + return self.container.parse(data) + + def build(self, *args: Any, **kwargs: Any) -> bytes: + return self.container.build(*args, **kwargs) + + return ParserBuilder() + + +T = TypeVar("T") + + +class JsonParsers: + """The collection of parsers used for additional JSON parsing""" + + class PydanticAdapter(JsonParser[BaseModel]): + """Parse Json using a Pydantic model + + Args: + model (type[BaseModel]): model to use for parsing + """ + + def __init__(self, model: type[BaseModel]) -> None: + self.model = model + + def parse(self, data: types.JsonDict) -> BaseModel: + """Parse json dict into model + + Args: + data (dict): data to parse + + Returns: + BaseModel: parsed model + """ + return self.model(**data) + + class LambdaParser(JsonParser[T]): + """Helper class to allow parser definition using a lambda + + Args: + parser (Callable[[dict], dict]): lambda to parse input + """ + + def __init__(self, parser: Callable[[types.JsonDict], T]) -> None: + self._parser = parser + + def parse(self, data: types.JsonDict) -> T: + """Use stored lambda parse for parsing + + Args: + data (dict): input dict to parse + + Returns: + T: parsed output + """ + return self._parser(data) + + class CameraStateParser(JsonParser): + """Parse integer numbers into Enums""" + + def parse(self, data: types.JsonDict) -> types.CameraState: + """Parse dict of integer values into human readable (i.e. enum'ed) setting / status map + + Args: + data (dict): input dict to parse + + Returns: + dict: output human readable dict + """ + parsed: dict = {} + # Parse status and settings values into nice human readable things + for (name, id_map) in [("status", StatusId), ("settings", SettingId)]: + for k, v in data[name].items(): + identifier = cast(types.ResponseType, id_map(int(k))) + try: + if not (parser_builder := GlobalParsers.get_query_container(identifier)): + parsed[identifier] = v + else: + parsed[identifier] = parser_builder(v) + except ValueError: + # This is the case where we receive a value that is not defined in our params. + # This shouldn't happen and is either a firmware bug or means the documentation needs to + # be updated. However, it isn't functionally critical. + logger.warning(f"{str(identifier)} does not contain a value {v}") + parsed[identifier] = v + return parsed + + +class JsonTransformers: + """Collection of Json-to-Json transformers""" + + class MapKey(JsonTransformer): + """Map all matching keys using the input function""" + + def __init__(self, key: str, func: Callable) -> None: + self.key = key + self.func = func + super().__init__() + + def transform(self, data: types.JsonDict) -> types.JsonDict: + """Transform json, mapping keys + + Args: + data (types.JsonDict): json data to transform + + Returns: + types.JsonDict: transformed json data + """ + map_keys(data, self.key, self.func) + return data + + +class ProtobufDictProxy(dict): + """Proxy a dict to appear as an object by giving its keys attribute access""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.__dict__ = self + + def __str__(self) -> str: + return pretty_print(self.__dict__) + + @classmethod + def from_proto(cls, proto_dict: dict) -> ProtobufDictProxy: + """Build a proxy from a dictionary attr-name to value + + Args: + proto_dict (dict): dict to build from + + Returns: + ProtobufDictProxy: built proxy + """ + + def recurse(obj: Any) -> Any: + # Recursion Cases + if isinstance(obj, list): + return [recurse(item) for item in obj] + if isinstance(obj, dict): + nested_dict = {} + for key, value in obj.items(): + nested_dict[key] = recurse(value) + return ProtobufDictProxy(nested_dict) + # Base Case + return obj + + return ProtobufDictProxy(recurse(proto_dict)) + + +class ByteParserBuilders: + """Collection byte-to-output type parse (and optionally builders)""" + + class GoProEnum(BytesParserBuilder): + """Parse into a GoProEnum + + Args: + target (type[GoProEnum]): enum type to parse into + """ + + def __init__(self, target: type[GoProEnum]) -> None: + + self._container = target + + def parse(self, data: bytes) -> GoProEnum: + """Parse bytes into GoPro enum + + Args: + data (bytes): bytes to parse + + Returns: + GoProEnum: parsed enum + """ + return self._container(data[0]) + + def build(self, *args: Any, **_: Any) -> bytes: + """Build bytes from GoPro Enum + + Args: + *args (Any): enum to use for building + **_ (Any): not used + + Returns: + bytes: built bytes + """ + return bytes([int(args[0])]) + + class Protobuf(BytesParser): + """Parse into a protobuf object + + The actual returned type is a proxy to a protobuf object but it's attributes can be accessed + using the protobuf definition + + Args: + proto (type[types.Protobuf]): protobuf definition to parse (a proxy) into + """ + + def __init__(self, proto: type[types.Protobuf]) -> None: + class ProtobufByteParser(BytesParser[dict]): + """Parse bytes into a dict using the protobuf""" + + protobuf = proto + + # TODO can we do this without relying on Protobuf internal implementation + # pylint: disable=not-callable + def parse(self, data: bytes) -> Any: + response: types.Protobuf = self.protobuf().FromString(bytes(data)) + + # Monkey patch the field-to-json function to use our enum translation + ProtobufPrinter._FieldToJsonObject = ( + lambda self, field, value: enum_factory(field.enum_type)(value) + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM + else original_field_to_json(self, field, value) + ) + as_dict = ProtobufToDict( + response, including_default_value_fields=False, preserving_proto_field_name=True + ) + # For any unset fields, use None + for key in response.DESCRIPTOR.fields_by_name: + if key not in as_dict: + as_dict[key] = None + # Proxy as an object + return ProtobufDictProxy.from_proto(as_dict) + + self._proto_parser = ProtobufByteParser() + + def parse(self, data: bytes) -> dict: + """Parse the bytes into a Protobuf Proxy + + Args: + data (bytes): bytes to parse + + Returns: + dict: protobuf proxy dict which provides attribute access + """ + return self._proto_parser.parse(data) + + class DateTime(BytesParser, BytesBuilder): + """Handle local and non-local datetime parsing / building""" + + def build(self, obj: datetime.datetime, tzone: int | None = None, is_dst: bool | None = None) -> bytes: + """Build bytestream from datetime and optional local arguments + + Args: + obj (datetime.datetime): date and time + tzone (int | None, optional): timezone (as UTC offset). Defaults to None. + is_dst (bool | None, optional): is daylight savings time?. Defaults to None. + + Returns: + bytes: bytestream built from datetime + """ + byte_data = [*Int16ub.build(obj.year), obj.month, obj.day, obj.hour, obj.minute, obj.second] + if tzone and is_dst: + byte_data.extend([*Int16sb.build(tzone), *Flag.build(is_dst)]) + return bytes(byte_data) + + def parse(self, data: bytes) -> dict: + """Parse bytestream into dict of datetime and potential timezone / dst + + Args: + data (bytes): bytestream to parse + + Returns: + dict: dict containing datetime + """ + is_dst_tz = len(data) == 9 + buf = data[1:] + year = Int16ub.parse(buf[0:2]) + + dt = datetime.datetime(year, *[int(x) for x in buf[2:7]]) # type: ignore + return ( + {"datetime": dt} + if is_dst_tz + else {"datetime": dt, "tzone": Int16sb.parse(buf[7:9]), "dst": bool(buf[9])} + ) + + class Construct(BytesParserBuilder): + """Parse bytes into a construct object + + Args: + construct (Construct): construct definition + """ + + def __init__(self, construct: Construct) -> None: + self._construct = construct_adapter_factory(construct) + + def parse(self, data: bytes) -> Construct: + """Parse bytes into construct container + + Args: + data (bytes): bytes to parse + + Returns: + Construct: construct container + """ + return self._construct.parse(data) + + def build(self, obj: Construct) -> bytes: + """Built bytes from filled out construct container + + Args: + obj (Construct): construct container + + Returns: + bytes: built bytes + """ + return self._construct.build(obj) + + class DeprecatedMarker(BytesParserBuilder[str]): + """Used to return "DEPRECATED" when a deprecated setting / status is attempted to be parsed / built""" + + def parse(self, data: bytes) -> str: + """Return string indicating this ID is deprecated + + Args: + data (bytes): ignored + + Returns: + str: "DEPRECATED" + """ + return "DEPRECATED" + + def build(self, obj: Any) -> bytes: + """Return empty bytes since this ID is deprecated + + Args: + obj (Any): ignored + + Returns: + bytes: empty + """ + return bytes() diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/ble/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/ble/__init__.py index 40602f17..3bb62b7f 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/ble/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/ble/__init__.py @@ -1,9 +1,13 @@ # __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). # This copyright was auto-generated on Tue Sep 7 21:35:53 UTC 2021 -"""Open GoPro BLE Interface interace and implementation""" +"""Open GoPro BLE Interface interface and implementation + +isort:skip_file +""" from open_gopro.exceptions import FailedToFindDevice, ConnectFailed, ConnectionTerminated, ResponseTimeout from .services import GattDB, Characteristic, Descriptor, Service, BleUUID, UUIDs, CharProps from .controller import BleDevice, BleHandle, NotiHandlerType, DisconnectHandlerType, BLEController from .client import BleClient +from .adapters import BleakWrapperController diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/ble/adapters/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/ble/adapters/__init__.py index 18d59f13..0d4c2dab 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/ble/adapters/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/ble/adapters/__init__.py @@ -1,6 +1,6 @@ # __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). # This copyright was auto-generated on Tue Sep 7 21:35:53 UTC 2021 -"""Bleak Adapter Implementation for the Open GoPro BLE Interface""" +"""Adapter Implementations for the Open GoPro BLE Interface""" from .bleak_wrapper import BleakWrapperController diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/ble/adapters/bleak_wrapper.py b/demos/python/sdk_wireless_camera_control/open_gopro/ble/adapters/bleak_wrapper.py index d51543f2..86cfca46 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/ble/adapters/bleak_wrapper.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/ble/adapters/bleak_wrapper.py @@ -3,34 +3,33 @@ """Manage a Bluetooth connection using bleak.""" -import sys import asyncio import logging import platform -import threading -from typing import Pattern, Any, Callable, Optional +import sys +from typing import Any, Callable, Optional, Pattern +import bleak import pexpect -from packaging.version import Version -from bleak import BleakScanner, BleakClient -from bleak.backends.device import BLEDevice as BleakDevice from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.device import BLEDevice as BleakDevice from bleak.backends.scanner import AdvertisementData +from packaging.version import Version -from open_gopro.util import Singleton from open_gopro.ble import ( - Service, + BLEController, + BleUUID, Characteristic, + CharProps, Descriptor, + FailedToFindDevice, GattDB, - BLEController, NotiHandlerType, - FailedToFindDevice, - BleUUID, + Service, UUIDs, - CharProps, ) from open_gopro.exceptions import ConnectFailed +from open_gopro.util import Singleton logger = logging.getLogger(__name__) @@ -61,7 +60,7 @@ def uuid2bleak_string(uuid: BleUUID) -> str: return f"{uuid.hex[:8]}-{uuid.hex[8:12]}-{uuid.hex[12:16]}-{uuid.hex[16:20]}-{uuid.hex[20:]}" -class BleakWrapperController(BLEController[BleakDevice, BleakClient], Singleton): +class BleakWrapperController(BLEController[BleakDevice, bleak.BleakClient], Singleton): """Wrapper around bleak to manage a Bluetooth connection.""" def __init__(self, exception_handler: Optional[Callable] = None) -> None: @@ -73,74 +72,34 @@ def __init__(self, exception_handler: Optional[Callable] = None) -> None: exception_handler (Callable, Optional): Used to catch asyncio exceptions from other tasks. Defaults to None. """ BLEController.__init__(self, exception_handler) - # Thread to run ble controller asyncio loop (to abstract asyncio from client as well as handle async notifications) - self._module_loop: asyncio.AbstractEventLoop # Will be set when module thread starts - self._module_thread = threading.Thread(daemon=True, target=self._run, name="data") - self._ready = threading.Event() - self._module_thread.start() - self._ready.wait() - - def _run(self) -> None: - """Thread to keep the event loop running to interface with the BLE adapter.""" - # Create loop for this new thread - self._module_loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._module_loop) - self._module_loop.set_exception_handler(self._exception_handler) - - # Run forever - self._ready.set() - self._module_loop.run_forever() - - def _as_coroutine(self, action: Callable, timeout: Optional[float] = None) -> Any: - """Run a function as a coroutine in the module thread. - - This will transfer execution of the given partial to the module thread of this instance - - Args: - action (Callable): function and parameters to run as corouting - timeout (float): Time to wait for coroutine to return (in seconds). Defaults to None (wait forever). - Returns: - Any: Passes return of coroutine through - """ - # Allow timeout exception to propagate - return asyncio.run_coroutine_threadsafe(action(), self._module_loop).result(timeout) - - def read(self, handle: BleakClient, uuid: BleUUID) -> bytearray: + async def read(self, handle: bleak.BleakClient, uuid: BleUUID) -> bytearray: """Read data from a BleUUID. Args: - handle (BleakClient): client to read from + handle (bleak.BleakClient): client to read from uuid (BleUUID): uuid to read Returns: bytearray: read data """ + logger.debug(f"Reading from {uuid}") + response = await handle.read_gatt_char(uuid2bleak_string(uuid)) + logger.debug(f'Received response on BleUUID [{uuid}]: {response.hex( ":")}') + return response - async def _async_read() -> bytearray: - logger.debug(f"Reading from {uuid}") - response = await handle.read_gatt_char(uuid2bleak_string(uuid)) - logger.debug(f'Received response on BleUUID [{uuid}]: {response.hex( ":")}') - return response - - return self._as_coroutine(_async_read) - - def write(self, handle: BleakClient, uuid: BleUUID, data: bytearray) -> None: + async def write(self, handle: bleak.BleakClient, uuid: BleUUID, data: bytearray) -> None: """Write data to a BleUUID. Args: - handle (BleakClient): Device to write to + handle (bleak.BleakClient): Device to write to uuid (BleUUID): characteristic BleUUID to write to data (bytearray): data to write """ + logger.debug(f"Writing to {uuid}: {uuid.hex}") + await handle.write_gatt_char(uuid2bleak_string(uuid), data, response=True) - async def _async_write() -> None: - logger.debug(f"Writing to {uuid}: {uuid.hex}") - await handle.write_gatt_char(uuid2bleak_string(uuid), data, response=True) - - self._as_coroutine(_async_write) - - def scan( + async def scan( self, token: Pattern, timeout: int = 5, service_uuids: Optional[list[BleUUID]] = None ) -> BleakDevice: """Scan for a regex in advertising data strings, optionally filtering on service BleUUID's @@ -150,43 +109,44 @@ def scan( timeout (int): Time to scan. Defaults to 5. service_uuids (Optional[list[BleUUID]]): The list of BleUUID's to filter on. Defaults to None. + Raises: + FailedToFindDevice: scan timed out without finding device + Returns: BleakDevice: The first matched device that was discovered """ + stop_event = asyncio.Event() + logger.info(f"Scanning for {token.pattern} bluetooth devices...") + devices: dict[str, BleakDevice] = {} + uuids = [] if service_uuids is None else [uuid2bleak_string(uuid) for uuid in service_uuids] - async def _async_scan() -> BleakDevice: - stop_event = asyncio.Event() - logger.info(f"Scanning for {token.pattern} bluetooth devices...") - devices: dict[str, BleakDevice] = {} - uuids = [] if service_uuids is None else [uuid2bleak_string(uuid) for uuid in service_uuids] - - def scan_callback(device: BleakDevice, adv_data: AdvertisementData) -> None: - """Only keep devices that have a device name token + def scan_callback(device: BleakDevice, adv_data: AdvertisementData) -> None: + """Only keep devices that have a device name token - For GoPro, this must be coming from the scan response - - Args: - device (BleakDevice): discovered device - adv_data (AdvertisementData): advertisement (and / or scan response) data - """ - if (name := adv_data.local_name) and name not in devices: - devices[name] = device - logger.info(f"\tDiscovered: {device}") - stop_event.set() - - # Now get list of connectable advertisements - async with BleakScanner(timeout=timeout, detection_callback=scan_callback, service_uuids=uuids): - await stop_event.wait() - # Now look for our matching device(s) - if not (matched_devices := [device for name, device in devices.items() if token.match(name)]): - raise FailedToFindDevice - logger.info(f"Found {len(matched_devices)} matching devices.") - # If there's more than 1, the first one gets lucky. - return matched_devices[0] - - return self._as_coroutine(_async_scan) - - def connect(self, disconnect_cb: Callable, device: BleakDevice, timeout: int = 15) -> BleakClient: + Args: + device (BleakDevice): discovered device + adv_data (AdvertisementData): advertisement (and / or scan response) data + """ + if (name := adv_data.local_name or device.name) and name not in devices: + devices[name] = device + logger.info(f"\tDiscovered: {device}") + stop_event.set() + + # Now get list of connectable advertisements + async with bleak.BleakScanner(timeout=timeout, detection_callback=scan_callback, service_uuids=uuids): + # The bleak scan timeout appears to not be used at least in some versions of bleak + try: + await asyncio.wait_for(stop_event.wait(), timeout) + except asyncio.TimeoutError as e: + raise FailedToFindDevice from e + # Now look for our matching device(s) + if not (matched_devices := [device for name, device in devices.items() if token.match(name)]): + raise FailedToFindDevice + logger.info(f"Found {len(matched_devices)} matching devices.") + # If there's more than 1, the first one gets lucky. + return matched_devices[0] + + async def connect(self, disconnect_cb: Callable, device: BleakDevice, timeout: int = 15) -> bleak.BleakClient: """Connect to a device. Args: @@ -198,7 +158,7 @@ def connect(self, disconnect_cb: Callable, device: BleakDevice, timeout: int = 1 ConnectFailed: Connection was not established Returns: - BleakClient: Connected device + bleak.BleakClient: Connected device """ class ConnectSession: @@ -249,108 +209,98 @@ async def catch_connection_failure(self) -> None: """Disconnection callback to be used during connection establishment""" await self._disconnected.wait() - async def _async_connect() -> tuple[BleakClient, Optional[BaseException]]: - logger.info(f"Establishing BLE connection to {device}...") - - connect_session = ConnectSession(disconnect_cb) - client = BleakClient( - device, disconnected_callback=connect_session.disconnect_cb, use_cached=False, timeout=timeout + logger.info(f"Establishing BLE connection to {device}...") + + connect_session = ConnectSession(disconnect_cb) + client = bleak.BleakClient( + device, disconnected_callback=connect_session.disconnect_cb, use_cached=False, timeout=timeout + ) + exception = None + try: + task_connect: asyncio.Task = asyncio.create_task(client.connect(timeout=timeout), name="connect") + task_disconnected: asyncio.Task = asyncio.create_task( + connect_session.catch_connection_failure(), name="disconnect" ) - exception = None - try: - task_connect: asyncio.Task = asyncio.create_task( - client.connect(timeout=timeout), name="connect" - ) - task_disconnected: asyncio.Task = asyncio.create_task( - connect_session.catch_connection_failure(), name="disconnect" - ) - finished, unfinished = await asyncio.wait( - [task_connect, task_disconnected], return_when=asyncio.FIRST_COMPLETED - ) - for task in finished: - if exception := task.exception(): - if isinstance(task.exception(), asyncio.exceptions.TimeoutError): - exception = Exception("Connection request timed out") - # Completion of these is tasks mutually exclusive so safe to stop now - break - for task in unfinished: - task.cancel() - if connect_session.did_fail: - exception = Exception("Connection failed during establishment..") - else: - connect_session.use_client_cb() - except Exception as e: # pylint: disable=broad-except - exception = e - - return client, exception - - client, exception = self._as_coroutine(_async_connect) + finished, unfinished = await asyncio.wait( + [task_connect, task_disconnected], return_when=asyncio.FIRST_COMPLETED + ) + for task in finished: + if exception := task.exception(): + if isinstance(task.exception(), asyncio.exceptions.TimeoutError): + exception = Exception("Connection request timed out") + # Completion of these is tasks mutually exclusive so safe to stop now + break + for task in unfinished: + task.cancel() + if connect_session.did_fail: + exception = Exception("Connection failed during establishment..") + else: + connect_session.use_client_cb() + except Exception as e: # pylint: disable=broad-except + exception = e + if exception: logger.warning(exception) raise ConnectFailed("BLE", 1, 1) from exception return client - def pair(self, handle: BleakClient) -> None: + async def pair(self, handle: bleak.BleakClient) -> None: """Pair to a device after connection. This is required for Windows and not allowed on Mac. Linux requires a separate process to interact with bluetoothctl to accept pairing. Args: - handle (BleakClient): Device to pair to + handle (bleak.BleakClient): Device to pair to """ - - async def _async_def_pair() -> None: - logger.debug("Attempting to pair...") - if (OS := platform.system()) == "Linux": - logger.info("Pairing with bluetoothctl") - # Manually control bluetoothctl on Linux - bluetoothctl = pexpect.spawn("bluetoothctl") - if logger.level == logging.DEBUG: - bluetoothctl.logfile = sys.stdout.buffer - bluetoothctl.expect("Agent registered") - # Get the version - bluetoothctl.sendline("version") - bluetoothctl.expect(r"Version") - bluetoothctl.expect(r"\n") - version = Version(bluetoothctl.before.decode("utf-8").strip()) - # First see if we are already paired - if version >= Version("5.66"): - bluetoothctl.sendline("devices Paired") - bluetoothctl.expect("devices Paired") - else: - bluetoothctl.sendline("paired-devices") - bluetoothctl.expect("paired-devices") - bluetoothctl.expect(r"#") - for device in bluetoothctl.before.decode("utf-8").splitlines(): - if "Device" in device and device.split()[1] == handle.address: - break # The device is already paired - else: - # We're not paired so do it now - bluetoothctl.sendline(f"pair {handle.address}") - if (match := bluetoothctl.expect(["Accept pairing", "Pairing successful"])) == 0: - bluetoothctl.sendline("yes") - bluetoothctl.expect("Pairing successful") - elif match == 1: # We received pairing successful so nothing else to do - pass - - elif OS == "Darwin": - # No pairing on Mac - pass + logger.debug("Attempting to pair...") + if (OS := platform.system()) == "Linux": + logger.info("Pairing with bluetoothctl") + # Manually control bluetoothctl on Linux + bluetoothctl = pexpect.spawn("bluetoothctl") + if logger.level == logging.DEBUG: + bluetoothctl.logfile = sys.stdout.buffer + bluetoothctl.expect("Agent registered") + # Get the version + bluetoothctl.sendline("version") + bluetoothctl.expect(r"Version") + bluetoothctl.expect(r"\n") + version = Version(bluetoothctl.before.decode("utf-8").strip()) + # First see if we are already paired + if version >= Version("5.66"): + bluetoothctl.sendline("devices Paired") + bluetoothctl.expect("devices Paired") else: - await handle.pair() - - logger.debug("Pairing complete!") - - self._as_coroutine(_async_def_pair) - - def enable_notifications(self, handle: BleakClient, handler: NotiHandlerType) -> None: + bluetoothctl.sendline("paired-devices") + bluetoothctl.expect("paired-devices") + bluetoothctl.expect(r"#") + for device in bluetoothctl.before.decode("utf-8").splitlines(): + if "Device" in device and device.split()[1] == handle.address: + break # The device is already paired + else: + # We're not paired so do it now + bluetoothctl.sendline(f"pair {handle.address}") + if (match := bluetoothctl.expect(["Accept pairing", "Pairing successful"])) == 0: + bluetoothctl.sendline("yes") + bluetoothctl.expect("Pairing successful") + elif match == 1: # We received pairing successful so nothing else to do + pass + + elif OS == "Darwin": + # No pairing on Mac + pass + else: + await handle.pair() + + logger.debug("Pairing complete!") + + async def enable_notifications(self, handle: bleak.BleakClient, handler: NotiHandlerType) -> None: """Enable all notifications. Search through all characteristics and enable any that have notification property. Args: - handle (BleakClient): Device to enable notifications for + handle (bleak.BleakClient): Device to enable notifications for handler (NotiHandlerType): Notification callback handler """ @@ -363,25 +313,22 @@ def bleak_notification_cb_adapter(characteristic: BleakGATTCharacteristic, data: """ handler(characteristic.handle, data) - async def _async_enable_notifications() -> None: - logger.info("Enabling notifications...") - for service in handle.services: - for char in service.characteristics: - if "notify" in char.properties: - logger.debug(f"Enabling notification on char {char.uuid}") - await handle.start_notify(char, bleak_notification_cb_adapter) - logger.info("Done enabling notifications") - - self._as_coroutine(_async_enable_notifications) + logger.info("Enabling notifications...") + for service in handle.services: + for char in service.characteristics: + if "notify" in char.properties: + logger.debug(f"Enabling notification on char {char.uuid}") + await handle.start_notify(char, bleak_notification_cb_adapter) + logger.info("Done enabling notifications") - def discover_chars(self, handle: BleakClient, uuids: Optional[type[UUIDs]] = None) -> GattDB: + async def discover_chars(self, handle: bleak.BleakClient, uuids: Optional[type[UUIDs]] = None) -> GattDB: """Discover all characteristics for a connected handle. By default, the BLE controller only knows Spec-Defined BleUUID's so any additional BleUUID's should be passed in with the uuids argument Args: - handle (BleakClient): BLE handle to discover for + handle (bleak.BleakClient): BLE handle to discover for uuids (type[UUIDs], Optional): Additional BleUUID information to use when building the Gatt Database. Defaults to None. @@ -403,72 +350,62 @@ def bleak_props_adapter(bleak_props: list[str]) -> CharProps: props |= bleak_props_to_enum[prop] return props - async def _async_discover_chars() -> GattDB: - logger.info("Discovering characteristics...") - services: list[Service] = [] - for service in handle.services: - service_uuid = ( - uuids[service.uuid] - if uuids and service.uuid in uuids - else BleUUID(service.description, hex=service.uuid) - ) - logger.debug(f"[Service] {service_uuid}") - - # Loop over all chars in service - chars: list[Characteristic] = [] - for char in service.characteristics: - # Get any descriptors if they exist - descriptors: list[Descriptor] = [] - for descriptor in char.descriptors: - descriptors.append( - Descriptor( - handle=descriptor.handle, - uuid=( - uuids[descriptor.uuid] - if uuids and descriptor.uuid in uuids - else BleUUID(descriptor.description, hex=descriptor.uuid) - ), - value=await handle.read_gatt_descriptor(descriptor.handle), - ) - ) - # Create new characteristic - chars.append( - Characteristic( - handle=char.handle, + logger.info("Discovering characteristics...") + services: list[Service] = [] + for service in handle.services: + service_uuid = ( + uuids[service.uuid] + if uuids and service.uuid in uuids + else BleUUID(service.description, hex=service.uuid) + ) + logger.debug(f"[Service] {service_uuid}") + + # Loop over all chars in service + chars: list[Characteristic] = [] + for char in service.characteristics: + # Get any descriptors if they exist + descriptors: list[Descriptor] = [] + for descriptor in char.descriptors: + descriptors.append( + Descriptor( + handle=descriptor.handle, uuid=( - uuids[char.uuid] - if uuids and char.uuid in uuids - else BleUUID(char.description, hex=char.uuid) + uuids[descriptor.uuid] + if uuids and descriptor.uuid in uuids + else BleUUID(descriptor.description, hex=descriptor.uuid) ), - props=bleak_props_adapter(char.properties), - init_descriptors=descriptors, + value=await handle.read_gatt_descriptor(descriptor.handle), ) ) - logger.debug(f"\t[Characteristic] {chars[-1]}") - - # Create new service - services.append(Service(uuid=service_uuid, start_handle=service.handle, init_chars=chars)) + # Create new characteristic + chars.append( + Characteristic( + handle=char.handle, + uuid=( + uuids[char.uuid] + if uuids and char.uuid in uuids + else BleUUID(char.description, hex=char.uuid) + ), + props=bleak_props_adapter(char.properties), + init_descriptors=descriptors, + ) + ) + logger.debug(f"\t[Characteristic] {chars[-1]}") - logger.info("Done discovering characteristics!") - return GattDB(services) + # Create new service + services.append(Service(uuid=service_uuid, start_handle=service.handle, init_chars=chars)) - return self._as_coroutine(_async_discover_chars) + logger.info("Done discovering characteristics!") + return GattDB(services) - def disconnect(self, handle: BleakClient) -> None: + async def disconnect(self, handle: bleak.BleakClient) -> None: """Terminate a BLE connection. Args: - handle (BleakClient): client to disconnect from - - Returns: - bool: True if disconnect was successful, False otherwise + handle (bleak.BleakClient): client to disconnect from """ - - async def _async_disconnect() -> None: - if handle.is_connected: - logger.info("Disconnecting...") - await handle.disconnect() - # Disconnect handler registered during connect will be asynchronously called - logger.info("Device disconnected!") - - return self._as_coroutine(_async_disconnect) + if handle.is_connected: + logger.info("Disconnecting...") + await handle.disconnect() + # Disconnect handler registered during connect will be asynchronously called + logger.info("Device disconnected!") diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/ble/client.py b/demos/python/sdk_wireless_camera_control/open_gopro/ble/client.py index 8383f95f..f1615560 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/ble/client.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/ble/client.py @@ -3,13 +3,14 @@ """Generic BLE Client definition that is composed of a BLE Controller.""" -import re import logging +import re from pathlib import Path -from typing import Generic, Optional, Union, Pattern +from typing import Generic, Optional, Pattern, Union from open_gopro.ble import BleUUID -from open_gopro.exceptions import FailedToFindDevice, ConnectFailed +from open_gopro.exceptions import ConnectFailed, FailedToFindDevice + from .controller import ( BLEController, BleDevice, @@ -17,7 +18,7 @@ DisconnectHandlerType, NotiHandlerType, ) -from .services import GattDB, BleUUID, UUIDs +from .services import BleUUID, GattDB, UUIDs logger = logging.getLogger(__name__) @@ -64,7 +65,7 @@ def __init__( self._identifier: Optional[str] = None if isinstance(self._target, Pattern) else str(self._target) self.uuids = uuids - def _find_device(self, timeout: int = 5, retries: int = 30) -> None: + async def _find_device(self, timeout: int = 5, retries: int = 30) -> None: """Scan for the target device. Args: @@ -78,13 +79,13 @@ def _find_device(self, timeout: int = 5, retries: int = 30) -> None: assert isinstance(self._target, Pattern) for retry in range(1, retries): try: - self._device = self._controller.scan(self._target, timeout, self._service_uuids) + self._device = await self._controller.scan(self._target, timeout, self._service_uuids) return except FailedToFindDevice: logger.warning(f"Failed to find a device in {timeout} seconds. Retrying #{retry}") raise FailedToFindDevice - def open(self, timeout: int = 10, retries: int = 5) -> None: + async def open(self, timeout: int = 10, retries: int = 5) -> None: """Open the client resource so that it is ready to send and receive data. Args: @@ -96,7 +97,7 @@ def open(self, timeout: int = 10, retries: int = 5) -> None: """ # If we need we need to find the device to connect if isinstance(self._target, Pattern): - self._find_device(timeout, retries) + await self._find_device(timeout, retries) # Otherwise we already have it else: self._device = self._target @@ -105,7 +106,7 @@ def open(self, timeout: int = 10, retries: int = 5) -> None: logger.info("Establishing the BLE connection") for retry in range(1, retries): try: - self._handle = self._controller.connect(self._disconnected_cb, self._device, timeout=timeout) + self._handle = await self._controller.connect(self._disconnected_cb, self._device, timeout=timeout) break except ConnectFailed as e: logger.warning(f"Failed to connect. Retrying #{retry}") @@ -114,25 +115,25 @@ def open(self, timeout: int = 10, retries: int = 5) -> None: assert self._handle is not None # Attempt to pair - self._controller.pair(self._handle) + await self._controller.pair(self._handle) # Discover characteristics - self._gatt_table = self._controller.discover_chars(self._handle, self.uuids) + self._gatt_table = await self._controller.discover_chars(self._handle, self.uuids) # Enable all GATT notifications - self._controller.enable_notifications(self._handle, self._notification_cb) + await self._controller.enable_notifications(self._handle, self._notification_cb) - def close(self) -> None: + async def close(self) -> None: """Close the client resource. This should always be called before exiting. """ if self.is_connected: logger.info("Terminating the BLE connection") - self._controller.disconnect(self._handle) + await self._controller.disconnect(self._handle) self._handle = None else: logger.debug("BLE already disconnected") - def read(self, uuid: BleUUID) -> bytearray: + async def read(self, uuid: BleUUID) -> bytearray: """Read byte data from a characteristic (identified by BleUUID) Args: @@ -141,28 +142,29 @@ def read(self, uuid: BleUUID) -> bytearray: Returns: bytearray: byte data that was read """ - return self._controller.read(self._handle, uuid) + return await self._controller.read(self._handle, uuid) - def write(self, uuid: BleUUID, data: bytearray) -> None: + async def write(self, uuid: BleUUID, data: bytearray) -> None: """Write byte data to a characteristic (identified by BleUUID) Args: uuid (BleUUID): characteristic to write to data (bytearray): byte data to write """ - self._controller.write(self._handle, uuid, data) + await self._controller.write(self._handle, uuid, data) @property def gatt_db(self) -> GattDB: """Return the attribute table + Raises: + RuntimeError: GATT table hasn't been discovered + Returns: GattDB: table of BLE attributes """ - if self._gatt_table is None: - # Discover characteristics - assert self._handle is not None - self._gatt_table = self._controller.discover_chars(self._handle) + if not self._gatt_table: + raise RuntimeError("GATT table has not yet been discovered") return self._gatt_table @property diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/ble/controller.py b/demos/python/sdk_wireless_camera_control/open_gopro/ble/controller.py index 8f3f66d1..c356a798 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/ble/controller.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/ble/controller.py @@ -5,9 +5,9 @@ import logging from abc import ABC, abstractmethod -from typing import Callable, Generic, Pattern, TypeVar, Optional +from typing import Callable, Generic, Optional, Pattern, TypeVar -from .services import GattDB, BleUUID, UUIDs +from .services import BleUUID, GattDB, UUIDs logger = logging.getLogger(__name__) @@ -24,7 +24,7 @@ def __init__(self, exception_handler: Optional[Callable] = None) -> None: self._exception_handler = exception_handler @abstractmethod - def read(self, handle: BleHandle, uuid: BleUUID) -> bytearray: + async def read(self, handle: BleHandle, uuid: BleUUID) -> bytearray: """Read a bytestream response from a BleUUID. Args: @@ -37,7 +37,7 @@ def read(self, handle: BleHandle, uuid: BleUUID) -> bytearray: raise NotImplementedError @abstractmethod - def write(self, handle: BleHandle, uuid: BleUUID, data: bytearray) -> None: + async def write(self, handle: BleHandle, uuid: BleUUID, data: bytearray) -> None: """Write a bytestream to a BleUUID. Args: @@ -51,9 +51,7 @@ def write(self, handle: BleHandle, uuid: BleUUID, data: bytearray) -> None: raise NotImplementedError @abstractmethod - def scan( - self, token: Pattern, timeout: int = 5, service_uuids: Optional[list[BleUUID]] = None - ) -> BleDevice: + async def scan(self, token: Pattern, timeout: int = 5, service_uuids: Optional[list[BleUUID]] = None) -> BleDevice: """Scan BLE device with a regex in it's device name. Args: @@ -67,7 +65,7 @@ def scan( raise NotImplementedError @abstractmethod - def connect(self, disconnect_cb: DisconnectHandlerType, device: BleDevice, timeout: int = 15) -> BleHandle: + async def connect(self, disconnect_cb: DisconnectHandlerType, device: BleDevice, timeout: int = 15) -> BleHandle: """Connect to a BLE device. Args: @@ -81,7 +79,7 @@ def connect(self, disconnect_cb: DisconnectHandlerType, device: BleDevice, timeo raise NotImplementedError @abstractmethod - def pair(self, handle: BleHandle) -> None: + async def pair(self, handle: BleHandle) -> None: """Pair to an already connected handle. Args: @@ -90,7 +88,7 @@ def pair(self, handle: BleHandle) -> None: raise NotImplementedError @abstractmethod - def enable_notifications(self, handle: BleHandle, handler: NotiHandlerType) -> None: + async def enable_notifications(self, handle: BleHandle, handler: NotiHandlerType) -> None: """Enable notifications for all notifiable characteristics. The handler is used to register for notifications. It will be called when a a notification @@ -103,7 +101,7 @@ def enable_notifications(self, handle: BleHandle, handler: NotiHandlerType) -> N raise NotImplementedError @abstractmethod - def discover_chars(self, handle: BleHandle, uuids: Optional[type[UUIDs]] = None) -> GattDB: + async def discover_chars(self, handle: BleHandle, uuids: Optional[type[UUIDs]] = None) -> GattDB: """Discover all characteristics for a connected handle. By default, the BLE controller only knows Spec-Defined BleUUID's so any additional BleUUID's should @@ -120,7 +118,7 @@ def discover_chars(self, handle: BleHandle, uuids: Optional[type[UUIDs]] = None) raise NotImplementedError @abstractmethod - def disconnect(self, handle: BleHandle) -> None: + async def disconnect(self, handle: BleHandle) -> None: """Terminate the BLE connection. Args: diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/ble/services.py b/demos/python/sdk_wireless_camera_control/open_gopro/ble/services.py index 8d944c48..8d9289a9 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/ble/services.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/ble/services.py @@ -4,14 +4,24 @@ """Objects to nicely interact with BLE services, characteristics, and attributes.""" from __future__ import annotations + import csv import json import logging import uuid +from dataclasses import InitVar, asdict, dataclass +from enum import IntEnum, IntFlag from pathlib import Path -from enum import IntFlag, IntEnum -from dataclasses import dataclass, asdict, InitVar -from typing import Iterator, Generator, Mapping, Optional, no_type_check, Union, Final, Any +from typing import ( + Any, + Final, + Generator, + Iterator, + Mapping, + Optional, + Union, + no_type_check, +) logger = logging.getLogger(__name__) @@ -115,7 +125,7 @@ def format(self) -> BleUUID.Format: return BleUUID.Format.BIT_16 if len(self.hex) == BleUUID.Format.BIT_16 else BleUUID.Format.BIT_128 def __str__(self) -> str: # pylint: disable=missing-return-doc - return self.hex if self.name == "" else self.name + return self.name if self.name else self.hex def __repr__(self) -> str: return self.__str__() @@ -474,15 +484,11 @@ def dump_to_csv(self, file: Path = Path("attributes.csv")) -> None: ) # For each characteristic in service for char in service.characteristics.values(): - w.writerow( - [char.descriptor_handle, SpecUuidNumber.CHAR_DECLARATION, "28:03", str(char.props), ""] - ) + w.writerow([char.descriptor_handle, SpecUuidNumber.CHAR_DECLARATION, "28:03", str(char.props), ""]) w.writerow([char.handle, char.name, char.uuid.hex, "", char.value]) # For each descriptor in characteristic for descriptor in char.descriptors.values(): - w.writerow( - [descriptor.handle, descriptor.name, descriptor.uuid.hex, "", descriptor.value] - ) + w.writerow([descriptor.handle, descriptor.name, descriptor.uuid.hex, "", descriptor.value]) class UUIDsMeta(type): diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/interface.py b/demos/python/sdk_wireless_camera_control/open_gopro/communicator_interface.py similarity index 71% rename from demos/python/sdk_wireless_camera_control/open_gopro/interface.py rename to demos/python/sdk_wireless_camera_control/open_gopro/communicator_interface.py index a0edafb2..feeed018 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/interface.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/communicator_interface.py @@ -4,37 +4,76 @@ """GoPro specific BLE client""" from __future__ import annotations -import re + import enum import inspect import logging -from pathlib import Path +import re from abc import ABC, abstractmethod -from typing import Generic, Optional, Union, Pattern, Any, TypeVar, Generator, Protocol +from pathlib import Path +from typing import Any, Generator, Generic, Pattern, Protocol, TypeVar, Union -from construct import BitStruct, BitsInteger, Padding, Const, Bit, Construct +from construct import Bit, BitsInteger, BitStruct, Const, Construct, Padding +from open_gopro import types from open_gopro.ble import ( + BleClient, + BLEController, BleDevice, BleHandle, - BLEController, + BleUUID, DisconnectHandlerType, NotiHandlerType, - BleClient, - BleUUID, +) +from open_gopro.constants import ActionId, CmdId, GoProUUIDs, SettingId, StatusId +from open_gopro.models.response import GoProResp, Header +from open_gopro.parser_interface import ( + BytesParser, + BytesTransformer, + GlobalParsers, + JsonParser, + JsonTransformer, + Parser, ) from open_gopro.wifi import WifiClient, WifiController -from open_gopro.responses import GoProResp, Header, BytesParser, JsonParser -from open_gopro.constants import GoProUUIDs, ProducerType, ResponseType, SettingId, StatusId, ActionId, CmdId logger = logging.getLogger(__name__) +############################################################################################################## +####### Communicators / Clients +############################################################################################################## + -class GoProHttp(ABC): +class BaseGoProCommunicator(ABC): + """Common Communicator interface""" + + @abstractmethod + def register_update(self, callback: types.UpdateCb, update: types.UpdateType) -> None: + """Register for callbacks when an update occurs + + Args: + callback (types.UpdateCb): callback to be notified in + update (types.UpdateType): update to register for + """ + raise NotImplementedError + + @abstractmethod + def unregister_update(self, callback: types.UpdateCb, update: types.UpdateType | None = None) -> None: + """Unregister for asynchronous update(s) + + Args: + callback (types.UpdateCb): callback to stop receiving update(s) on + update (types.UpdateType | None): updates to unsubscribe for. Defaults to None (all + updates that use this callback will be unsubscribed). + """ + raise NotImplementedError + + +class GoProHttp(BaseGoProCommunicator): """Base class interface for all HTTP commands""" @abstractmethod - def _get(self, url: str, parser: Optional[JsonParser] = None, **kwargs: Any) -> GoProResp: + async def _http_get(self, url: str, parser: Parser | None = None, **kwargs: Any) -> GoProResp: """Send an HTTP GET request to a string endpoint. Args: @@ -50,7 +89,7 @@ def _get(self, url: str, parser: Optional[JsonParser] = None, **kwargs: Any) -> raise NotImplementedError @abstractmethod - def _stream_to_file(self, url: str, file: Path) -> GoProResp: + async def _stream_to_file(self, url: str, file: Path) -> GoProResp: """Send an HTTP GET request to an Open GoPro endpoint to download a binary file. Args: @@ -72,32 +111,32 @@ def __init__(self, controller: WifiController): self._wifi: WifiClient = WifiClient(controller) @property - def password(self) -> Optional[str]: + def password(self) -> str | None: """Get the GoPro AP's password Returns: - Optional[str]: password or None if it is not known + str | None: password or None if it is not known """ return self._wifi.password @property - def ssid(self) -> Optional[str]: + def ssid(self) -> str | None: """Get the GoPro AP's WiFi SSID Returns: - Optional[str]: SSID or None if it is not known + str | None: SSID or None if it is not known """ return self._wifi.ssid -class GoProBle(ABC, Generic[BleHandle, BleDevice]): +class GoProBle(BaseGoProCommunicator, Generic[BleHandle, BleDevice]): """GoPro specific BLE Client Args: controller (BLEController): controller implementation to use for this client disconnected_cb (DisconnectHandlerType): disconnected callback notification_cb (NotiHandlerType): notification callback - target (Union[Pattern, BleDevice]): regex or device to connect to + target (Pattern | BleDevice): regex or device to connect to """ def __init__( @@ -105,7 +144,7 @@ def __init__( controller: BLEController, disconnected_cb: DisconnectHandlerType, notification_cb: NotiHandlerType, - target: Union[Pattern, BleDevice], + target: Pattern | BleDevice, ) -> None: self._ble: BleClient = BleClient( controller, @@ -116,45 +155,15 @@ def __init__( ) @abstractmethod - def _register_listener(self, producer: ProducerType) -> None: - """Register to receive notifications for a producer. - - Args: - producer (ProducerType): producer that we want to receive notifications from - """ - raise NotImplementedError - - @abstractmethod - def _unregister_listener(self, producer: ProducerType) -> None: - """Stop receiving notifications from a producer. - - Args: - producer (ProducerType): Producer to stop receiving notifications for - """ - raise NotImplementedError - - @abstractmethod - def get_notification(self, timeout: Optional[float] = None) -> GoProResp: - """Get a notification that was received from a registered producer. - - Args: - timeout (float, Optional): Time to wait for a notification before returning. Defaults to None (wait forever) - - Returns: - GoProResp: the received update - """ - raise NotImplementedError - - @abstractmethod - def _send_ble_message( - self, uuid: BleUUID, data: bytearray, response_id: ResponseType, **kwargs: Any + async def _send_ble_message( + self, uuid: BleUUID, data: bytearray, response_id: types.ResponseType, **kwargs: Any ) -> GoProResp: """Write a characteristic and block until its corresponding notification response is received. Args: uuid (BleUUID): characteristic to write to data (bytearray): bytes to write - response_id (ResponseType): identifier to claim parsed response in notification handler + response_id (types.ResponseType): identifier to claim parsed response in notification handler **kwargs (Any): - rules (list[MessageRules]): rules to be enforced for this message @@ -164,7 +173,7 @@ def _send_ble_message( raise NotImplementedError @abstractmethod - def _read_characteristic(self, uuid: BleUUID) -> GoProResp: + async def _read_characteristic(self, uuid: BleUUID) -> GoProResp: """Read a characteristic and block until its corresponding notification response is received. Args: @@ -250,10 +259,10 @@ class GoProWirelessInterface(GoProBle, GoProWifi, Generic[BleDevice, BleHandle]) def __init__( self, ble_controller: BLEController, - wifi_controller: Optional[WifiController], + wifi_controller: WifiController | None, disconnected_cb: DisconnectHandlerType, notification_cb: NotiHandlerType, - target: Union[Pattern, BleDevice], + target: Pattern | BleDevice, ) -> None: """Constructor @@ -262,7 +271,7 @@ def __init__( wifi_controller (Optional[WifiController]): Wifi controller instance disconnected_cb (DisconnectHandlerType): callback for BLE disconnects notification_cb (NotiHandlerType): callback for BLE received notifications - target (Union[Pattern, BleDevice]): BLE device to search for + target (Pattern | BleDevice): BLE device to search for """ # Initialize GoPro Communication Client GoProBle.__init__(self, ble_controller, disconnected_cb, notification_cb, target) @@ -273,6 +282,12 @@ def __init__( CommunicatorType = TypeVar("CommunicatorType", bound=Union[GoProBle, GoProHttp]) IdType = TypeVar("IdType", SettingId, StatusId, ActionId, CmdId, BleUUID, str) ParserType = TypeVar("ParserType", BytesParser, JsonParser) +FilterType = TypeVar("FilterType", BytesTransformer, JsonTransformer) + + +############################################################################################################## +####### Messages (commands, etc.) +############################################################################################################## class RuleSignature(Protocol): @@ -296,25 +311,25 @@ class MessageRules(enum.Enum): WAIT_FOR_ENCODING_START = enum.auto() #: Message must not complete until encoding has started -class Message(Generic[CommunicatorType, IdType, ParserType], ABC): +class Message(Generic[CommunicatorType, IdType], ABC): """Base class for all messages that will be contained in a Messages class""" def __init__( self, identifier: IdType, - parser: Optional[ParserType] = None, - rules: Optional[dict[MessageRules, RuleSignature]] = None, + parser: Parser | None = None, + rules: dict[MessageRules, RuleSignature] | None = None, ) -> None: """Constructor Args: identifier (IdType): id to access this message by parser (ParserType): optional parser and builder - rules (Optional[dict[MessageRules, RuleSignature]], optional): rules to apply when executing this + rules (dict[MessageRules, RuleSignature] | None): rules to apply when executing this message. Defaults to None. """ self._identifier: IdType = identifier - self._parser: Optional[ParserType] = parser + self._parser = parser self._rules = rules or {} def _evaluate_rules(self, **kwargs: Any) -> list[MessageRules]: @@ -333,7 +348,7 @@ def _evaluate_rules(self, **kwargs: Any) -> list[MessageRules]: return enforced_rules @abstractmethod - def __call__(self, __communicator__: CommunicatorType, **kwargs: Any) -> Any: + async def __call__(self, __communicator__: CommunicatorType, **kwargs: Any) -> Any: """Execute the message by sending it to the target device Args: @@ -346,7 +361,7 @@ def __call__(self, __communicator__: CommunicatorType, **kwargs: Any) -> Any: raise NotImplementedError @abstractmethod - def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: + def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: """Return the attributes of the message as a dict Args: @@ -354,12 +369,12 @@ def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: **kwargs (Any): additional entries for the dict Returns: - dict[str, Any]: message as dict + types.JsonDict: message as dict """ raise NotImplementedError -class BleMessage(Message[GoProBle, IdType, BytesParser]): +class BleMessage(Message[GoProBle, IdType]): """The base class for all BLE messages to store common info Args: @@ -370,46 +385,46 @@ class BleMessage(Message[GoProBle, IdType, BytesParser]): def __init__( self, uuid: BleUUID, - parser: Optional[BytesParser], + parser: Parser | None, identifier: IdType, - rules: Optional[dict[MessageRules, RuleSignature]] = None, + rules: dict[MessageRules, RuleSignature] | None = None, ) -> None: Message.__init__(self, identifier, parser, rules) self._uuid = uuid self._base_dict = {"protocol": "BLE", "uuid": self._uuid} - if self._parser: - GoProResp._add_global_parser(identifier, self._parser) + if parser: + GlobalParsers.add(identifier, parser) -class HttpMessage(Message[GoProHttp, IdType, JsonParser]): +class HttpMessage(Message[GoProHttp, IdType]): """The base class for all HTTP messages. Stores common information.""" def __init__( self, endpoint: str, identifier: IdType, - components: Optional[list[str]] = None, - arguments: Optional[list[str]] = None, - parser: Optional[JsonParser] = None, - rules: Optional[dict[MessageRules, RuleSignature]] = None, + components: list[str] | None = None, + arguments: list[str] | None = None, + parser: Parser | None = None, + rules: dict[MessageRules, RuleSignature] | None = None, ) -> None: """Constructor Args: endpoint (str): base endpoint identifier (IdType): explicitly set message identifier. Defaults to None (generated from endpoint). - components (Optional[list[str]]): conditional endpoint components. Defaults to None. - arguments (Optional[list[str]]): URL argument names. Defaults to None. - parser (Optional[JsonParser]): additional parsing of JSON response. Defaults to None. - rules (Optional[dict[MessageRules, RuleSignature]], optional): rules to apply when executing this + components (list[str] | None): conditional endpoint components. Defaults to None. + arguments (list[str] | None): URL argument names. Defaults to None. + parser (Parser | None]): additional parsing of JSON response. Defaults to None. + rules (dict[MessageRules, RuleSignature] | None): rules to apply when executing this message. Defaults to None. """ self._endpoint = endpoint self._components = components self._args = arguments Message.__init__(self, identifier, parser, rules=rules) - self._base_dict: dict[str, Any] = { + self._base_dict: types.JsonDict = { "id": self._identifier, "protocol": "HTTP", "endpoint": self._endpoint, @@ -418,7 +433,7 @@ def __init__( def __str__(self) -> str: return str(self._identifier).title() - def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: + def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: """Return the attributes of the message as a dict Args: @@ -426,7 +441,7 @@ def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: **kwargs (Any): additional entries for the dict Returns: - dict[str, Any]: message as dict + types.JsonDict: message as dict """ # If any kwargs keys were to conflict with base dict, append underscore return self._base_dict | {f"{'_' if k in ['id', 'protocol'] else ''}{k}": v for k, v in kwargs.items()} @@ -452,14 +467,14 @@ def __init__(self, communicator: CommunicatorType) -> None: """ self._communicator = communicator # Append any automatically discovered instance attributes (i.e. for settings and statuses) - message_map: dict[Union[IdType, str], MessageType] = {} + message_map: dict[IdType | str, MessageType] = {} for message in self.__dict__.values(): if isinstance(message, Message): message_map[message._identifier] = message # type: ignore # Append any automatically discovered methods (i.e. for commands) for name, method in inspect.getmembers(self, predicate=inspect.ismethod): if not name.startswith("_"): - message_map[name.replace("_", " ").title()] = method + message_map[name.replace("_", " ").title()] = method # type: ignore dict.__init__(self, message_map) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/constants.py b/demos/python/sdk_wireless_camera_control/open_gopro/constants.py index 2e3a0abf..195de895 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/constants.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/constants.py @@ -4,160 +4,12 @@ """Constant numbers shared across the GoPro module. These do not change across Open GoPro Versions""" from __future__ import annotations -from enum import IntEnum, Enum, IntFlag, EnumMeta -from dataclasses import dataclass -from typing import Protocol, Union, Iterator, TypeVar, Any, no_type_check, Final -import construct +from dataclasses import dataclass +from typing import Final from open_gopro.ble import BleUUID, UUIDs - -T = TypeVar("T") - - -############################################################################################################## -##################### Custom Enum for this Project ######################################################## -############################################################################################################## - - -class ProtobufDescriptor(Protocol): - """Protocol definition for Protobuf enum descriptor used to generate GoPro enums from protobufs""" - - @property - def name(self) -> str: - """Human readable name of protobuf enum - - # noqa: DAR202 - - Returns: - str: enum name - """ - - @property - def values_by_name(self) -> dict: - """Get the enum values by name - - # noqa: DAR202 - - Returns: - dict: Dict of enum values mapped by name - """ - - @property - def values_by_number(self) -> dict: - """Get the enum values by number - - # noqa: DAR202 - - Returns: - dict: dict of enum numbers mapped by numberf - """ - - -class GoProEnumMeta(EnumMeta): - """Modify enum metaclass to build GoPro specific enums""" - - _is_proto = False - _iter_skip_names = ("NOT_APPLICABLE", "DESCRIPTOR") - - @no_type_check - def __new__(mcs, name, bases, classdict, **kwargs) -> GoProEnumMeta: # noqa - is_proto = "__is_proto__" in classdict - classdict["_ignore_"] = "__is_proto__" - classdict["__doc__"] = "" # Don't use useless "An enumeration" docstring - e = super().__new__(mcs, name, bases, classdict, **kwargs) - setattr(e, "_is_proto", is_proto) - return e - - @no_type_check - def __contains__(cls: type[Any], obj: object) -> bool: - if isinstance(obj, Enum): - return super().__contains__(obj) - if isinstance(obj, int): - return obj in [x.value for x in cls._member_map_.values()] - if isinstance(obj, str): - return obj.lower() in [x.name.lower() for x in cls._member_map_.values()] - raise TypeError( - f"unsupported operand type(s) for 'in': {type(obj).__qualname__} and {cls.__class__.__qualname__}" - ) - - def __iter__(cls: type[T]) -> Iterator[T]: - """Do not return enum values whose name is in the _iter_skip_names list - - Returns: - Iterator[T]: enum iterator - """ - return iter([x[1] for x in cls._member_map_.items() if x[0] not in GoProEnumMeta._iter_skip_names]) # type: ignore - - -class GoProFlagEnum(IntFlag, metaclass=GoProEnumMeta): - """GoPro specific enum to be used for all settings, statuses, and parameters - - The names NOT_APPLICABLE and DESCRIPTOR are special as they will not be returned as part of the enum iterator - """ - - def __eq__(self, other: object) -> bool: - if type(self)._is_proto: - if isinstance(other, int): - return self.value == other - if isinstance(other, str): - return self.name == other - if isinstance(other, Enum): - return self.value == other.value - raise TypeError(f"Unexpected case: proto enum can only be str or int, not {type(other)}") - return super().__eq__(other) - - def __hash__(self) -> Any: - return hash(self.name or "" + str(self.value)) - - -class GoProEnum(IntEnum, metaclass=GoProEnumMeta): - """GoPro specific enum to be used for all settings, statuses, and parameters - - The names NOT_APPLICABLE and DESCRIPTOR are special as they will not be returned as part of the enum iterator - """ - - def __eq__(self, other: object) -> bool: - if type(self)._is_proto: - if isinstance(other, int): - return self.value == other - if isinstance(other, str): - return self.name == other - if isinstance(other, Enum): - return self.value == other.value - raise TypeError(f"Unexpected case: proto enum can only be str or int, not {type(other)}") - return super().__eq__(other) - - def __hash__(self) -> Any: - return hash(self.name + str(self.value)) - - -def enum_factory(proto_enum: ProtobufDescriptor) -> type[GoProEnum]: - """Dynamically build a GoProEnum from a protobuf enum - - Args: - proto_enum (ProtobufDescriptor): input protobuf enum descriptor - - Returns: - GoProEnum: generated GoProEnum - """ - return GoProEnum( # type: ignore # pylint: disable=too-many-function-args - proto_enum.name, # type: ignore - { - **dict( - zip( - proto_enum.values_by_name.keys(), - proto_enum.values_by_number.keys(), - ) - ), - "__is_proto__": True, - }, - ) - - -############################################################################################################## -##################### End Enum Definition ################################################################### -############################################################################################################## +from open_gopro.enum import GoProEnum GOPRO_BASE_UUID: Final = "b5f9{}-aa8d-11e3-9046-0002a5d5c51b" @@ -324,6 +176,7 @@ class SettingId(GoProEnum): INTERNAL_104 = 104 INTERNAL_105 = 105 INTERNAL_106 = 106 + VIDEO_ASPECT_RATIO = 108 INTERNAL_111 = 111 INTERNAL_112 = 112 INTERNAL_114 = 114 @@ -369,9 +222,11 @@ class SettingId(GoProEnum): INTERNAL_164 = 164 INTERNAL_165 = 165 INTERNAL_166 = 166 - INTERNAL_167 = 167 + HINDSIGHT = 167 INTERNAL_168 = 168 INTERNAL_169 = 169 + PHOTO_INTERVAL = 171 + PHOTO_INTERVAL_DURATION = 172 VIDEO_PERFORMANCE_MODE = 173 INTERNAL_174 = 174 CAMERA_UX_MODE = 175 @@ -381,6 +236,18 @@ class SettingId(GoProEnum): STAR_TRAIL_LENGTH = 179 SYSTEM_VIDEO_MODE = 180 INTERNAL_181 = 181 + BIT_RATE = 182 + BIT_DEPTH = 183 + VIDEO_PROFILE = 184 + VIDEO_EASY_ASPECT_RATIO = 185 + VIDEO_MODE = 186 + TIMELAPSE_MODE = 187 + MULTI_SHOT_EASY_ASPECT_RATIO = 188 + ADDON_MAX_LENS_MOD = 189 + ADDON_MAX_LENS_MOD_ENABLE = 190 + PHOTO_MODE = 191 + MULTI_SHOT_NLV_ASPECT_RATIO = 192 + FRAMING = 193 PROTOBUF_SETTING = 0xF3 INVALID_FOR_TESTING = 0xFF @@ -513,11 +380,24 @@ class StatusId(GoProEnum): TOTAL_SD_SPACE_KB = 117 -ProducerType = tuple[QueryCmdId, Union[SettingId, StatusId]] -"""Types that can be registered for.""" +class WebcamStatus(GoProEnum): + """Webcam Statuses / states""" + + OFF = 0 + IDLE = 1 + HIGH_POWER_PREVIEW = 2 + LOW_POWER_PREVIEW = 3 -CmdType = Union[CmdId, QueryCmdId, ActionId] -"""Types that identify a command.""" -ResponseType = Union[CmdType, StatusId, SettingId, BleUUID, str, construct.Enum] -"""Types that are used to identify a response.""" +class WebcamError(GoProEnum): + """Errors common among Webcam commands""" + + SUCCESS = 0 + SET_PRESET = 1 + SET_WINDOW_SIZE = 2 + EXEC_STREAM = 3 + SHUTTER = 4 + COM_TIMEOUT = 5 + INVALID_PARAM = 6 + UNAVAILABLE = 7 + EXIT = 8 diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/connect_wifi.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/connect_wifi.py index 2ced820d..92b28f0d 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/connect_wifi.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/connect_wifi.py @@ -3,16 +3,18 @@ """Connect to the Wifi AP of a GoPro camera.""" -import logging import argparse +import asyncio +import logging from typing import Optional from rich.console import Console from open_gopro import WirelessGoPro -from open_gopro.util import setup_logging, set_stream_logging_level, add_cli_args_and_parse +from open_gopro.logger import set_stream_logging_level, setup_logging +from open_gopro.util import add_cli_args_and_parse, ainput -console = Console() # rich consoler printer +console = Console() def parse_arguments() -> argparse.Namespace: @@ -20,31 +22,27 @@ def parse_arguments() -> argparse.Namespace: return add_cli_args_and_parse(parser) -def main(args: argparse.Namespace) -> None: +async def main(args: argparse.Namespace) -> None: setup_logging(__name__, args.log) gopro: Optional[WirelessGoPro] = None - with WirelessGoPro( - args.identifier, wifi_interface=args.wifi_interface, sudo_password=args.password - ) as gopro: + async with WirelessGoPro(args.identifier, wifi_interface=args.wifi_interface, sudo_password=args.password) as gopro: # Now we only want errors set_stream_logging_level(logging.ERROR) - gopro.http_command.set_keep_alive() - console.print("\n\n🎆🎇✨ Success!! Wifi AP is connected 📡\n") console.print("Send commands as per https://gopro.github.io/OpenGoPro/http") - input("\nPress enter to disconnect Wifi and exit...") + await ainput("[blue]Press enter to disconnect Wifi and exit...", console.print) console.print("Exiting...") if gopro: - gopro.close() + await gopro.close() # Needed for poetry scripts defined in pyproject.toml def entrypoint() -> None: - main(parse_arguments()) + asyncio.run(main(parse_arguments())) if __name__ == "__main__": diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/__init__.py index 62126287..699cdbd4 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/__init__.py @@ -7,8 +7,9 @@ try: import tkinter - import PIL + import cv2 + import PIL except ModuleNotFoundError: print( diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/__init__.py index 9520542a..9cbcfff8 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/__init__.py @@ -6,8 +6,14 @@ import platform from typing import Union +from open_gopro.api import ( + BleCommands, + BleSettings, + BleStatuses, + HttpCommands, + HttpSettings, +) from open_gopro.api.builders import BleMessage, HttpMessage -from open_gopro.api import BleSettings, BleCommands, BleStatuses, HttpCommands, HttpSettings if (OS := platform.system().lower()) == "windows": THEME = "vista" diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/controllers.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/controllers.py index 671cc29a..e2a7a8e1 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/controllers.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/controllers.py @@ -4,17 +4,18 @@ """GUI controllers and associated common functionality""" from __future__ import annotations -import enum -import queue -import datetime + import asyncio +import datetime +import enum import logging -from pathlib import Path -from functools import partial +import queue +import tkinter as tk from abc import ABC, abstractmethod from dataclasses import dataclass -import tkinter as tk -from typing import Optional, Callable, Any, Union, no_type_check +from functools import partial +from pathlib import Path +from typing import Any, Callable, Optional, Union, no_type_check import cv2 import PIL.Image @@ -22,7 +23,8 @@ import wrapt from open_gopro.demos.gui.components import models, views -from open_gopro.util import setup_logging, pretty_print, add_logging_handler +from open_gopro.logger import add_logging_handler, setup_logging +from open_gopro.util import pretty_print ResponseHandlerType = Callable[[str, models.GoProResp], None] @@ -269,17 +271,11 @@ def update_status(self, status: StatusType) -> None: elif status in StatusBar.Wifi: self.view.update_status(self.view.wifi_status, status.value, status.name.replace("_", " ").title()) elif status in StatusBar.Ready: - self.view.update_status( - self.view.ready_status, status.value, status.name.replace("_", " ").title() - ) + self.view.update_status(self.view.ready_status, status.value, status.name.replace("_", " ").title()) elif status in StatusBar.Encoding: - self.view.update_status( - self.view.encoding_status, status.value, status.name.replace("_", " ").title() - ) + self.view.update_status(self.view.encoding_status, status.value, status.name.replace("_", " ").title()) elif status in StatusBar.Stream: - self.view.update_status( - self.view.stream_status, status.value, status.name.replace("_", " ").title() - ) + self.view.update_status(self.view.stream_status, status.value, status.name.replace("_", " ").title()) else: raise ValueError(f"No handler for status {status}") @@ -379,13 +375,14 @@ def handle_auto_start(self, identifier: str, response: models.GoProResp) -> None response (models.GoProResp): response that was received """ # Get binary's (which are of type Path) are not handled. TODO updating typing for this - if not isinstance(response, models.GoProResp) or not response.is_ok: + if not isinstance(response, models.GoProResp) or not response.ok: return + # TODO This is broken video_source: Optional[str] = None if str(identifier).lower() == "livestream": - video_source = response["url"] - elif str(identifier).lower() == "set preview stream" and "start" in (response.endpoint or ""): + video_source = response.identifier # type: ignore + elif str(identifier).lower() == "set preview stream" and "start" in (str(response.identifier) or ""): video_source = models.PREVIEW_STREAM_URL if video_source: @@ -708,9 +705,7 @@ def message_sender_factory(self, attribute: Optional[str] = None, use_args: bool async def on_message_send(self: MessagePalette) -> Optional[models.GoProResp]: assert self.active_message method = ( - self.active_message.message - if attribute is None - else getattr(self.active_message.message, attribute) + self.active_message.message if attribute is None else getattr(self.active_message.message, attribute) ) args = [] kwargs = {} @@ -746,9 +741,7 @@ class StatusTab(Controller): poll_period (int, optional): how often to refresh updates (in ms). Defaults to 200. """ - def __init__( - self, loop: asyncio.AbstractEventLoop, model: models.GoProModel, poll_period: int = 200 - ) -> None: + def __init__(self, loop: asyncio.AbstractEventLoop, model: models.GoProModel, poll_period: int = 200) -> None: super().__init__(loop) self.model = model self.period = poll_period diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/models.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/models.py index 83c03f97..f94d5cf7 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/models.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/models.py @@ -8,25 +8,42 @@ # to dynamically build messages from __future__ import annotations -import re + +import asyncio +import datetime import enum -import time -import logging import inspect -from pathlib import Path -import datetime +import logging +import re import typing -from typing import Pattern, Any, Callable, Generator, Optional, no_type_check, Union, Final +from pathlib import Path +from typing import ( + Any, + Callable, + Final, + Generator, + Optional, + Pattern, + Union, + no_type_check, +) -from wrapt.decorators import BoundFunctionWrapper import construct +from wrapt.decorators import BoundFunctionWrapper -from open_gopro import WirelessGoPro, constants -from open_gopro.api import BleStatus, HttpSetting, BleSetting -from open_gopro.interface import BleMessage, HttpMessage, Message, Messages, GoProBle, GoProHttp -import open_gopro.api.params +import open_gopro.api.params # needed for dynamic execution import open_gopro.api.params as Params -from open_gopro.responses import GoProResp, ResponseType +from open_gopro import WirelessGoPro, constants, proto, types +from open_gopro.api import BleSetting, BleStatus, HttpSetting +from open_gopro.communicator_interface import ( + BleMessage, + GoProBle, + GoProHttp, + HttpMessage, + Message, + Messages, +) +from open_gopro.models.response import GoProResp PREVIEW_STREAM_URL: Final = r"udp://127.0.0.1:8554" @@ -63,7 +80,7 @@ def __init__(self) -> None: # Initial instantiation just to get command strings self.gopro: CompoundGoPro = CompoundGoPro() - def start(self, identifier: Optional[Pattern]) -> None: + async def start(self, identifier: Optional[Pattern]) -> None: """Open the model (i.e. connect BLE and Wifi to camera) Args: @@ -71,7 +88,7 @@ def start(self, identifier: Optional[Pattern]) -> None: """ # Reinstantiate self.gopro = CompoundGoPro(target=identifier) - self.gopro.open() + await self.gopro.open() # NOTE: the following properties must be evaluated dynamically since self.gopro is changing # TODO hash and evaluate lazily @@ -204,9 +221,7 @@ def get_args_info(cls, message: Message) -> tuple[list[str], list[type]]: """ arg_types: list[type] = [] arg_names: list[str] = [] - method_info = inspect.getfullargspec( - message if (is_command := cls.is_command(message)) else message.set - ) + method_info = inspect.getfullargspec(message if (is_command := cls.is_command(message)) else message.set) for arg in method_info.kwonlyargs if is_command else method_info.args[1:]: if arg.startswith("_"): continue @@ -224,9 +239,7 @@ def get_args_info(cls, message: Message) -> tuple[list[str], list[type]]: arg_names.append(arg) return arg_names, arg_types - def get_message_info( - self, message: Message - ) -> tuple[list[Callable], list[Callable], list[type], list[str]]: + def get_message_info(self, message: Message) -> tuple[list[Callable], list[Callable], list[type], list[str]]: """For a given message, get its adapters, validator, argument types, and argument names Args: @@ -288,21 +301,21 @@ def updates( update value, update type) """ - def get_update_type(container: GoProResp, identifier: ResponseType) -> GoProModel.Update: + def get_update_type(container: GoProResp, identifier: types.ResponseType) -> GoProModel.Update: if container.protocol is GoProResp.Protocol.BLE: - if container.cmd in [ + if container.identifier in [ constants.QueryCmdId.GET_CAPABILITIES_VAL, constants.QueryCmdId.REG_CAPABILITIES_UPDATE, constants.QueryCmdId.SETTING_CAPABILITY_PUSH, ]: return GoProModel.Update.CAPABILITY - if container.cmd in [ + if container.identifier in [ constants.QueryCmdId.GET_SETTING_VAL, constants.QueryCmdId.REG_SETTING_VAL_UPDATE, constants.QueryCmdId.SETTING_VAL_PUSH, ]: return GoProModel.Update.SETTING - if container.cmd in [ + if container.identifier in [ constants.QueryCmdId.GET_STATUS_VAL, constants.QueryCmdId.REG_STATUS_VAL_UPDATE, constants.QueryCmdId.STATUS_VAL_PUSH, @@ -319,11 +332,12 @@ def get_update_type(container: GoProResp, identifier: ResponseType) -> GoProMode raise TypeError(f"Received unexpected protocol {container.protocol}") if isinstance(response, GoProResp): - for identifier, value in response.items(): + for identifier, value in response.data(): if type(identifier) in (constants.SettingId, constants.StatusId): yield identifier, value, get_update_type(response, identifier) elif response is None: - while update := self.gopro.get_notification(0): + # TODO need to update to new method. This is broken + while update := self.gopro.get_notification(0): # type: ignore for identifier, value in update.items(): yield identifier, value, get_update_type(update, identifier) @@ -338,7 +352,7 @@ def __init__(self, communicator: WirelessGoPro, identifier: Any, parser: Any = N def __str__(self) -> str: return self._identifier - def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: + def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: """Return the command as a dict Args: @@ -346,7 +360,7 @@ def _as_dict(self, *_: Any, **kwargs: Any) -> dict[str, Any]: **kwargs (Any) : additional dict keys to append Returns: - dict[str, Any]: Message as dict + types.JsonDict: Message as dict """ return {"protocol": "Complex", "id": self._identifier} | kwargs @@ -363,14 +377,14 @@ def __init__(self, communicator: WirelessGoPro) -> None: """ class LiveStream(CompoundCommand): - def __call__( # type: ignore + async def __call__( # type: ignore self, *, ssid: str, password: str, url: str, - window_size: Params.WindowSize, - lens_type: Params.LensType, + window_size: proto.EnumWindowSize, + lens_type: proto.EnumLens, min_bit: int, max_bit: int, start_bit: int, @@ -381,60 +395,24 @@ def __call__( # type: ignore ssid (str): SSID to connect to password (str): password of WiFi network url (str): url used to stream. Set to empty string to invalidate/cancel stream - window_size (open_gopro.api.params.WindowSize): Streaming video resolution - lens_type (open_gopro.api.params.LensType): Streaming Field of View + window_size (open_gopro.api.proto.EnumWindowSize): Streaming video resolution + lens_type (open_gopro.api.proto.EnumLens): Streaming Field of View min_bit (int): Desired minimum streaming bitrate (>= 800) max_bit (int): Desired maximum streaming bitrate (<= 8000) start_bit (int): Initial streaming bitrate (honored if 800 <= value <= 8000) - Raises: - RuntimeError: could not find desired ssid - Returns: GoProResp: status and url to start livestream """ - self._communicator.ble_command.set_shutter(shutter=Params.Toggle.DISABLE) - self._communicator.ble_command.register_livestream_status( - register=[Params.RegisterLiveStream.STATUS] + await self._communicator.ble_command.set_shutter(shutter=Params.Toggle.DISABLE) + await self._communicator.ble_command.register_livestream_status( + register=[proto.EnumRegisterLiveStreamStatus] ) - self._communicator.ble_command.scan_wifi_networks() - # Wait to receive scanning success - scan_id: Optional[int] = None - while update := self._communicator.get_notification(): - if ( - update == constants.ActionId.NOTIF_START_SCAN - and update["scanning_state"] == Params.ScanState.SUCCESS - ): - scan_id = update["scan_id"] - break - - # Get scan results and see if we need to provision - assert scan_id - for entry in self._communicator.ble_command.get_ap_entries(scan_id=scan_id)["entries"]: - if entry["ssid"] == ssid: - # Are we already provisioned? - if entry["scan_entry_flags"] & Params.ScanEntry.CONFIGURED: - logger.info("Connecting to already provisioned network...") - self._communicator.ble_command.request_wifi_connect(ssid=ssid) - else: - logger.info("Provisioning new network...") - self._communicator.ble_command.request_wifi_connect_new( - ssid=ssid, password=password - ) - # Wait to receive provisioning done notification (it is "New" AP in both cases) - while update := self._communicator.get_notification(): - if ( - update == constants.ActionId.NOTIF_PROVIS_STATE - and update["provisioning_state"] == Params.ProvisioningState.SUCCESS_NEW_AP - ): - break - break - else: - raise RuntimeError(f"Could not find network {ssid}") + await self._communicator.connect_to_access_point(ssid, password) # Start livestream - self._communicator.ble_command.set_livestream_mode( + await self._communicator.ble_command.set_livestream_mode( url=url, window_size=window_size, cert=bytes(), @@ -443,19 +421,29 @@ def __call__( # type: ignore starting_bitrate=start_bit, lens=lens_type, ) - # Wait to receive livestream started status - while update := self._communicator.get_notification(): - if ( - update == constants.ActionId.LIVESTREAM_STATUS_NOTIF - and update["live_stream_status"] == Params.LiveStreamStatus.READY - ): - break - time.sleep(2) - assert self._communicator.ble_command.set_shutter(shutter=Params.Toggle.ENABLE).is_ok - - response = GoProResp(meta=["Livestream"], raw_packet={"url": url}) - response._parse() - return response + + live_stream_ready = asyncio.Event() + + async def wait_for_livestream_ready(_: Any, value: proto.NotifyLiveStreamStatus) -> None: + if value.live_stream_status == proto.EnumLiveStreamStatus.LIVE_STREAM_STATE_READY: + live_stream_ready.set() + + self._communicator.register_update( + wait_for_livestream_ready, constants.ActionId.LIVESTREAM_STATUS_NOTIF + ) + logger.info("Starting livestream") + assert (await self._communicator.ble_command.set_shutter(shutter=Params.Toggle.ENABLE)).ok + logger.info("Waiting for livestream to be ready...\n") + await live_stream_ready.wait() + + assert self._communicator.ble_command.set_shutter(shutter=Params.Toggle.DISABLE) + + return GoProResp( + protocol=GoProResp.Protocol.BLE, + status=constants.ErrorCode.SUCCESS, + data=None, + identifier="LiveStream", + ) self.livestream = LiveStream(communicator, "Livestream") diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/util.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/util.py index 9c91512d..cc184e86 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/util.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/util.py @@ -4,12 +4,16 @@ """Common GUI utilities""" from __future__ import annotations + +import logging import queue import threading -from typing import Callable, Any +from typing import Any, Callable import cv2 +logger = logging.getLogger(__name__) + def display_video_blocking(source: str, printer: Callable = print) -> None: """Open a video source to display it, and block until the user stops it by sending 'q' @@ -35,17 +39,21 @@ def __init__(self, source: str, printer: Callable = print) -> None: printer (Callable): used to display output message. Defaults to print. """ self.printer = printer - printer("Starting viewer...") + self.printer("Starting viewer...") self.cap = cv2.VideoCapture(source + "?overrun_nonfatal=1&fifo_size=50000000", cv2.CAP_FFMPEG) self.q: queue.Queue[Any] = queue.Queue() super().__init__(daemon=True) self.start() - printer("Viewer started") - printer("Press 'q' in viewer to quit") + self.printer("Viewer started") + self.printer("Press 'q' in viewer to quit") while True: - frame = self.q.get() - cv2.imshow("frame", frame) - if cv2.waitKey(1) & 0xFF == ord("q"): + try: + frame = self.q.get() + cv2.imshow("frame", frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + except Exception as e: # pylint: disable=broad-exception-caught + logger.warning(e) break self.cap.release() cv2.destroyAllWindows() @@ -53,13 +61,15 @@ def __init__(self, source: str, printer: Callable = print) -> None: def run(self) -> None: """Read frames as soon as they are available, keeping only most recent one""" while True: - ret, frame = self.cap.read() - if not ret: - break - if not self.q.empty(): - try: + try: + ret, frame = self.cap.read() + if not ret: + self.printer("Received empty frame.") + continue + if not self.q.empty(): self.q.get_nowait() # discard previous (unprocessed) frame - except queue.Empty: - pass - - self.q.put(frame) + self.q.put(frame) + except queue.Empty: + pass + except: # pylint: disable=bare-except + break diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/views.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/views.py index 1b8d4028..9d2393bc 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/views.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/components/views.py @@ -6,20 +6,32 @@ # pylint: disable = arguments-differ from __future__ import annotations + import enum import json import logging import tkinter as tk -from tkinter import ttk, font import tkinter.scrolledtext as ScrolledText -from abc import abstractmethod, ABC -from urllib.parse import urlparse, parse_qs -from typing import Any, Union, Sequence, Callable, Optional, Generator, Generic, TypeVar, cast, no_type_check - -from PIL import Image, ImageTk, ImageDraw - +from abc import ABC, abstractmethod +from tkinter import font, ttk +from typing import ( + Any, + Callable, + Generator, + Generic, + Optional, + Sequence, + TypeVar, + Union, + cast, + no_type_check, +) +from urllib.parse import parse_qs, urlparse + +from PIL import Image, ImageDraw, ImageTk + +from open_gopro.demos.gui.components import THEME, models from open_gopro.util import pretty_print -from open_gopro.demos.gui.components import models, THEME MAX_TREEVIEW_ID = 1000000 @@ -392,7 +404,13 @@ def __init__(self, record: logging.LogRecord, fmt: str) -> None: def sanitize_message(self) -> None: """Clean up the directional headers from the logger message string""" self.message = ( - self.message.replace(self.ASYNC_TOKEN, "").strip("<>").strip("-").strip("<>").replace("\t", "") + self.message.replace(self.ASYNC_TOKEN, "") + .strip("<>") + .strip("-") + .strip("<>") + .replace("\t", "") + .replace("\n", "") + .strip(",") ) @property @@ -421,11 +439,11 @@ def logview_entries(self) -> Generator[tuple[str, tuple[str, ...]], None, None]: # First yield top level out: list[str] = [] for fmtstr in self.fmt: - out.append(pretty_print(work_dict.pop(fmtstr, ""))) - yield pretty_print(work_dict.pop("id")), tuple(out) + out.append(pretty_print(work_dict.pop(fmtstr, ""), should_quote=False)) + yield pretty_print(work_dict.pop("id"), should_quote=False), tuple(out) # Yield any additional items for key, value in work_dict.items(): - yield pretty_print(key), (pretty_print(str(value)),) + yield pretty_print(key, should_quote=False), (pretty_print(str(value), should_quote=False),) def __str__(self) -> str: return json.dumps(self.data, indent=4) @@ -438,10 +456,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.tv.column("protocol", width=20) self.tv.column("status", width=50) self.tv.column("target", width=50) - self.tv.heading("log_time", text="Time") + self.tv.heading("log_time", text="Time / Value") self.tv.heading("protocol", text="Protocol") self.tv.heading("status", text="Status") - self.tv.heading("target", text="UUID/Endpoint") + self.tv.heading("target", text="UUID / Endpoint") self.tv.tag_configure(Log.Format.TX.name, background=Log.Format.TX.value, foreground="black") self.tv.tag_configure(Log.Format.RX.name, background=Log.Format.RX.value, foreground="black") self.tv.tag_configure(Log.Format.ERROR.name, background=Log.Format.ERROR.value, foreground="black") @@ -450,12 +468,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.sbv.config(command=self.tv.yview) self.index = 0 self.img_open = ImageTk.PhotoImage(TreeViewLog.im_open, name="img_open", master=self.winfo_toplevel()) - self.img_close = ImageTk.PhotoImage( - TreeViewLog.im_close, name="img_close", master=self.winfo_toplevel() - ) - self.img_empty = ImageTk.PhotoImage( - TreeViewLog.im_empty, name="img_empty", master=self.winfo_toplevel() - ) + self.img_close = ImageTk.PhotoImage(TreeViewLog.im_close, name="img_close", master=self.winfo_toplevel()) + self.img_empty = ImageTk.PhotoImage(TreeViewLog.im_empty, name="img_empty", master=self.winfo_toplevel()) self.style = ttk.Style(self.winfo_toplevel()) self.style.element_create( "Treeitem.myindicator", @@ -637,9 +651,7 @@ def _create_with_args(self, param: str) -> None: self.value_label = ttk.Label(self.active_arg_frame, text=param, anchor="w") self.value_label.pack(side=tk.LEFT) - def create_option_menu( - self, param: str, options: Sequence[str], default: Optional[str] = None - ) -> GetterType: + def create_option_menu(self, param: str, options: Sequence[str], default: Optional[str] = None) -> GetterType: """Create a drop down menu for an argument Args: @@ -817,9 +829,7 @@ def _common_update( # The values key in TreeView will return all of its value (values and caps from our perspective) current_values = self.tv.item(tree_index)["values"] # Need to check is not None because falsy empty string is a valid input - new_values = tuple( - new if new is not None else old for new, old in zip((value, capability), current_values) - ) + new_values = tuple(new if new is not None else old for new, old in zip((value, capability), current_values)) self.tv.item(tree_index, tags=("recent",), values=new_values) else: store(int(identifier)) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/gui_demo.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/gui_demo.py index 550e30b2..229f04ff 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/gui_demo.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/gui_demo.py @@ -4,12 +4,13 @@ """Top level of GUI""" from __future__ import annotations + import asyncio import logging import tkinter as tk -from tkinter import ttk, font +from tkinter import font, ttk -from open_gopro.demos.gui.components import views, controllers, models, THEME +from open_gopro.demos.gui.components import THEME, controllers, models, views logger = logging.getLogger(__name__) @@ -66,9 +67,7 @@ def create_controllers(self) -> None: self.video_controller.bind_status_bar(self.statusbar_controller) self.menubar_controller = controllers.Menubar(self.loop, self.quit) self.statustab_controller = controllers.StatusTab(self.loop, self.gopro_model) - self.message_palette_controller.register_response_handler( - self.statustab_controller.display_response_updates - ) + self.message_palette_controller.register_response_handler(self.statustab_controller.display_response_updates) self.message_palette_controller.register_response_handler(self.video_controller.handle_auto_start) self.statustab_controller.bind_statusbar(self.statusbar_controller) for controller in self.controllers: diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/livestream.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/livestream.py index 7f14beb9..a374f0a5 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/livestream.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/livestream.py @@ -3,116 +3,75 @@ """Example to start and view a livestream""" -import time import argparse -from typing import Optional +import asyncio from rich.console import Console -from open_gopro import WirelessGoPro, Params, constants -from open_gopro.demos.gui.components.util import display_video_blocking -from open_gopro.util import setup_logging, add_cli_args_and_parse +from open_gopro import Params, WirelessGoPro +from open_gopro.constants import WebcamError, WebcamStatus +from open_gopro.logger import setup_logging +from open_gopro.util import add_cli_args_and_parse, ainput -console = Console() # rich consoler printer +console = Console() -def main(args: argparse.Namespace) -> None: +async def wait_for_webcam_status(gopro: WirelessGoPro, status: WebcamStatus, timeout: int = 10) -> bool: + """Wait for a specified webcam status for a given timeout + + Args: + gopro (WirelessGoPro): gopro to communicate with + status (WebcamStatus): status to wait for + timeout (int): timeout in seconds. Defaults to 10. + + Returns: + bool: True if status was received before timing out, False if timed out or received error + """ + + async def poll_for_status() -> bool: + # Poll until status is received + while True: + response = (await gopro.http_command.webcam_status()).data + if response.error != WebcamError.SUCCESS: + # Something bad happened + return False + if response.status == status: + # We found the desired status + return True + + # Wait for either status or timeout + try: + return await asyncio.wait_for(poll_for_status(), timeout) + except TimeoutError: + return False + + +async def main(args: argparse.Namespace) -> None: setup_logging(__name__, args.log) - with WirelessGoPro(args.identifier, enable_wifi=False) as gopro: - gopro.ble_command.set_shutter(shutter=Params.Toggle.DISABLE) - gopro.ble_command.register_livestream_status(register=[Params.RegisterLiveStream.STATUS]) - - console.print(f"Connecting to {args.ssid}...") - gopro.ble_command.scan_wifi_networks() - # Wait to receive scanning success - scan_id: Optional[int] = None - while update := gopro.get_notification(): - if ( - update == constants.ActionId.NOTIF_START_SCAN - and update["scanning_state"] == Params.ScanState.SUCCESS - ): - scan_id = update["scan_id"] - break - # Get scan results and see if we need to provision - assert scan_id - for entry in gopro.ble_command.get_ap_entries(scan_id=scan_id)["entries"]: - if entry["ssid"] == args.ssid: - # Are we already provisioned? - if entry["scan_entry_flags"] & Params.ScanEntry.CONFIGURED: - console.print("Connecting to already provisioned network...") - gopro.ble_command.request_wifi_connect(ssid=args.ssid) - else: - console.print("Provisioning new network...") - gopro.ble_command.request_wifi_connect_new(ssid=args.ssid, password=args.password) - # Wait to receive provisioning done notification (it is "New" AP in both cases) - while update := gopro.get_notification(): - if ( - update == constants.ActionId.NOTIF_PROVIS_STATE - and update["provisioning_state"] == Params.ProvisioningState.SUCCESS_NEW_AP - ): - break - break - else: - raise RuntimeError(f"Could not find network {args.ssid}") - - # Start livestream - console.print("Configuring livestream...") - gopro.ble_command.set_livestream_mode( - url=args.url, - window_size=args.resolution, - cert=bytes(), - minimum_bitrate=args.min_bit, - maximum_bitrate=args.max_bit, - starting_bitrate=args.start_bit, - lens=args.fov, - ) - # Wait to receive livestream started status - console.print("Waiting for livestream to be ready...\n") - while update := gopro.get_notification(): - if ( - update == constants.ActionId.LIVESTREAM_STATUS_NOTIF - and update["live_stream_status"] == Params.LiveStreamStatus.READY - ): - break - console.print("Starting livestream") - time.sleep(2) - assert gopro.ble_command.set_shutter(shutter=Params.Toggle.ENABLE).is_ok - - console.print("Displaying the video...") - display_video_blocking(args.url) - - assert gopro.ble_command.set_shutter(shutter=Params.Toggle.DISABLE) - gopro.ble_command.release_network() + async with WirelessGoPro(args.identifier) as gopro: + await gopro.ble_command.set_shutter(shutter=Params.Toggle.DISABLE) + if (await gopro.http_command.webcam_status()).data.status != WebcamStatus.OFF: + console.print("[blue]Webcam is currently on. Turning if off.") + assert (await gopro.http_command.webcam_stop()).ok + await wait_for_webcam_status(gopro, WebcamStatus.OFF) + + console.print("[blue]Starting webcam...") + await gopro.http_command.webcam_start() + await wait_for_webcam_status(gopro, WebcamStatus.HIGH_POWER_PREVIEW) + + await ainput("Press enter to exit.", console.print) def parse_arguments() -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Connect to the GoPro via BLE only, configure then start a Livestream, then display it with CV2." - ) - parser.add_argument("ssid", type=str, help="WiFi SSID to connect to.") - parser.add_argument("password", type=str, help="Password of WiFi SSID.") - parser.add_argument("url", type=str, help="RTMP server URL to stream to.") - parser.add_argument("--min_bit", type=int, help="Minimum bitrate.", default=1000) - parser.add_argument("--max_bit", type=int, help="Maximum bitrate.", default=1000) - parser.add_argument("--start_bit", type=int, help="Starting bitrate.", default=1000) - parser.add_argument( - "--resolution", - help="Resolution.", - choices=[x.value for x in Params.WindowSize], - default=Params.WindowSize.SIZE_480.value, - ) - parser.add_argument( - "--fov", - help="Field of View.", - choices=[x.value for x in Params.LensType], - default=Params.LensType.WIDE.value, + description="Connect to the GoPro via BLE and Wifi, start and view wireless webcam." ) - return add_cli_args_and_parse(parser, wifi=False) + return add_cli_args_and_parse(parser) def entrypoint() -> None: - main(parse_arguments()) + asyncio.run(main(parse_arguments())) if __name__ == "__main__": diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/preview_stream.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/preview_stream.py new file mode 100644 index 00000000..fa5aff5e --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/preview_stream.py @@ -0,0 +1,45 @@ +# preview_stream.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 + +"""Example to start and view a preview stream""" + +import argparse +import asyncio + +from rich.console import Console + +from open_gopro import Params, WirelessGoPro +from open_gopro.demos.gui.components.util import display_video_blocking +from open_gopro.logger import setup_logging +from open_gopro.util import add_cli_args_and_parse + +console = Console() + + +async def main(args: argparse.Namespace) -> None: + setup_logging(__name__, args.log) + + async with WirelessGoPro(args.identifier) as gopro: + await gopro.http_command.set_preview_stream(mode=Params.Toggle.DISABLE) + await gopro.ble_command.set_shutter(shutter=Params.Toggle.DISABLE) + assert (await gopro.http_command.set_preview_stream(mode=Params.Toggle.ENABLE)).ok + + console.print("Displaying the preview stream...") + display_video_blocking(r"udp://127.0.0.1:8554", printer=console.print) + + await gopro.http_command.set_preview_stream(mode=Params.Toggle.DISABLE) + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Connect to the GoPro via BLE and Wifi, start a preview stream, then display it with CV2." + ) + return add_cli_args_and_parse(parser, wifi=False) + + +def entrypoint() -> None: + asyncio.run(main(parse_arguments())) + + +if __name__ == "__main__": + entrypoint() diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/webcam.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/webcam.py index 59aadd2f..02b03edc 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/webcam.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/gui/webcam.py @@ -1,61 +1,111 @@ # usb.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). # This copyright was auto-generated on Fri Nov 18 00:18:13 UTC 2022 -"""Usb demo""" +"""USB webcam demo""" import argparse +import asyncio from typing import Final -from pathlib import Path from rich.console import Console -from open_gopro import WiredGoPro, Params +from open_gopro import Params, WiredGoPro, WirelessGoPro +from open_gopro.constants import WebcamError, WebcamStatus from open_gopro.demos.gui.components.util import display_video_blocking -from open_gopro.util import setup_logging +from open_gopro.gopro_base import GoProBase +from open_gopro.logger import setup_logging +from open_gopro.util import add_cli_args_and_parse -console = Console() # rich consoler printer +console = Console() STREAM_URL: Final[str] = r"udp://0.0.0.0:8554" -def main(args: argparse.Namespace) -> None: - setup_logging(__name__, args.log) - - with WiredGoPro(args.identifier) as gopro: - # Start webcam - gopro.http_command.wired_usb_control(control=Params.Toggle.DISABLE) - assert gopro.http_command.webcam_start().is_ok - - # Start player - display_video_blocking(STREAM_URL, printer=console.print) # blocks until user exists viewer - assert gopro.http_command.webcam_stop().is_ok - assert gopro.http_command.webcam_exit().is_ok - - console.print("Exiting...") +async def wait_for_webcam_status(gopro: GoProBase, statuses: set[WebcamStatus], timeout: int = 10) -> bool: + """Wait for specified webcam status(es) for a given timeout + + Args: + gopro (GoProBase): gopro to communicate with + statuses (set[WebcamStatus]): statuses to wait for + timeout (int): timeout in seconds. Defaults to 10. + + Returns: + bool: True if status was received before timing out, False if timed out or received error + """ + + async def poll_for_status() -> bool: + # Poll until status is received + while True: + response = (await gopro.http_command.webcam_status()).data + if response.error != WebcamError.SUCCESS: + # Something bad happened + return False + if response.status in statuses: + # We found the desired status + return True + + # Wait for either status or timeout + try: + return await asyncio.wait_for(poll_for_status(), timeout) + except TimeoutError: + return False + + +async def main(args: argparse.Namespace) -> None: + logger = setup_logging(__name__, args.log) + gopro: GoProBase | None = None + + try: + async with ( + WirelessGoPro(args.identifier, wifi_interface=args.wifi_interface) # type: ignore + if args.wireless + else WiredGoPro(args.identifier) + ) as gopro: + assert gopro + await gopro.http_command.wired_usb_control(control=Params.Toggle.DISABLE) + + await gopro.http_command.set_shutter(shutter=Params.Toggle.DISABLE) + if (await gopro.http_command.webcam_status()).data.status not in { + WebcamStatus.OFF, + WebcamStatus.IDLE, + }: + console.print("[blue]Webcam is currently on. Turning if off.") + assert (await gopro.http_command.webcam_stop()).ok + await wait_for_webcam_status(gopro, {WebcamStatus.OFF}) + + console.print("[blue]Starting webcam...") + await gopro.http_command.webcam_start() + await wait_for_webcam_status(gopro, {WebcamStatus.HIGH_POWER_PREVIEW}) + + # Start player + display_video_blocking(STREAM_URL, printer=console.print) # blocks until user exists viewer + console.print("[blue]Stopping webcam...") + assert (await gopro.http_command.webcam_stop()).ok + await wait_for_webcam_status(gopro, {WebcamStatus.OFF, WebcamStatus.IDLE}) + assert (await gopro.http_command.webcam_exit()).ok + await wait_for_webcam_status(gopro, {WebcamStatus.OFF}) + console.print("Exiting...") + + except Exception as e: # pylint: disable = broad-except + logger.error(repr(e)) + + if gopro: + await gopro.close() def parse_arguments() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Setup and view a GoPro webcam.") parser.add_argument( - "-i", - "--identifier", - type=str, - help="Last 3 digits of GoPro serial number, which is the last 3 digits of the default camera SSID. If \ - not specified, first GoPro discovered via mDNS will be used", - ) - parser.add_argument( - "-l", - "--log", - type=Path, - help="Location to store detailed log", - default="gopro_demo.log", + "--wireless", + action="store_true", + help="Set to use wireless (BLE / WIFI) instead of wired (USB)) interface", ) - return parser.parse_args() + return add_cli_args_and_parse(parser) # Needed for poetry scripts defined in pyproject.toml def entrypoint() -> None: - main(parse_arguments()) + asyncio.run(main(parse_arguments())) if __name__ == "__main__": diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/log_battery.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/log_battery.py index 397bdb2f..78cb29fa 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/log_battery.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/log_battery.py @@ -3,25 +3,26 @@ """Example to continuously read the battery (with no Wifi connection)""" +import argparse +import asyncio import csv -import time import logging -import argparse -import threading -from pathlib import Path -from datetime import datetime from dataclasses import dataclass -from typing import Optional, Literal +from datetime import datetime +from pathlib import Path +from typing import Optional from rich.console import Console -from open_gopro import WirelessGoPro +from open_gopro import WirelessGoPro, types from open_gopro.constants import StatusId -from open_gopro.util import setup_logging, set_stream_logging_level, add_cli_args_and_parse +from open_gopro.logger import set_stream_logging_level, setup_logging +from open_gopro.util import add_cli_args_and_parse -console = Console() # rich consoler printer +console = Console() -BarsType = Literal[0, 1, 2, 3] +last_percentage = 0 +last_bars = 0 @dataclass @@ -30,7 +31,7 @@ class Sample: index: int percentage: int - bars: BarsType + bars: int def __post_init__(self) -> None: self.time = datetime.now() @@ -58,46 +59,36 @@ def dump_results_as_csv(location: Path) -> None: w.writerow([s.index, (s.time - initial_time).seconds, s.percentage, s.bars]) -def process_battery_notifications( - gopro: WirelessGoPro, initial_bars: BarsType, initial_percentage: int -) -> None: - """Separate thread to continuously check for and store battery notifications. - - If the CLI parameter was set to poll, this isn't used. +async def process_battery_notifications(update: types.UpdateType, value: int) -> None: + """Handle asynchronous battery update notifications Args: - gopro (WirelessGoPro): instance to get updates from - initial_bars (BarsType): Initial bars level when notifications were enabled - initial_percentage (int): Initial percentage when notifications were enabled + update (types.UpdateType): type of update + value (int): value of update """ - last_percentage = initial_percentage - last_bars = initial_bars - - # Block until we receive an update - while notification := gopro.get_notification(): - # Update data points if they have changed - last_percentage = ( - notification.data[StatusId.INT_BATT_PER] - if StatusId.INT_BATT_PER in notification.data - else last_percentage - ) - last_bars = ( - notification.data[StatusId.BATT_LEVEL] if StatusId.BATT_LEVEL in notification.data else last_bars - ) - # Append and print sample - global SAMPLE_INDEX - SAMPLES.append(Sample(index=SAMPLE_INDEX, percentage=last_percentage, bars=last_bars)) - console.print(str(SAMPLES[-1])) - SAMPLE_INDEX += 1 - - -def main(args: argparse.Namespace) -> None: + + global last_percentage + global last_bars + + if update == StatusId.INT_BATT_PER: + last_percentage = value + elif update == StatusId.BATT_LEVEL: + last_bars = value + + # Append and print sample + global SAMPLE_INDEX + SAMPLES.append(Sample(index=SAMPLE_INDEX, percentage=last_percentage, bars=last_bars)) + console.print(str(SAMPLES[-1])) + SAMPLE_INDEX += 1 + + +async def main(args: argparse.Namespace) -> None: logger = setup_logging(__name__, args.log) global SAMPLE_INDEX gopro: Optional[WirelessGoPro] = None try: - with WirelessGoPro(args.identifier, enable_wifi=False) as gopro: + async with WirelessGoPro(args.identifier, enable_wifi=False) as gopro: set_stream_logging_level(logging.ERROR) if args.poll: @@ -106,27 +97,34 @@ def main(args: argparse.Namespace) -> None: SAMPLES.append( Sample( index=SAMPLE_INDEX, - percentage=gopro.ble_status.int_batt_per.get_value().flatten, - bars=gopro.ble_status.batt_level.get_value().flatten, + percentage=(await gopro.ble_status.int_batt_per.get_value()).data, + bars=(await gopro.ble_status.batt_level.get_value()).data, ) ) console.print(str(SAMPLES[-1])) SAMPLE_INDEX += 1 - time.sleep(args.poll) + await asyncio.sleep(args.poll) # Otherwise set up notifications else: + global last_bars + global last_percentage + console.print("Configuring battery notifications...") # Enable notifications of the relevant battery statuses. Also store initial values. - bars = gopro.ble_status.batt_level.register_value_update().flatten - percentage = gopro.ble_status.int_batt_per.register_value_update().flatten + last_bars = ( + await gopro.ble_status.batt_level.register_value_update(process_battery_notifications) + ).data + last_percentage = ( + await gopro.ble_status.int_batt_per.register_value_update(process_battery_notifications) + ).data + # Append initial sample + SAMPLES.append(Sample(index=SAMPLE_INDEX, percentage=last_percentage, bars=last_bars)) + console.print(str(SAMPLES[-1])) + # Start a thread to handle asynchronous battery level notifications - threading.Thread( - target=process_battery_notifications, args=(gopro, bars, percentage), daemon=True - ).start() - with console.status("[bold green]Receiving battery notifications until it dies..."): - # Sleep forever, allowing notification handler thread to deal with battery level notifications - while True: - time.sleep(1) + console.print("[bold green]Receiving battery notifications until it dies...") + while True: + await asyncio.sleep(1) except KeyboardInterrupt: logger.warning("Received keyboard interrupt. Shutting down...") @@ -134,7 +132,7 @@ def main(args: argparse.Namespace) -> None: csv_location = Path(args.log.parent) / "battery_results.csv" dump_results_as_csv(csv_location) if gopro: - gopro.close() + await gopro.close() console.print("Exiting...") @@ -153,7 +151,7 @@ def parse_arguments() -> argparse.Namespace: def entrypoint() -> None: - main(parse_arguments()) + asyncio.run(main(parse_arguments())) if __name__ == "__main__": diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/photo.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/photo.py index 37344faa..634ec962 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/photo.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/photo.py @@ -4,54 +4,56 @@ """Entrypoint for taking a picture demo.""" import argparse -from typing import Optional, Union +import asyncio from pathlib import Path from rich.console import Console -from open_gopro import WirelessGoPro, WiredGoPro, Params -from open_gopro.util import setup_logging, add_cli_args_and_parse +from open_gopro import Params, WiredGoPro, WirelessGoPro, proto +from open_gopro.gopro_base import GoProBase +from open_gopro.logger import setup_logging +from open_gopro.util import add_cli_args_and_parse -console = Console() # rich consoler printer +console = Console() -def main(args: argparse.Namespace) -> None: +async def main(args: argparse.Namespace) -> None: logger = setup_logging(__name__, args.log) + gopro: GoProBase | None = None - gopro: Optional[Union[WiredGoPro, WirelessGoPro]] = None try: - with ( + async with ( WiredGoPro(args.identifier) # type: ignore if args.wired else WirelessGoPro(args.identifier, wifi_interface=args.wifi_interface) ) as gopro: assert gopro # Configure settings to prepare for photo - gopro.http_setting.video_performance_mode.set(Params.PerformanceMode.MAX_PERFORMANCE) - gopro.http_setting.max_lens_mode.set(Params.MaxLensMode.DEFAULT) - gopro.http_setting.camera_ux_mode.set(Params.CameraUxMode.PRO) - gopro.http_command.set_turbo_mode(mode=Params.Toggle.DISABLE) - assert gopro.http_command.load_preset_group(group=Params.PresetGroup.PHOTO).is_ok + await gopro.http_setting.video_performance_mode.set(Params.PerformanceMode.MAX_PERFORMANCE) + await gopro.http_setting.max_lens_mode.set(Params.MaxLensMode.DEFAULT) + await gopro.http_setting.camera_ux_mode.set(Params.CameraUxMode.PRO) + await gopro.http_command.set_turbo_mode(mode=Params.Toggle.DISABLE) + assert (await gopro.http_command.load_preset_group(group=proto.EnumPresetGroup.PRESET_GROUP_ID_PHOTO)).ok # Get the media list before - media_set_before = set(x["n"] for x in gopro.http_command.get_media_list().flatten) + media_set_before = set((await gopro.http_command.get_media_list()).data.files) # Take a photo console.print("Capturing a photo...") - assert gopro.http_command.set_shutter(shutter=Params.Toggle.ENABLE).is_ok + assert (await gopro.http_command.set_shutter(shutter=Params.Toggle.ENABLE)).ok # Get the media list after - media_set_after = set(x["n"] for x in gopro.http_command.get_media_list().flatten) + media_set_after = set((await gopro.http_command.get_media_list()).data.files) # The photo is (most likely) the difference between the two sets photo = media_set_after.difference(media_set_before).pop() # Download the photo console.print("Downloading the photo...") - gopro.http_command.download_file(camera_file=photo, local_file=args.output) + await gopro.http_command.download_file(camera_file=photo.filename, local_file=args.output) console.print(f"Success!! :smiley: File has been downloaded to {args.output}") except Exception as e: # pylint: disable = broad-except logger.error(repr(e)) if gopro: - gopro.close() + await gopro.close() def parse_arguments() -> argparse.Namespace: @@ -73,7 +75,7 @@ def parse_arguments() -> argparse.Namespace: # Needed for poetry scripts defined in pyproject.toml def entrypoint() -> None: - main(parse_arguments()) + asyncio.run(main(parse_arguments())) if __name__ == "__main__": diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/preset_control.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/preset_control.py deleted file mode 100644 index 4bd6afd8..00000000 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/preset_control.py +++ /dev/null @@ -1,932 +0,0 @@ -# preset_control.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Tue Aug 30 17:53:34 UTC 2022 - -"""Entrypoint for taking a picture demo.""" - -from __future__ import annotations -import enum -import logging -import argparse -from typing import Optional, Union, Callable - -from rich.console import Console -from rich.prompt import IntPrompt, Prompt -from rich.table import Table -from rich import box - -from open_gopro import WirelessGoPro, GoProResp -from open_gopro import Params as GoProParams -from open_gopro.constants import GoProEnum, SettingId -from open_gopro.util import set_stream_logging_level, setup_logging, add_cli_args_and_parse - -console = Console() # rich consoler printer -gopro: Optional[WirelessGoPro] = None - -######### BEGIN PRESET PARSER ######### - - -class Setting: - """Class representation of preset setting - - Data populated by parsing preset status response - """ - - def __init__(self, settingDict: dict): - """Constructor - - Raises: - RuntimeError: Error parsing Setting from response dict - - Args: - settingDict (dict): dict representation of setting - """ - self.id: Union[SettingId, int] - self.value: Union[GoProEnum, int] - self.is_caption: bool - - valueType: Optional[Callable] = None - if (parsedId := settingDict.get("id")) is not None: - if parsedId in [i.value for i in SettingId]: - self.id = SettingId(parsedId) - valueType = GoProResp._get_query_container(self.id) - else: - self.id = parsedId - else: - raise RuntimeError("Error parsing preset id for Setting") - if (parsedValue := settingDict.get("value")) is not None: - self.value = valueType(parsedValue) if valueType else parsedValue - else: - raise RuntimeError("Error parsing preset value for Setting") - if (parsedis_caption := settingDict.get("is_caption")) is not None: - self.is_caption = parsedis_caption - else: - raise RuntimeError("Error parsing preset is_caption for Setting") - - def to_table(self) -> Table: - """Formats data into rich table - - Returns: - Table: rich table representation of data - """ - settingTable = Table("Setting", box=box.SQUARE) - - settingTable.add_row("id:", str(self.id)) - settingTable.add_row("value:", str(self.value)) - settingTable.add_row("is_caption:", str(self.is_caption)) - - return settingTable - - -class Preset: - """Class representation of preset - - Data populated by parsing preset status response - """ - - def __init__(self, presetDict: dict): - """Constructor - - Raises: - RuntimeError: Error parsing Setting from response dict - - Args: - presetDict (dict): dict representation of preset - """ - self.id: int - self.user_defined: bool = False - self.is_modified: bool = False - self.mode: Optional[enum.Enum] = None - self.title_id: Optional[enum.Enum] = None - self.icon: Optional[enum.Enum] = None - self.is_fixed: Optional[bool] = None - self.setting_array: Optional[list[Setting]] = None - - if (parsedId := presetDict.get("id")) is not None: - self.id = parsedId - else: - raise RuntimeError("Error parsing preset id for Preset") - - if (parsed_user_defined := presetDict.get("user_defined")) is not None: - self.user_defined = parsed_user_defined - else: - raise RuntimeError("Error parsing user_defined for Preset") - - if (parsed_is_modified := presetDict.get("is_modified")) is not None: - self.is_modified = parsed_is_modified - else: - raise RuntimeError("Error parsing is_modified for Preset") - - if (parsedMode := presetDict.get("mode")) is not None: - self.mode = parsedMode - if (parsed_title_id := presetDict.get("title_id")) is not None: - self.title_id = parsed_title_id - if (parsed_icon := presetDict.get("icon")) is not None: - self.icon = parsed_icon - if (parsed_is_fixed := presetDict.get("is_fixed")) is not None: - self.is_fixed = parsed_is_fixed - if (settingDictArray := presetDict.get("setting_array")) is not None: - if isinstance(settingDictArray, list): - self.setting_array = [] - for settingDict in settingDictArray: - self.setting_array.append(Setting(settingDict)) - - def to_table(self, activePreset: Optional[int] = None) -> Table: - """Formats data into rich table - - Args: - activePreset (Union[GoProParams.Preset, int, None]): The currently active preset if known, used to highlight active preset in table - - Returns: - Table: rich table representation of data - """ - presetTable = Table("Preset", box=box.SQUARE) - if (activePreset is not None) and activePreset == self.id: - presetTable.add_column("ACTIVE") - presetTable.box = box.HEAVY - else: - presetTable.add_column("") - - presetTable.add_row("id:", str(self.id)) - presetTable.add_row("user_defined:", str(self.user_defined)) - - presetTable.add_row("is_modified:", str(self.is_modified)) - - if self.mode is not None: - presetTable.add_row("mode:", str(self.mode)) - if self.title_id is not None: - presetTable.add_row("title_id:", str(self.title_id)) - if self.icon is not None: - presetTable.add_row("icon:", str(self.icon)) - if self.is_fixed is not None: - presetTable.add_row("is_fixed:", str(self.is_fixed)) - if self.setting_array is not None: - settingTables: list[Table] = [] - for setting in self.setting_array: - settingTables.append(setting.to_table()) - # Use horizontal grid layout for compactness - setting_arrayGrid = Table.grid(expand=False) - setting_arrayGrid.add_row(*settingTables) - presetTable.add_row("Settings:", setting_arrayGrid) - - return presetTable - - -class PresetGroup: - """Class representation of preset group - - Data populated by parsing preset status response - """ - - def __init__(self, presetGroupDict: dict): - """Constructor - - Raises: - RuntimeError: Error parsing Setting from response dict - - Args: - presetGroupDict (dict): dict representation of preset group - """ - self.id: Union[GoProParams.PresetGroup, int] - self.presetArray: list[Preset] = [] - self.can_add_preset: Optional[bool] = None - - # Parse id - if (parsedId := presetGroupDict.get("id")) is not None: - self.id = parsedId - else: - raise RuntimeError("Error parsing id for PresetGroup") - - # Parse can_add_preset - if (parsed_can_add_preset := presetGroupDict.get("can_add_preset")) is not None: - self.can_add_preset = parsed_can_add_preset - - # Parse preset_array - presetDictArray: Optional[list[dict]] = presetGroupDict.get("preset_array") - if presetDictArray is not None: - for preset in presetDictArray: - self.presetArray.append(Preset(preset)) - else: - raise RuntimeError("Error parsing presetArray for PresetGroup") - - def to_table(self, activePreset: Optional[int] = None) -> Table: - """Formats data into rich table - - Args: - activePreset (Optional[int]): The currently active preset if known, used to highlight active preset in table - - Returns: - Table: rich table representation of data - """ - presetGroupTable = Table("Preset Group", box=box.SQUARE) - - presetGroupTable.add_row("id:", str(self.id)) - - if self.can_add_preset is not None: - presetGroupTable.add_row("can_add_preset:", str(self.can_add_preset)) - - # Horizontal layout is more compact but can cause present and setting attributes to be cut off - for preset in self.presetArray: - presetGroupTable.add_row("", preset.to_table(activePreset)) - - return presetGroupTable - - def get_preset_by_id(self, presetId: int) -> Optional[Preset]: - """helper function for fetching the preset with specified preset id - - Args: - presetId (int): The preset id being searched for - - Returns: - Optional[Preset]: The matching preset found - """ - for preset in self.presetArray: - if presetId == preset.id: - return preset - return None - - -class PresetCollection: - """Class representation of preset collection - - Data populated by parsing preset status response - """ - - def __init__(self, presetCollectionDict: dict): - """Constructor - - Raises: - RuntimeError: Error parsing Setting from response dict - - Args: - presetCollectionDict (dict): dict representation of preset collection - """ - self.presetGroupArray: list[PresetGroup] = [] - - # Parse presetGroupArray - presetGroupDictArray = presetCollectionDict.get("preset_group_array") - if isinstance(presetGroupDictArray, list): - for presetGroupDict in presetGroupDictArray: - self.presetGroupArray.append(PresetGroup(presetGroupDict)) - else: - raise RuntimeError("Error parsing presetGroupArray for PresetCollection") - - # - def to_table(self, activePreset: Optional[int] = None) -> Table: - """Formats data into rich table - - Args: - activePreset (Union[GoProParams.Preset, int, None]): The currently active preset if known, used to - highlight active preset in table - - Returns: - Table: rich table representation of data - """ - presetCollectionTable = Table("Preset Collection", box=box.SQUARE) - - for presetGroup in self.presetGroupArray: - presetCollectionTable.add_row(presetGroup.to_table(activePreset)) - - return presetCollectionTable - - def get_preset_tuple(self, presetId: int) -> tuple[Optional[PresetGroup], Optional[Preset]]: - """helper function for fetching the preset with specified preset id and its parent preset group - - Args: - presetId (int): The preset id being searched for - - Returns: - tuple[Optional[PresetGroup], Optional[Preset]]: The matching preset and preset group if found - """ - for presetGroup in self.presetGroupArray: - if preset := presetGroup.get_preset_by_id(presetId): - return presetGroup, preset - return None, None - - -######### END PRESET PARSER ######### - - -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Connect to a GoPro camera's Wifi Access Point.") - return add_cli_args_and_parse(parser) - - -class MenuMode(enum.Enum): - """Enum to identify the current menu state""" - - MAIN = 0 - TOP_LEVEL_SETTINGS = 1 - COLLECTION = 2 - DEMO = 3 - - -class CameraModel(enum.Enum): - """Enum to identify the camera model""" - - HERO10 = 0 - HERO11 = 1 - HERO11_MINI = 2 - - -def applySettingChange(setting: SettingId, value: GoProEnum) -> None: - """Applies specified value to the specified setting - - Args: - setting (SettingId): The setting to modify - value (GoProEnum): The value to apply to the setting - """ - if gopro is None: - return - - if setting not in gopro.ble_setting: - return - - gopro.ble_setting[setting].set(value) - - -def printCurrentPresetStatus() -> None: - """Prints the current preset status""" - if gopro is None: - return - - presetCollection = PresetCollection(gopro.ble_command.get_preset_status().data) - activePresetId = gopro.ble_status.active_preset.get_value().flatten - console.print(presetCollection.to_table(activePresetId)) - - -def presetDemo(cameraModel: CameraModel) -> None: - """Demonstrates setting combinations which cause distinct preset collections - - Args: - cameraModel (CameraModel): Model for the paired camera - """ - if gopro is None: - return - - console.print( - "[green]This demo walks through all setting combinations that cause distinct preset combinations." - ) - - if cameraModel == CameraModel.HERO11_MINI: - # Easy - console.print("[blue bold]Preset Collection 1") - console.print("[gray]UX Mode:[/gray] [purple]Easy") - applySettingChange(SettingId.CAMERA_UX_MODE, GoProParams.CameraUxMode.EASY) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - # Pro - console.print("[blue bold]Preset Collection 2") - console.print("[gray]UX Mode:[/gray] [purple]Pro") - applySettingChange(SettingId.CAMERA_UX_MODE, GoProParams.CameraUxMode.PRO) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to end demo") - - else: - # Max Lens - console.print("[blue bold]Preset Collection 1") - console.print("[gray]Max Lens:[/gray] [purple]On") - applySettingChange(SettingId.MAX_LENS_MOD, GoProParams.MaxLensMode.MAX_LENS) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - if cameraModel == CameraModel.HERO11: - # Easy Highest Quality - console.print("[blue bold]Preset Collection 2") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]UX Mode:[/gray] [purple]Easy") - console.print("[gray]Easy Night Photo:[/gray] [purple]Off") - console.print("[gray]System Video Mode:[/gray] [purple]Highest Quality") - applySettingChange(SettingId.MAX_LENS_MOD, GoProParams.MaxLensMode.DEFAULT) - applySettingChange(SettingId.CAMERA_UX_MODE, GoProParams.CameraUxMode.EASY) - gopro.ble_command.load_preset_group(group=GoProParams.PresetGroup.PHOTO) - applySettingChange(SettingId.PHOTO_EASY_MODE, GoProParams.PhotoEasyMode.OFF) - gopro.ble_command.load_preset_group(group=GoProParams.PresetGroup.VIDEO) - applySettingChange(SettingId.SYSTEM_VIDEO_MODE, GoProParams.SystemVideoMode.HIGHEST_QUALITY) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - # Easy Highest Quality Night Photo - console.print("[blue bold]Preset Collection 3") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]UX Mode:[/gray] [purple]Easy") - console.print("[gray]Easy Night Photo:[/gray] [purple]On") - console.print("[gray]System Video Mode:[/gray] [purple]Highest Quality") - gopro.ble_command.load_preset_group(group=GoProParams.PresetGroup.PHOTO) - applySettingChange(SettingId.PHOTO_EASY_MODE, GoProParams.PhotoEasyMode.NIGHT_PHOTO) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - # Easy Extended Battery - console.print("[blue bold]Preset Collection 4") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]UX Mode:[/gray] [purple]Easy") - console.print("[gray]Easy Night Photo:[/gray] [purple]Off") - console.print("[gray]System Video Mode:[/gray] [purple]Extended Battery") - applySettingChange(SettingId.PHOTO_EASY_MODE, GoProParams.PhotoEasyMode.OFF) - gopro.ble_command.load_preset_group(group=GoProParams.PresetGroup.VIDEO) - applySettingChange(SettingId.SYSTEM_VIDEO_MODE, GoProParams.SystemVideoMode.EXTENDED_BATTERY) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - # Easy Extended Battery Night Photo - console.print("[blue bold]Preset Collection 5") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]UX Mode:[/gray] [purple]Easy") - console.print("[gray]Easy Night Photo:[/gray] [purple]On") - - console.print("[gray]System Video Mode:[/gray] [purple]Extended Battery") - gopro.ble_command.load_preset_group(group=GoProParams.PresetGroup.PHOTO) - applySettingChange(SettingId.PHOTO_EASY_MODE, GoProParams.PhotoEasyMode.NIGHT_PHOTO) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - # Pro Highest Quality - console.print("[blue bold]Preset Collection 6") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]UX Mode:[/gray] [purple]Pro") - console.print("[gray]System Video Mode:[/gray] [purple]Highest Quality") - applySettingChange(SettingId.CAMERA_UX_MODE, GoProParams.CameraUxMode.PRO) - gopro.ble_command.load_preset_group(group=GoProParams.PresetGroup.VIDEO) - applySettingChange(SettingId.SYSTEM_VIDEO_MODE, GoProParams.SystemVideoMode.EXTENDED_BATTERY) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - # Pro Extended Battery - console.print("[blue bold]Preset Collection 7") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]UX Mode:[/gray] [purple]Pro") - console.print("[gray]System Video Mode:[/gray] [purple]Extended Battery") - applySettingChange(SettingId.CAMERA_UX_MODE, GoProParams.CameraUxMode.PRO) - gopro.ble_command.load_preset_group(group=GoProParams.PresetGroup.VIDEO) - applySettingChange(SettingId.SYSTEM_VIDEO_MODE, GoProParams.SystemVideoMode.EXTENDED_BATTERY) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to end demo") - else: - # Max Performance - console.print("[blue bold]Preset Collection 2") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]Video Performance Mode:[/gray] [purple]Max Performance") - applySettingChange(SettingId.MAX_LENS_MOD, GoProParams.MaxLensMode.DEFAULT) - applySettingChange(SettingId.VIDEO_PERFORMANCE_MODE, GoProParams.PerformanceMode.MAX_PERFORMANCE) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - # Extended Battery - console.print("[blue bold]Preset Collection 3") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]Video Performance Mode:[/gray] [purple]Extended Battery") - applySettingChange(SettingId.VIDEO_PERFORMANCE_MODE, GoProParams.PerformanceMode.EXTENDED_BATTERY) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to continue to the next preset collection") - - # Stationary - console.print("[blue bold]Preset Collection 4") - console.print("[gray]Max Lens:[/gray] [purple]Off") - console.print("[gray]Video Performance Mode:[/gray] [purple]Stationary") - applySettingChange(SettingId.VIDEO_PERFORMANCE_MODE, GoProParams.PerformanceMode.STATIONARY) - printCurrentPresetStatus() - Prompt.ask(prompt="[yellow]Press enter to end demo") - - -def main(args: argparse.Namespace) -> None: - global gopro - logger = setup_logging(__name__, args.log) - - # Constants - topLevelSettings: list[SettingId] - hero10_topLevelSettings: list[SettingId] = [SettingId.MAX_LENS_MOD, SettingId.VIDEO_PERFORMANCE_MODE] - hero11_topLevelSettings: list[SettingId] = [ - SettingId.MAX_LENS_MOD, - SettingId.SYSTEM_VIDEO_MODE, - SettingId.CAMERA_UX_MODE, - SettingId.PHOTO_EASY_MODE, - ] - hero11_mini_topLevelSettings: list[SettingId] = [SettingId.CAMERA_UX_MODE] - - # State - collectionCacheInvalidated = True - activeCacheInvalidated = True - loopRunning = True - menuMode: MenuMode = MenuMode.MAIN - focusedPresetGroup: Optional[PresetGroup] = None - focusedPreset: Optional[Preset] = None - focusedSetting: Optional[Setting] = None - focusedTopLevelSetting: Optional[SettingId] = None - activePresetGroup: Optional[PresetGroup] = None - activePreset: Optional[Preset] = None - presetCollectionCache: Optional[PresetCollection] = None - cameraModel: CameraModel = CameraModel.HERO10 - - with WirelessGoPro( - args.identifier, - wifi_interface=args.wifi_interface, - sudo_password=args.password, - enable_wifi=False, - ) as gopro: - # Now we only want errors - set_stream_logging_level(logging.ERROR) - - if (fwVersion := gopro.ble_command.get_hardware_info().data.get("firmware_version")) is not None: - if fwVersion.split(".")[0] == "H21": - cameraModel = CameraModel.HERO10 - elif fwVersion.split(".")[0] == "H22": - if fwVersion.split(".")[1] == "01": - cameraModel = CameraModel.HERO11 - else: - cameraModel = CameraModel.HERO11_MINI - - if cameraModel == CameraModel.HERO10: - console.print("Detected Hero 10") - topLevelSettings = hero10_topLevelSettings - elif cameraModel == CameraModel.HERO11: - console.print("Detected Hero 11 camera") - topLevelSettings = hero11_topLevelSettings - elif cameraModel == CameraModel.HERO11_MINI: - console.print("Detected Hero 11 Mini camera") - topLevelSettings = hero11_mini_topLevelSettings - else: - console.print("Unsupported camera detected") - return - - while loopRunning: - # REFRESH INVALID CACHES - if collectionCacheInvalidated: - console.print("[yellow]Refreshing preset status cache") - presetCollectionCache = PresetCollection( - gopro.ble_command.get_preset_status(register=[GoProParams.RegisterPreset.PRESET]).data - ) - collectionCacheInvalidated = False - - if presetCollectionCache is None: - logger.error("Unable to fetch or parse current preset status, ending demo") - loopRunning = False - continue - - if activeCacheInvalidated: - console.print("[yellow]Refreshing active preset cache") - activePresetId = gopro.ble_status.active_preset.get_value().flatten - activePresetGroup, activePreset = presetCollectionCache.get_preset_tuple(activePresetId) - activeCacheInvalidated = False - - # MAIN MENU - if menuMode == MenuMode.MAIN: - console.print("[bold blue]Main Menu") - menu = Table.grid(expand=False) - menu.add_column(justify="right", width=4) - menu.add_column() - menu.add_column() - menu.add_row("0", " ) ", "Print current preset status") - menu.add_row("1", " ) ", "Modify top level settings") - menu.add_row("2", " ) ", "Modify preset collection settings") - menu.add_row("3", " ) ", "Run Preset Availability Demo") - menu.add_row("4", " ) ", "Exit") - console.print(menu) - option = IntPrompt.ask( - prompt="Select an option from the menu above", - choices=[str(i) for i in range(0, 5)], - show_choices=False, - ) - if option == 0: - if activePreset is not None: - console.print(presetCollectionCache.to_table(activePreset.id)) - else: - console.print(presetCollectionCache.to_table()) - elif option == 1: - menuMode = MenuMode.TOP_LEVEL_SETTINGS - focusedTopLevelSetting = None - elif option == 2: - menuMode = MenuMode.COLLECTION - focusedPresetGroup = None - focusedPreset = None - focusedSetting = None - elif option == 3: - menuMode = MenuMode.DEMO - else: - loopRunning = False - - # TOP LEVEL SETTING MENU - elif menuMode == MenuMode.TOP_LEVEL_SETTINGS: - if focusedTopLevelSetting is None: - menu = Table.grid(expand=False) - menu.add_column(justify="right", width=4) - menu.add_column() - menu.add_column() - menu.add_row("0", " ) ", "Back to Main Menu") - option_offset = 1 - for i, settingId in enumerate(topLevelSettings): - if settingId not in gopro.ble_setting: - menu.add_row( - str(i + option_offset), - " ) ", - "(Inaccessible) Setting " + str(settingId), - ) - else: - menu.add_row(str(i + option_offset), " ) ", "Modify setting " + str(settingId)) - - console.print("[bold blue]Top Level Setting Menu") - console.print(menu) - option = IntPrompt.ask( - prompt="Select an option from the menu above", - choices=[str(i) for i in range(len(topLevelSettings) + option_offset)], - show_choices=False, - ) - if option == 0: - menuMode = MenuMode.MAIN - if topLevelSettings[option - option_offset] not in gopro.ble_setting: - console.print("[red]Invalid selection") - else: - focusedTopLevelSetting = topLevelSettings[option - option_offset] - else: - menu = Table.grid(expand=False) - menu.add_column(justify="right", width=4) - menu.add_column() - menu.add_column() - menu.add_row("0", " ) ", "Back to Top Level Settings Menu") - option_offset = 1 - - try: - settingHandler = gopro.ble_setting[focusedTopLevelSetting] - - logger.debug(f"Fetching value for setting {str(focusedTopLevelSetting)}") - response = settingHandler.get_value() - logger.debug(response) - - currentValue = response.data.get(focusedTopLevelSetting) - - logger.debug(f"Fetching capabilities for setting {str(focusedTopLevelSetting)}") - response = settingHandler.get_capabilities_values() - logger.debug(response) - - settingValues = response.data.get(focusedTopLevelSetting) - if settingValues is None: - console.print( - f"No reported capabilities, cannot edit setting {str(focusedTopLevelSetting)}" - ) - focusedTopLevelSetting = None - else: - for i, value in enumerate(settingValues): - if value == currentValue: - menu.add_row( - str(i + option_offset), - " ) ", - "Capability " + str(value) + " [green](CURRENT)", - ) - else: - menu.add_row(str(i + option_offset), " ) ", "Capability " + str(value)) - - console.print(f"[bold blue]Setting Menu: ({str(focusedTopLevelSetting)})") - console.print(menu) - option = IntPrompt.ask( - prompt="Select an option from the menu above", - choices=[str(i) for i in range(len(settingValues) + option_offset)], - show_choices=False, - ) - if option == 0: - focusedTopLevelSetting = None - else: - console.print( - f"Changing setting {str(focusedTopLevelSetting)} value to {str(settingValues[option - option_offset])}" - ) - logger.debug( - f"Changing setting {str(focusedTopLevelSetting)} to {str(settingValues[option - option_offset])}" - ) - response = settingHandler.set(settingValues[option - option_offset]) - logger.debug(response) - collectionCacheInvalidated = True - activeCacheInvalidated = True - except KeyError: - console.print( - f"Unable to edit setting, no setting handler found for {str(focusedTopLevelSetting)}" - ) - focusedTopLevelSetting = None - # COLLECTION MENU - elif menuMode == MenuMode.COLLECTION: - if focusedPresetGroup is None: - menu = Table.grid(expand=False) - menu.add_column(justify="right", width=4) - menu.add_column() - menu.add_column() - menu.add_column() - menu.add_row("0", " ) ", "Back to Main Menu") - option_offset = 1 - if isinstance(presetCollectionCache.presetGroupArray, list): - for i, group in enumerate(presetCollectionCache.presetGroupArray): - if group == activePresetGroup: - menu.add_row( - str(i + option_offset), - " ) ", - "Preset Group " + str(group.id) + " [green](ACTIVE)", - ) - else: - menu.add_row( - str(i + option_offset), - " ) ", - "Preset Group " + str(group.id), - ) - - console.print("[bold blue]Preset Collection Menu") - console.print(menu) - option = IntPrompt.ask( - prompt="Select an option from the menu above", - choices=[ - str(i) - for i in range(len(presetCollectionCache.presetGroupArray) + option_offset) - ], - show_choices=False, - ) - if option == 0: - menuMode = MenuMode.MAIN - else: - focusedPresetGroup = presetCollectionCache.presetGroupArray[option - option_offset] - - elif focusedPreset is None: - menu = Table.grid(expand=False) - menu.add_column(justify="right", width=4) - menu.add_column() - menu.add_column() - menu.add_column() - menu.add_row("0", " ) ", "Back to Preset Collection Menu") - option_offset = 1 - if isinstance(focusedPresetGroup.presetArray, list): - for i, preset in enumerate(focusedPresetGroup.presetArray): - if preset.title_id is None: - if preset == activePreset: - menu.add_row( - str(i + option_offset), - " ) ", - "Preset " + str(preset.id) + " [green](ACTIVE)", - ) - else: - menu.add_row( - str(i + option_offset), - " ) ", - "Preset " + str(preset.id), - ) - else: - if preset == activePreset: - menu.add_row( - str(i + option_offset), - " ) ", - "Preset " + str(preset.title_id) + " [green](ACTIVE)", - ) - else: - menu.add_row( - str(i + option_offset), - " ) ", - "Preset " + str(preset.title_id), - ) - - console.print(f"[bold blue]Preset Group Menu: ({str(focusedPresetGroup.id)})") - - console.print(menu) - option = IntPrompt.ask( - prompt="Select an option from the menu above", - choices=[ - str(i) for i in range(len(focusedPresetGroup.presetArray) + option_offset) - ], - show_choices=False, - ) - if option == 0: - focusedPresetGroup = None - elif ( - focusedPresetGroup.presetArray[option - option_offset].setting_array is None - ) or (focusedPresetGroup.presetArray[option - option_offset].title_id is None): - console.print("[red]Invalid selection") - else: - focusedPreset = focusedPresetGroup.presetArray[option - option_offset] - - elif focusedSetting is None: - menu = Table.grid(expand=False) - menu.add_column(justify="right", width=4) - menu.add_column() - menu.add_column() - menu.add_row("0", " ) ", "Back to Preset Group Menu") - option_offset = 1 - - if focusedPreset != activePreset: - menu.add_row("1", " ) ", "Load this preset") - option_offset += 1 - else: - if isinstance(focusedPreset.setting_array, list): - for i, setting in enumerate(focusedPreset.setting_array): - if setting.id in gopro.ble_setting: - menu.add_row( - str(i + option_offset), - " ) ", - "Setting " + str(setting.id), - ) - else: - menu.add_row( - str(i + option_offset), - " ) ", - "Setting " + str(setting.id) + " [red](NO API SUPPORT)", - ) - - if focusedPreset == activePreset: - console.print( - f"[bold blue]Preset Menu: ({str(focusedPreset.title_id)})[/bold blue] [green](ACTIVE)" - ) - else: - console.print(f"[bold blue]Preset Menu: ({str(focusedPreset.title_id)})") - console.print(menu) - optionCount = option_offset - if focusedPreset.setting_array is not None: - optionCount += len(focusedPreset.setting_array) - option = IntPrompt.ask( - prompt="Select an option from the menu above", - choices=[str(i) for i in range(optionCount)], - show_choices=False, - ) - if option == 0: - focusedPreset = None - elif option == 1 and (focusedPreset != activePreset): - if focusedPreset.title_id is None: - console.print(f"[purple]Loading preset {str(focusedPreset.id)}") - else: - console.print(f"[purple]Loading preset {str(focusedPreset.title_id)}") - gopro.ble_command.load_preset(preset=focusedPreset.id) - activeCacheInvalidated = True - elif isinstance(focusedPreset.setting_array, list): - if focusedPreset.setting_array[option - option_offset].id not in gopro.ble_setting: - console.print("[red]Invalid selection") - else: - focusedSetting = focusedPreset.setting_array[option - option_offset] - - else: - menu = Table.grid(expand=False) - - menu.add_column(justify="right", width=4) - menu.add_column() - menu.add_column() - menu.add_row("0", " ) ", "Back to Preset Menu") - option_offset = 1 - - try: - if not isinstance(focusedSetting.id, SettingId): - raise KeyError - - settingHandler = gopro.ble_setting[focusedSetting.id] - except KeyError: - console.print( - f"Unable to edit setting, no setting handler found for {str(focusedSetting.id)}" - ) - focusedSetting = None - return - - logger.debug("Fetching capabilities for setting " + str(focusedSetting.id)) - response = settingHandler.get_capabilities_values() - logger.debug(response) - - settingValues = response.data.get(focusedSetting.id) - - if not isinstance(settingValues, list): - console.print( - f"Failed to retrieve capabilities, cannot edit setting {str(focusedSetting.id)}" - ) - focusedSetting = None - else: - for i, value in enumerate(settingValues): - if value == focusedSetting.value: - menu.add_row( - str(i + option_offset), - " ) ", - "Capability " + str(value) + " [green](CURRENT)", - ) - else: - menu.add_row(str(i + option_offset), " ) ", "Capability " + str(value)) - - console.print(f"[bold blue]Setting Menu: ({str(focusedSetting.id)})") - console.print(menu) - option = IntPrompt.ask( - prompt="Select an option from the menu above", - choices=[str(i) for i in range(len(settingValues) + option_offset)], - show_choices=False, - ) - if option == 0: - focusedSetting = None - else: - console.print( - f"Changing setting {str(focusedSetting.id)} value to {str(settingValues[option - option_offset])}" - ) - - logger.debug( - f"Changing setting {str(focusedSetting.id)} to {str(settingValues[option - option_offset])}" - ) - response = settingHandler.set(settingValues[option - option_offset]) - logger.debug(response) - focusedSetting.value = settingValues[option - option_offset] - else: - presetDemo(cameraModel) - collectionCacheInvalidated = True - activeCacheInvalidated = True - menuMode = MenuMode.MAIN - - -# Needed for poetry scripts defined in pyproject.toml -def entrypoint() -> None: - main(parse_arguments()) - - -if __name__ == "__main__": - entrypoint() diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/video.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/video.py index 9f65ba03..04c8e99d 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/video.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/video.py @@ -3,60 +3,59 @@ """Entrypoint for taking a video demo.""" -import time import argparse +import asyncio from pathlib import Path -from typing import Optional, Union from rich.console import Console -from open_gopro import WirelessGoPro, WiredGoPro, Params -from open_gopro.util import setup_logging, add_cli_args_and_parse +from open_gopro import Params, WiredGoPro, WirelessGoPro, proto +from open_gopro.logger import setup_logging +from open_gopro.util import add_cli_args_and_parse -console = Console() # rich consoler printer +console = Console() -def main(args: argparse.Namespace) -> None: +async def main(args: argparse.Namespace) -> None: logger = setup_logging(__name__, args.log) - gopro: Optional[Union[WirelessGoPro, WiredGoPro]] = None + gopro: WirelessGoPro | WiredGoPro | None = None try: - with ( + async with ( WiredGoPro(args.identifier) # type: ignore if args.wired else WirelessGoPro(args.identifier, wifi_interface=args.wifi_interface) ) as gopro: assert gopro # Configure settings to prepare for video - if gopro.is_encoding: - gopro.http_command.set_shutter(shutter=Params.Toggle.DISABLE) - gopro.http_setting.video_performance_mode.set(Params.PerformanceMode.MAX_PERFORMANCE) - gopro.http_setting.max_lens_mode.set(Params.MaxLensMode.DEFAULT) - gopro.http_setting.camera_ux_mode.set(Params.CameraUxMode.PRO) - gopro.http_command.set_turbo_mode(mode=Params.Toggle.DISABLE) - assert gopro.http_command.load_preset_group(group=Params.PresetGroup.VIDEO).is_ok + await gopro.http_command.set_shutter(shutter=Params.Toggle.DISABLE) + await gopro.http_setting.video_performance_mode.set(Params.PerformanceMode.MAX_PERFORMANCE) + await gopro.http_setting.max_lens_mode.set(Params.MaxLensMode.DEFAULT) + await gopro.http_setting.camera_ux_mode.set(Params.CameraUxMode.PRO) + await gopro.http_command.set_turbo_mode(mode=Params.Toggle.DISABLE) + assert (await gopro.http_command.load_preset_group(group=proto.EnumPresetGroup.PRESET_GROUP_ID_VIDEO)).ok # Get the media list before - media_set_before = set(x["n"] for x in gopro.http_command.get_media_list().flatten) + media_set_before = set((await gopro.http_command.get_media_list()).data.files) # Take a video console.print("Capturing a video...") - assert gopro.http_command.set_shutter(shutter=Params.Toggle.ENABLE).is_ok - time.sleep(args.record_time) - assert gopro.http_command.set_shutter(shutter=Params.Toggle.DISABLE).is_ok + assert (await gopro.http_command.set_shutter(shutter=Params.Toggle.ENABLE)).ok + await asyncio.sleep(args.record_time) + assert (await gopro.http_command.set_shutter(shutter=Params.Toggle.DISABLE)).ok # Get the media list after - media_set_after = set(x["n"] for x in gopro.http_command.get_media_list().flatten) + media_set_after = set((await gopro.http_command.get_media_list()).data.files) # The video (is most likely) the difference between the two sets video = media_set_after.difference(media_set_before).pop() # Download the video console.print("Downloading the video...") - gopro.http_command.download_file(camera_file=video, local_file=args.output) + await gopro.http_command.download_file(camera_file=video.filename, local_file=args.output) console.print(f"Success!! :smiley: File has been downloaded to {args.output}") except Exception as e: # pylint: disable = broad-except logger.error(repr(e)) if gopro: - gopro.close() + await gopro.close() console.print("Exiting...") @@ -84,7 +83,7 @@ def parse_arguments() -> argparse.Namespace: # Needed for poetry scripts defined in pyproject.toml def entrypoint() -> None: - main(parse_arguments()) + asyncio.run(main(parse_arguments())) if __name__ == "__main__": diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/enum.py b/demos/python/sdk_wireless_camera_control/open_gopro/enum.py new file mode 100644 index 00000000..308f345a --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/enum.py @@ -0,0 +1,128 @@ +# enum.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 + +"""Custom enum definition""" + +from __future__ import annotations + +from enum import Enum, EnumMeta, IntEnum +from typing import Any, Iterator, Protocol, TypeVar, no_type_check + +T = TypeVar("T") + + +class ProtobufDescriptor(Protocol): + """Protocol definition for Protobuf enum descriptor used to generate GoPro enums from protobufs""" + + @property + def name(self) -> str: + """Human readable name of protobuf enum + + # noqa: DAR202 + + Returns: + str: enum name + """ + + @property + def values_by_name(self) -> dict: + """Get the enum values by name + + # noqa: DAR202 + + Returns: + dict: Dict of enum values mapped by name + """ + + @property + def values_by_number(self) -> dict: + """Get the enum values by number + + # noqa: DAR202 + + Returns: + dict: dict of enum numbers mapped by number + """ + + +class GoProEnumMeta(EnumMeta): + """Modify enum metaclass to build GoPro specific enums""" + + _is_proto = False + _iter_skip_names = ("NOT_APPLICABLE", "DESCRIPTOR") + + @no_type_check + def __new__(mcs, name, bases, classdict, **kwargs) -> GoProEnumMeta: # noqa + is_proto = "__is_proto__" in classdict + classdict["_ignore_"] = "__is_proto__" + classdict["__doc__"] = "" # Don't use useless "An enumeration" docstring + e = super().__new__(mcs, name, bases, classdict, **kwargs) + setattr(e, "_is_proto", is_proto) + return e + + @no_type_check + def __contains__(cls: type[Any], obj: object) -> bool: + if isinstance(obj, Enum): + return super().__contains__(obj) + if isinstance(obj, int): + return obj in [x.value for x in cls._member_map_.values()] + if isinstance(obj, str): + return obj.lower() in [x.name.lower() for x in cls._member_map_.values()] + raise TypeError( + f"unsupported operand type(s) for 'in': {type(obj).__qualname__} and {cls.__class__.__qualname__}" + ) + + def __iter__(cls: type[T]) -> Iterator[T]: + """Do not return enum values whose name is in the _iter_skip_names list + + Returns: + Iterator[T]: enum iterator + """ + return iter([x[1] for x in cls._member_map_.items() if x[0] not in GoProEnumMeta._iter_skip_names]) # type: ignore + + +class GoProEnum(IntEnum, metaclass=GoProEnumMeta): + """GoPro specific enum to be used for all settings, statuses, and parameters + + The names NOT_APPLICABLE and DESCRIPTOR are special as they will not be returned as part of the enum iterator + """ + + def __eq__(self, other: object) -> bool: + if type(self)._is_proto: + if isinstance(other, int): + return self.value == other + if isinstance(other, str): + return self.name == other + if isinstance(other, Enum): + return self.value == other.value + raise TypeError(f"Unexpected case: proto enum can only be str or int, not {type(other)}") + return super(IntEnum, self).__eq__(other) + + def __hash__(self) -> Any: + return hash(self.name + str(self.value)) + + def __str__(self) -> str: + return super(IntEnum, self).__str__() + + +def enum_factory(proto_enum: ProtobufDescriptor) -> type[GoProEnum]: + """Dynamically build a GoProEnum from a protobuf enum + + Args: + proto_enum (ProtobufDescriptor): input protobuf enum descriptor + + Returns: + GoProEnum: generated GoProEnum + """ + keys = proto_enum.values_by_name.keys() + values = list(proto_enum.values_by_number.keys()) + # This has somehow changed between protobuf versions + if isinstance(proto_enum.values_by_number, dict): + values.reverse() + return GoProEnum( # type: ignore # pylint: disable=too-many-function-args + proto_enum.name, # type: ignore + { + **dict(zip(keys, values)), + "__is_proto__": True, + }, + ) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/exceptions.py b/demos/python/sdk_wireless_camera_control/open_gopro/exceptions.py index a35dc0bc..e8c0580a 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/exceptions.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/exceptions.py @@ -55,9 +55,7 @@ class ConnectFailed(GoProError): """ def __init__(self, connection: str, timeout: float, retries: int): - super().__init__( - f"{connection} connection failed to establish after {retries} retries with timeout {timeout}" - ) + super().__init__(f"{connection} connection failed to establish after {retries} retries with timeout {timeout}") class ConnectionTerminated(GoProError): diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/gopro_base.py b/demos/python/sdk_wireless_camera_control/open_gopro/gopro_base.py index fd89bdef..86d3ee6e 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/gopro_base.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/gopro_base.py @@ -4,21 +4,22 @@ """Implements top level interface to GoPro module.""" from __future__ import annotations -import time -import json + +import asyncio import enum +import json import logging -import traceback import threading -from pathlib import Path +import traceback from abc import ABC, abstractmethod -from typing import Any, Final, Callable, TypeVar, Generic, Optional +from pathlib import Path +from typing import Any, Awaitable, Callable, Final, Generic, Optional, TypeVar -import wrapt import requests +import wrapt -from open_gopro.interface import JsonParser import open_gopro.exceptions as GpException +from open_gopro import types from open_gopro.api import ( BleCommands, BleSettings, @@ -28,17 +29,15 @@ WiredApi, WirelessApi, ) -from open_gopro.responses import GoProResp +from open_gopro.constants import ErrorCode +from open_gopro.models.response import GoProResp, RequestsHttpRespBuilderDirector +from open_gopro.parser_interface import Parser logger = logging.getLogger(__name__) -WRITE_TIMEOUT: Final = 5 -GET_TIMEOUT: Final = 5 -HTTP_GET_RETRIES: Final = 5 - GoPro = TypeVar("GoPro", bound="GoProBase") ApiType = TypeVar("ApiType", WiredApi, WirelessApi) -MessageMethodType = Callable[[Any, bool], GoProResp] +MessageMethodType = Callable[[Any, bool], Awaitable[GoProResp]] class GoProMessageInterface(enum.Enum): @@ -49,9 +48,7 @@ class GoProMessageInterface(enum.Enum): @wrapt.decorator -def catch_thread_exception( - wrapped: Callable, instance: GoProBase, args: Any, kwargs: Any -) -> Optional[Callable]: +def catch_thread_exception(wrapped: Callable, instance: GoProBase, args: Any, kwargs: Any) -> Optional[Callable]: """Catch any exceptions from this method and pass them to the exception handler identifier by thread name Args: @@ -65,7 +62,7 @@ def catch_thread_exception( """ try: return wrapped(*args, **kwargs) - except Exception as e: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-exception-caught instance._handle_exception(threading.current_thread().name, {"exception": e}) return None @@ -94,23 +91,22 @@ def wrapper(wrapped: Callable, instance: GoProBase, args: Any, kwargs: Any) -> C class GoProBase(ABC, Generic[ApiType]): """The base class for communicating with all GoPro Clients""" + GET_TIMEOUT: Final = 5 + HTTP_GET_RETRIES: Final = 5 + def __init__(self, **kwargs: Any) -> None: self._should_maintain_state = kwargs.get("maintain_state", True) self._exception_cb = kwargs.get("exception_cb", None) - self._internal_state = GoProBase._InternalState.ENCODING | GoProBase._InternalState.SYSTEM_BUSY - def __enter__(self: GoPro) -> GoPro: - self.open() + async def __aenter__(self: GoPro) -> GoPro: + await self.open() return self - def __exit__(self, *_: Any) -> None: - self.close() - - def __del__(self) -> None: - self.close() + async def __aexit__(self, *_: Any) -> None: + await self.close() @abstractmethod - def open(self, timeout: int = 10, retries: int = 5) -> None: + async def open(self, timeout: int = 10, retries: int = 5) -> None: """Connect to the GoPro Client and prepare it for communication Args: @@ -120,37 +116,19 @@ def open(self, timeout: int = 10, retries: int = 5) -> None: raise NotImplementedError @abstractmethod - def close(self) -> None: + async def close(self) -> None: """Gracefully close the GoPro Client connection""" raise NotImplementedError @property - def is_encoding(self) -> bool: - """Is the camera currently encoding? - - Raises: - InvalidConfiguration: if maintain_state is False, there is no way to know the GoPro's state - - Returns: - bool: True if yes, False if no - """ - if not self._should_maintain_state: - raise GpException.InvalidConfiguration("Not maintaining BLE state so encoding is not applicable") - return bool(self._internal_state & GoProBase._InternalState.ENCODING) - - @property - def is_busy(self) -> bool: - """Is the camera currently performing a task that prevents it from accepting commands? - - Raises: - InvalidConfiguration: if maintain_state is False, there is no way to know the GoPro's state + @abstractmethod + async def is_ready(self) -> bool: + """Is gopro ready to receive commands Returns: - bool: True if yes, False if no + bool: yes if ready, no otherwise """ - if not self._should_maintain_state: - raise GpException.InvalidConfiguration("Not maintaining BLE state so busy is not applicable") - return bool(self._internal_state & GoProBase._InternalState.SYSTEM_BUSY) + raise NotImplementedError @property @abstractmethod @@ -257,7 +235,7 @@ def is_http_connected(self) -> bool: # End Public API ########################################################################################################## - def _handle_exception(self, source: Any, context: dict[str, Any]) -> None: + def _handle_exception(self, source: Any, context: types.JsonDict) -> None: """Gather exceptions from module threads and send through callback if registered. Note that this function signature matches asyncio's exception callback requirement. @@ -327,8 +305,17 @@ def _catch_thread_exception(*args: Any, **kwargs: Any) -> Optional[Callable]: """ return catch_thread_exception(*args, **kwargs) + # TODO use requests in async manner @ensure_opened((GoProMessageInterface.HTTP,)) - def _get(self, url: str, parser: Optional[JsonParser] = None) -> GoProResp: + async def _http_get( # pylint: disable=unused-argument + self, + url: str, + parser: Parser | None, + headers: dict | None = None, + certificate: Path | None = None, + timeout: int = GET_TIMEOUT, + **kwargs: Any, + ) -> GoProResp: """Send an HTTP GET request to an Open GoPro endpoint. There should hopefully not be a scenario where this needs to be called directly as it is generally @@ -336,11 +323,14 @@ def _get(self, url: str, parser: Optional[JsonParser] = None) -> GoProResp: Args: url (str): endpoint URL - parser (Optional[JsonParser]): Optional parser to further parse received JSON dict. Defaults to - None. + parser (Parser, optional): Optional parser to further parse received JSON dict. + headers (dict | None, optional): dict of additional HTTP headers. Defaults to None. + certificate (Path | None, optional): path to certificate CA bundle. Defaults to None. + timeout (int): timeout in seconds before retrying. Defaults to GET_TIMEOUT + kwargs (Any): additional arguments to be consumed by decorator / subclass Raises: - ResponseTimeout: Response was not received in GET_TIMEOUT seconds + ResponseTimeout: Response was not received in timeout seconds Returns: GoProResp: response @@ -348,38 +338,38 @@ def _get(self, url: str, parser: Optional[JsonParser] = None) -> GoProResp: url = self._base_url + url logger.debug(f"Sending: {url}") + # Dynamically build get kwargs + request_args: dict[str, Any] = {} + if headers: + request_args["headers"] = headers + if certificate: + request_args["verify"] = str(certificate) + response: Optional[GoProResp] = None - for _ in range(HTTP_GET_RETRIES): + for retry in range(GoProBase.HTTP_GET_RETRIES): try: - request = requests.get(url, timeout=GET_TIMEOUT) - request.raise_for_status() + request = requests.get(url, timeout=timeout, **request_args) logger.trace(f"received raw json: {json.dumps(request.json() if request.text else {}, indent=4)}") # type: ignore - response = GoProResp._from_http_response(parser, request) - break - except requests.exceptions.HTTPError as e: - # The camera responded with an error. Break since we successfully sent the command and attempt - # to continue - logger.warning(e) - response = GoProResp._from_http_response(parser, e.response) + if not request.ok: + logger.warning(f"Received non-success status {request.status_code}: {request.reason}") + response = RequestsHttpRespBuilderDirector(request, parser)() break except requests.exceptions.ConnectionError as e: # This appears to only occur after initial connection after pairing logger.warning(repr(e)) - # Back off before retrying - time.sleep(2) - except Exception as e: # pylint: disable=broad-except + # Back off before retrying. TODO This appears to be needed on MacOS + await asyncio.sleep(2) + except Exception as e: # pylint: disable=broad-exception-caught logger.critical(f"Unexpected error: {repr(e)}") - else: - pass - logger.warning("Retrying to send the command...") + logger.warning(f"Retrying #{retry} to send the command...") else: - raise GpException.ResponseTimeout(HTTP_GET_RETRIES) + raise GpException.ResponseTimeout(GoProBase.HTTP_GET_RETRIES) assert response is not None return response @ensure_opened((GoProMessageInterface.HTTP,)) - def _stream_to_file(self, url: str, file: Path) -> GoProResp: + async def _stream_to_file(self, url: str, file: Path) -> GoProResp[Path]: """Send an HTTP GET request to an Open GoPro endpoint to download a binary file. There should hopefully not be a scenario where this needs to be called directly as it is generally @@ -396,11 +386,16 @@ def _stream_to_file(self, url: str, file: Path) -> GoProResp: url = self._base_url + url logger.debug(f"Sending: {url}") - with requests.get(url, stream=True, timeout=GET_TIMEOUT) as request: + with requests.get(url, stream=True, timeout=GoProBase.GET_TIMEOUT) as request: request.raise_for_status() with open(file, "wb") as f: logger.debug(f"receiving stream to {file}...") for chunk in request.iter_content(chunk_size=8192): f.write(chunk) - return GoProResp._from_stream_response(request) + return GoProResp( + protocol=GoProResp.Protocol.HTTP, + status=ErrorCode.SUCCESS, + data=file, + identifier=url, + ) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/gopro_wired.py b/demos/python/sdk_wireless_camera_control/open_gopro/gopro_wired.py index d47f9d52..fb844bbf 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/gopro_wired.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/gopro_wired.py @@ -4,22 +4,30 @@ """Implements top level interface to GoPro module.""" from __future__ import annotations -import re -import time -import queue + +import asyncio import logging from pathlib import Path -from typing import Final, Optional, Any, Union +from typing import Any, Final import wrapt -from zeroconf import IPVersion, ServiceBrowser, ServiceListener, Zeroconf import open_gopro.exceptions as GpException +import open_gopro.wifi.mdns_scanner # Imported this way for pytest monkeypatching +from open_gopro import types +from open_gopro.api import ( + BleCommands, + BleSettings, + BleStatuses, + HttpCommands, + HttpSettings, + Params, + WiredApi, +) +from open_gopro.communicator_interface import GoProWiredInterface, MessageRules +from open_gopro.constants import StatusId from open_gopro.gopro_base import GoProBase, MessageMethodType -from open_gopro.constants import StatusId, SettingId -from open_gopro.responses import GoProResp -from open_gopro.api import WiredApi, BleCommands, BleSettings, BleStatuses, HttpCommands, HttpSettings, Params -from open_gopro.interface import GoProWiredInterface, JsonParser, MessageRules +from open_gopro.models import GoProResp logger = logging.getLogger(__name__) @@ -28,9 +36,7 @@ @wrapt.decorator -def enforce_message_rules( - wrapped: MessageMethodType, instance: WiredGoPro, args: Any, kwargs: Any -) -> GoProResp: +async def enforce_message_rules(wrapped: MessageMethodType, instance: WiredGoPro, args: Any, kwargs: Any) -> GoProResp: """Wrap the input message method, applying any message rules (MessageRules) Args: @@ -48,20 +54,20 @@ def enforce_message_rules( if instance._should_maintain_state and instance.is_open and not MessageRules.FASTPASS in rules: # Wait for not encoding and not busy logger.trace("Waiting for camera to be ready to receive messages.") # type: ignore - instance._wait_for_state({StatusId.ENCODING: False, StatusId.SYSTEM_BUSY: False}) + await instance._wait_for_state({StatusId.ENCODING: False, StatusId.SYSTEM_BUSY: False}) logger.trace("Camera is ready to receive messages") # type: ignore - response = wrapped(*args, **kwargs) + response = await wrapped(*args, **kwargs) else: # Either we're not maintaining state, we're not opened yet, or this is a fastpass message - response = wrapped(*args, **kwargs) + response = await wrapped(*args, **kwargs) # Release the lock if we acquired it if instance._should_maintain_state: - if response.is_ok: + if response.ok: # Is there any special handling required after receiving the response? if MessageRules.WAIT_FOR_ENCODING_START in rules: logger.trace("Waiting to receive encoding started.") # type: ignore # Wait for encoding to start - instance._wait_for_state({StatusId.ENCODING: True}) + await instance._wait_for_state({StatusId.ENCODING: True}) return response @@ -78,21 +84,20 @@ class WiredGoPro(GoProBase[WiredApi], GoProWiredInterface): It can be used via context manager: - >>> from open_gopro import WiredGoPro - >>> with WiredGoPro() as gopro: - >>> gopro.http_command.set_shutter(Params.Toggle.ENABLE) + >>> async with WiredGoPro() as gopro: + >>> print("Yay! I'm connected via USB, opened, and ready to send / get data now!") + >>> # Send some messages now Or without: - >>> from open_gopro import WiredGoPro >>> gopro = WiredGoPro() - >>> gopro.open() - >>> gopro.http_command.set_shutter(Params.Toggle.ENABLE) - >>> gopro.close() + >>> await gopro.open() + >>> print("Yay! I'm connected via USB, opened, and ready to send / get data now!") + >>> # Send some messages now Args: serial (Optional[str]): (at least) last 3 digits of GoPro Serial number. If not set, first GoPro - discovered from mDNS will be used. + discovered from mDNS will be used. Defaults to None kwargs (Any): additional keyword arguments to pass to base class """ @@ -100,15 +105,18 @@ class WiredGoPro(GoProBase[WiredApi], GoProWiredInterface): _BASE_ENDPOINT: Final[str] = "http://{ip}:8080/" _MDNS_SERVICE_NAME: Final[str] = "_gopro-web._tcp.local." - def __init__(self, serial: Optional[str], **kwargs: Any) -> None: + def __init__(self, serial: str | None = None, **kwargs: Any) -> None: GoProBase.__init__(self, **kwargs) GoProWiredInterface.__init__(self) self._serial = serial # We currently only support version 2.0 self._wired_api = WiredApi(self) self._open = False + self._poll_period = kwargs.get("poll_period", 2) + self._encoding = False + self._busy = False - def open(self, timeout: int = 10, retries: int = 1) -> None: + async def open(self, timeout: int = 10, retries: int = 1) -> None: """Connect to the Wired GoPro Client and prepare it for communication Args: @@ -124,28 +132,43 @@ def open(self, timeout: int = 10, retries: int = 1) -> None: if not self._serial: for retry in range(retries + 1): try: - self._serial = WiredGoPro._find_serial_via_mdns(timeout) - if self._serial: - break + ip_addr = await open_gopro.wifi.mdns_scanner.find_first_ip_addr( + WiredGoPro._MDNS_SERVICE_NAME, timeout + ) + self._serial = "GoPro X" + "".join([ip_addr[5], *ip_addr[8:10]]) + break except GpException.FailedToFindDevice as e: if retry == retries: raise e logger.warning(f"Failed to discover GoPro. Retrying #{retry + 1}") - self.http_command.wired_usb_control(control=Params.Toggle.ENABLE) + await self.http_command.wired_usb_control(control=Params.Toggle.ENABLE) # Find and configure API version - if (version := self.http_command.get_open_gopro_api_version().flatten) != self.version: + version = await self.http_command.get_open_gopro_api_version() + if (version := (await self.http_command.get_open_gopro_api_version()).data) != self.version: raise GpException.InvalidOpenGoProVersion(version) logger.info(f"Using Open GoPro API version {version}") # Wait for initial ready state - self._wait_for_state({StatusId.ENCODING: False, StatusId.SYSTEM_BUSY: False}) + await self._wait_for_state({StatusId.ENCODING: False, StatusId.SYSTEM_BUSY: False}) self._open = True - def close(self) -> None: + async def close(self) -> None: """Gracefully close the GoPro Client connection""" + @property + async def is_ready(self) -> bool: + """Is gopro ready to receive commands + + Returns: + bool: yes if ready, no otherwise + """ + current_state = (await self.http_command.get_camera_state()).data + self._encoding = bool(current_state[StatusId.ENCODING]) + self._busy = bool(current_state[StatusId.SYSTEM_BUSY]) + return not (self._encoding or self._busy) + @property def identifier(self) -> str: """Unique identifier for the connected GoPro Client @@ -243,93 +266,48 @@ def is_http_connected(self) -> bool: """ return True # TODO find a better way to do this - ########################################################################################################## - # End Public API - ########################################################################################################## - - @classmethod - def _find_serial_via_mdns(cls, timeout: int) -> str: - """Query the mDNS server to find a GoPro + def register_update(self, callback: types.UpdateCb, update: types.UpdateType) -> None: + """Register for callbacks when an update occurs Args: - timeout (int): how long to search for before timing out + callback (types.UpdateCb): callback to be notified in + update (types.UpdateType): update to register for Raises: - FailedToFindDevice: search timed out - RuntimeError: unexpected runtime error + NotImplementedError: not yet possible + """ + raise NotImplementedError - Returns: - str: First discovered IP address matching base GoPro socket address for USB connections + def unregister_update(self, callback: types.UpdateCb, update: types.UpdateType | None = None) -> None: + """Unregister for asynchronous update(s) + + Args: + callback (types.UpdateCb): callback to stop receiving update(s) on + update (types.UpdateType | None): updates to unsubscribe for. Defaults to None (all + updates that use this callback will be unsubscribed). + + Raises: + NotImplementedError: not yet possible """ + raise NotImplementedError - class ZeroconfListener(ServiceListener): - """Listens for mDNS services on the local system and save fully-formed ipaddr URLs""" - - def __init__(self) -> None: - self.urls: queue.Queue[str] = queue.Queue() - - def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: - """Callback called by ServiceBrowser when a new service is discovered - - Args: - zc (Zeroconf): instantiated zeroconf object that owns the search - type_ (str): name of mDNS service that search is occurring on - name (str): discovered device - """ - if not (info := zc.get_service_info(type_, name)): - return # Could not resolve info - - for ipv4_address in info.parsed_addresses(IPVersion.V4Only): - if re.match(r"172.2\d.1\d\d.51", ipv4_address): - self.urls.put_nowait(ipv4_address) - - def update_service(self, *_: Any) -> None: - """Not used - - Args: - *_ (Any): not used - """ - - def remove_service(self, *_: Any) -> None: - """Not used - - Args: - *_ (Any): not used - """ - - logger.info("Querying mDNS to find a GoPro...") - zeroconf = Zeroconf(unicast=True) - listener = ZeroconfListener() - browser = ServiceBrowser(zeroconf, WiredGoPro._MDNS_SERVICE_NAME, listener=listener) - # Wait for URL discovery - gopro_ip: Optional[str] = None - exc: Optional[queue.Empty] = None - try: - gopro_ip = listener.urls.get(timeout=timeout) - except queue.Empty as e: - exc = e - browser.cancel() - zeroconf.close() - if gopro_ip: - logger.info(f"Found GoPro @ {gopro_ip}") - return "".join([gopro_ip[5], *gopro_ip[8:10]]) - if exc: - raise GpException.FailedToFindDevice() from exc - raise RuntimeError("Should never get here") - - def _wait_for_state(self, check: dict[Union[StatusId, SettingId], Any], poll_period: int = 1) -> None: + ########################################################################################################## + # End Public API + ########################################################################################################## + + async def _wait_for_state(self, check: types.CameraState) -> None: """Poll the current state until a variable amount of states are all equal to desired values Args: check (dict[Union[StatusId, SettingId], Any]): dict{setting / status: value} of settings / statuses and values to wait for - poll_period (int): How frequently (in seconds) to poll the current state. Defaults to 1. """ - while state := self.http_command.get_camera_state(): + while True: + state = (await self.http_command.get_camera_state()).data for key, value in check.items(): - if state[key] != value: - time.sleep(poll_period) + if state.get(key) != value: + await asyncio.sleep(self._poll_period) break # Get new state and try again else: return # Everything matches. Exit @@ -353,9 +331,5 @@ def _base_url(self) -> str: return WiredGoPro._BASE_ENDPOINT.format(ip=WiredGoPro._BASE_IP.format(*self._serial[-3:])) @enforce_message_rules - def _get(self, url: str, parser: Optional[JsonParser] = None, **kwargs: Any) -> GoProResp: - return super()._get(url, parser, **kwargs) - - @enforce_message_rules - def _stream_to_file(self, url: str, file: Path) -> GoProResp: - return super()._stream_to_file(url, file) + async def _stream_to_file(self, url: str, file: Path) -> GoProResp: + return await super()._stream_to_file(url, file) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/gopro_wireless.py b/demos/python/sdk_wireless_camera_control/open_gopro/gopro_wireless.py index ff76183c..2ec35932 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/gopro_wireless.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/gopro_wireless.py @@ -4,45 +4,44 @@ """Implements top level interface to GoPro module.""" from __future__ import annotations -import time -import queue + +import asyncio import logging -import threading +import queue +from collections import defaultdict from pathlib import Path -from queue import Queue -from typing import Any, Final, Optional, Pattern +from typing import Any, Final, Pattern import wrapt import open_gopro.exceptions as GpException -from open_gopro.gopro_base import GoProBase, MessageMethodType, GoProMessageInterface -from open_gopro.ble import BleUUID -from open_gopro.ble.adapters import BleakWrapperController -from open_gopro.wifi.adapters import Wireless -from open_gopro.util import SnapshotQueue, Logger -from open_gopro.responses import GoProResp, ResponseType, JsonParser -from open_gopro.constants import GoProUUIDs, StatusId, QueryCmdId, ProducerType +from open_gopro import proto, types from open_gopro.api import ( - WirelessApi, BleCommands, BleSettings, BleStatuses, HttpCommands, HttpSettings, Params, + WirelessApi, ) -from open_gopro.interface import GoProWirelessInterface, MessageRules +from open_gopro.ble import BleakWrapperController, BleUUID +from open_gopro.communicator_interface import GoProWirelessInterface, MessageRules +from open_gopro.constants import ActionId, GoProUUIDs, QueryCmdId, SettingId, StatusId +from open_gopro.gopro_base import GoProBase, GoProMessageInterface, MessageMethodType +from open_gopro.logger import Logger +from open_gopro.models.response import BleRespBuilder, GoProResp +from open_gopro.parser_interface import Parser +from open_gopro.util import SnapshotQueue, get_current_dst_aware_time +from open_gopro.wifi import WifiCli logger = logging.getLogger(__name__) KEEP_ALIVE_INTERVAL: Final = 28 -WRITE_TIMEOUT: Final = 5 -GET_TIMEOUT: Final = 5 -HTTP_GET_RETRIES: Final = 5 @wrapt.decorator -def enforce_message_rules( +async def enforce_message_rules( wrapped: MessageMethodType, instance: WirelessGoPro, args: Any, kwargs: Any ) -> GoProResp: """Wrap the input message method, applying any message rules (MessageRules) @@ -62,23 +61,23 @@ def enforce_message_rules( have_lock = False if instance._should_maintain_state and instance.is_open and not MessageRules.FASTPASS in rules: logger.trace(f"{wrapped.__name__} acquiring lock") # type: ignore - instance._ready.acquire() + await instance._ready_lock.acquire() logger.trace(f"{wrapped.__name__} has the lock") # type: ignore have_lock = True - response = wrapped(*args, **kwargs) + response = await wrapped(*args, **kwargs) else: # Either we're not maintaining state, we're not opened yet, or this is a fastpass message - response = wrapped(*args, **kwargs) + response = await wrapped(*args, **kwargs) # Release the lock if we acquired it if instance._should_maintain_state: if have_lock: - instance._ready.release() + instance._ready_lock.release() logger.trace(f"{wrapped.__name__} released the lock") # type: ignore - if response.is_ok: - # Is there any special handling required after receiving the response? - if MessageRules.WAIT_FOR_ENCODING_START in rules: - logger.trace("Waiting to receive encoding started.") # type: ignore - instance._encoding_started.wait() + # Is there any special handling required after receiving the response? + if MessageRules.WAIT_FOR_ENCODING_START in rules: + logger.trace("Waiting to receive encoding started.") # type: ignore + await instance._encoding_started.wait() + instance._encoding_started.clear() return response @@ -94,6 +93,7 @@ class WirelessGoPro(GoProBase[WirelessApi], GoProWirelessInterface): - discovering GATT characteristics - enabling notifications - discovering Open GoPro version + - setting the date, time, timezone, and DST - transferring data This will handle, for Wifi: @@ -111,17 +111,16 @@ class WirelessGoPro(GoProBase[WirelessApi], GoProWirelessInterface): It can be used via context manager: - >>> from open_gopro import WirelessGoPro - >>> with WirelessGoPro() as gopro: - >>> gopro.ble_command.set_shutter(Params.Toggle.ENABLE) + >>> async with WirelessGoPro() as gopro: + >>> print("Yay! I'm connected via BLE, Wifi, opened, and ready to send / get data now!") + >>> # Send some messages now Or without: - >>> from open_gopro import WirelessGoPro >>> gopro = WirelessGoPro() - >>> gopro.open() - >>> gopro.ble_command.set_shutter(Params.Toggle.ENABLE) - >>> gopro.close() + >>> await gopro.open() + >>> print("Yay! I'm connected via BLE, Wifi, opened, and ready to send / get data now!") + >>> # Send some messages now Args: target (Pattern, Optional): A regex to search for the target GoPro's name. For example, "GoPro 0456"). @@ -142,11 +141,13 @@ class WirelessGoPro(GoProBase[WirelessApi], GoProWirelessInterface): the interface can also be specified manually with the 'wifi_interface' argument. """ + WRITE_TIMEOUT: Final = 5 + def __init__( self, - target: Optional[Pattern] = None, - wifi_interface: Optional[str] = None, - sudo_password: Optional[str] = None, + target: Pattern | None = None, + wifi_interface: str | None = None, + sudo_password: str | None = None, enable_wifi: bool = True, **kwargs: Any, ) -> None: @@ -154,7 +155,7 @@ def __init__( # Store initialization information self._should_enable_wifi = enable_wifi ble_adapter = kwargs.get("ble_adapter", BleakWrapperController) - wifi_adapter = kwargs.get("wifi_adapter", Wireless) + wifi_adapter = kwargs.get("wifi_adapter", WifiCli) # Set up API delegate self._wireless_api = WirelessApi(self) @@ -174,35 +175,27 @@ def __init__( ) raise e - # Current accumulating synchronous responses, indexed by GoProUUIDs. This assumes there can only be one active response per BleUUID - self._active_resp: dict[BleUUID, GoProResp] = {} + # Builders for currently accumulating synchronous responses, indexed by GoProUUIDs. This assumes there + # can only be one active response per BleUUID + self._active_builders: dict[BleUUID, BleRespBuilder] = {} # Responses that we are waiting for. - self._sync_resp_wait_q: SnapshotQueue = SnapshotQueue() + self._sync_resp_wait_q: SnapshotQueue[types.ResponseType] = SnapshotQueue() # Synchronous response that has been parsed and are ready for their sender to receive as the response. - self._sync_resp_ready_q: SnapshotQueue = SnapshotQueue() + self._sync_resp_ready_q: SnapshotQueue[types.ResponseType] = SnapshotQueue() - # For outputting asynchronously received information - self._out_q: Queue[GoProResp] = Queue() - self._listeners: dict[ProducerType, bool] = {} + self._listeners: dict[types.UpdateType, set[types.UpdateCb]] = defaultdict(set) - # Set up BLE threading - self._ble_disconnect_event = threading.Event() - self._ble_disconnect_event.set() - self._keep_alive_thread = threading.Thread( - target=self._periodic_keep_alive, daemon=True, name="keep alive" - ) + # TO be set up when opening in async context + self._loop: asyncio.AbstractEventLoop self._open = False + self._ble_disconnect_event: asyncio.Event - # If we are to perform BLE housekeeping if self._should_maintain_state: - self._ready: threading.Lock = threading.Lock() - # Busy / encoding management - self._state_condition: threading.Condition = threading.Condition() - self._encoding_started: threading.Event = threading.Event() - self._encoding_started.clear() - self._internal_state = GoProBase._InternalState.ENCODING | GoProBase._InternalState.SYSTEM_BUSY - self._state_thread = threading.Thread(target=self._maintain_state, name="state", daemon=True) - self._state_thread.start() + self._ready_lock: asyncio.Lock + self._keep_alive_task: asyncio.Task + self._encoding: bool + self._busy: bool + self._encoding_started: asyncio.Event @property def identifier(self) -> str: @@ -239,7 +232,10 @@ def is_http_connected(self) -> bool: Returns: bool: True if yes, False if no """ - return self._wifi.is_connected + try: + return self._wifi.is_connected + except AttributeError: + return False @property def ble_command(self) -> BleCommands: @@ -286,7 +282,7 @@ def http_setting(self) -> HttpSettings: """ return self._api.http_setting - def open(self, timeout: int = 10, retries: int = 5) -> None: + async def open(self, timeout: int = 10, retries: int = 5) -> None: """Perform all initialization commands for ble and wifi For BLE: scan and find device, establish connection, discover characteristics, configure queries @@ -302,32 +298,50 @@ def open(self, timeout: int = 10, retries: int = 5) -> None: timeout (int): How long to wait for each connection before timing out. Defaults to 10. retries (int): How many connection attempts before considering connection failed. Defaults to 5. """ + # Set up concurrency + self._loop = asyncio.get_running_loop() + self._open = False + self._ble_disconnect_event = asyncio.Event() + + # If we are to perform BLE housekeeping + if self._should_maintain_state: + self._ready_lock = asyncio.Lock() + self._keep_alive_task = asyncio.create_task(self._periodic_keep_alive()) + self._encoding = True + self._busy = True + self._encoding_started = asyncio.Event() + try: - # Establish BLE connection and start maintenance threads if desired - self._open_ble(timeout, retries) + await self._open_ble(timeout, retries) + + # Set current dst-aware time + assert ( + await self.ble_command.set_date_time_tz_dst( + **dict(zip(("date_time", "tz_offset", "is_dst"), get_current_dst_aware_time())) + ) + ).ok # Find and configure API version - version = self.ble_command.get_open_gopro_api_version().flatten - version_str = f"{version.major}.{version.minor}" - if version_str != self.version: + version = (await self.ble_command.get_open_gopro_api_version()).data + if version != self.version: raise GpException.InvalidOpenGoProVersion(version) - logger.info(f"Using Open GoPro API version {version_str}") + logger.info(f"Using Open GoPro API version {version}") # Establish Wifi connection if desired if self._should_enable_wifi: - self._open_wifi(timeout, retries) + await self._open_wifi(timeout, retries) else: # Otherwise, turn off Wifi logger.info("Turning off the camera's Wifi radio") - self.ble_command.enable_wifi_ap(enable=False) + await self.ble_command.enable_wifi_ap(enable=False) self._open = True except Exception as e: logger.error(f"Error while opening: {e}") - self.close() + await self.close() raise e - def close(self) -> None: + async def close(self) -> None: """Safely stop the GoPro instance. This will disconnect BLE and WiFI if applicable. @@ -335,38 +349,36 @@ def close(self) -> None: If not using the context manager, it is mandatory to call this before exiting the program in order to prevent reconnection issues because the OS has never disconnected from the previous session. """ - self._close_wifi() - self._close_ble() + await self._close_wifi() + await self._close_ble() self._open = False - @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) - def get_notification(self, timeout: Optional[float] = None) -> Optional[GoProResp]: - """Get an asynchronous notification that we received from a registered listener. - - If timeout is None, this will block until a notification is received. - The updates are received via FIFO. + def register_update(self, callback: types.UpdateCb, update: types.UpdateType) -> None: + """Register for callbacks when an update occurs Args: - timeout (float, Optional): Time to wait for a notification before returning. Defaults to None (wait forever) - - Returns: - GoProResp: Received notification if there is one in the queue or None otherwise + callback (types.UpdateCb): callback to be notified in + update (types.UpdateType): update to register for """ - try: - return self._out_q.get(timeout=timeout) - except queue.Empty: - return None + self._listeners[update].add(callback) - @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) - def keep_alive(self) -> bool: - """Send a heartbeat to prevent the BLE connection from dropping. + def unregister_update(self, callback: types.UpdateCb, update: types.UpdateType | None = None) -> None: + """Unregister for asynchronous update(s) - This is sent automatically by the GoPro instance if its `maintain_ble` argument is not False. - - Returns: - bool: True if it succeeded,. False otherwise + Args: + callback (types.UpdateCb): callback to stop receiving update(s) on + update (types.UpdateType | None): updates to unsubscribe for. Defaults to None (all + updates that use this callback will be unsubscribed). """ - return self.ble_setting.led.set(Params.LED.BLE_KEEP_ALIVE).is_ok + if update: + self._listeners.get(update, set()).remove(callback) + else: + # If update was not specified, remove all uses of callback + for key in dict(self._listeners).keys(): + try: + self._listeners[key].remove(callback) + except KeyError: + continue @property def is_open(self) -> bool: @@ -377,94 +389,106 @@ def is_open(self) -> bool: """ return self._open - ########################################################################################################## - # End Public API - ########################################################################################################## - - @GoProBase._catch_thread_exception - def _maintain_state(self) -> None: - """Thread to keep track of ready / encoding and acquire / release ready lock.""" - logger.trace("Initial acquiring of lock") # type: ignore - self._ready.acquire() - have_lock = True - while True: - with self._state_condition: - self._state_condition.wait() - if have_lock and not (self.is_busy or self.is_encoding): - self._ready.release() - have_lock = False - logger.trace("Control released lock") # type: ignore - elif not have_lock and (self.is_busy or self.is_encoding): - logger.trace("Control acquiring lock") # type: ignore - self._ready.acquire() - logger.trace("Control has lock") # type: ignore - have_lock = True - if self.is_encoding: - logger.trace("Control setting encoded started") # type: ignore - self._encoding_started.set() - - # TODO how to stop this? - logger.debug("Maintain state thread exiting...") - - def _set_state_encoding(self, encoding: bool) -> None: - """Set whether or not the GoPro is currently encoding + @property + async def is_ready(self) -> bool: + """Is gopro ready to receive commands - Args: - encoding (bool): True if encoding + Returns: + bool: yes if ready, no otherwise """ - with self._state_condition: - if encoding is True: - self._internal_state |= GoProBase._InternalState.ENCODING - else: - self._internal_state &= ~GoProBase._InternalState.ENCODING - self._state_condition.notify() + return not (self._busy or self._encoding) - def _set_state_busy(self, busy: bool) -> None: - """Set whether or not the GoPro is currently busy + ########################################################################################################## + #### Abstracted commands - Args: - busy (bool): True if busy - """ - with self._state_condition: - if busy is False: - self._internal_state &= ~GoProBase._InternalState.SYSTEM_BUSY - else: - self._internal_state |= GoProBase._InternalState.SYSTEM_BUSY - self._state_condition.notify() + # TODO move these into delegate / mixin? - @GoProBase._catch_thread_exception - def _periodic_keep_alive(self) -> None: - """Thread to periodically send the keep alive message via BLE.""" - while self.is_ble_connected: - try: - if self.keep_alive(): - time.sleep(KEEP_ALIVE_INTERVAL) - except Exception: # pylint: disable=broad-except - # If the connection disconnects while we were trying to send, there can be any number - # of exceptions. This is expected and this thread will exit on the next while check. - pass - logger.debug("periodic keep alive thread exiting...") + # TODO message rules are a mess here. Since these send other commands that need message rules, we deadlock + # if we try to apply message rules to these - def _register_listener(self, producer: ProducerType) -> None: - """Register a producer to store notifications from. + @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) + async def keep_alive(self) -> bool: + """Send a heartbeat to prevent the BLE connection from dropping. + + This is sent automatically by the GoPro instance if its `maintain_ble` argument is not False. - The notifications can be accessed via the get_notification() method. + Returns: + bool: True if it succeeded,. False otherwise + """ + return (await self.ble_setting.led.set(Params.LED.BLE_KEEP_ALIVE)).ok + + @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) + async def connect_to_access_point(self, ssid: str, password: str) -> bool: + """Connect the camera to a Wifi Access Point Args: - producer (ProducerType): Producer to listen to. + ssid (str): SSID of AP + password (str): password of AP + + Returns: + bool: True if AP is currently connected, False otherwise """ - self._listeners[producer] = True + scan_result: asyncio.Queue[proto.NotifStartScanning] = asyncio.Queue() + provisioned_result: asyncio.Queue[proto.NotifProvisioningState] = asyncio.Queue() + + async def wait_for_scan(_: Any, result: proto.NotifStartScanning) -> None: + await scan_result.put(result) + + async def wait_for_provisioning(_: Any, result: proto.NotifProvisioningState) -> None: + await provisioned_result.put(result) + + # Wait to receive scanning success + logger.info("Scanning for Wifi networks") + self.register_update(wait_for_scan, ActionId.NOTIF_START_SCAN) + await self.ble_command.scan_wifi_networks() + if (sresult := await scan_result.get()).scanning_state != proto.EnumScanning.SCANNING_SUCCESS: + logger.error(f"Scan failed: {str(sresult.scanning_state)}") + return False + scan_id = sresult.scan_id + self.unregister_update(wait_for_scan) + + # Get scan results and see if we need to provision + for entry in (await self.ble_command.get_ap_entries(scan_id=scan_id)).data.entries: + if entry.ssid == ssid: + self.register_update(wait_for_provisioning, ActionId.NOTIF_PROVIS_STATE) + # Are we already provisioned? + if entry.scan_entry_flags & proto.EnumScanEntryFlags.SCAN_FLAG_CONFIGURED: + logger.info(f"Connecting to already provisioned network {ssid}...") + await self.ble_command.request_wifi_connect(ssid=ssid) + else: + logger.info(f"Provisioning new network {ssid}...") + await self.ble_command.request_wifi_connect_new(ssid=ssid, password=password) + if ( + presult := (await provisioned_result.get()) + ).provisioning_state != proto.EnumProvisioning.PROVISIONING_SUCCESS_NEW_AP: + logger.error(f"Provision failed: {str(presult.provisioning_state)}") + return False + self.unregister_update(wait_for_provisioning) + return True + return False + + ########################################################################################################## + # End Public API + ########################################################################################################## - def _unregister_listener(self, producer: ProducerType) -> None: - """Unregister a producer in order to stop listening to its notifications. + async def _notify_listeners(self, update: types.UpdateType, value: Any) -> None: + """Notify all registered listeners of this update Args: - producer (ProducerType): Producer to stop listening to. + update (types.UpdateType): update to notify + value (Any): value to notify """ - if producer in self._listeners: - del self._listeners[producer] + for listener in self._listeners.get(update, []): + await listener(update, value) + + async def _periodic_keep_alive(self) -> None: + """Task to periodically send the keep alive message via BLE.""" + while True: + await asyncio.sleep(KEEP_ALIVE_INTERVAL) + if self.is_ble_connected: + await self.keep_alive() - def _open_ble(self, timeout: int = 10, retries: int = 5) -> None: + async def _open_ble(self, timeout: int = 10, retries: int = 5) -> None: """Connect the instance to a device via BLE. Args: @@ -472,34 +496,69 @@ def _open_ble(self, timeout: int = 10, retries: int = 5) -> None: retries (int): How many tries to reconnect after failures. Defaults to 5. """ # Establish connection, pair, etc. - self._ble.open(timeout, retries) - # Start keep alive and state maintenance + await self._ble.open(timeout, retries) + # Start state maintenance if self._should_maintain_state: - self.ble_status.encoding_active.register_value_update() - self.ble_status.system_busy.register_value_update() - self.keep_alive() - self._keep_alive_thread.start() + await self._ready_lock.acquire() + encoding = (await self.ble_status.encoding_active.register_value_update(self._update_internal_state)).data + await self._update_internal_state(StatusId.ENCODING, encoding) + busy = (await self.ble_status.system_busy.register_value_update(self._update_internal_state)).data + await self._update_internal_state(StatusId.SYSTEM_BUSY, busy) logger.info("BLE is ready!") - def _update_internal_state(self, response: GoProResp) -> None: - """Update the internal state based on the received response. + async def _update_internal_state(self, update: types.UpdateType, value: int) -> None: + """Update the internal state based on a status update. + + Used to update encoding and / or busy status - Update encoding and / or busy status and notify state maintenance thread. + Args: + update (types.UpdateType): type of update (status ID) + value (int): updated value + """ + have_lock = not await self.is_ready + logger.trace(f"State update received {update.name} ==> {value}, current {self._encoding=} {self._busy=}") # type: ignore + should_notify_encoding = False + if update == StatusId.ENCODING: + self._encoding = bool(value) + if self._encoding: + should_notify_encoding = True + elif update == StatusId.SYSTEM_BUSY: + self._busy = bool(value) + + ready_now = await self.is_ready + if have_lock and ready_now: + self._ready_lock.release() + logger.trace("Control released lock") # type: ignore + elif not have_lock and not ready_now: + logger.trace("Control acquiring lock") # type: ignore + await self._ready_lock.acquire() + logger.trace("Control has lock") # type: ignore + + if should_notify_encoding and self.is_open: + logger.trace("Control setting encoded started") # type: ignore + self._encoding_started.set() + + async def _route_response(self, response: GoProResp) -> None: + """After parsing response, route it to any stakeholders (such as registered listeners) Args: - response (GoProResp): received response to parse for state changes + response (GoProResp): parsed response """ - if not self._should_maintain_state: - return - if response.cmd in [ - QueryCmdId.REG_STATUS_VAL_UPDATE, - QueryCmdId.GET_STATUS_VAL, - QueryCmdId.STATUS_VAL_PUSH, - ]: - if StatusId.ENCODING in response.data: - self._set_state_encoding(response[StatusId.ENCODING]) - if StatusId.SYSTEM_BUSY in response.data: - self._set_state_busy(response[StatusId.SYSTEM_BUSY]) + # Check if this is the awaited synchronous response (id matches). Note! these have to come in order. + response_claimed = False + if await self._sync_resp_wait_q.peek_front() == response.identifier: + # Dequeue it and put this on the ready queue + await self._sync_resp_wait_q.get() + await self._sync_resp_ready_q.put(response) + response_claimed = True + # If this wasn't the awaited synchronous response... + if not response_claimed: + logger.info(Logger.build_log_rx_str(response, asynchronous=True)) + if isinstance(response.identifier, QueryCmdId): + for update_id, value in response.data.items(): + await self._notify_listeners(update_id, value) + elif isinstance(response.identifier, (StatusId, SettingId, ActionId)): + await self._notify_listeners(response.identifier, response.data) def _notification_handler(self, handle: int, data: bytearray) -> None: """Receive notifications from the BLE controller. @@ -508,52 +567,36 @@ def _notification_handler(self, handle: int, data: bytearray) -> None: handle (int): Attribute handle that notification was received on. data (bytearray): Bytestream that was received. """ - # Responses we don't care about. For now, just the BLE-spec defined battery characteristic - if (uuid := self._ble.gatt_db.handle2uuid(handle)) == GoProUUIDs.BATT_LEVEL: - return - logger.debug(f'Received response on BleUUID [{uuid}]: {data.hex(":")}') - - # Add to response dict if not already there - if uuid not in self._active_resp: - self._active_resp[uuid] = GoProResp(meta=[uuid]) - - self._active_resp[uuid]._accumulate(data) - - if (response := self._active_resp[uuid]).is_received: - response._parse() - - self._update_internal_state(response) - - # Check if this is the awaited synchronous response (id matches). Note! these have to come in order. - response_claimed = False - if not self._sync_resp_wait_q.empty(): - queue_snapshot = self._sync_resp_wait_q.snapshot() - if queue_snapshot[0] == response.identifier: - # Dequeue it and put this on the ready queue - self._sync_resp_wait_q.get_nowait() - self._sync_resp_ready_q.put_nowait(response) - response_claimed = True - - # If this wasn't the awaited synchronous response... - if not response_claimed: - logger.info(Logger.build_log_rx_str(response, asynchronous=True)) - # See if there are any registered responses that need to be enqueued for client consumption - for key in list(response.data.keys()): - if (response.cmd, key) not in self._listeners and not response.is_protobuf: - del response.data[key] - # Enqueue the response if there is anything left - if len(response.data) > 0: - self._out_q.put_nowait(response) - - # Clear active response from response dict - del self._active_resp[uuid] - - def _close_ble(self) -> None: + + async def _async_notification_handler() -> None: + # Responses we don't care about. For now, just the BLE-spec defined battery characteristic + if (uuid := self._ble.gatt_db.handle2uuid(handle)) == GoProUUIDs.BATT_LEVEL: + return + logger.debug(f'Received response on BleUUID [{uuid}]: {data.hex(":")}') + # Add to response dict if not already there + if uuid not in self._active_builders: + builder = BleRespBuilder() + builder.set_uuid(uuid) + self._active_builders[uuid] = builder + # Accumulate the packet + self._active_builders[uuid].accumulate(data) + if (builder := self._active_builders[uuid]).is_finished_accumulating: + response = builder.build() + # Perform response post-processing tasks + await self._route_response(response) + # Clear active response from response dict + del self._active_builders[uuid] + + asyncio.run_coroutine_threadsafe(_async_notification_handler(), self._loop) + + async def _close_ble(self) -> None: """Terminate BLE connection if it is connected""" if self.is_ble_connected and self._ble is not None: self._ble_disconnect_event.clear() - self._ble.close() - self._ble_disconnect_event.wait() + if self._should_maintain_state: + self._keep_alive_task.cancel() + await self._ble.close() + await self._ble_disconnect_event.wait() def _disconnect_handler(self, _: Any) -> None: """Disconnect callback from BLE controller @@ -567,15 +610,15 @@ def _disconnect_handler(self, _: Any) -> None: @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) @enforce_message_rules - def _send_ble_message( - self, uuid: BleUUID, data: bytearray, response_id: ResponseType, **_: Any + async def _send_ble_message( + self, uuid: BleUUID, data: bytearray, response_id: types.ResponseType, **_: Any ) -> GoProResp: """Write a characteristic and block until its corresponding notification response is received. Args: uuid (BleUUID): characteristic to write to data (bytearray): bytes to write - response_id (ResponseType): identifier to claim parsed response in notification handler + response_id (types.ResponseType): identifier to claim parsed response in notification handler **_ (Any): not used Raises: @@ -585,29 +628,29 @@ def _send_ble_message( GoProResp: received response """ # Store information on the response we are expecting - self._sync_resp_wait_q.put(response_id) + await self._sync_resp_wait_q.put(response_id) # Fragment data and write it for packet in self._fragment(data): logger.debug(f"Writing to [{uuid.name}] UUID: {packet.hex(':')}") - self._ble.write(uuid, packet) + await self._ble.write(uuid, packet) # Wait to be notified that response was received try: - response: GoProResp = self._sync_resp_ready_q.get(timeout=WRITE_TIMEOUT) + response: GoProResp = await asyncio.wait_for(self._sync_resp_ready_q.get(), WirelessGoPro.WRITE_TIMEOUT) except queue.Empty as e: - logger.error(f"Response timeout of {WRITE_TIMEOUT} seconds!") - raise GpException.ResponseTimeout(WRITE_TIMEOUT) from e + logger.error(f"Response timeout of {WirelessGoPro.WRITE_TIMEOUT} seconds!") + raise GpException.ResponseTimeout(WirelessGoPro.WRITE_TIMEOUT) from e # Check status - if not response.is_ok: + if not response.ok: logger.warning(f"Received non-success status: {response.status}") return response @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) @enforce_message_rules - def _read_characteristic(self, uuid: BleUUID) -> GoProResp: + async def _read_characteristic(self, uuid: BleUUID) -> GoProResp: """Read a characteristic's data by GoProUUIDs. There should hopefully not be a scenario where this needs to be called directly as it is generally @@ -619,12 +662,15 @@ def _read_characteristic(self, uuid: BleUUID) -> GoProResp: Returns: GoProResp: response from UUID read """ - received_data = self._ble.read(uuid) + received_data = await self._ble.read(uuid) logger.debug(f"Reading from {uuid.name}") - return GoProResp._from_read_response(uuid, received_data) + builder = BleRespBuilder() + builder.set_uuid(uuid) + builder.set_packet(received_data) + return builder.build() @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) - def _open_wifi(self, timeout: int = 10, retries: int = 5) -> None: + async def _open_wifi(self, timeout: int = 10, retries: int = 5) -> None: """Connect to a GoPro device via Wifi. Args: @@ -635,33 +681,40 @@ def _open_wifi(self, timeout: int = 10, retries: int = 5) -> None: ConnectFailed: Was not able to establish the Wifi Connection """ logger.info("Discovering Wifi AP info and enabling via BLE") - # TODO skip if we're already connected to this SSID - password = self.ble_command.get_wifi_password().flatten - ssid = self.ble_command.get_wifi_ssid().flatten + password = (await self.ble_command.get_wifi_password()).data + ssid = (await self.ble_command.get_wifi_ssid()).data for retry in range(1, retries): try: - assert self.ble_command.enable_wifi_ap(enable=True).is_ok + assert (await self.ble_command.enable_wifi_ap(enable=True)).ok self._wifi.open(ssid, password, timeout, 1) break except GpException.ConnectFailed: logger.warning(f"Wifi connection failed. Retrying #{retry}") # In case camera Wifi is in strange disable, reset it - assert self.ble_command.enable_wifi_ap(enable=False).is_ok + assert (await self.ble_command.enable_wifi_ap(enable=False)).ok else: raise GpException.ConnectFailed("Wifi Connection failed", timeout, retries) - def _close_wifi(self) -> None: + async def _close_wifi(self) -> None: """Terminate the Wifi connection.""" if hasattr(self, "_wifi"): # Corner case where instantiation fails before superclass is initialized self._wifi.close() @enforce_message_rules - def _get(self, url: str, parser: Optional[JsonParser] = None, **kwargs: Any) -> GoProResp: - return super()._get(url, parser, **kwargs) + async def _http_get( + self, + url: str, + parser: Parser | None = None, + headers: dict | None = None, + certificate: Path | None = None, + timeout: int = GoProBase.GET_TIMEOUT, + **kwargs: Any, + ) -> GoProResp: + return await super()._http_get(url, parser, **kwargs) @enforce_message_rules - def _stream_to_file(self, url: str, file: Path) -> GoProResp: - return super()._stream_to_file(url, file) + async def _stream_to_file(self, url: str, file: Path) -> GoProResp[Path]: + return await super()._stream_to_file(url, file) @property def _base_url(self) -> str: diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/logger.py b/demos/python/sdk_wireless_camera_control/open_gopro/logger.py new file mode 100644 index 00000000..edccacfc --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/logger.py @@ -0,0 +1,267 @@ +# logger.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Thu Aug 24 17:08:14 UTC 2023 + +"""Logger abstraction above default python logging""" + +from __future__ import annotations + +import http.client as http_client +import logging +from pathlib import Path +from typing import Any, Final + +from rich import traceback +from rich.logging import RichHandler + + +class Logger: + """A singleton class to manage logging for the Open GoPro internal modules + + Args: + logger (logging.Logger): input logger that will be modified and then returned + output (Path, Optional): Path of log file for file stream handler. If not set, will not log to file. + modules (dict[str, int], Optional): Optional override of modules / levels. Will be merged into default + modules. + """ + + _instances: dict[type[Logger], Logger] = {} + ARROW_HEAD_COUNT: Final = 8 + ARROW_TAIL_COUNT: Final = 14 + + def __new__(cls, *_: Any) -> Any: # noqa https://github.com/PyCQA/pydocstyle/issues/515 + if cls not in cls._instances: + c = object.__new__(cls) + cls._instances[cls] = c + return c + raise RuntimeError("The logger can only be setup once and this should be done at the top level.") + + def __init__( + self, + logger: Any, + output: Path | None = None, + modules: dict[str, int] | None = None, + ) -> None: + self.modules = { + "open_gopro.gopro_base": logging.DEBUG, # TRACE for raw HTTP responses + "open_gopro.gopro_wired": logging.DEBUG, # TRACE for concurrency debugging + "open_gopro.gopro_wireless": logging.DEBUG, # TRACE for concurrency debugging + "open_gopro.api.builders": logging.DEBUG, + "open_gopro.api.http_commands": logging.DEBUG, + "open_gopro.api.ble_commands": logging.DEBUG, + "open_gopro.communication_client": logging.DEBUG, + "open_gopro.ble.adapters.bleak_wrapper": logging.INFO, # DEBUG for pexpect communication + "open_gopro.ble.client": logging.DEBUG, + "open_gopro.wifi.adapters.wireless": logging.DEBUG, + "open_gopro.wifi.mdns_scanner": logging.DEBUG, + "open_gopro.responses": logging.DEBUG, + "open_gopro.util": logging.DEBUG, + "bleak": logging.DEBUG, + "urllib3": logging.DEBUG, + "http.client": logging.DEBUG, + } + + self.logger = logger + self.modules = {**self.modules, **modules} if modules else self.modules + self.handlers: list[logging.Handler] = [] + + # monkey-patch a `print` global into the http.client module; all calls to + # print() in that module will then use our logger's debug method + http_client.HTTPConnection.debuglevel = 1 + http_client.print = lambda *args: logging.getLogger("http.client").debug(" ".join(args)) # type: ignore + + self.file_handler: logging.Handler | None + if output: + # Logging to file with millisecond timing + self.file_handler = logging.FileHandler(output, mode="w") + file_formatter = logging.Formatter( + fmt="%(threadName)13s:%(asctime)s.%(msecs)03d %(filename)-40s %(lineno)4s %(levelname)-8s | %(message)s", + datefmt="%H:%M:%S", + ) + self.file_handler.setFormatter(file_formatter) + self.file_handler.setLevel(logging.TRACE) # type: ignore # pylint: disable=no-member + logger.addHandler(self.file_handler) + self.addLoggingHandler(self.file_handler) + else: + self.file_handler = None + + # Use Rich for colorful console logging + self.stream_handler = RichHandler(rich_tracebacks=True, enable_link_path=True, show_time=False) + stream_formatter = logging.Formatter("%(asctime)s.%(msecs)03d %(message)s", datefmt="%H:%M:%S") + self.stream_handler.setFormatter(stream_formatter) + self.stream_handler.setLevel(logging.INFO) + logger.addHandler(self.stream_handler) + self.addLoggingHandler(self.stream_handler) + + self.addLoggingLevel("TRACE", logging.DEBUG - 5) + logger.setLevel(logging.TRACE) # type: ignore # pylint: disable=no-member + + traceback.install() # Enable exception tracebacks in rich logger + + @classmethod + def get_instance(cls) -> Logger: + """Get the singleton instance + + Raises: + RuntimeError: Has not yet been instantiated + + Returns: + Logger: singleton instance + """ + if not (logger := cls._instances.get(Logger, None)): + raise RuntimeError("Logging must first be setup") + return logger + + def addLoggingHandler(self, handler: logging.Handler) -> None: + """Add a handler for all of the internal GoPro modules + + Args: + handler (logging.Handler): handler to add + """ + self.logger.addHandler(handler) + self.handlers.append(handler) + + # Enable / disable logging in modules + for module, level in self.modules.items(): + l = logging.getLogger(module) + l.setLevel(level) + l.addHandler(handler) + + # From https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945 + @staticmethod + def addLoggingLevel(levelName: str, levelNum: int) -> None: + """Comprehensively adds a new logging level to the `logging` module and the currently configured logging class. + + `levelName` becomes an attribute of the `logging` module with the value + `levelNum`. `methodName` becomes a convenience method for both `logging` + itself and the class returned by `logging.getLoggerClass()` (usually just + `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is + used. + + To avoid accidental clobberings of existing attributes, this method will + raise an `AttributeError` if the level name is already an attribute of the + `logging` module or if the method name is already present + + Example: + -------- + >>> addLoggingLevel('TRACE', logging.DEBUG - 5) + >>> logging.getLogger(__name__).setLevel("TRACE") + >>> logging.getLogger(__name__).trace('that worked') + >>> logging.trace('so did this') + >>> logging.TRACE + 5 + + Args: + levelName (str): name of level (i.e. TRACE) + levelNum (int): integer level of new logging level + """ + methodName = levelName.lower() + + def logForLevel(self: Any, message: str, *args: Any, **kwargs: Any) -> None: + if self.isEnabledFor(levelNum): + self._log(levelNum, message, args, **kwargs) + + def logToRoot(message: str, *args: Any, **kwargs: Any) -> None: + logging.log(levelNum, message, *args, **kwargs) + + logging.addLevelName(levelNum, levelName) + setattr(logging, levelName, levelNum) + setattr(logging.getLoggerClass(), methodName, logForLevel) + setattr(logging, methodName, logToRoot) + + @staticmethod + def build_log_tx_str(stringable: Any) -> str: + """Build a string with Tx arrows + + Args: + stringable (Any): stringable object to surround with arrows + + Returns: + str: string surrounded by Tx arrows + """ + s = str(stringable).strip(r"{}") + arrow = f"{'<'*Logger.ARROW_HEAD_COUNT}{'-'*Logger.ARROW_TAIL_COUNT}" + return f"\n{arrow}{s}{arrow}\n" + + @staticmethod + def build_log_rx_str(stringable: Any, asynchronous: bool = False) -> str: + """Build a string with Rx arrows + + Args: + stringable (Any): stringable object to surround with arrows + asynchronous (bool): Should the arrows contain ASYNC?. Defaults to False. + + Returns: + str: string surrounded by Rx arrows + """ + s = str(stringable).strip(r"{}") + assert Logger.ARROW_TAIL_COUNT > 5 + if asynchronous: + arrow = f"{'-'*(Logger.ARROW_TAIL_COUNT//2-3)}ASYNC{'-'*(Logger.ARROW_TAIL_COUNT//2-2)}{'>'*Logger.ARROW_HEAD_COUNT}" + else: + arrow = f"{'-'*Logger.ARROW_TAIL_COUNT}{'>'*Logger.ARROW_HEAD_COUNT}" + return f"\n{arrow}{s}{arrow}\n" + + +def setup_logging( + base: logging.Logger | str, output: Path | None = None, modules: dict[str, int] | None = None +) -> logging.Logger: + """Configure the GoPro modules for logging and get a logger that can be used by the application + + This can only be called once and should be done at the top level of the application. + + Args: + base (Union[logging.Logger, str]): Name of application (i.e. __name__) or preconfigured logger to use as base + output (Path, Optional): Path of log file for file stream handler. If not set, will not log to file. + modules (dict[str, int], Optional): Optional override of modules / levels. Will be merged into default + modules. + + Raises: + TypeError: Base logger is not of correct type + + Returns: + logging.Logger: updated logger that the application can use for logging + """ + if isinstance(base, str): + base = logging.getLogger(base) + elif not isinstance(base, logging.Logger): + raise TypeError("Base must be of type logging.Logger or str") + l = Logger(base, output, modules) + return l.logger + + +def set_file_logging_level(level: int) -> None: + """Change the global logging level for the default file output handler + + Args: + level (int): level to set + """ + if fh := Logger.get_instance().file_handler: + fh.setLevel(level) + + +def set_stream_logging_level(level: int) -> None: + """Change the global logging level for the default stream output handler + + Args: + level (int): level to set + """ + Logger.get_instance().stream_handler.setLevel(level) + + +def set_logging_level(level: int) -> None: + """Change the global logging level for the default file and stream output handlers + + Args: + level (int): level to set + """ + set_file_logging_level(level) + set_stream_logging_level(level) + + +def add_logging_handler(handler: logging.Handler) -> None: + """Add a handler to all of the GoPro internal modules + + Args: + handler (logging.Handler): handler to add + """ + Logger.get_instance().addLoggingHandler(handler) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/models/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/models/__init__.py new file mode 100644 index 00000000..de3bf0e7 --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/models/__init__.py @@ -0,0 +1,15 @@ +# __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jun 26 18:26:05 UTC 2023 + +"""Data models for use throughout this package""" + +from .general import CameraInfo, TzDstDateTime +from .media_list import ( + GroupedMediaItem, + MediaItem, + MediaList, + MediaMetadata, + PhotoMetadata, + VideoMetadata, +) +from .response import GoProResp diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/models/bases.py b/demos/python/sdk_wireless_camera_control/open_gopro/models/bases.py new file mode 100644 index 00000000..dd550fe8 --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/models/bases.py @@ -0,0 +1,20 @@ +# bases.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 + +"""Base classes shared throughout models""" + +from pydantic import BaseModel + +from open_gopro.util import pretty_print, scrub + + +class CustomBaseModel(BaseModel): + """Additional functionality added to Pydantic BaseModel""" + + def __hash__(self) -> int: + return hash((type(self),) + tuple(getattr(self, f) for f in self.__fields__.keys())) + + def __str__(self) -> str: + d = dict(self) + scrub(d, bad_values=[None]) + return pretty_print(d) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/models/general.py b/demos/python/sdk_wireless_camera_control/open_gopro/models/general.py new file mode 100644 index 00000000..63739f53 --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/models/general.py @@ -0,0 +1,58 @@ +# general.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 + +"""Other models that don't deserve their own file""" + +from __future__ import annotations + +import datetime +from typing import Optional + +from pydantic import Field + +from open_gopro import constants +from open_gopro.models.bases import CustomBaseModel + + +class CameraInfo(CustomBaseModel): + """General camera info""" + + model_number: int #: Camera model number + model_name: str #: Camera model name as string + firmware_version: str #: Complete firmware version + serial_number: str #: Camera serial number + ap_mac_addr: str #: Camera access point MAC address + ap_ssid: str #: Camera access point SSID name + + +class TzDstDateTime(CustomBaseModel): + """DST aware datetime""" + + datetime: datetime.datetime + tzone: int + dst: bool + + +class SupportedOption(CustomBaseModel): + """A supported option in an invalid setting response""" + + display_name: str + id: int + + +class WebcamResponse(CustomBaseModel): + """Common Response from Webcam Commands""" + + status: Optional[constants.WebcamStatus] = Field(default=None) + error: constants.WebcamError + setting_id: Optional[str] = Field(default=None) + supported_options: Optional[list[SupportedOption]] = Field(default=None) + + +class HttpInvalidSettingResponse(CustomBaseModel): + """Invalid settings response with optional supported options""" + + error: int + setting_id: constants.SettingId + option_id: Optional[int] = Field(default=None) + supported_options: Optional[list[SupportedOption]] = Field(default=None) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/models/media_list.py b/demos/python/sdk_wireless_camera_control/open_gopro/models/media_list.py new file mode 100644 index 00000000..00f4fd46 --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/models/media_list.py @@ -0,0 +1,150 @@ +# media_list.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jun 26 18:26:05 UTC 2023 + +"""Media List and Metadata containers and helper methods""" + +from __future__ import annotations + +from abc import ABC +from typing import Optional + +from pydantic import Field, validator + +from open_gopro import types +from open_gopro.models.bases import CustomBaseModel + +############################################################################################################## +# Metadata +############################################################################################################## + + +class MediaMetadata(ABC, CustomBaseModel): + """Base Media Metadata class""" + + content_type: str = Field(alias="ct") #: Media content type + creation_timestamp: str = Field(alias="cre") #: Creation time in seconds since epoch + file_size: str = Field(alias="s") #: File size in bytes + gumi: str = Field(alias="gumi") #: Globally Unique Media ID + height: str = Field(alias="h") #: Height of media in pixels + width: str = Field(alias="w") #: Width of media in pixels + hilight_count: str = Field(alias="hc") #: Number of hilights in media + image_stabilization: str = Field(alias="eis") #: 1 if stabilized, 0 otherwise + metadata_present: str = Field(alias="mp") #: 1 if metadata is present, 0 otherwise + rotate: str = Field(alias="rot") #: Media rotation + transcoded: str = Field(alias="tr") #: 1 if file is transcoded, 0 otherwise + upload_status: str = Field(alias="us") #: Whether or not the media file has been uploaded + media_offload_state: Optional[list[str]] = Field(alias="mos", default=None) #: List of offload states + parent_gumi: Optional[str] = Field(alias="pgumi", default=None) #: Only present if in a clip + field_of_view: Optional[str] = Field(alias="fov", default=None) #: Field of View + lens_config: Optional[str] = Field(alias="lc", default=None) #: Lens configuration + lens_projection: Optional[str] = Field(alias="prjn", default=None) #: Lens projection + + @classmethod + def from_json(cls, json_str: types.JsonDict) -> MediaMetadata: + """Build a metadata object given JSON input + + Args: + json_str (types.JsonDict): raw JSON + + Returns: + MediaMetadata: parsed metadata + """ + # Choose a field that only exists in video to see if this is a video + return (VideoMetadata if "ao" in json_str else PhotoMetadata)(**json_str) + + +class VideoMetadata(MediaMetadata): + """Metadata for a video file""" + + audio_option: str = Field(alias="ao") #: Auto, wind, or stereo + avc_level: str = Field(alias="profile") #: Advanced Video Codec Level + avc_profile: str = Field(alias="avc_profile") #: Advanced Video Code Profile + clipped: str = Field(alias="cl") #: 1 if clipped, 0 otherwise + duration: str = Field(alias="dur") #: Video duration in seconds + frame_rate: str = Field(alias="fps") # Video frame rate in frames / second + frame_rate_divisor: str = Field(alias="fps_denom") #: Used to modify frame rate + hilight_list: list[str] = Field(alias="hi") #: List of hlights in ms offset from start of video + lrv_file_size: str = Field(alias="ls") #: Low Resolution Video file size in bytes. -1 if there is no LRV + max_auto_hilight_score: str = Field(alias="mahs") #: Maximum auto-hilight score + protune_audio: str = Field(alias="pta") #: 1 if protune audio is present, 0 otherwise + subsample: str = Field(alias="subsample") #: 1 if subsampled from other video, 0 otherwise + progressive: Optional[str] = Field(alias="progr", default=None) #: 1 if progressive, 0 otherwise + + +class PhotoMetadata(MediaMetadata): + """Metadata for a Photo file""" + + raw: Optional[str] = Field(default=None) #: 1 if photo has raw version, 0 otherwise + """1 if photo taken with wide dynamic range, 0 otherwise""" + wide_dynamic_range: Optional[str] = Field(alias="wdr", default=None) + """1 if photo taken with high dynamic range, 0 otherwise""" + high_dynamic_range: Optional[str] = Field(alias="hdr", default=None) + + +############################################################################################################## +# Media List +############################################################################################################## + + +class MediaItem(CustomBaseModel): + """Base Media Item class""" + + filename: str = Field(alias="n") #: Name of media item + creation_timestamp: str = Field(alias="cre") #: Creation time in seconds since epoch + modified_time: str = Field(alias="mod") #: Time file was last modified in seconds since epoch + low_res_video_size: Optional[str] = Field(alias="glrv", default=None) #: Low resolution video size + lrv_file_size: Optional[str] = Field(alias="ls", default=None) #: Low resolution file size + session_id: Optional[str] = Field(alias="id", default=None) # Media list session identifier + + +class GroupedMediaItem(MediaItem): + """Media Item that is also a grouped item. + + An example of a grouped item is a burst photo. + """ + + group_id: str = Field(alias="g", default=None) #: Group Identifier + group_size: str = Field(alias="s", default=None) # Number of files in the group + group_first_member_id: str = Field(alias="b", default=None) # ID of first member in the group + group_last_member_id: str = Field(alias="l", default=None) #: ID of last member in the group + group_missing_ids: list[str] = Field(alias="m", default=None) #: File ID's that are missing or deleted + """(b -> burst, c -> continuous shot, n -> night lapse, t -> time lapse)""" + group_type: str = Field(alias="t", default=None) + + +class MediaFileSystem(CustomBaseModel): + """Grouping of media items into filesystem(s)""" + + directory: str = Field(alias="d") # Directory that the files are in + file_system: list[MediaItem] = Field(alias="fs") #: List of files + + @validator("file_system", pre=True, each_item=True) + @classmethod + def identify_item(cls, item: types.JsonDict) -> MediaItem: + """Extent item into GroupedMediaItem if it such an item + + A group item is identified by the presence of a "g" field + + Args: + item (types.JsonDict): input JSON + + Returns: + MediaItem: parsed media item + """ + return (GroupedMediaItem if "g" in item else MediaItem)(**item) + + +class MediaList(CustomBaseModel): + """Top level media list object""" + + identifier: str = Field(alias="id") #: String identifier of this media list + media: list[MediaFileSystem] #: Media filesystem(s) + + @property + def files(self) -> list[MediaItem]: + """Helper method to get list of media items + + Returns: + list[MediaItem]: all media items in this media list + """ + return [item for media in self.media for item in media.file_system] diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/models/response.py b/demos/python/sdk_wireless_camera_control/open_gopro/models/response.py new file mode 100644 index 00000000..df15512e --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/models/response.py @@ -0,0 +1,521 @@ +# responses.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed, Sep 1, 2021 5:05:49 PM + +"""Any responses that are returned from GoPro commands.""" + +from __future__ import annotations + +import enum +import logging +from abc import ABC, abstractmethod +from collections import defaultdict +from dataclasses import dataclass +from typing import Any, Final, Generic, TypeVar + +import requests + +from open_gopro import types +from open_gopro.api.parsers import JsonParsers +from open_gopro.ble import BleUUID +from open_gopro.constants import ( + ActionId, + CmdId, + ErrorCode, + FeatureId, + GoProEnum, + GoProUUIDs, + QueryCmdId, + SettingId, + StatusId, +) +from open_gopro.exceptions import ResponseParseError +from open_gopro.parser_interface import GlobalParsers, Parser +from open_gopro.proto import EnumResultGeneric +from open_gopro.util import pretty_print + +logger = logging.getLogger(__name__) + +CONT_MASK: Final = 0b10000000 +HDR_MASK: Final = 0b01100000 +GEN_LEN_MASK: Final = 0b00011111 +EXT_13_BYTE0_MASK: Final = 0b00011111 + + +class Header(enum.Enum): + """Packet Headers.""" + + GENERAL = 0b00 + EXT_13 = 0b01 + EXT_16 = 0b10 + RESERVED = 0b11 + CONT = enum.auto() + + +T = TypeVar("T") + + +@dataclass +class GoProResp(Generic[T]): + """The object used to encapsulate all GoPro responses. + + It consists of several common properties / attribute and a data attribute that varies per response. + + >>> gopro = WirelessGoPro() + >>> await gopro.open() + >>> response = await (gopro.ble_setting.resolution).get_value() + >>> print(response) + + Now let's inspect the responses various attributes / properties: + + >>> print(response.status) + ErrorCode.SUCCESS + >>> print(response.ok) + True + >>> print(response.identifier) + QueryCmdId.GET_SETTING_VAL + >>> print(response.protocol) + Protocol.BLE + + Now let's print it's data as (as JSON): + + >>> print(response) + { + "id" : "QueryCmdId.GET_SETTING_VAL", + "status" : "ErrorCode.SUCCESS", + "protocol" : "Protocol.BLE", + "data" : { + "SettingId.RESOLUTION" : "Resolution.RES_4K_16_9", + }, + } + + Attributes: + protocol (GoProResp.Protocol): protocol response was received on + status (ErrorCode): status of response + data (T): parsed response data + identifier (types.ResponseType): response identifier, the type of which will vary depending on the response + """ + + class Protocol(enum.Enum): + """Protocol that Command will be sent on.""" + + BLE = "BLE" + HTTP = "HTTP" + + protocol: GoProResp.Protocol + status: ErrorCode + data: T + identifier: types.ResponseType + + def _as_dict(self) -> dict: + """Represent the response as dictionary, merging it's data and meta information + + Returns: + dict: dict representation + """ + d = { + "id": self.identifier, + "status": self.status, + "protocol": self.protocol, + } + if self.data: + d["data"] = self.data # type: ignore + return d + + def __eq__(self, obj: object) -> bool: + if isinstance(obj, GoProEnum): + return self.identifier == obj + if isinstance(obj, GoProResp): + return self.identifier == obj.identifier + raise TypeError("Equal can only compare GoProResp and types.ResponseType") + + def __str__(self) -> str: + return pretty_print(self._as_dict()) + + def __repr__(self) -> str: + return f"GoProResp <{str(self.identifier)}>" + + @property + def ok(self) -> bool: + """Are there any errors in this response? + + Returns: + bool: True if the response is ok (i.e. there are no errors), False otherwise + """ + return self.status in [ErrorCode.SUCCESS, ErrorCode.UNKNOWN] + + +class RespBuilder(ABC, Generic[T]): + """Common Response Builder Interface""" + + class _State(enum.Enum): + """Describes the state of building the response.""" + + INITIALIZED = enum.auto() + ACCUMULATED = enum.auto() + PARSED = enum.auto() + ERROR = enum.auto() + + def __init__(self) -> None: + self._packet: T + self._status: ErrorCode = ErrorCode.UNKNOWN + self._state: RespBuilder._State = RespBuilder._State.INITIALIZED + self._parser: Parser | None = None + + @abstractmethod + def build(self) -> GoProResp: + """Build a response + + Returns: + GoProResp: built response + """ + raise NotImplementedError + + +class HttpRespBuilder(RespBuilder[types.JsonDict]): + """HTTP Response Builder + + This is not intended to be fool proof to use as the user must understand which fields are needed. + Directors should be created if this needs to be simplified. + """ + + def __init__(self) -> None: + super().__init__() + self._endpoint: str + self._response: types.JsonDict + + def set_response(self, response: types.JsonDict) -> None: + """Store the JSON data. This is mandatory. + + Args: + response (types.JsonDict): json data_ + """ + self._response = response + + def set_status(self, status: ErrorCode) -> None: + """Store the status. This is mandatory. + + Args: + status (ErrorCode): status of response + """ + self._status = status + + def set_parser(self, parser: Parser) -> None: + """Store a parser. This is optional. + + Args: + parser (Parser): monolithic parser + """ + self._parser = parser + + def set_endpoint(self, endpoint: str) -> None: + """Store the endpoint. This is mandatory. + + Args: + endpoint (str): endpoint of response. + """ + self._endpoint = endpoint + + def build(self) -> GoProResp: + """Build the GoPro response from the information accumulated about the HTTP response + + Returns: + GoProResp: built response + """ + # Is there a parser for this? Most of them do not have one yet. + data = self._parser.parse(self._response) if self._parser else self._response + return GoProResp( + protocol=GoProResp.Protocol.HTTP, + status=self._status, + identifier=self._endpoint, + data=data, + ) + + +class RequestsHttpRespBuilderDirector: + """An abstraction to help simplify using the HTTP Response Builder for requests + + Args: + response (requests.models.Response): direct response from requests + parser (Parser | None): parsers to use on the requests response + """ + + def __init__(self, response: requests.models.Response, parser: Parser | None) -> None: + + self.response = response + self.parser = parser or Parser(json_parser=JsonParsers.LambdaParser(lambda data: data)) + + def __call__(self) -> GoProResp: + """Build the response + + Returns: + GoProResp: built response + """ + builder = HttpRespBuilder() + builder.set_endpoint(self.response.url) + builder.set_status(ErrorCode.SUCCESS if self.response.ok else ErrorCode.ERROR) + builder.set_parser(self.parser) + builder.set_response(self.response.json() if self.response.text else {}) + return builder.build() + + +class BleRespBuilder(RespBuilder[bytearray]): + """BLE Response Builder + + This is not intended to be fool proof to use as the user must understand which fields are needed. + Directors should be created if this needs to be simplified. + """ + + def __init__(self) -> None: + self._bytes_remaining = 0 + self._uuid: BleUUID + self._identifier: types.ResponseType + self._feature_id: FeatureId | None = None + self._action_id: ActionId | None = None + super().__init__() + + @property + def is_response_protobuf(self) -> bool: + """Is this a protobuf response? + + Returns: + bool: True if protobuf, False otherwise + """ + return isinstance(self._identifier, (ActionId, FeatureId)) + + def _set_response_meta(self) -> None: + """Set the identifier based on what is currently known about the packet""" + # If it's a protobuf command + identifier = self._packet[0] + try: + FeatureId(identifier) + self._identifier = ActionId(self._packet[1]) + # Otherwise it's a TLV command + except ValueError: + if self._uuid is GoProUUIDs.CQ_SETTINGS_RESP: + self._identifier = SettingId(identifier) + elif self._uuid is GoProUUIDs.CQ_QUERY_RESP: + self._identifier = QueryCmdId(identifier) + elif self._uuid in [GoProUUIDs.CQ_COMMAND_RESP, GoProUUIDs.CN_NET_MGMT_RESP]: + self._identifier = CmdId(identifier) + else: + self._identifier = self._uuid + + def set_parser(self, parser: Parser) -> None: + """Store a parser. This is optional. + + Args: + parser (Parser): monolithic parser + """ + self._parser = parser + + def set_packet(self, packet: bytes) -> None: + """Store the complete data that comprises the response. + + This is mutually exclusive with accumulate. It is only for responses (such as direct UUID reads) that + do not follow the packet fragmentation scheme. + + Args: + packet (bytes): packet to store + """ + self._packet = bytearray(packet) + + def accumulate(self, data: bytes) -> None: + """Accumulate BLE byte data. + + This is mutually exclusive with accumulate. It should be used in any case where the response follows + the packet fragmentation scheme. + + Args: + data (bytes): byte level BLE data + """ + buf = bytearray(data) + if buf[0] & CONT_MASK: + buf.pop(0) + else: + # This is a new packet so start with an empty byte array + self._packet = bytearray([]) + hdr = Header((buf[0] & HDR_MASK) >> 5) + if hdr is Header.GENERAL: + self._bytes_remaining = buf[0] & GEN_LEN_MASK + buf = buf[1:] + elif hdr is Header.EXT_13: + self._bytes_remaining = ((buf[0] & EXT_13_BYTE0_MASK) << 8) + buf[1] + buf = buf[2:] + elif hdr is Header.EXT_16: + self._bytes_remaining = (buf[1] << 8) + buf[2] + buf = buf[3:] + + # Append payload to buffer and update remaining / complete + self._packet.extend(buf) + self._bytes_remaining -= len(buf) + + if self._bytes_remaining < 0: + logger.error("received too much data. parsing is in unknown state") + elif self._bytes_remaining == 0: + self._state = RespBuilder._State.ACCUMULATED + + def set_status(self, status: ErrorCode) -> None: + """Store the status. This is sometimes optional. + + Args: + status (ErrorCode): status + """ + self._status = status + + def set_uuid(self, uuid: BleUUID) -> None: + """Store the UUID. This is mandatory. + + Args: + uuid (BleUUID): uuid + """ + self._uuid = uuid + + @property + def is_finished_accumulating(self) -> bool: + """Has the response been completely received? + + Returns: + bool: True if completely received, False if not + """ + return self._state is not RespBuilder._State.INITIALIZED + + @property + def _is_protobuf(self) -> bool: + """Is this response a protobuf response + + Returns: + bool: Yes if true, No otherwise + """ + return isinstance(self._identifier, (ActionId, FeatureId)) + + @property + def _is_direct_read(self) -> bool: + """Is this response a direct read of a BLE characteristic + + Returns: + bool: Yes if true, No otherwise + """ + return isinstance(self._identifier, BleUUID) + + def build(self) -> GoProResp: + """Parse the accumulated response (either from a BLE bytestream or an HTTP JSON dict). + + Raises: + NotImplementedError: Parsing for this id is not yet supported + ResponseParseError: Error when parsing data + + Returns: + GoProResp: built response + """ + self._set_response_meta() + buf = self._packet + + if not self._is_direct_read: # length byte + buf.pop(0) + if self._is_protobuf: # feature ID byte + buf.pop(0) + + try: + parsed: Any = None + query_type: type[StatusId] | type[SettingId] | StatusId | SettingId | None = None + # Need to delineate QueryCmd responses between settings and status + if not self._is_protobuf: + if isinstance(self._identifier, (SettingId, StatusId)): + query_type = self._identifier + elif isinstance(self._identifier, QueryCmdId): + if self._identifier in [ + QueryCmdId.GET_STATUS_VAL, + QueryCmdId.REG_STATUS_VAL_UPDATE, + QueryCmdId.UNREG_STATUS_VAL_UPDATE, + QueryCmdId.STATUS_VAL_PUSH, + ]: + query_type = StatusId + elif self._identifier is QueryCmdId.GET_SETTING_NAME: + raise NotImplementedError + else: + query_type = SettingId + + # Query (setting get value, status get value, etc.) + if query_type: + is_multiple = self._identifier in [ + QueryCmdId.GET_CAPABILITIES_VAL, + QueryCmdId.REG_CAPABILITIES_UPDATE, + QueryCmdId.SETTING_CAPABILITY_PUSH, + ] + + camera_state: types.CameraState = defaultdict(list) + self._status = ErrorCode(buf[0]) + buf = buf[1:] + # Parse all parameters + while len(buf) != 0: + param_id = query_type(buf[0]) # type: ignore + param_len = buf[1] + buf = buf[2:] + # Special case where we register for a push notification for something that does not yet + # have a value + if param_len == 0: + camera_state[param_id] = [] + continue + param_val = buf[:param_len] + buf = buf[param_len:] + + # Add parsed value to response's data dict + try: + if not (parser := GlobalParsers.get_parser(param_id)): + # We don't have defined params for all ID's yet. Just store raw bytes + logger.warning(f"No parser defined for {param_id}") + camera_state[param_id] = param_val + continue + # These can be more than 1 value so use a list + if is_multiple: + # Parse using parser from global map and append + camera_state[param_id].append(parser.parse(param_val)) + else: + # Parse using parser from map and set + camera_state[param_id] = parser.parse(param_val) + except ValueError: + # This is the case where we receive a value that is not defined in our params. + # This shouldn't happen and means the documentation needs to be updated. However, it + # isn't functionally critical + logger.warning(f"{param_id} does not contain a value {param_val}") + camera_state[param_id] = param_val + # Flatten if not multiple + if is_multiple: + self._identifier = list(camera_state.keys())[0] + parsed = list(camera_state.values())[0] + else: + parsed = camera_state + else: # Commands, Protobuf, and direct Reads + if is_cmd := isinstance(self._identifier, CmdId): + # All (non-protobuf) commands have a status + self._status = ErrorCode(buf[0]) + buf = buf[1:] + # Use parser if explicitly passed otherwise get global parser + if not (parser := self._parser or GlobalParsers.get_parser(self._identifier)) and not is_cmd: + error_msg = f"No parser exists for {self._identifier}" + logger.error(error_msg) + raise ResponseParseError(str(self._identifier), self._packet, msg=error_msg) + # Parse payload if a parser was found. + if parser: + parsed = parser.parse(buf) + + # TODO make status checking an abstract method of a shared base class + # Attempt to determine and / or extract status (we already got command status above) + if self._is_direct_read and len(self._packet): + # Assume success on direct reads if there was any data + self._status = ErrorCode.SUCCESS + # Check for result field in protobuf's + elif self._is_protobuf and "result" in parsed: + self._status = ( + ErrorCode.SUCCESS + if parsed.get("result") == EnumResultGeneric.RESULT_SUCCESS + else ErrorCode.ERROR + ) + except KeyError as e: + self._state = RespBuilder._State.ERROR + raise ResponseParseError(str(self._identifier), buf) from e + + # Recursively scrub away parsing artifacts + self._state = RespBuilder._State.PARSED + + return GoProResp(protocol=GoProResp.Protocol.BLE, status=self._status, data=parsed, identifier=self._identifier) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/parser_interface.py b/demos/python/sdk_wireless_camera_control/open_gopro/parser_interface.py new file mode 100644 index 00000000..7f136c7a --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/parser_interface.py @@ -0,0 +1,262 @@ +# parsers.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jun 26 18:26:05 UTC 2023 + +"""Parser Protocol and Bases""" + +from __future__ import annotations + +import logging +from abc import ABC, abstractmethod +from collections import defaultdict +from typing import Any, Callable, ClassVar, Generic, Protocol, TypeVar, cast + +from open_gopro import types +from open_gopro.constants import ActionId, FeatureId + +logger = logging.getLogger(__name__) + +T_co = TypeVar("T_co", covariant=True) +T = TypeVar("T") + +######################################################################################## +####### Transformers +######################################################################################## + + +class BaseTransformer(ABC, Generic[T]): + """Transformer interface. + + A transformer is something that transforms the input into the same output type + """ + + @abstractmethod + def transform(self, data: T) -> T: + """Transform data into output matching the input type + + Args: + data (T): data to transform + + Returns: + T: transformed data + """ + raise NotImplementedError + + +class BytesTransformer(BaseTransformer[bytes]): + """Bytes to Bytes transformer interface""" + + +class JsonTransformer(BaseTransformer[types.JsonDict]): + """Json to json transformer interface""" + + +######################################################################################## +####### Parsers +######################################################################################## + + +class BaseParser(ABC, Generic[T, T_co]): + """Base Parser Interface + + A parser is something that transforms input into a different type + """ + + @abstractmethod + def parse(self, data: T) -> T_co: # pylint: disable=method-hidden + """Parse data into output type + + Args: + data (T): input data to parse + + Returns: + T_co: parsed output + """ + raise NotImplementedError + + +class JsonParser(BaseParser[types.JsonDict, T_co]): + """Json to Target Type Parser Interface""" + + +class BytesParser(BaseParser[bytes, T_co]): + """Bytes to Target Type Parser Interface""" + + +class Parser(ABC, Generic[T]): + """The common monolithic Parser that is used for all byte and json parsing / transforming + + Algorithm is: + 1. Variable number of byte transformers (bytes --> bytes) + 2. One bytes Json adapter (bytes --> json) + 3. Variable number of json transformers (json --> json) + 4. One JSON parser (json -> Any) + + Args: + byte_transformers (list[BytesTransformer] | None, optional): bytes --> bytes. Defaults to None. + byte_json_adapter (BytesParser[types.JsonDict] | None, optional): bytes --> json. Defaults to None. + json_transformers (list[JsonTransformer] | None, optional): json --> json. Defaults to None. + json_parser (JsonParser[T] | None, optional): json --> T. Defaults to None. + """ + + def __init__( + self, + byte_transformers: list[BytesTransformer] | None = None, + byte_json_adapter: BytesParser[types.JsonDict] | None = None, + json_transformers: list[JsonTransformer] | None = None, + json_parser: JsonParser[T] | None = None, + ) -> None: + self.byte_transformers = byte_transformers or [] + self.byte_json_adapter = byte_json_adapter + self.json_transformers = json_transformers or [] + self.json_parser = json_parser + + def parse(self, data: bytes | bytearray | types.JsonDict) -> T: + """Perform the parsing using the stored transformers and parsers + + Args: + data (bytes | bytearray | types.JsonDict): input bytes or json to parse + + Raises: + RuntimeError: attempted to parse bytes when a byte-json adapter does not exist + + Returns: + T: TODO + """ + parsed_json: types.JsonDict + if isinstance(data, (bytes, bytearray)): + data = bytes(data) + if not self.byte_json_adapter: + raise RuntimeError("Can not parse bytes without Json Adapter") + # Filter bytes + parsed_bytes = bytes(data) + for byte_transformer in self.byte_transformers: + parsed_bytes = byte_transformer.transform(data) + parsed_json = self.byte_json_adapter.parse(parsed_bytes) + else: + parsed_json = data + + for json_transformer in self.json_transformers: + parsed_json = json_transformer.transform(parsed_json) + if self.json_parser: + return self.json_parser.parse(parsed_json) + return cast(T, parsed_json) + + +######################################################################################## +####### Builders +######################################################################################## + + +class BytesBuilder(Protocol): + """Base bytes serializer protocol definition""" + + def build(self, obj: Any) -> bytes: + """Build bytestream from object + + # noqa: DAR202 + + Args: + obj (Any): object to serialize + + Returns: + bytes: serialized bytestream + """ + + +class BytesParserBuilder(BytesParser[T_co], BytesBuilder): + """Class capable of both building / parsing bytes to / from object""" + + @abstractmethod + def parse(self, data: bytes) -> T_co: + """Parse input bytes to output + + Args: + data (bytes): data to parsed + + Returns: + T_co: parsed output + """ + raise NotImplementedError + + @abstractmethod + def build(self, obj: Any) -> bytes: + """Build bytestream from object + + # noqa: DAR202 + + Args: + obj (Any): object to serialize + + Returns: + bytes: serialized bytestream + """ + + +class GlobalParsers: + """Parsers that relate globally to ID's as opposed to contextualized per-message + + This is intended to be used as a singleton, i.e. not instantiated + + Returns: + _type_: _description_ + """ + + _feature_action_id_map: ClassVar[dict[FeatureId, list[ActionId]]] = defaultdict(list) + _global_parsers: ClassVar[dict[types.ResponseType, Parser]] = {} + + @classmethod + def add_feature_action_id_mapping(cls, feature_id: FeatureId, action_id: ActionId) -> None: + """Add a feature id-to-action id mapping entry + + Args: + feature_id (FeatureId): Feature ID of protobuf command + action_id (ActionId): Action ID of protobuf command + """ + cls._feature_action_id_map[feature_id].append(action_id) + + @classmethod + def add(cls, identifier: types.ResponseType, parser: Parser) -> None: + """Add a global parser that can be accessed by this class's class methods + + Args: + identifier (types.ResponseType): identifier to add parser for + parser (Parser): parser to add + """ + cls._global_parsers[identifier] = parser + + @classmethod + def get_query_container(cls, identifier: types.ResponseType) -> Callable | None: + """Attempt to get a callable that will translate an input value to the ID-appropriate value. + + For example, _get_query_container(SettingId.RESOLUTION) will return + :py:class:`open_gopro.api.params.Resolution` + + As another example, _get_query_container(StatusId.TURBO_MODE) will return bool() + + Note! Not all ID's are currently parsed so None will be returned if the container does not exist + + Args: + identifier (Union[SettingId, StatusId]): identifier to find container for + + Returns: + Callable: container if found else None + """ + try: + parser_builder = cast(BytesParserBuilder, cls._global_parsers[identifier].byte_json_adapter) + return lambda data, parse=parser_builder.parse, build=parser_builder.build: parse(build(data)) + except KeyError: + return None + + @classmethod + def get_parser(cls, identifier: types.ResponseType) -> Parser | None: + """Get a globally defined parser for the given ID. + + Currently, only BLE uses globally defined parsers + + Args: + identifier (types.ResponseType): ID to get parser for + + Returns: + Optional[Parser]: parser if found, else None + """ + return cls._global_parsers.get(identifier) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/__init__.py index 99224b47..825209bf 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/__init__.py @@ -1,28 +1,52 @@ # __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). # This copyright was auto-generated on Wed, Sep 1, 2021 5:05:49 PM -"""All Protobuf defnitions.""" +"""Protobufs that will be needed by the user. + +They are imported here so they can be imported from open_gopro.proto +""" -from open_gopro.proto.turbo_transfer_pb2 import RequestSetTurboActive -from open_gopro.proto.response_generic_pb2 import ResponseGeneric -from open_gopro.proto.request_get_preset_status_pb2 import RequestGetPresetStatus -from open_gopro.proto.preset_status_pb2 import NotifyPresetStatus -from open_gopro.proto.set_camera_control_status_pb2 import RequestSetCameraControlStatus from open_gopro.proto.live_streaming_pb2 import ( - RequestSetLiveStreamMode, - RequestGetLiveStreamStatus, + EnumLens, + EnumLiveStreamStatus, + EnumRegisterLiveStreamStatus, + EnumWindowSize, NotifyLiveStreamStatus, + RequestGetLiveStreamStatus, + RequestSetLiveStreamMode, ) from open_gopro.proto.network_management_pb2 import ( - RequestConnectNew, - ResponseConnectNew, - RequestConnect, - ResponseConnect, + EnumNetworkOwner, + EnumProvisioning, + EnumScanEntryFlags, + EnumScanning, NotifProvisioningState, - RequestStartScan, - ResponseStartScanning, - RequestGetApEntries, - ResponseGetApEntries, NotifStartScanning, + RequestConnect, + RequestConnectNew, + RequestGetApEntries, RequestReleaseNetwork, + RequestStartScan, + ResponseConnect, + ResponseConnectNew, + ResponseGetApEntries, + ResponseStartScanning, + ScanEntry, +) +from open_gopro.proto.preset_status_pb2 import ( + EnumPresetGroup, + NotifyPresetStatus, + Preset, + PresetGroup, + PresetSetting, ) +from open_gopro.proto.request_get_preset_status_pb2 import ( + EnumRegisterPresetStatus, + RequestGetPresetStatus, +) +from open_gopro.proto.response_generic_pb2 import EnumResultGeneric, ResponseGeneric +from open_gopro.proto.set_camera_control_status_pb2 import ( + EnumCameraControlStatus, + RequestSetCameraControlStatus, +) +from open_gopro.proto.turbo_transfer_pb2 import RequestSetTurboActive diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/live_streaming_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/live_streaming_pb2.py index 4a39d4df..324426ab 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/live_streaming_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/live_streaming_pb2.py @@ -1,11 +1,11 @@ # live_streaming_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Jun 12 19:49:21 UTC 2023 +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/live_streaming_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/live_streaming_pb2.pyi index 199ed529..4d310084 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/live_streaming_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/live_streaming_pb2.pyi @@ -47,34 +47,60 @@ class _EnumLiveStreamErrorEnumTypeWrapper( ): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor LIVE_STREAM_ERROR_NONE: _EnumLiveStreamError.ValueType + "No error (success)" LIVE_STREAM_ERROR_NETWORK: _EnumLiveStreamError.ValueType + "General network error during the stream" LIVE_STREAM_ERROR_CREATESTREAM: _EnumLiveStreamError.ValueType + "Startup error: bad URL or valid with live stream server" LIVE_STREAM_ERROR_OUTOFMEMORY: _EnumLiveStreamError.ValueType + "Not enough memory on camera to complete task" LIVE_STREAM_ERROR_INPUTSTREAM: _EnumLiveStreamError.ValueType + "Failed to get stream from low level camera system" LIVE_STREAM_ERROR_INTERNET: _EnumLiveStreamError.ValueType + "No internet access detected on startup of streamer" LIVE_STREAM_ERROR_OSNETWORK: _EnumLiveStreamError.ValueType + "Error occured in linux networking stack. usually means the server closed the connection" LIVE_STREAM_ERROR_SELECTEDNETWORKTIMEOUT: _EnumLiveStreamError.ValueType + "Timed out attemping to connect to the wifi network when attemping live stream" LIVE_STREAM_ERROR_SSL_HANDSHAKE: _EnumLiveStreamError.ValueType + "SSL handshake failed (commonly caused due to incorrect time / time zone)" LIVE_STREAM_ERROR_CAMERA_BLOCKED: _EnumLiveStreamError.ValueType + "Low level camera system rejected attempt to start live stream" LIVE_STREAM_ERROR_UNKNOWN: _EnumLiveStreamError.ValueType + "Unknown" LIVE_STREAM_ERROR_SD_CARD_FULL: _EnumLiveStreamError.ValueType + "Can not perform livestream because sd card is full" LIVE_STREAM_ERROR_SD_CARD_REMOVED: _EnumLiveStreamError.ValueType + "Livestream stopped because sd card was removed" class EnumLiveStreamError(_EnumLiveStreamError, metaclass=_EnumLiveStreamErrorEnumTypeWrapper): ... LIVE_STREAM_ERROR_NONE: EnumLiveStreamError.ValueType +"No error (success)" LIVE_STREAM_ERROR_NETWORK: EnumLiveStreamError.ValueType +"General network error during the stream" LIVE_STREAM_ERROR_CREATESTREAM: EnumLiveStreamError.ValueType +"Startup error: bad URL or valid with live stream server" LIVE_STREAM_ERROR_OUTOFMEMORY: EnumLiveStreamError.ValueType +"Not enough memory on camera to complete task" LIVE_STREAM_ERROR_INPUTSTREAM: EnumLiveStreamError.ValueType +"Failed to get stream from low level camera system" LIVE_STREAM_ERROR_INTERNET: EnumLiveStreamError.ValueType +"No internet access detected on startup of streamer" LIVE_STREAM_ERROR_OSNETWORK: EnumLiveStreamError.ValueType +"Error occured in linux networking stack. usually means the server closed the connection" LIVE_STREAM_ERROR_SELECTEDNETWORKTIMEOUT: EnumLiveStreamError.ValueType +"Timed out attemping to connect to the wifi network when attemping live stream" LIVE_STREAM_ERROR_SSL_HANDSHAKE: EnumLiveStreamError.ValueType +"SSL handshake failed (commonly caused due to incorrect time / time zone)" LIVE_STREAM_ERROR_CAMERA_BLOCKED: EnumLiveStreamError.ValueType +"Low level camera system rejected attempt to start live stream" LIVE_STREAM_ERROR_UNKNOWN: EnumLiveStreamError.ValueType +"Unknown" LIVE_STREAM_ERROR_SD_CARD_FULL: EnumLiveStreamError.ValueType +"Can not perform livestream because sd card is full" LIVE_STREAM_ERROR_SD_CARD_REMOVED: EnumLiveStreamError.ValueType +"Livestream stopped because sd card was removed" global___EnumLiveStreamError = EnumLiveStreamError class _EnumLiveStreamStatus: @@ -86,22 +112,36 @@ class _EnumLiveStreamStatusEnumTypeWrapper( ): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor LIVE_STREAM_STATE_IDLE: _EnumLiveStreamStatus.ValueType + "Initial status. Livestream has not yet been configured" LIVE_STREAM_STATE_CONFIG: _EnumLiveStreamStatus.ValueType + "Livestream is being configured" LIVE_STREAM_STATE_READY: _EnumLiveStreamStatus.ValueType + "Livestream has finished configuration and is ready to start streaming" LIVE_STREAM_STATE_STREAMING: _EnumLiveStreamStatus.ValueType + "Livestream is actively streaming" LIVE_STREAM_STATE_COMPLETE_STAY_ON: _EnumLiveStreamStatus.ValueType + "Live stream is exiting. No errors occured." LIVE_STREAM_STATE_FAILED_STAY_ON: _EnumLiveStreamStatus.ValueType + "Live stream is exiting. An error occurred." LIVE_STREAM_STATE_RECONNECTING: _EnumLiveStreamStatus.ValueType + "An error occurred during livestream and stream is attempting to reconnect." class EnumLiveStreamStatus(_EnumLiveStreamStatus, metaclass=_EnumLiveStreamStatusEnumTypeWrapper): ... LIVE_STREAM_STATE_IDLE: EnumLiveStreamStatus.ValueType +"Initial status. Livestream has not yet been configured" LIVE_STREAM_STATE_CONFIG: EnumLiveStreamStatus.ValueType +"Livestream is being configured" LIVE_STREAM_STATE_READY: EnumLiveStreamStatus.ValueType +"Livestream has finished configuration and is ready to start streaming" LIVE_STREAM_STATE_STREAMING: EnumLiveStreamStatus.ValueType +"Livestream is actively streaming" LIVE_STREAM_STATE_COMPLETE_STAY_ON: EnumLiveStreamStatus.ValueType +"Live stream is exiting. No errors occured." LIVE_STREAM_STATE_FAILED_STAY_ON: EnumLiveStreamStatus.ValueType +"Live stream is exiting. An error occurred." LIVE_STREAM_STATE_RECONNECTING: EnumLiveStreamStatus.ValueType +"An error occurred during livestream and stream is attempting to reconnect." global___EnumLiveStreamStatus = EnumLiveStreamStatus class _EnumRegisterLiveStreamStatus: @@ -292,9 +332,7 @@ class RequestGetLiveStreamStatus(google.protobuf.message.Message): *, register_live_stream_status: collections.abc.Iterable[global___EnumRegisterLiveStreamStatus.ValueType] | None = ..., - unregister_live_stream_status: collections.abc.Iterable[ - global___EnumRegisterLiveStreamStatus.ValueType - ] + unregister_live_stream_status: collections.abc.Iterable[global___EnumRegisterLiveStreamStatus.ValueType] | None = ... ) -> None: ... def ClearField( diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/network_management_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/network_management_pb2.py index 42f7ebef..1a3c5c69 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/network_management_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/network_management_pb2.py @@ -1,11 +1,11 @@ # network_management_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Jun 12 19:49:21 UTC 2023 +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder _sym_db = _symbol_database.Default() from . import response_generic_pb2 as response__generic__pb2 diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/network_management_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/network_management_pb2.pyi index b7eae6b2..5a385253 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/network_management_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/network_management_pb2.pyi @@ -222,10 +222,7 @@ class RequestConnect(google.protobuf.message.Message): "Deprecated" def __init__( - self, - *, - ssid: builtins.str | None = ..., - owner_purpose: global___EnumNetworkOwner.ValueType | None = ... + self, *, ssid: builtins.str | None = ..., owner_purpose: global___EnumNetworkOwner.ValueType | None = ... ) -> None: ... def HasField( self, field_name: typing_extensions.Literal["owner_purpose", b"owner_purpose", "ssid", b"ssid"] @@ -338,7 +335,7 @@ class RequestGetApEntries(google.protobuf.message.Message): max_entries: builtins.int "Used for paging. Value must be < NotifStartScanning.total_entries" scan_id: builtins.int - "ID corresponding to a set of scan results (i.e." + "ID corresponding to a set of scan results (i.e. NotifStartScanning.scan_id)" def __init__( self, @@ -550,9 +547,7 @@ class ResponseGetApEntries(google.protobuf.message.Message): ) -> builtins.bool: ... def ClearField( self, - field_name: typing_extensions.Literal[ - "entries", b"entries", "result", b"result", "scan_id", b"scan_id" - ], + field_name: typing_extensions.Literal["entries", b"entries", "result", b"result", "scan_id", b"scan_id"], ) -> None: ... global___ResponseGetApEntries = ResponseGetApEntries diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/preset_status_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/preset_status_pb2.py index 0d54bada..2f94fa30 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/preset_status_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/preset_status_pb2.py @@ -1,11 +1,11 @@ # preset_status_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Jun 12 19:49:21 UTC 2023 +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/preset_status_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/preset_status_pb2.pyi index b23ee58f..5a286fb0 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/preset_status_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/preset_status_pb2.pyi @@ -407,9 +407,7 @@ class NotifyPresetStatus(google.protobuf.message.Message): self, ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___PresetGroup]: """Array of Preset Groups""" - def __init__( - self, *, preset_group_array: collections.abc.Iterable[global___PresetGroup] | None = ... - ) -> None: ... + def __init__(self, *, preset_group_array: collections.abc.Iterable[global___PresetGroup] | None = ...) -> None: ... def ClearField( self, field_name: typing_extensions.Literal["preset_group_array", b"preset_group_array"] ) -> None: ... @@ -539,9 +537,7 @@ class PresetGroup(google.protobuf.message.Message): ) -> None: ... def HasField( self, - field_name: typing_extensions.Literal[ - "can_add_preset", b"can_add_preset", "icon", b"icon", "id", b"id" - ], + field_name: typing_extensions.Literal["can_add_preset", b"can_add_preset", "icon", b"icon", "id", b"id"], ) -> builtins.bool: ... def ClearField( self, @@ -565,11 +561,7 @@ class PresetSetting(google.protobuf.message.Message): 'Does this setting appear on the Preset "pill" in the camera UI?' def __init__( - self, - *, - id: builtins.int | None = ..., - value: builtins.int | None = ..., - is_caption: builtins.bool | None = ... + self, *, id: builtins.int | None = ..., value: builtins.int | None = ..., is_caption: builtins.bool | None = ... ) -> None: ... def HasField( self, diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/request_get_preset_status_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/request_get_preset_status_pb2.py index ba2c110d..6c3a98c1 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/request_get_preset_status_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/request_get_preset_status_pb2.py @@ -1,11 +1,11 @@ # request_get_preset_status_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Jun 12 19:49:21 UTC 2023 +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/request_get_preset_status_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/request_get_preset_status_pb2.pyi index a5d78e48..d6d8f750 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/request_get_preset_status_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/request_get_preset_status_pb2.pyi @@ -29,14 +29,16 @@ class _EnumRegisterPresetStatusEnumTypeWrapper( ): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor REGISTER_PRESET_STATUS_PRESET: _EnumRegisterPresetStatus.ValueType + "Send notification when properties of a preset change" REGISTER_PRESET_STATUS_PRESET_GROUP_ARRAY: _EnumRegisterPresetStatus.ValueType + "Send notification when properties of a preset group change" -class EnumRegisterPresetStatus( - _EnumRegisterPresetStatus, metaclass=_EnumRegisterPresetStatusEnumTypeWrapper -): ... +class EnumRegisterPresetStatus(_EnumRegisterPresetStatus, metaclass=_EnumRegisterPresetStatusEnumTypeWrapper): ... REGISTER_PRESET_STATUS_PRESET: EnumRegisterPresetStatus.ValueType +"Send notification when properties of a preset change" REGISTER_PRESET_STATUS_PRESET_GROUP_ARRAY: EnumRegisterPresetStatus.ValueType +"Send notification when properties of a preset group change" global___EnumRegisterPresetStatus = EnumRegisterPresetStatus class RequestGetPresetStatus(google.protobuf.message.Message): @@ -47,24 +49,18 @@ class RequestGetPresetStatus(google.protobuf.message.Message): @property def register_preset_status( self, - ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ - global___EnumRegisterPresetStatus.ValueType - ]: + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___EnumRegisterPresetStatus.ValueType]: """Array of Preset statuses to be notified about""" @property def unregister_preset_status( self, - ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ - global___EnumRegisterPresetStatus.ValueType - ]: + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___EnumRegisterPresetStatus.ValueType]: """Array of Preset statuses to stop being notified about""" def __init__( self, *, - register_preset_status: collections.abc.Iterable[global___EnumRegisterPresetStatus.ValueType] - | None = ..., - unregister_preset_status: collections.abc.Iterable[global___EnumRegisterPresetStatus.ValueType] - | None = ... + register_preset_status: collections.abc.Iterable[global___EnumRegisterPresetStatus.ValueType] | None = ..., + unregister_preset_status: collections.abc.Iterable[global___EnumRegisterPresetStatus.ValueType] | None = ... ) -> None: ... def ClearField( self, diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/response_generic_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/response_generic_pb2.py index 6e93dd10..9e6f018a 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/response_generic_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/response_generic_pb2.py @@ -1,11 +1,11 @@ # response_generic_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Jun 12 19:49:21 UTC 2023 +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/set_camera_control_status_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/set_camera_control_status_pb2.py index 041b01f2..5892f377 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/set_camera_control_status_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/set_camera_control_status_pb2.py @@ -1,11 +1,11 @@ # set_camera_control_status_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Jun 12 19:49:21 UTC 2023 +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/set_camera_control_status_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/set_camera_control_status_pb2.pyi index 4accb045..c7aba5a7 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/set_camera_control_status_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/set_camera_control_status_pb2.pyi @@ -45,9 +45,7 @@ class RequestSetCameraControlStatus(google.protobuf.message.Message): camera_control_status: global___EnumCameraControlStatus.ValueType "Declare who is taking control of the camera" - def __init__( - self, *, camera_control_status: global___EnumCameraControlStatus.ValueType | None = ... - ) -> None: ... + def __init__(self, *, camera_control_status: global___EnumCameraControlStatus.ValueType | None = ...) -> None: ... def HasField( self, field_name: typing_extensions.Literal["camera_control_status", b"camera_control_status"] ) -> builtins.bool: ... diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/turbo_transfer_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/turbo_transfer_pb2.py index 62212e00..d8e0191f 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/turbo_transfer_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/turbo_transfer_pb2.py @@ -1,11 +1,11 @@ # turbo_transfer_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Jun 12 19:49:21 UTC 2023 +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/responses.py b/demos/python/sdk_wireless_camera_control/open_gopro/responses.py deleted file mode 100644 index f5db817b..00000000 --- a/demos/python/sdk_wireless_camera_control/open_gopro/responses.py +++ /dev/null @@ -1,777 +0,0 @@ -# responses.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Wed, Sep 1, 2021 5:05:49 PM - -"""Any responses that are returned from GoPro commands.""" - -from __future__ import annotations -from abc import abstractmethod, ABC -import enum -import logging -from collections import defaultdict -from typing import ( - Any, - Optional, - Union, - Iterator, - ItemsView, - ValuesView, - KeysView, - Final, - ClassVar, - Protocol, - TypeVar, - Generic, - Callable, -) - -import requests - -from open_gopro.exceptions import ResponseParseError -from open_gopro.constants import ( - ActionId, - FeatureId, - GoProEnum, - StatusId, - CmdId, - ErrorCode, - SettingId, - QueryCmdId, - ResponseType, - CmdType, - GoProUUIDs, -) -from open_gopro.util import scrub, jsonify -from open_gopro.ble import BleUUID -from open_gopro.proto.response_generic_pb2 import EnumResultGeneric - -logger = logging.getLogger(__name__) - -CONT_MASK: Final = 0b10000000 -HDR_MASK: Final = 0b01100000 -GEN_LEN_MASK: Final = 0b00011111 -EXT_13_BYTE0_MASK: Final = 0b00011111 - -T_co = TypeVar("T_co", covariant=True) -T = TypeVar("T") - - -class Parser(ABC, Generic[T, T_co]): - """Base Parser Interface - - Supports addition to other Parser's - """ - - def __init__(self) -> None: - self._parsers: list[Callable] = [self.parse] - - def _combined_parse(self, data: T) -> T_co: - """Parse data using all parsers serially - - Args: - data (T): input data to parse - - Returns: - T_co: parsed output - """ - response = self._parsers[0](data) - for parser in self._parsers[1:]: - response = parser(response) - return response - - @abstractmethod - def parse(self, data: T) -> T_co: # pylint: disable=method-hidden - """Initial (and potentially only) parser method - - Args: - data (T): input data to parse - - - - Returns: - T_co: parsed output - """ - raise NotImplementedError - - def __add__(self, other: Parser) -> Parser: - if not isinstance(other, Parser): - raise TypeError("Can only add Parser's") - self._parsers.append(other.parse) - self.parse = self._combined_parse # type: ignore - return self - - def __radd__(self, other: Parser) -> Parser: - return other.__add__(self) - - -class BytesParser(Parser[bytes, T_co]): - """Bytes parser protocol to be used by non-construct parsers""" - - @abstractmethod - def parse(self, data: bytes) -> T_co: - """Parse input bytes to output - - Args: - data (bytes): data to parsed - - Returns: - T_co: parsed output - """ - raise NotImplementedError - - -class BytesBuilder(Protocol): - """Base bytes serializer protocol definition""" - - def build(self, obj: Any) -> bytes: - """Build bytestream from object - - # noqa: DAR202 - - Args: - obj (Any): object to serialize - - Returns: - bytes: serialized bytestream - """ - - -class BytesParserBuilder(BytesParser[T_co], BytesBuilder): - """Class capable of both building / parsing bytes to / from object""" - - @abstractmethod - def parse(self, data: bytes) -> T_co: - """Parse input bytes to output - - Args: - data (bytes): data to parsed - - Returns: - T_co: parsed output - """ - raise NotImplementedError - - @abstractmethod - def build(self, obj: Any) -> bytes: - """Build bytestream from object - - # noqa: DAR202 - - Args: - obj (Any): object to serialize - - Returns: - bytes: serialized bytestream - """ - - -class JsonParser(Parser[dict, dict]): - """The JSON Parser interface""" - - @abstractmethod - def parse(self, data: dict) -> dict: - """Parse input json to modified output json - - Args: - data (dict): data to parsed - - Returns: - dict: parsed output - """ - raise NotImplementedError - - -class Header(enum.Enum): - """Packet Headers.""" - - GENERAL = 0b00 - EXT_13 = 0b01 - EXT_16 = 0b10 - RESERVED = 0b11 - CONT = enum.auto() - - -class GoProResp: - """A flexible object to be used to encapsulate all GoPro responses. - - It can be instantiated with varying levels of information and filled out as more is received. - The end user should never need to create a response and will only be consuming them. - - It is mostly a wrapper around a JSON-like dictionary (GoProResp.data) - For example, first send the command and store the response: - - >>> response = ble_setting.resolution.get_value() - - Now let's inspect the responses various attributes / properties: - - >>> print(response.status) - ErrorCode.SUCCESS - >>> print(response.is_ok) - True - >>> print(response.id) - QueryCmdId.GET_SETTING_VAL - >>> print(response.cmd) - QueryCmdId.GET_SETTING_VAL - >>> print(response.uuid) - GoProUUIDs.CQ_QUERY_RESP - - Now let's print it as (as JSON): - - >>> print(response.data) - { - "status": "SUCCESS", - "id": "QueryCmdId.GET_SETTING_VAL", - "SettingId.RESOLUTION": [ - "RES_1080" - ] - } - """ - - _feature_action_id_map: ClassVar[dict[FeatureId, list[ActionId]]] = defaultdict(list) - _global_parsers: ClassVar[dict[ResponseType, Parser]] = {} - - class _State(enum.Enum): - """Describes the state of the GoProResp.""" - - INITIALIZED = enum.auto() - ACCUMULATED = enum.auto() - PARSED = enum.auto() - ERROR = enum.auto() - - class Protocol(enum.Enum): - """Protocol that Command will be sent on.""" - - BLE = "BLE" - HTTP = "HTTP" - - def __init__( - self, - meta: list[ResponseType], - parser: Optional[Parser] = None, - status: ErrorCode = ErrorCode.SUCCESS, - raw_packet: Optional[Union[bytearray, dict[str, Any]]] = None, - ) -> None: - """Constructor - - Args: - meta (list[ResponseType]): A list of all information known about the response. - parser (Optional[Parser]): Optional parser. If not passed, parser will be found from global - parsers - status (ErrorCode): A status if known at time of instantiation. Defaults to ErrorCode.SUCCESS. - raw_packet (Optional[Union[bytearray, dict[str, Any]]]): The unparsed input if known at time of - instantiation. Defaults to None. - """ - # A list describing all of the currently known information about the response. - # This will be appended to as more information is discovered. The various properties of GoProResp will - # use this list to parse out their relevant information. - self._meta: list[ResponseType] = meta - # Parsers to use to parse this response - self._parser = parser - - self.status: ErrorCode = status - """Status of the response""" - - # Start with empty list as default value in case we need to append. - # If we end up not needing a list, we will just overwrite the default - self.data: dict[Any, Any] = defaultdict(list) - """Response data which is really JSON data stored as a dict""" - - self._raw_packet = raw_packet - self._bytes_remaining = 0 - self._state: GoProResp._State = GoProResp._State.INITIALIZED - - @classmethod - def _from_read_response(cls, uuid: BleUUID, data: bytearray) -> GoProResp: - """Build a GoProResp from a read response. - - Args: - uuid (BleUUID): BleUUID that read command was received from - data (bytearray): received bytestream - - Returns: - GoProResp: created response instance - """ - resp = cls(meta=[uuid], status=ErrorCode.SUCCESS, raw_packet=data) - resp._parse() - return resp - - @classmethod - def _from_http_response( - cls, parser: Optional[JsonParser], response: requests.models.Response - ) -> GoProResp: - """Build a GoProResp from an HTTP response from the requests package. - - Args: - parser (Optional[JsonParser]): parsers to use to parse received data - response (requests.models.Response): HTTP response - - Returns: - GoProResp: created response instance - """ - resp = cls( - meta=[response.url], - parser=parser, - status=ErrorCode.SUCCESS if response.ok else ErrorCode.ERROR, - raw_packet=response.json() if response.text else {}, - ) - resp._parse() - return resp - - @classmethod - def _from_stream_response(cls, response: requests.models.Response) -> GoProResp: - """Build a GoProResp from an HTTP response that read binary data - - Args: - response (requests.models.Response): HTTP response (that has already been consumed) - - Returns: - GoProResp: created response instance - """ - resp = cls( - meta=[response.url], - parser=None, - status=ErrorCode.SUCCESS if response.ok else ErrorCode.ERROR, - raw_packet={}, - ) - resp._parse() - return resp - - @classmethod - def _get_response_meta(cls, data: bytes, uuid: BleUUID) -> list[ResponseType]: - """Get a response's meta information from raw bytes and the UUID it was received on - - Args: - data (bytes): bytes received as BLE notification - uuid (BleUUID): UUID response was received on - - Returns: - list[ResponseType]: List of meta information in order from least to most specific - """ - meta: list[ResponseType] = [uuid] - - # If it's a protobuf command - identifier = data[0] - try: - FeatureId(identifier) - meta.append(ActionId(data[1])) - # Otherwise it's a TLV command - except ValueError: - if uuid is GoProUUIDs.CQ_SETTINGS_RESP: - meta.append(SettingId(identifier)) - elif uuid is GoProUUIDs.CQ_QUERY_RESP: - meta.append(QueryCmdId(identifier)) - elif uuid in [GoProUUIDs.CQ_COMMAND_RESP, GoProUUIDs.CN_NET_MGMT_RESP]: - meta.append(CmdId(identifier)) - # Else case is direct so the UUID (which is already in meta) is the identifier - return meta - - @classmethod - def _add_feature_action_id_mapping(cls, feature_id: FeatureId, action_id: ActionId) -> None: - """Add a feature id-to-action id mapping entry - - Args: - feature_id (FeatureId): Feature ID of protobuf command - action_id (ActionId): Action ID of protobuf command - """ - cls._feature_action_id_map[feature_id].append(action_id) - - @classmethod - def _add_global_parser(cls, identifier: ResponseType, parser: Parser) -> None: - """Add a global parser that can be accessed by this class's class methods - - Args: - identifier (ResponseType): identifier to add parser for - parser (Parser): parser to add - """ - cls._global_parsers[identifier] = parser - - @classmethod - def _get_query_container(cls, identifier: Union[SettingId, StatusId]) -> Optional[Callable]: - """Attempt to get a callable that will translate an input value to the ID-appropriate value. - - For example, _get_query_container(SettingId.RESOLUTION) will return - :py:class:`open_gopro.api.params.Resolution` - - As another example, _get_query_container(StatusId.TURBO_MODE) will return bool() - - Note! Not all ID's are currently parsed so None will be returned if the container does not exist - - Args: - identifier (Union[SettingId, StatusId]): identifier to find container for - - Returns: - Callable: container if found else None - """ - try: - return lambda x, pb=cls._global_parsers[identifier]: pb.parse(pb.build(x)) - except KeyError: - return None - - @classmethod - def _get_global_parser(cls, identifier: ResponseType) -> Optional[Parser]: - """Get a globally defined parser for the given ID. - - Currently, only BLE uses globally defined parsers - - Args: - identifier (ResponseType): ID to get parser for - - Returns: - Optional[Parser]: parser if found, else None - """ - return cls._global_parsers.get(identifier) - - def __eq__(self, obj: object) -> bool: - if isinstance(obj, GoProEnum): - return self.identifier == obj - if isinstance(obj, GoProResp): - return self.identifier == obj.identifier - raise TypeError("Equal can only compare GoProResp and ResponseType") - - def __getitem__(self, key: Any) -> Any: - return self.data[key] - - def __contains__(self, item: Any) -> bool: - return item in self.data - - def __iter__(self) -> Iterator: - return iter(self.data) - - def __str__(self) -> str: - return jsonify(self._as_dict()) - - def _as_dict(self) -> dict[str, Any]: - """Return the response as a dict - - Returns: - dict[str, Any]: response as dict - """ - work_dict = {"id": self.identifier, "protocol": self.protocol.name, "status": self.status.name} - if self.cmd: - work_dict["command"] = self.cmd - if self.uuid: - work_dict["uuid"] = self.uuid - if self.endpoint: - work_dict["endpoint"] = self.endpoint - return {**work_dict, **self.data} - - def __repr__(self) -> str: - return f"GoProResp <{str(self.identifier)}: {self._state}>" - - def items(self) -> ItemsView[Any, Any]: - """Pass-through to access data dict's "items" method - - Returns: - ItemsView[Any, Any]: all of data's keys - """ - return self.data.items() - - def keys(self) -> KeysView[Any]: - """Pass-through to access data dict's "keys" method - - Returns: - ItemsView[Any, Any]: all of data's items - """ - return self.data.keys() - - def values(self) -> ValuesView[Any]: - """Pass-through to access data dict's "values" method - - Returns: - ItemsView[Any, Any]: all of data's values - """ - return self.data.values() - - @property - def protocol(self) -> GoProResp.Protocol: - """Get the protocol that the response was received on - - Returns: - GoProResp.Protocol: protocol - """ - return GoProResp.Protocol.BLE if self.uuid else GoProResp.Protocol.HTTP - - @property - def is_received(self) -> bool: - """Has the response been completely received? - - Returns: - bool: True if completely received, False if not - """ - return self._state is not GoProResp._State.INITIALIZED - - @property - def is_parsed(self) -> bool: - """Has the response been successfully parsed? - - Returns: - bool: True if it has been parsed, False if not - """ - return self._state is GoProResp._State.PARSED - - @property - def is_ok(self) -> bool: - """Are there any errors in this response? - - Returns: - bool: True if the response is ok (i.e. there are no errors), False otherwise - """ - return self.status in [ErrorCode.SUCCESS, ErrorCode.UNKNOWN] - - @property - def identifier(self) -> ResponseType: - """Get the identifier of the response. - - Will vary depending on what type of response this is: - - - for a direct BLE read / write to a characteristic, it will be a :py:class:`open_gopro.ble.services.BleUUID` - - for a BLE command response, it will be a :py:class:`open_gopro.constants.CmdType` - - for a BLE setting / status response / update, it will be a :py:class:`open_gopro.constants.SettingId` - or :py:class:`open_gopro.constants.StatusId` - - for an HTTP response, it will be a string of the HTTP endpoint - - Returns: - ResponseType: the identifier - """ - return self._meta[-1] - - @property - def cmd(self) -> Optional[CmdType]: - """Attempt to get the command ID of the response. - - If the response is not a command response, it won't have a command. - - Returns: - Optional[CmdType]: Command ID if relevant, otherwise None - """ - for x in self._meta: - if isinstance(x, (QueryCmdId, CmdId)): - return x - return None - - @property - def uuid(self) -> Optional[BleUUID]: - """Attempt to get the BleUUID the response was received on. - - If the response is not a BLE response, it won't have a BleUUID. - - Returns: - Optional[BleUUID]: BleUUID if relevant, otherwise None - """ - for x in self._meta: - if isinstance(x, BleUUID): - return x - return None - - @property - def endpoint(self) -> Optional[str]: - """Attempt to get the endpoint that response was received from. - - If the response is not an HTTP response, it won't have an endpoint. - - Returns: - Optional[str]: Endpoint if relevant, otherwise None - """ - for x in self._meta: - if isinstance(x, str): - return x - return None - - @property - def is_protobuf(self) -> bool: - """Is this response a protobuf response - - Returns: - bool: Yes if true, No otherwise - """ - return bool({ActionId, FeatureId}.intersection({type(x) for x in self._meta})) - - @property - def is_direct_read(self) -> bool: - """Is this response a direct read of a BLE characteristic - - Returns: - bool: Yes if true, No otherwise - """ - return len(self._meta) == 1 and isinstance(self.identifier, BleUUID) - - @property - def flatten(self) -> Any: - """Attempt to flatten / simplify the JSON response (GoProResp.data). - - - If there is only one entry in the JSON dict and it is a single value, return the value - - If there is only one entry in the JSON dict and it is a list of values, return the list - - Otherwise, just return the JSON dict - - Returns: - Any: A single value, a list of values, or the JSON dict - """ - values = list(self.data.values()) - if len(values) == 1 and type(values[0] not in [list, dict]): - return values[0] - - if len(values) == 1 and type(values[0] is list): - return values - - return self.data - - # to be @overload'ed if anything besides BLE notifications need to be accumulated - def _accumulate(self, data: bytes) -> None: - """Accumulate BLE byte data. - - If there is no more data left to accumulate, the bytestream will be parsed. - - Args: - data (bytes): byte level BLE data - """ - buf = bytearray(data) - if buf[0] & CONT_MASK: - buf.pop(0) - else: - # This is a new packet so start with an empty byte array - self._raw_packet = bytearray([]) - hdr = Header((buf[0] & HDR_MASK) >> 5) - if hdr is Header.GENERAL: - self._bytes_remaining = buf[0] & GEN_LEN_MASK - buf = buf[1:] - elif hdr is Header.EXT_13: - self._bytes_remaining = ((buf[0] & EXT_13_BYTE0_MASK) << 8) + buf[1] - buf = buf[2:] - elif hdr is Header.EXT_16: - self._bytes_remaining = (buf[1] << 8) + buf[2] - buf = buf[3:] - - # Append payload to buffer and update remaining / complete - assert isinstance(self._raw_packet, bytearray) - self._raw_packet.extend(buf) - self._bytes_remaining -= len(buf) - - if self._bytes_remaining < 0: - logger.error("received too much data. parsing is in unknown state") - elif self._bytes_remaining == 0: - self._state = GoProResp._State.ACCUMULATED - - def _parse(self) -> None: - """Parse the accumulated response (either from a BLE bytestream or an HTTP JSON dict). - - Raises: - NotImplementedError: Parsing for this id is not yet supported - ResponseParseError: Error when parsing data - RuntimeError: unexpected state - """ - # Is this a BLE response? - if isinstance(self._raw_packet, bytearray): - assert self.uuid - buf: bytearray = self._raw_packet - self._meta = self._get_response_meta(buf, self.uuid) - - if not self.is_direct_read: # length byte - buf.pop(0) - if self.is_protobuf: # feature ID byte - buf.pop(0) - - try: - query_type: Optional[Union[type[StatusId], type[SettingId], StatusId, SettingId]] = None - # Need to delineate QueryCmd responses between settings and status - if not self.is_protobuf: - if isinstance(self.identifier, (SettingId, StatusId)): - query_type = self.identifier - elif isinstance(self.identifier, QueryCmdId): - if self.identifier in [ - QueryCmdId.GET_STATUS_VAL, - QueryCmdId.REG_STATUS_VAL_UPDATE, - QueryCmdId.UNREG_STATUS_VAL_UPDATE, - QueryCmdId.STATUS_VAL_PUSH, - ]: - query_type = StatusId - elif self.identifier is QueryCmdId.GET_SETTING_NAME: - raise NotImplementedError - else: - query_type = SettingId - - # Query (setting get value, status get value, etc.) - if query_type: - self.status = ErrorCode(buf[0]) - buf = buf[1:] - # Parse all parameters - while len(buf) != 0: - param_id = query_type(buf[0]) # type: ignore - param_len = buf[1] - buf = buf[2:] - # Special case where we register for a push notification for something that does not yet - # have a value - if param_len == 0: - self.data[param_id] = [] - continue - param_val = buf[:param_len] - buf = buf[param_len:] - - # Add parsed value to response's data dict - try: - if not (parser := self._get_global_parser(param_id)): - # We don't have defined params for all ID's yet. Just store raw bytes - logger.warning(f"No parser defined for {param_id}") - self.data[param_id] = param_val - continue - # These can be more than 1 value so use a list - if self.cmd in [ - QueryCmdId.GET_CAPABILITIES_VAL, - QueryCmdId.REG_CAPABILITIES_UPDATE, - QueryCmdId.SETTING_CAPABILITY_PUSH, - ]: - # Parse using parser from global map and append - self.data[param_id].append(parser.parse(param_val)) - else: - # Parse using parser from map and set - self.data[param_id] = parser.parse(param_val) - except ValueError: - # This is the case where we receive a value that is not defined in our params. - # This shouldn't happen and means the documentation needs to be updated. However, it - # isn't functionally critical - logger.warning(f"{param_id} does not contain a value {param_val}") - self.data[param_id] = param_val - else: # Commands, Protobuf, and direct Reads - if is_cmd := isinstance(self.identifier, CmdId): - # All (non-protobuf) commands have a status - self.status = ErrorCode(buf[0]) - buf = buf[1:] - # Use parser if explicitly passed otherwise get global parser - if not (parser := self._get_global_parser(self.identifier) or self._parser) and not is_cmd: - error_msg = f"No parser exists for {self.identifier}" - logger.error(error_msg) - raise ResponseParseError(str(self.identifier), self._raw_packet, msg=error_msg) - # Parse payload if a parser was found. - if parser: - self.data = parser.parse(buf) - # Attempt to determine and / or extract status (we already got command status above) - if self.is_direct_read and len(self._raw_packet): - # Assume success on direct reads if there was any data - self.status = ErrorCode.SUCCESS - elif ( - self.is_protobuf and self.data and "result" in self.data - ): # Check for result field in protobuf's - self.status = ( - ErrorCode.SUCCESS - if self.data["result"] == EnumResultGeneric.RESULT_SUCCESS - else ErrorCode.ERROR - ) - elif not is_cmd: - self.status = ErrorCode.UNKNOWN - except KeyError as e: - self._state = GoProResp._State.ERROR - raise ResponseParseError(str(self.identifier), buf) from e - - # Is this an HTTP response? - elif isinstance(self._raw_packet, dict): - # Is there a parser for this? Most of them do not have one yet. - self.data = self._parser.parse(self._raw_packet) if self._parser else self._raw_packet - - # This should never happen - else: - raise RuntimeError( - f"Unexpected data type ({str(type(self._raw_packet))}) when attempting to parse response" - ) - - # Recursively scrub away parsing artifacts - scrub(self.data, "_io") - scrub(self.data, "status") - self._state = GoProResp._State.PARSED diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/types.py b/demos/python/sdk_wireless_camera_control/open_gopro/types.py new file mode 100644 index 00000000..dadaa14b --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/types.py @@ -0,0 +1,38 @@ +# types.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jul 31 17:04:07 UTC 2023 + +"""Commonly reused type aliases""" + +from __future__ import annotations + +from typing import Any, Callable, Coroutine, Union + +import construct +from google.protobuf.message import Message as Protobuf # pylint: disable=unused-import + +from open_gopro.constants import ( + ActionId, + BleUUID, + CmdId, + QueryCmdId, + SettingId, + StatusId, +) + +# Note! We need to use Union here for Python 3.9 support + +ProducerType = tuple[QueryCmdId, Union[SettingId, StatusId]] +"""Types that can be registered for.""" + +CmdType = Union[CmdId, QueryCmdId, ActionId] +"""Types that identify a command.""" + +ResponseType = Union[CmdType, StatusId, SettingId, BleUUID, str, construct.Enum] +"""Types that are used to identify a response.""" + +CameraState = dict[Union[SettingId, StatusId], Any] + +JsonDict = dict[str, Any] + +UpdateType = Union[SettingId, StatusId, ActionId] +UpdateCb = Callable[[UpdateType, Any], Coroutine[Any, Any, None]] diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/util.py b/demos/python/sdk_wireless_camera_control/open_gopro/util.py index 15cfe3e1..4a370883 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/util.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/util.py @@ -4,19 +4,20 @@ """Miscellaneous utilities for the GoPro package.""" from __future__ import annotations -import sys + +import argparse +import asyncio import enum -import json -import queue import logging -import argparse import subprocess +import sys +from datetime import datetime from pathlib import Path -import http.client as http_client -from typing import Any, Optional, Union, Final, Callable +from typing import Any, Callable, Generic, TypeVar -from rich.logging import RichHandler -from rich import traceback +import pytz +from pydantic import BaseModel +from tzlocal import get_localzone util_logger = logging.getLogger(__name__) @@ -32,258 +33,6 @@ def __new__(cls, *_: Any) -> Any: # noqa https://github.com/PyCQA/pydocstyle/is return cls._instances[cls] -class Logger: - """A singleton class to manage logging for the Open GoPro internal modules - - Args: - logger (logging.Logger): input logger that will be modified and then returned - output (Path, Optional): Path of log file for file stream handler. If not set, will not log to file. - modules (dict[str, int], Optional): Optional override of modules / levels. Will be merged into default - modules. - """ - - _instances: dict[type[Logger], Logger] = {} - ARROW_HEAD_COUNT: Final = 8 - ARROW_TAIL_COUNT: Final = 14 - - def __new__(cls, *_: Any) -> Any: # noqa https://github.com/PyCQA/pydocstyle/issues/515 - if cls not in cls._instances: - c = object.__new__(cls) - cls._instances[cls] = c - return c - raise RuntimeError("The logger can only be setup once and this should be done at the top level.") - - def __init__( - self, - logger: Any, - output: Optional[Path] = None, - modules: Optional[dict[str, int]] = None, - ) -> None: - self.modules = { - "open_gopro.gopro_base": logging.DEBUG, # TRACE for raw HTTP responses - "open_gopro.gopro_wired": logging.DEBUG, # TRACE for concurrency debugging - "open_gopro.gopro_wireless": logging.DEBUG, # TRACE for concurrency debugging - "open_gopro.api.builders": logging.DEBUG, - "open_gopro.api.http_commands": logging.DEBUG, - "open_gopro.api.ble_commands": logging.DEBUG, - "open_gopro.communication_client": logging.DEBUG, - "open_gopro.ble.adapters.bleak_wrapper": logging.INFO, # DEBUG for pexpect communication - "open_gopro.ble.client": logging.DEBUG, - "open_gopro.wifi.adapters.wireless": logging.DEBUG, - "open_gopro.responses": logging.DEBUG, - "open_gopro.util": logging.INFO, - "bleak": logging.ERROR, - "urllib3": logging.WARNING, - "http.client": logging.WARNING, - } - - self.logger = logger - self.modules = {**self.modules, **modules} if modules else self.modules - self.handlers: list[logging.Handler] = [] - - # monkey-patch a `print` global into the http.client module; all calls to - # print() in that module will then use our logger's debug method - http_client.HTTPConnection.debuglevel = 1 - http_client.print = lambda *args: logging.getLogger("http.client").debug(" ".join(args)) # type: ignore - - self.file_handler: Optional[logging.Handler] - if output: - # Logging to file with millisecond timing - self.file_handler = logging.FileHandler(output, mode="w") - file_formatter = logging.Formatter( - fmt="%(threadName)13s:%(asctime)s.%(msecs)03d %(filename)-40s %(lineno)4s %(levelname)-8s | %(message)s", - datefmt="%H:%M:%S", - ) - self.file_handler.setFormatter(file_formatter) - self.file_handler.setLevel(logging.TRACE) # type: ignore # pylint: disable=no-member - logger.addHandler(self.file_handler) - self.addLoggingHandler(self.file_handler) - else: - self.file_handler = None - - # Use Rich for colorful console logging - self.stream_handler = RichHandler(rich_tracebacks=True, enable_link_path=True, show_time=False) - stream_formatter = logging.Formatter("%(asctime)s.%(msecs)03d %(message)s", datefmt="%H:%M:%S") - self.stream_handler.setFormatter(stream_formatter) - self.stream_handler.setLevel(logging.INFO) - logger.addHandler(self.stream_handler) - self.addLoggingHandler(self.stream_handler) - - self.addLoggingLevel("TRACE", logging.DEBUG - 5) - logger.setLevel(logging.TRACE) # type: ignore # pylint: disable=no-member - - traceback.install() # Enable exception tracebacks in rich logger - - @classmethod - def get_instance(cls) -> Logger: - """Get the singleton instance - - Raises: - RuntimeError: Has not yet been instantiated - - Returns: - Logger: singleton instance - """ - if not (logger := cls._instances.get(Logger, None)): - raise RuntimeError("Logging must first be setup") - return logger - - def addLoggingHandler(self, handler: logging.Handler) -> None: - """Add a handler for all of the internal GoPro modules - - Args: - handler (logging.Handler): handler to add - """ - self.logger.addHandler(handler) - self.handlers.append(handler) - - # Enable / disable logging in modules - for module, level in self.modules.items(): - l = logging.getLogger(module) - l.setLevel(level) - l.addHandler(handler) - - # From https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945 - @staticmethod - def addLoggingLevel(levelName: str, levelNum: int) -> None: - """Comprehensively adds a new logging level to the `logging` module and the currently configured logging class. - - `levelName` becomes an attribute of the `logging` module with the value - `levelNum`. `methodName` becomes a convenience method for both `logging` - itself and the class returned by `logging.getLoggerClass()` (usually just - `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is - used. - - To avoid accidental clobberings of existing attributes, this method will - raise an `AttributeError` if the level name is already an attribute of the - `logging` module or if the method name is already present - - Example: - -------- - >>> addLoggingLevel('TRACE', logging.DEBUG - 5) - >>> logging.getLogger(__name__).setLevel("TRACE") - >>> logging.getLogger(__name__).trace('that worked') - >>> logging.trace('so did this') - >>> logging.TRACE - 5 - - Args: - levelName (str): name of level (i.e. TRACE) - levelNum (int): integer level of new logging level - """ - methodName = levelName.lower() - - def logForLevel(self: Any, message: str, *args: Any, **kwargs: Any) -> None: - if self.isEnabledFor(levelNum): - self._log(levelNum, message, args, **kwargs) - - def logToRoot(message: str, *args: Any, **kwargs: Any) -> None: - logging.log(levelNum, message, *args, **kwargs) - - logging.addLevelName(levelNum, levelName) - setattr(logging, levelName, levelNum) - setattr(logging.getLoggerClass(), methodName, logForLevel) - setattr(logging, methodName, logToRoot) - - @staticmethod - def build_log_tx_str(stringable: Any) -> str: - """Build a string with Tx arrows - - Args: - stringable (Any): stringable object to surround with arrows - - Returns: - str: string surrounded by Tx arrows - """ - s = str(stringable).strip(r"{}") - arrow = f"{'<'*Logger.ARROW_HEAD_COUNT}{'-'*Logger.ARROW_TAIL_COUNT}" - return f"\n\n{arrow}{s}{arrow}\n" - - @staticmethod - def build_log_rx_str(stringable: Any, asynchronous: bool = False) -> str: - """Build a string with Rx arrows - - Args: - stringable (Any): stringable object to surround with arrows - asynchronous (bool): Should the arrows contain ASYNC?. Defaults to False. - - Returns: - str: string surrounded by Rx arrows - """ - s = str(stringable).strip(r"{}") - assert Logger.ARROW_TAIL_COUNT > 5 - if asynchronous: - arrow = f"{'-'*(Logger.ARROW_TAIL_COUNT//2-3)}ASYNC{'-'*(Logger.ARROW_TAIL_COUNT//2-2)}{'>'*Logger.ARROW_HEAD_COUNT}" - else: - arrow = f"{'-'*Logger.ARROW_TAIL_COUNT}{'>'*Logger.ARROW_HEAD_COUNT}" - return f"\n\n{arrow}{s}{arrow}\n" - - -def setup_logging( - base: Union[logging.Logger, str], output: Optional[Path] = None, modules: Optional[dict[str, int]] = None -) -> logging.Logger: - """Configure the GoPro modules for logging and get a logger that can be used by the application - - This can only be called once and should be done at the top level of the application. - - Args: - base (Union[logging.Logger, str]): Name of application (i.e. __name__) or preconfigured logger to use as base - output (Path, Optional): Path of log file for file stream handler. If not set, will not log to file. - modules (dict[str, int], Optional): Optional override of modules / levels. Will be merged into default - modules. - - Raises: - TypeError: Base logger is not of correct type - - Returns: - logging.Logger: updated logger that the application can use for logging - """ - if isinstance(base, str): - base = logging.getLogger(base) - elif not isinstance(base, logging.Logger): - raise TypeError("Base must be of type logging.Logger or str") - l = Logger(base, output, modules) - return l.logger - - -def set_file_logging_level(level: int) -> None: - """Change the global logging level for the default file output handler - - Args: - level (int): level to set - """ - if fh := Logger.get_instance().file_handler: - fh.setLevel(level) - - -def set_stream_logging_level(level: int) -> None: - """Change the global logging level for the default stream output handler - - Args: - level (int): level to set - """ - Logger.get_instance().stream_handler.setLevel(level) - - -def set_logging_level(level: int) -> None: - """Change the global logging level for the default file and stream output handlers - - Args: - level (int): level to set - """ - set_file_logging_level(level) - set_stream_logging_level(level) - - -def add_logging_handler(handler: logging.Handler) -> None: - """Add a handler to all of the GoPro internal modules - - Args: - handler (logging.Handler): handler to add - """ - Logger.get_instance().addLoggingHandler(handler) - - def map_keys(obj: Any, key: str, func: Callable[[Any], Any]) -> None: """Map all matching keys (deeply searched) using the input function @@ -306,56 +55,56 @@ def map_keys(obj: Any, key: str, func: Callable[[Any], Any]) -> None: pass -def scrub(obj: Any, bad_value: str) -> None: - """Recursively scrub a dict or list to remove a given value in place. - - Args: - obj (Any): dict or list to operate on. If neither, it will return immediately. - bad_value (str): key to remove - """ - if isinstance(obj, dict): - for key in list(obj.keys()): - if key == bad_value: - del obj[key] - else: - scrub(obj[key], bad_value) - elif isinstance(obj, list): - for i in reversed(range(len(obj))): - if obj[i] == bad_value: - del obj[i] - else: - scrub(obj[i], bad_value) - else: - # neither a dict nor a list, do nothing - pass - - -def jsonify(obj: Any) -> str: - """Turn an object into a JSON string - - This will use the pretty_print function to recursively iterate through the object and turn normally - non-jsonifable objects into JSON +def scrub(obj: Any, bad_keys: list | None = None, bad_values: list | None = None) -> None: + """Recursively scrub a collection (dict / list) of bad keys and / or bad values Args: - obj (Any): object to jsonify + obj (Any): collection to scrub + bad_keys (list | None, optional): Keys to remove. Defaults to None. + bad_values (list | None, optional): Values to remove. Defaults to None. - Returns: - str: JSON string + Raises: + ValueError: Missing bad keys / values """ - return json.dumps(pretty_print(obj, stringify_all=False), indent=4) + bad_keys = bad_keys or [] + bad_values = bad_values or [] + if not (bad_values or bad_keys): + raise ValueError("Must pass either / or bad_keys or bad_values") + + def recurse(obj: Any) -> None: + if isinstance(obj, dict): + for key, value in {**obj}.items(): + if key in bad_keys or value in bad_values: + del obj[key] + else: + recurse(obj[key]) + elif isinstance(obj, list): + for i, value in enumerate(list(obj)): + if value in bad_values: + del obj[i] + else: + recurse(obj[i]) + else: + # neither a dict nor a list, do nothing + pass + + recurse(obj) -def pretty_print(obj: Any, stringify_all: bool = True) -> str: - """Recursively iterate through object and turn elements into strings as desired for eventual json dumping +def pretty_print(obj: Any, stringify_all: bool = True, should_quote: bool = True) -> str: + """Recursively iterate through object and turn elements into strings Args: obj (Any): object to recurse through stringify_all (bool): At the end of each recursion, should the element be turned into a string? For example, should an int be turned into a str? Defaults to True. + should_quote (bool): _description_. Defaults to True. Returns: str: pretty-printed string """ + output = "" + nest_level = 0 def sanitize(e: Any) -> str: """Get the value part and replace any underscored with spaces @@ -370,55 +119,78 @@ def sanitize(e: Any) -> str: value_part = value_part.replace("_", " ").title() return value_part - def stringify(elem: Any) -> Union[str, int, float]: + def stringify(elem: Any) -> Any: """Get the string value of an element if it is not a number (int, float, etc.) Args: elem (Any): element to potentially stringify Returns: - str: string representation + Any: string representation or original object """ + + def quote(elem: Any) -> Any: + return f'"{elem}"' if should_quote else elem + + ret: str if isinstance(elem, (bytes, bytearray)): - return elem.hex(":") + ret = quote(elem.hex(":")) if isinstance(elem, enum.Enum) and isinstance(elem, int): - return str(elem) if not stringify_all else sanitize(elem) - if stringify_all: - return str(elem) - if not isinstance(elem, (int, float)): - return str(elem) - return elem - - def recurse(elem: Any) -> Any: + ret = quote(str(elem) if not stringify_all else sanitize(elem)) + if isinstance(elem, (bool, int, float)): + ret = quote(elem) if stringify_all else elem + ret = str(elem) + return quote(ret) + + def recurse(elem: Any) -> None: """Recursion function Args: elem (Any): current element to work on - - Returns: - Any: element after recursion is done """ + nonlocal output + nonlocal nest_level + indent_size = 4 + # Convert to dict if possible + if isinstance(elem, BaseModel): + elem = dict(elem) + scrub(elem, bad_values=[None]) if isinstance(elem, dict): # nested dictionary - d = {} + nest_level += 1 + output += "{" for k, v in elem.items(): - if isinstance(v, (dict, list)): - d[recurse(k)] = recurse(v) + output += f"\n{' ' * (indent_size * nest_level)}" + # Add key + recurse(k) + output += " : " + # Add value + if isinstance(v, (dict, list, BaseModel)): + recurse(v) else: - d[recurse(k)] = stringify(v) + output += stringify(v) + output += "," - if stringify_all: - return json.dumps(d) - return d + nest_level -= 1 + output += f"\n{' '* (indent_size * nest_level)}}}" - if isinstance(elem, list): + elif isinstance(elem, list): # nested list - l = [recurse(t) for t in elem] - return " , ".join(l) if stringify_all else l + nest_level += 1 + output += f"[\n{' '* (indent_size * nest_level)}" + if len(elem): + for item in elem[:-1]: + recurse(item) + output += ", " + recurse(elem[-1]) + nest_level -= 1 + output += f"\n{' '* (indent_size * nest_level)}]" - return stringify(elem) + else: + output += stringify(elem) - return recurse(obj) + recurse(obj) + return output def cmd(command: str) -> str: @@ -446,20 +218,27 @@ def cmd(command: str) -> str: return response -class SnapshotQueue(queue.Queue): +T = TypeVar("T") + + +class SnapshotQueue(asyncio.Queue, Generic[T]): """A subclass of the default queue module to safely take a snapshot of the queue This is so we can access the elements (in a thread safe manner) without dequeuing them. """ - def snapshot(self) -> list[Any]: - """Acquire the mutex, then return the queue's elements as a list. + def __init__(self, maxsize: int = 0) -> None: + self._lock = asyncio.Lock() + super().__init__(maxsize) + + async def peek_front(self) -> T | None: + """Get the first element without dequeueing it Returns: - list[Any]: List of queue elements + T | None: First element of None if the queue is empty """ - with self.mutex: - return list(self.queue) + async with self._lock: + return None if self.empty() else self._queue[0] # type: ignore def add_cli_args_and_parse( @@ -510,3 +289,34 @@ def add_cli_args_and_parse( args.password = sys.stdin.readline() if args.password else None return args + + +async def ainput(string: str, printer: Callable = sys.stdout.write) -> str: + """Async version of input + + Args: + string (str): prompt string + printer (Callable): Callable to display prompt. Defaults to sys.stdout.write. + + Returns: + str: Input read from console + """ + await asyncio.get_event_loop().run_in_executor(None, lambda s=string: printer(s + " ")) + return await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline) + + +def get_current_dst_aware_time() -> tuple[datetime, int, bool]: + """Get the current time, utc offset in minutes, and daylight savings time + + Returns: + tuple[datetime, int, bool]: [time, utc_offset in minutes, is_dst?] + """ + tz = pytz.timezone(get_localzone().key) # type: ignore + now = tz.localize(datetime.now(), is_dst=None) + try: + is_dst = now.tzinfo._dst.seconds != 0 # type: ignore + offset = (now.utcoffset().total_seconds() - now.tzinfo._dst.seconds) / 60 # type: ignore + except AttributeError: + is_dst = False + offset = now.utcoffset().total_seconds() / 60 # type: ignore + return (now, int(offset), is_dst) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/__init__.py index a1682027..1393786d 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/__init__.py @@ -1,7 +1,11 @@ # __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). # This copyright was auto-generated on Tue Sep 7 21:35:53 UTC 2021 -"""Open GoPro WiFi Interface interace and implementation""" +"""Open GoPro WiFi Interface interface and implementation + +isort:skip_file +""" from .controller import SsidState, WifiController from .client import WifiClient +from .adapters import WifiCli diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/adapters/__init__.py b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/adapters/__init__.py index 49824968..bf31d5ac 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/adapters/__init__.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/adapters/__init__.py @@ -3,4 +3,4 @@ """Universal WiFi adapter implementation for Open GoPro WiFi interface""" -from .wireless import Wireless +from .wireless import WifiCli diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/adapters/wireless.py b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/adapters/wireless.py index 248d712c..274d82b0 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/adapters/wireless.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/adapters/wireless.py @@ -4,18 +4,20 @@ """Manage a WiFI connection using native OS commands.""" from __future__ import annotations -import os -import re -import time + import ctypes +import html import locale import logging +import os import platform +import re import tempfile +import time from enum import Enum, auto from getpass import getpass from shutil import which -from typing import Optional, Any, Callable +from typing import Any, Callable, Optional import wrapt from packaging.version import Version @@ -47,12 +49,12 @@ def ensure_us_english() -> None: @wrapt.decorator -def pass_through_to_driver(wrapped: Callable, instance: Wireless, args: Any, kwargs: Any) -> Any: +def pass_through_to_driver(wrapped: Callable, instance: WifiCli, args: Any, kwargs: Any) -> Any: """Call this same method on the _driver attribute Args: wrapped (Callable): method to call - instance (Wireless): instance to use to find driver + instance (WifiCli): instance to use to find driver args (Any): positional arguments kwargs (Any): keyword arguments @@ -63,7 +65,7 @@ def pass_through_to_driver(wrapped: Callable, instance: Wireless, args: Any, kwa return driver_method(*args, **kwargs) -class Wireless(WifiController): +class WifiCli(WifiController): """Top level abstraction of different Wifi drivers. If interface is not specified (i.e. it is None), we will attempt to automatically @@ -312,9 +314,7 @@ def connect(self, ssid: str, password: str, timeout: float = 15) -> bool: # attempt to connect logger.info(f"Connecting to {ssid}...") - response = cmd( - f'{self.sudo} nmcli dev wifi connect "{ssid}" password "{password}" iface "{self.interface}"' - ) + response = cmd(f'{self.sudo} nmcli dev wifi connect "{ssid}" password "{password}" iface "{self.interface}"') # parse response return not self._error_in_response(response) @@ -472,9 +472,7 @@ def connect(self, ssid: str, password: str, timeout: float = 15) -> bool: # attempt to connect logger.info(f"Connecting to {ssid}...") - response = cmd( - f'{self.sudo} nmcli dev wifi connect "{ssid}" password "{password}" ifname "{self.interface}"' - ) + response = cmd(f'{self.sudo} nmcli dev wifi connect "{ssid}" password "{password}" ifname "{self.interface}"') # parse response return not self._error_in_response(response) @@ -860,8 +858,9 @@ def connect(self, ssid: str, password: str, timeout: float = 15) -> bool: Returns: bool: True if connected, False otherwise """ - # Replace ampersand as it causes problems - password = password.replace("&", "&") + # Replace xml tokens (&, <, >, etc.) + password = html.escape(password) + ssid = html.escape(ssid) logger.info(f"Attempting to establish Wifi connection to {ssid}...") diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/client.py b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/client.py index 62a756cd..0cdb821b 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/client.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/client.py @@ -7,7 +7,8 @@ from typing import Optional from open_gopro.exceptions import ConnectFailed -from .controller import WifiController, SsidState + +from .controller import SsidState, WifiController logger = logging.getLogger(__name__) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/wifi/mdns_scanner.py b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/mdns_scanner.py new file mode 100644 index 00000000..aee49e8c --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/open_gopro/wifi/mdns_scanner.py @@ -0,0 +1,88 @@ +# mdns_scanner.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Tue Aug 8 18:10:56 UTC 2023 + +"""MDNS utility functions""" + +import asyncio +import logging +from typing import Any + +import zeroconf + +# Imported this way for monkeypatching in pytest +import zeroconf.asyncio + +from open_gopro import exceptions as GpException + +logger = logging.getLogger(__name__) + + +class ZeroconfListener(zeroconf.ServiceListener): + """Listens for mDNS services on the local system and save fully-formed ipaddr URLs""" + + def __init__(self) -> None: + self.urls: asyncio.Queue[str] = asyncio.Queue() + + def add_service(self, zc: zeroconf.Zeroconf, type_: str, name: str) -> None: + """Callback called by ServiceBrowser when a new service is discovered + + Args: + zc (Zeroconf): instantiated zeroconf object that owns the search + type_ (str): name of mDNS service that search is occurring on + name (str): discovered device + """ + logger.debug(f"Found MDNS service {name}") + self.urls.put_nowait(name) + + def update_service(self, *_: Any) -> None: + """Not used + + Args: + *_ (Any): not used + """ + + def remove_service(self, *_: Any) -> None: + """Not used + + Args: + *_ (Any): not used + """ + + +async def find_first_ip_addr(service: str, timeout: int = 5) -> str: + """Query the mDNS server to find a an IP address matching a service + + The first IP address matching the service will be returned + + Args: + service (str): service name to scan for + timeout (int): how long to search for before timing out in seconds + + Raises: + FailedToFindDevice: search timed out + + Returns: + str: First discovered IP address matching service + """ + logger.info(f"Querying mDNS to find {service}...") + listener = ZeroconfListener() + with zeroconf.Zeroconf(unicast=True) as zc: + zeroconf.asyncio.AsyncServiceBrowser(zc, service, listener) + try: + name = await asyncio.wait_for(listener.urls.get(), timeout) + async with zeroconf.asyncio.AsyncZeroconf(unicast=True) as azc: + if info := await azc.async_get_service_info(service, name): + ip_addr = info.parsed_addresses()[0] + return ip_addr + raise GpException.FailedToFindDevice() + except Exception as e: + raise GpException.FailedToFindDevice() from e + + +async def get_all_services() -> list[str]: + """Get all service names + + Returns: + tuple[str, ...]: tuple of service names + """ + return list(await zeroconf.asyncio.AsyncZeroconfServiceTypes.async_find()) diff --git a/demos/python/sdk_wireless_camera_control/poetry.lock b/demos/python/sdk_wireless_camera_control/poetry.lock index 51013e8f..9011c08b 100644 --- a/demos/python/sdk_wireless_camera_control/poetry.lock +++ b/demos/python/sdk_wireless_camera_control/poetry.lock @@ -1,2031 +1,2087 @@ -[[package]] -name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "argcomplete" -version = "2.0.0" -description = "Bash tab completion for argparse" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -test = ["coverage", "flake8", "pexpect", "wheel"] - -[[package]] -name = "astroid" -version = "2.14.2" -description = "An abstract syntax tree for Python with inference support." -category = "dev" -optional = false -python-versions = ">=3.7.2" - -[package.dependencies] -lazy-object-proxy = ">=1.4.0" -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} -wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} - -[[package]] -name = "async-timeout" -version = "4.0.2" -description = "Timeout context manager for asyncio programs" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - -[[package]] -name = "babel" -version = "2.11.0" -description = "Internationalization utilities" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pytz = ">=2015.7" - -[[package]] -name = "black" -version = "22.12.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "bleak" -version = "0.19.5" -description = "Bluetooth Low Energy platform Agnostic Klient" -category = "main" -optional = false -python-versions = ">=3.7,<4.0" - -[package.dependencies] -async-timeout = ">=3.0.0,<5" -bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\""} -dbus-fast = {version = ">=1.22.0,<2.0.0", markers = "platform_system == \"Linux\""} -pyobjc-core = {version = ">=8.5.1,<9.0.0", markers = "platform_system == \"Darwin\""} -pyobjc-framework-CoreBluetooth = {version = ">=8.5.1,<9.0.0", markers = "platform_system == \"Darwin\""} -pyobjc-framework-libdispatch = {version = ">=8.5.1,<9.0.0", markers = "platform_system == \"Darwin\""} - -[[package]] -name = "bleak-winrt" -version = "1.2.0" -description = "Python WinRT bindings for Bleak" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "certifi" -version = "2022.12.7" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "charset-normalizer" -version = "3.0.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" - -[[package]] -name = "colorlog" -version = "6.7.0" -description = "Add colours to the output of Python's logging module." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -development = ["black", "flake8", "mypy", "pytest", "types-colorama"] - -[[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" -category = "main" -optional = false -python-versions = "*" - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[[package]] -name = "construct" -version = "2.10.68" -description = "A powerful declarative symmetric parser/builder for binary data" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -extras = ["arrow", "cloudpickle", "enum34", "lz4", "numpy", "ruamel.yaml"] - -[[package]] -name = "construct-typing" -version = "0.5.5" -description = "Extension for the python package 'construct' that adds typing features" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -construct = "2.10.68" - -[[package]] -name = "coverage" -version = "6.5.0" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "coverage-badge" -version = "1.1.0" -description = "Generate coverage badges for Coverage.py." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -coverage = "*" - -[[package]] -name = "darglint" -version = "1.8.1" -description = "A utility for ensuring Google-style docstrings stay up to date with the source code." -category = "dev" -optional = false -python-versions = ">=3.6,<4.0" - -[[package]] -name = "dbus-fast" -version = "1.84.2" -description = "A faster version of dbus-next" -category = "main" -optional = false -python-versions = ">=3.7,<4.0" - -[package.dependencies] -async-timeout = {version = ">=3.0.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "dill" -version = "0.3.6" -description = "serialize all of python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -graph = ["objgraph (>=1.7.2)"] - -[[package]] -name = "distlib" -version = "0.3.6" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "docutils" -version = "0.18.1" -description = "Docutils -- Python Documentation Utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "exceptiongroup" -version = "1.1.0" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.9.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "ifaddr" -version = "0.2.0" -description = "Cross-platform network interface and IP address enumeration library" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "importlib-metadata" -version = "6.0.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.8.0" - -[package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "lazy-object-proxy" -version = "1.9.0" -description = "A fast and thorough lazy object proxy." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "markupsafe" -version = "2.1.2" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "mypy" -version = "1.0.1" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "mypy-protobuf" -version = "3.3.0" -description = "Generate mypy stub files from protobuf specs" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -protobuf = ">=3.19.4" -types-protobuf = ">=3.19.12" - -[[package]] -name = "nox" -version = "2022.8.7" -description = "Flexible test automation." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -argcomplete = ">=1.9.4,<3.0" -colorlog = ">=2.6.1,<7.0.0" -packaging = ">=20.9" -py = ">=1.4,<2.0.0" -virtualenv = ">=14" - -[package.extras] -tox-to-nox = ["jinja2", "tox"] - -[[package]] -name = "nox-poetry" -version = "1.0.1" -description = "nox-poetry" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" - -[package.dependencies] -nox = ">=2020.8.22" -packaging = ">=20.9" -tomlkit = ">=0.7" - -[[package]] -name = "numpy" -version = "1.24.2" -description = "Fundamental package for array computing in Python" -category = "main" -optional = true -python-versions = ">=3.8" - -[[package]] -name = "opencv-python" -version = "4.7.0.72" -description = "Wrapper package for OpenCV python bindings." -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -numpy = [ - {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, -] - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "pastel" -version = "0.2.1" -description = "Bring colors to your terminal." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pathspec" -version = "0.11.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "pexpect" -version = "4.8.0" -description = "Pexpect allows easy control of interactive console applications." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pillow" -version = "9.4.0" -description = "Python Imaging Library (Fork)" -category = "main" -optional = true -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "platformdirs" -version = "3.0.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "poethepoet" -version = "0.15.0" -description = "A task runner that works well with poetry." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -pastel = ">=0.2.1,<0.3.0" -tomli = ">=1.2.2" - -[package.extras] -poetry-plugin = ["poetry (>=1.0,<2.0)"] - -[[package]] -name = "protobuf" -version = "3.20.3" -description = "Protocol Buffers" -category = "main" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "protoletariat" -version = "0.9.4" -description = "Python protocol buffers for the rest of us" -category = "dev" -optional = false -python-versions = ">=3.7,<3.11" - -[package.dependencies] -click = ">=7.1.2,<9" -protobuf = ">=3.19.1,<4.0.0" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -snowballstemmer = ">=2.2.0" -tomli = {version = ">=1.2.3", optional = true, markers = "python_version < \"3.11\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli (>=1.2.3)"] - -[[package]] -name = "pygments" -version = "2.14.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pylint" -version = "2.16.2" -description = "python code static checker" -category = "dev" -optional = false -python-versions = ">=3.7.2" - -[package.dependencies] -astroid = ">=2.14.2,<=2.16.0-dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = {version = ">=0.2", markers = "python_version < \"3.11\""} -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pyobjc-core" -version = "8.5.1" -description = "Python<->ObjC Interoperability Module" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "pyobjc-framework-cocoa" -version = "8.5.1" -description = "Wrappers for the Cocoa frameworks on macOS" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyobjc-core = ">=8.5.1" - -[[package]] -name = "pyobjc-framework-corebluetooth" -version = "8.5.1" -description = "Wrappers for the framework CoreBluetooth on macOS" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyobjc-core = ">=8.5.1" -pyobjc-framework-Cocoa = ">=8.5.1" - -[[package]] -name = "pyobjc-framework-libdispatch" -version = "8.5.1" -description = "Wrappers for libdispatch on macOS" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyobjc-core = ">=8.5.1" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "7.2.1" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.17.2" -description = "Pytest support for asyncio" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -pytest = ">=6.1.0" - -[package.extras] -testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)"] - -[[package]] -name = "pytest-cov" -version = "3.0.0" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-html" -version = "3.2.0" -description = "pytest plugin for generating HTML reports" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -py = ">=1.8.2" -pytest = ">=5.0,<6.0.0 || >6.0.0" -pytest-metadata = "*" - -[[package]] -name = "pytest-metadata" -version = "2.0.4" -description = "pytest plugin for test session metadata" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" - -[package.dependencies] -pytest = ">=3.0.0,<8.0.0" - -[[package]] -name = "pytz" -version = "2022.7.1" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "requests" -version = "2.28.2" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-mock" -version = "1.10.0" -description = "Mock out responses from the requests package" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -requests = ">=2.3,<3" -six = "*" - -[package.extras] -fixture = ["fixtures"] -test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testrepository (>=0.0.18)", "testtools"] - -[[package]] -name = "rich" -version = "12.6.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" -optional = false -python-versions = ">=3.6.3,<4.0.0" - -[package.dependencies] -commonmark = ">=0.9.0,<0.10.0" -pygments = ">=2.6.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] - -[[package]] -name = "setuptools" -version = "67.4.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "sphinx" -version = "5.3.0" -description = "Python documentation generator" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" -imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.12" -requests = ">=2.5.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] - -[[package]] -name = "sphinx-rtd-theme" -version = "1.2.0" -description = "Read the Docs theme for Sphinx" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" - -[package.dependencies] -docutils = "<0.19" -sphinx = ">=1.6,<7" -sphinxcontrib-jquery = {version = ">=2.0.0,<3.0.0 || >3.0.0", markers = "python_version > \"3\""} - -[package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.4" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "dev" -optional = false -python-versions = ">=3.8" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.1" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" -optional = false -python-versions = ">=3.8" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jquery" -version = "2.0.0" -description = "Extension to include jQuery on newer Sphinx releases" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxemoji" -version = "0.2.0" -description = "An extension to use emoji codes in your Sphinx documentation" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -sphinx = ">=1.8" - -[[package]] -name = "tk" -version = "0.1.0" -description = "TensorKit is a deep learning helper between Python and C++." -category = "main" -optional = true -python-versions = "*" - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tomlkit" -version = "0.11.6" -description = "Style preserving TOML library" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "types-attrs" -version = "19.1.0" -description = "Typing stubs for attrs" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-protobuf" -version = "4.21.0.7" -description = "Typing stubs for protobuf" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "types-requests" -version = "2.28.11.14" -description = "Typing stubs for requests" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -types-urllib3 = "<1.27" - -[[package]] -name = "types-urllib3" -version = "1.26.25.7" -description = "Typing stubs for urllib3" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "urllib3" -version = "1.26.14" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "virtualenv" -version = "20.19.0" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<4" - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "zeroconf" -version = "0.39.4" -description = "Pure Python Multicast DNS Service Discovery Library (Bonjour/Avahi compatible)" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -async-timeout = ">=4.0.1" -ifaddr = ">=0.1.7" - -[[package]] -name = "zipp" -version = "3.14.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[extras] -gui = ["opencv-python", "tk", "Pillow"] - -[metadata] -lock-version = "1.1" -python-versions = ">=3.9,<3.11" -content-hash = "a482f1e79d98c4b1b178758cf66c15640c2034ee07a4616783c149b87b141860" - -[metadata.files] -alabaster = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, -] -argcomplete = [ - {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"}, - {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"}, -] -astroid = [ - {file = "astroid-2.14.2-py3-none-any.whl", hash = "sha256:0e0e3709d64fbffd3037e4ff403580550f14471fd3eaae9fa11cc9a5c7901153"}, - {file = "astroid-2.14.2.tar.gz", hash = "sha256:a3cf9f02c53dd259144a7e8f3ccd75d67c9a8c716ef183e0c1f291bc5d7bb3cf"}, -] -async-timeout = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, -] -attrs = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] -babel = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, -] -black = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] -bleak = [ - {file = "bleak-0.19.5-py3-none-any.whl", hash = "sha256:31e92e6754379bb394b8544457c77ce09a8a7dbc5f9adf3119b34576c901ef1e"}, - {file = "bleak-0.19.5.tar.gz", hash = "sha256:87845a96453c58c19031c735444a7b3156800534bcd3f23ba74e119e9ae3cd88"}, -] -bleak-winrt = [ - {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, - {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, - {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, - {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, - {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, - {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, - {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, - {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, - {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, - {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, - {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, -] -certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] -charset-normalizer = [ - {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, - {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -colorlog = [ - {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, - {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"}, -] -commonmark = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] -construct = [ - {file = "construct-2.10.68.tar.gz", hash = "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45"}, -] -construct-typing = [ - {file = "construct-typing-0.5.5.tar.gz", hash = "sha256:29d1a07df539ae096bd1388ad7f58714d229a303047425d3830b282d8b154572"}, - {file = "construct_typing-0.5.5-py3-none-any.whl", hash = "sha256:bfce2fa170373abe782c2ebaa7f52e14e8f7863a437b7ab63bf74287f922a655"}, -] -coverage = [ - {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, - {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, - {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, - {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, - {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, - {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, - {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, - {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, - {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, - {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, - {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, - {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, - {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, - {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, - {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, - {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, -] -coverage-badge = [ - {file = "coverage-badge-1.1.0.tar.gz", hash = "sha256:c824a106503e981c02821e7d32f008fb3984b2338aa8c3800ec9357e33345b78"}, - {file = "coverage_badge-1.1.0-py2.py3-none-any.whl", hash = "sha256:e365d56e5202e923d1b237f82defd628a02d1d645a147f867ac85c58c81d7997"}, -] -darglint = [ - {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, - {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, -] -dbus-fast = [ - {file = "dbus_fast-1.84.2-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0e5fdb1c4841080b9087a10eed6472d485d44cbe04c4e37eb4c660c34d1e3ea2"}, - {file = "dbus_fast-1.84.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29e9c25f8e99c1d9c15372161a8a3e1cec4b46c56689aa2f7bb6378de4137873"}, - {file = "dbus_fast-1.84.2-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:13995e9d3503be3c4c31abeb95bc38ac6f8277f5a376f4aaefdc4a6d72aae151"}, - {file = "dbus_fast-1.84.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1f13eefd103c02711f76276fe2f7265ccb0cedaf943bc0a51ebd2524c031b7cb"}, - {file = "dbus_fast-1.84.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a9714560f1a89b1e0015d69db00796fa9c83e599f35e7d5113270a3991cec93f"}, - {file = "dbus_fast-1.84.2-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:dc9d02c13a0c46762b27ffa2ecd7500e5b78ccdf91889ddd5539c6e944edb4a4"}, - {file = "dbus_fast-1.84.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65a1ffa691e4457b25eb29c5a8e8d2394927d1f3dce44d9b84952d7d5b94920f"}, - {file = "dbus_fast-1.84.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7fe9d9017b17e993360605541842e05bd10cf51856e39302b517c8f16da38cf1"}, - {file = "dbus_fast-1.84.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f7eb6c7b416e3d728e2e7232925edb2775694925c0c0d25c8d5f26a5726a6f69"}, - {file = "dbus_fast-1.84.2-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:bf4c19554a27c5cd1485c0a97afa71e352c5f0e511e4a209cbd85ed25104f828"}, - {file = "dbus_fast-1.84.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:106e0fb2470169a669da5aaf38cc2442708f1e8dac67a9cf772f5d87977201ba"}, - {file = "dbus_fast-1.84.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c332486374f029c69810ca98c92c55ae2030a51cd1d5b3c04eb3ecee5d31e1f0"}, - {file = "dbus_fast-1.84.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bba1b3aa1f5f87d20982e5479567d623627241399a33d2fd4882184fa762b85b"}, - {file = "dbus_fast-1.84.2-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1a5391eefdd69ba0f09e95848f1a1ecb269cfda81404d3b3fe76952a37845c4c"}, - {file = "dbus_fast-1.84.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e638af8a0176ee668eb55668817990d696a3f7b31135f1222f80ac36f6eef56a"}, - {file = "dbus_fast-1.84.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3e1f504db2f29cc2c3602bbb972eacfffd4307faf4d3a7e881bf9c4b13742723"}, - {file = "dbus_fast-1.84.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1affd246014e79d4007066f5953cc6e45f25cf990255eaab4298ec88ce07b153"}, - {file = "dbus_fast-1.84.2-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:43fca5a6608de9b75f0e110e335a264ca94336b9a74ca61f1bc98e49f2eaaaee"}, - {file = "dbus_fast-1.84.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c0b9064cd1de80c6ca89fe2431a6ab34030e0d4dc1d79f8dc1f3fe5dc52483b"}, - {file = "dbus_fast-1.84.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9a1b4a1f30a73b24a23a261c18ddead63687e077e4c98d6ba7228d13e541fb43"}, - {file = "dbus_fast-1.84.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd0ba12abc443ded3284b6a399cad6f18a1660ed5b20813510835b2363c7f585"}, - {file = "dbus_fast-1.84.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0b0a9e109e1a4f8fc3ad23579b7fd5d60ade459fa82eeb26e3d70c57ca4ae658"}, - {file = "dbus_fast-1.84.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c31817c62d58de8ad53a6fcde92ad146a7e47c3f9e395f0c60b71305d5d3d2c"}, - {file = "dbus_fast-1.84.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:48068eadf1613c2263b5babe3feacc52be3fcd26823b623ba6a5ad7787e0fe52"}, - {file = "dbus_fast-1.84.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57280df4ddd820d513cdc9c3aa0ecd9c897686cc7b374f6d774462869ae55bb8"}, - {file = "dbus_fast-1.84.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1803462a53365de05f5a6df74c74cae19d5743d60213344823baf9f50aa7ad5d"}, - {file = "dbus_fast-1.84.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7b16fe43bfdf049a00024ed45d119ad39f3f8bcb85066e2a7d4f06a6d074878"}, - {file = "dbus_fast-1.84.2.tar.gz", hash = "sha256:62b00b85c5835bff1d7ab5b12d494e588d92612bedbd7ca86176861729b8e4bc"}, -] -dill = [ - {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, - {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, -] -distlib = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] -docutils = [ - {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, - {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, -] -exceptiongroup = [ - {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, - {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, -] -filelock = [ - {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, - {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -ifaddr = [ - {file = "ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748"}, - {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, -] -imagesize = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] -importlib-metadata = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, -] -iniconfig = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] -isort = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, -] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, -] -mccabe = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] -mypy = [ - {file = "mypy-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71a808334d3f41ef011faa5a5cd8153606df5fc0b56de5b2e89566c8093a0c9a"}, - {file = "mypy-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920169f0184215eef19294fa86ea49ffd4635dedfdea2b57e45cb4ee85d5ccaf"}, - {file = "mypy-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a0f74a298769d9fdc8498fcb4f2beb86f0564bcdb1a37b58cbbe78e55cf8c0"}, - {file = "mypy-1.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65b122a993d9c81ea0bfde7689b3365318a88bde952e4dfa1b3a8b4ac05d168b"}, - {file = "mypy-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5deb252fd42a77add936b463033a59b8e48eb2eaec2976d76b6878d031933fe4"}, - {file = "mypy-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2013226d17f20468f34feddd6aae4635a55f79626549099354ce641bc7d40262"}, - {file = "mypy-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:48525aec92b47baed9b3380371ab8ab6e63a5aab317347dfe9e55e02aaad22e8"}, - {file = "mypy-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96b8a0c019fe29040d520d9257d8c8f122a7343a8307bf8d6d4a43f5c5bfcc8"}, - {file = "mypy-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:448de661536d270ce04f2d7dddaa49b2fdba6e3bd8a83212164d4174ff43aa65"}, - {file = "mypy-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d42a98e76070a365a1d1c220fcac8aa4ada12ae0db679cb4d910fabefc88b994"}, - {file = "mypy-1.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64f48c6176e243ad015e995de05af7f22bbe370dbb5b32bd6988438ec873919"}, - {file = "mypy-1.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd63e4f50e3538617887e9aee91855368d9fc1dea30da743837b0df7373bc4"}, - {file = "mypy-1.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbeb24514c4acbc78d205f85dd0e800f34062efcc1f4a4857c57e4b4b8712bff"}, - {file = "mypy-1.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a2948c40a7dd46c1c33765718936669dc1f628f134013b02ff5ac6c7ef6942bf"}, - {file = "mypy-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bc8d6bd3b274dd3846597855d96d38d947aedba18776aa998a8d46fabdaed76"}, - {file = "mypy-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:17455cda53eeee0a4adb6371a21dd3dbf465897de82843751cf822605d152c8c"}, - {file = "mypy-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e831662208055b006eef68392a768ff83596035ffd6d846786578ba1714ba8f6"}, - {file = "mypy-1.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e60d0b09f62ae97a94605c3f73fd952395286cf3e3b9e7b97f60b01ddfbbda88"}, - {file = "mypy-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:0af4f0e20706aadf4e6f8f8dc5ab739089146b83fd53cb4a7e0e850ef3de0bb6"}, - {file = "mypy-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24189f23dc66f83b839bd1cce2dfc356020dfc9a8bae03978477b15be61b062e"}, - {file = "mypy-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93a85495fb13dc484251b4c1fd7a5ac370cd0d812bbfc3b39c1bafefe95275d5"}, - {file = "mypy-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f546ac34093c6ce33f6278f7c88f0f147a4849386d3bf3ae193702f4fe31407"}, - {file = "mypy-1.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c6c2ccb7af7154673c591189c3687b013122c5a891bb5651eca3db8e6c6c55bd"}, - {file = "mypy-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:15b5a824b58c7c822c51bc66308e759243c32631896743f030daf449fe3677f3"}, - {file = "mypy-1.0.1-py3-none-any.whl", hash = "sha256:eda5c8b9949ed411ff752b9a01adda31afe7eae1e53e946dbdf9db23865e66c4"}, - {file = "mypy-1.0.1.tar.gz", hash = "sha256:28cea5a6392bb43d266782983b5a4216c25544cd7d80be681a155ddcdafd152d"}, -] -mypy-extensions = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] -mypy-protobuf = [ - {file = "mypy-protobuf-3.3.0.tar.gz", hash = "sha256:24f3b0aecb06656e983f58e07c732a90577b9d7af3e1066fc2b663bbf0370248"}, - {file = "mypy_protobuf-3.3.0-py3-none-any.whl", hash = "sha256:15604f6943b16c05db646903261e3b3e775cf7f7990b7c37b03d043a907b650d"}, -] -nox = [ - {file = "nox-2022.8.7-py3-none-any.whl", hash = "sha256:96cca88779e08282a699d672258ec01eb7c792d35bbbf538c723172bce23212c"}, - {file = "nox-2022.8.7.tar.gz", hash = "sha256:1b894940551dc5c389f9271d197ca5d655d40bdc6ccf93ed6880e4042760a34b"}, -] -nox-poetry = [ - {file = "nox-poetry-1.0.1.tar.gz", hash = "sha256:8a1b96f2d321e91917f0aa770adb6079f3f3dc8cf01447944977cb78ccafda15"}, - {file = "nox_poetry-1.0.1-py3-none-any.whl", hash = "sha256:6ed30e33b782cecba081dbb79626f60c3acf517c535b89ef8699071fd70567cd"}, -] -numpy = [ - {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"}, - {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"}, - {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"}, - {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"}, - {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"}, - {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"}, - {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"}, - {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"}, - {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"}, - {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"}, - {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"}, - {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"}, - {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"}, - {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"}, - {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"}, - {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"}, - {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"}, - {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"}, - {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"}, - {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"}, - {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"}, - {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"}, - {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"}, - {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"}, - {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"}, - {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"}, -] -opencv-python = [ - {file = "opencv-python-4.7.0.72.tar.gz", hash = "sha256:3424794a711f33284581f3c1e4b071cfc827d02b99d6fd9a35391f517c453306"}, - {file = "opencv_python-4.7.0.72-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:d4f8880440c433a0025d78804dda6901d1e8e541a561dda66892d90290aef881"}, - {file = "opencv_python-4.7.0.72-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:7a297e7651e22eb17c265ddbbc80e2ba2a8ff4f4a1696a67c45e5f5798245842"}, - {file = "opencv_python-4.7.0.72-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd08343654c6b88c5a8c25bf425f8025aed2e3189b4d7306b5861d32affaf737"}, - {file = "opencv_python-4.7.0.72-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebfc0a3a2f57716e709028b992e4de7fd8752105d7a768531c4f434043c6f9ff"}, - {file = "opencv_python-4.7.0.72-cp37-abi3-win32.whl", hash = "sha256:eda115797b114fc16ca6f182b91c5d984f0015c19bec3145e55d33d708e9bae1"}, - {file = "opencv_python-4.7.0.72-cp37-abi3-win_amd64.whl", hash = "sha256:812af57553ec1c6709060c63f6b7e9ad07ddc0f592f3ccc6d00c71e0fe0e6376"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pastel = [ - {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, - {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, -] -pathspec = [ - {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, - {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, -] -pexpect = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, -] -pillow = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, - {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, - {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, - {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, - {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, - {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, - {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, - {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, -] -platformdirs = [ - {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, - {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -poethepoet = [ - {file = "poethepoet-0.15.0-py3-none-any.whl", hash = "sha256:8ca49d8a9928a3ce1753315d6df0866888557eccb0fe37a8c88fea47454cfe12"}, - {file = "poethepoet-0.15.0.tar.gz", hash = "sha256:5843260c9074b6c42bf2e51f21107efe37e230cf75da3dd3f4b43904f365b26c"}, -] -protobuf = [ - {file = "protobuf-3.20.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99"}, - {file = "protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e"}, - {file = "protobuf-3.20.3-cp310-cp310-win32.whl", hash = "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c"}, - {file = "protobuf-3.20.3-cp310-cp310-win_amd64.whl", hash = "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7"}, - {file = "protobuf-3.20.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469"}, - {file = "protobuf-3.20.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4"}, - {file = "protobuf-3.20.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4"}, - {file = "protobuf-3.20.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454"}, - {file = "protobuf-3.20.3-cp37-cp37m-win32.whl", hash = "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905"}, - {file = "protobuf-3.20.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c"}, - {file = "protobuf-3.20.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7"}, - {file = "protobuf-3.20.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"}, - {file = "protobuf-3.20.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050"}, - {file = "protobuf-3.20.3-cp38-cp38-win32.whl", hash = "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86"}, - {file = "protobuf-3.20.3-cp38-cp38-win_amd64.whl", hash = "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9"}, - {file = "protobuf-3.20.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b"}, - {file = "protobuf-3.20.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b"}, - {file = "protobuf-3.20.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402"}, - {file = "protobuf-3.20.3-cp39-cp39-win32.whl", hash = "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480"}, - {file = "protobuf-3.20.3-cp39-cp39-win_amd64.whl", hash = "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7"}, - {file = "protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db"}, - {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, -] -protoletariat = [ - {file = "protoletariat-0.9.4-py3-none-any.whl", hash = "sha256:25f2dec490ef66a3a577c6956d03835ffbcb769865ecefd709798a81ffd46b30"}, - {file = "protoletariat-0.9.4.tar.gz", hash = "sha256:f34633c9b1d4b0c4efccadd09a4f2a3c3dc1ddb3d38240dfdbafad2553f85fe3"}, -] -ptyprocess = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pydocstyle = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] -pygments = [ - {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, - {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, -] -pylint = [ - {file = "pylint-2.16.2-py3-none-any.whl", hash = "sha256:ff22dde9c2128cd257c145cfd51adeff0be7df4d80d669055f24a962b351bbe4"}, - {file = "pylint-2.16.2.tar.gz", hash = "sha256:13b2c805a404a9bf57d002cd5f054ca4d40b0b87542bdaba5e05321ae8262c84"}, -] -pyobjc-core = [ - {file = "pyobjc-core-8.5.1.tar.gz", hash = "sha256:f8592a12de076c27006700c4a46164478564fa33d7da41e7cbdd0a3bf9ddbccf"}, - {file = "pyobjc_core-8.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b62dcf987cc511188fc2aa5b4d3b9fd895361ea4984380463497ce4b0752ddf4"}, - {file = "pyobjc_core-8.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0accc653501a655f66c13f149a1d3d30e6cb65824edf852f7960a00c4f930d5b"}, - {file = "pyobjc_core-8.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f82b32affc898e9e5af041c1cecde2c99f2ce160b87df77f678c99f1550a4655"}, - {file = "pyobjc_core-8.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f7b2f6b6f3caeb882c658fe0c7098be2e8b79893d84daa8e636cb3e58a07df00"}, - {file = "pyobjc_core-8.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:872c0202c911a5a2f1269261c168e36569f6ddac17e5d854ac19e581726570cc"}, - {file = "pyobjc_core-8.5.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:21f92e231a4bae7f2d160d065f5afbf5e859a1e37f29d34ac12592205fc8c108"}, - {file = "pyobjc_core-8.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:315334dd09781129af6a39641248891c4caa57043901750b0139c6614ce84ec0"}, -] -pyobjc-framework-cocoa = [ - {file = "pyobjc-framework-Cocoa-8.5.1.tar.gz", hash = "sha256:9a3de5cdb4644e85daf53f2ed912ef6c16ea5804a9e65552eafe62c2e139eb8c"}, - {file = "pyobjc_framework_Cocoa-8.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa572acc2628488a47be8d19f4701fc96fce7377cc4da18316e1e08c3918521a"}, - {file = "pyobjc_framework_Cocoa-8.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cb3ae21c8d81b7f02a891088c623cef61bca89bd671eff58c632d2f926b649f3"}, - {file = "pyobjc_framework_Cocoa-8.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:88f08f5bd94c66d373d8413c1d08218aff4cff0b586e0cc4249b2284023e7577"}, - {file = "pyobjc_framework_Cocoa-8.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:063683b57e4bd88cb0f9631ae65d25ec4eecf427d2fe8d0c578f88da9c896f3f"}, - {file = "pyobjc_framework_Cocoa-8.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f8806ddfac40620fb27f185d0f8937e69e330617319ecc2eccf6b9c8451bdd1"}, - {file = "pyobjc_framework_Cocoa-8.5.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7733a9a201df9e0cc2a0cf7bf54d76bd7981cba9b599353b243e3e0c9eefec10"}, - {file = "pyobjc_framework_Cocoa-8.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f0ab227f99d3e25dd3db73f8cde0999914a5f0dd6a08600349d25f95eaa0da63"}, -] -pyobjc-framework-corebluetooth = [ - {file = "pyobjc-framework-CoreBluetooth-8.5.1.tar.gz", hash = "sha256:b4f621fc3b5bf289db58e64fd746773b18297f87a0ffc5502de74f69133301c1"}, - {file = "pyobjc_framework_CoreBluetooth-8.5.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:bc720f2987a4d28dc73b13146e7c104d717100deb75c244da68f1d0849096661"}, - {file = "pyobjc_framework_CoreBluetooth-8.5.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2167f22886beb5b3ae69e475e055403f28eab065c49a25e2b98b050b483be799"}, - {file = "pyobjc_framework_CoreBluetooth-8.5.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:aa9587a36eca143701731e8bb6c369148f8cc48c28168d41e7323828e5117f2d"}, -] -pyobjc-framework-libdispatch = [ - {file = "pyobjc-framework-libdispatch-8.5.1.tar.gz", hash = "sha256:066fb34fceb326307559104d45532ec2c7b55426f9910b70dbefd5d1b8fd530f"}, - {file = "pyobjc_framework_libdispatch-8.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a316646ab30ba2a97bc828f8e27e7bb79efdf993d218a9c5118396b4f81dc762"}, - {file = "pyobjc_framework_libdispatch-8.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7730a29e4d9c7d8c2e8d9ffb60af0ab6699b2186296d2bff0a2dd54527578bc3"}, - {file = "pyobjc_framework_libdispatch-8.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:76208d9d2b0071df2950800495ac0300360bb5f25cbe9ab880b65cb809764979"}, - {file = "pyobjc_framework_libdispatch-8.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1ad9aa4773ff1d89bf4385c081824c4f8708b50e3ac2fe0a9d590153242c0f67"}, - {file = "pyobjc_framework_libdispatch-8.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:81e1833bd26f15930faba678f9efdffafc79ec04e2ea8b6d1b88cafc0883af97"}, - {file = "pyobjc_framework_libdispatch-8.5.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:73226e224436eb6383e7a8a811c90ed597995adb155b4f46d727881a383ac550"}, - {file = "pyobjc_framework_libdispatch-8.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d115355ce446fc073c75cedfd7ab0a13958adda8e3a3b1e421e1f1e5f65640da"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, - {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, -] -pytest-asyncio = [ - {file = "pytest-asyncio-0.17.2.tar.gz", hash = "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4"}, - {file = "pytest_asyncio-0.17.2-py3-none-any.whl", hash = "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -pytest-html = [ - {file = "pytest-html-3.2.0.tar.gz", hash = "sha256:c4e2f4bb0bffc437f51ad2174a8a3e71df81bbc2f6894604e604af18fbe687c3"}, - {file = "pytest_html-3.2.0-py3-none-any.whl", hash = "sha256:868c08564a68d8b2c26866f1e33178419bb35b1e127c33784a28622eb827f3f3"}, -] -pytest-metadata = [ - {file = "pytest_metadata-2.0.4-py3-none-any.whl", hash = "sha256:acb739f89fabb3d798c099e9e0c035003062367a441910aaaf2281bc1972ee14"}, - {file = "pytest_metadata-2.0.4.tar.gz", hash = "sha256:fcc653f65fe3035b478820b5284fbf0f52803622ee3f60a2faed7a7d3ba1f41e"}, -] -pytz = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, -] -requests = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, -] -requests-mock = [ - {file = "requests-mock-1.10.0.tar.gz", hash = "sha256:59c9c32419a9fb1ae83ec242d98e889c45bd7d7a65d48375cc243ec08441658b"}, - {file = "requests_mock-1.10.0-py2.py3-none-any.whl", hash = "sha256:2fdbb637ad17ee15c06f33d31169e71bf9fe2bdb7bc9da26185be0dd8d842699"}, -] -rich = [ - {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, - {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, -] -setuptools = [ - {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, - {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -sphinx = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, -] -sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.2.0-py2.py3-none-any.whl", hash = "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2"}, - {file = "sphinx_rtd_theme-1.2.0.tar.gz", hash = "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, -] -sphinxcontrib-jquery = [ - {file = "sphinxcontrib-jquery-2.0.0.tar.gz", hash = "sha256:8fb65f6dba84bf7bcd1aea1f02ab3955ac34611d838bcc95d4983b805b234daa"}, - {file = "sphinxcontrib_jquery-2.0.0-py3-none-any.whl", hash = "sha256:ed47fa425c338ffebe3c37e1cdb56e30eb806116b85f01055b158c7057fdb995"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] -sphinxemoji = [ - {file = "sphinxemoji-0.2.0.tar.gz", hash = "sha256:27861d1dd7c6570f5e63020dac9a687263f7481f6d5d6409eb31ecebcc804e4c"}, -] -tk = [ - {file = "tk-0.1.0-py3-none-any.whl", hash = "sha256:703a69ff0d5ba2bd2f7440582ad10160e4a6561595d33457dc6caa79b9bf4930"}, - {file = "tk-0.1.0.tar.gz", hash = "sha256:60bc8923d5d35f67f5c6bd93d4f0c49d2048114ec077768f959aef36d4ed97f8"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -tomlkit = [ - {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, - {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, -] -types-attrs = [ - {file = "types_attrs-19.1.0-py2.py3-none-any.whl", hash = "sha256:d11acf7a2531a7c52a740c30fa3eb8d01d3066c10d34c01ff5e59502caac5352"}, -] -types-protobuf = [ - {file = "types-protobuf-4.21.0.7.tar.gz", hash = "sha256:6ecaddcc7aed2c636745a17c1411932cdef7a035304d50ffd4140297b6b882e8"}, - {file = "types_protobuf-4.21.0.7-py3-none-any.whl", hash = "sha256:5f4c0ba5840d66a9f32bcfd153a5d41c503f409223eae4fda876cdd6b50a638a"}, -] -types-requests = [ - {file = "types-requests-2.28.11.14.tar.gz", hash = "sha256:232792870b60adb07d23175451ab4e6190021b0c584edf052d92d9b993118f06"}, - {file = "types_requests-2.28.11.14-py3-none-any.whl", hash = "sha256:f84613b0d4c5d0eeb7879dfa05e14a3702b9c1f7a4ee81dfe9b4321b13fe93a1"}, -] -types-urllib3 = [ - {file = "types-urllib3-1.26.25.7.tar.gz", hash = "sha256:df4d3e5472bf8830bd74eac12d56e659f88662ba040c7d106bf3a5bee26fff28"}, - {file = "types_urllib3-1.26.25.7-py3-none-any.whl", hash = "sha256:28d2d7f5c31ff8ed4d9d2e396ce906c49d37523c3ec207d03d3b1695755a7199"}, -] -typing-extensions = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, -] -urllib3 = [ - {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, - {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, -] -virtualenv = [ - {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, - {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, -] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] -zeroconf = [ - {file = "zeroconf-0.39.4-py3-none-any.whl", hash = "sha256:d60eae9e9c99d1a168ce9ff9de7e7398c23754a0c2004ded230f8d529c5260a0"}, - {file = "zeroconf-0.39.4.tar.gz", hash = "sha256:701e4d697f89fe952aa9c13a512ed6bf472dcf4f0a6d275e71085604b3882295"}, -] -zipp = [ - {file = "zipp-3.14.0-py3-none-any.whl", hash = "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7"}, - {file = "zipp-3.14.0.tar.gz", hash = "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb"}, -] +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "0.7.13" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + +[[package]] +name = "astroid" +version = "2.15.6" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, + {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, +] + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +wrapt = [ + {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "autodoc-pydantic" +version = "1.9.0" +description = "Seamlessly integrate pydantic models in your Sphinx documentation." +category = "dev" +optional = false +python-versions = ">=3.7.1,<4.0.0" +files = [ + {file = "autodoc_pydantic-1.9.0-py3-none-any.whl", hash = "sha256:cbf7ec2f27f913629bd38f9944fa6c4a86541c3cadba4a6fa9d2079e500223d8"}, + {file = "autodoc_pydantic-1.9.0.tar.gz", hash = "sha256:0f35f8051abe77b5ae16d8a1084c47a5871435e2ca9060e36c838d063c03cc89"}, +] + +[package.dependencies] +pydantic = ">=1.5,<2.0.0" +Sphinx = ">=3.4" + +[package.extras] +dev = ["coverage (>=7,<8)", "flake8 (>=3,<4)", "pytest (>=7,<8)", "sphinx-copybutton (>=0.4,<0.5)", "sphinx-rtd-theme (>=1.0,<2.0)", "sphinx-tabs (>=3,<4)", "sphinxcontrib-mermaid (>=0.7,<0.8)", "tox (>=3,<4)"] +docs = ["sphinx-copybutton (>=0.4,<0.5)", "sphinx-rtd-theme (>=1.0,<2.0)", "sphinx-tabs (>=3,<4)", "sphinxcontrib-mermaid (>=0.7,<0.8)"] +erdantic = ["erdantic (>=0.5,<0.6)"] +test = ["coverage (>=7,<8)", "pytest (>=7,<8)"] + +[[package]] +name = "babel" +version = "2.12.1" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, +] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "bleak" +version = "0.20.2" +description = "Bluetooth Low Energy platform Agnostic Klient" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "bleak-0.20.2-py3-none-any.whl", hash = "sha256:ce3106b7258212d92bb77be06f9301774f51f5bbc9f7cd50976ad794e9514dba"}, + {file = "bleak-0.20.2.tar.gz", hash = "sha256:6c92a47abe34e6dea8ffc5cea9457cbff6e1be966854839dbc25cddb36b79ee4"}, +] + +[package.dependencies] +async-timeout = {version = ">=3.0.0,<5", markers = "python_version < \"3.11\""} +bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\""} +dbus-fast = {version = ">=1.83.0,<2.0.0", markers = "platform_system == \"Linux\""} +pyobjc-core = {version = ">=9.0.1,<10.0.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-CoreBluetooth = {version = ">=9.0.1,<10.0.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-libdispatch = {version = ">=9.0.1,<10.0.0", markers = "platform_system == \"Darwin\""} + +[[package]] +name = "bleak-winrt" +version = "1.2.0" +description = "Python WinRT bindings for Bleak" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, + {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, + {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, + {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, + {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, + {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, + {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, + {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, + {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, + {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, + {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, +] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[[package]] +name = "click" +version = "8.1.6" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "construct" +version = "2.10.68" +description = "A powerful declarative symmetric parser/builder for binary data" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "construct-2.10.68.tar.gz", hash = "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45"}, +] + +[package.extras] +extras = ["arrow", "cloudpickle", "enum34", "lz4", "numpy", "ruamel.yaml"] + +[[package]] +name = "construct-typing" +version = "0.6.2" +description = "Extension for the python package 'construct' that adds typing features" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "construct-typing-0.6.2.tar.gz", hash = "sha256:948e998cfc003681dc34f2d071c3a688cf35b805cbe107febbc488ef967ccba1"}, + {file = "construct_typing-0.6.2-py3-none-any.whl", hash = "sha256:ebea6989ac622d0c4eb457092cef0c7bfbcfa110bd018670fea7064d0bc09e47"}, +] + +[package.dependencies] +construct = "2.10.68" +typing-extensions = ">=4.6.0" + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "coverage-badge" +version = "1.1.0" +description = "Generate coverage badges for Coverage.py." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "coverage-badge-1.1.0.tar.gz", hash = "sha256:c824a106503e981c02821e7d32f008fb3984b2338aa8c3800ec9357e33345b78"}, + {file = "coverage_badge-1.1.0-py2.py3-none-any.whl", hash = "sha256:e365d56e5202e923d1b237f82defd628a02d1d645a147f867ac85c58c81d7997"}, +] + +[package.dependencies] +coverage = "*" + +[[package]] +name = "darglint" +version = "1.8.1" +description = "A utility for ensuring Google-style docstrings stay up to date with the source code." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, + {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, +] + +[[package]] +name = "dbus-fast" +version = "1.91.2" +description = "A faster version of dbus-next" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "dbus_fast-1.91.2-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:43125edae7fd4ea166dbfc3bd41a8dcc2b837c0a34c9bd3b80c4770090f389d7"}, + {file = "dbus_fast-1.91.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4490900906dbcfe60680cd0c0cffebb198d44eebea9913734e37eaf87f8544b3"}, + {file = "dbus_fast-1.91.2-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:aaec7799bb21e412efdc46ee2e27b33ceec93687d27bf0fb9912618b30fd757a"}, + {file = "dbus_fast-1.91.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cf5bc7e6fb14ffa852b721df230d056d530bdd0365c6b06329004d93820dc94"}, + {file = "dbus_fast-1.91.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1b9f7bfbe89c85e950c49aaffbef145d9f6273efe1d7f633fd95ea023d462abd"}, + {file = "dbus_fast-1.91.2-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:95456895fe7b033e44072b1a1d4249a14d8a059bc4ea23898678ed114c2a05d8"}, + {file = "dbus_fast-1.91.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4276e7b6d15a2a747166415e4dad84ad81671b05dd7e736edf1ebcf61bb0f4"}, + {file = "dbus_fast-1.91.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7d79493623e0ef8bad0cc91a0a2039e31e3b83e0d1c6b52b5c66c83c4d95854b"}, + {file = "dbus_fast-1.91.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b988968be9a7130e7a48922def947ab7d4c92e379344d49c082369afdd27a9fa"}, + {file = "dbus_fast-1.91.2-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:ff3faaf133711c88c7c2d65a944f9bba45b68a5e2aacf883a361244e89389cd1"}, + {file = "dbus_fast-1.91.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2100474cd1e4cdb5edb0067b09ae2897b9d6a44fbe8b7001191831439f387561"}, + {file = "dbus_fast-1.91.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:47f122b915a4f465d6668b290382b9d186f4d12e7b9a3154fcadfb69859cd0cb"}, + {file = "dbus_fast-1.91.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:431c4f4d17119936b3669cd00cfa58c19181b54f1174a753709c31d184b9748f"}, + {file = "dbus_fast-1.91.2-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c0b8d61d8085d6fd0ef5d6facd0e4724efebfead7cef4abe55bb36556b77f491"}, + {file = "dbus_fast-1.91.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57b8fb842ca697a650ca25df1c450fecbb7ee61eae11a023c0e78beb89637d17"}, + {file = "dbus_fast-1.91.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4113fbd5176d8833b201e07476597860a1970f5c878f0bdaa162a68fa0d19e79"}, + {file = "dbus_fast-1.91.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:16eeb1e101d185332d72f50f6b950e89e39dce72b65657a23aaac09082c75641"}, + {file = "dbus_fast-1.91.2-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:6e17f673a64d06014915d2a44c1e25fb7bf64c7042cf97d7103cda6eb698c97d"}, + {file = "dbus_fast-1.91.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:171556d70107e2095c7bf671e17f2a5f146f6e39383b16c8ffa61128ae5bccf6"}, + {file = "dbus_fast-1.91.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0cdc1046f53be21afdb2f251fbf2d5f2bf2e972a71e0490879f9197090890e29"}, + {file = "dbus_fast-1.91.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b87c15dd93566fe2fe0847af646ca0630faa0804a6ad008850893227ed91b48a"}, + {file = "dbus_fast-1.91.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:25935439e366b32938aa1c50d4c450cb2c95a16dfdf0a84a5c390c18554f5e09"}, + {file = "dbus_fast-1.91.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c01e81622d52679e7ffe66ce0a5e24a9c5c4a769319fd6078dd1c4ded95f2e3"}, + {file = "dbus_fast-1.91.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:972ac0d7ad24be4f6cad33387971e361041b3bea18b88c1e9e06d406823a0e65"}, + {file = "dbus_fast-1.91.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c7ffc2b930e050ddef58c82d6ebfcff0b1f121de385b24adfd373f1ab60f44b"}, + {file = "dbus_fast-1.91.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1e91ebca89efb53d9f0387a94922fda69d0e91fecb826a70214f78a65e27d781"}, + {file = "dbus_fast-1.91.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8824c374af2047c1110a8cb497b271feb10aee138181473570dff4cc6d71e732"}, + {file = "dbus_fast-1.91.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b582bcc524dfacf99ce21b76ec9b1cc39586d02ff5598b4a96310be4329b8033"}, + {file = "dbus_fast-1.91.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30b91698fb28c7ff11ca2de10582addd80582ee3fba6bb067f874e098ee8c0e0"}, + {file = "dbus_fast-1.91.2.tar.gz", hash = "sha256:648b70804da35c92ac44af1d321aeb19df6596dbad362adc2f2507fd99f0f5ae"}, +] + +[[package]] +name = "dill" +version = "0.3.7" +description = "serialize all of Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "docutils" +version = "0.18.1" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, + {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "ifaddr" +version = "0.2.0" +description = "Cross-platform network interface and IP address enumeration library" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748"}, + {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.8.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, + {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.9.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.5.1" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, + {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, + {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, + {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, + {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, + {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, + {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, + {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, + {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, + {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, + {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, + {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, + {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, + {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, + {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, + {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, + {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, + {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "mypy-protobuf" +version = "3.3.0" +description = "Generate mypy stub files from protobuf specs" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-protobuf-3.3.0.tar.gz", hash = "sha256:24f3b0aecb06656e983f58e07c732a90577b9d7af3e1066fc2b663bbf0370248"}, + {file = "mypy_protobuf-3.3.0-py3-none-any.whl", hash = "sha256:15604f6943b16c05db646903261e3b3e775cf7f7990b7c37b03d043a907b650d"}, +] + +[package.dependencies] +protobuf = ">=3.19.4" +types-protobuf = ">=3.19.12" + +[[package]] +name = "numpy" +version = "1.25.2" +description = "Fundamental package for array computing in Python" +category = "main" +optional = true +python-versions = ">=3.9" +files = [ + {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, + {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, + {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, + {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, + {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, + {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, + {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, + {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, + {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, + {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, + {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, +] + +[[package]] +name = "opencv-python" +version = "4.8.0.76" +description = "Wrapper package for OpenCV python bindings." +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "opencv-python-4.8.0.76.tar.gz", hash = "sha256:56d84c43ce800938b9b1ec74b33942b2edbcef3f70c2754eb9bfe5dff1ee3ace"}, + {file = "opencv_python-4.8.0.76-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:67bce4b9aad307c98a9a07c6afb7de3a4e823c1f4991d6d8e88e229e7dfeee59"}, + {file = "opencv_python-4.8.0.76-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:48eb3121d809a873086d6677565e3ac963e6946110d13cd115533fa70e2aa2eb"}, + {file = "opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93871871b1c9d6b125cddd45b0638a2fa01ee9fd37f5e428823f750e404f2f15"}, + {file = "opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcb4944211acf13742dbfd9d3a11dc4e36353ffa1746f2c7dcd6a01c32d1376"}, + {file = "opencv_python-4.8.0.76-cp37-abi3-win32.whl", hash = "sha256:b2349dc9f97ed6c9ba163d0a7a24bcef9695a3e216cd143e92f1b9659c5d9a49"}, + {file = "opencv_python-4.8.0.76-cp37-abi3-win_amd64.whl", hash = "sha256:ba32cfa75a806abd68249699d34420737d27b5678553387fc5768747a6492147"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, + {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, + {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, + {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, +] + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pillow" +version = "9.5.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "3.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "poethepoet" +version = "0.15.0" +description = "A task runner that works well with poetry." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "poethepoet-0.15.0-py3-none-any.whl", hash = "sha256:8ca49d8a9928a3ce1753315d6df0866888557eccb0fe37a8c88fea47454cfe12"}, + {file = "poethepoet-0.15.0.tar.gz", hash = "sha256:5843260c9074b6c42bf2e51f21107efe37e230cf75da3dd3f4b43904f365b26c"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +tomli = ">=1.2.2" + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + +[[package]] +name = "protobuf" +version = "3.20.3" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "protobuf-3.20.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99"}, + {file = "protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e"}, + {file = "protobuf-3.20.3-cp310-cp310-win32.whl", hash = "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c"}, + {file = "protobuf-3.20.3-cp310-cp310-win_amd64.whl", hash = "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7"}, + {file = "protobuf-3.20.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469"}, + {file = "protobuf-3.20.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4"}, + {file = "protobuf-3.20.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4"}, + {file = "protobuf-3.20.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454"}, + {file = "protobuf-3.20.3-cp37-cp37m-win32.whl", hash = "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905"}, + {file = "protobuf-3.20.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c"}, + {file = "protobuf-3.20.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7"}, + {file = "protobuf-3.20.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"}, + {file = "protobuf-3.20.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050"}, + {file = "protobuf-3.20.3-cp38-cp38-win32.whl", hash = "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86"}, + {file = "protobuf-3.20.3-cp38-cp38-win_amd64.whl", hash = "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9"}, + {file = "protobuf-3.20.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b"}, + {file = "protobuf-3.20.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b"}, + {file = "protobuf-3.20.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402"}, + {file = "protobuf-3.20.3-cp39-cp39-win32.whl", hash = "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480"}, + {file = "protobuf-3.20.3-cp39-cp39-win_amd64.whl", hash = "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7"}, + {file = "protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db"}, + {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, +] + +[[package]] +name = "protoletariat" +version = "3.2.16" +description = "Python protocol buffers for the rest of us" +category = "dev" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "protoletariat-3.2.16-py3-none-any.whl", hash = "sha256:d9dba0ec7f0d5332d0c8621d07b3164293d983d1f970dcbb2c83c6585cdc39ff"}, + {file = "protoletariat-3.2.16.tar.gz", hash = "sha256:af8587d9ef976b8853791291616da58c2ae1ca7aaef0e9b545bf364b42c767f3"}, +] + +[package.dependencies] +click = ">=8,<9" +protobuf = ">=3.19.1,<5" + +[package.extras] +grpcio-tools = ["grpcio-tools (>=1.42.0,<2)"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pydantic" +version = "1.10.12" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, + {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, + {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, + {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, + {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, + {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, + {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, + {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, + {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, + {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" +tomli = {version = ">=1.2.3", optional = true, markers = "python_version < \"3.11\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pylint" +version = "2.17.5" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"}, + {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"}, +] + +[package.dependencies] +astroid = ">=2.15.6,<=2.17.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, +] +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pyobjc-core" +version = "9.2" +description = "Python<->ObjC Interoperability Module" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyobjc-core-9.2.tar.gz", hash = "sha256:d734b9291fec91ff4e3ae38b9c6839debf02b79c07314476e87da8e90b2c68c3"}, + {file = "pyobjc_core-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa674a39949f5cde8e5c7bbcd24496446bfc67592b028aedbec7f81dc5fc4daa"}, + {file = "pyobjc_core-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bbc8de304ee322a1ee530b4d2daca135a49b4a49aa3cedc6b2c26c43885f4842"}, + {file = "pyobjc_core-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0fa950f092673883b8bd28bc18397415cabb457bf410920762109b411789ade9"}, + {file = "pyobjc_core-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:586e4cae966282eaa61b21cae66ccdcee9d69c036979def26eebdc08ddebe20f"}, + {file = "pyobjc_core-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41189c2c680931c0395a55691763c481fc681f454f21bb4f1644f98c24a45954"}, + {file = "pyobjc_core-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:2d23ee539f2ba5e9f5653d75a13f575c7e36586fc0086792739e69e4c2617eda"}, + {file = "pyobjc_core-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b9809cf96678797acb72a758f34932fe8e2602d5ab7abec15c5ac68ddb481720"}, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "9.2" +description = "Wrappers for the Cocoa frameworks on macOS" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-Cocoa-9.2.tar.gz", hash = "sha256:efd78080872d8c8de6c2b97e0e4eac99d6203a5d1637aa135d071d464eb2db53"}, + {file = "pyobjc_framework_Cocoa-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9e02d8a7cc4eb7685377c50ba4f17345701acf4c05b1e7480d421bff9e2f62a4"}, + {file = "pyobjc_framework_Cocoa-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3b1e6287b3149e4c6679cdbccd8e9ef6557a4e492a892e80a77df143f40026d2"}, + {file = "pyobjc_framework_Cocoa-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:312977ce2e3989073c6b324c69ba24283de206fe7acd6dbbbaf3e29238a22537"}, + {file = "pyobjc_framework_Cocoa-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aae7841cf40c26dd915f4dd828f91c6616e6b7998630b72e704750c09e00f334"}, + {file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:739a421e14382a46cbeb9a883f192dceff368ad28ec34d895c48c0ad34cf2c1d"}, + {file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:32d9ac1033fac1b821ddee8c68f972a7074ad8c50bec0bea9a719034c1c2fb94"}, + {file = "pyobjc_framework_Cocoa-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b236bb965e41aeb2e215d4e98a5a230d4b63252c6d26e00924ea2e69540a59d6"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "9.2" +description = "Wrappers for the framework CoreBluetooth on macOS" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-CoreBluetooth-9.2.tar.gz", hash = "sha256:cb2481b1dfe211ae9ce55f36537dc8155dbf0dc8ff26e0bc2e13f7afb0a291d1"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:53d888742119d0f0c725d0b0c2389f68e8f21f0cba6d6aec288c53260a0196b6"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:179532882126526e38fe716a50fb0ee8f440e0b838d290252c515e622b5d0e49"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:256a5031ea9d8a7406541fa1b0dfac549b1de93deae8284605f9355b13fb58be"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" +pyobjc-framework-Cocoa = ">=9.2" + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "9.2" +description = "Wrappers for libdispatch on macOS" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-libdispatch-9.2.tar.gz", hash = "sha256:542e7f7c2b041939db5ed6f3119c1d67d73ec14a996278b92485f8513039c168"}, + {file = "pyobjc_framework_libdispatch-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88d4091d4bcb5702783d6e86b4107db973425a17d1de491543f56bd348909b60"}, + {file = "pyobjc_framework_libdispatch-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a67b007113328538b57893cc7829a722270764cdbeae6d5e1460a1d911314df"}, + {file = "pyobjc_framework_libdispatch-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6fccea1a57436cf1ac50d9ebc6e3e725bcf77f829ba6b118e62e6ed7866d359d"}, + {file = "pyobjc_framework_libdispatch-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6eba747b7ad91b0463265a7aee59235bb051fb97687f35ca2233690369b5e4e4"}, + {file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e835495860d04f63c2d2f73ae3dd79da4222864c107096dc0f99e8382700026"}, + {file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1b107e5c3580b09553030961ea6b17abad4a5132101eab1af3ad2cb36d0f08bb"}, + {file = "pyobjc_framework_libdispatch-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:83cdb672acf722717b5ecf004768f215f02ac02d7f7f2a9703da6e921ab02222"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" + +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.17.2" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.17.2.tar.gz", hash = "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4"}, + {file = "pytest_asyncio-0.17.2-py3-none-any.whl", hash = "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d"}, +] + +[package.dependencies] +pytest = ">=6.1.0" + +[package.extras] +testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-html" +version = "3.2.0" +description = "pytest plugin for generating HTML reports" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-html-3.2.0.tar.gz", hash = "sha256:c4e2f4bb0bffc437f51ad2174a8a3e71df81bbc2f6894604e604af18fbe687c3"}, + {file = "pytest_html-3.2.0-py3-none-any.whl", hash = "sha256:868c08564a68d8b2c26866f1e33178419bb35b1e127c33784a28622eb827f3f3"}, +] + +[package.dependencies] +py = ">=1.8.2" +pytest = ">=5.0,<6.0.0 || >6.0.0" +pytest-metadata = "*" + +[[package]] +name = "pytest-metadata" +version = "3.0.0" +description = "pytest plugin for test session metadata" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_metadata-3.0.0-py3-none-any.whl", hash = "sha256:a17b1e40080401dc23177599208c52228df463db191c1a573ccdffacd885e190"}, + {file = "pytest_metadata-3.0.0.tar.gz", hash = "sha256:769a9c65d2884bd583bc626b0ace77ad15dbe02dd91a9106d47fd46d9c2569ca"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "tox (>=3.24.5)"] + +[[package]] +name = "pytest-timeout" +version = "2.1.0" +description = "pytest plugin to abort hanging tests" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-timeout-2.1.0.tar.gz", hash = "sha256:c07ca07404c612f8abbe22294b23c368e2e5104b521c1790195561f37e1ac3d9"}, + {file = "pytest_timeout-2.1.0-py3-none-any.whl", hash = "sha256:f6f50101443ce70ad325ceb4473c4255e9d74e3c7cd0ef827309dfa4c0d975c6"}, +] + +[package.dependencies] +pytest = ">=5.0.0" + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-mock" +version = "1.11.0" +description = "Mock out responses from the requests package" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "requests-mock-1.11.0.tar.gz", hash = "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4"}, + {file = "requests_mock-1.11.0-py2.py3-none-any.whl", hash = "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15"}, +] + +[package.dependencies] +requests = ">=2.3,<3" +six = "*" + +[package.extras] +fixture = ["fixtures"] +test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testtools"] + +[[package]] +name = "rich" +version = "12.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.3,<4.0.0" +files = [ + {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, + {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, +] + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "5.3.0" +description = "Python documentation generator" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, + {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, +] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.20" +imagesize = ">=1.3" +importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.12" +requests = ">=2.5.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] + +[[package]] +name = "sphinx-rtd-theme" +version = "1.2.2" +description = "Read the Docs theme for Sphinx" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "sphinx_rtd_theme-1.2.2-py2.py3-none-any.whl", hash = "sha256:6a7e7d8af34eb8fc57d52a09c6b6b9c46ff44aea5951bc831eeb9245378f3689"}, + {file = "sphinx_rtd_theme-1.2.2.tar.gz", hash = "sha256:01c5c5a72e2d025bd23d1f06c59a4831b06e6ce6c01fdd5ebfe9986c0a880fc7"}, +] + +[package.dependencies] +docutils = "<0.19" +sphinx = ">=1.6,<7" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.7" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +category = "dev" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_applehelp-1.0.7-py3-none-any.whl", hash = "sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d"}, + {file = "sphinxcontrib_applehelp-1.0.7.tar.gz", hash = "sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.5" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +category = "dev" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_devhelp-1.0.5-py3-none-any.whl", hash = "sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f"}, + {file = "sphinxcontrib_devhelp-1.0.5.tar.gz", hash = "sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.4" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_htmlhelp-2.0.4-py3-none-any.whl", hash = "sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9"}, + {file = "sphinxcontrib_htmlhelp-2.0.4.tar.gz", hash = "sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +category = "dev" +optional = false +python-versions = ">=2.7" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.6" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +category = "dev" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_qthelp-1.0.6-py3-none-any.whl", hash = "sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4"}, + {file = "sphinxcontrib_qthelp-1.0.6.tar.gz", hash = "sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.8" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +category = "dev" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_serializinghtml-1.1.8-py3-none-any.whl", hash = "sha256:27849e7227277333d3d32f17c138ee148a51fa01f888a41cd6d4e73bcabe2d06"}, + {file = "sphinxcontrib_serializinghtml-1.1.8.tar.gz", hash = "sha256:aaf3026335146e688fd209b72320314b1b278320cf232e3cda198f873838511a"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxemoji" +version = "0.2.0" +description = "An extension to use emoji codes in your Sphinx documentation" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "sphinxemoji-0.2.0.tar.gz", hash = "sha256:27861d1dd7c6570f5e63020dac9a687263f7481f6d5d6409eb31ecebcc804e4c"}, +] + +[package.dependencies] +sphinx = ">=1.8" + +[[package]] +name = "tk" +version = "0.1.0" +description = "TensorKit is a deep learning helper between Python and C++." +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "tk-0.1.0-py3-none-any.whl", hash = "sha256:703a69ff0d5ba2bd2f7440582ad10160e4a6561595d33457dc6caa79b9bf4930"}, + {file = "tk-0.1.0.tar.gz", hash = "sha256:60bc8923d5d35f67f5c6bd93d4f0c49d2048114ec077768f959aef36d4ed97f8"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.1" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, + {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, +] + +[[package]] +name = "types-attrs" +version = "19.1.0" +description = "Typing stubs for attrs" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types_attrs-19.1.0-py2.py3-none-any.whl", hash = "sha256:d11acf7a2531a7c52a740c30fa3eb8d01d3066c10d34c01ff5e59502caac5352"}, +] + +[[package]] +name = "types-protobuf" +version = "4.24.0.1" +description = "Typing stubs for protobuf" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-protobuf-4.24.0.1.tar.gz", hash = "sha256:90adea3b693d6a40d8ef075c58fe6b5cc6e01fe1496301a7e6fc70398dcff92e"}, + {file = "types_protobuf-4.24.0.1-py3-none-any.whl", hash = "sha256:df203a204e4ae97d4cca4c9cf725262579dd7857a19f9e7fc74871ccfa073c01"}, +] + +[[package]] +name = "types-pytz" +version = "2023.3.0.1" +description = "Typing stubs for pytz" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-pytz-2023.3.0.1.tar.gz", hash = "sha256:1a7b8d4aac70981cfa24478a41eadfcd96a087c986d6f150d77e3ceb3c2bdfab"}, + {file = "types_pytz-2023.3.0.1-py3-none-any.whl", hash = "sha256:65152e872137926bb67a8fe6cc9cfd794365df86650c5d5fdc7b167b0f38892e"}, +] + +[[package]] +name = "types-requests" +version = "2.31.0.2" +description = "Typing stubs for requests" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"}, + {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"}, +] + +[package.dependencies] +types-urllib3 = "*" + +[[package]] +name = "types-tzlocal" +version = "5.0.1.1" +description = "Typing stubs for tzlocal" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-tzlocal-5.0.1.1.tar.gz", hash = "sha256:d0a9fb6f846c0416f18a040f2d15714d3d52a6165c541e9f4eef7356b53af011"}, + {file = "types_tzlocal-5.0.1.1-py3-none-any.whl", hash = "sha256:55a66e06e0332d6e04886cf578953dcc465b3054e3f0a337da3bdcbaa59122a4"}, +] + +[package.dependencies] +types-pytz = "*" + +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +description = "Typing stubs for urllib3" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, + {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "tzlocal" +version = "5.0.1" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"}, + {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[[package]] +name = "urllib3" +version = "2.0.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, + {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + +[[package]] +name = "zeroconf" +version = "0.80.0" +description = "A pure python implementation of multicast DNS service discovery" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "zeroconf-0.80.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:603d753065c9b71d85b9261d8f7b5c1bc8581312a1446144b272c352630c606e"}, + {file = "zeroconf-0.80.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:06151609299c90673037e70ab03e6272cf0ada4bf97568cb39a32e8783c6bc84"}, + {file = "zeroconf-0.80.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae86c7c9e28c824d4511dd43eee2d5e6ab837657919a57324defa33911c358d"}, + {file = "zeroconf-0.80.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:8677ca21f38c6c77ced4471115ba60e1b1d5c1ff25b9f0e2b2d52a69ce96c5bc"}, + {file = "zeroconf-0.80.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fe77e0e5706f00f741c84600e489c850255768a97b23463bfafe1caa4ae37585"}, + {file = "zeroconf-0.80.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c449ca12ff53b0657d28d82347e08c9a4fa16ebc553bd13f16416f47b5f1521"}, + {file = "zeroconf-0.80.0-cp310-cp310-win32.whl", hash = "sha256:256b3584a3ede5c32efa6721de92ff0b898f2d5562de0c03afdce4776200eb97"}, + {file = "zeroconf-0.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:7d76962efbe4eaef060b9ef7185fa49418233b1d3afd229cfed5c506329e40be"}, + {file = "zeroconf-0.80.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:f2784aa28375cc7dd1906f566d62eaa31f0a3f5d594830072f3946b999b9d7a8"}, + {file = "zeroconf-0.80.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3befafe9bfc903b8bcdd52178ccb8b3a395c2f98aa3b60619a5f37ba99d71f97"}, + {file = "zeroconf-0.80.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0c990d3a17a2c90cd6254569343a68b3ea9d282aa22983f5b1766da4c7cba5"}, + {file = "zeroconf-0.80.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:010e4334b9919581072fd0e47dba0137a48251f37b8182b161d536641d04bac2"}, + {file = "zeroconf-0.80.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:39c6e78cd4f33b96d2bd2d4c18b623fbc02b1733fa4aafcfdd89682b9998f520"}, + {file = "zeroconf-0.80.0-cp311-cp311-win32.whl", hash = "sha256:cbc761a79e9d115227b340bab77caf5ef531026395030ec913fcf34053d8a834"}, + {file = "zeroconf-0.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:61e21280234556d4602125c4beb5a56ce63563e1c8493b8154a3f57ea5b0197e"}, + {file = "zeroconf-0.80.0-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:512a9ce69564cd437cb33be7d0739745c27fe8e932b5bdcdeb996da5c0e07f6d"}, + {file = "zeroconf-0.80.0-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:83b81086789b8ba3eab27311b3ae8f8732ed61f85cee0540c8e36731963ed696"}, + {file = "zeroconf-0.80.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0614195eb5904c97e4b8c3ca2ec3b710ff856bf824b196991a121cfe70f0d67c"}, + {file = "zeroconf-0.80.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c26829f24a28cdfe349b780de6b5a60b45562e5cd83c0a044ec0da2c63862e47"}, + {file = "zeroconf-0.80.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:eb35d4a2f2dfd891f46d399b204449b2a5de8c6f421e227ba5742a2d95f257af"}, + {file = "zeroconf-0.80.0-cp37-cp37m-win32.whl", hash = "sha256:4f3fde5b0bbcce95967d51c00ce907ef59b17bfcd29e5ed8498c8733c07454ac"}, + {file = "zeroconf-0.80.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ef12724ef61e54c28f948ba424120e0df9956633b8f855fe9460ddae2fe3d3ed"}, + {file = "zeroconf-0.80.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:83b3265db6a4f69153c7f4b050ca903b0e395993883df3822c4694920da177f4"}, + {file = "zeroconf-0.80.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:94c23488c6e20102563913a771faf2691b221027d7ee6118758a2979acef5805"}, + {file = "zeroconf-0.80.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a24c0b02f875c6de800725734218fcda3ba7d939203b503ddcfd4e1fdf41b09e"}, + {file = "zeroconf-0.80.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f97785697a4020b68ce45d0fdb06af7357b2c5cc836f6bc8fb79f8599bb1c02c"}, + {file = "zeroconf-0.80.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7058fed1ae7e3697e31be5df7f0a70b247904ae045825852efec33db2ea374a3"}, + {file = "zeroconf-0.80.0-cp38-cp38-win32.whl", hash = "sha256:6e5b9d56c5dfead6780452b494a0bf2962201e0538fc1cab155e0d3ee0e9187b"}, + {file = "zeroconf-0.80.0-cp38-cp38-win_amd64.whl", hash = "sha256:3a846e5f4dd5cd21aa7399f7c5c3c0029b6102191b7f46e3f1a8bf410632142a"}, + {file = "zeroconf-0.80.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:2c558a059f0ad8c4aab204f346ee02c860af84dfd25b3d1048520c0d6938adfc"}, + {file = "zeroconf-0.80.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:ef15450c7b6b8a60a8d4ec6eb8a5fffe7c021203e898745087670dba1a39aec1"}, + {file = "zeroconf-0.80.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a2cee02d96be0f315edeee5808d61c02cc9251723e12e66659ac162715c074"}, + {file = "zeroconf-0.80.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:735b2513c3dc6fffd8f39fd76f9e586a49c3228a431a855f562a6aa0456bc4d0"}, + {file = "zeroconf-0.80.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2a92b39f4d896e47b760715ffc99029f655a2d4f20836667693e48be039a2db7"}, + {file = "zeroconf-0.80.0-cp39-cp39-win32.whl", hash = "sha256:fa71d26c123c5e4e1468a3c4eef230ee75efbefe739d7ce469225f11f9c7a6f3"}, + {file = "zeroconf-0.80.0-cp39-cp39-win_amd64.whl", hash = "sha256:9537513bec501f1b197423f87f3d9aa00c526982a0fd7f1480ce0fd0f699d848"}, + {file = "zeroconf-0.80.0-pp37-pypy37_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5e2988f75bbcfaa13d954231cc7c80088b8ac51a9f90bacfa9c0bbdcd456b117"}, + {file = "zeroconf-0.80.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:7c53dc3b2dbfb73e6c8fbcbb37d7f08fd92bd122ea8c579bb8fe17f8ba95b9dc"}, + {file = "zeroconf-0.80.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da026ef285c7db97baffd18757e295fe5637e036ea02baaa6ee66f72ebb3a77"}, + {file = "zeroconf-0.80.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:33a9d8644b1d3eda480a63969f66094339427b81f11db24c80b695630c33f2df"}, + {file = "zeroconf-0.80.0-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:c76c1f77a086967c315ef6df8414ad5dc64436e5a31c1ca6eea6f7ddba0e042e"}, + {file = "zeroconf-0.80.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:6d2d79bbb31e645025b0634609ec7ff9718cf2393a93c3b3a4f5b6fec1bfad09"}, + {file = "zeroconf-0.80.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:989877bb4625b57ea0e26ca54c495c17ed41879e40adb296d17274a281b75aee"}, + {file = "zeroconf-0.80.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:11b18b613e0868b802f65d5850ac815b52e15b586e8b493718581f49672c8d71"}, + {file = "zeroconf-0.80.0-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:291e9992e56f6696132a41cfb58f73d45b736cf2707a4fdf653b9bfdb6329876"}, + {file = "zeroconf-0.80.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0fade43887a2ad37d1666a20b53f498426388d8ab66f5ceaa6455fabf4921efb"}, + {file = "zeroconf-0.80.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03aecfecadcd7b6c8b618fb547e6df1792965c9598af60c0e61f96cd327cbee9"}, + {file = "zeroconf-0.80.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:839eb3af2b38a0fb4bd95a0dc96e4785dbff597f461cfd3978f82f11f42338b5"}, + {file = "zeroconf-0.80.0.tar.gz", hash = "sha256:fb10eaa6938f2c0237f653f831429d848c83e23a81c50358ae9a8142b50e9565"}, +] + +[package.dependencies] +async-timeout = {version = ">=3.0.0", markers = "python_version < \"3.11\""} +ifaddr = ">=0.1.7" + +[[package]] +name = "zipp" +version = "3.16.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, + {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[extras] +gui = ["Pillow", "opencv-python", "tk"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.12" +content-hash = "4dddeafabbb8f1aa084e294a8820b0ce40cfc63cbce9e2b956e8ce6d4b434888" diff --git a/demos/python/sdk_wireless_camera_control/pyproject.toml b/demos/python/sdk_wireless_camera_control/pyproject.toml index 25314d06..1dc83ac3 100644 --- a/demos/python/sdk_wireless_camera_control/pyproject.toml +++ b/demos/python/sdk_wireless_camera_control/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] [tool.poetry.scripts] @@ -26,18 +27,18 @@ gopro-photo = "open_gopro.demos.photo:entrypoint" gopro-video = "open_gopro.demos.video:entrypoint" gopro-log-battery = "open_gopro.demos.log_battery:entrypoint" gopro-wifi = "open_gopro.demos.connect_wifi:entrypoint" -gopro-presets = "open_gopro.demos.preset_control:entrypoint" gopro-webcam = "open_gopro.demos.gui.webcam:entrypoint" -gopro-gui = "open_gopro.demos.gui.gui_demo:entrypoint" gopro-livestream = "open_gopro.demos.gui.livestream:entrypoint" +gopro-preview-stream = "open_gopro.demos.gui.preview_stream:entrypoint" +gopro-gui = "open_gopro.demos.gui.gui_demo:entrypoint" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] -python = ">=3.9,<3.11" -bleak = "=0.19.5" +python = ">=3.9,<3.12" +bleak = "=0.20.2" construct = "^2" wrapt = "^1" requests = "^2" @@ -45,10 +46,13 @@ protobuf = "^3" packaging = "^21" rich = "^12" pexpect = "^4" -zeroconf = "^0.39.4" +zeroconf = "^0" +pydantic = "^1" opencv-python = { version = "^4", optional = true } tk = { version= "^0.1", optional = true } Pillow = {version= "^9", optional = true} +pytz = "^2023.3" +tzlocal = "^5.0.1" [tool.poetry.extras] gui = ["opencv-python", "tk", "pillow"] @@ -66,20 +70,23 @@ pylint = "^2" mypy = "*" types-requests = "^2" types-attrs = "^19" +types-pytz = "^2023.3.0.1" +types-tzlocal = "^5.0.1.1" mypy-protobuf = "^3" -construct-typing = "^0.5" +construct-typing = "^0" sphinx = "^5" sphinx-rtd-theme = "^1" sphinxemoji = "^0" coverage-badge = "=1.1.0" darglint = "^1" poethepoet = "^0.15" -protoletariat = "^0.9" -nox = "=2022.8.7" -nox-poetry = "=1.0.1" +autodoc-pydantic = "^1" +pytest-timeout = "^2" +isort = "^5" +protoletariat = "^3" [tool.poe.tasks.unit_tests] -cmd = "pytest tests/unit --cov-fail-under=60" +cmd = "pytest tests/unit --cov-fail-under=70" help = "Run unit tests" [tool.poe.tasks.types] @@ -87,13 +94,21 @@ cmd = "mypy open_gopro" help = "Check types" [tool.poe.tasks.lint] -cmd = "pylint --no-docstring-rgx=__|main|parse_arguments|entrypoint open_gopro" +cmd = "pylint open_gopro" help = "Run pylint" -[tool.poe.tasks.format] -cmd = "black open_gopro tests" +[tool.poe.tasks.format_code] +cmd = "black open_gopro tests noxfile.py docs/conf.py" help = "Apply black formatting to source code" +[tool.poe.tasks.sort_imports] +cmd = "isort open_gopro tests" +help = "Sort imports with isort" + +[tool.poe.tasks.format] +sequence = ["format_code", "sort_imports"] +help = "Format code and sort imports" + [tool.poe.tasks.pydocstyle] cmd = "pydocstyle --config pyproject.toml -v open_gopro" help = "check docstrings style" @@ -168,6 +183,7 @@ log_file_level = "DEBUG" log_file_format = "%(threadName)13s: %(name)40s:%(lineno)5d %(asctime)s.%(msecs)03d %(levelname)-8s | %(message)s" log_file_date_format = "%H:%M:%S" filterwarnings = "ignore::DeprecationWarning" +# timeout = 10 addopts = [ "-s", "--capture=tee-sys", @@ -183,7 +199,7 @@ addopts = [ data_file = ".reports/coverage/.coverage" branch = true source = ["open_gopro"] -omit = ["*/constants.py", "*/params.py", "open_gopro/demos*"] +omit = ["*/constants.py", "*/params.py", "open_gopro/proto*", "open_gopro/demos*"] [tool.coverage.html] directory = ".reports/coverage" @@ -192,6 +208,7 @@ directory = ".reports/coverage" exclude_lines = ["raise NotImplementedError"] [tool.pylint.'MASTER'] +no-docstring-rgx="__|main|parse_arguments|entrypoint" extension-pkg-whitelist = "cv2" # TODO this isn't working load-plugins = "pylint.extensions.docparams" accept-no-param-doc = "yes" @@ -228,7 +245,6 @@ disable = [ "invalid-name", "unsubscriptable-object", "no-member", # This is bad. We need to make stub files since mypy and pylint do not follow decorators well - ] [tool.pylint.'FORMAT'] @@ -238,7 +254,7 @@ max-line-length = 160 ignored-modules = "cv2" [tool.black] -line-length = 111 +line-length = 120 exclude = ".venv" [tool.pydocstyle] @@ -246,3 +262,6 @@ convention = "google" add-ignore = "D415, D107, D105" match = '(?!params|.*pb).*\.py' match-dir = '(?!.*demos).*' + +[tool.isort] +profile = "black" \ No newline at end of file diff --git a/demos/python/sdk_wireless_camera_control/tests/__init__.py b/demos/python/sdk_wireless_camera_control/tests/__init__.py index 75f0764a..88bc9941 100644 --- a/demos/python/sdk_wireless_camera_control/tests/__init__.py +++ b/demos/python/sdk_wireless_camera_control/tests/__init__.py @@ -4,7 +4,15 @@ # Open GoPro API Versions to test versions = ["2.0"] +from open_gopro import GoProResp, constants from open_gopro.api import WirelessApi # The global parser map only gets set when API is instantiated. So ensure this is done. WirelessApi(None) # type: ignore + +mock_good_response = GoProResp( + protocol=GoProResp.Protocol.BLE, + status=constants.ErrorCode.SUCCESS, + identifier="test response", + data=None, +) diff --git a/demos/python/sdk_wireless_camera_control/tests/conftest.py b/demos/python/sdk_wireless_camera_control/tests/conftest.py index 6bc9b1bd..75618167 100644 --- a/demos/python/sdk_wireless_camera_control/tests/conftest.py +++ b/demos/python/sdk_wireless_camera_control/tests/conftest.py @@ -3,48 +3,47 @@ # pylint: disable=redefined-outer-name -import re import asyncio import logging +import re +from dataclasses import dataclass from pathlib import Path -from typing import Pattern, Generic, Optional, Any -from dataclasses import dataclass, field +from typing import Any, Generic, Optional, Pattern import pytest -from open_gopro.ble.services import CharProps -from tests import versions -from open_gopro import WirelessGoPro +from open_gopro import WiredGoPro, WirelessGoPro, types +from open_gopro.api import ( + BleCommands, + BleSettings, + BleStatuses, + HttpCommands, + HttpSettings, + WirelessApi, +) from open_gopro.ble import ( BleClient, BLEController, BleDevice, BleHandle, - DisconnectHandlerType, - NotiHandlerType, - GattDB, BleUUID, - UUIDs, - Descriptor, Characteristic, + Descriptor, + DisconnectHandlerType, + GattDB, + NotiHandlerType, Service, + UUIDs, ) -from open_gopro.wifi import WifiClient, WifiController, SsidState from open_gopro.ble.adapters.bleak_wrapper import BleakWrapperController -from open_gopro.responses import GoProResp -from open_gopro.constants import ErrorCode, ProducerType, CmdId, GoProUUIDs, ResponseType -from open_gopro.interface import GoProBle, GoProWifi -from open_gopro.api import ( - WirelessApi, - BleCommands, - BleSettings, - BleStatuses, - HttpCommands, - HttpSettings, - Params, -) +from open_gopro.ble.services import CharProps +from open_gopro.communicator_interface import GoProBle, GoProWifi +from open_gopro.constants import CmdId, ErrorCode, GoProUUIDs, StatusId from open_gopro.exceptions import ConnectFailed, FailedToFindDevice -from open_gopro.util import setup_logging, set_logging_level +from open_gopro.logger import set_logging_level, setup_logging +from open_gopro.models.response import GoProResp +from open_gopro.wifi import SsidState, WifiClient, WifiController +from tests import mock_good_response, versions api_versions = {"2.0": WirelessApi} @@ -96,13 +95,13 @@ def event_loop(): @pytest.fixture(scope="module") -async def bleak_wrapper(): +async def mock_bleak_wrapper(): ble = BleakWrapperController() yield ble @pytest.fixture(scope="module") -async def bleak_client(): +async def mock_bleak_client(): def disconnected_cb(_) -> None: print("Entered test disconnect callback") @@ -112,10 +111,10 @@ def notification_cb(handle: int, data: bytearray) -> None: ble = BleClient( BleakWrapperController(), disconnected_cb, notification_cb, target=re.compile("###invalid_device###") ) - ble.open(timeout=30) + await ble.open(timeout=30) print("GoPro Bleak opened!") yield ble - ble.close() + await ble.close() ############################################################################################################## @@ -124,23 +123,23 @@ def notification_cb(handle: int, data: bytearray) -> None: @pytest.fixture() -def descriptor(): +def mock_descriptor(): yield Descriptor(0xABCD, UUIDs.CLIENT_CHAR_CONFIG) @pytest.fixture() -def characteristic(descriptor: Descriptor): - yield Characteristic(2, UUIDs.ACC_APPEARANCE, CharProps.READ, init_descriptors=[descriptor]) +def mock_characteristic(mock_descriptor: Descriptor): + yield Characteristic(2, UUIDs.ACC_APPEARANCE, CharProps.READ, init_descriptors=[mock_descriptor]) @pytest.fixture() -def service(characteristic: Characteristic): - yield Service(UUIDs.S_GENERIC_ACCESS, 3, init_chars=[characteristic]) +def mock_service(mock_characteristic: Characteristic): + yield Service(UUIDs.S_GENERIC_ACCESS, 3, init_chars=[mock_characteristic]) @pytest.fixture() -def gatt_db(service: Service): - yield GattDB([service]) +def mock_gatt_db(mock_service: Service): + yield GattDB([mock_service]) ############################################################################################################## @@ -149,43 +148,43 @@ def gatt_db(service: Service): @dataclass -class GattTable: - def handle2uuid(self): - ... +class MockGattTable: + def handle2uuid(self, *args): + return GoProUUIDs.CQ_QUERY_RESP -class BleControllerTest(BLEController, Generic[BleHandle, BleDevice]): +class MockBleController(BLEController, Generic[BleHandle, BleDevice]): # pylint: disable=signature-differs def __init__(self, *args, **kwargs) -> None: - pass + self.gatt_db = MockGattTable() - def scan(self, token: Pattern, timeout: int, service_uuids: list[BleUUID] = None) -> str: + async def scan(self, token: Pattern, timeout: int, service_uuids: list[BleUUID] = None) -> str: if token == re.compile("device"): return "scanned_device" raise FailedToFindDevice - def read(self, handle: BleHandle, uuid: str) -> bytearray: + async def read(self, handle: BleHandle, uuid: str) -> bytearray: return bytearray() - def write(self, handle: BleHandle, uuid: str, data: bytearray) -> None: + async def write(self, handle: BleHandle, uuid: str, data: bytearray) -> None: return - def connect(self, disconnect_cb: DisconnectHandlerType, device: BleDevice, timeout: int) -> str: + async def connect(self, disconnect_cb: DisconnectHandlerType, device: BleDevice, timeout: int) -> str: if disconnect_cb is None: raise ConnectFailed("forced connect fail from test", timeout, 1) return "connected_device" - def pair(self, handle: BleHandle) -> None: + async def pair(self, handle: BleHandle) -> None: return - def enable_notifications(self, handle: BleHandle, handler: NotiHandlerType) -> None: + async def enable_notifications(self, handle: BleHandle, handler: NotiHandlerType) -> None: return - def discover_chars(self, handle: BleHandle, service_uuids: list[BleUUID] = None) -> GattTable: - return GattTable() + async def discover_chars(self, handle: BleHandle, service_uuids: list[BleUUID] = None) -> MockGattTable: + return self.gatt_db - def disconnect(self, handle: BleHandle) -> None: + async def disconnect(self, handle: BleHandle) -> None: return @@ -198,9 +197,9 @@ def notification_handler(handle: int, data: bytearray) -> None: @pytest.fixture(scope="module") -async def ble_client(): +async def mock_ble_client(): test_client = BleClient( - controller=BleControllerTest(), + controller=MockBleController(), disconnected_cb=disconnection_handler, notification_cb=notification_handler, target=(re.compile("device"), []), @@ -208,36 +207,26 @@ async def ble_client(): yield test_client -class BleCommunicatorTest(GoProBle): +class MockBleCommunicator(GoProBle): # pylint: disable=signature-differs def __init__(self, test_version: str) -> None: - super().__init__( - BleControllerTest(), disconnection_handler, notification_handler, re.compile("target") - ) + super().__init__(MockBleController(), disconnection_handler, notification_handler, re.compile("target")) self._api = api_versions[test_version](self) - def _register_listener(self, _) -> None: - return True + def register_update(self, callback: types.UpdateCb, update: types.UpdateType) -> None: + return - def _unregister_listener(self, _) -> None: - return True + def unregister_update(self, callback: types.UpdateCb, update: types.UpdateType = None) -> None: + return - def get_notification(self, timeout: float) -> int: - return 1 + async def _send_ble_message( + self, uuid: BleUUID, data: bytearray, response_id: types.ResponseType, **kwargs + ) -> dict: + return dict(uuid=uuid, packet=data) - def _send_ble_message( - self, uuid: BleUUID, data: bytearray, response_id: ResponseType, **kwargs - ) -> GoProResp: - response = good_response - response._meta = [uuid] - response._raw_packet = data - return response - - def _read_characteristic(self, uuid: BleUUID) -> GoProResp: - response = good_response - response._meta = [uuid] - return response + async def _read_characteristic(self, uuid: BleUUID) -> dict: + return dict(uuid=uuid) @property def ble_command(self) -> BleCommands: @@ -253,8 +242,8 @@ def ble_status(self) -> BleStatuses: @pytest.fixture(scope="module", params=versions) -async def ble_communicator(request): - test_client = BleCommunicatorTest(request.param) +async def mock_ble_communicator(request): + test_client = MockBleCommunicator(request.param) yield test_client @@ -263,7 +252,7 @@ async def ble_communicator(request): ############################################################################################################## -class WifiControllerTest(WifiController): +class MockWifiController(WifiController): # pylint: disable=signature-differs def __init__(self, interface: Optional[str] = None, password: Optional[str] = None) -> None: @@ -289,30 +278,35 @@ def is_on(self) -> bool: @pytest.fixture(scope="module") -async def wifi_client(): - test_client = WifiClient(controller=WifiControllerTest()) +async def mock_wifi_client(): + test_client = WifiClient(controller=MockWifiController()) yield test_client @dataclass -class DummyWifiResponse: +class MockWifiResponse: url: str - _meta: list = field(default_factory=list) -class WifiCommunicatorTest(GoProWifi): +class MockWifiCommunicator(GoProWifi): # pylint: disable=signature-differs def __init__(self, test_version: str): - super().__init__(WifiControllerTest()) + super().__init__(MockWifiController()) self._api = api_versions[test_version](self) - def _get(self, url: str, _=None, **kwargs): - return DummyWifiResponse(url) + async def _http_get(self, url: str, _=None, **kwargs): + return MockWifiResponse(url) - def _stream_to_file(self, url: str, file: Path): + async def _stream_to_file(self, url: str, file: Path): return url, file + def register_update(self, callback: types.UpdateCb, update: types.UpdateType) -> None: + return + + def unregister_update(self, callback: types.UpdateCb, update: types.UpdateType = None) -> None: + return + @property def http_command(self) -> HttpCommands: return self._api.http_command @@ -323,8 +317,8 @@ def http_setting(self) -> HttpSettings: @pytest.fixture(scope="module", params=versions) -async def wifi_communicator(request): - test_client = WifiCommunicatorTest(request.param) +async def mock_wifi_communicator(request): + test_client = MockWifiCommunicator(request.param) yield test_client @@ -333,84 +327,100 @@ async def wifi_communicator(request): ############################################################################################################## -@dataclass -class Version: - major: int - minor: int - - -class FlattenPatch: +class DataPatch: def __init__(self, value: Any) -> None: self.value = value @property - def flatten(self) -> Any: + def data(self) -> Any: return self.value -good_response = GoProResp(meta=["test_response"], status=ErrorCode.SUCCESS) +class MockWiredGoPro(WiredGoPro): + def __init__(self, test_version: str) -> None: + super().__init__(serial=None, poll_period=0.5) + self.http_command.wired_usb_control = self._mock_wired_usb_control + self.http_command.get_open_gopro_api_version = self._mock_get_version + self.http_command.get_camera_state = self._mock_get_state + self.state_response: types.CameraState = {} -_test_response_id = CmdId.SET_SHUTTER + async def _mock_get_state(self, *args, **kwargs): + return DataPatch(self.state_response) + + def set_state_response(self, response: types.CameraState): + self.state_response = response + + async def _mock_wired_usb_control(self, *args, **kwargs): + return + async def _mock_get_version(self, *args, **kwargs): + return DataPatch("2.0") -def _test_parse(self: GoProResp) -> None: - self._state = GoProResp._State.PARSED - self._meta = [_test_response_id] +@pytest.fixture(scope="function") +async def mock_wired_gopro(): + test_client = MockWiredGoPro("2.0") + yield test_client -class GoProTest(WirelessGoPro): + +class MockWirelessGoPro(WirelessGoPro): def __init__(self, test_version: str) -> None: super().__init__( target=re.compile("device"), - ble_adapter=BleControllerTest, - wifi_adapter=WifiControllerTest, + ble_adapter=MockBleController, + wifi_adapter=MockWifiController, enable_wifi=True, maintain_state=False, ) self._test_version = test_version - self._api.ble_command.get_open_gopro_api_version = self._test_return_version - self._ble.write = self._test_write + self._api.ble_command.get_open_gopro_api_version = self._mock_version + self._ble.write = self._mock_write + self._ble._gatt_table = MockGattTable() self._ble._controller.disconnect = self._disconnect_handler self._test_response_uuid = GoProUUIDs.CQ_COMMAND self._test_response_data = bytearray() - def _open_wifi(self, timeout: int = 15, retries: int = 5) -> None: - self._api.ble_command.get_wifi_password = self._test_return_password - super()._open_wifi(timeout, retries) + async def _open_wifi(self, timeout: int = 15, retries: int = 5) -> None: + self._api.ble_command.get_wifi_password = self._mock_password + self._api.ble_command.get_wifi_ssid = self._mock_ssid + await super()._open_wifi(timeout, retries) - def _open_ble(self, timeout: int, retries: int) -> None: - super()._open_ble(timeout=timeout, retries=retries) - self._ble._gatt_table.handle2uuid = self._test_return_uuid + async def _open_ble(self, timeout: int, retries: int) -> None: + await super()._open_ble(timeout=timeout, retries=retries) + self._ble._gatt_table.handle2uuid = self._mock_uuid - def _send_ble_message( + async def _send_ble_message( self, uuid: BleUUID, data: bytearray, - response_id: ResponseType, + response_id: types.ResponseType, response_data: list[bytearray] = None, response_uuid: BleUUID = None, **kwargs ) -> GoProResp: if response_uuid is None: - return good_response + return mock_good_response else: self._test_response_data = response_data self._test_response_uuid = response_uuid global _test_response_id _test_response_id = response_id - self._ble.write = self._test_write - return super()._send_ble_message(uuid, data, response_id) + self._ble.write = self._mock_write + return await super()._send_ble_message(uuid, data, response_id) + + async def _mock_version(self) -> DataPatch: + return DataPatch("2.0") - def _test_return_version(self) -> FlattenPatch: - return FlattenPatch(Version(*[int(x) for x in self._test_version.split(".")])) + async def _mock_password(self) -> DataPatch: + return DataPatch("password") - def _test_return_password(self) -> FlattenPatch: - return FlattenPatch("password") + async def _mock_ssid(self) -> DataPatch: + return DataPatch("ssid") - def _test_return_uuid(self, _) -> BleUUID: + def _mock_uuid(self, _) -> BleUUID: return self._test_response_uuid - def _test_write(self, uuid: str, data: bytearray) -> None: + async def _mock_write(self, uuid: str, data: bytearray) -> None: assert self._test_response_data is not None for packet in self._test_response_data: self._notification_handler(0, packet) @@ -423,42 +433,64 @@ def is_ble_connected(self) -> bool: def is_http_connected(self) -> bool: return True + def close(self) -> None: + pass -@pytest.fixture(scope="module", params=versions) -async def gopro_client(request): - original_parse = GoProResp._parse - GoProResp._parse = _test_parse - test_client = GoProTest(request.param) + +_test_response_id = CmdId.SET_SHUTTER + + +# TODO use mocking library instead of doing this manually? +@pytest.fixture(params=versions) +async def mock_wireless_gopro_basic(request): + test_client = MockWirelessGoPro(request.param) yield test_client - GoProResp._parse = original_parse + test_client.close() -class GoProTestMaintainBle(WirelessGoPro): +class MockGoProMaintainBle(WirelessGoPro): def __init__(self) -> None: super().__init__( target=re.compile("device"), - ble_adapter=BleControllerTest, - wifi_adapter=WifiControllerTest, + ble_adapter=MockBleController, + wifi_adapter=MockWifiController, enable_wifi=True, maintain_ble=True, ) self._test_version = "2.0" - self._api.ble_command.get_open_gopro_api_version = self._test_return_version - self.ble_status.encoding_active.register_value_update = lambda *args: None - self.ble_status.system_ready.register_value_update = lambda *args: None - self.keep_alive = lambda *args: True - self._open_wifi = lambda *args: None - self._sync_resp_ready_q.get = lambda *args, **kwargs: good_response + self._api.ble_command.get_open_gopro_api_version = self._mock_get_version + self.ble_status.encoding_active.register_value_update = self._mock_register_encoding + self.ble_status.system_busy.register_value_update = self._mock_register_busy + self.ble_setting.led.set = self._mock_led_set + self._open_wifi = self._mock_open_wifi + self._sync_resp_ready_q.get = self._mock_q_get + + async def _mock_q_get(self, *args, **kwargs): + return mock_good_response + + async def _mock_led_set(self, *args): + return mock_good_response + + async def _mock_open_wifi(self, *args): + return None + + async def _mock_register_encoding(self, *args): + return DataPatch({StatusId.ENCODING: 1}) + + async def _mock_register_busy(self, *args): + return DataPatch({StatusId.SYSTEM_BUSY: 1}) + + async def mock_handle2uuid(self, *args): + return GoProUUIDs.CQ_QUERY_RESP - def _open_ble(self, timeout: int, retries: int) -> None: - super()._open_ble(timeout=timeout, retries=retries) - self._ble._gatt_table.handle2uuid = lambda *args: GoProUUIDs.CQ_QUERY_RESP + async def _open_ble(self, timeout: int, retries: int) -> None: + await super()._open_ble(timeout=timeout, retries=retries) - def _test_return_version(self) -> FlattenPatch: - return FlattenPatch(Version(*[int(x) for x in self._test_version.split(".")])) + async def _mock_get_version(self) -> DataPatch: + return DataPatch("2.0") @pytest.fixture(scope="function") -async def gopro_client_maintain_ble(): - test_client = GoProTestMaintainBle() +async def mock_wireless_gopro(): + test_client = MockGoProMaintainBle() yield test_client diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_ble_commands.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_ble_commands.py index 35ee285a..b1e946ad 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_ble_commands.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_ble_commands.py @@ -3,152 +3,36 @@ import inspect import logging -from construct import Int32ub - -from open_gopro.interface import GoProBle -from open_gopro.constants import SettingId, StatusId, GoProUUIDs, CmdId, QueryCmdId -from open_gopro import proto, Params -from open_gopro.responses import GoProResp -from tests.conftest import BleCommunicatorTest - - -def test_write_command_correct_uuid_cmd_id(ble_communicator: GoProBle): - response = ble_communicator.ble_command.set_shutter(shutter=Params.Toggle.ENABLE) - assert response.uuid == GoProUUIDs.CQ_COMMAND - assert response._raw_packet[0] == CmdId.SET_SHUTTER.value - - -def test_write_command_correct_parameter_data(ble_communicator: GoProBle): - response = ble_communicator.ble_command.load_preset(preset=5) - assert response.uuid == GoProUUIDs.CQ_COMMAND - assert Int32ub.parse(response._raw_packet[-4:]) == 5 - - -def test_read_command_correct_uuid(ble_communicator: GoProBle): - response = ble_communicator.ble_command.get_wifi_ssid() - assert response.uuid == GoProUUIDs.WAP_SSID - - -def test_setting_set(ble_communicator: GoProBle): - response = ble_communicator.ble_setting.resolution.set(Params.Resolution.RES_1080) - assert response.uuid == GoProUUIDs.CQ_SETTINGS - assert response._raw_packet[0] == SettingId.RESOLUTION.value - assert response._raw_packet[2] == Params.Resolution.RES_1080.value - - -def test_setting_get_value(ble_communicator: GoProBle): - response = ble_communicator.ble_setting.resolution.get_value() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.GET_SETTING_VAL.value - assert response._raw_packet[1] == SettingId.RESOLUTION.value - - -def test_setting_get_capabilities_values(ble_communicator: GoProBle): - response = ble_communicator.ble_setting.resolution.get_capabilities_values() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.GET_CAPABILITIES_VAL.value - assert response._raw_packet[1] == SettingId.RESOLUTION.value - - -def test_setting_register_value_update(ble_communicator: GoProBle): - response = ble_communicator.ble_setting.resolution.register_value_update() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.REG_SETTING_VAL_UPDATE.value - assert response._raw_packet[1] == SettingId.RESOLUTION.value - - -def test_setting_unregister_value_update(ble_communicator: GoProBle): - response = ble_communicator.ble_setting.resolution.unregister_value_update() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.UNREG_SETTING_VAL_UPDATE.value - assert response._raw_packet[1] == SettingId.RESOLUTION.value - - -def test_setting_register_capability_update(ble_communicator: GoProBle): - response = ble_communicator.ble_setting.resolution.register_capability_update() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.REG_CAPABILITIES_UPDATE.value - assert response._raw_packet[1] == SettingId.RESOLUTION.value - - -def test_setting_unregister_capability_update(ble_communicator: GoProBle): - response = ble_communicator.ble_setting.resolution.unregister_capability_update() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.UNREG_CAPABILITIES_UPDATE.value - assert response._raw_packet[1] == SettingId.RESOLUTION.value - - -def test_status_get_value(ble_communicator: GoProBle): - response = ble_communicator.ble_status.encoding_active.get_value() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.GET_STATUS_VAL.value - assert response._raw_packet[1] == StatusId.ENCODING.value - - -def test_status_register_value_update(ble_communicator: GoProBle): - assert ble_communicator._register_listener(None) - response = ble_communicator.ble_status.encoding_active.register_value_update() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.REG_STATUS_VAL_UPDATE.value - assert response._raw_packet[1] == StatusId.ENCODING.value - - -def test_status_unregister_value_update(ble_communicator: GoProBle): - assert ble_communicator._unregister_listener(None) - response = ble_communicator.ble_status.encoding_active.unregister_value_update() - assert response.uuid == GoProUUIDs.CQ_QUERY - assert response._raw_packet[0] == QueryCmdId.UNREG_STATUS_VAL_UPDATE.value - assert response._raw_packet[1] == StatusId.ENCODING.value +from typing import cast +import pytest +from construct import Int32ub -def test_proto_command_arg(ble_communicator: GoProBle): - response = ble_communicator.ble_command.set_turbo_mode(mode=Params.Toggle.ENABLE) - assert response.uuid == GoProUUIDs.CQ_COMMAND - assert response._raw_packet == bytearray(b"\xf1k\x08\x01") - out = proto.ResponseGeneric.FromString(response._raw_packet[2:]) - str(out) +from open_gopro import Params, proto +from open_gopro.communicator_interface import GoProBle +from open_gopro.constants import CmdId, GoProUUIDs, QueryCmdId, SettingId, StatusId +from open_gopro.gopro_base import GoProBase +from tests.conftest import MockBleCommunicator -def test_commands_iteration(ble_communicator: BleCommunicatorTest): - for commands in [ - ble_communicator._api.ble_command, - ble_communicator._api.ble_setting, - ble_communicator._api.ble_status, - ble_communicator._api.http_command, - ble_communicator._api.http_setting, - ]: - count = 0 - for _ in commands: - count += 1 - assert count > 0 +@pytest.mark.asyncio +async def test_write_command_correct_uuid_cmd_id(mock_ble_communicator: GoProBase): + response = await mock_ble_communicator.ble_command.set_shutter(shutter=Params.Toggle.ENABLE) + response = cast(dict, response) + assert response["uuid"] == GoProUUIDs.CQ_COMMAND + assert response["packet"][0] == CmdId.SET_SHUTTER.value -def test_commands_subscriptable(ble_communicator: BleCommunicatorTest): - for commands, identifier in zip( - [ - # ble_communicator._api.ble_command, - ble_communicator._api.ble_setting, - ble_communicator._api.ble_status, - # ble_communicator._api.http_command, - ble_communicator._api.http_setting, - ], - [ - # CmdId, - SettingId, - StatusId, - # CmdId, - SettingId, - ], - ): - try: - assert commands[list(identifier)[0]] - except TypeError: - assert True - continue +@pytest.mark.asyncio +async def test_write_command_correct_parameter_data(mock_ble_communicator: GoProBase): + response = await mock_ble_communicator.ble_command.load_preset(preset=5) + response = cast(dict, response) + assert response["uuid"] == GoProUUIDs.CQ_COMMAND + assert Int32ub.parse(response["packet"][-4:]) == 5 -def test_ensure_no_positional_args(ble_communicator: BleCommunicatorTest): - for command in ble_communicator.ble_command.values(): - if inspect.getfullargspec(command).args != ["self"]: - logging.error("All arguments to commands must be keyword-only") - assert True +@pytest.mark.asyncio +async def test_read_command_correct_uuid(mock_ble_communicator: GoProBase): + response = await mock_ble_communicator.ble_command.get_wifi_ssid() + response = cast(dict, response) + assert response["uuid"] == GoProUUIDs.WAP_SSID diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_bleak_wrapper.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_bleak_wrapper.py index 2aa030d1..12000958 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_bleak_wrapper.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_bleak_wrapper.py @@ -3,16 +3,221 @@ """Unit testing of bleak controller""" +import asyncio +import re +from dataclasses import dataclass, field + import pytest +from open_gopro.ble import BleUUID from open_gopro.ble.adapters.bleak_wrapper import BleakWrapperController +from open_gopro.constants import GoProUUIDs +from open_gopro.exceptions import ConnectFailed, FailedToFindDevice -def test_singleton(bleak_wrapper: BleakWrapperController): +def test_singleton(mock_bleak_wrapper: BleakWrapperController): new_bleak_wrapper = BleakWrapperController() - assert bleak_wrapper is new_bleak_wrapper + assert mock_bleak_wrapper is new_bleak_wrapper + + +@pytest.mark.asyncio +async def test_scan_success(mock_bleak_wrapper: BleakWrapperController, monkeypatch): + callback = asyncio.Queue() + + @dataclass + class MockDevice: + address: str = "address" + + @dataclass + class MockAdvData: + local_name: str = "GoPro 1234" + + class MockBleakScanner: + def __init__(self, *args, **kwargs) -> None: + self.callback = kwargs["detection_callback"] + + async def __aenter__(self, *args, **kwargs): + await callback.put(self.callback) + return self + + async def __aexit__(self, *_) -> None: + pass + + async def provide_device(): + cb = await callback.get() + cb(MockDevice(), MockAdvData()) + + monkeypatch.setattr("bleak.BleakScanner", MockBleakScanner) + + results = await asyncio.gather( + mock_bleak_wrapper.scan( + token=re.compile(r"GoPro [A-Z0-9]{4}"), + timeout=1, + service_uuids=[GoProUUIDs.CQ_QUERY], + ), + provide_device(), + ) + assert results[0].address == "address" + + +@pytest.mark.asyncio +async def test_scan_wrong_devices_found(mock_bleak_wrapper: BleakWrapperController, monkeypatch): + callback = asyncio.Queue() + + @dataclass + class MockDevice: + address: str = "address" + + @dataclass + class MockAdvData: + local_name: str = "GoPro 1234" + + class MockBleakScanner: + def __init__(self, *args, **kwargs) -> None: + self.callback = kwargs["detection_callback"] + + async def __aenter__(self, *args, **kwargs): + await callback.put(self.callback) + return self + + async def __aexit__(self, *_) -> None: + pass + + async def provide_device(): + cb = await callback.get() + cb(MockDevice(), MockAdvData()) + + monkeypatch.setattr("bleak.BleakScanner", MockBleakScanner) + + with pytest.raises(FailedToFindDevice): + await asyncio.gather( + mock_bleak_wrapper.scan( + token=re.compile(r"something_else"), + timeout=1, + service_uuids=[GoProUUIDs.CQ_QUERY], + ), + provide_device(), + ) + + +@pytest.mark.asyncio +async def test_scan_timeout(mock_bleak_wrapper: BleakWrapperController, monkeypatch): + class MockBleakScanner: + def __init__(self, *args, **kwargs) -> None: + ... + + async def __aenter__(self, *args, **kwargs): + return self + + async def __aexit__(self, *_) -> None: + pass + + monkeypatch.setattr("bleak.BleakScanner", MockBleakScanner) + + # Validate error if timeout + with pytest.raises(FailedToFindDevice): + await mock_bleak_wrapper.scan( + token=re.compile(r"something_else"), + timeout=1, + service_uuids=[GoProUUIDs.CQ_QUERY], + ) + + +@pytest.mark.asyncio +async def test_connect_success(mock_bleak_wrapper: BleakWrapperController, monkeypatch): + class MockBleakClient: + def __init__(self, *args, **kwargs) -> None: + self.is_connected = False + + async def connect(self, *args, **kwargs): + self.is_connected = True + + @dataclass + class MockDevice: + address: str = "address" + + monkeypatch.setattr("bleak.BleakClient", MockBleakClient) + client = await mock_bleak_wrapper.connect(lambda *args: None, MockDevice()) + assert client.is_connected + + +@pytest.mark.asyncio +async def test_connect_timeout(mock_bleak_wrapper: BleakWrapperController, monkeypatch): + class MockBleakClient: + def __init__(self, *args, **kwargs) -> None: + self.is_connected = False + + async def connect(self, *args, **kwargs): + raise asyncio.exceptions.TimeoutError + + @dataclass + class MockDevice: + address: str = "address" + + monkeypatch.setattr("bleak.BleakClient", MockBleakClient) + with pytest.raises(ConnectFailed): + await mock_bleak_wrapper.connect(lambda *args: None, MockDevice()) + + +@pytest.mark.asyncio +async def test_connect_fail_during_establishment(mock_bleak_wrapper: BleakWrapperController, monkeypatch): + callback = asyncio.Queue() + + class MockBleakClient: + def __init__(self, *args, **kwargs) -> None: + self.is_connected = False + self.callback = kwargs["disconnected_callback"] + + async def connect(self, *args, **kwargs): + await callback.put(self.callback) + + @dataclass + class MockDevice: + address: str = "address" + + async def disconnect(): + cb = await callback.get() + cb() + + monkeypatch.setattr("bleak.BleakClient", MockBleakClient) + with pytest.raises(ConnectFailed): + await asyncio.gather( + mock_bleak_wrapper.connect(lambda *args: None, MockDevice()), + disconnect(), + ) + + +@pytest.mark.asyncio +async def test_discovery(mock_bleak_wrapper: BleakWrapperController): + @dataclass + class MockDescriptor: + handle: int = 0 + uuid: BleUUID = GoProUUIDs.ACC_APPEARANCE + description: str = "descriptor" + + @dataclass + class MockChar: + descriptors: list[MockDescriptor] = field(default_factory=lambda: [MockDescriptor()]) + handle: int = 1 + uuid: BleUUID = GoProUUIDs.ACC_CENTRAL_ADDR_RES + properties: list[str] = field(default_factory=lambda: ["broadcast", "read"]) + description: str = "characteristic" + + @dataclass + class MockService: + characteristics: list[MockChar] = field(default_factory=lambda: [MockChar()]) + handle: int = 2 + uuid: BleUUID = GoProUUIDs.ACC_DEVICE_NAME + description: str = "service" + + @dataclass + class MockBleakClient: + services: list[MockService] = field(default_factory=lambda: [MockService()]) + async def read_gatt_descriptor(self, *args): + return 0 -def test_module_loop_running(bleak_wrapper: BleakWrapperController): - assert bleak_wrapper._module_thread.is_alive() - assert bleak_wrapper._ready.is_set() + gatt_db = await mock_bleak_wrapper.discover_chars(MockBleakClient(), uuids=GoProUUIDs) + assert len(gatt_db.services) == 1 + assert len(gatt_db.characteristics) == 1 + assert len(list(gatt_db.characteristics.values())[0].descriptors) == 1 diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_enums.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_enums.py new file mode 100644 index 00000000..36263a1b --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_enums.py @@ -0,0 +1,48 @@ +# test_enums.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Aug 16 22:48:50 UTC 2023 + +# Note: This may seem trivial but there were some major changes around this in Python 3.11 + +import enum + +from open_gopro import proto +from open_gopro.enum import GoProEnum, enum_factory + + +class EnumTest(GoProEnum): + RESULT_SUCCESS = 1 + TWO = 2 + NOT_APPLICABLE = 3 + DESCRIPTOR = 4 + FIVE = 5 + + +class NormalEnum(enum.Enum): + TWO = 2 + + +def test_str(): + assert EnumTest.TWO.name == "TWO" + assert EnumTest.TWO.value == 2 + assert str(EnumTest.TWO) == "EnumTest.TWO" + + +def test_equal(): + assert EnumTest.TWO == 2 + assert not EnumTest.TWO == "TWO" + assert not EnumTest.TWO == NormalEnum.TWO + assert EnumTest.TWO == EnumTest.TWO + + +def test_proto_enum_equal(): + proto_enum = enum_factory(proto.EnumResultGeneric.DESCRIPTOR) + assert proto_enum.RESULT_SUCCESS == 1 + assert proto_enum.RESULT_SUCCESS == EnumTest.RESULT_SUCCESS + assert proto_enum.RESULT_SUCCESS == "RESULT_SUCCESS" + + +def test_special_values(): + assert EnumTest.FIVE in list(EnumTest) + assert EnumTest.FIVE in EnumTest + assert EnumTest.NOT_APPLICABLE not in list(EnumTest) + assert EnumTest.DESCRIPTOR not in list(EnumTest) diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro.py deleted file mode 100644 index 9d263d29..00000000 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro.py +++ /dev/null @@ -1,110 +0,0 @@ -# test_Wirelessgopro.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://Wirelessgopro.com/OpenGoPro). -# This copyright was auto-generated on Fri Sep 10 01:35:03 UTC 2021 - -# pylint: disable=redefined-outer-name - - -"""Unit testing of GoPro Client""" - -import time -import threading -from pathlib import Path - -import pytest -import requests -import requests_mock - -from open_gopro.gopro_wireless import WirelessGoPro, Params, GoProResp -from open_gopro.exceptions import InvalidConfiguration, ResponseTimeout, GoProNotOpened -from open_gopro.constants import StatusId, SettingId - -ready = False - - -def test_control(): - assert True - - -def test_ble_threads_start(gopro_client_maintain_ble: WirelessGoPro): - def open_client(): - gopro_client_maintain_ble.open() - global ready - ready = True - - threading.Thread(target=open_client, daemon=True).start() - while not ready: - time.sleep(0.1) - not_encoding = bytearray([0x05, 0x13, 0x00, StatusId.ENCODING.value, 0x01, 0x00]) - gopro_client_maintain_ble._notification_handler(0xFF, not_encoding) - not_busy = bytearray([0x05, 0x13, 0x00, StatusId.SYSTEM_BUSY.value, 0x01, 0x00]) - gopro_client_maintain_ble._notification_handler(0xFF, not_busy) - assert not gopro_client_maintain_ble.is_busy - assert not gopro_client_maintain_ble.is_encoding - - -def test_gopro_open(gopro_client: WirelessGoPro): - gopro_client.open() - assert gopro_client.is_ble_connected - assert gopro_client.is_http_connected - assert gopro_client.identifier == "scanned_device" - - -def test_http_get(gopro_client: WirelessGoPro, monkeypatch): - endpoint = "gopro/camera/stream/start" - session = requests.Session() - adapter = requests_mock.Adapter() - session.mount(gopro_client._base_url + endpoint, adapter) - adapter.register_uri("GET", gopro_client._base_url + endpoint, json="{}") - monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) - response = gopro_client._get(endpoint) - assert response.is_ok - - -def test_http_file(gopro_client: WirelessGoPro, monkeypatch): - out_file = Path("test.mp4") - endpoint = "videos/DCIM/100GOPRO/dummy.MP4" - session = requests.Session() - adapter = requests_mock.Adapter() - session.mount(gopro_client._base_url + endpoint, adapter) - adapter.register_uri("GET", gopro_client._base_url + endpoint, text="BINARY DATA") - monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) - gopro_client._stream_to_file(endpoint, out_file) - assert out_file.exists() - - -def test_http_response_timeout(gopro_client: WirelessGoPro, monkeypatch): - with pytest.raises(ResponseTimeout): - endpoint = "gopro/camera/stream/start" - session = requests.Session() - adapter = requests_mock.Adapter() - session.mount(gopro_client._base_url + endpoint, adapter) - adapter.register_uri("GET", gopro_client._base_url + endpoint, exc=requests.exceptions.ConnectTimeout) - monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) - gopro_client._get(endpoint) - - -def test_http_response_error(gopro_client: WirelessGoPro, monkeypatch): - endpoint = "gopro/camera/stream/start" - session = requests.Session() - adapter = requests_mock.Adapter() - session.mount(gopro_client._base_url + endpoint, adapter) - adapter.register_uri( - "GET", gopro_client._base_url + endpoint, status_code=403, reason="something bad happened", json="{}" - ) - monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) - response = gopro_client._get(endpoint) - assert not response.is_ok - - -def test_get_update(gopro_client: WirelessGoPro): - gopro_client._out_q.put(1) - assert gopro_client.get_notification() == 1 - - -def test_keep_alive(gopro_client: WirelessGoPro): - assert gopro_client.keep_alive() - - -def test_get_param_values_by_id(gopro_client: WirelessGoPro): - vector = list(Params.Resolution)[0] - assert GoProResp._get_query_container(SettingId.RESOLUTION)(vector.value) == vector diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro_ble.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro_ble.py index 9da225df..d137cb11 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro_ble.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro_ble.py @@ -18,53 +18,55 @@ def disconnection_handler(_) -> None: print("Entered test disconnect callback") -def test_gopro_ble_client_instantiation(ble_client: BleClient): - assert not ble_client.is_discovered - assert not ble_client.is_connected +def test_gopro_ble_client_instantiation(mock_ble_client: BleClient): + assert not mock_ble_client.is_discovered + assert not mock_ble_client.is_connected -def test_gopro_ble_client_failed_to_find_device(ble_client: BleClient): - ble_client._target = re.compile("invalid_device") +@pytest.mark.asyncio +async def test_gopro_ble_client_failed_to_find_device(mock_ble_client: BleClient): + mock_ble_client._target = re.compile("invalid_device") with pytest.raises(FailedToFindDevice): - ble_client._find_device() - assert not ble_client.is_discovered - assert not ble_client.is_connected + await mock_ble_client._find_device() + assert not mock_ble_client.is_discovered + assert not mock_ble_client.is_connected -def test_gopro_ble_client_failed_to_connect(ble_client: BleClient): - ble_client._target = re.compile("device") - ble_client._disconnected_cb = None +@pytest.mark.asyncio +async def test_gopro_ble_client_failed_to_connect(mock_ble_client: BleClient): + mock_ble_client._target = re.compile("device") + mock_ble_client._disconnected_cb = None with pytest.raises(ConnectFailed): - ble_client.open() - assert ble_client.is_discovered - assert not ble_client.is_connected + await mock_ble_client.open() + assert mock_ble_client.is_discovered + assert not mock_ble_client.is_connected -def test_gopro_ble_client_open(ble_client: BleClient): - ble_client._disconnected_cb = disconnection_handler - ble_client.open() - assert ble_client.is_discovered - assert ble_client.is_connected +@pytest.mark.asyncio +async def test_gopro_ble_client_open(mock_ble_client: BleClient): + mock_ble_client._disconnected_cb = disconnection_handler + await mock_ble_client.open() + assert mock_ble_client.is_discovered + assert mock_ble_client.is_connected -def test_gopro_ble_client_identifier(ble_client: BleClient): - assert ble_client.identifier == "scanned_device" +@pytest.mark.asyncio +async def test_gopro_ble_client_identifier(mock_ble_client: BleClient): + assert mock_ble_client.identifier == "scanned_device" -def test_gopro_ble_client_read(ble_client: BleClient): - assert ble_client.read("uuid") == bytearray() +@pytest.mark.asyncio +async def test_gopro_ble_client_read(mock_ble_client: BleClient): + assert await mock_ble_client.read("uuid") == bytearray() -def test_gopro_ble_client_write(ble_client: BleClient): - ble_client.write("uuid", bytearray()) +@pytest.mark.asyncio +async def test_gopro_ble_client_write(mock_ble_client: BleClient): + await mock_ble_client.write("uuid", bytearray()) assert True -def test_get_gatt_table(ble_client: BleClient): - ble_client._gatt_table = None - assert ble_client.gatt_db is not None - - -def test_gopro_ble_client_close(ble_client: BleClient): - ble_client.close() - assert not ble_client.is_connected +@pytest.mark.asyncio +async def test_gopro_ble_client_close(mock_ble_client: BleClient): + await mock_ble_client.close() + assert not mock_ble_client.is_connected diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro_wifi.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro_wifi.py index 4ee8a1a8..c7b72d10 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro_wifi.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_gopro_wifi.py @@ -8,26 +8,26 @@ import pytest -from open_gopro.wifi import WifiClient from open_gopro.exceptions import ConnectFailed +from open_gopro.wifi import WifiClient -def test_gopro_wifi_client_failed_to_connect(wifi_client: WifiClient): +def test_gopro_wifi_client_failed_to_connect(mock_wifi_client: WifiClient): with pytest.raises(ConnectFailed): - wifi_client.open("test_ssid", "invalid_password") + mock_wifi_client.open("test_ssid", "invalid_password") -def test_gopro_wifi_client_open(wifi_client: WifiClient): - wifi_client.open("test_ssid", "password") - assert wifi_client.ssid == "test_ssid" - assert wifi_client.password == "password" +def test_gopro_wifi_client_open(mock_wifi_client: WifiClient): + mock_wifi_client.open("test_ssid", "password") + assert mock_wifi_client.ssid == "test_ssid" + assert mock_wifi_client.password == "password" -def test_gopro_wifi_client_is_connected(wifi_client: WifiClient): - assert wifi_client.is_connected +def test_gopro_wifi_client_is_connected(mock_wifi_client: WifiClient): + assert mock_wifi_client.is_connected -def test_gopro_wifi_client_close(wifi_client: WifiClient): - wifi_client.close() - assert wifi_client.ssid == "test_ssid" - assert wifi_client.password == "password" +def test_gopro_wifi_client_close(mock_wifi_client: WifiClient): + mock_wifi_client.close() + assert mock_wifi_client.ssid == "test_ssid" + assert mock_wifi_client.password == "password" diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_http_commands.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_http_commands.py index 4daa906f..f9a8eb84 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_http_commands.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_http_commands.py @@ -2,36 +2,45 @@ # This copyright was auto-generated on Wed, Sep 1, 2021 5:05:55 PM import inspect +import logging from pathlib import Path -from open_gopro.interface import GoProWifi +import pytest +from open_gopro.communicator_interface import GoProWifi -def test_get_with_no_params(wifi_communicator: GoProWifi): - response = wifi_communicator.http_command.get_media_list() + +@pytest.mark.asyncio +async def test_get_with_no_params(mock_wifi_communicator: GoProWifi): + response = await mock_wifi_communicator.http_command.get_media_list() assert response.url == "gopro/media/list" -def test_get_with_params(wifi_communicator: GoProWifi): +@pytest.mark.asyncio +async def test_get_with_params(mock_wifi_communicator: GoProWifi): zoom = 99 - response = wifi_communicator.http_command.set_digital_zoom(percent=zoom) + response = await mock_wifi_communicator.http_command.set_digital_zoom(percent=zoom) assert response.url == f"gopro/camera/digital_zoom?percent={zoom}" -def test_with_multiple_params(wifi_communicator: GoProWifi): +@pytest.mark.asyncio +async def test_with_multiple_params(mock_wifi_communicator: GoProWifi): media_file = "XXX.mp4" offset_ms = 2500 - response = wifi_communicator.http_command.add_file_hilight(file=media_file, offset=offset_ms) + response = await mock_wifi_communicator.http_command.add_file_hilight(file=media_file, offset=offset_ms) assert response.url == "gopro/media/hilight/file?path=100GOPRO/XXX.mp4&ms=2500" -def test_get_binary(wifi_communicator: GoProWifi): - file = wifi_communicator.http_command.download_file(camera_file="test_file", local_file=Path("local_file")) +@pytest.mark.asyncio +async def test_get_binary(mock_wifi_communicator: GoProWifi): + file = await mock_wifi_communicator.http_command.download_file( + camera_file="test_file", local_file=Path("local_file") + ) assert str(file[1]) == "local_file" -def test_ensure_no_positional_args(wifi_communicator: GoProWifi): - for command in wifi_communicator.http_command.values(): +def test_ensure_no_positional_args(mock_wifi_communicator: GoProWifi): + for command in mock_wifi_communicator.http_command.values(): if inspect.getfullargspec(command).args != ["self"]: logging.error("All arguments to commands must be keyword-only") assert True diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_models.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_models.py new file mode 100644 index 00000000..3e1942bf --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_models.py @@ -0,0 +1,296 @@ +# test_media_list.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jun 26 18:26:05 UTC 2023 + +from typing import Final + +from open_gopro import constants +from open_gopro.models import ( + GroupedMediaItem, + MediaItem, + MediaList, + MediaMetadata, + PhotoMetadata, + VideoMetadata, +) +from open_gopro.models.general import HttpInvalidSettingResponse, WebcamResponse + +SINGLE_MEDIA_ITEM: Final = { + "n": "GX010001.MP4", + "cre": "1656931398", + "mod": "1656931398", + "glrv": "1366268", + "ls": "-1", + "s": "27469309", +} + +GROUPED_MEDIA_ITEM: Final = { + "n": "G0010010.JPG", + "g": "1", + "b": "10", + "l": "39", + "cre": "1657016833", + "mod": "1657016833", + "s": "170696972", + "t": "b", + "m": [], +} + + +MEDIA_LIST: Final = { + "id": "23544241138403583", + "media": [ + { + "d": "100GOPRO", + "fs": [ + { + "n": "GX010001.MP4", + "cre": "1656931398", + "mod": "1656931398", + "glrv": "1366268", + "ls": "-1", + "s": "27469309", + }, + {"n": "GOPR0002.JPG", "cre": "1656931409", "mod": "1656931409", "s": "5518647"}, + {"n": "GOPR0003.JPG", "cre": "1656931440", "mod": "1656931440", "s": "4672440"}, + { + "n": "GX010004.MP4", + "cre": "1657013120", + "mod": "1657013120", + "glrv": "2489198", + "ls": "-1", + "s": "47939086", + }, + {"n": "GOPR0005.JPG", "cre": "1657013127", "mod": "1657013127", "s": "7010699"}, + {"n": "GOPR0006.JPG", "cre": "1657013129", "mod": "1657013129", "s": "8596771"}, + { + "n": "GX010007.MP4", + "cre": "1657013162", + "mod": "1657013162", + "glrv": "1800635", + "ls": "-1", + "s": "33849822", + }, + { + "n": "GX010008.MP4", + "cre": "1657013166", + "mod": "1657013166", + "glrv": "2400680", + "ls": "-1", + "s": "45571078", + }, + { + "n": "GX010009.MP4", + "cre": "1657013171", + "mod": "1657013171", + "glrv": "2121971", + "ls": "-1", + "s": "41702381", + }, + { + "n": "G0010010.JPG", + "g": "1", + "b": "10", + "l": "39", + "cre": "1657016833", + "mod": "1657016833", + "s": "170696972", + "t": "b", + "m": [], + }, + { + "n": "G0020041.JPG", + "g": "2", + "b": "41", + "l": "70", + "cre": "1657018747", + "mod": "1657018747", + "s": "166729035", + "t": "b", + "m": [], + }, + { + "n": "GX010040.MP4", + "cre": "1657018743", + "mod": "1657018743", + "glrv": "1167331", + "ls": "-1", + "s": "25086075", + }, + ], + } + ], +} + + +def test_single_media_item(): + assert MediaItem(**SINGLE_MEDIA_ITEM) + + +def test_grouped_media_item(): + assert GroupedMediaItem(**GROUPED_MEDIA_ITEM) + + +def test_media_list(): + media_list = MediaList(**MEDIA_LIST) + assert media_list + items = media_list.files + assert len(items) == 12 + assert len([item for item in items if isinstance(item, GroupedMediaItem)]) == 2 + + +VIDEO_METADATA: Final = { + "cre": "1656927817", + "s": "27469309", + "mahs": "0", + "us": "0", + "mos": [], + "eis": "0", + "pta": "1", + "ao": "stereo", + "tr": "0", + "mp": "0", + "ct": "0", + "rot": "0", + "fov": "0", + "lc": "0", + "prjn": "9", + "gumi": "1fd0ef36481b8ce8fdcb21e8f4ca2637", + "ls": "1366268", + "cl": "0", + "avc_profile": "255", + "profile": "255", + "hc": "0", + "hi": [], + "dur": "4", + "w": "5312", + "h": "2988", + "fps": "1001", + "fps_denom": "30000", + "prog": "1", + "subsample": "0", +} + +PHOTO_METADATA: Final = { + "cre": "1656931408", + "s": "5518647", + "hc": "0", + "us": "0", + "mos": [], + "eis": "0", + "hdr": "0", + "wdr": "0", + "raw": "0", + "tr": "0", + "mp": "0", + "ct": "4", + "rot": "0", + "fov": "28", + "lc": "0", + "prjn": "9", + "gumi": "7e39f1de649dfdf94a84ca12d99c4ce5", + "w": "5568", + "h": "4872", +} + + +def test_video(): + meta = MediaMetadata.from_json(VIDEO_METADATA) + assert isinstance(meta, VideoMetadata) + + +def test_photo(): + assert isinstance(MediaMetadata.from_json(PHOTO_METADATA), PhotoMetadata) + + +WEBCAM_SUCCESS_RSP = { + "status": "2", + "error": "0", +} + + +def test_webcam_success_response(): + response = WebcamResponse(**WEBCAM_SUCCESS_RSP) + assert response.status == constants.WebcamStatus.HIGH_POWER_PREVIEW + assert response.error == constants.WebcamError.SUCCESS + + +WEBCAM_FAILURE_RSP = { + "error": "4", + "option_id": "2", + "setting_id": "135", + "supported_options": [ + { + "display_name": "Auto Boost", + "id": "4", + }, + { + "display_name": "Boost", + "id": "3", + }, + { + "display_name": "On", + "id": "1", + }, + { + "display_name": "Off", + "id": "0", + }, + ], +} + + +def test_webcam_failure_response(): + response = WebcamResponse(**WEBCAM_FAILURE_RSP) + assert response.error == constants.WebcamError.SHUTTER + # Test our scrubbing of null values + assert "None" not in str(response) + + +HTTP_INVALID_SETTING_RSP = { + "error": "4", + "option_id": "100", + "setting_id": "135", + "supported_options": [ + { + "display_name": "Auto Boost", + "id": "4", + }, + { + "display_name": "Boost", + "id": "3", + }, + { + "display_name": "On", + "id": "1", + }, + { + "display_name": "Off", + "id": "0", + }, + ], +} + + +def test_invalid_setting_http_response(): + response = HttpInvalidSettingResponse(**HTTP_INVALID_SETTING_RSP) + assert response.error == 4 + assert len(response.supported_options) == 4 + + +test = { + "error": 4, + "option_id": 100, + "setting_id": 135, + "supported_options": [ + {"display_name": "Auto Boost", "id": 4}, + {"display_name": "Boost", "id": 3}, + {"display_name": "On", "id": 1}, + {"display_name": "Off", "id": 0}, + ], +} + + +def test_printing(): + response = HttpInvalidSettingResponse(**HTTP_INVALID_SETTING_RSP) + str(response) + assert True diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_parsers.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_parsers.py new file mode 100644 index 00000000..6b74da01 --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_parsers.py @@ -0,0 +1,32 @@ +# test_parsers.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Mon Jul 31 17:04:06 UTC 2023 + +from typing import cast + +from open_gopro.api.ble_commands import BleCommands +from open_gopro.api.parsers import ByteParserBuilders +from open_gopro.communicator_interface import GoProBle +from open_gopro.constants import CmdId +from open_gopro.models.response import GlobalParsers +from open_gopro.parser_interface import Parser +from open_gopro.proto import EnumResultGeneric, ResponseGetApEntries, ScanEntry + + +def test_version_response(mock_ble_communicator: GoProBle): + BleCommands(mock_ble_communicator) + parser = GlobalParsers.get_parser(CmdId.GET_THIRD_PARTY_API_VERSION) + builder = parser.byte_json_adapter.build + raw_bytes = builder({"major": 1, "minor": 2}) + assert parser.parse(raw_bytes) == "1.2" + + +def test_recursive_protobuf_proxying(): + scan1 = ScanEntry(ssid="one", signal_strength_bars=0, signal_frequency_mhz=0, scan_entry_flags=0) + scan2 = ScanEntry(ssid="two", signal_strength_bars=0, signal_frequency_mhz=0, scan_entry_flags=0) + response = ResponseGetApEntries(result=EnumResultGeneric.RESULT_SUCCESS, scan_id=1, entries=[scan1, scan2]) + raw = response.SerializeToString() + parser = Parser[ResponseGetApEntries](byte_json_adapter=ByteParserBuilders.Protobuf(ResponseGetApEntries)) + parsed = parser.parse(raw) + assert len(parsed.entries) == 2 + assert parsed.entries[0].ssid == "one" + assert parsed.entries[1].ssid == "two" diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_responses.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_responses.py index ccc0a29e..f76b5259 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_responses.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_responses.py @@ -6,49 +6,51 @@ # pylint: disable= redefined-outer-name import requests - -import pytest import requests_mock -from open_gopro import WirelessGoPro -from open_gopro.constants import ActionId, CmdId, GoProUUIDs, QueryCmdId, SettingId, StatusId -from open_gopro.responses import GoProResp -from open_gopro.api.http_commands import HttpParsers - +from open_gopro.api.parsers import JsonParsers +from open_gopro.constants import ( + ActionId, + CmdId, + ErrorCode, + GoProUUIDs, + QueryCmdId, + SettingId, + StatusId, +) +from open_gopro.models.response import ( + BleRespBuilder, + HttpRespBuilder, + RequestsHttpRespBuilderDirector, +) +# Resolution capability response with no valid capabilities test_push_receive_no_parameter = bytearray([0x08, 0xA2, 0x00, 0x02, 0x00, 0x03, 0x00, 0x79, 0x00]) def test_push_response_no_parameter_values(): - r = GoProResp([GoProUUIDs.CQ_QUERY_RESP]) - r._accumulate(test_push_receive_no_parameter) - assert r.is_received - r._parse() - assert r.is_parsed - assert r.is_ok - assert r.identifier is QueryCmdId.SETTING_CAPABILITY_PUSH - assert r.cmd is QueryCmdId.SETTING_CAPABILITY_PUSH - assert r.uuid == GoProUUIDs.CQ_QUERY_RESP - assert r.endpoint is None - assert r[SettingId.RESOLUTION] == [] - assert isinstance(r.flatten, dict) + builder = BleRespBuilder() + builder.set_uuid(GoProUUIDs.CQ_QUERY_RESP) + builder.accumulate(test_push_receive_no_parameter) + assert builder.is_finished_accumulating + r = builder.build() + assert r.ok + assert r.identifier == SettingId.RESOLUTION + assert r.data == [] test_read_receive = bytearray([0x64, 0x62, 0x32, 0x2D, 0x73, 0x58, 0x56, 0x2D, 0x66, 0x62, 0x38]) def test_read_command(): - r = GoProResp._from_read_response(GoProUUIDs.WAP_PASSWORD, test_read_receive) - assert r.is_parsed - assert r.is_received - assert r.is_received - assert r.is_ok + builder = BleRespBuilder() + builder.set_uuid(GoProUUIDs.WAP_PASSWORD) + builder.set_packet(test_read_receive) + r = builder.build() + assert r.ok assert r.identifier is GoProUUIDs.WAP_PASSWORD - assert r.cmd is None - assert r.endpoint is None - assert r["password"] == "db2-sXV-fb8" + assert r.data == "db2-sXV-fb8" assert len(str(r)) > 0 - assert isinstance(r.flatten, str) test_write_send = bytearray([0x05]) ## Sleep @@ -56,12 +58,13 @@ def test_read_command(): def test_write_command(): - r = GoProResp([GoProUUIDs.CQ_COMMAND_RESP, CmdId.SLEEP]) - r._accumulate(test_write_recieve) - assert r.is_received - r._parse() - assert r.is_parsed - assert r.is_ok + builder = BleRespBuilder() + builder.set_uuid(GoProUUIDs.CQ_COMMAND_RESP) + builder.accumulate(test_write_recieve) + assert builder.is_finished_accumulating + r = builder.build() + assert r.identifier is CmdId.SLEEP + assert r.ok test_complex_write_send = bytes([0x13]) @@ -463,31 +466,22 @@ def test_write_command(): def test_complex_write_command(): - r = GoProResp([GoProUUIDs.CQ_QUERY_RESP, CmdId.GET_CAMERA_STATUSES]) + builder = BleRespBuilder() + builder.set_uuid(GoProUUIDs.CQ_QUERY_RESP) idx = 0 - while not r.is_received: + while not builder.is_finished_accumulating: end = len(test_complex_write_receive) if idx + 20 > len(test_complex_write_receive) else idx + 20 - r._accumulate(test_complex_write_receive[idx:end]) + builder.accumulate(test_complex_write_receive[idx:end]) idx = end - assert r.is_received - r._parse() - assert r.is_parsed - assert r.is_received - assert "DEPRECATED" in list(r.values()) - assert r.is_ok + assert builder.is_finished_accumulating + r = builder.build() + assert "DEPRECATED" in list(r.data.values()) + assert r.ok assert r.identifier is QueryCmdId.GET_STATUS_VAL - assert r.cmd is QueryCmdId.GET_STATUS_VAL - assert r.uuid == GoProUUIDs.CQ_QUERY_RESP - assert StatusId.ENCODING in r # Test iterator - for x in r: + for x in r.data: assert isinstance(x, StatusId) assert len(str(r)) > 0 - assert isinstance(r.flatten, dict) - # Test dict methods - assert len(r.items()) - assert len(r.keys()) - assert len(r.values()) test_json = { @@ -679,26 +673,21 @@ def test_http_response_with_extra_parsing(): with requests_mock.Mocker() as m: m.get(url, json=test_json) response = requests.get(url) - r = GoProResp._from_http_response(HttpParsers.CameraStateParser(), response) - - assert r.is_parsed - assert r.is_received - assert r.is_received - assert r.is_ok - assert r.cmd is None - assert r.uuid == None + director = RequestsHttpRespBuilderDirector(response, JsonParsers.CameraStateParser()) + r = director() + assert "DEPRECATED" in r.data.values() + assert r.ok assert len(str(r)) > 0 - assert r.endpoint receive_proto = bytes([0x0D, 0xF5, 0xFF, 0x28, 0x07, 0x30, 0x01, 0x38, 0x03, 0x40, 0x00, 0x80, 0x01, 0x01]) def test_proto(): - - r = GoProResp([GoProUUIDs.CQ_QUERY_RESP]) - r._accumulate(receive_proto) - assert r.is_received - r._parse() - assert r.is_parsed - assert r.is_ok + builder = BleRespBuilder() + builder.set_uuid(GoProUUIDs.CQ_QUERY_RESP) + builder.accumulate(receive_proto) + assert builder.is_finished_accumulating + r = builder.build() + assert r.identifier is ActionId.INTERNAL_FF + assert r.ok diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_services.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_services.py index 410e92cd..dbcd32b9 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_services.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_services.py @@ -4,14 +4,14 @@ # pylint: disable = redefined-outer-name import uuid -from typing import List, Dict +from typing import Dict, List import pytest -from open_gopro.ble.services import UUIDs, UUIDsMeta, BLE_BASE_UUID +from open_gopro.ble import BleUUID, Characteristic, Descriptor, GattDB, Service +from open_gopro.ble.services import BLE_BASE_UUID, UUIDs, UUIDsMeta from open_gopro.constants import BleUUID -from open_gopro.ble import Descriptor, Characteristic, Service, GattDB, BleUUID -from tests.conftest import gatt_db +from tests.conftest import mock_gatt_db def test_128_bit_uuid(): @@ -75,51 +75,51 @@ class TestUUIDs(UUIDs): BAD_ATTRIBUTE = 1 -def test_descriptor(descriptor: Descriptor): - assert descriptor.handle > 0 - assert descriptor.name == descriptor.uuid.name +def test_descriptor(mock_descriptor: Descriptor): + assert mock_descriptor.handle > 0 + assert mock_descriptor.name == mock_descriptor.uuid.name -def test_characteristic(characteristic: Characteristic): - assert characteristic.handle > 0 - assert characteristic.name == characteristic.uuid.name - assert len(characteristic.descriptors) +def test_characteristic(mock_characteristic: Characteristic): + assert mock_characteristic.handle > 0 + assert mock_characteristic.name == mock_characteristic.uuid.name + assert len(mock_characteristic.descriptors) - assert characteristic.is_readable - assert not characteristic.is_writeable - assert not characteristic.is_notifiable - assert not characteristic.is_indicatable + assert mock_characteristic.is_readable + assert not mock_characteristic.is_writeable + assert not mock_characteristic.is_notifiable + assert not mock_characteristic.is_indicatable -def test_service(service: Service): - assert service.start_handle > 0 - assert service.name == service.uuid.name - assert len(service.characteristics) > 0 +def test_service(mock_service: Service): + assert mock_service.start_handle > 0 + assert mock_service.name == mock_service.uuid.name + assert len(mock_service.characteristics) > 0 -def test_characteristic_view(gatt_db: GattDB): +def test_characteristic_view(mock_gatt_db: GattDB): # Get all attributes by nested looping through services chars: list[Characteristic] = [] - for service in gatt_db.services.values(): + for service in mock_gatt_db.services.values(): for char in service.characteristics.values(): chars.append(char) - assert len(chars) == len(gatt_db.characteristics) + assert len(chars) == len(mock_gatt_db.characteristics) for char in chars: - assert char.uuid in gatt_db.characteristics + assert char.uuid in mock_gatt_db.characteristics - for char in gatt_db.characteristics: + for char in mock_gatt_db.characteristics: assert len(char.uuid.hex) - assert list(gatt_db.characteristics.keys()) == [c.uuid for c in chars] - assert list([c.uuid for c in gatt_db.characteristics.values()]) == [c.uuid for c in chars] + assert list(mock_gatt_db.characteristics.keys()) == [c.uuid for c in chars] + assert list([c.uuid for c in mock_gatt_db.characteristics.values()]) == [c.uuid for c in chars] -def test_gatt_db(gatt_db: GattDB): - handles = set([c.handle for c in gatt_db.characteristics]) - uuids = set([c.uuid for c in gatt_db.characteristics]) - assert handles == set([gatt_db.uuid2handle(uuid) for uuid in uuids]) - assert uuids == set([gatt_db.handle2uuid(handle) for handle in handles]) +def test_gatt_db(mock_gatt_db: GattDB): + handles = set([c.handle for c in mock_gatt_db.characteristics]) + uuids = set([c.uuid for c in mock_gatt_db.characteristics]) + assert handles == set([mock_gatt_db.uuid2handle(uuid) for uuid in uuids]) + assert uuids == set([mock_gatt_db.handle2uuid(handle) for handle in handles]) - gatt_db.dump_to_csv() + mock_gatt_db.dump_to_csv() diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_wifi.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_wifi_adapter.py similarity index 94% rename from demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_wifi.py rename to demos/python/sdk_wireless_camera_control/tests/unit/test_wifi_adapter.py index d7c22b2a..453ffbae 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_wifi.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_wifi_adapter.py @@ -2,14 +2,15 @@ # This copyright was auto-generated on Wed Sep 15 23:48:50 UTC 2021 from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import InitVar, dataclass from enum import Enum, auto from typing import Literal -from abc import abstractmethod, ABC -from dataclasses import InitVar, dataclass import pytest -from open_gopro.wifi.adapters.wireless import Wireless, SsidState +from open_gopro.wifi.adapters.wireless import SsidState, WifiCli operating_systems = ["windows"] @@ -210,29 +211,29 @@ def command_sender(request, monkeypatch): @pytest.fixture(scope="function") def wireless(command_sender): - test_client = Wireless() + test_client = WifiCli() yield test_client -def test_power(wireless: Wireless): +def test_power(wireless: WifiCli): assert wireless.power(True) is False assert wireless.power(False) is True -def test_initialized(wireless: Wireless): +def test_initialized(wireless: WifiCli): assert wireless.current() == (None, SsidState.DISCONNECTED) -def test_connect(wireless: Wireless, command_sender: CommandSender): +def test_connect(wireless: WifiCli, command_sender: CommandSender): command_sender.interface_state = InterfaceState.CONNECTED assert wireless.connect(SSID, PASSWORD, timeout=1) -def test_disconnect(wireless: Wireless): +def test_disconnect(wireless: WifiCli): assert wireless.disconnect() -def test_is_on(wireless: Wireless, command_sender: CommandSender): +def test_is_on(wireless: WifiCli, command_sender: CommandSender): assert wireless.is_on command_sender.interface_state = InterfaceState.DISABLED assert not wireless.is_on diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_wired_gopro.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_wired_gopro.py new file mode 100644 index 00000000..7db55810 --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_wired_gopro.py @@ -0,0 +1,91 @@ +# test_Wirelessgopro.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://Wirelessgopro.com/OpenGoPro). +# This copyright was auto-generated on Fri Sep 10 01:35:03 UTC 2021 + +# pylint: disable=redefined-outer-name + + +"""Unit testing of GoPro Client""" + +import asyncio +from dataclasses import dataclass +from typing import Final + +import pytest + +from open_gopro import WiredGoPro, constants +from open_gopro.wifi.mdns_scanner import ZeroconfListener, find_first_ip_addr + +IP_ADDR: Final[str] = "172.20.123.51" + + +@pytest.mark.asyncio +async def test_mdns_scan(monkeypatch): + @dataclass + class MockServiceInfo: + def parsed_addresses(self, *args, **kwargs): + return [IP_ADDR] + + class MockZeroconf: + def __init__(self, *args, **kwargs) -> None: + pass + + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + pass + + def get_service_info(self, *args, **kwargs): + return MockServiceInfo() + + def close(self): + ... + + class MockAsyncZeroConf: + def __init__(self, *args, **kwargs) -> None: + pass + + async def __aenter__(self, *args, **kwargs): + return self + + async def __aexit__(self, *args, **kwargs): + ... + + async def async_get_service_info(*args, **kwargs): + return MockServiceInfo() + + class MockServiceBrowser: + def __init__(self, zc: MockZeroconf, service_name: str, listener: ZeroconfListener) -> None: + self.service_name = service_name + listener.urls.put_nowait("result") + + def cancel(self): + ... + + monkeypatch.setattr("zeroconf.Zeroconf", MockZeroconf) + monkeypatch.setattr("zeroconf.asyncio.AsyncServiceBrowser", MockServiceBrowser) + monkeypatch.setattr("zeroconf.asyncio.AsyncZeroconf", MockAsyncZeroConf) + assert (await find_first_ip_addr("service")) == IP_ADDR + + +@pytest.mark.asyncio +async def test_wired_lifecycle(mock_wired_gopro: WiredGoPro, monkeypatch): + class MockMdnsScanner: + async def find_first_ip_addr(self, *args, **kwargs) -> str: + return IP_ADDR + + async def set_ready(): + await asyncio.sleep(1) # Allow initial poll to fail + mock_wired_gopro.set_state_response( # type: ignore + { + constants.StatusId.ENCODING: 0, + constants.StatusId.SYSTEM_BUSY: 0, + } + ) + + monkeypatch.setattr("open_gopro.wifi.mdns_scanner", MockMdnsScanner) + await asyncio.gather(mock_wired_gopro.open(), set_ready()) + assert mock_wired_gopro.is_open + assert await mock_wired_gopro.is_ready + assert mock_wired_gopro.identifier == "GoPro X023" + assert mock_wired_gopro._base_url == f"http://{IP_ADDR}:8080/" diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_gopro.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_gopro.py new file mode 100644 index 00000000..5b04e2e2 --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_wireless_gopro.py @@ -0,0 +1,171 @@ +# test_Wirelessgopro.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://Wirelessgopro.com/OpenGoPro). +# This copyright was auto-generated on Fri Sep 10 01:35:03 UTC 2021 + +# pylint: disable=redefined-outer-name + + +"""Unit testing of GoPro Client""" + +import asyncio +from pathlib import Path + +import pytest +import requests +import requests_mock + +from open_gopro.constants import SettingId, StatusId +from open_gopro.exceptions import GoProNotOpened, ResponseTimeout +from open_gopro.gopro_wireless import Params, WirelessGoPro, types +from open_gopro.models.response import GlobalParsers +from tests import mock_good_response + + +@pytest.mark.asyncio +async def test_lifecycle(mock_wireless_gopro: WirelessGoPro): + async def set_disconnect_event(): + mock_wireless_gopro._disconnect_handler(None) + + # We're not yet open so can't send commands + assert not mock_wireless_gopro.is_open + with pytest.raises(GoProNotOpened): + await mock_wireless_gopro.ble_command.enable_wifi_ap(enable=False) + + # Mock ble / wifi open + await mock_wireless_gopro.open() + assert mock_wireless_gopro.is_open + + # Ensure we can't send commands because not ready + assert not await mock_wireless_gopro.is_ready + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(mock_wireless_gopro.ble_command.enable_wifi_ap(enable=False), 1) + + # Mock receiving initial not-encoding and not-busy statuses + await mock_wireless_gopro._update_internal_state(update=StatusId.ENCODING, value=False) + await mock_wireless_gopro._update_internal_state(update=StatusId.SYSTEM_BUSY, value=False) + assert await mock_wireless_gopro.is_ready + + results = await asyncio.gather( + mock_wireless_gopro.ble_command.enable_wifi_ap(enable=False), + mock_wireless_gopro._sync_resp_ready_q.put(mock_good_response), + ) + + assert results[0].ok + assert await mock_wireless_gopro.ble_command.get_open_gopro_api_version() + + # Mock closing + asyncio.gather(mock_wireless_gopro.close(), set_disconnect_event()) + assert mock_wireless_gopro._keep_alive_task.cancelled + + +@pytest.mark.asyncio +async def test_gopro_open(mock_wireless_gopro_basic: WirelessGoPro): + await mock_wireless_gopro_basic.open() + assert mock_wireless_gopro_basic.is_ble_connected + assert mock_wireless_gopro_basic.is_http_connected + assert mock_wireless_gopro_basic.identifier == "scanned_device" + + +@pytest.mark.asyncio +async def test_http_get(mock_wireless_gopro_basic: WirelessGoPro, monkeypatch): + endpoint = "gopro/camera/stream/start" + session = requests.Session() + adapter = requests_mock.Adapter() + session.mount(mock_wireless_gopro_basic._base_url + endpoint, adapter) + adapter.register_uri("GET", mock_wireless_gopro_basic._base_url + endpoint, json="{}") + monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) + response = await mock_wireless_gopro_basic._http_get(endpoint) + assert response.ok + + +@pytest.mark.asyncio +async def test_http_file(mock_wireless_gopro_basic: WirelessGoPro, monkeypatch): + out_file = Path("test.mp4") + endpoint = "videos/DCIM/100GOPRO/dummy.MP4" + session = requests.Session() + adapter = requests_mock.Adapter() + session.mount(mock_wireless_gopro_basic._base_url + endpoint, adapter) + adapter.register_uri("GET", mock_wireless_gopro_basic._base_url + endpoint, text="BINARY DATA") + monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) + await mock_wireless_gopro_basic._stream_to_file(endpoint, out_file) + assert out_file.exists() + + +@pytest.mark.asyncio +async def test_http_response_timeout(mock_wireless_gopro_basic: WirelessGoPro, monkeypatch): + with pytest.raises(ResponseTimeout): + endpoint = "gopro/camera/stream/start" + session = requests.Session() + adapter = requests_mock.Adapter() + session.mount(mock_wireless_gopro_basic._base_url + endpoint, adapter) + adapter.register_uri( + "GET", mock_wireless_gopro_basic._base_url + endpoint, exc=requests.exceptions.ConnectTimeout + ) + monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) + await mock_wireless_gopro_basic._http_get(endpoint, timeout=1) + + +@pytest.mark.asyncio +async def test_http_response_error(mock_wireless_gopro_basic: WirelessGoPro, monkeypatch): + endpoint = "gopro/camera/stream/start" + session = requests.Session() + adapter = requests_mock.Adapter() + session.mount(mock_wireless_gopro_basic._base_url + endpoint, adapter) + adapter.register_uri( + "GET", + mock_wireless_gopro_basic._base_url + endpoint, + status_code=403, + reason="something bad happened", + json="{}", + ) + monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) + response = await mock_wireless_gopro_basic._http_get(endpoint) + assert not response.ok + + +@pytest.mark.asyncio +async def test_get_update(mock_wireless_gopro_basic: WirelessGoPro): + mock_wireless_gopro_basic._loop = asyncio.get_running_loop() + event = asyncio.Event() + + async def receive_encoding_status(id: types.UpdateType, value: bool): + event.set() + + mock_wireless_gopro_basic.register_update(receive_encoding_status, StatusId.ENCODING) + not_encoding = bytearray([0x05, 0x13, 0x00, StatusId.ENCODING.value, 0x01, 0x00]) + mock_wireless_gopro_basic._notification_handler(0xFF, not_encoding) + await event.wait() + + # Now ensure unregistering works + event.clear() + mock_wireless_gopro_basic.unregister_update(receive_encoding_status, StatusId.ENCODING) + not_encoding = bytearray([0x05, 0x13, 0x00, StatusId.ENCODING.value, 0x01, 0x00]) + mock_wireless_gopro_basic._notification_handler(0xFF, not_encoding) + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(event.wait(), 1) + + +@pytest.mark.asyncio +async def test_get_update_unregister_all(mock_wireless_gopro_basic: WirelessGoPro): + event = asyncio.Event() + mock_wireless_gopro_basic._loop = asyncio.get_running_loop() + + async def receive_encoding_status(id: types.UpdateType, value: bool): + event.set() + + mock_wireless_gopro_basic.register_update(receive_encoding_status, StatusId.ENCODING) + not_encoding = bytearray([0x05, 0x13, 0x00, StatusId.ENCODING.value, 0x01, 0x00]) + mock_wireless_gopro_basic._notification_handler(0xFF, not_encoding) + await event.wait() + + # Now ensure unregistering works + event.clear() + mock_wireless_gopro_basic.unregister_update(receive_encoding_status) + not_encoding = bytearray([0x05, 0x13, 0x00, StatusId.ENCODING.value, 0x01, 0x00]) + mock_wireless_gopro_basic._notification_handler(0xFF, not_encoding) + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(event.wait(), 1) + + +def test_get_param_values_by_id(): + vector = list(Params.Resolution)[0] + assert GlobalParsers.get_query_container(SettingId.RESOLUTION)(vector.value) == vector diff --git a/demos/python/sdk_wireless_camera_control/tools/start_rtmp_server.sh b/demos/python/sdk_wireless_camera_control/tools/start_rtmp_server.sh deleted file mode 100755 index 4f48a982..00000000 --- a/demos/python/sdk_wireless_camera_control/tools/start_rtmp_server.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -# start_rtmp_server.sh/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Wed Jul 6 19:59:53 UTC 2022 - - -docker kill rtmp-server >/dev/null 2>&1 -docker run --rm --detach -p 1935:1935 --name rtmp-server tiangolo/nginx-rtmp -echo "rtmp-server Docker container running" -echo "Access the livestream at rtmp:///live/test" -echo -echo "where the IP address of the adapter where the container is running" diff --git a/docs/index.md b/docs/index.md index dd858ae9..b81ed63a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,6 +34,7 @@ Open GoPro API is supported on all camera models since Hero 9 with the following | Hero 10 Black | v01.10.00 | | Hero 11 Black | v01.10.00 | | Hero 11 Black Mini | v01.10.00 | +| Hero 12 Black | v01.10.00 | While we strive to provide the same API functionality and logic for all newly launched cameras, minor changes are sometimes necessary. These are typically a consequence of HW component upgrades or improving or optimizing @@ -98,7 +99,7 @@ directly from the cameras, either via USB or wireless connection. | Stream Type | Description | WiFi | USB | Record while Streaming | | ----------- | --------------------------------------------- | :--: | :-: | :--------------------: | -| : Preview : | Moderate video quality, primarily for framing | | | \ | +| : Preview : | Moderate video quality, primarily for framing | | | `>=` Hero 12 | | Stream | Low latency stabilization | ✔️ | ✔️ | \ | | | Low power consumption | | | | | : Webcam : | Cinematic video quality | | | \ | @@ -106,6 +107,7 @@ directly from the cameras, either via USB or wireless connection. | : Live : | Cinematic video quality | | | \ | | Stream | Optional hypersmooth stabilization | ✔️ | | ✔️ | + Each of the streaming types has different resolutions, bit rates, imaging pipelines, and different levels of configurability. Refer to the [FAQ]({% link faq.md %}). diff --git a/docs/specs/ble_versions/ble_2_0.md b/docs/specs/ble_versions/ble_2_0.md index 43173dba..9a59adc1 100644 --- a/docs/specs/ble_versions/ble_2_0.md +++ b/docs/specs/ble_versions/ble_2_0.md @@ -42,6 +42,12 @@ Below is a table of cameras that support GoPro's public BLE API: Marketing Name Minimal Firmware Version + + 62 + H23.01 + HERO12 Black + v01.10.00 + 60 H22.03 @@ -651,6 +657,10 @@ every 3.0 seconds after a connection is established. ## Limitations +### HERO12 Black +
    +
  • The camera will reject requests to change settings while encoding; for example, if Hindsight feature is active, the user cannot change settings
  • +
### HERO11 Black Mini
  • The camera will reject requests to change settings while encoding; for example, if Hindsight feature is active, the user cannot change settings
  • @@ -845,6 +855,7 @@ Below is a table of commands that can be sent to the camera and how to send them Description Request Response + HERO12 Black HERO11 Black Mini HERO11 Black HERO10 Black @@ -860,6 +871,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x01 @@ -871,6 +883,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x05 @@ -882,6 +895,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x0D @@ -893,6 +907,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x0E @@ -904,6 +919,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x0F @@ -913,6 +929,7 @@ Below is a table of commands that can be sent to the camera and how to send them 02:0F:00 + @@ -924,6 +941,7 @@ Below is a table of commands that can be sent to the camera and how to send them Complex + @@ -937,6 +955,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x17 @@ -948,6 +967,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x18 @@ -959,6 +979,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x3C @@ -970,6 +991,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x3E @@ -981,6 +1003,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x3E @@ -988,6 +1011,7 @@ Below is a table of commands that can be sent to the camera and how to send them Photo 04:3E:02:03:E9 02:3E:00 + @@ -999,6 +1023,7 @@ Below is a table of commands that can be sent to the camera and how to send them Timelapse 04:3E:02:03:EA 02:3E:00 + @@ -1014,6 +1039,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x50 @@ -1025,6 +1051,7 @@ Below is a table of commands that can be sent to the camera and how to send them + 0x51 @@ -1036,6 +1063,7 @@ Below is a table of commands that can be sent to the camera and how to send them + @@ -1390,6 +1418,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Option Request Response + HERO12 Black HERO11 Black Mini HERO11 Black HERO10 Black @@ -1401,6 +1430,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set video resolution (id: 2) to 4k (id: 1) 03:02:01:01 02:02:00 + @@ -1412,6 +1442,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set video resolution (id: 2) to 2.7k (id: 4) 03:02:01:04 02:02:00 + @@ -1423,6 +1454,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set video resolution (id: 2) to 2.7k 4:3 (id: 6) 03:02:01:06 02:02:00 + @@ -1437,6 +1469,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + @@ -1445,6 +1478,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set video resolution (id: 2) to 1080 (id: 9) 03:02:01:09 02:02:00 + @@ -1456,6 +1490,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set video resolution (id: 2) to 4k 4:3 (id: 18) 03:02:01:12 02:02:00 + @@ -1470,6 +1505,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + @@ -1480,6 +1516,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:02:00 + @@ -1491,6 +1528,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:02:00 + @@ -1500,6 +1538,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set video resolution (id: 2) to 5.3k 4:3 (id: 27) 03:02:01:1B 02:02:00 + @@ -1513,6 +1552,31 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:02:00 + + + + + + 2 + Resolution + Set video resolution (id: 2) to 4k 9:16 (id: 29) + 03:02:01:1D + 02:02:00 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 1080 9:16 (id: 30) + 03:02:01:1E + 02:02:00 + + + @@ -1522,10 +1586,83 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set video resolution (id: 2) to 5.3k (id: 100) 03:02:01:64 02:02:00 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 5.3k 16:9 (id: 101) + 03:02:01:65 + 02:02:00 + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 4k 16:9 (id: 102) + 03:02:01:66 + 02:02:00 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 4k 4:3 (id: 103) + 03:02:01:67 + 02:02:00 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 2.7k 16:9 (id: 104) + 03:02:01:68 + 02:02:00 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 2.7k 4:3 (id: 105) + 03:02:01:69 + 02:02:00 + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 1080 16:9 (id: 106) + 03:02:01:6A + 02:02:00 + + + 3 @@ -1537,6 +1674,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 3 @@ -1548,6 +1686,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 3 @@ -1559,6 +1698,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 3 @@ -1570,6 +1710,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 3 @@ -1581,6 +1722,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 3 @@ -1592,6 +1734,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 3 @@ -1603,6 +1746,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 3 @@ -1614,6 +1758,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 3 @@ -1625,6 +1770,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + 59 @@ -1632,6 +1778,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set auto power down (id: 59) to never (id: 0) 03:3B:01:00 01:3B:00 + \>= v02.10.00 @@ -1643,6 +1790,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set auto power down (id: 59) to 1 min (id: 1) 03:3B:01:01 01:3B:00 + \>= v02.10.00 \>= v02.01.00 @@ -1654,6 +1802,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set auto power down (id: 59) to 5 min (id: 4) 03:3B:01:04 01:3B:00 + \>= v02.10.00 @@ -1665,6 +1814,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set auto power down (id: 59) to 15 min (id: 6) 03:3B:01:06 01:3B:00 + @@ -1676,6 +1826,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set auto power down (id: 59) to 30 min (id: 7) 03:3B:01:07 01:3B:00 + @@ -1687,6 +1838,7 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set auto power down (id: 59) to 8 seconds (id: 11) 03:3B:01:0B 01:3B:00 + \>= v02.10.00 @@ -1698,12 +1850,61 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are Set auto power down (id: 59) to 30 seconds (id: 12) 03:3B:01:0C 01:3B:00 + \>= v02.10.00 + 108 + Aspect Ratio + Set video aspect ratio (id: 108) to 4:3 (id: 0) + 03:6C:01:00 + 02:6C:00 + + + + + + + + 108 + Aspect Ratio + Set video aspect ratio (id: 108) to 16:9 (id: 1) + 03:6C:01:01 + 02:6C:00 + + + + + + + + 108 + Aspect Ratio + Set video aspect ratio (id: 108) to 8:7 (id: 3) + 03:6C:01:03 + 02:6C:00 + + + + + + + + 108 + Aspect Ratio + Set video aspect ratio (id: 108) to 9:16 (id: 4) + 03:6C:01:04 + 02:6C:00 + + + + + + + 121 Video Digital Lenses Set video digital lenses (id: 121) to wide (id: 0) @@ -1713,8 +1914,9 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to narrow (id: 2) @@ -1722,10 +1924,11 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:79:00 + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to superview (id: 3) @@ -1735,8 +1938,9 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to linear (id: 4) @@ -1746,19 +1950,21 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to max superview (id: 7) 03:79:01:07 02:79:00 + \>= v02.00.00 - + 121 Video Digital Lenses Set video digital lenses (id: 121) to linear + horizon leveling (id: 8) @@ -1768,8 +1974,9 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to hyperview (id: 9) @@ -1777,10 +1984,11 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:79:00 + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to linear + horizon lock (id: 10) @@ -1788,10 +1996,23 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:79:00 + + 121 + Video Digital Lenses + Set video digital lenses (id: 121) to max hyperview (id: 11) + 03:79:01:0B + 02:79:00 + + + + + + + 122 Photo Digital Lenses Set photo digital lenses (id: 122) to narrow (id: 19) @@ -1799,43 +2020,47 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:7A:00 + - + 122 Photo Digital Lenses Set photo digital lenses (id: 122) to max superview (id: 100) 03:7A:01:64 02:7A:00 + - + 122 Photo Digital Lenses Set photo digital lenses (id: 122) to wide (id: 101) 03:7A:01:65 02:7A:00 + - + 122 Photo Digital Lenses Set photo digital lenses (id: 122) to linear (id: 102) 03:7A:01:66 02:7A:00 + - + 123 Time Lapse Digital Lenses Set time lapse digital lenses (id: 123) to narrow (id: 19) @@ -1843,87 +2068,95 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:7B:00 + - + 123 Time Lapse Digital Lenses Set time lapse digital lenses (id: 123) to max superview (id: 100) 03:7B:01:64 02:7B:00 + - + 123 Time Lapse Digital Lenses Set time lapse digital lenses (id: 123) to wide (id: 101) 03:7B:01:65 02:7B:00 + - + 123 Time Lapse Digital Lenses Set time lapse digital lenses (id: 123) to linear (id: 102) 03:7B:01:66 02:7B:00 + - + 128 Media Format Set media format (id: 128) to time lapse video (id: 13) 03:80:01:0D 02:80:00 + - + 128 Media Format Set media format (id: 128) to time lapse photo (id: 20) 03:80:01:14 02:80:00 + - + 128 Media Format Set media format (id: 128) to night lapse photo (id: 21) 03:80:01:15 02:80:00 + - + 128 Media Format Set media format (id: 128) to night lapse video (id: 26) 03:80:01:1A 02:80:00 + - + 134 Anti-Flicker Set setup anti flicker (id: 134) to 60hz (id: 2) @@ -1933,8 +2166,9 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + - + 134 Anti-Flicker Set setup anti flicker (id: 134) to 50hz (id: 3) @@ -1944,8 +2178,9 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + - + 135 Hypersmooth Set video hypersmooth (id: 135) to off (id: 0) @@ -1955,19 +2190,21 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are + - + 135 Hypersmooth - Set video hypersmooth (id: 135) to on (id: 1) + Set video hypersmooth (id: 135) to low (id: 1) 03:87:01:01 02:87:00 + - + 135 Hypersmooth Set video hypersmooth (id: 135) to high (id: 2) @@ -1975,21 +2212,23 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:87:00 + - + 135 Hypersmooth Set video hypersmooth (id: 135) to boost (id: 3) 03:87:01:03 02:87:00 + - + 135 Hypersmooth Set video hypersmooth (id: 135) to auto boost (id: 4) @@ -1997,10 +2236,11 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:87:00 + - + 135 Hypersmooth Set video hypersmooth (id: 135) to standard (id: 100) @@ -2008,120 +2248,371 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:87:00 + - + 150 Horizon Leveling Set video horizon levelling (id: 150) to off (id: 0) 03:96:01:00 02:96:00 + \>= v02.00.00 - + 150 Horizon Leveling Set video horizon levelling (id: 150) to on (id: 1) 03:96:01:01 02:96:00 + \>= v02.00.00 - + 150 Horizon Leveling Set video horizon levelling (id: 150) to locked (id: 2) 03:96:01:02 02:96:00 + - + 151 Horizon Leveling Set photo horizon levelling (id: 151) to off (id: 0) 03:97:01:00 02:97:00 + - + 151 Horizon Leveling Set photo horizon levelling (id: 151) to locked (id: 2) 03:97:01:02 02:97:00 + - + 162 Max Lens Set max lens (id: 162) to off (id: 0) 03:A2:01:00 02:A2:00 + \>= v01.20.00 - + 162 Max Lens Set max lens (id: 162) to on (id: 1) 03:A2:01:01 02:A2:00 + \>= v01.20.00 - + 167 Hindsight* Set hindsight (id: 167) to 15 seconds (id: 2) 03:A7:01:02 02:A7:00 + - + 167 Hindsight* Set hindsight (id: 167) to 30 seconds (id: 3) 03:A7:01:03 02:A7:00 + - + 167 Hindsight* Set hindsight (id: 167) to off (id: 4) 03:A7:01:04 02:A7:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to off (id: 0) + 03:AB:01:00 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 0.5s (id: 2) + 03:AB:01:02 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 1s (id: 3) + 03:AB:01:03 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 2s (id: 4) + 03:AB:01:04 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 5s (id: 5) + 03:AB:01:05 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 10s (id: 6) + 03:AB:01:06 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 30s (id: 7) + 03:AB:01:07 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 60s (id: 8) + 03:AB:01:08 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 120s (id: 9) + 03:AB:01:09 + 02:AB:00 + + + + + + + + 171 + Interval + Set photo single interval (id: 171) to 3s (id: 10) + 03:AB:01:0A + 02:AB:00 + + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to off (id: 0) + 03:AC:01:00 + 02:AC:00 + + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to 15 seconds (id: 1) + 03:AC:01:01 + 02:AC:00 + + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to 30 seconds (id: 2) + 03:AC:01:02 + 02:AC:00 + + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to 1 minute (id: 3) + 03:AC:01:03 + 02:AC:00 + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to 5 minutes (id: 4) + 03:AC:01:04 + 02:AC:00 + + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to 15 minutes (id: 5) + 03:AC:01:05 + 02:AC:00 + + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to 30 minutes (id: 6) + 03:AC:01:06 + 02:AC:00 + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to 1 hour (id: 7) + 03:AC:01:07 + 02:AC:00 + + + + + + + 172 + Duration + Set photo interval duration (id: 172) to 2 hours (id: 8) + 03:AC:01:08 + 02:AC:00 + + + + + 172 + Duration + Set photo interval duration (id: 172) to 3 hours (id: 9) + 03:AC:01:09 + 02:AC:00 + + + + + + + 173 Video Performance Mode Set video performance mode (id: 173) to maximum video performance (id: 0) @@ -2129,10 +2620,11 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:AD:00 + \>= v01.16.00 - + 173 Video Performance Mode Set video performance mode (id: 173) to extended battery (id: 1) @@ -2140,10 +2632,11 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:AD:00 + \>= v01.16.00 - + 173 Video Performance Mode Set video performance mode (id: 173) to tripod / stationary video (id: 2) @@ -2151,10 +2644,11 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:AD:00 + \>= v01.16.00 - + 175 Controls Set controls (id: 175) to easy (id: 0) @@ -2162,10 +2656,11 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:AF:00 + - + 175 Controls Set controls (id: 175) to pro (id: 1) @@ -2173,435 +2668,1159 @@ All settings are sent to UUID GP-0074. All values are hexadecimal and length are 02:AF:00 + - + 176 Speed Set speed (id: 176) to 8x ultra slo-mo (id: 0) 03:B0:01:00 02:B0:00 + - + 176 Speed Set speed (id: 176) to 4x super slo-mo (id: 1) 03:B0:01:01 02:B0:00 + - + 176 Speed Set speed (id: 176) to 2x slo-mo (id: 2) 03:B0:01:02 02:B0:00 + - + 176 Speed Set speed (id: 176) to 1x (low light) (id: 3) 03:B0:01:03 02:B0:00 + - + 176 Speed Set speed (id: 176) to 4x super slo-mo (ext. batt) (id: 4) 03:B0:01:04 02:B0:00 + - + 176 Speed Set speed (id: 176) to 2x slo-mo (ext. batt) (id: 5) 03:B0:01:05 02:B0:00 + - + 176 Speed Set speed (id: 176) to 1x (ext. batt, low light) (id: 6) 03:B0:01:06 02:B0:00 + - + 176 Speed Set speed (id: 176) to 8x ultra slo-mo (50hz) (id: 7) 03:B0:01:07 02:B0:00 + - + 176 Speed Set speed (id: 176) to 4x super slo-mo (50hz) (id: 8) 03:B0:01:08 02:B0:00 + - + 176 Speed Set speed (id: 176) to 2x slo-mo (50hz) (id: 9) 03:B0:01:09 02:B0:00 + - + 176 Speed Set speed (id: 176) to 1x (low light, 50hz) (id: 10) 03:B0:01:0A 02:B0:00 + - + 176 Speed Set speed (id: 176) to 4x super slo-mo (ext. batt, 50hz) (id: 11) 03:B0:01:0B 02:B0:00 + - + 176 Speed Set speed (id: 176) to 2x slo-mo (ext. batt, 50hz) (id: 12) 03:B0:01:0C 02:B0:00 + - + 176 Speed Set speed (id: 176) to 1x (ext. batt, low light, 50hz) (id: 13) 03:B0:01:0D 02:B0:00 + - + 176 Speed Set speed (id: 176) to 8x ultra slo-mo (ext. batt) (id: 14) 03:B0:01:0E 02:B0:00 + \>= v02.01.00 - + 176 Speed Set speed (id: 176) to 8x ultra slo-mo (ext. batt, 50hz) (id: 15) 03:B0:01:0F 02:B0:00 + \>= v02.01.00 - + 176 Speed Set speed (id: 176) to 8x ultra slo-mo (long. batt) (id: 16) 03:B0:01:10 02:B0:00 + \>= v02.01.00 - + 176 Speed Set speed (id: 176) to 4x super slo-mo (long. batt) (id: 17) 03:B0:01:11 02:B0:00 + \>= v02.01.00 - + 176 Speed Set speed (id: 176) to 2x slo-mo (long. batt) (id: 18) 03:B0:01:12 02:B0:00 + \>= v02.01.00 - + 176 Speed Set speed (id: 176) to 1x (long. batt, low light) (id: 19) 03:B0:01:13 02:B0:00 + \>= v02.01.00 - + 176 Speed Set speed (id: 176) to 8x ultra slo-mo (long. batt, 50hz) (id: 20) 03:B0:01:14 02:B0:00 + \>= v02.01.00 - + 176 Speed Set speed (id: 176) to 4x super slo-mo (long. batt, 50hz) (id: 21) 03:B0:01:15 02:B0:00 + \>= v02.01.00 - + 176 Speed Set speed (id: 176) to 2x slo-mo (long. batt, 50hz) (id: 22) 03:B0:01:16 02:B0:00 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 1x (long. batt, low light, 50hz) (id: 23) + 03:B0:01:17 + 02:B0:00 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (4k) (id: 24) + 03:B0:01:18 + 02:B0:00 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 4x super slo-mo (2.7k) (id: 25) + 03:B0:01:19 + 02:B0:00 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (4k, 50hz) (id: 26) + 03:B0:01:1A + 02:B0:00 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 4x super slo-mo (2.7k, 50hz) (id: 27) + 03:B0:01:1B + 02:B0:00 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 28) + 03:B0:01:1C + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 29) + 03:B0:01:1D + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 30) + 03:B0:01:1E + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 31) + 03:B0:01:1F + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 32) + 03:B0:01:20 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 33) + 03:B0:01:21 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 34) + 03:B0:01:22 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 35) + 03:B0:01:23 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 36) + 03:B0:01:24 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 37) + 03:B0:01:25 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 38) + 03:B0:01:26 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 39) + 03:B0:01:27 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 40) + 03:B0:01:28 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 41) + 03:B0:01:29 + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 42) + 03:B0:01:2A + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 43) + 03:B0:01:2B + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 44) + 03:B0:01:2C + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 45) + 03:B0:01:2D + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 46) + 03:B0:01:2E + 02:B0:00 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 47) + 03:B0:01:2F + 02:B0:00 + + + + + + + + 177 + Enable Night Photo + Set enable night photo (id: 177) to off (id: 0) + 03:B1:01:00 + 02:B1:00 + + + + + + + + 177 + Enable Night Photo + Set enable night photo (id: 177) to on (id: 1) + 03:B1:01:01 + 02:B1:00 + + + + + + + + 178 + Wireless Band + Set wireless band (id: 178) to 2.4ghz (id: 0) + 03:B2:01:00 + 02:B2:00 + + + + + + + + 178 + Wireless Band + Set wireless band (id: 178) to 5ghz (id: 1) + 03:B2:01:01 + 02:B2:00 + + + + + + + + 179 + Trail Length + Set trail length (id: 179) to short (id: 1) + 03:B3:01:01 + 02:B3:00 + + + + + + + + 179 + Trail Length + Set trail length (id: 179) to long (id: 2) + 03:B3:01:02 + 02:B3:00 + + + + + + + + 179 + Trail Length + Set trail length (id: 179) to max (id: 3) + 03:B3:01:03 + 02:B3:00 + + + + + + + + 180 + Video Mode + Set video mode (id: 180) to highest quality (id: 0) + 03:B4:01:00 + 02:B4:00 + + + + + + + + 180 + Video Mode + Set video mode (id: 180) to extended battery (id: 1) + 03:B4:01:01 + 02:B4:00 + + + + + + + + 180 + Video Mode + Set video mode (id: 180) to extended battery (green icon) (id: 101) + 03:B4:01:65 + 02:B4:00 + + + \>= v02.01.00 + + + + + 180 + Video Mode + Set video mode (id: 180) to longest battery (green icon) (id: 102) + 03:B4:01:66 + 02:B4:00 + + + \>= v02.01.00 + + + + + 182 + Bit Rate + Set system video bit rate (id: 182) to standard (id: 0) + 03:B6:01:00 + 02:B6:00 + + + + + + + + 182 + Bit Rate + Set system video bit rate (id: 182) to high (id: 1) + 03:B6:01:01 + 02:B6:00 + + + + + + + + 183 + Bit Depth + Set system video bit depth (id: 183) to 8-bit (id: 0) + 03:B7:01:00 + 02:B7:00 + + + + + + + + 183 + Bit Depth + Set system video bit depth (id: 183) to 10-bit (id: 2) + 03:B7:01:02 + 02:B7:00 + + + + + + + + 184 + Profiles + Set video profile (id: 184) to standard (id: 0) + 03:B8:01:00 + 02:B8:00 + + + + + + + + 184 + Profiles + Set video profile (id: 184) to hdr (id: 1) + 03:B8:01:01 + 02:B8:00 + + + + + + + + 184 + Profiles + Set video profile (id: 184) to log (id: 2) + 03:B8:01:02 + 02:B8:00 + + + + + + + + 185 + Aspect Ratio + Set video easy aspect ratio (id: 185) to widescreen (id: 0) + 03:B9:01:00 + 02:B9:00 + + + + + + + + 185 + Aspect Ratio + Set video easy aspect ratio (id: 185) to mobile (id: 1) + 03:B9:01:01 + 02:B9:00 + + + + + + + + 185 + Aspect Ratio + Set video easy aspect ratio (id: 185) to universal (id: 2) + 03:B9:01:02 + 02:B9:00 + + + + + + + + 186 + Video Mode + Set video easy presets (id: 186) to highest quality (id: 0) + 03:BA:01:00 + 02:BA:00 + + + + + + + + 186 + Video Mode + Set video easy presets (id: 186) to standard quality (id: 1) + 03:BA:01:01 + 02:BA:00 + + + + + + + + 186 + Video Mode + Set video easy presets (id: 186) to basic quality (id: 2) + 03:BA:01:02 + 02:BA:00 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to timewarp (id: 0) + 03:BB:01:00 + 02:BB:00 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to star trails (id: 1) + 03:BB:01:01 + 02:BB:00 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to light painting (id: 2) + 03:BB:01:02 + 02:BB:00 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to vehicle lights (id: 3) + 03:BB:01:03 + 02:BB:00 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to max timewarp (id: 4) + 03:BB:01:04 + 02:BB:00 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to max star trails (id: 5) + 03:BB:01:05 + 02:BB:00 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to max light painting (id: 6) + 03:BB:01:06 + 02:BB:00 + + - \>= v02.01.00 - - 176 - Speed - Set speed (id: 176) to 1x (long. batt, low light, 50hz) (id: 23) - 03:B0:01:17 - 02:B0:00 + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to max vehicle lights (id: 7) + 03:BB:01:07 + 02:BB:00 + + - \>= v02.01.00 - 176 - Speed - Set speed (id: 176) to 2x slo-mo (4k) (id: 24) - 03:B0:01:18 - 02:B0:00 + 188 + Aspect Ratio + Set multi shot easy aspect ratio (id: 188) to widescreen (id: 0) + 03:BC:01:00 + 02:BC:00 + + - \>= v02.01.00 - 176 - Speed - Set speed (id: 176) to 4x super slo-mo (2.7k) (id: 25) - 03:B0:01:19 - 02:B0:00 + 188 + Aspect Ratio + Set multi shot easy aspect ratio (id: 188) to mobile (id: 1) + 03:BC:01:01 + 02:BC:00 + + - \>= v02.01.00 - 176 - Speed - Set speed (id: 176) to 2x slo-mo (4k, 50hz) (id: 26) - 03:B0:01:1A - 02:B0:00 + 188 + Aspect Ratio + Set multi shot easy aspect ratio (id: 188) to universal (id: 2) + 03:BC:01:02 + 02:BC:00 + + - \>= v02.01.00 - - 176 - Speed - Set speed (id: 176) to 4x super slo-mo (2.7k, 50hz) (id: 27) - 03:B0:01:1B - 02:B0:00 + + 189 + Max Lens Mod + Set system addon lens active (id: 189) to none (id: 0) + 03:BD:01:00 + 02:BD:00 + + - \>= v02.01.00 - 177 - Enable Night Photo - Set enable night photo (id: 177) to off (id: 0) - 03:B1:01:00 - 02:B1:00 - + 189 + Max Lens Mod + Set system addon lens active (id: 189) to max lens 1.0 (id: 1) + 03:BD:01:01 + 02:BD:00 + + - 177 - Enable Night Photo - Set enable night photo (id: 177) to on (id: 1) - 03:B1:01:01 - 02:B1:00 - + 189 + Max Lens Mod + Set system addon lens active (id: 189) to max lens 2.0 (id: 2) + 03:BD:01:02 + 02:BD:00 + + - 178 - Wireless Band - Set wireless band (id: 178) to 2.4ghz (id: 0) - 03:B2:01:00 - 02:B2:00 - + 190 + Max Lens Mod Enable + Set system addon lens status (id: 190) to off (id: 0) + 03:BE:01:00 + 02:BE:00 + + - 178 - Wireless Band - Set wireless band (id: 178) to 5ghz (id: 1) - 03:B2:01:01 - 02:B2:00 - + 190 + Max Lens Mod Enable + Set system addon lens status (id: 190) to on (id: 1) + 03:BE:01:01 + 02:BE:00 + + - 179 - Trail Length - Set trail length (id: 179) to short (id: 1) - 03:B3:01:01 - 02:B3:00 - + 191 + Photo Mode + Set photo easy presets (id: 191) to super photo (id: 0) + 03:BF:01:00 + 02:BF:00 + + - 179 - Trail Length - Set trail length (id: 179) to long (id: 2) - 03:B3:01:02 - 02:B3:00 - + 191 + Photo Mode + Set photo easy presets (id: 191) to night photo (id: 1) + 03:BF:01:01 + 02:BF:00 + + - - 179 - Trail Length - Set trail length (id: 179) to max (id: 3) - 03:B3:01:03 - 02:B3:00 - + + 192 + Aspect Ratio + Set multi shot nlv aspect ratio (id: 192) to 4:3 (id: 0) + 03:C0:01:00 + 02:C0:00 + + - 180 - Video Mode - Set video mode (id: 180) to highest quality (id: 0) - 03:B4:01:00 - 02:B4:00 - + 192 + Aspect Ratio + Set multi shot nlv aspect ratio (id: 192) to 16:9 (id: 1) + 03:C0:01:01 + 02:C0:00 + + - 180 - Video Mode - Set video mode (id: 180) to extended battery (id: 1) - 03:B4:01:01 - 02:B4:00 + 192 + Aspect Ratio + Set multi shot nlv aspect ratio (id: 192) to 8:7 (id: 3) + 03:C0:01:03 + 02:C0:00 + + + + + + + 193 + Framing + Set video easy framing (id: 193) to widescreen (id: 0) + 03:C1:01:00 + 02:C1:00 + + - - 180 - Video Mode - Set video mode (id: 180) to extended battery (green icon) (id: 101) - 03:B4:01:65 - 02:B4:00 + + 193 + Framing + Set video easy framing (id: 193) to vertical (id: 1) + 03:C1:01:01 + 02:C1:00 + + - \>= v02.01.00 - - 180 - Video Mode - Set video mode (id: 180) to longest battery (green icon) (id: 102) - 03:B4:01:66 - 02:B4:00 + + 193 + Framing + Set video easy framing (id: 193) to full frame (id: 2) + 03:C1:01:02 + 02:C1:00 + + - \>= v02.01.00 @@ -2673,7 +3892,11 @@ If the user tries to set Video FPS to 240, it will fail because 4K/240fps is not Release - capabilities.xlsx
    capabilities.json + capabilities.xlsx
    capabilities.json + HERO12 Black + v01.10.00 + + HERO11 Black Mini v02.30.00 @@ -3005,6 +4228,7 @@ Below is a table of supported status IDs.
    Description Type Values + HERO12 Black HERO11 Black Mini HERO11 Black HERO10 Black @@ -3020,6 +4244,7 @@ Below is a table of supported status IDs.
    + 2 @@ -3031,6 +4256,7 @@ Below is a table of supported status IDs.
    + 6 @@ -3042,6 +4268,7 @@ Below is a table of supported status IDs.
    + 8 @@ -3053,6 +4280,7 @@ Below is a table of supported status IDs.
    + 9 @@ -3064,6 +4292,7 @@ Below is a table of supported status IDs.
    + 10 @@ -3075,6 +4304,7 @@ Below is a table of supported status IDs.
    + 11 @@ -3086,6 +4316,7 @@ Below is a table of supported status IDs.
    + 13 @@ -3097,6 +4328,7 @@ Below is a table of supported status IDs.
    + 17 @@ -3108,6 +4340,7 @@ Below is a table of supported status IDs.
    + 19 @@ -3119,6 +4352,7 @@ Below is a table of supported status IDs.
    + 20 @@ -3130,6 +4364,7 @@ Below is a table of supported status IDs.
    + 21 @@ -3137,6 +4372,7 @@ Below is a table of supported status IDs.
    Time (milliseconds) since boot of last successful pairing complete action integer * + @@ -3152,6 +4388,7 @@ Below is a table of supported status IDs.
    + 23 @@ -3163,6 +4400,7 @@ Below is a table of supported status IDs.
    + 24 @@ -3174,6 +4412,7 @@ Below is a table of supported status IDs.
    + 26 @@ -3181,6 +4420,7 @@ Below is a table of supported status IDs.
    Wireless remote control version integer * + @@ -3196,6 +4436,7 @@ Below is a table of supported status IDs.
    + 28 @@ -3203,6 +4444,7 @@ Below is a table of supported status IDs.
    Wireless Pairing State integer * + @@ -3218,6 +4460,7 @@ Below is a table of supported status IDs.
    + 30 @@ -3229,6 +4472,7 @@ Below is a table of supported status IDs.
    + 31 @@ -3240,6 +4484,7 @@ Below is a table of supported status IDs.
    + 32 @@ -3251,6 +4496,7 @@ Below is a table of supported status IDs.
    + 33 @@ -3262,6 +4508,7 @@ Below is a table of supported status IDs.
    + 34 @@ -3269,6 +4516,7 @@ Below is a table of supported status IDs.
    How many photos can be taken before sdcard is full integer * + @@ -3284,6 +4532,7 @@ Below is a table of supported status IDs.
    + 36 @@ -3291,6 +4540,7 @@ Below is a table of supported status IDs.
    How many group photos can be taken with current settings before sdcard is full integer * + @@ -3306,6 +4556,7 @@ Below is a table of supported status IDs.
    + 38 @@ -3317,6 +4568,7 @@ Below is a table of supported status IDs.
    + 39 @@ -3328,6 +4580,7 @@ Below is a table of supported status IDs.
    + 41 @@ -3339,6 +4592,7 @@ Below is a table of supported status IDs.
    + 42 @@ -3350,6 +4604,7 @@ Below is a table of supported status IDs.
    + 45 @@ -3361,6 +4616,7 @@ Below is a table of supported status IDs.
    + 49 @@ -3372,6 +4628,7 @@ Below is a table of supported status IDs.
    + 54 @@ -3383,6 +4640,7 @@ Below is a table of supported status IDs.
    + 55 @@ -3394,6 +4652,7 @@ Below is a table of supported status IDs.
    + 56 @@ -3405,6 +4664,7 @@ Below is a table of supported status IDs.
    + 58 @@ -3416,6 +4676,7 @@ Below is a table of supported status IDs.
    + 59 @@ -3427,6 +4688,7 @@ Below is a table of supported status IDs.
    + 60 @@ -3438,6 +4700,7 @@ Below is a table of supported status IDs.
    + 64 @@ -3449,6 +4712,7 @@ Below is a table of supported status IDs.
    + 65 @@ -3456,6 +4720,7 @@ Below is a table of supported status IDs.
    Liveview Exposure Select Mode integer 0: Disabled
    1: Auto
    2: ISO Lock
    3: Hemisphere
    + @@ -3467,6 +4732,7 @@ Below is a table of supported status IDs.
    Liveview Exposure Select: y-coordinate (percent) percent 0-100 + @@ -3478,6 +4744,7 @@ Below is a table of supported status IDs.
    Liveview Exposure Select: y-coordinate (percent) percent 0-100 + @@ -3493,6 +4760,7 @@ Below is a table of supported status IDs.
    + 69 @@ -3504,6 +4772,7 @@ Below is a table of supported status IDs.
    + 70 @@ -3515,6 +4784,7 @@ Below is a table of supported status IDs.
    + 74 @@ -3526,6 +4796,7 @@ Below is a table of supported status IDs.
    + 75 @@ -3537,6 +4808,7 @@ Below is a table of supported status IDs.
    + 76 @@ -3548,6 +4820,7 @@ Below is a table of supported status IDs.
    + 77 @@ -3559,6 +4832,7 @@ Below is a table of supported status IDs.
    + 78 @@ -3570,6 +4844,7 @@ Below is a table of supported status IDs.
    + 79 @@ -3579,6 +4854,7 @@ Below is a table of supported status IDs.
    0: False
    1: True
    + @@ -3592,6 +4868,7 @@ Below is a table of supported status IDs.
    + 82 @@ -3603,6 +4880,7 @@ Below is a table of supported status IDs.
    + 83 @@ -3614,6 +4892,7 @@ Below is a table of supported status IDs.
    + 85 @@ -3625,6 +4904,7 @@ Below is a table of supported status IDs.
    + 86 @@ -3636,6 +4916,7 @@ Below is a table of supported status IDs.
    + 88 @@ -3647,6 +4928,7 @@ Below is a table of supported status IDs.
    + 89 @@ -3658,6 +4940,7 @@ Below is a table of supported status IDs.
    + 93 @@ -3669,6 +4952,7 @@ Below is a table of supported status IDs.
    + 94 @@ -3676,6 +4960,7 @@ Below is a table of supported status IDs.
    Current Photo Preset (ID) integer * + @@ -3691,6 +4976,7 @@ Below is a table of supported status IDs.
    + 96 @@ -3702,6 +4988,7 @@ Below is a table of supported status IDs.
    + 97 @@ -3713,6 +5000,7 @@ Below is a table of supported status IDs.
    + 98 @@ -3724,6 +5012,7 @@ Below is a table of supported status IDs.
    + 99 @@ -3732,6 +5021,7 @@ Below is a table of supported status IDs.
    integer * + @@ -3743,6 +5033,7 @@ Below is a table of supported status IDs.
    integer * + @@ -3757,6 +5048,7 @@ Below is a table of supported status IDs.
    + 102 @@ -3768,6 +5060,7 @@ Below is a table of supported status IDs.
    + 103 @@ -3779,6 +5072,7 @@ Below is a table of supported status IDs.
    + 104 @@ -3788,15 +5082,17 @@ Below is a table of supported status IDs.
    0: False
    1: True
    + 105 Camera lens type - Camera lens type (reflects changes to setting 162) + Camera lens type (reflects changes to setting 162 or setting 189) integer - 0: Default
    1: Max Lens
    + 0: Default
    1: Max Lens
    2: Max Lens 2.0
    + @@ -3808,6 +5104,7 @@ Below is a table of supported status IDs.
    Is Video Hindsight Capture Active? boolean 0: False
    1: True
    + @@ -3819,6 +5116,7 @@ Below is a table of supported status IDs.
    Scheduled Capture Preset ID integer * + @@ -3830,6 +5128,7 @@ Below is a table of supported status IDs.
    Is Scheduled Capture set? boolean 0: False
    1: True
    + @@ -3841,6 +5140,7 @@ Below is a table of supported status IDs.
    Media Mode Status (bitmasked) integer 0: 000 = Selfie mod: 0, HDMI: 0, Media Mod Connected: False
    1: 001 = Selfie mod: 0, HDMI: 0, Media Mod Connected: True
    2: 010 = Selfie mod: 0, HDMI: 1, Media Mod Connected: False
    3: 011 = Selfie mod: 0, HDMI: 1, Media Mod Connected: True
    4: 100 = Selfie mod: 1, HDMI: 0, Media Mod Connected: False
    5: 101 = Selfie mod: 1, HDMI: 0, Media Mod Connected: True
    6: 110 = Selfie mod: 1, HDMI: 1, Media Mod Connected: False
    7: 111 = Selfie mod: 1, HDMI: 1, Media Mod Connected: True
    + @@ -3855,6 +5155,7 @@ Below is a table of supported status IDs.
    + @@ -3866,6 +5167,7 @@ Below is a table of supported status IDs.
    + @@ -3878,6 +5180,7 @@ Below is a table of supported status IDs.
    + 114 @@ -3888,6 +5191,7 @@ Below is a table of supported status IDs.
    + @@ -3899,6 +5203,7 @@ Below is a table of supported status IDs.
    + @@ -3909,6 +5214,7 @@ Below is a table of supported status IDs.
    0: Disabled
    1: Enabled
    + \>= v01.30.00 @@ -3920,6 +5226,7 @@ Below is a table of supported status IDs.
    * + @@ -4059,6 +5366,7 @@ For consistency, best practice is to always serialize the protobuf objects regar Description Request Response + HERO12 Black HERO11 Black Mini HERO11 Black HERO10 Black @@ -4075,6 +5383,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + @@ -4086,6 +5395,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + 0x03 @@ -4097,6 +5407,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + 0x04 @@ -4108,6 +5419,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + @@ -4119,6 +5431,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + 0x05 @@ -4130,6 +5443,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + @@ -4141,6 +5455,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + 0xF1 @@ -4151,6 +5466,7 @@ For consistency, best practice is to always serialize the protobuf objects regar ResponseGeneric + \>= v01.20.00 @@ -4164,6 +5480,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + 0x79 @@ -4175,6 +5492,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + 0xF5 @@ -4187,6 +5505,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + @@ -4198,6 +5517,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + 0x74 @@ -4209,6 +5529,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + @@ -4220,6 +5541,7 @@ For consistency, best practice is to always serialize the protobuf objects regar + @@ -4269,6 +5591,26 @@ Below is a table of settings that affect the current preset collection and there 180 Video Mode + + 186 + Video Mode + + + 187 + Lapse Mode + + + 189 + Max Lens Mod + + + 190 + Max Lens Mod Enable + + + 191 + Photo Mode + diff --git a/docs/specs/capabilities.json b/docs/specs/capabilities.json index 29972abc..52281a09 100644 --- a/docs/specs/capabilities.json +++ b/docs/specs/capabilities.json @@ -1,4 +1,3722 @@ { + "HERO12 Black": { + "v01.10.00": { + "states": [ + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "30", + "option_id": 8 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "60", + "option_id": 5 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "100", + "option_id": 2 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "100", + "option_id": 2 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "100", + "option_id": 2 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "100", + "option_id": 2 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "120", + "option_id": 1 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "200", + "option_id": 13 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "200", + "option_id": 13 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "200", + "option_id": 13 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "200", + "option_id": 13 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "1080 16:9", + "option_id": 106 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "240", + "option_id": 0 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "2.7K 16:9", + "option_id": 104 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "200", + "option_id": 13 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "2.7K 16:9", + "option_id": 104 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "200", + "option_id": 13 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "2.7K 16:9", + "option_id": 104 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "200", + "option_id": 13 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "2.7K 16:9", + "option_id": 104 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "200", + "option_id": 13 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "2.7K 16:9", + "option_id": 104 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "240", + "option_id": 0 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "30", + "option_id": 8 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "60", + "option_id": 5 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "100", + "option_id": 2 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "100", + "option_id": 2 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "100", + "option_id": 2 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "100", + "option_id": 2 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "4K 16:9", + "option_id": 102 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "120", + "option_id": 1 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Superview", + "option_id": 3 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Linear", + "option_id": 4 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "24", + "option_id": 10 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "HyperView", + "option_id": 9 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "25", + "option_id": 9 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "30", + "option_id": 8 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Low", + "option_id": 1 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Auto Boost", + "option_id": 4 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "50", + "option_id": 6 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "60Hz", + "option_id": 2 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ], + [ + { + "setting_name": "Resolution", + "setting_id": 2, + "option_name": "5.3K 16:9", + "option_id": 101 + }, + { + "setting_name": "Frames Per Second", + "setting_id": 3, + "option_name": "60", + "option_id": 5 + }, + { + "setting_name": "Video Digital Lenses", + "setting_id": 121, + "option_name": "Wide", + "option_id": 0 + }, + { + "setting_name": "Anti-Flicker", + "setting_id": 134, + "option_name": "50Hz", + "option_id": 3 + }, + { + "setting_name": "Hypersmooth", + "setting_id": 135, + "option_name": "Off", + "option_id": 0 + } + ] + ] + } + }, "HERO11 Black Mini": { "v02.30.00": { "states": [ @@ -62,7 +3780,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -190,7 +3908,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -318,7 +4036,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -446,7 +4164,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -574,7 +4292,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -702,7 +4420,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -862,7 +4580,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -1054,7 +4772,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -1246,7 +4964,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -1438,7 +5156,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -1630,7 +5348,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -1822,7 +5540,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -2014,7 +5732,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -2206,7 +5924,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -2398,7 +6116,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -2590,7 +6308,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -2718,7 +6436,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -2846,7 +6564,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -2974,7 +6692,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -3102,7 +6820,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -3230,7 +6948,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -3390,7 +7108,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -3582,7 +7300,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -3710,7 +7428,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -3838,7 +7556,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -3966,7 +7684,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -4126,7 +7844,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -4318,7 +8036,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -4510,7 +8228,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -4638,7 +8356,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -4766,7 +8484,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -4894,7 +8612,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -5022,7 +8740,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -5150,7 +8868,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -5278,7 +8996,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -5406,7 +9124,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -5534,7 +9252,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -5694,7 +9412,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -5886,7 +9604,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -6078,7 +9796,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -6206,7 +9924,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -6334,7 +10052,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -6462,7 +10180,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -6622,7 +10340,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -6814,7 +10532,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -6878,7 +10596,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -6978,7 +10696,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -7106,7 +10824,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -7234,7 +10952,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -7362,7 +11080,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -7490,7 +11208,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -7618,7 +11336,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -7778,7 +11496,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -7970,7 +11688,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -8162,7 +11880,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -8354,7 +12072,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -8546,7 +12264,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -8738,7 +12456,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -8930,7 +12648,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -9122,7 +12840,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -9314,7 +13032,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -9506,7 +13224,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -9634,7 +13352,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -9762,7 +13480,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -9890,7 +13608,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -10018,7 +13736,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -10146,7 +13864,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -10306,7 +14024,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -10498,7 +14216,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -10626,7 +14344,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -10754,7 +14472,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -10882,7 +14600,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -11042,7 +14760,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -11234,7 +14952,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -11426,7 +15144,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -11554,7 +15272,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -11682,7 +15400,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -11810,7 +15528,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -11938,7 +15656,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -12066,7 +15784,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -12194,7 +15912,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -12322,7 +16040,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -12450,7 +16168,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -12610,7 +16328,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -12802,7 +16520,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -12994,7 +16712,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -13122,7 +16840,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -13250,7 +16968,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -13378,7 +17096,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -13538,7 +17256,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -13730,7 +17448,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -13794,7 +17512,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -13894,7 +17612,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -14022,7 +17740,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -14150,7 +17868,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -14278,7 +17996,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -14406,7 +18124,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -14534,7 +18252,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -14694,7 +18412,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -14886,7 +18604,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -15078,7 +18796,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -15270,7 +18988,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -15462,7 +19180,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -15654,7 +19372,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -15846,7 +19564,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -16038,7 +19756,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -16230,7 +19948,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -16422,7 +20140,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -16550,7 +20268,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -16678,7 +20396,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -16806,7 +20524,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -16934,7 +20652,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -17062,7 +20780,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -17222,7 +20940,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -17414,7 +21132,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -17606,7 +21324,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -17798,7 +21516,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -17926,7 +21644,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -18054,7 +21772,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -18182,7 +21900,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -18342,7 +22060,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -18534,7 +22252,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -18726,7 +22444,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -18854,7 +22572,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -18982,7 +22700,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -19110,7 +22828,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -19238,7 +22956,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -19366,7 +23084,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -19494,7 +23212,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -19622,7 +23340,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -19750,7 +23468,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -19910,7 +23628,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -20102,7 +23820,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -20294,7 +24012,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -20422,7 +24140,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -20550,7 +24268,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -20678,7 +24396,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -20838,7 +24556,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21030,7 +24748,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21094,7 +24812,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21194,7 +24912,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21322,7 +25040,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21450,7 +25168,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21578,7 +25296,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21706,7 +25424,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21834,7 +25552,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -21994,7 +25712,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -22186,7 +25904,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -22378,7 +26096,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -22570,7 +26288,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -22762,7 +26480,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -22954,7 +26672,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -23146,7 +26864,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -23338,7 +27056,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -23530,7 +27248,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -23722,7 +27440,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -23850,7 +27568,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -23978,7 +27696,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -24106,7 +27824,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -24234,7 +27952,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -24362,7 +28080,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -24522,7 +28240,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -24714,7 +28432,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -24906,7 +28624,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -25098,7 +28816,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -25226,7 +28944,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -25354,7 +29072,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -25482,7 +29200,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -25642,7 +29360,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -25834,7 +29552,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -26026,7 +29744,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -26154,7 +29872,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -26282,7 +30000,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -26410,7 +30128,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -26538,7 +30256,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -26666,7 +30384,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -26794,7 +30512,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -26922,7 +30640,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -27050,7 +30768,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -27210,7 +30928,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -27402,7 +31120,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -27594,7 +31312,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -27722,7 +31440,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -27850,7 +31568,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -27978,7 +31696,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -28138,7 +31856,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -28330,7 +32048,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -28394,7 +32112,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -28494,7 +32212,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -28622,7 +32340,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -28750,7 +32468,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -28878,7 +32596,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -29006,7 +32724,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -29134,7 +32852,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -29294,7 +33012,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -29486,7 +33204,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -29678,7 +33396,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -29870,7 +33588,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -30062,7 +33780,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -30254,7 +33972,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -30446,7 +34164,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -30638,7 +34356,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -30830,7 +34548,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -31022,7 +34740,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -31150,7 +34868,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -31278,7 +34996,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -31406,7 +35124,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -31534,7 +35252,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -31662,7 +35380,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -31822,7 +35540,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -32014,7 +35732,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -32206,7 +35924,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -32398,7 +36116,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -32526,7 +36244,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -32654,7 +36372,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -32782,7 +36500,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -32942,7 +36660,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -33134,7 +36852,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -33326,7 +37044,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -33454,7 +37172,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -33582,7 +37300,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -33710,7 +37428,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -33838,7 +37556,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -33966,7 +37684,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -34094,7 +37812,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -34222,7 +37940,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -34350,7 +38068,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -34510,7 +38228,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -34702,7 +38420,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -34894,7 +38612,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -35022,7 +38740,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -35150,7 +38868,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -35278,7 +38996,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -35438,7 +39156,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -35630,7 +39348,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -35694,7 +39412,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -35796,7 +39514,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -35924,7 +39642,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -36052,7 +39770,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -36180,7 +39898,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -36308,7 +40026,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -36436,7 +40154,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -36596,7 +40314,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -36788,7 +40506,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -36980,7 +40698,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -37172,7 +40890,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -37364,7 +41082,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -37556,7 +41274,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -37748,7 +41466,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -37940,7 +41658,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -38132,7 +41850,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -38324,7 +42042,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -38452,7 +42170,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -38580,7 +42298,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -38708,7 +42426,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -38836,7 +42554,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -38964,7 +42682,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -39124,7 +42842,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -39316,7 +43034,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -39444,7 +43162,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -39572,7 +43290,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -39700,7 +43418,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -39860,7 +43578,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -40052,7 +43770,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -40244,7 +43962,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -40372,7 +44090,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -40500,7 +44218,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -40628,7 +44346,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -40756,7 +44474,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -40884,7 +44602,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -41012,7 +44730,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -41140,7 +44858,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -41268,7 +44986,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -41428,7 +45146,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -41620,7 +45338,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -41812,7 +45530,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -41940,7 +45658,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -42068,7 +45786,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -42196,7 +45914,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -42356,7 +46074,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -42548,7 +46266,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -42612,7 +46330,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -42712,7 +46430,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -42840,7 +46558,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -42968,7 +46686,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -43096,7 +46814,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -43224,7 +46942,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -43352,7 +47070,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -43512,7 +47230,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -43704,7 +47422,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -43896,7 +47614,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -44088,7 +47806,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -44280,7 +47998,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -44472,7 +48190,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -44664,7 +48382,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -44856,7 +48574,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -45048,7 +48766,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -45240,7 +48958,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -45368,7 +49086,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -45496,7 +49214,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -45624,7 +49342,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -45752,7 +49470,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -45880,7 +49598,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -46040,7 +49758,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -46232,7 +49950,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -46360,7 +50078,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -46488,7 +50206,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -46616,7 +50334,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -46776,7 +50494,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -46968,7 +50686,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -47160,7 +50878,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -47288,7 +51006,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -47416,7 +51134,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -47544,7 +51262,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -47672,7 +51390,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -47800,7 +51518,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -47928,7 +51646,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -48056,7 +51774,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -48184,7 +51902,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -48344,7 +52062,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -48536,7 +52254,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -48728,7 +52446,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -48856,7 +52574,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -48984,7 +52702,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -49112,7 +52830,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -49272,7 +52990,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -49464,7 +53182,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -49528,7 +53246,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -49628,7 +53346,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -49756,7 +53474,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -49884,7 +53602,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -50012,7 +53730,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -50140,7 +53858,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -50268,7 +53986,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -50428,7 +54146,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -50620,7 +54338,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -50812,7 +54530,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -51004,7 +54722,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -51196,7 +54914,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -51388,7 +55106,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -51580,7 +55298,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -51772,7 +55490,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -51964,7 +55682,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -52156,7 +55874,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -52284,7 +56002,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -52412,7 +56130,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -52540,7 +56258,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -52668,7 +56386,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -52796,7 +56514,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -52956,7 +56674,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -53148,7 +56866,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -53340,7 +57058,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -53532,7 +57250,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -53660,7 +57378,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -53788,7 +57506,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -53916,7 +57634,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -54076,7 +57794,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -54268,7 +57986,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -54460,7 +58178,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -54588,7 +58306,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -54716,7 +58434,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -54844,7 +58562,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -54972,7 +58690,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -55100,7 +58818,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -55228,7 +58946,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -55356,7 +59074,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -55484,7 +59202,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -55644,7 +59362,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -55836,7 +59554,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -56028,7 +59746,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -56156,7 +59874,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -56284,7 +60002,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -56412,7 +60130,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -56572,7 +60290,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -56764,7 +60482,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -56828,7 +60546,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -56928,7 +60646,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -57056,7 +60774,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -57184,7 +60902,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -57312,7 +61030,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -57440,7 +61158,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -57568,7 +61286,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -57728,7 +61446,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -57920,7 +61638,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -58112,7 +61830,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -58304,7 +62022,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -58496,7 +62214,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -58688,7 +62406,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -58880,7 +62598,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -59072,7 +62790,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -59264,7 +62982,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -59456,7 +63174,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -59584,7 +63302,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -59712,7 +63430,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -59840,7 +63558,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -59968,7 +63686,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -60096,7 +63814,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -60256,7 +63974,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -60448,7 +64166,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -60640,7 +64358,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -60832,7 +64550,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -60960,7 +64678,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -61088,7 +64806,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -61216,7 +64934,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -61376,7 +65094,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -61568,7 +65286,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -61760,7 +65478,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -61888,7 +65606,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -62016,7 +65734,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -62144,7 +65862,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -62272,7 +65990,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -62400,7 +66118,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -62528,7 +66246,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -62656,7 +66374,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -62784,7 +66502,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -62944,7 +66662,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -63136,7 +66854,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -63328,7 +67046,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -63456,7 +67174,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -63584,7 +67302,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -63712,7 +67430,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -63872,7 +67590,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -64064,7 +67782,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -64128,7 +67846,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -64228,7 +67946,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -64356,7 +68074,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -64484,7 +68202,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -64612,7 +68330,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -64740,7 +68458,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -64868,7 +68586,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -65028,7 +68746,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -65220,7 +68938,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -65412,7 +69130,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -65604,7 +69322,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -65796,7 +69514,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -65988,7 +69706,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -66180,7 +69898,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -66372,7 +70090,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -66564,7 +70282,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -66756,7 +70474,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -66884,7 +70602,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -67012,7 +70730,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -67140,7 +70858,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -67268,7 +70986,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -67396,7 +71114,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -67556,7 +71274,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -67748,7 +71466,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -67940,7 +71658,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -68132,7 +71850,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -68260,7 +71978,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -68388,7 +72106,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -68516,7 +72234,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -68676,7 +72394,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -68868,7 +72586,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -69060,7 +72778,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -69188,7 +72906,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -69316,7 +73034,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -69444,7 +73162,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -69572,7 +73290,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -69700,7 +73418,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -69828,7 +73546,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -69956,7 +73674,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -70084,7 +73802,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -70244,7 +73962,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -70436,7 +74154,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -70628,7 +74346,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -70756,7 +74474,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -70884,7 +74602,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -71012,7 +74730,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -71172,7 +74890,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -71364,7 +75082,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -71428,7 +75146,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -71528,7 +75246,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -71656,7 +75374,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -71784,7 +75502,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -71912,7 +75630,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -72040,7 +75758,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -72168,7 +75886,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -72328,7 +76046,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -72520,7 +76238,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -72712,7 +76430,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -72904,7 +76622,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -73096,7 +76814,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -73288,7 +77006,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -73480,7 +77198,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -73672,7 +77390,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -73864,7 +77582,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -74056,7 +77774,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -74184,7 +77902,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -74312,7 +78030,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -74440,7 +78158,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -74568,7 +78286,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -74696,7 +78414,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -74856,7 +78574,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -75048,7 +78766,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -75240,7 +78958,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -75432,7 +79150,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -75560,7 +79278,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -75688,7 +79406,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -75816,7 +79534,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -75976,7 +79694,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -76168,7 +79886,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -76360,7 +80078,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -76488,7 +80206,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -76616,7 +80334,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -76744,7 +80462,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -76872,7 +80590,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -77000,7 +80718,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -77128,7 +80846,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -77256,7 +80974,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -77384,7 +81102,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -77544,7 +81262,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -77736,7 +81454,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -77928,7 +81646,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -78056,7 +81774,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -78184,7 +81902,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -78312,7 +82030,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -78472,7 +82190,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -78664,7 +82382,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -78728,7 +82446,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -124432,7 +128150,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -124560,7 +128278,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -124816,7 +128534,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -124912,7 +128630,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -125008,7 +128726,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -125136,7 +128854,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -125296,7 +129014,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -125488,7 +129206,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -125680,7 +129398,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -125840,7 +129558,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -126000,7 +129718,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -126096,7 +129814,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -126320,7 +130038,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -126416,7 +130134,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -126544,7 +130262,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -126704,7 +130422,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -126864,7 +130582,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -127024,7 +130742,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -127216,7 +130934,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -127376,7 +131094,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -127536,7 +131254,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -127664,7 +131382,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -127920,7 +131638,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -128016,7 +131734,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -128112,7 +131830,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -128240,7 +131958,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -128400,7 +132118,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -128592,7 +132310,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -128752,7 +132470,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -128848,7 +132566,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -129072,7 +132790,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -129168,7 +132886,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -129296,7 +133014,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -129456,7 +133174,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -129552,7 +133270,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -129776,7 +133494,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -129872,7 +133590,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -130000,7 +133718,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -130164,7 +133882,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -130292,7 +134010,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -130548,7 +134266,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -130644,7 +134362,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -130740,7 +134458,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -130868,7 +134586,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -131028,7 +134746,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -131220,7 +134938,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -131412,7 +135130,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -131572,7 +135290,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -131732,7 +135450,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -131828,7 +135546,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -132052,7 +135770,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -132148,7 +135866,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -132276,7 +135994,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -132436,7 +136154,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -132596,7 +136314,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -132756,7 +136474,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -132948,7 +136666,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -133108,7 +136826,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -133268,7 +136986,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -133396,7 +137114,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -133652,7 +137370,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -133748,7 +137466,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -133844,7 +137562,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -133972,7 +137690,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -134132,7 +137850,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -134324,7 +138042,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -134484,7 +138202,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -134580,7 +138298,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -134804,7 +138522,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -134900,7 +138618,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -135028,7 +138746,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -135188,7 +138906,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -135284,7 +139002,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -135508,7 +139226,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -135604,7 +139322,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], @@ -135732,7 +139450,7 @@ { "setting_name": "Hypersmooth", "setting_id": 135, - "option_name": "On", + "option_name": "Low", "option_id": 1 } ], diff --git a/docs/specs/capabilities.xlsx b/docs/specs/capabilities.xlsx index 46c3df09..8f72d835 100644 --- a/docs/specs/capabilities.xlsx +++ b/docs/specs/capabilities.xlsx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7559b68b2a6429553f2a202b9bc32a700cb2f7f9b47a0e620acace6d3bc0e728 -size 100218 +oid sha256:e945fd0a85ea62dbe5ad37f7872a2af63cd958e58937f56d6feca688fcad8126 +size 103251 diff --git a/docs/specs/http_versions/http_2_0.md b/docs/specs/http_versions/http_2_0.md index 0ff2ebfd..2534074a 100644 --- a/docs/specs/http_versions/http_2_0.md +++ b/docs/specs/http_versions/http_2_0.md @@ -43,6 +43,12 @@ Below is a table of cameras that support GoPro's public HTTP API: Marketing Name Minimal Firmware Version + + 62 + H23.01 + HERO12 Black + v01.10.00 + 60 H22.03 @@ -279,6 +285,7 @@ Below is a table of commands that can be sent to the camera and how to send them Description HTTP Method Endpoint + HERO12 Black HERO11 Black Mini HERO11 Black HERO10 Black @@ -291,6 +298,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/camera/analytics/set_client_info + \>= v01.30.00 \>= v01.70.00 @@ -303,6 +311,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Digital Zoom @@ -313,6 +322,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Get Date/Time @@ -321,6 +331,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/camera/get_date_time + \>= v01.30.00 \>= v01.70.00 @@ -333,6 +344,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: GPMF @@ -343,6 +355,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: GPMF @@ -353,6 +366,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: HiLight (Add) @@ -361,6 +375,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/media/hilight/file?path=100GOPRO/XXX.JPG + \>= v01.30.00 \>= v01.70.00 @@ -371,6 +386,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/media/hilight/file?path=100GOPRO/XXX.MP4&ms=2500 + \>= v01.30.00 \>= v01.70.00 @@ -381,6 +397,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/media/hilight/remove?path=100GOPRO/XXX.JPG + \>= v01.30.00 \>= v01.70.00 @@ -391,6 +408,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/media/hilight/remove?path=100GOPRO/XXX.MP4&ms=2500 + \>= v01.30.00 \>= v01.70.00 @@ -401,6 +419,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/media/hilight/moment + \>= v01.30.00 @@ -413,6 +432,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Info @@ -423,6 +443,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: List @@ -433,6 +454,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Screennail @@ -443,6 +465,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Screennail @@ -453,6 +476,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Telemetry @@ -463,6 +487,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Telemetry @@ -473,6 +498,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Thumbnail @@ -483,6 +509,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Thumbnail @@ -493,6 +520,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Turbo Transfer @@ -503,6 +531,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Media: Turbo Transfer @@ -513,6 +542,7 @@ Below is a table of commands that can be sent to the camera and how to send them + OTA Update @@ -523,6 +553,7 @@ Below is a table of commands that can be sent to the camera and how to send them + OTA Update @@ -533,6 +564,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Open GoPro @@ -543,6 +575,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Presets: Get Status @@ -553,6 +586,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Presets: Load @@ -563,6 +597,7 @@ Below is a table of commands that can be sent to the camera and how to send them + Presets: Load Group @@ -573,12 +608,14 @@ Below is a table of commands that can be sent to the camera and how to send them + Presets: Load Group Photo GET /gopro/camera/presets/set_group?id=1001 + @@ -589,6 +626,7 @@ Below is a table of commands that can be sent to the camera and how to send them Timelapse GET /gopro/camera/presets/set_group?id=1002 + @@ -601,6 +639,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/camera/control/set_ui_controller?p=0 + \>= v01.20.00 @@ -611,6 +650,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/camera/control/set_ui_controller?p=2 + \>= v01.20.00 @@ -621,6 +661,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/camera/set_date_time?date=2023_1_31&time=3_4_5 + \>= v01.30.00 \>= v01.70.00 @@ -631,6 +672,7 @@ Below is a table of commands that can be sent to the camera and how to send them /gopro/camera/set_date_time?date=2023_1_31&time=3_4_5&tzone=-120&dst=1 + @@ -642,6 +684,7 @@ Below is a table of commands that can be sent to the camera and how to send them + @@ -652,9 +695,21 @@ Below is a table of commands that can be sent to the camera and how to send them + + Simple OTA Update + Simple ota update with file: update.zip + POST + /gp/gpUpdate (plus data) + + + + + + + Soft Update Soft update: show canceled/failed ui on the camera GET @@ -663,8 +718,9 @@ Below is a table of commands that can be sent to the camera and how to send them + - + Soft Update Soft update: delete cached update files GET @@ -673,8 +729,9 @@ Below is a table of commands that can be sent to the camera and how to send them + - + Soft Update Soft update: get current update state GET @@ -683,8 +740,9 @@ Below is a table of commands that can be sent to the camera and how to send them + - + Soft Update Soft update: display update ui on camera GET @@ -693,8 +751,9 @@ Below is a table of commands that can be sent to the camera and how to send them + - + Soft Update Soft update: initiate firmware update GET @@ -703,8 +762,9 @@ Below is a table of commands that can be sent to the camera and how to send them + - + Stream: Start Start preview stream GET @@ -713,8 +773,9 @@ Below is a table of commands that can be sent to the camera and how to send them + - + Stream: Stop Stop preview stream GET @@ -723,104 +784,115 @@ Below is a table of commands that can be sent to the camera and how to send them + - + Webcam: Exit Exit webcam mode GET /gopro/webcam/exit + - + Webcam: Preview Start preview stream GET /gopro/webcam/preview + - + Webcam: Start Start webcam GET /gopro/webcam/start + \>= v01.40.00 - + Webcam: Start Start webcam GET /gopro/webcam/start?port=12345 + \>= v02.01.00 - + Webcam: Start Start webcam (res: resolution_1080, fov: wide) GET /gopro/webcam/start?res=12&fov=0 + - + Webcam: Status Get webcam status GET /gopro/webcam/status + - + Webcam: Stop Stop webcam GET /gopro/webcam/stop + - + Webcam: Version Get webcam api version GET /gopro/webcam/version + - + Wired USB Control Disable wired usb control GET /gopro/camera/control/wired_usb?p=0 + \>= v01.30.00 - + Wired USB Control Enable wired usb control GET /gopro/camera/control/wired_usb?p=1 + \>= v01.30.00 @@ -853,6 +925,7 @@ Below is a table of setting options detailing how to set every option supported Option HTTP Method Endpoint + HERO12 Black HERO11 Black Mini HERO11 Black HERO10 Black @@ -864,6 +937,7 @@ Below is a table of setting options detailing how to set every option supported Set video resolution (id: 2) to 4k (id: 1) GET /gopro/camera/setting?setting=2&option=1 + @@ -875,6 +949,7 @@ Below is a table of setting options detailing how to set every option supported Set video resolution (id: 2) to 2.7k (id: 4) GET /gopro/camera/setting?setting=2&option=4 + @@ -886,6 +961,7 @@ Below is a table of setting options detailing how to set every option supported Set video resolution (id: 2) to 2.7k 4:3 (id: 6) GET /gopro/camera/setting?setting=2&option=6 + @@ -900,6 +976,7 @@ Below is a table of setting options detailing how to set every option supported + @@ -908,6 +985,7 @@ Below is a table of setting options detailing how to set every option supported Set video resolution (id: 2) to 1080 (id: 9) GET /gopro/camera/setting?setting=2&option=9 + @@ -919,6 +997,7 @@ Below is a table of setting options detailing how to set every option supported Set video resolution (id: 2) to 4k 4:3 (id: 18) GET /gopro/camera/setting?setting=2&option=18 + @@ -933,6 +1012,7 @@ Below is a table of setting options detailing how to set every option supported + @@ -943,6 +1023,7 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=2&option=25 + @@ -954,6 +1035,7 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=2&option=26 + @@ -963,6 +1045,7 @@ Below is a table of setting options detailing how to set every option supported Set video resolution (id: 2) to 5.3k 4:3 (id: 27) GET /gopro/camera/setting?setting=2&option=27 + @@ -976,6 +1059,31 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=2&option=28 + + + + + + 2 + Resolution + Set video resolution (id: 2) to 4k 9:16 (id: 29) + GET + /gopro/camera/setting?setting=2&option=29 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 1080 9:16 (id: 30) + GET + /gopro/camera/setting?setting=2&option=30 + + + @@ -985,10 +1093,83 @@ Below is a table of setting options detailing how to set every option supported Set video resolution (id: 2) to 5.3k (id: 100) GET /gopro/camera/setting?setting=2&option=100 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 5.3k 16:9 (id: 101) + GET + /gopro/camera/setting?setting=2&option=101 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 4k 16:9 (id: 102) + GET + /gopro/camera/setting?setting=2&option=102 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 4k 4:3 (id: 103) + GET + /gopro/camera/setting?setting=2&option=103 + + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 2.7k 16:9 (id: 104) + GET + /gopro/camera/setting?setting=2&option=104 + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 2.7k 4:3 (id: 105) + GET + /gopro/camera/setting?setting=2&option=105 + + + + + + + 2 + Resolution + Set video resolution (id: 2) to 1080 16:9 (id: 106) + GET + /gopro/camera/setting?setting=2&option=106 + + + 3 @@ -1000,6 +1181,7 @@ Below is a table of setting options detailing how to set every option supported + 3 @@ -1011,6 +1193,7 @@ Below is a table of setting options detailing how to set every option supported + 3 @@ -1022,6 +1205,7 @@ Below is a table of setting options detailing how to set every option supported + 3 @@ -1033,6 +1217,7 @@ Below is a table of setting options detailing how to set every option supported + 3 @@ -1044,6 +1229,7 @@ Below is a table of setting options detailing how to set every option supported + 3 @@ -1055,6 +1241,7 @@ Below is a table of setting options detailing how to set every option supported + 3 @@ -1066,6 +1253,7 @@ Below is a table of setting options detailing how to set every option supported + 3 @@ -1077,6 +1265,7 @@ Below is a table of setting options detailing how to set every option supported + 3 @@ -1088,6 +1277,7 @@ Below is a table of setting options detailing how to set every option supported + 43 @@ -1099,6 +1289,7 @@ Below is a table of setting options detailing how to set every option supported + 43 @@ -1110,6 +1301,7 @@ Below is a table of setting options detailing how to set every option supported + 43 @@ -1121,6 +1313,7 @@ Below is a table of setting options detailing how to set every option supported + 43 @@ -1132,6 +1325,7 @@ Below is a table of setting options detailing how to set every option supported + 59 @@ -1139,6 +1333,7 @@ Below is a table of setting options detailing how to set every option supported Set auto power down (id: 59) to never (id: 0) GET /gopro/camera/setting?setting=59&option=0 + \>= v02.10.00 @@ -1150,6 +1345,7 @@ Below is a table of setting options detailing how to set every option supported Set auto power down (id: 59) to 1 min (id: 1) GET /gopro/camera/setting?setting=59&option=1 + \>= v02.10.00 \>= v02.01.00 @@ -1161,6 +1357,7 @@ Below is a table of setting options detailing how to set every option supported Set auto power down (id: 59) to 5 min (id: 4) GET /gopro/camera/setting?setting=59&option=4 + \>= v02.10.00 @@ -1172,6 +1369,7 @@ Below is a table of setting options detailing how to set every option supported Set auto power down (id: 59) to 15 min (id: 6) GET /gopro/camera/setting?setting=59&option=6 + @@ -1183,6 +1381,7 @@ Below is a table of setting options detailing how to set every option supported Set auto power down (id: 59) to 30 min (id: 7) GET /gopro/camera/setting?setting=59&option=7 + @@ -1194,6 +1393,7 @@ Below is a table of setting options detailing how to set every option supported Set auto power down (id: 59) to 8 seconds (id: 11) GET /gopro/camera/setting?setting=59&option=11 + \>= v02.10.00 @@ -1205,12 +1405,61 @@ Below is a table of setting options detailing how to set every option supported Set auto power down (id: 59) to 30 seconds (id: 12) GET /gopro/camera/setting?setting=59&option=12 + \>= v02.10.00 + 108 + Aspect Ratio + Set video aspect ratio (id: 108) to 4:3 (id: 0) + GET + /gopro/camera/setting?setting=108&option=0 + + + + + + + + 108 + Aspect Ratio + Set video aspect ratio (id: 108) to 16:9 (id: 1) + GET + /gopro/camera/setting?setting=108&option=1 + + + + + + + + 108 + Aspect Ratio + Set video aspect ratio (id: 108) to 8:7 (id: 3) + GET + /gopro/camera/setting?setting=108&option=3 + + + + + + + + 108 + Aspect Ratio + Set video aspect ratio (id: 108) to 9:16 (id: 4) + GET + /gopro/camera/setting?setting=108&option=4 + + + + + + + 121 Video Digital Lenses Set video digital lenses (id: 121) to wide (id: 0) @@ -1220,8 +1469,9 @@ Below is a table of setting options detailing how to set every option supported + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to narrow (id: 2) @@ -1229,10 +1479,11 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=121&option=2 + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to superview (id: 3) @@ -1242,8 +1493,9 @@ Below is a table of setting options detailing how to set every option supported + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to linear (id: 4) @@ -1253,19 +1505,21 @@ Below is a table of setting options detailing how to set every option supported + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to max superview (id: 7) GET /gopro/camera/setting?setting=121&option=7 + \>= v02.00.00 - + 121 Video Digital Lenses Set video digital lenses (id: 121) to linear + horizon leveling (id: 8) @@ -1275,8 +1529,9 @@ Below is a table of setting options detailing how to set every option supported + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to hyperview (id: 9) @@ -1284,10 +1539,11 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=121&option=9 + - + 121 Video Digital Lenses Set video digital lenses (id: 121) to linear + horizon lock (id: 10) @@ -1295,10 +1551,23 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=121&option=10 + + 121 + Video Digital Lenses + Set video digital lenses (id: 121) to max hyperview (id: 11) + GET + /gopro/camera/setting?setting=121&option=11 + + + + + + + 122 Photo Digital Lenses Set photo digital lenses (id: 122) to narrow (id: 19) @@ -1306,43 +1575,47 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=122&option=19 + - + 122 Photo Digital Lenses Set photo digital lenses (id: 122) to max superview (id: 100) GET /gopro/camera/setting?setting=122&option=100 + - + 122 Photo Digital Lenses Set photo digital lenses (id: 122) to wide (id: 101) GET /gopro/camera/setting?setting=122&option=101 + - + 122 Photo Digital Lenses Set photo digital lenses (id: 122) to linear (id: 102) GET /gopro/camera/setting?setting=122&option=102 + - + 123 Time Lapse Digital Lenses Set time lapse digital lenses (id: 123) to narrow (id: 19) @@ -1350,87 +1623,95 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=123&option=19 + - + 123 Time Lapse Digital Lenses Set time lapse digital lenses (id: 123) to max superview (id: 100) GET /gopro/camera/setting?setting=123&option=100 + - + 123 Time Lapse Digital Lenses Set time lapse digital lenses (id: 123) to wide (id: 101) GET /gopro/camera/setting?setting=123&option=101 + - + 123 Time Lapse Digital Lenses Set time lapse digital lenses (id: 123) to linear (id: 102) GET /gopro/camera/setting?setting=123&option=102 + - + 128 Media Format Set media format (id: 128) to time lapse video (id: 13) GET /gopro/camera/setting?setting=128&option=13 + - + 128 Media Format Set media format (id: 128) to time lapse photo (id: 20) GET /gopro/camera/setting?setting=128&option=20 + - + 128 Media Format Set media format (id: 128) to night lapse photo (id: 21) GET /gopro/camera/setting?setting=128&option=21 + - + 128 Media Format Set media format (id: 128) to night lapse video (id: 26) GET /gopro/camera/setting?setting=128&option=26 + - + 134 Anti-Flicker Set setup anti flicker (id: 134) to 60hz (id: 2) @@ -1440,8 +1721,9 @@ Below is a table of setting options detailing how to set every option supported + - + 134 Anti-Flicker Set setup anti flicker (id: 134) to 50hz (id: 3) @@ -1451,8 +1733,9 @@ Below is a table of setting options detailing how to set every option supported + - + 135 Hypersmooth Set video hypersmooth (id: 135) to off (id: 0) @@ -1462,19 +1745,21 @@ Below is a table of setting options detailing how to set every option supported + - + 135 Hypersmooth - Set video hypersmooth (id: 135) to on (id: 1) + Set video hypersmooth (id: 135) to low (id: 1) GET /gopro/camera/setting?setting=135&option=1 + - + 135 Hypersmooth Set video hypersmooth (id: 135) to high (id: 2) @@ -1482,21 +1767,23 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=135&option=2 + - + 135 Hypersmooth Set video hypersmooth (id: 135) to boost (id: 3) GET /gopro/camera/setting?setting=135&option=3 + - + 135 Hypersmooth Set video hypersmooth (id: 135) to auto boost (id: 4) @@ -1504,10 +1791,11 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=135&option=4 + - + 135 Hypersmooth Set video hypersmooth (id: 135) to standard (id: 100) @@ -1515,819 +1803,2197 @@ Below is a table of setting options detailing how to set every option supported /gopro/camera/setting?setting=135&option=100 + - + 150 Horizon Leveling Set video horizon levelling (id: 150) to off (id: 0) GET /gopro/camera/setting?setting=150&option=0 + \>= v02.00.00 - + 150 Horizon Leveling Set video horizon levelling (id: 150) to on (id: 1) GET /gopro/camera/setting?setting=150&option=1 + \>= v02.00.00 - + 150 Horizon Leveling Set video horizon levelling (id: 150) to locked (id: 2) GET /gopro/camera/setting?setting=150&option=2 + - + 151 Horizon Leveling Set photo horizon levelling (id: 151) to off (id: 0) GET /gopro/camera/setting?setting=151&option=0 + - + 151 Horizon Leveling Set photo horizon levelling (id: 151) to locked (id: 2) GET /gopro/camera/setting?setting=151&option=2 + - + 162 Max Lens Set max lens (id: 162) to off (id: 0) GET /gopro/camera/setting?setting=162&option=0 + \>= v01.20.00 - + 162 Max Lens Set max lens (id: 162) to on (id: 1) GET /gopro/camera/setting?setting=162&option=1 + \>= v01.20.00 - + 167 Hindsight* Set hindsight (id: 167) to 15 seconds (id: 2) GET /gopro/camera/setting?setting=167&option=2 + - + 167 Hindsight* Set hindsight (id: 167) to 30 seconds (id: 3) GET /gopro/camera/setting?setting=167&option=3 + - + 167 Hindsight* Set hindsight (id: 167) to off (id: 4) GET /gopro/camera/setting?setting=167&option=4 + - - 173 - Video Performance Mode - Set video performance mode (id: 173) to maximum video performance (id: 0) + + 171 + Interval + Set photo single interval (id: 171) to off (id: 0) GET - /gopro/camera/setting?setting=173&option=0 + /gopro/camera/setting?setting=171&option=0 + + - \>= v01.16.00 - - 173 - Video Performance Mode - Set video performance mode (id: 173) to extended battery (id: 1) + + 171 + Interval + Set photo single interval (id: 171) to 0.5s (id: 2) GET - /gopro/camera/setting?setting=173&option=1 + /gopro/camera/setting?setting=171&option=2 + + - \>= v01.16.00 - - 173 - Video Performance Mode - Set video performance mode (id: 173) to tripod / stationary video (id: 2) + + 171 + Interval + Set photo single interval (id: 171) to 1s (id: 3) GET - /gopro/camera/setting?setting=173&option=2 + /gopro/camera/setting?setting=171&option=3 + + - \>= v01.16.00 - 175 - Controls - Set controls (id: 175) to easy (id: 0) + 171 + Interval + Set photo single interval (id: 171) to 2s (id: 4) GET - /gopro/camera/setting?setting=175&option=0 - + /gopro/camera/setting?setting=171&option=4 + + - 175 - Controls - Set controls (id: 175) to pro (id: 1) + 171 + Interval + Set photo single interval (id: 171) to 5s (id: 5) GET - /gopro/camera/setting?setting=175&option=1 - + /gopro/camera/setting?setting=171&option=5 + + - - 176 - Speed - Set speed (id: 176) to 8x ultra slo-mo (id: 0) + + 171 + Interval + Set photo single interval (id: 171) to 10s (id: 6) GET - /gopro/camera/setting?setting=176&option=0 - + /gopro/camera/setting?setting=171&option=6 + + - - 176 - Speed - Set speed (id: 176) to 4x super slo-mo (id: 1) + + 171 + Interval + Set photo single interval (id: 171) to 30s (id: 7) GET - /gopro/camera/setting?setting=176&option=1 - + /gopro/camera/setting?setting=171&option=7 + + - - 176 - Speed - Set speed (id: 176) to 2x slo-mo (id: 2) + + 171 + Interval + Set photo single interval (id: 171) to 60s (id: 8) GET - /gopro/camera/setting?setting=176&option=2 - + /gopro/camera/setting?setting=171&option=8 + + - - 176 - Speed - Set speed (id: 176) to 1x (low light) (id: 3) + + 171 + Interval + Set photo single interval (id: 171) to 120s (id: 9) GET - /gopro/camera/setting?setting=176&option=3 - + /gopro/camera/setting?setting=171&option=9 + + - - 176 - Speed - Set speed (id: 176) to 4x super slo-mo (ext. batt) (id: 4) + + 171 + Interval + Set photo single interval (id: 171) to 3s (id: 10) GET - /gopro/camera/setting?setting=176&option=4 - + /gopro/camera/setting?setting=171&option=10 + + - 176 - Speed - Set speed (id: 176) to 2x slo-mo (ext. batt) (id: 5) + 172 + Duration + Set photo interval duration (id: 172) to off (id: 0) GET - /gopro/camera/setting?setting=176&option=5 - + /gopro/camera/setting?setting=172&option=0 + + - 176 - Speed - Set speed (id: 176) to 1x (ext. batt, low light) (id: 6) + 172 + Duration + Set photo interval duration (id: 172) to 15 seconds (id: 1) GET - /gopro/camera/setting?setting=176&option=6 - + /gopro/camera/setting?setting=172&option=1 + + - 176 - Speed - Set speed (id: 176) to 8x ultra slo-mo (50hz) (id: 7) + 172 + Duration + Set photo interval duration (id: 172) to 30 seconds (id: 2) GET - /gopro/camera/setting?setting=176&option=7 - + /gopro/camera/setting?setting=172&option=2 + + - 176 - Speed - Set speed (id: 176) to 4x super slo-mo (50hz) (id: 8) + 172 + Duration + Set photo interval duration (id: 172) to 1 minute (id: 3) GET - /gopro/camera/setting?setting=176&option=8 - + /gopro/camera/setting?setting=172&option=3 + + - 176 - Speed - Set speed (id: 176) to 2x slo-mo (50hz) (id: 9) + 172 + Duration + Set photo interval duration (id: 172) to 5 minutes (id: 4) GET - /gopro/camera/setting?setting=176&option=9 - + /gopro/camera/setting?setting=172&option=4 + + - 176 - Speed - Set speed (id: 176) to 1x (low light, 50hz) (id: 10) + 172 + Duration + Set photo interval duration (id: 172) to 15 minutes (id: 5) GET - /gopro/camera/setting?setting=176&option=10 - + /gopro/camera/setting?setting=172&option=5 + + - 176 - Speed - Set speed (id: 176) to 4x super slo-mo (ext. batt, 50hz) (id: 11) + 172 + Duration + Set photo interval duration (id: 172) to 30 minutes (id: 6) GET - /gopro/camera/setting?setting=176&option=11 - + /gopro/camera/setting?setting=172&option=6 + + - 176 - Speed - Set speed (id: 176) to 2x slo-mo (ext. batt, 50hz) (id: 12) + 172 + Duration + Set photo interval duration (id: 172) to 1 hour (id: 7) GET - /gopro/camera/setting?setting=176&option=12 - + /gopro/camera/setting?setting=172&option=7 + + - 176 - Speed - Set speed (id: 176) to 1x (ext. batt, low light, 50hz) (id: 13) + 172 + Duration + Set photo interval duration (id: 172) to 2 hours (id: 8) GET - /gopro/camera/setting?setting=176&option=13 - + /gopro/camera/setting?setting=172&option=8 - - - 176 - Speed - Set speed (id: 176) to 8x ultra slo-mo (ext. batt) (id: 14) - GET - /gopro/camera/setting?setting=176&option=14 - - \>= v02.01.00 - 176 - Speed - Set speed (id: 176) to 8x ultra slo-mo (ext. batt, 50hz) (id: 15) + 172 + Duration + Set photo interval duration (id: 172) to 3 hours (id: 9) GET - /gopro/camera/setting?setting=176&option=15 + /gopro/camera/setting?setting=172&option=9 + + - \>= v02.01.00 - - 176 - Speed - Set speed (id: 176) to 8x ultra slo-mo (long. batt) (id: 16) + + 173 + Video Performance Mode + Set video performance mode (id: 173) to maximum video performance (id: 0) GET - /gopro/camera/setting?setting=176&option=16 + /gopro/camera/setting?setting=173&option=0 + - \>= v02.01.00 + \>= v01.16.00 - - 176 - Speed - Set speed (id: 176) to 4x super slo-mo (long. batt) (id: 17) + + 173 + Video Performance Mode + Set video performance mode (id: 173) to extended battery (id: 1) GET - /gopro/camera/setting?setting=176&option=17 + /gopro/camera/setting?setting=173&option=1 - \>= v02.01.00 + \>= v01.16.00 + - - 176 - Speed - Set speed (id: 176) to 2x slo-mo (long. batt) (id: 18) + + 173 + Video Performance Mode + Set video performance mode (id: 173) to tripod / stationary video (id: 2) GET - /gopro/camera/setting?setting=176&option=18 + /gopro/camera/setting?setting=173&option=2 + - \>= v02.01.00 + \>= v01.16.00 - 176 - Speed - Set speed (id: 176) to 1x (long. batt, low light) (id: 19) + 175 + Controls + Set controls (id: 175) to easy (id: 0) GET - /gopro/camera/setting?setting=176&option=19 - - \>= v02.01.00 + /gopro/camera/setting?setting=175&option=0 + + + - 176 - Speed - Set speed (id: 176) to 8x ultra slo-mo (long. batt, 50hz) (id: 20) + 175 + Controls + Set controls (id: 175) to pro (id: 1) GET - /gopro/camera/setting?setting=176&option=20 - - \>= v02.01.00 + /gopro/camera/setting?setting=175&option=1 + + + - + 176 Speed - Set speed (id: 176) to 4x super slo-mo (long. batt, 50hz) (id: 21) + Set speed (id: 176) to 8x ultra slo-mo (id: 0) GET - /gopro/camera/setting?setting=176&option=21 + /gopro/camera/setting?setting=176&option=0 + - \>= v02.01.00 + - + 176 Speed - Set speed (id: 176) to 2x slo-mo (long. batt, 50hz) (id: 22) + Set speed (id: 176) to 4x super slo-mo (id: 1) GET - /gopro/camera/setting?setting=176&option=22 + /gopro/camera/setting?setting=176&option=1 + - \>= v02.01.00 + - + 176 Speed - Set speed (id: 176) to 1x (long. batt, low light, 50hz) (id: 23) + Set speed (id: 176) to 2x slo-mo (id: 2) GET - /gopro/camera/setting?setting=176&option=23 + /gopro/camera/setting?setting=176&option=2 + - \>= v02.01.00 + - + 176 Speed - Set speed (id: 176) to 2x slo-mo (4k) (id: 24) + Set speed (id: 176) to 1x (low light) (id: 3) GET - /gopro/camera/setting?setting=176&option=24 + /gopro/camera/setting?setting=176&option=3 + - \>= v02.01.00 + - + 176 Speed - Set speed (id: 176) to 4x super slo-mo (2.7k) (id: 25) + Set speed (id: 176) to 4x super slo-mo (ext. batt) (id: 4) GET - /gopro/camera/setting?setting=176&option=25 + /gopro/camera/setting?setting=176&option=4 - \>= v02.01.00 + + - + 176 Speed - Set speed (id: 176) to 2x slo-mo (4k, 50hz) (id: 26) + Set speed (id: 176) to 2x slo-mo (ext. batt) (id: 5) GET - /gopro/camera/setting?setting=176&option=26 + /gopro/camera/setting?setting=176&option=5 - \>= v02.01.00 + + - + 176 Speed - Set speed (id: 176) to 4x super slo-mo (2.7k, 50hz) (id: 27) + Set speed (id: 176) to 1x (ext. batt, low light) (id: 6) GET - /gopro/camera/setting?setting=176&option=27 - - \>= v02.01.00 - + /gopro/camera/setting?setting=176&option=6 - - - 177 - Enable Night Photo - Set enable night photo (id: 177) to off (id: 0) - GET - /gopro/camera/setting?setting=177&option=0 - 177 - Enable Night Photo - Set enable night photo (id: 177) to on (id: 1) + 176 + Speed + Set speed (id: 176) to 8x ultra slo-mo (50hz) (id: 7) GET - /gopro/camera/setting?setting=177&option=1 - + /gopro/camera/setting?setting=176&option=7 - - - - 178 - Wireless Band - Set wireless band (id: 178) to 2.4ghz (id: 0) - GET - /gopro/camera/setting?setting=178&option=0 - - - 178 - Wireless Band - Set wireless band (id: 178) to 5ghz (id: 1) + + 176 + Speed + Set speed (id: 176) to 4x super slo-mo (50hz) (id: 8) GET - /gopro/camera/setting?setting=178&option=1 + /gopro/camera/setting?setting=176&option=8 + - 179 - Trail Length - Set trail length (id: 179) to short (id: 1) + 176 + Speed + Set speed (id: 176) to 2x slo-mo (50hz) (id: 9) GET - /gopro/camera/setting?setting=179&option=1 + /gopro/camera/setting?setting=176&option=9 + - 179 - Trail Length - Set trail length (id: 179) to long (id: 2) + 176 + Speed + Set speed (id: 176) to 1x (low light, 50hz) (id: 10) GET - /gopro/camera/setting?setting=179&option=2 + /gopro/camera/setting?setting=176&option=10 + - 179 - Trail Length - Set trail length (id: 179) to max (id: 3) + 176 + Speed + Set speed (id: 176) to 4x super slo-mo (ext. batt, 50hz) (id: 11) GET - /gopro/camera/setting?setting=179&option=3 - + /gopro/camera/setting?setting=176&option=11 + + - - 180 - Video Mode - Set video mode (id: 180) to highest quality (id: 0) + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (ext. batt, 50hz) (id: 12) GET - /gopro/camera/setting?setting=180&option=0 + /gopro/camera/setting?setting=176&option=12 + - - 180 - Video Mode - Set video mode (id: 180) to extended battery (id: 1) + + 176 + Speed + Set speed (id: 176) to 1x (ext. batt, low light, 50hz) (id: 13) GET - /gopro/camera/setting?setting=180&option=1 + /gopro/camera/setting?setting=176&option=13 + - - 180 - Video Mode - Set video mode (id: 180) to extended battery (green icon) (id: 101) + + 176 + Speed + Set speed (id: 176) to 8x ultra slo-mo (ext. batt) (id: 14) GET - /gopro/camera/setting?setting=180&option=101 + /gopro/camera/setting?setting=176&option=14 + \>= v02.01.00 - - 180 - Video Mode - Set video mode (id: 180) to longest battery (green icon) (id: 102) + + 176 + Speed + Set speed (id: 176) to 8x ultra slo-mo (ext. batt, 50hz) (id: 15) GET - /gopro/camera/setting?setting=180&option=102 + /gopro/camera/setting?setting=176&option=15 + \>= v02.01.00 - - - + + 176 + Speed + Set speed (id: 176) to 8x ultra slo-mo (long. batt) (id: 16) + GET + /gopro/camera/setting?setting=176&option=16 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 4x super slo-mo (long. batt) (id: 17) + GET + /gopro/camera/setting?setting=176&option=17 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (long. batt) (id: 18) + GET + /gopro/camera/setting?setting=176&option=18 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 1x (long. batt, low light) (id: 19) + GET + /gopro/camera/setting?setting=176&option=19 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 8x ultra slo-mo (long. batt, 50hz) (id: 20) + GET + /gopro/camera/setting?setting=176&option=20 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 4x super slo-mo (long. batt, 50hz) (id: 21) + GET + /gopro/camera/setting?setting=176&option=21 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (long. batt, 50hz) (id: 22) + GET + /gopro/camera/setting?setting=176&option=22 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 1x (long. batt, low light, 50hz) (id: 23) + GET + /gopro/camera/setting?setting=176&option=23 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (4k) (id: 24) + GET + /gopro/camera/setting?setting=176&option=24 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 4x super slo-mo (2.7k) (id: 25) + GET + /gopro/camera/setting?setting=176&option=25 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (4k, 50hz) (id: 26) + GET + /gopro/camera/setting?setting=176&option=26 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 4x super slo-mo (2.7k, 50hz) (id: 27) + GET + /gopro/camera/setting?setting=176&option=27 + + + \>= v02.01.00 + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 28) + GET + /gopro/camera/setting?setting=176&option=28 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 29) + GET + /gopro/camera/setting?setting=176&option=29 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 30) + GET + /gopro/camera/setting?setting=176&option=30 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 31) + GET + /gopro/camera/setting?setting=176&option=31 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 32) + GET + /gopro/camera/setting?setting=176&option=32 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 33) + GET + /gopro/camera/setting?setting=176&option=33 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 34) + GET + /gopro/camera/setting?setting=176&option=34 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 35) + GET + /gopro/camera/setting?setting=176&option=35 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 36) + GET + /gopro/camera/setting?setting=176&option=36 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 37) + GET + /gopro/camera/setting?setting=176&option=37 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 38) + GET + /gopro/camera/setting?setting=176&option=38 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 39) + GET + /gopro/camera/setting?setting=176&option=39 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 40) + GET + /gopro/camera/setting?setting=176&option=40 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 41) + GET + /gopro/camera/setting?setting=176&option=41 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 42) + GET + /gopro/camera/setting?setting=176&option=42 + + + + + + + + 176 + Speed + Set speed (id: 176) to 2x slo-mo (id: 43) + GET + /gopro/camera/setting?setting=176&option=43 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 44) + GET + /gopro/camera/setting?setting=176&option=44 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 45) + GET + /gopro/camera/setting?setting=176&option=45 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 46) + GET + /gopro/camera/setting?setting=176&option=46 + + + + + + + + 176 + Speed + Set speed (id: 176) to 1x speed / low light (id: 47) + GET + /gopro/camera/setting?setting=176&option=47 + + + + + + + + 177 + Enable Night Photo + Set enable night photo (id: 177) to off (id: 0) + GET + /gopro/camera/setting?setting=177&option=0 + + + + + + + + 177 + Enable Night Photo + Set enable night photo (id: 177) to on (id: 1) + GET + /gopro/camera/setting?setting=177&option=1 + + + + + + + + 178 + Wireless Band + Set wireless band (id: 178) to 2.4ghz (id: 0) + GET + /gopro/camera/setting?setting=178&option=0 + + + + + + + + 178 + Wireless Band + Set wireless band (id: 178) to 5ghz (id: 1) + GET + /gopro/camera/setting?setting=178&option=1 + + + + + + + + 179 + Trail Length + Set trail length (id: 179) to short (id: 1) + GET + /gopro/camera/setting?setting=179&option=1 + + + + + + + + 179 + Trail Length + Set trail length (id: 179) to long (id: 2) + GET + /gopro/camera/setting?setting=179&option=2 + + + + + + + + 179 + Trail Length + Set trail length (id: 179) to max (id: 3) + GET + /gopro/camera/setting?setting=179&option=3 + + + + + + + + 180 + Video Mode + Set video mode (id: 180) to highest quality (id: 0) + GET + /gopro/camera/setting?setting=180&option=0 + + + + + + + + 180 + Video Mode + Set video mode (id: 180) to extended battery (id: 1) + GET + /gopro/camera/setting?setting=180&option=1 + + + + + + + + 180 + Video Mode + Set video mode (id: 180) to extended battery (green icon) (id: 101) + GET + /gopro/camera/setting?setting=180&option=101 + + + \>= v02.01.00 + + + + + 180 + Video Mode + Set video mode (id: 180) to longest battery (green icon) (id: 102) + GET + /gopro/camera/setting?setting=180&option=102 + + + \>= v02.01.00 + + + + + 182 + Bit Rate + Set system video bit rate (id: 182) to standard (id: 0) + GET + /gopro/camera/setting?setting=182&option=0 + + + + + + + + 182 + Bit Rate + Set system video bit rate (id: 182) to high (id: 1) + GET + /gopro/camera/setting?setting=182&option=1 + + + + + + + + 183 + Bit Depth + Set system video bit depth (id: 183) to 8-bit (id: 0) + GET + /gopro/camera/setting?setting=183&option=0 + + + + + + + + 183 + Bit Depth + Set system video bit depth (id: 183) to 10-bit (id: 2) + GET + /gopro/camera/setting?setting=183&option=2 + + + + + + + + 184 + Profiles + Set video profile (id: 184) to standard (id: 0) + GET + /gopro/camera/setting?setting=184&option=0 + + + + + + + + 184 + Profiles + Set video profile (id: 184) to hdr (id: 1) + GET + /gopro/camera/setting?setting=184&option=1 + + + + + + + + 184 + Profiles + Set video profile (id: 184) to log (id: 2) + GET + /gopro/camera/setting?setting=184&option=2 + + + + + + + + 185 + Aspect Ratio + Set video easy aspect ratio (id: 185) to widescreen (id: 0) + GET + /gopro/camera/setting?setting=185&option=0 + + + + + + + + 185 + Aspect Ratio + Set video easy aspect ratio (id: 185) to mobile (id: 1) + GET + /gopro/camera/setting?setting=185&option=1 + + + + + + + + 185 + Aspect Ratio + Set video easy aspect ratio (id: 185) to universal (id: 2) + GET + /gopro/camera/setting?setting=185&option=2 + + + + + + + + 186 + Video Mode + Set video easy presets (id: 186) to highest quality (id: 0) + GET + /gopro/camera/setting?setting=186&option=0 + + + + + + + + 186 + Video Mode + Set video easy presets (id: 186) to standard quality (id: 1) + GET + /gopro/camera/setting?setting=186&option=1 + + + + + + + + 186 + Video Mode + Set video easy presets (id: 186) to basic quality (id: 2) + GET + /gopro/camera/setting?setting=186&option=2 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to timewarp (id: 0) + GET + /gopro/camera/setting?setting=187&option=0 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to star trails (id: 1) + GET + /gopro/camera/setting?setting=187&option=1 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to light painting (id: 2) + GET + /gopro/camera/setting?setting=187&option=2 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to vehicle lights (id: 3) + GET + /gopro/camera/setting?setting=187&option=3 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to max timewarp (id: 4) + GET + /gopro/camera/setting?setting=187&option=4 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to max star trails (id: 5) + GET + /gopro/camera/setting?setting=187&option=5 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to max light painting (id: 6) + GET + /gopro/camera/setting?setting=187&option=6 + + + + + + + + 187 + Lapse Mode + Set multi shot easy presets (id: 187) to max vehicle lights (id: 7) + GET + /gopro/camera/setting?setting=187&option=7 + + + + + + + + 188 + Aspect Ratio + Set multi shot easy aspect ratio (id: 188) to widescreen (id: 0) + GET + /gopro/camera/setting?setting=188&option=0 + + + + + + + + 188 + Aspect Ratio + Set multi shot easy aspect ratio (id: 188) to mobile (id: 1) + GET + /gopro/camera/setting?setting=188&option=1 + + + + + + + + 188 + Aspect Ratio + Set multi shot easy aspect ratio (id: 188) to universal (id: 2) + GET + /gopro/camera/setting?setting=188&option=2 + + + + + + + + 189 + Max Lens Mod + Set system addon lens active (id: 189) to none (id: 0) + GET + /gopro/camera/setting?setting=189&option=0 + + + + + + + + 189 + Max Lens Mod + Set system addon lens active (id: 189) to max lens 1.0 (id: 1) + GET + /gopro/camera/setting?setting=189&option=1 + + + + + + + + 189 + Max Lens Mod + Set system addon lens active (id: 189) to max lens 2.0 (id: 2) + GET + /gopro/camera/setting?setting=189&option=2 + + + + + + + + 190 + Max Lens Mod Enable + Set system addon lens status (id: 190) to off (id: 0) + GET + /gopro/camera/setting?setting=190&option=0 + + + + + + + + 190 + Max Lens Mod Enable + Set system addon lens status (id: 190) to on (id: 1) + GET + /gopro/camera/setting?setting=190&option=1 + + + + + + + + 191 + Photo Mode + Set photo easy presets (id: 191) to super photo (id: 0) + GET + /gopro/camera/setting?setting=191&option=0 + + + + + + + + 191 + Photo Mode + Set photo easy presets (id: 191) to night photo (id: 1) + GET + /gopro/camera/setting?setting=191&option=1 + + + + + + + + 192 + Aspect Ratio + Set multi shot nlv aspect ratio (id: 192) to 4:3 (id: 0) + GET + /gopro/camera/setting?setting=192&option=0 + + + + + + + + 192 + Aspect Ratio + Set multi shot nlv aspect ratio (id: 192) to 16:9 (id: 1) + GET + /gopro/camera/setting?setting=192&option=1 + + + + + + + + 192 + Aspect Ratio + Set multi shot nlv aspect ratio (id: 192) to 8:7 (id: 3) + GET + /gopro/camera/setting?setting=192&option=3 + + + + + + + + 193 + Framing + Set video easy framing (id: 193) to widescreen (id: 0) + GET + /gopro/camera/setting?setting=193&option=0 + + + + + + + + 193 + Framing + Set video easy framing (id: 193) to vertical (id: 1) + GET + /gopro/camera/setting?setting=193&option=1 + + + + + + + + 193 + Framing + Set video easy framing (id: 193) to full frame (id: 2) + GET + /gopro/camera/setting?setting=193&option=2 + + + + + + + + + + +## Camera Capabilities +

    +Camera capabilities usually change from one camera to another and often change from one release to the next. +Below are documents that detail whitelists for basic video settings for every supported camera release. +

    + +### Note about Dependency Ordering and Blacklisting +

    +Capability documents define supported camera states. +Each state is comprised of a set of setting options that are presented in dependency order. +This means each state is guaranteed to be attainable if and only if the setting options are set in the order presented. +Failure to adhere to dependency ordering may result in the camera's blacklist rules rejecting a set-setting command. +

    + +### Example + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CameraCommand 1Command 2Command 3Command 4Command 5Guaranteed Valid?
    HERO10 BlackRes: 1080Anti-Flicker: 60Hz (NTSC)FPS: 240FOV: WideHypersmooth: OFF
    HERO10 BlackFPS: 240Anti-Flicker: 60Hz (NTSC)Res: 1080FOV: WideHypersmooth: OFF
    +

    +In the example above, the first set of commands will always work for basic video presets such as Standard. +

    + +

    +In the second example, suppose the camera's Video Resolution was previously set to 4K. +If the user tries to set Video FPS to 240, it will fail because 4K/240fps is not supported. +

    + +### Capability Documents + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DocumentsProductRelease
    capabilities.xlsx
    capabilities.json
    HERO12 Blackv01.10.00
    HERO11 Black Miniv02.30.00
    v02.20.00
    v02.10.00
    v02.00.00
    v01.10.00
    HERO11 Blackv02.12.00
    v02.10.00
    v02.01.00
    v01.20.00
    v01.12.00
    v01.10.00
    HERO10 Blackv01.50.00
    v01.46.00
    v01.42.00
    v01.40.00
    v01.30.00
    v01.20.00
    v01.16.00
    v01.10.00
    HERO9 Blackv01.72.00
    v01.70.00
    -## Camera Capabilities +### Spreadsheet Format

    -Camera capabilities usually change from one camera to another and often change from one release to the next. -Below are documents that detail whitelists for basic video settings for every supported camera release. +The capabilities spreadsheet contains worksheets for every supported release. +Each row in a worksheet represents a whitelisted state and is presented in dependency order as outlined above.

    -### Note about Dependency Ordering and Blacklisting +### JSON Format

    -Capability documents define supported camera states. -Each state is comprised of a set of setting options that are presented in dependency order. -This means each state is guaranteed to be attainable if and only if the setting options are set in the order presented. -Failure to adhere to dependency ordering may result in the camera's blacklist rules rejecting a set-setting command. +The capabilities JSON contains a set of whitelist states for every supported release. +Each state is comprised of a list of objects that contain setting and option IDs necessary to construct set-setting +commands and are given in dependency order as outlined above.

    -### Example +

    +Below is a simplified example of the capabilities JSON file; a formal schema is also available here: +capabilities_schema.json +

    + +``` +{ + "(PRODUCT_NAME)": { + "(RELEASE_VERSION)": { + "states": [ + [ + {"setting_name": "(str)", "setting_id": (int), "option_name": "(str)", "option_id": (int)}, + ... + ], + ... + ], + }, + ... + }, + ... +} +``` + + +# Media +

    +The camera provides an endpoint to query basic details about media captured on the sdcard. +

    + + +## Chapters +

    +All GoPro cameras break longer videos into chapters. +GoPro cameras currently limit file sizes on sdcards to 4GB for both FAT32 and exFAT file systems. +This limitation is most commonly seen when recording longer (10+ minute) videos. +In practice, the camera will split video media into chapters named Gqccmmmm.MP4 (and ones for THM/LRV) such that: +

    + +
      +
    • q: Quality Level (X: Extreme, H: High, M: Medium, L: Low)
    • +
    • cc: Chapter Number (01-99)
    • +
    • mmmm: Media ID (0001-9999)
    • +
    + +

    +When media becomes chaptered, the camera increments subsequent Chapter Numbers while leaving the Media ID unchanged. +For example, if the user records a long High-quality video that results in 4 chapters, the files on the sdcard may +look like the following: +

    + +``` +-rwxrwxrwx@ 1 gopro 123456789 4006413091 Jan 1 00:00 GH010078.MP4 +-rwxrwxrwx@ 1 gopro 123456789 17663 Jan 1 00:00 GH010078.THM +-rwxrwxrwx@ 1 gopro 123456789 4006001541 Jan 1 00:00 GH020078.MP4 +-rwxrwxrwx@ 1 gopro 123456789 17357 Jan 1 00:00 GH020078.THM +-rwxrwxrwx@ 1 gopro 123456789 4006041985 Jan 1 00:00 GH030078.MP4 +-rwxrwxrwx@ 1 gopro 123456789 17204 Jan 1 00:00 GH030078.THM +-rwxrwxrwx@ 1 gopro 123456789 756706872 Jan 1 00:00 GH040078.MP4 +-rwxrwxrwx@ 1 gopro 123456789 17420 Jan 1 00:00 GH040078.THM +-rwxrwxrwx@ 1 gopro 123456789 184526939 Jan 1 00:00 GL010078.LRV +-rwxrwxrwx@ 1 gopro 123456789 184519787 Jan 1 00:00 GL020078.LRV +-rwxrwxrwx@ 1 gopro 123456789 184517614 Jan 1 00:00 GL030078.LRV +-rwxrwxrwx@ 1 gopro 123456789 34877660 Jan 1 00:00 GL040078.LRV +``` + + +## Media Info Format +

    +The Media: Info command provides additional details about a media above and beyond its counterpart, the Media: List command. +Such information includes resolution, frame rate, duration, hilight info, etc. +

    + +### Example Video Info: +``` +{ + "cre": "1613676644", + "s": "11305367", + "mahs": "1", + "us": "0", + "mos": [], + "eis": "0", + "pta": "1", + "ao": "stereo", + "tr": "0", + "mp": "0", + "ct": "0", + "rot": "0", + "fov": "4", + "lc": "0", + "prjn": "6", + "gumi": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "ls": "1072714", + "cl": "0", + "avc_profile": "4", + "profile": "42", + "hc": "0", + "hi": [], + "dur": "2", + "w": "1920", + "h": "1080", + "fps": "60000", + "fps_denom": "1001", + "prog": "1", + "subsample": "0" +} +``` + +### Common Keys (Video / Photo) - - - - - - - + + + + - - - - - - - + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CameraCommand 1Command 2Command 3Command 4Command 5Guaranteed Valid?KeyTypeDescriptionExamples
    HERO10 BlackRes: 1080Anti-Flicker: 60Hz (NTSC)FPS: 240FOV: WideHypersmooth: OFFaostringAudio Optionoff, stereo, wind, auto
    HERO10 BlackFPS: 240Anti-Flicker: 60Hz (NTSC)Res: 1080FOV: WideHypersmooth: OFFavc_profileuint8Advanced Video Codec Profile0..255
    clboolFile clipped from another source?0:false, 1:true
    creuint32File creation timestamp (sec since epoch)1692992748
    ctuint32Content type0..12
    duruint32Duration of video in seconds42
    eisboolFile made with Electronic Image Stabilization0:false, 1:true
    fpsuint32Frame rate (numerator)1001
    fps_denomuint32Frme rate (denominator)30000
    gumistringGlobally Unique Media ID"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    huint32Video height in pixels1080
    hcuint32Hilight countvideo:0..99, photo:0..1
    hdrboolPhoto taken with High Dynamic Range?0:false, 1:true
    hiArray of uint32Offset to hilights in media in milliseconds[1500, 4700]
    lcuint32Spherical Lens Config0:front, 1:rear
    lsint32Low Resolution Video file size in bytes (or -1 if no LRV file)-1, 1234567890
    mosArray of stringMobile Offload State"app", "pc", "other"
    mpboolMetadata Present?0:no metadata, 1:metadata exists
    profileuint8Advanced Video Codec Level0..255
    progboolIs video progressive?0:interlaced, 1:progressive
    ptaboolMedia has Protune audio file?0:false, 1:true
    rawboolPhoto has raw version?0:false, 1:true
    suint64File size in bytes1234567890
    subsampleboolIs video subsampled?0:false, 1:true
    trboolIs file transcoded?0:false, 1:true
    wuint32Width of media in pixels1920
    wdrboolPhoto taken with Wide Dynamic Range?0:false, 1:true
    -

    -In the example above, the first set of commands will always work for basic video presets such as Standard. -

    - -

    -In the second example, suppose the camera's Video Resolution was previously set to 4K. -If the user tries to set Video FPS to 240, it will fail because 4K/240fps is not supported. -

    -### Capability Documents +### Video Keys - - - + + + + - - - + + + + - + + + + - + + + + - + + + + - + + + + - - + + + + - + + + + - + + + + - + + + + - + + + + - + + + + - - + + + + + + +
    DocumentsProductReleaseKeyTypeDescriptionExamples
    capabilities.xlsx
    capabilities.json
    HERO11 Black Miniv02.30.00aostringAudio Optionoff, stereo, wind, auto
    v02.20.00avc_profileuint8Advanced Video Codec Profile0..255
    v02.10.00clboolFile clipped from another source?0:false, 1:true
    v02.00.00duruint32Duration of video in seconds42
    v01.10.00fpsuint32Frame rate (numerator)1001
    HERO11 Blackv02.12.00fps_denomuint32Frme rate (denominator)30000
    v02.10.00hiArray of uint32Offset to hilights in media in milliseconds[1500, 4700]
    v02.01.00lsint32Low Resolution Video file size in bytes (or -1 if no LRV file)-1, 1234567890
    v01.20.00profileuint8Advanced Video Codec Level0..255
    v01.12.00progboolIs video progressive?0:interlaced, 1:progressive
    v01.10.00ptaboolMedia has Protune audio file?0:false, 1:true
    HERO10 Blackv01.50.00subsampleboolIs video subsampled?0:false, 1:true
    + +### Photo Keys + + + + + + + - + + + + - + + + + - + + + + + + +
    KeyTypeDescriptionExamples
    v01.46.00hdrboolPhoto taken with High Dynamic Range?0:false, 1:true
    v01.42.00rawboolPhoto has raw version?0:false, 1:true
    v01.40.00wdrboolPhoto taken with Wide Dynamic Range?0:false, 1:true
    + +### Media Info: Content Type +

    +The "ct" (Content Type) metadata indicates what mode (or group) the media was captured in. +

    + +

    +Note: All Time Lapse modes that result in MPEG media use the same content type ID. +

    + + + + + - + + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - - + + - + +
    IDMode
    v01.30.00Video0
    v01.20.00Looping1
    v01.16.00Chaptered Video2
    Time Lapse3
    Single Photo4
    Burst Photo5
    Time Lapse Photo6
    Night Lapse Photo8
    Night Photo9
    v01.10.00Continuous Photo10
    HERO9 Blackv01.72.00Raw Photo11
    v01.70.00Live Burst12
    -### Spreadsheet Format -

    -The capabilities spreadsheet contains worksheets for every supported release. -Each row in a worksheet represents a whitelisted state and is presented in dependency order as outlined above. -

    - -### JSON Format -

    -The capabilities JSON contains a set of whitelist states for every supported release. -Each state is comprised of a list of objects that contain setting and option IDs necessary to construct set-setting -commands and are given in dependency order as outlined above. -

    - -

    -Below is a simplified example of the capabilities JSON file; a formal schema is also available here: -capabilities_schema.json -

    - -``` -{ - "(PRODUCT_NAME)": { - "(RELEASE_VERSION)": { - "states": [ - [ - {"setting_name": "(str)", "setting_id": (int), "option_name": "(str)", "option_id": (int)}, - ... - ], - ... - ], - }, - ... - }, - ... -} -``` - - -# Media -

    -The camera provides an endpoint to query basic details about media captured on the sdcard. -

    - - -## Chapters -

    -All GoPro cameras break longer videos into chapters. -GoPro cameras currently limit file sizes on sdcards to 4GB for both FAT32 and exFAT file systems. -This limitation is most commonly seen when recording longer (10+ minute) videos. -In practice, the camera will split video media into chapters named Gqccmmmm.MP4 (and ones for THM/LRV) such that: -

    - -
      -
    • q: Quality Level (X: Extreme, H: High, M: Medium, L: Low)
    • -
    • cc: Chapter Number (01-99)
    • -
    • mmmm: Media ID (0001-9999)
    • -
    - -

    -When media becomes chaptered, the camera increments subsequent Chapter Numbers while leaving the Media ID unchanged. -For example, if the user records a long High-quality video that results in 4 chapters, the files on the sdcard may -look like the following: -

    - -``` --rwxrwxrwx@ 1 gopro 123456789 4006413091 Jan 1 00:00 GH010078.MP4 --rwxrwxrwx@ 1 gopro 123456789 17663 Jan 1 00:00 GH010078.THM --rwxrwxrwx@ 1 gopro 123456789 4006001541 Jan 1 00:00 GH020078.MP4 --rwxrwxrwx@ 1 gopro 123456789 17357 Jan 1 00:00 GH020078.THM --rwxrwxrwx@ 1 gopro 123456789 4006041985 Jan 1 00:00 GH030078.MP4 --rwxrwxrwx@ 1 gopro 123456789 17204 Jan 1 00:00 GH030078.THM --rwxrwxrwx@ 1 gopro 123456789 756706872 Jan 1 00:00 GH040078.MP4 --rwxrwxrwx@ 1 gopro 123456789 17420 Jan 1 00:00 GH040078.THM --rwxrwxrwx@ 1 gopro 123456789 184526939 Jan 1 00:00 GL010078.LRV --rwxrwxrwx@ 1 gopro 123456789 184519787 Jan 1 00:00 GL020078.LRV --rwxrwxrwx@ 1 gopro 123456789 184517614 Jan 1 00:00 GL030078.LRV --rwxrwxrwx@ 1 gopro 123456789 34877660 Jan 1 00:00 GL040078.LRV -``` - ## Media List Format

    @@ -2362,6 +4028,10 @@ The outer structure of the media list and the inner structure of individual medi b ID of first member of a group (for grouped media items) + + cre + Creation timestamp (seconds since epoch) + d Directory name @@ -2415,7 +4085,7 @@ The outer structure of the media list and the inner structure of individual medi ### Grouped Media Items

    -In order to minimize the size of the JSON transmitted by the camera, grouped media items such as Burst Photos, +To minimize the size of the JSON transmitted by the camera, grouped media items such as Burst Photos, Time Lapse Photos, Night Lapse Photos, etc are represented with a single item in the media list with additional keys that allow the user to extrapolate individual filenames for each member of the group.

    @@ -2472,7 +4142,6 @@ G0010394.JPG, G0010395.JPG. G0010396.JPG

    - ## Media HiLights

    The HiLight Tags feature allows the user to tag moments of interest either during video @@ -2682,6 +4351,7 @@ Below is a table of supported status IDs.
    Description Type Values + HERO12 Black HERO11 Black Mini HERO11 Black HERO10 Black @@ -2697,6 +4367,7 @@ Below is a table of supported status IDs.
    + 2 @@ -2708,6 +4379,7 @@ Below is a table of supported status IDs.
    + 6 @@ -2719,6 +4391,7 @@ Below is a table of supported status IDs.
    + 8 @@ -2730,6 +4403,7 @@ Below is a table of supported status IDs.
    + 9 @@ -2741,6 +4415,7 @@ Below is a table of supported status IDs.
    + 10 @@ -2752,6 +4427,7 @@ Below is a table of supported status IDs.
    + 11 @@ -2763,6 +4439,7 @@ Below is a table of supported status IDs.
    + 13 @@ -2774,6 +4451,7 @@ Below is a table of supported status IDs.
    + 17 @@ -2785,6 +4463,7 @@ Below is a table of supported status IDs.
    + 19 @@ -2796,6 +4475,7 @@ Below is a table of supported status IDs.
    + 20 @@ -2807,6 +4487,7 @@ Below is a table of supported status IDs.
    + 21 @@ -2814,6 +4495,7 @@ Below is a table of supported status IDs.
    Time (milliseconds) since boot of last successful pairing complete action integer * + @@ -2829,6 +4511,7 @@ Below is a table of supported status IDs.
    + 23 @@ -2840,6 +4523,7 @@ Below is a table of supported status IDs.
    + 24 @@ -2851,6 +4535,7 @@ Below is a table of supported status IDs.
    + 26 @@ -2858,6 +4543,7 @@ Below is a table of supported status IDs.
    Wireless remote control version integer * + @@ -2873,6 +4559,7 @@ Below is a table of supported status IDs.
    + 28 @@ -2880,6 +4567,7 @@ Below is a table of supported status IDs.
    Wireless Pairing State integer * + @@ -2895,6 +4583,7 @@ Below is a table of supported status IDs.
    + 30 @@ -2906,6 +4595,7 @@ Below is a table of supported status IDs.
    + 31 @@ -2917,6 +4607,7 @@ Below is a table of supported status IDs.
    + 32 @@ -2928,6 +4619,7 @@ Below is a table of supported status IDs.
    + 33 @@ -2939,6 +4631,7 @@ Below is a table of supported status IDs.
    + 34 @@ -2946,6 +4639,7 @@ Below is a table of supported status IDs.
    How many photos can be taken before sdcard is full integer * + @@ -2961,6 +4655,7 @@ Below is a table of supported status IDs.
    + 36 @@ -2968,6 +4663,7 @@ Below is a table of supported status IDs.
    How many group photos can be taken with current settings before sdcard is full integer * + @@ -2983,6 +4679,7 @@ Below is a table of supported status IDs.
    + 38 @@ -2994,6 +4691,7 @@ Below is a table of supported status IDs.
    + 39 @@ -3005,6 +4703,7 @@ Below is a table of supported status IDs.
    + 41 @@ -3016,6 +4715,7 @@ Below is a table of supported status IDs.
    + 42 @@ -3027,6 +4727,7 @@ Below is a table of supported status IDs.
    + 45 @@ -3038,6 +4739,7 @@ Below is a table of supported status IDs.
    + 49 @@ -3049,6 +4751,7 @@ Below is a table of supported status IDs.
    + 54 @@ -3060,6 +4763,7 @@ Below is a table of supported status IDs.
    + 55 @@ -3071,6 +4775,7 @@ Below is a table of supported status IDs.
    + 56 @@ -3082,6 +4787,7 @@ Below is a table of supported status IDs.
    + 58 @@ -3093,6 +4799,7 @@ Below is a table of supported status IDs.
    + 59 @@ -3104,6 +4811,7 @@ Below is a table of supported status IDs.
    + 60 @@ -3115,6 +4823,7 @@ Below is a table of supported status IDs.
    + 64 @@ -3126,6 +4835,7 @@ Below is a table of supported status IDs.
    + 65 @@ -3133,6 +4843,7 @@ Below is a table of supported status IDs.
    Liveview Exposure Select Mode integer 0: Disabled
    1: Auto
    2: ISO Lock
    3: Hemisphere
    + @@ -3144,6 +4855,7 @@ Below is a table of supported status IDs.
    Liveview Exposure Select: y-coordinate (percent) percent 0-100 + @@ -3155,6 +4867,7 @@ Below is a table of supported status IDs.
    Liveview Exposure Select: y-coordinate (percent) percent 0-100 + @@ -3170,6 +4883,7 @@ Below is a table of supported status IDs.
    + 69 @@ -3181,6 +4895,7 @@ Below is a table of supported status IDs.
    + 70 @@ -3192,6 +4907,7 @@ Below is a table of supported status IDs.
    + 74 @@ -3203,6 +4919,7 @@ Below is a table of supported status IDs.
    + 75 @@ -3214,6 +4931,7 @@ Below is a table of supported status IDs.
    + 76 @@ -3225,6 +4943,7 @@ Below is a table of supported status IDs.
    + 77 @@ -3236,6 +4955,7 @@ Below is a table of supported status IDs.
    + 78 @@ -3247,6 +4967,7 @@ Below is a table of supported status IDs.
    + 79 @@ -3256,6 +4977,7 @@ Below is a table of supported status IDs.
    0: False
    1: True
    + @@ -3269,6 +4991,7 @@ Below is a table of supported status IDs.
    + 82 @@ -3280,6 +5003,7 @@ Below is a table of supported status IDs.
    + 83 @@ -3291,6 +5015,7 @@ Below is a table of supported status IDs.
    + 85 @@ -3302,6 +5027,7 @@ Below is a table of supported status IDs.
    + 86 @@ -3313,6 +5039,7 @@ Below is a table of supported status IDs.
    + 88 @@ -3324,6 +5051,7 @@ Below is a table of supported status IDs.
    + 89 @@ -3335,6 +5063,7 @@ Below is a table of supported status IDs.
    + 93 @@ -3346,6 +5075,7 @@ Below is a table of supported status IDs.
    + 94 @@ -3353,6 +5083,7 @@ Below is a table of supported status IDs.
    Current Photo Preset (ID) integer * + @@ -3368,6 +5099,7 @@ Below is a table of supported status IDs.
    + 96 @@ -3379,6 +5111,7 @@ Below is a table of supported status IDs.
    + 97 @@ -3390,6 +5123,7 @@ Below is a table of supported status IDs.
    + 98 @@ -3401,6 +5135,7 @@ Below is a table of supported status IDs.
    + 99 @@ -3409,6 +5144,7 @@ Below is a table of supported status IDs.
    integer * + @@ -3420,6 +5156,7 @@ Below is a table of supported status IDs.
    integer * + @@ -3434,6 +5171,7 @@ Below is a table of supported status IDs.
    + 102 @@ -3445,6 +5183,7 @@ Below is a table of supported status IDs.
    + 103 @@ -3456,6 +5195,7 @@ Below is a table of supported status IDs.
    + 104 @@ -3465,15 +5205,17 @@ Below is a table of supported status IDs.
    0: False
    1: True
    + 105 Camera lens type - Camera lens type (reflects changes to setting 162) + Camera lens type (reflects changes to setting 162 or setting 189) integer - 0: Default
    1: Max Lens
    + 0: Default
    1: Max Lens
    2: Max Lens 2.0
    + @@ -3485,6 +5227,7 @@ Below is a table of supported status IDs.
    Is Video Hindsight Capture Active? boolean 0: False
    1: True
    + @@ -3496,6 +5239,7 @@ Below is a table of supported status IDs.
    Scheduled Capture Preset ID integer * + @@ -3507,6 +5251,7 @@ Below is a table of supported status IDs.
    Is Scheduled Capture set? boolean 0: False
    1: True
    + @@ -3518,6 +5263,7 @@ Below is a table of supported status IDs.
    Media Mode Status (bitmasked) integer 0: 000 = Selfie mod: 0, HDMI: 0, Media Mod Connected: False
    1: 001 = Selfie mod: 0, HDMI: 0, Media Mod Connected: True
    2: 010 = Selfie mod: 0, HDMI: 1, Media Mod Connected: False
    3: 011 = Selfie mod: 0, HDMI: 1, Media Mod Connected: True
    4: 100 = Selfie mod: 1, HDMI: 0, Media Mod Connected: False
    5: 101 = Selfie mod: 1, HDMI: 0, Media Mod Connected: True
    6: 110 = Selfie mod: 1, HDMI: 1, Media Mod Connected: False
    7: 111 = Selfie mod: 1, HDMI: 1, Media Mod Connected: True
    + @@ -3532,6 +5278,7 @@ Below is a table of supported status IDs.
    + @@ -3543,6 +5290,7 @@ Below is a table of supported status IDs.
    + @@ -3555,6 +5303,7 @@ Below is a table of supported status IDs.
    + 114 @@ -3565,6 +5314,7 @@ Below is a table of supported status IDs.
    + @@ -3576,6 +5326,7 @@ Below is a table of supported status IDs.
    + @@ -3586,6 +5337,7 @@ Below is a table of supported status IDs.
    0: Disabled
    1: Enabled
    + \>= v01.30.00 @@ -3597,6 +5349,7 @@ Below is a table of supported status IDs.
    * + @@ -3652,6 +5405,26 @@ Below is a table of settings that affect the current preset collection and there 180 Video Mode + + 186 + Video Mode + + + 187 + Lapse Mode + + + 189 + Max Lens Mod + + + 190 + Max Lens Mod Enable + + + 191 + Photo Mode + @@ -3774,6 +5547,7 @@ Note:

    The Over The Air (OTA) update feature allows the user to update the camera's firmware via HTTP connection. +There are two ways to perform OTA updates: Simple OTA Update and Resumable OTA Update.

    @@ -3781,27 +5555,73 @@ Firmware update files can be obtained from GoPro's firmware catalog.

    -### OTA Update Flow

    -The OTA update process involves uploading chunks (or all) of a file along with its corresponding SHA1 hash, -marking the file complete and then telling the camera to begin the update process. -For specific command examples, see /gp/gpSoftUpdate in -Commands Quick Reference. +Note: In order to complete the firmware update process, the camera will reboot one or more times. +This will cause any existing HTTP connections to be lost.

    +### Simple OTA Update

    -Note: Near the end of the firmware update process, in order to complete, the camera will need to reboot 1-2 times. -This will cause any existing HTTP connections to be lost. +The simple OTA update process is done by sending an entire update file to the camera in a single HTTP/POST. +Details can be found in the diagram below. +

    + +```plantuml! + + +title Simple OTA Update + +actor Client +participant Camera + +' Update Page and Firmware Catalog +' https://gopro.com/en/us/update +' https://api.gopro.com/firmware/v2/catalog + +== Obtain UPDATE.zip from update page or firmware catalog == + +Client -> Client: Calculate SHA1_HASH for UPDATE.zip +Client -> Camera: HTTP/POST: /gp/gpUpdate +note right +Content-Type: multipart/form-data +Data: + DirectToSD=1 + update=1 + sha1= + file= +end note + +Client <-- Camera: HTTP/200 (OK) +note right +JSON: { "status":"0" } +end note + +== WiFi connection terminates == +== Camera displays "Update Complete" OSD, reboots 1-2 times == + + + + +``` + +### Resumable OTA Update +

    +The resumable OTA update process involves uploading chunks (or all) of a file, marking the file complete and then telling the camera to begin the update process. +Chunks are stored until they are explicitly deleted, allowing the client to stop and resume as needed. +Details can be found in the diagram below.

    + ```plantuml! +title Resumable OTA Update + Actor Client participant Camera -note across: Obtain UPDATE.zip from update page or firmware catalog -note across: Calculate SHA1_HASH for UPDATE.zip -Client -> Camera: HTTP/GET: /gp/gpSoftUpdate?request=delete +== Obtain UPDATE.zip from update page or firmware catalog == +Client -> Client: Calculate SHA1_HASH for UPDATE.zip +Client -> Camera: HTTP/GET: /gp/gpSoftUpdate?request=delete note right: Delete any old/cached data Client <-- Camera: HTTP/200 (OK) note right @@ -3815,7 +5635,7 @@ JSON { end note Client -> Camera: HTTP/GET: /gp/gpSoftUpdate?request=showui -note right: Display update OSD on camera UI +note right: Display update OSD on camera UI (optional) Client <-- Camera: HTTP/200 (OK) note right JSON: { @@ -3891,8 +5711,8 @@ JSON: { end note end -note across: WiFi connection lost -note across: Camera displays OSD "Update Complete", reboots 1-2 times +== WiFi connection lost == +== Camera displays OSD "Update Complete", reboots 1-2 times == @@ -3900,6 +5720,7 @@ note across: Camera displays OSD "Update Complete", reboots 1-2 times ``` ### OTA Update Status Codes + @@ -4025,7 +5846,7 @@ LPP --> READY: Stop\nExit ### Webcam Commands

    -Note: Prior to issuing webcam commands, Wired USB Control must be disabled. +Note: For USB connections, prior to issuing webcam commands, Wired USB Control must be disabled. For details about how to send this and webcam commands, see Commands Quick Reference.

    @@ -4037,41 +5858,42 @@ For details about how to send this and webcam commands, see @@ -4169,6 +5991,15 @@ If fov is not set, camera will default to the last-set fov or Wide if fov has ne + + + + + + + + + @@ -4207,8 +6038,59 @@ If fov is not set, camera will default to the last-set fov or Wide if fov has ne
    Resolution FOV
    HERO12 Black720p (id: 7)Wide (id: 0), Narrow (id: 2), Superview (id: 3), Linear (id: 4)
    1080p (id: 12)Wide (id: 0), Narrow (id: 2), Superview (id: 3), Linear (id: 4)
    HERO11 Black 720p (id: 7)
    +### Webcam Stabilization + +

    +Should the client require stabilization, the Hypersmooth setting can be used while in the state: READY (Status: OFF). +This setting can only be set while webcam is disabled, which requires either sending the Webcam: Exit command or reseating the USB-C connection to the camera. +

    + +

    +Note: The Low Hypersmooth option provides lower/lighter stabilization when used in Webcam mode vs other camera modes. +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CameraVersionSupported Hypersmooth Options
    HERO12 Blackv01.10.00+Off (id: 0), Low (id: 1), Auto Boost (id: 4)
    HERO11 Black Miniv01.10.00+Off (id: 0), Low (id: 1), Boost (id: 3), Auto Boost (id: 4)
    HERO11 Blackv01.10.00+Off (id: 0), Low (id: 1), Boost (id: 3), Auto Boost (id: 4)
    HERO10 Blackv01.10.00+Off (id: 0), High (id: 2), Boost (id: 3), Standard (id: 100)
    HERO9 Blackv01.70.00+Off (id: 0), Low (id: 1), High (id: 2), Boost (id: 3)
    + # Limitations +## HERO12 Black +
    ## HERO11 Black Mini
    • The camera will reject requests to change settings while encoding; for example, if Hindsight feature is active, the user cannot change settings
    • diff --git a/protobuf/preset_status.proto b/protobuf/preset_status.proto index 38e9b554..0c6b5947 100644 --- a/protobuf/preset_status.proto +++ b/protobuf/preset_status.proto @@ -1,5 +1,5 @@ /* preset_status.proto/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */ -/* This copyright was auto-generated on Fri Jun 9 22:49:36 UTC 2023 */ +/* This copyright was auto-generated on Fri Sep 8 18:51:54 UTC 2023 */ /* Defines the structure of protobuf message received from camera containing preset status @@ -56,129 +56,155 @@ enum EnumPresetGroupIcon { } enum EnumPresetIcon { - PRESET_ICON_VIDEO = 0; - PRESET_ICON_ACTIVITY = 1; - PRESET_ICON_CINEMATIC = 2; - PRESET_ICON_PHOTO = 3; - PRESET_ICON_LIVE_BURST = 4; - PRESET_ICON_BURST = 5; - PRESET_ICON_PHOTO_NIGHT = 6; - PRESET_ICON_TIMEWARP = 7; - PRESET_ICON_TIMELAPSE = 8; - PRESET_ICON_NIGHTLAPSE = 9; - PRESET_ICON_SNAIL = 10; - PRESET_ICON_VIDEO_2 = 11; - PRESET_ICON_360_VIDEO = 12; - PRESET_ICON_PHOTO_2 = 13; - PRESET_ICON_PANORAMA = 14; - PRESET_ICON_BURST_2 = 15; - PRESET_ICON_TIMEWARP_2 = 16; - PRESET_ICON_TIMELAPSE_2 = 17; - PRESET_ICON_CUSTOM = 18; - PRESET_ICON_AIR = 19; - PRESET_ICON_BIKE = 20; - PRESET_ICON_EPIC = 21; - PRESET_ICON_INDOOR = 22; - PRESET_ICON_MOTOR = 23; - PRESET_ICON_MOUNTED = 24; - PRESET_ICON_OUTDOOR = 25; - PRESET_ICON_POV = 26; - PRESET_ICON_SELFIE = 27; - PRESET_ICON_SKATE = 28; - PRESET_ICON_SNOW = 29; - PRESET_ICON_TRAIL = 30; - PRESET_ICON_TRAVEL = 31; - PRESET_ICON_WATER = 32; - PRESET_ICON_LOOPING = 33; + PRESET_ICON_VIDEO = 0; + PRESET_ICON_ACTIVITY = 1; + PRESET_ICON_CINEMATIC = 2; + PRESET_ICON_PHOTO = 3; + PRESET_ICON_LIVE_BURST = 4; + PRESET_ICON_BURST = 5; + PRESET_ICON_PHOTO_NIGHT = 6; + PRESET_ICON_TIMEWARP = 7; + PRESET_ICON_TIMELAPSE = 8; + PRESET_ICON_NIGHTLAPSE = 9; + PRESET_ICON_SNAIL = 10; + PRESET_ICON_VIDEO_2 = 11; + PRESET_ICON_360_VIDEO = 12; + PRESET_ICON_PHOTO_2 = 13; + PRESET_ICON_PANORAMA = 14; + PRESET_ICON_BURST_2 = 15; + PRESET_ICON_TIMEWARP_2 = 16; + PRESET_ICON_TIMELAPSE_2 = 17; + PRESET_ICON_CUSTOM = 18; + PRESET_ICON_AIR = 19; + PRESET_ICON_BIKE = 20; + PRESET_ICON_EPIC = 21; + PRESET_ICON_INDOOR = 22; + PRESET_ICON_MOTOR = 23; + PRESET_ICON_MOUNTED = 24; + PRESET_ICON_OUTDOOR = 25; + PRESET_ICON_POV = 26; + PRESET_ICON_SELFIE = 27; + PRESET_ICON_SKATE = 28; + PRESET_ICON_SNOW = 29; + PRESET_ICON_TRAIL = 30; + PRESET_ICON_TRAVEL = 31; + PRESET_ICON_WATER = 32; + PRESET_ICON_LOOPING = 33; /* Reserved 34 - 50 for Custom presets */ - PRESET_ICON_MAX_VIDEO = 55; - PRESET_ICON_MAX_PHOTO = 56; - PRESET_ICON_MAX_TIMEWARP = 57; - PRESET_ICON_BASIC = 58; - PRESET_ICON_ULTRA_SLO_MO = 59; - PRESET_ICON_STANDARD_ENDURANCE = 60; - PRESET_ICON_ACTIVITY_ENDURANCE = 61; - PRESET_ICON_CINEMATIC_ENDURANCE = 62; - PRESET_ICON_SLOMO_ENDURANCE = 63; - PRESET_ICON_STATIONARY_1 = 64; - PRESET_ICON_STATIONARY_2 = 65; - PRESET_ICON_STATIONARY_3 = 66; - PRESET_ICON_STATIONARY_4 = 67; - PRESET_ICON_STAR_TRAIL = 76; - PRESET_ICON_LIGHT_PAINTING = 77; - PRESET_ICON_LIGHT_TRAIL = 78; - PRESET_ICON_FULL_FRAME = 79; - PRESET_ICON_TIMELAPSE_PHOTO = 1000; - PRESET_ICON_NIGHTLAPSE_PHOTO = 1001; + PRESET_ICON_MAX_VIDEO = 55; + PRESET_ICON_MAX_PHOTO = 56; + PRESET_ICON_MAX_TIMEWARP = 57; + PRESET_ICON_BASIC = 58; + PRESET_ICON_ULTRA_SLO_MO = 59; + PRESET_ICON_STANDARD_ENDURANCE = 60; + PRESET_ICON_ACTIVITY_ENDURANCE = 61; + PRESET_ICON_CINEMATIC_ENDURANCE = 62; + PRESET_ICON_SLOMO_ENDURANCE = 63; + PRESET_ICON_STATIONARY_1 = 64; + PRESET_ICON_STATIONARY_2 = 65; + PRESET_ICON_STATIONARY_3 = 66; + PRESET_ICON_STATIONARY_4 = 67; + PRESET_ICON_SIMPLE_SUPER_PHOTO = 70; + PRESET_ICON_SIMPLE_NIGHT_PHOTO = 71; + PRESET_ICON_HIGHEST_QUALITY_VIDEO = 73; + PRESET_ICON_STANDARD_QUALITY_VIDEO = 74; + PRESET_ICON_BASIC_QUALITY_VIDEO = 75; + PRESET_ICON_STAR_TRAIL = 76; + PRESET_ICON_LIGHT_PAINTING = 77; + PRESET_ICON_LIGHT_TRAIL = 78; + PRESET_ICON_FULL_FRAME = 79; + PRESET_ICON_EASY_MAX_VIDEO = 80; + PRESET_ICON_EASY_MAX_PHOTO = 81; + PRESET_ICON_EASY_MAX_TIMEWARP = 82; + PRESET_ICON_EASY_MAX_STAR_TRAIL = 83; + PRESET_ICON_EASY_MAX_LIGHT_PAINTING = 84; + PRESET_ICON_EASY_MAX_LIGHT_TRAIL = 85; + PRESET_ICON_MAX_STAR_TRAIL = 89; + PRESET_ICON_MAX_LIGHT_PAINTING = 90; + PRESET_ICON_MAX_LIGHT_TRAIL = 91; + PRESET_ICON_TIMELAPSE_PHOTO = 1000; + PRESET_ICON_NIGHTLAPSE_PHOTO = 1001; } enum EnumPresetTitle { - PRESET_TITLE_ACTIVITY = 0; - PRESET_TITLE_STANDARD = 1; - PRESET_TITLE_CINEMATIC = 2; - PRESET_TITLE_PHOTO = 3; - PRESET_TITLE_LIVE_BURST = 4; - PRESET_TITLE_BURST = 5; - PRESET_TITLE_NIGHT = 6; - PRESET_TITLE_TIME_WARP = 7; - PRESET_TITLE_TIME_LAPSE = 8; - PRESET_TITLE_NIGHT_LAPSE = 9; - PRESET_TITLE_VIDEO = 10; - PRESET_TITLE_SLOMO = 11; - PRESET_TITLE_360_VIDEO = 12; - PRESET_TITLE_PHOTO_2 = 13; - PRESET_TITLE_PANORAMA = 14; - PRESET_TITLE_360_PHOTO = 15; - PRESET_TITLE_TIME_WARP_2 = 16; - PRESET_TITLE_360_TIME_WARP = 17; - PRESET_TITLE_CUSTOM = 18; - PRESET_TITLE_AIR = 19; - PRESET_TITLE_BIKE = 20; - PRESET_TITLE_EPIC = 21; - PRESET_TITLE_INDOOR = 22; - PRESET_TITLE_MOTOR = 23; - PRESET_TITLE_MOUNTED = 24; - PRESET_TITLE_OUTDOOR = 25; - PRESET_TITLE_POV = 26; - PRESET_TITLE_SELFIE = 27; - PRESET_TITLE_SKATE = 28; - PRESET_TITLE_SNOW = 29; - PRESET_TITLE_TRAIL = 30; - PRESET_TITLE_TRAVEL = 31; - PRESET_TITLE_WATER = 32; - PRESET_TITLE_LOOPING = 33; + PRESET_TITLE_ACTIVITY = 0; + PRESET_TITLE_STANDARD = 1; + PRESET_TITLE_CINEMATIC = 2; + PRESET_TITLE_PHOTO = 3; + PRESET_TITLE_LIVE_BURST = 4; + PRESET_TITLE_BURST = 5; + PRESET_TITLE_NIGHT = 6; + PRESET_TITLE_TIME_WARP = 7; + PRESET_TITLE_TIME_LAPSE = 8; + PRESET_TITLE_NIGHT_LAPSE = 9; + PRESET_TITLE_VIDEO = 10; + PRESET_TITLE_SLOMO = 11; + PRESET_TITLE_360_VIDEO = 12; + PRESET_TITLE_PHOTO_2 = 13; + PRESET_TITLE_PANORAMA = 14; + PRESET_TITLE_360_PHOTO = 15; + PRESET_TITLE_TIME_WARP_2 = 16; + PRESET_TITLE_360_TIME_WARP = 17; + PRESET_TITLE_CUSTOM = 18; + PRESET_TITLE_AIR = 19; + PRESET_TITLE_BIKE = 20; + PRESET_TITLE_EPIC = 21; + PRESET_TITLE_INDOOR = 22; + PRESET_TITLE_MOTOR = 23; + PRESET_TITLE_MOUNTED = 24; + PRESET_TITLE_OUTDOOR = 25; + PRESET_TITLE_POV = 26; + PRESET_TITLE_SELFIE = 27; + PRESET_TITLE_SKATE = 28; + PRESET_TITLE_SNOW = 29; + PRESET_TITLE_TRAIL = 30; + PRESET_TITLE_TRAVEL = 31; + PRESET_TITLE_WATER = 32; + PRESET_TITLE_LOOPING = 33; /* Reserved 34 - 50 for custom presets. */ - PRESET_TITLE_360_TIMELAPSE = 51; - PRESET_TITLE_360_NIGHT_LAPSE = 52; - PRESET_TITLE_360_NIGHT_PHOTO = 53; - PRESET_TITLE_PANO_TIME_LAPSE = 54; - PRESET_TITLE_MAX_VIDEO = 55; - PRESET_TITLE_MAX_PHOTO = 56; - PRESET_TITLE_MAX_TIMEWARP = 57; - PRESET_TITLE_BASIC = 58; - PRESET_TITLE_ULTRA_SLO_MO = 59; - PRESET_TITLE_STANDARD_ENDURANCE = 60; - PRESET_TITLE_ACTIVITY_ENDURANCE = 61; - PRESET_TITLE_CINEMATIC_ENDURANCE = 62; - PRESET_TITLE_SLOMO_ENDURANCE = 63; - PRESET_TITLE_STATIONARY_1 = 64; - PRESET_TITLE_STATIONARY_2 = 65; - PRESET_TITLE_STATIONARY_3 = 66; - PRESET_TITLE_STATIONARY_4 = 67; - PRESET_TITLE_SIMPLE_VIDEO = 68; - PRESET_TITLE_SIMPLE_TIME_WARP = 69; - PRESET_TITLE_SIMPLE_SUPER_PHOTO = 70; - PRESET_TITLE_SIMPLE_NIGHT_PHOTO = 71; - PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE = 72; - PRESET_TITLE_HIGHEST_QUALITY = 73; - PRESET_TITLE_EXTENDED_BATTERY = 74; - PRESET_TITLE_LONGEST_BATTERY = 75; - PRESET_TITLE_STAR_TRAIL = 76; - PRESET_TITLE_LIGHT_PAINTING = 77; - PRESET_TITLE_LIGHT_TRAIL = 78; - PRESET_TITLE_FULL_FRAME = 79; - PRESET_TITLE_MAX_LENS_VIDEO = 80; - PRESET_TITLE_MAX_LENS_TIMEWARP = 81; + PRESET_TITLE_360_TIMELAPSE = 51; + PRESET_TITLE_360_NIGHT_LAPSE = 52; + PRESET_TITLE_360_NIGHT_PHOTO = 53; + PRESET_TITLE_PANO_TIME_LAPSE = 54; + PRESET_TITLE_MAX_VIDEO = 55; + PRESET_TITLE_MAX_PHOTO = 56; + PRESET_TITLE_MAX_TIMEWARP = 57; + PRESET_TITLE_BASIC = 58; + PRESET_TITLE_ULTRA_SLO_MO = 59; + PRESET_TITLE_STANDARD_ENDURANCE = 60; + PRESET_TITLE_ACTIVITY_ENDURANCE = 61; + PRESET_TITLE_CINEMATIC_ENDURANCE = 62; + PRESET_TITLE_SLOMO_ENDURANCE = 63; + PRESET_TITLE_STATIONARY_1 = 64; + PRESET_TITLE_STATIONARY_2 = 65; + PRESET_TITLE_STATIONARY_3 = 66; + PRESET_TITLE_STATIONARY_4 = 67; + PRESET_TITLE_SIMPLE_VIDEO = 68; + PRESET_TITLE_SIMPLE_TIME_WARP = 69; + PRESET_TITLE_SIMPLE_SUPER_PHOTO = 70; + PRESET_TITLE_SIMPLE_NIGHT_PHOTO = 71; + PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE = 72; + PRESET_TITLE_HIGHEST_QUALITY = 73; + PRESET_TITLE_EXTENDED_BATTERY = 74; + PRESET_TITLE_LONGEST_BATTERY = 75; + PRESET_TITLE_STAR_TRAIL = 76; + PRESET_TITLE_LIGHT_PAINTING = 77; + PRESET_TITLE_LIGHT_TRAIL = 78; + PRESET_TITLE_FULL_FRAME = 79; + PRESET_TITLE_MAX_LENS_VIDEO = 80; + PRESET_TITLE_MAX_LENS_TIMEWARP = 81; + PRESET_TITLE_STANDARD_QUALITY_VIDEO = 82; + PRESET_TITLE_BASIC_QUALITY_VIDEO = 83; + PRESET_TITLE_EASY_MAX_VIDEO = 84; + PRESET_TITLE_EASY_MAX_PHOTO = 85; + PRESET_TITLE_EASY_MAX_TIMEWARP = 86; + PRESET_TITLE_EASY_MAX_STAR_TRAIL = 87; + PRESET_TITLE_EASY_MAX_LIGHT_PAINTING = 88; + PRESET_TITLE_EASY_MAX_LIGHT_TRAIL = 89; + PRESET_TITLE_MAX_STAR_TRAIL = 90; + PRESET_TITLE_MAX_LIGHT_PAINTING = 91; + PRESET_TITLE_MAX_LIGHT_TRAIL = 92; + PRESET_TITLE_HIGHEST_QUALITY_VIDEO = 93; } message NotifyPresetStatus { diff --git a/tools/test_media_server/.dockerignore b/tools/test_media_server/.dockerignore new file mode 100644 index 00000000..1aa2afea --- /dev/null +++ b/tools/test_media_server/.dockerignore @@ -0,0 +1,10 @@ +# Start by ignoring everything +* + +# Explicitly unignore what we need for building +!nginx.conf +!hls.html +!entrypoint.sh +# TODO once we find which one works. Remove the other one +!cert_request.ext +!cert_request.ini diff --git a/tools/test_media_server/.gitignore b/tools/test_media_server/.gitignore new file mode 100644 index 00000000..9ea105fb --- /dev/null +++ b/tools/test_media_server/.gitignore @@ -0,0 +1,3 @@ +/.ssl/ +**/.vscode +**/*.log \ No newline at end of file diff --git a/tools/test_media_server/Dockerfile b/tools/test_media_server/Dockerfile new file mode 100644 index 00000000..e08f55ef --- /dev/null +++ b/tools/test_media_server/Dockerfile @@ -0,0 +1,120 @@ +# Dockerfile/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Fri Jun 9 22:45:24 UTC 2023 + +ARG ALPINE_VERSION=3.16 + +##### Building stage ##### +FROM alpine:${ALPINE_VERSION} as builder + +# Versions of nginx, rtmp-module and ffmpeg +ARG NGINX_VERSION=1.23.0 +ARG NGINX_RTMP_MODULE_VERSION=1.2.2 +ARG FFMPEG_VERSION=5.1 + +# Install dependencies +RUN apk update && \ + apk --no-cache add \ + bash build-base ca-certificates \ + openssl openssl-dev make \ + gcc libgcc libc-dev rtmpdump-dev \ + zlib-dev musl-dev pcre pcre-dev lame-dev \ + yasm pkgconf pkgconfig libtheora-dev \ + libvorbis-dev libvpx-dev freetype-dev \ + x264-dev x265-dev && \ + rm -rf /var/lib/apt/lists/* + +# Download nginx source +RUN mkdir -p /tmp/build && \ + cd /tmp/build && \ + wget https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz && \ + tar zxf nginx-${NGINX_VERSION}.tar.gz && \ + rm nginx-${NGINX_VERSION}.tar.gz + +# Download rtmp-module source +RUN cd /tmp/build && \ + wget https://github.com/arut/nginx-rtmp-module/archive/v${NGINX_RTMP_MODULE_VERSION}.tar.gz && \ + tar zxf v${NGINX_RTMP_MODULE_VERSION}.tar.gz && \ + rm v${NGINX_RTMP_MODULE_VERSION}.tar.gz + +# Build nginx with nginx-rtmp module +RUN cd /tmp/build/nginx-${NGINX_VERSION} && \ + ./configure \ + --sbin-path=/usr/local/sbin/nginx \ + --conf-path=/etc/nginx/nginx.conf \ + --error-log-path=/var/log/nginx/error.log \ + --http-log-path=/var/log/nginx/access.log \ +--pid-path=/var/run/nginx/nginx.pid \ + --lock-path=/var/lock/nginx.lock \ + --http-client-body-temp-path=/tmp/nginx-client-body \ + --with-http_ssl_module \ + --with-stream \ + --with-stream_ssl_module \ + --with-threads \ + --add-module=/tmp/build/nginx-rtmp-module-${NGINX_RTMP_MODULE_VERSION} && \ + make CFLAGS=-Wno-error -j $(getconf _NPROCESSORS_ONLN) && \ + make install + +# Download ffmpeg source +RUN cd /tmp/build && \ + wget http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz && \ + tar zxf ffmpeg-${FFMPEG_VERSION}.tar.gz && \ + rm ffmpeg-${FFMPEG_VERSION}.tar.gz + +# Build ffmpeg +RUN cd /tmp/build/ffmpeg-${FFMPEG_VERSION} && \ + ./configure \ + --enable-version3 \ + --enable-gpl \ + --enable-small \ + --enable-libx264 \ + --enable-libx265 \ + --enable-libvpx \ + --enable-libtheora \ + --enable-libvorbis \ + --enable-librtmp \ + --enable-postproc \ + --enable-swresample \ +--enable-libfreetype \ + --enable-libmp3lame \ + --disable-debug \ + --disable-doc \ + --disable-ffplay \ + --extra-libs="-lpthread -lm" && \ + make -j $(getconf _NPROCESSORS_ONLN) && \ + make install + +# Copy stats.xsl file to nginx html directory and clean build files +RUN cp /tmp/build/nginx-rtmp-module-${NGINX_RTMP_MODULE_VERSION}/stat.xsl /usr/local/nginx/html/stat.xsl && \ + rm -rf /tmp/build + +##### Building the final image ##### +FROM alpine:${ALPINE_VERSION} + +# Install dependencies +RUN apk update && \ + apk --no-cache add \ + bash ca-certificates openssl \ + pcre libtheora libvorbis lame libvpx \ + librtmp x264-dev x265-dev freetype htop && \ + rm -rf /var/lib/apt/lists/* + +# Copy files from build stage to final stage +COPY --from=builder /usr/local /usr/local +COPY --from=builder /etc/nginx /etc/nginx +COPY --from=builder /var/log/nginx /var/log/nginx +COPY --from=builder /var/lock /var/lock +COPY --from=builder /var/run/nginx /var/run/nginx + +# Forward logs to Docker +RUN ln -sf /dev/stdout /var/log/nginx/access.log && \ + ln -sf /dev/stderr /var/log/nginx/error.log + +COPY ./hls.html /usr/local/nginx/html/player.html +COPY ./nginx.conf /etc/nginx/nginx.conf +COPY ./cert_request.ext /cert_request.ext +COPY ./cert_request.ini /cert_request.ini + +# Copy run script to container +COPY entrypoint.sh /entrypoint.sh + +CMD ["bash", "/entrypoint.sh"] diff --git a/tools/test_media_server/README.md b/tools/test_media_server/README.md new file mode 100644 index 00000000..56460c95 --- /dev/null +++ b/tools/test_media_server/README.md @@ -0,0 +1,81 @@ +- [What is this?](#what-is-this) +- [Usage](#usage) + - [SSL Configuration](#ssl-configuration) +- [Test Stream](#test-stream) + - [RTMP](#rtmp) + - [RTMPS](#rtmps) +- [More Information](#more-information) + +# What is this? + +This is a test server that can used for isolated testing of the following streams: + +- RTMP +- RTMP + +# Usage + +Start server with: + +``` +SSL_DOMAIN="{IP_ADDRESS}" docker-compose up +``` + +where `IP_ADDRESS` is the IP Address of the device the server is running on. + +The server accepts communication on the following endpoints / ports: + +| Port | Endpoint | Communication Type | Example | +| ---- | --------- | ---------------------- | ----------------------------------- | +| 8080 | | HLS stream viewer | http://{IP_ADDRESS}:8080 | +| 8080 | stats | stream stats via HTTP | http://{IP_ADDRESS}:8080/stats | +| 8443 | stats | stream stats via HTTPS | https://{IP_ADDRESS}:8443/stats | +| 1935 | live/test | RTMP stream | rtmp://{IP_ADDRESS}:1935/live/test | +| 1936 | live/test | RTMPS stream | rtmps://{IP_ADDRESS}:1936/live/test | + +The general usage is: + +1. Start an RTMP(S) stream to one of the `live/test` endpoints. +2. View stream with at port 8080 (`http://{IP_ADDRESS}:8080`) +3. Optionally check stats at one of the stats endpoints + +## SSL Configuration + +A certificate and key are generated when docker image starts and placed in the `.ssl` directory. + +To use this certificate to communicate with the test server, install `rtmp.crt` as trusted root certificate. +The steps for this vary per OS. On Ubuntu for example: + +``` +$ sudo cp ./.ssl/self-signed/rtmp.crt /usr/local/share/ca-certificates +$ sudo update-ca-certificates +``` + +# Test Stream + +For sanity testing, an RTMP(S) stream can be sent to the test server as follows: + +## RTMP + +1. Test via: + +``` +docker run --rm jrottenberg/ffmpeg:4.1-alpine -r 30 -f lavfi -i testsrc -vf scale=1280:960 -vcodec libx264 -profile:v baseline -pix_fmt yuv420p -f flv rtmp://{IP_ADDRESS}:1935/live/test +``` + +2. View at: http://localhost:8080 + +## RTMPS + +1. Test via: + +``` +docker run --rm jrottenberg/ffmpeg:4.1-alpine -r 30 -f lavfi -i testsrc -vf scale=1280:960 -vcodec libx264 -profile:v baseline -pix_fmt yuv420p -f flv rtmps://{IP_ADDRESS}:1936/live/test +``` + +2. View at: http://localhost:8080 + +# More Information + +Per-stream stats can be viewed at `http://localhost:8080/stats`. The `.xml` provided by this endpoint could, +for example, be used as a programmatic way to verify a stream has started / stopped. diff --git a/tools/test_media_server/cert_request.ext b/tools/test_media_server/cert_request.ext new file mode 100644 index 00000000..22334571 --- /dev/null +++ b/tools/test_media_server/cert_request.ext @@ -0,0 +1,19 @@ +[ req ] +authorityKeyIdentifier = keyid,issuer +distinguished_name = req_distinguished_name +prompt = no +req_extensions = req_ext +[ req_distinguished_name ] +countryName = US +stateOrProvinceName = CA +localityName = San Mateo +organizationName = GoPro +organizationalUnitName = Server +commonName = GoPro Server +[ req_ext ] +basicConstraints = CA:FALSE +subjectAltName = @alt_names +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectKeyIdentifier = hash +[ alt_names ] +IP.1 = __IP_ADDR__ diff --git a/tools/test_media_server/cert_request.ini b/tools/test_media_server/cert_request.ini new file mode 100644 index 00000000..cc6bc633 --- /dev/null +++ b/tools/test_media_server/cert_request.ini @@ -0,0 +1,17 @@ +[req] +default_bits = 2048 +default_md = sha256 +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no +[req_distinguished_name] +C = US +ST = VA +L = SomeCity +O = MyCompany +OU = MyDivision +CN = __IP_ADDR__ +[v3_req] +subjectAltName = @alt_names +[alt_names] +IP.1 = __IP_ADDR__ \ No newline at end of file diff --git a/tools/test_media_server/docker-compose.yml b/tools/test_media_server/docker-compose.yml new file mode 100644 index 00000000..5cdb80d2 --- /dev/null +++ b/tools/test_media_server/docker-compose.yml @@ -0,0 +1,15 @@ +# docker-compose.yml/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Fri Jun 9 22:45:24 UTC 2023 + +services: + nginx-rtmps: + build: . + volumes: + - ./.ssl:/ssl + ports: + - 1935:1935 # RTMP + - 1936:1936 # RTMPS + - 8080:8080 # HTTP + - 8443:8443 # HTTPS + environment: + - SSL_DOMAIN=${SSL_DOMAIN} diff --git a/tools/test_media_server/entrypoint.sh b/tools/test_media_server/entrypoint.sh new file mode 100644 index 00000000..1dc3baf0 --- /dev/null +++ b/tools/test_media_server/entrypoint.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# entrypoint.sh/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Fri Jun 9 22:45:24 UTC 2023 + + +set -e + +function generate_root_cert +{ + echo -e "$(date +"%Y-%m-%d %H:%M:%S") INFO: Generating a Self Signing Certificate Authority..." + openssl genrsa -out /ssl/self_signed/RTMP-CA.key 2048 + openssl req -x509 -new -nodes -key /ssl/self_signed/RTMP-CA.key -sha256 -days 1825 -subj '/CN=RTMP-Server-CA' -out /ssl/self_signed/RTMP-CA.crt + cp -fv /ssl/self_signed/RTMP-CA.crt /ssl/ +} + +function generate_cert_from_root +{ + SUBJ="/CN=$SSL_DOMAIN" + echo -e "$(date +"%Y-%m-%d %H:%M:%S") INFO: The generated certificate will be valid for: $SSL_DOMAIN" + openssl genrsa -out /ssl/self_signed/rtmp.key 2048 + openssl req -new -key /ssl/self_signed/rtmp.key -subj $SUBJ -out /tmp/rtmp.csr + openssl x509 -req -in /tmp/rtmp.csr -CA /ssl/self_signed/RTMP-CA.crt -CAkey /ssl/self_signed/RTMP-CA.key -CAcreateserial -days 365 -sha256 -out /ssl/self_signed/rtmp.crt +} + +# This is richard's method. It does not work with Chrome. It apparently works with the camera but I haven't seen this work yet +# function generate_standalone_cert +# { +# # Using IP Address, build temporary request file from template +# cp /cert_request.ext /ssl/temp.ext +# sed -i "s/__IP_ADDR__/$SSL_DOMAIN/g" /ssl/temp.ext +# openssl genrsa -out /ssl/self_signed/rtmp.key 2048 +# openssl req -new -config /ssl/temp.ext -key /ssl/self_signed/rtmp.key -out /ssl/self_signed/rtmp.csr +# openssl x509 -req -days 300 -in /ssl/self_signed/rtmp.csr -extfile /ssl/temp.ext -extensions req_ext -signkey /ssl/self_signed/rtmp.key -out /ssl/self_signed/rtmp.crt +# rm /ssl/temp.ext +# # Print it to the console +# openssl x509 -in /ssl/self_signed/rtmp.crt -noout -text +# cat /ssl/self_signed/rtmp.crt +# } + +function generate_standalone_cert +{ + # Using IP Address, build temporary request file from template + cp /cert_request.ini /ssl/temp.ini + sed -i "s/__IP_ADDR__/$SSL_DOMAIN/g" /ssl/temp.ini + openssl req -new -nodes -x509 -days 365 -keyout /ssl/self_signed/rtmp.key -out /ssl/self_signed/rtmp.crt -config /ssl/temp.ini + # Print it to the console + openssl x509 -in /ssl/self_signed/rtmp.crt -noout -text + rm /ssl/temp.ini +} + +if [[ $SSL_DOMAIN == "" ]]; then + echo "You need to set the SSL_DOMAIN env variable" + exit 1 +fi + +# Create fresh ssl directory +rm -rf /ssl/* && mkdir -p /ssl/self_signed + +# This was the original way of generating a root certificate and then generating indivudal certs from this +# for each domain. +# generate_root_cert +# generate_cert_from_root + +generate_standalone_cert + +echo -e "$(date +"%Y-%m-%d %H:%M:%S") INFO: Starting Nginx!" +exec nginx -g "daemon off;" diff --git a/tools/test_media_server/hls.html b/tools/test_media_server/hls.html new file mode 100644 index 00000000..9531d222 --- /dev/null +++ b/tools/test_media_server/hls.html @@ -0,0 +1,59 @@ + + + + + HLS streaming + + + + + + + + + +

      HLS Player (using hls.js)

      + +
      +
      + +
      +
      + + + + diff --git a/tools/test_media_server/nginx.conf b/tools/test_media_server/nginx.conf new file mode 100644 index 00000000..278e9e45 --- /dev/null +++ b/tools/test_media_server/nginx.conf @@ -0,0 +1,122 @@ +worker_processes auto; + +error_log /dev/stdout info; + +events { + worker_connections 1024; +} + +# RTMPS configuration +stream { + upstream backend { + server 127.0.0.1:1935; + } + + server { + listen 1936 ssl; + proxy_pass backend; + ssl_certificate /ssl/self_signed/rtmp.crt; + ssl_certificate_key /ssl/self_signed/rtmp.key; + } +} + +# RTMP configuration +rtmp { + server { + listen 1935; + chunk_size 4000; + + # This application is to accept incoming stream + application live { + # Allows live input + live on; + # Drop Publishing connections that havnt sent any stream data for over 10 seconds + drop_idle_publisher 10s; + # Local push for built in players + push rtmp://localhost:1935/show; + } + + # This is the HLS application + application show { + # Allows live input from above application + live on; + # Disable consuming the stream from nginx as rtmp + deny play all; + + # Enable HTTP Live Streaming + hls on; + hls_fragment 3; + hls_playlist_length 20; + hls_path /mnt/hls/; # hls fragments path + } + } +} + +# HTTP configuration +http { + sendfile off; + tcp_nopush on; + access_log /dev/stdout combined; + directio 512; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + # HTTP server required to serve the player and HLS fragments + server { + listen 8080; + + listen 8443 ssl; + ssl_certificate /ssl/self_signed/rtmp.crt; + ssl_certificate_key /ssl/self_signed/rtmp.key; + + # Redirect requests for http://:8080/ to http://:8080/player + location = / { + # This is required to handle reverse proxy's like NginxProxyManager, otherwise the redirect will + # include this servers port in the redirect. + absolute_redirect off; + return 302 /player.html; + } + + # Serve HLS fragments + location /hls { + types { + application/vnd.apple.mpegurl m3u8; + video/mp2t ts; + } + + root /mnt; + + # Disable cache + add_header Cache-Control no-cache; + + # CORS setup + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length'; + + # allow CORS preflight requests + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + } + + # This URL provides RTMP statistics in XML + location /stat { + rtmp_stat all; + # Use stat.xsl stylesheet + rtmp_stat_stylesheet stat.xsl; + } + + location /stat.xsl { + # XML stylesheet to view RTMP stats. + root /usr/local/nginx/html; + } + } +} \ No newline at end of file