From 69924b489dba67414ae940541d3a9945ca17c9b9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 9 Apr 2024 19:54:04 +0000 Subject: [PATCH] Python SDK Refactor and Tutorial Updates * Rearchitect Python SDK to encapsulate all operations in Message objects * Share Protobuf building architecture between Python SDK and Tutorials * Add missing functionality from last 2 Hero 12 releases * Implement PUT support and add custom preset update operation * Add in new setting options * Refactor Tutorials to allow for non-TLV packets * Add Protobuf Tutorials * Add WiFi Station Mode Tutorial * Add COHN Tutotrial --- .admin/proto_build/.dockerignore | 4 + .admin/proto_build/Dockerfile | 15 + .admin/proto_build/README.md | 22 + .admin/proto_build/entrypoint.sh | 27 + .admin/proto_build/requirements.txt | 3 + .gitignore | 3 + Makefile | 26 +- demos/kotlin/tutorial/.gitignore | 1 + demos/kotlin/tutorial/.idea/compiler.xml | 2 +- demos/kotlin/tutorial/.idea/gradle.xml | 4 +- demos/kotlin/tutorial/.idea/misc.xml | 3 +- .../tutorials/Tutorial2SendBleCommands.kt | 4 +- .../Tutorial3ParseBleTlvResponses.kt | 246 +-- .../tutorials/Tutorial4BleQueries.kt | 199 +-- .../tutorials/Tutorial6SendWifiCommands.kt | 4 +- .../open_gopro_tutorial/util/Extensions.kt | 21 + .../open_gopro_tutorial/util/GoProData.kt | 6 +- .../open_gopro_tutorial/ExampleUnitTest.kt | 6 +- demos/kotlin/tutorial/build.gradle | 4 +- demos/kotlin/tutorial/gradle.properties | 3 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../sdk_wireless_camera_control/.gitignore | 6 +- .../sdk_wireless_camera_control/docs/api.rst | 15 +- .../sdk_wireless_camera_control/docs/conf.py | 1 + .../docs/usage.rst | 16 +- .../open_gopro/api/__init__.py | 9 +- .../open_gopro/api/ble_commands.py | 34 +- .../open_gopro/api/builders.py | 702 ++++---- .../open_gopro/api/http_commands.py | 78 +- .../open_gopro/api/params.py | 67 +- .../open_gopro/api/parsers.py | 58 +- .../open_gopro/communicator_interface.py | 341 ++-- .../open_gopro/demos/cohn.py | 13 +- .../demos/custom_preset_udpate_demo.py | 88 +- .../open_gopro/demos/gui/livestream.py | 2 +- .../open_gopro/demos/gui/webcam.py | 2 +- .../open_gopro/demos/log_battery.py | 1 + .../open_gopro/demos/photo.py | 40 +- .../open_gopro/demos/video.py | 9 +- .../open_gopro/gopro_base.py | 181 ++- .../open_gopro/gopro_wired.py | 75 +- .../open_gopro/gopro_wireless.py | 230 ++- .../open_gopro/logger.py | 5 +- .../open_gopro/models/general.py | 2 - .../open_gopro/models/media_list.py | 18 +- .../open_gopro/models/response.py | 50 +- .../open_gopro/parser_interface.py | 5 +- .../open_gopro/proto/cohn_pb2.py | 71 +- .../open_gopro/proto/cohn_pb2.pyi | 542 ++++--- .../open_gopro/proto/live_streaming_pb2.py | 63 +- .../open_gopro/proto/live_streaming_pb2.pyi | 902 ++++++----- .../open_gopro/proto/media_pb2.py | 43 +- .../open_gopro/proto/media_pb2.pyi | 138 +- .../proto/network_management_pb2.py | 95 +- .../proto/network_management_pb2.pyi | 1203 +++++++------- .../open_gopro/proto/preset_status_pb2.py | 75 +- .../open_gopro/proto/preset_status_pb2.pyi | 1441 ++++++++--------- .../proto/request_get_preset_status_pb2.py | 39 +- .../proto/request_get_preset_status_pb2.pyi | 170 +- .../open_gopro/proto/response_generic_pb2.py | 43 +- .../open_gopro/proto/response_generic_pb2.pyi | 174 +- .../proto/set_camera_control_status_pb2.py | 39 +- .../proto/set_camera_control_status_pb2.pyi | 135 +- .../open_gopro/proto/turbo_transfer_pb2.py | 35 +- .../open_gopro/proto/turbo_transfer_pb2.pyi | 70 +- .../open_gopro/types.py | 2 + .../open_gopro/util.py | 6 +- .../sdk_wireless_camera_control/poetry.lock | 987 +++++------ .../pyproject.toml | 42 +- .../tests/conftest.py | 74 +- .../tests/test_ble_commands.py | 27 +- .../tests/test_http_commands.py | 29 +- .../tests/test_logging.py | 248 +++ .../tests/test_parsers.py | 4 +- .../tests/test_wireless_gopro.py | 33 +- .../tools/build_protos.sh | 27 - demos/python/tutorial/.python-version | 2 +- demos/python/tutorial/README.md | 10 +- demos/python/tutorial/poetry.lock | 501 +++--- demos/python/tutorial/pyproject.toml | 50 +- demos/python/tutorial/tests/conftest.py | 8 +- demos/python/tutorial/tests/test_tutorials.py | 85 +- .../tutorial/tutorial_modules/__init__.py | 18 +- .../tutorial_1_connect_ble/ble_connect.py | 20 +- .../__init__.py | 2 +- .../ble_command_load_group.py | 30 +- .../ble_command_set_fps.py | 29 +- .../ble_command_set_resolution.py | 30 +- .../ble_command_set_shutter.py | 65 +- .../__init__.py | 2 + .../ble_command_get_hardware_info.py | 238 +++ .../ble_command_get_state.py | 157 -- .../ble_command_get_version.py | 56 +- .../tutorial_4_ble_queries/__init__.py | 2 + .../ble_query_poll_multiple_setting_values.py | 91 +- .../ble_query_poll_resolution_value.py | 117 +- ...query_register_resolution_value_updates.py | 97 +- .../tutorial_5_ble_protobuf/__init__.py | 2 + .../decipher_response.py | 348 ++++ .../tutorial_5_ble_protobuf/proto/__init__.py | 31 + .../tutorial_5_ble_protobuf/proto/cohn_pb2.py | 38 + .../proto/cohn_pb2.pyi | 279 ++++ .../proto/live_streaming_pb2.py | 34 + .../proto/live_streaming_pb2.pyi | 461 ++++++ .../proto/media_pb2.py | 24 + .../proto/media_pb2.pyi | 75 + .../proto/network_management_pb2.py | 50 + .../proto/network_management_pb2.pyi | 633 ++++++++ .../proto/preset_status_pb2.py | 40 + .../proto/preset_status_pb2.pyi | 702 ++++++++ .../proto/request_get_preset_status_pb2.py | 22 + .../proto/request_get_preset_status_pb2.pyi | 93 ++ .../proto/response_generic_pb2.py | 24 + .../proto/response_generic_pb2.pyi | 90 + .../proto/set_camera_control_status_pb2.py | 22 + .../proto/set_camera_control_status_pb2.pyi | 74 + .../proto/turbo_transfer_pb2.py | 20 + .../proto/turbo_transfer_pb2.pyi | 36 + .../protobuf_example.py | 32 + .../tutorial_5_ble_protobuf/set_turbo_mode.py | 114 ++ .../tutorial_6_connect_wifi/__init__.py | 2 + .../tutorial_6_connect_wifi/connect_as_sta.py | 258 +++ .../enable_wifi_ap.py} | 50 +- .../__init__.py | 0 .../wifi_command_get_media_list.py | 0 .../wifi_command_get_state.py | 0 .../wifi_command_load_group.py | 0 .../wifi_command_preview_stream.py | 0 .../wifi_command_set_resolution.py | 2 - .../wifi_command_set_shutter.py | 0 .../__init__.py | 0 .../wifi_media_download_file.py | 24 +- .../wifi_media_get_gpmf.py | 24 +- .../wifi_media_get_screennail.py | 24 +- .../wifi_media_get_thumbnail.py | 24 +- .../tutorial_9_cohn/__init__.py | 2 + .../tutorial_9_cohn/communicate_via_cohn.py | 48 + .../tutorial_9_cohn/provision_cohn.py | 292 ++++ docker-compose.yml | 16 +- docs/_config.yml | 2 +- docs/_data/navigation.yml | 10 +- .../tutorial_1_connect_ble/tutorial.md | 129 +- .../tutorial_2_send_ble_commands/tutorial.md | 265 +-- .../tutorial.md | 894 +++++----- .../tutorial_4_ble_queries/tutorial.md | 621 ++++--- .../tutorial_5_ble_protobuf/tutorial.md | 506 ++++++ .../tutorial_5_connect_wifi/tutorial.md | 370 ----- .../tutorial_6_connect_wifi/tutorial.md | 765 +++++++++ .../tutorial.md | 119 +- .../tutorial.md | 99 +- docs/_tutorials/tutorial_9_cohn/tutorial.md | 514 ++++++ docs/assets/css/custom.css | 4 + .../images/tutorials/complex_response_doc.png | Bin 0 -> 58495 bytes .../images/tutorials/kotlin/tutorial_6.png | Bin 15180 -> 0 bytes .../images/tutorials/kotlin/tutorial_7.png | Bin 16730 -> 15180 bytes .../images/tutorials/kotlin/tutorial_8.png | Bin 0 -> 16730 bytes docs/assets/images/tutorials/protobuf_doc.png | Bin 0 -> 61593 bytes .../images/tutorials/protobuf_message_doc.png | Bin 0 -> 28581 bytes docs/ble/_static/css/badge_only.css | 2 +- docs/ble/_static/documentation_options.js | 2 +- docs/ble/_static/jquery.js | 2 +- docs/ble/_static/js/badge_only.js | 2 +- .../ble/_static/js/html5shiv-printshiv.min.js | 2 +- docs/ble/_static/js/html5shiv.min.js | 2 +- docs/ble/_static/js/theme.js | 2 +- docs/ble/_static/language_data.js | 2 +- docs/ble/_static/pygments.css | 2 +- docs/ble/_static/sphinx_highlight.js | 2 +- docs/ble/_static/style.css | 2 +- docs/ble/features/access_points.html | 65 +- docs/ble/features/cohn.html | 29 +- docs/ble/features/control.html | 79 +- docs/ble/features/hilights.html | 21 +- docs/ble/features/live_streaming.html | 27 +- docs/ble/features/presets.html | 59 +- docs/ble/features/query.html | 201 +-- docs/ble/features/settings.html | 21 +- docs/ble/features/statuses.html | 198 ++- docs/ble/genindex.html | 13 +- docs/ble/index.html | 13 +- docs/ble/operation-operation_index.html | 1278 ++++----------- docs/ble/protocol.html | 13 +- docs/ble/protocol/ble_setup.html | 13 +- docs/ble/protocol/data_protocol.html | 13 +- docs/ble/protocol/id_tables.html | 395 +++-- docs/ble/protocol/protobuf.html | 13 +- docs/ble/protocol/state_management.html | 23 +- docs/ble/search.html | 13 +- docs/ble/searchindex.js | 4 +- docs/http.html | 856 ++++++++-- docs/index.md | 8 +- docs/tutorials.md | 5 + protobuf/cohn.proto | 2 +- protobuf/live_streaming.proto | 2 +- protobuf/media.proto | 2 +- protobuf/network_management.proto | 2 +- protobuf/preset_status.proto | 2 +- protobuf/request_get_preset_status.proto | 2 +- protobuf/response_generic.proto | 2 +- protobuf/set_camera_control_status.proto | 2 +- protobuf/turbo_transfer.proto | 2 +- 201 files changed, 14593 insertions(+), 8135 deletions(-) create mode 100644 .admin/proto_build/.dockerignore create mode 100644 .admin/proto_build/Dockerfile create mode 100644 .admin/proto_build/README.md create mode 100644 .admin/proto_build/entrypoint.sh create mode 100644 .admin/proto_build/requirements.txt create mode 100644 demos/python/sdk_wireless_camera_control/tests/test_logging.py delete mode 100755 demos/python/sdk_wireless_camera_control/tools/build_protos.sh rename demos/python/tutorial/tutorial_modules/{tutorial_5_connect_wifi => tutorial_2_send_ble_commands}/__init__.py (58%) create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/__init__.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_hardware_info.py delete mode 100644 demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_state.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/__init__.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/__init__.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/decipher_response.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/__init__.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/cohn_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/cohn_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/live_streaming_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/live_streaming_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/media_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/media_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/network_management_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/network_management_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/preset_status_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/preset_status_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/request_get_preset_status_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/request_get_preset_status_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/response_generic_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/response_generic_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/set_camera_control_status_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/set_camera_control_status_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/turbo_transfer_pb2.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/turbo_transfer_pb2.pyi create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/protobuf_example.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/set_turbo_mode.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/__init__.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/connect_as_sta.py rename demos/python/tutorial/tutorial_modules/{tutorial_5_connect_wifi/wifi_enable.py => tutorial_6_connect_wifi/enable_wifi_ap.py} (63%) rename demos/python/tutorial/tutorial_modules/{tutorial_6_send_wifi_commands => tutorial_7_send_wifi_commands}/__init__.py (100%) rename demos/python/tutorial/tutorial_modules/{tutorial_6_send_wifi_commands => tutorial_7_send_wifi_commands}/wifi_command_get_media_list.py (100%) rename demos/python/tutorial/tutorial_modules/{tutorial_6_send_wifi_commands => tutorial_7_send_wifi_commands}/wifi_command_get_state.py (100%) rename demos/python/tutorial/tutorial_modules/{tutorial_6_send_wifi_commands => tutorial_7_send_wifi_commands}/wifi_command_load_group.py (100%) rename demos/python/tutorial/tutorial_modules/{tutorial_6_send_wifi_commands => tutorial_7_send_wifi_commands}/wifi_command_preview_stream.py (100%) rename demos/python/tutorial/tutorial_modules/{tutorial_6_send_wifi_commands => tutorial_7_send_wifi_commands}/wifi_command_set_resolution.py (97%) rename demos/python/tutorial/tutorial_modules/{tutorial_6_send_wifi_commands => tutorial_7_send_wifi_commands}/wifi_command_set_shutter.py (100%) rename demos/python/tutorial/tutorial_modules/{tutorial_7_camera_media_list => tutorial_8_camera_media_list}/__init__.py (100%) rename demos/python/tutorial/tutorial_modules/{tutorial_7_camera_media_list => tutorial_8_camera_media_list}/wifi_media_download_file.py (69%) rename demos/python/tutorial/tutorial_modules/{tutorial_7_camera_media_list => tutorial_8_camera_media_list}/wifi_media_get_gpmf.py (68%) rename demos/python/tutorial/tutorial_modules/{tutorial_7_camera_media_list => tutorial_8_camera_media_list}/wifi_media_get_screennail.py (68%) rename demos/python/tutorial/tutorial_modules/{tutorial_7_camera_media_list => tutorial_8_camera_media_list}/wifi_media_get_thumbnail.py (68%) create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_9_cohn/__init__.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_9_cohn/communicate_via_cohn.py create mode 100644 demos/python/tutorial/tutorial_modules/tutorial_9_cohn/provision_cohn.py create mode 100644 docs/_tutorials/tutorial_5_ble_protobuf/tutorial.md delete mode 100644 docs/_tutorials/tutorial_5_connect_wifi/tutorial.md create mode 100644 docs/_tutorials/tutorial_6_connect_wifi/tutorial.md rename docs/_tutorials/{tutorial_6_send_wifi_commands => tutorial_7_send_wifi_commands}/tutorial.md (87%) rename docs/_tutorials/{tutorial_7_camera_media_list => tutorial_8_camera_media_list}/tutorial.md (85%) create mode 100644 docs/_tutorials/tutorial_9_cohn/tutorial.md create mode 100644 docs/assets/images/tutorials/complex_response_doc.png delete mode 100644 docs/assets/images/tutorials/kotlin/tutorial_6.png create mode 100644 docs/assets/images/tutorials/kotlin/tutorial_8.png create mode 100644 docs/assets/images/tutorials/protobuf_doc.png create mode 100644 docs/assets/images/tutorials/protobuf_message_doc.png diff --git a/.admin/proto_build/.dockerignore b/.admin/proto_build/.dockerignore new file mode 100644 index 00000000..6af6d45b --- /dev/null +++ b/.admin/proto_build/.dockerignore @@ -0,0 +1,4 @@ +** + +!entrypoint.sh +!requirements.txt diff --git a/.admin/proto_build/Dockerfile b/.admin/proto_build/Dockerfile new file mode 100644 index 00000000..009fd57b --- /dev/null +++ b/.admin/proto_build/Dockerfile @@ -0,0 +1,15 @@ +# Dockerfile/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:54 UTC 2024 + +FROM python:3.11-bookworm + +RUN apt-get update && apt-get install -y \ + protobuf-compiler \ + bash + +COPY . /workdir +RUN pip install -r /workdir/requirements.txt + +RUN chmod +x /workdir/entrypoint.sh + +ENTRYPOINT [ "/workdir/entrypoint.sh" ] diff --git a/.admin/proto_build/README.md b/.admin/proto_build/README.md new file mode 100644 index 00000000..28fa9574 --- /dev/null +++ b/.admin/proto_build/README.md @@ -0,0 +1,22 @@ +# Protobuf Builder + +This is a Docker image to build the [Protobuf](../../protobuf/) files in this repo. It currently only build Python +output but should be used in the future for other languages. + +## Usage + +It is intended to be used via the [docker compose configuration](../../docker-compose.yml). + +First build: + +```shell +docker compose build proto-build +``` + +Then build the protobuf files with: + +```shell +docker compose run --rm proto-build +``` + +The output files will be placed in the [build directory](../../.build/protobuf). diff --git a/.admin/proto_build/entrypoint.sh b/.admin/proto_build/entrypoint.sh new file mode 100644 index 00000000..d772529a --- /dev/null +++ b/.admin/proto_build/entrypoint.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# entrypoint.sh/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:54 UTC 2024 + + +PROTO_SRC_DIR=/proto_in +PROTO_PYTHON_OUT_DIR=/proto_python_out + +rm -rf $PROTO_PYTHON_OUT_DIR && mkdir -p $PROTO_PYTHON_OUT_DIR + +echo +echo "Building protobuf python files and stubs from .proto source files..." +pushd $PROTO_SRC_DIR +protoc --include_imports --descriptor_set_out=$PROTO_PYTHON_OUT_DIR/descriptors --python_out=$PROTO_PYTHON_OUT_DIR --mypy_out=$PROTO_PYTHON_OUT_DIR * +popd + +pushd $PROTO_PYTHON_OUT_DIR +echo +echo "Converting relative imports to absolute..." +protol -o . --in-place raw descriptors +rm descriptors + +# Format generated files +echo +echo "Formatting..." +black . +popd diff --git a/.admin/proto_build/requirements.txt b/.admin/proto_build/requirements.txt new file mode 100644 index 00000000..49d735aa --- /dev/null +++ b/.admin/proto_build/requirements.txt @@ -0,0 +1,3 @@ +mypy-protobuf==3.5.0 +protoletariat==3.2.19 +black \ No newline at end of file diff --git a/.gitignore b/.gitignore index 64db009c..fdf3c187 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +**/*.crt +/.build + /.env **.log diff --git a/Makefile b/Makefile index 0eeb5f50..c148d8ba 100644 --- a/Makefile +++ b/Makefile @@ -17,38 +17,50 @@ help: ## Display this help which is generated from Make goal comments # Docker images are currently not public. So we build if pull fails for the local use case. .PHONY: docker-setup docker-setup: - -@docker-compose pull || docker-compose build + -@docker compose pull jekyll plant-uml || docker compose build jekyll plant-uml .PHONY: docker-kill docker-kill: - -@docker kill jekyll plant_uml > /dev/null 2>&1 + -@docker kill jekyll plant-uml > /dev/null 2>&1 .PHONY: clean clean: ## Clean cached jekyll files @echo "🧼 Cleaning jekyll artifacts..." - -@docker-compose down > /dev/null 2>&1 + -@docker compose down > /dev/null 2>&1 @rm -rf docs/_site docs/.jekyll-cache docs/.jekyll-metadata .PHONY: serve serve: docker-kill docker-setup serve: ## Serve site locally @echo COMMAND="-u http://localhost:4998/ -b \"\" -p 4998 serve" > .env - @docker-compose up + @docker compose up @rm -rf .env .PHONY: build build: docker-setup build: ## Build site for deployment @echo COMMAND=\"-u ${BUILD_HOST_URL} -b ${BUILD_BASE_URL} build\" > .env - @docker-compose up --abort-on-container-exit + @docker compose up --abort-on-container-exit @rm -rf .env +PROTO_BUILD_DIR=.build/protobuf/python/* +PYTHON_TUTORIAL_PROTO_DIR=demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto +PYTHON_SDK_PROTO_DIR=demos/python/sdk_wireless_camera_control/open_gopro/proto + +.PHONY: protos +protos: ## Build generated code from protobuf files + @docker compose run --build --rm proto-build + @rm -rf ${PYTHON_TUTORIAL_PROTO_DIR}/*pb2.py* && mkdir -p ${PYTHON_TUTORIAL_PROTO_DIR} + @cp ${PROTO_BUILD_DIR} ${PYTHON_TUTORIAL_PROTO_DIR} + @rm -rf ${PYTHON_SDK_PROTO_DIR}/*pb2.py* && mkdir -p ${PYTHON_SDK_PROTO_DIR} + @cp ${PROTO_BUILD_DIR} ${PYTHON_SDK_PROTO_DIR} + .PHONY: tests tests: docker-setup clean tests: ## Serve, then run link checker. Times out after 5 minutes. - -@docker-compose pull linkchecker || docker-compose build linkchecker + -@docker compose pull linkchecker || docker compose build linkchecker @echo COMMAND="-u http://jekyll:4998/ -b \"\" -p 4998 serve" > .env - @docker-compose --profile test up --abort-on-container-exit + @docker compose --profile test up --abort-on-container-exit @rm -rf .env .PHONY: copyright diff --git a/demos/kotlin/tutorial/.gitignore b/demos/kotlin/tutorial/.gitignore index aa724b77..1faf7e30 100644 --- a/demos/kotlin/tutorial/.gitignore +++ b/demos/kotlin/tutorial/.gitignore @@ -1,3 +1,4 @@ +/.idea/ *.iml .gradle /local.properties diff --git a/demos/kotlin/tutorial/.idea/compiler.xml b/demos/kotlin/tutorial/.idea/compiler.xml index fb7f4a8a..b589d56e 100644 --- a/demos/kotlin/tutorial/.idea/compiler.xml +++ b/demos/kotlin/tutorial/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/demos/kotlin/tutorial/.idea/gradle.xml b/demos/kotlin/tutorial/.idea/gradle.xml index a2d7c213..0897082f 100644 --- a/demos/kotlin/tutorial/.idea/gradle.xml +++ b/demos/kotlin/tutorial/.idea/gradle.xml @@ -4,15 +4,15 @@ diff --git a/demos/kotlin/tutorial/.idea/misc.xml b/demos/kotlin/tutorial/.idea/misc.xml index bdd92780..8978d23d 100644 --- a/demos/kotlin/tutorial/.idea/misc.xml +++ b/demos/kotlin/tutorial/.idea/misc.xml @@ -1,7 +1,6 @@ - - + diff --git a/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial2SendBleCommands.kt b/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial2SendBleCommands.kt index 3a398581..7cd3295c 100644 --- a/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial2SendBleCommands.kt +++ b/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial2SendBleCommands.kt @@ -41,7 +41,7 @@ class Tutorial2SendBleCommands(number: Int, name: String, prerequisites: List { +sealed class Response(val uuid: GoProUUID) { private enum class Header(val value: UByte) { GENERAL(0b00U), EXT_13(0b01U), EXT_16(0b10U), RESERVED(0b11U); @@ -40,18 +45,10 @@ sealed class Response { } private var bytesRemaining = 0 - protected var packet = ubyteArrayOf() - abstract val data: T - var id by notNull() - protected set - var status by notNull() + var rawBytes = ubyteArrayOf() protected set val isReceived get() = bytesRemaining == 0 - var isParsed = false - protected set - - override fun toString() = prettyJson.encodeToString(data.toJsonElement()) fun accumulate(data: UByteArray) { var buf = data @@ -60,72 +57,65 @@ sealed class Response { buf = buf.drop(1).toUByteArray() // Pop the header byte } else { // This is a new packet so start with empty array - packet = ubyteArrayOf() + rawBytes = ubyteArrayOf() when (Header.fromValue((buf.first() and Mask.Header.value).toInt() shr 5)) { Header.GENERAL -> { bytesRemaining = buf[0].and(Mask.GenLength.value).toInt() buf = buf.drop(1).toUByteArray() } + Header.EXT_13 -> { bytesRemaining = ((buf[0].and(Mask.Ext13Byte0.value) .toLong() shl 8) or buf[1].toLong()).toInt() buf = buf.drop(2).toUByteArray() } + Header.EXT_16 -> { bytesRemaining = ((buf[1].toLong() shl 8) or buf[2].toLong()).toInt() buf = buf.drop(3).toUByteArray() } + Header.RESERVED -> { throw Exception("Unexpected RESERVED header") } } } // Accumulate the payload now that headers are handled and dropped - packet += buf + rawBytes += buf bytesRemaining -= buf.size - Timber.i("Received packet of length ${buf.size}. $bytesRemaining bytes remaining") + Timber.d("Received packet of length ${buf.size}. $bytesRemaining bytes remaining") if (bytesRemaining < 0) { throw Exception("Unrecoverable parsing error. Received too much data.") } } - abstract fun parse() - - class Complex : Response>() { - override val data: MutableList = mutableListOf() + open class Tlv(uuid: GoProUUID) : Response(uuid) { + var payload = ubyteArrayOf() + private set + var id by notNull() + private set + var status by notNull() + private set - override fun parse() { + open fun parse() { require(isReceived) - // Parse header bytes - id = packet[0].toInt() - status = packet[1].toInt() - var buf = packet.drop(2) - // Parse remaining packet - while (buf.isNotEmpty()) { - // Get each parameter's ID and length - val paramLen = buf[0].toInt() - buf = buf.drop(1) - // Get the parameter's value - val paramVal = buf.take(paramLen) - // Store in data list - data += paramVal.toUByteArray() - // Advance the buffer for continued parsing - buf = buf.drop(paramLen) - } - isParsed = true + id = rawBytes[0].toInt() + status = rawBytes[1].toInt() + // Store remainder as payload for further parsing later + payload = rawBytes.drop(2).toUByteArray() } + + override fun toString(): String = "ID: $id, Status: $status, Payload: ${payload.toHexString()}" } - class Query : Response>() { - override val data: MutableMap = mutableMapOf() + class Query(uuid: GoProUUID) : Tlv(uuid) { + val data: MutableMap = mutableMapOf() + override fun toString() = prettyJson.encodeToString(data.toJsonElement()) override fun parse() { - require(isReceived) - id = packet[0].toInt() - status = packet[1].toInt() - // Parse remaining packet - var buf = packet.drop(2) + super.parse() + var buf = payload.toList() while (buf.isNotEmpty()) { // Get each parameter's ID and length val paramId = buf[0] @@ -138,57 +128,124 @@ sealed class Response { // Advance the buffer for continued parsing buf = buf.drop(paramLen) } - isParsed = true } } + class Unknown(uuid: GoProUUID): Response(uuid) {} + companion object { - fun fromUuid(uuid: GoProUUID): Response<*> = - when (uuid) { - GoProUUID.CQ_COMMAND_RSP -> Complex() - GoProUUID.CQ_QUERY_RSP -> Query() - else -> throw Exception("Not supported") + fun muxByUuid(uuid: GoProUUID) : Response { + return when (uuid) { + GoProUUID.CQ_SETTING_RSP, GoProUUID.CQ_COMMAND_RSP -> Tlv(uuid) + GoProUUID.CQ_QUERY_RSP -> Query(uuid) + else -> Unknown(uuid) } + } } +} +@OptIn(ExperimentalUnsignedTypes::class) +data class OpenGoProVersion(val minor: Int, val major: Int) { + companion object { + fun fromBytes(data: UByteArray): OpenGoProVersion { + var buf = data.toUByteArray() + val majorLen = buf[0].toInt() + buf = buf.drop(1).toUByteArray() + val major = buf.take(majorLen).toInt() + buf = buf.drop(1).toUByteArray() + val minorLen = buf[0].toInt() + buf = buf.drop(1).toUByteArray() + val minor = buf.take(minorLen).toInt() + return OpenGoProVersion(minor, major) + } + } } @OptIn(ExperimentalUnsignedTypes::class) -class Tutorial3ParseBleTlvResponses(number: Int, name: String, prerequisites: List) : - Tutorial(number, name, prerequisites) { - private val receivedResponse: Channel> = Channel() - private var response: Response<*>? = null +data class HardwareInfo( + val modelNumber: Int, + val modelName: String, + val firmwareVersion: String, + val serialNumber: String, + val apSsid: String, + val apMacAddress: String +) { + companion object { + fun fromBytes(data: UByteArray): HardwareInfo { + // Parse header bytes + var buf = data.toUByteArray() + // Get model number + val modelNumLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val model = buf.take(modelNumLength).toInt() + buf = buf.drop(modelNumLength).toUByteArray() + // Get model name + val modelNameLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val modelName = buf.take(modelNameLength).decodeToString() + buf = buf.drop(modelNameLength).toUByteArray() + // Advance past deprecated bytes + val deprecatedLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + buf = buf.drop(deprecatedLength).toUByteArray() + // Get firmware version + val firmwareLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val firmware = buf.take(firmwareLength).decodeToString() + buf = buf.drop(firmwareLength).toUByteArray() + // Get serial number + val serialLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val serial = buf.take(serialLength).decodeToString() + buf = buf.drop(serialLength).toUByteArray() + // Get AP SSID + val ssidLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val ssid = buf.take(ssidLength).decodeToString() + buf = buf.drop(ssidLength).toUByteArray() + // Get MAC Address + val macLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val mac = buf.take(macLength).decodeToString() - @OptIn(ExperimentalUnsignedTypes::class) - private fun tlvResponseNotificationHandler(characteristic: UUID, data: UByteArray) { - GoProUUID.fromUuid(characteristic)?.let { uuid -> - // If response is currently empty, create a new one - response = response ?: Response.fromUuid(uuid) + return HardwareInfo(model, modelName, firmware, serial, ssid, mac) } - ?: return // We don't care about non-GoPro characteristics (i.e. the BT Core Battery service) - - Timber.d("Received response on $characteristic: ${data.toHexString()}") + } +} - response?.let { rsp -> - rsp.accumulate(data) - if (rsp.isReceived) { - rsp.parse() +@OptIn(ExperimentalUnsignedTypes::class) +class Tutorial3ParseBleTlvResponses( + number: Int, + name: String, + prerequisites: List +) : + Tutorial(number, name, prerequisites) { + private val receivedResponses: Channel = Channel() + private val responsesByUuid = GoProUUID.mapByUuid { Response.muxByUuid(it) } - // If the response has success status... - if (rsp.status == 0) Timber.i("Received the expected successful response") - else Timber.i("Received unexpected response") + @OptIn(ExperimentalUnsignedTypes::class) + private fun notificationHandler(characteristic: UUID, data: UByteArray) { + // Get the UUID (assuming it is a GoPro UUID) + val uuid = GoProUUID.fromUuid(characteristic) ?: return + Timber.d("Received response on $uuid") - // Notify the command sender the the procedure is complete - response = null // Clear for next command - CoroutineScope(Dispatchers.IO).launch { receivedResponse.send(rsp) } + responsesByUuid[uuid]?.let { response -> + response.accumulate(data) + if (response.isReceived) { + if (uuid == GoProUUID.CQ_COMMAND_RSP) { + CoroutineScope(Dispatchers.IO).launch { receivedResponses.send(response) } + } else { + Timber.e("Unexpected Response") + } + responsesByUuid[uuid] = Response.muxByUuid(uuid) } - } ?: throw Exception("This should be impossible") + } } @OptIn(ExperimentalUnsignedTypes::class) private val bleListeners by lazy { BleEventListener().apply { - onNotification = ::tlvResponseNotificationHandler + onNotification = ::notificationHandler } } @@ -203,26 +260,27 @@ class Tutorial3ParseBleTlvResponses(number: Int, name: String, prerequisites: Li ble.registerListener(goproAddress, bleListeners) Timber.i("Getting the Open GoPro version") - val getVersion = ubyteArrayOf(0x01U, 0x51U) - ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, getVersion) - val version = receivedResponse.receive() as Response.Complex // Wait to receive response - val major = version.data[0].first().toInt() - val minor = version.data[1].first().toInt() - Timber.i("Got the Open GoPro version successfully: $major.$minor") - - Timber.i("Getting the camera's settings") - val getCameraSettings = ubyteArrayOf(0x01U, 0x12U) - ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, getCameraSettings) - val settings = receivedResponse.receive() - Timber.i("Got the camera's settings successfully") - Timber.i(settings.toString()) - - Timber.i("Getting the camera's statuses") - val getCameraStatuses = ubyteArrayOf(0x01U, 0x13U) - ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, getCameraStatuses) - val statuses = receivedResponse.receive() - Timber.i("Got the camera's statuses successfully") - Timber.i(statuses.toString()) + val versionRequest = ubyteArrayOf(0x01U, 0x51U) + ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, versionRequest) + // Wait to receive response the parse it + var tlvResponse = receivedResponses.receive() as Response.Tlv + tlvResponse.parse() + Timber.i("Received response: $tlvResponse") + val version = OpenGoProVersion.fromBytes(tlvResponse.payload) + Timber.i("Got the Open GoPro version successfully: ${version.major}.${version.minor}") + + Timber.i("Getting the Hardware Info") + val hardwareInfoRequest = ubyteArrayOf(0x01U, 0x3CU) + ble.writeCharacteristic( + goproAddress, + GoProUUID.CQ_COMMAND.uuid, + hardwareInfoRequest + ) + // Wait to receive response the parse it + tlvResponse = receivedResponses.receive() as Response.Tlv + tlvResponse.parse() + val hardwareInfo = HardwareInfo.fromBytes(tlvResponse.payload) + Timber.i("Got the Hardware Info successfully: $hardwareInfo") // Other tutorials will use their own notification handler so unregister ours now ble.unregisterListener(bleListeners) diff --git a/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial4BleQueries.kt b/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial4BleQueries.kt index 2c47d009..c2f47bc5 100644 --- a/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial4BleQueries.kt +++ b/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial4BleQueries.kt @@ -8,14 +8,14 @@ import com.example.open_gopro_tutorial.AppContainer import com.example.open_gopro_tutorial.DataStore import com.example.open_gopro_tutorial.network.BleEventListener import com.example.open_gopro_tutorial.network.Bluetooth -import com.example.open_gopro_tutorial.util.* +import com.example.open_gopro_tutorial.util.GoProUUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import timber.log.Timber import java.io.File -import java.util.* +import java.util.UUID private const val RESOLUTION_ID: UByte = 2U @@ -26,6 +26,7 @@ class Tutorial4BleQueries(number: Int, name: String, prerequisites: List = Channel() - private var response: Response.Query? = null private lateinit var resolution: Resolution - - @RequiresPermission(allOf = ["android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT"]) - private suspend fun performPollingTutorial(ble: Bluetooth, goproAddress: String) { - @OptIn(ExperimentalUnsignedTypes::class) - fun resolutionPollingNotificationHandler(characteristic: UUID, data: UByteArray) { - GoProUUID.fromUuid(characteristic)?.let { - // If response is currently empty, create a new one - response = - response ?: Response.Query() // We're only handling queries in this tutorial - } - ?: return // We don't care about non-GoPro characteristics (i.e. the BT Core Battery service) - - Timber.d("Received response on $characteristic: ${data.toHexString()}") - - response?.let { rsp -> - rsp.accumulate(data) - if (rsp.isReceived) { - rsp.parse() - - // If this is a query response, it must contain a resolution value - if (characteristic == GoProUUID.CQ_QUERY_RSP.uuid) { - Timber.i("Received resolution query response") - } - // If this is a setting response, it will just show the status - else if (characteristic == GoProUUID.CQ_SETTING_RSP.uuid) { - Timber.i("Command sent successfully") + private lateinit var receivedResponses: Channel + private val responsesByUuid = GoProUUID.mapByUuid { Response.muxByUuid(it) } + + @OptIn(ExperimentalUnsignedTypes::class) + private fun notificationHandler(characteristic: UUID, data: UByteArray) { + // Get the UUID (assuming it is a GoPro UUID) + val uuid = GoProUUID.fromUuid(characteristic) ?: return + Timber.d("Received response on $uuid") + + responsesByUuid[uuid]?.let { response -> + response.accumulate(data) + if (response.isReceived) { + when (uuid) { + GoProUUID.CQ_QUERY_RSP -> { + Timber.d("Received Query Response") + CoroutineScope(Dispatchers.IO).launch { + receivedResponses.send( + response + ) + } } - // Anything else is unexpected. This shouldn't happen - else { - Timber.i("Received unexpected response") + + GoProUUID.CQ_SETTING_RSP -> { + CoroutineScope(Dispatchers.IO).launch { + receivedResponses.send( + response + ) + } + Timber.d("Received set setting response.") } - // Notify the command sender the the procedure is complete - response = null // Clear for next command - CoroutineScope(Dispatchers.IO).launch { receivedResponse.send(rsp) } + else -> Timber.e("Unexpected Response") } + responsesByUuid[uuid] = Response.muxByUuid(uuid) } } + } - @OptIn(ExperimentalUnsignedTypes::class) - val bleListeners by lazy { - BleEventListener().apply { - onNotification = ::resolutionPollingNotificationHandler - } + @OptIn(ExperimentalUnsignedTypes::class) + val bleListeners by lazy { + BleEventListener().apply { + onNotification = ::notificationHandler } + } - // Register our notification handler callback for characteristic change updates - ble.registerListener(goproAddress, bleListeners) + @RequiresPermission(allOf = ["android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT"]) + private suspend fun performPollingTutorial(ble: Bluetooth, goproAddress: String) { + // Flush the channel. Just create a new one. + receivedResponses = Channel() // Write to query BleUUID to poll the current resolution Timber.i("Polling the current resolution") val pollResolution = ubyteArrayOf(0x02U, 0x12U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) - resolution = Resolution.fromValue( - receivedResponse.receive().data.getValue(RESOLUTION_ID).first() - ) + val queryResponse = (receivedResponses.receive() as Response.Query).apply { parse() } + resolution = Resolution.fromValue(queryResponse.data.getValue(RESOLUTION_ID).first()) Timber.i("Camera resolution is $resolution") // Write to command request BleUUID to change the video resolution (either to 1080 or 2.7K) - val newResolution = + val targetResolution = if (resolution == Resolution.RES_2_7K) Resolution.RES_1080 else Resolution.RES_2_7K - Timber.i("Changing the resolution to $newResolution") - val setResolution = ubyteArrayOf(0x03U, RESOLUTION_ID, 0x01U, newResolution.value) + Timber.i("Changing the resolution to $targetResolution") + val setResolution = ubyteArrayOf(0x03U, RESOLUTION_ID, 0x01U, targetResolution.value) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_SETTING.uuid, setResolution) - val setResolutionResponse = receivedResponse.receive() + val setResolutionResponse = (receivedResponses.receive() as Response.Tlv).apply { parse() } if (setResolutionResponse.status == 0) { Timber.i("Resolution successfully changed") } else { - Timber.e("Failed to set resolution") + Timber.e("Failed to set resolution to to $targetResolution. Ensure camera is in a valid state to allow this resolution.") + return } // Now let's poll until an update occurs Timber.i("Polling the resolution until it changes") - while (resolution != newResolution) { + while (resolution != targetResolution) { ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) - resolution = Resolution.fromValue( - receivedResponse.receive().data.getValue(RESOLUTION_ID).first() - ) + val queryNotification = + (receivedResponses.receive() as Response.Query).apply { parse() } + resolution = + Resolution.fromValue(queryNotification.data.getValue(RESOLUTION_ID).first()) Timber.i("Camera resolution is currently $resolution") } - - // Other tutorials will use their own notification handler so unregister ours now - ble.unregisterListener(bleListeners) } @RequiresPermission(allOf = ["android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT"]) private suspend fun performRegisteringForAsyncUpdatesTutorial( ble: Bluetooth, goproAddress: String ) { - @OptIn(ExperimentalUnsignedTypes::class) - fun resolutionRegisteringNotificationHandler(characteristic: UUID, data: UByteArray) { - GoProUUID.fromUuid(characteristic)?.let { - // If response is currently empty, create a new one - response = response ?: Response.Query() // We're only handling queries in this tutorial - } ?: return // We don't care about non-GoPro characteristics (i.e. the BT Core Battery service) - - Timber.d("Received response on $characteristic: ${data.toHexString()}") - - response?.let { rsp -> - rsp.accumulate(data) - - if (rsp.isReceived) { - rsp.parse() - - // If this is a query response, it must contain a resolution value - if (characteristic == GoProUUID.CQ_QUERY_RSP.uuid) { - Timber.i("Received resolution query response") - resolution = Resolution.fromValue(rsp.data.getValue(RESOLUTION_ID).first()) - Timber.i("Resolution is now $resolution") - } - // If this is a setting response, it will just show the status - else if (characteristic == GoProUUID.CQ_SETTING_RSP.uuid) { - Timber.i("Command sent successfully") - } - // Anything else is unexpected. This shouldn't happen - else { - Timber.i("Received unexpected response") - } - - // Notify the command sender the the procedure is complete - response = null - CoroutineScope(Dispatchers.IO).launch { receivedResponse.send(rsp) } - } - } - } - - @OptIn(ExperimentalUnsignedTypes::class) - val bleListeners by lazy { - BleEventListener().apply { - onNotification = ::resolutionRegisteringNotificationHandler - } - } - - // Register our notification handler callback for characteristic change updates - ble.registerListener(goproAddress, bleListeners) + // Flush the channel. Just create a new one. + receivedResponses = Channel() // Register with the GoPro for updates when resolution updates occur Timber.i("Registering for resolution value updates") val registerResolutionUpdates = ubyteArrayOf(0x02U, 0x52U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, registerResolutionUpdates) + val queryResponse = + (receivedResponses.receive() as Response.Query).apply { parse() } + resolution = + Resolution.fromValue(queryResponse.data.getValue(RESOLUTION_ID).first()) Timber.i("Camera resolution is $resolution") // Write to command request BleUUID to change the video resolution (either to 1080 or 2.7K) - val newResolution = + val targetResolution = if (resolution == Resolution.RES_2_7K) Resolution.RES_1080 else Resolution.RES_2_7K - Timber.i("Changing the resolution to $newResolution") - val setResolution = ubyteArrayOf(0x03U, RESOLUTION_ID, 0x01U, newResolution.value) + Timber.i("Changing the resolution to $targetResolution") + val setResolution = ubyteArrayOf(0x03U, RESOLUTION_ID, 0x01U, targetResolution.value) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_SETTING.uuid, setResolution) - val setResolutionResponse = receivedResponse.receive() + val setResolutionResponse = (receivedResponses.receive() as Response.Tlv).apply { parse() } if (setResolutionResponse.status == 0) { Timber.i("Resolution successfully changed") } else { - Timber.e("Failed to set resolution") + Timber.e("Failed to set resolution to to $targetResolution. Ensure camera is in a valid state to allow this resolution.") + return } // Verify we receive the update from the camera when the resolution changes - while (resolution != newResolution) { + while (resolution != targetResolution) { Timber.i("Waiting for camera to inform us about the resolution change") - receivedResponse.receive() + val queryNotification = + (receivedResponses.receive() as Response.Query).apply { parse() } + resolution = + Resolution.fromValue(queryNotification.data.getValue(RESOLUTION_ID).first()) + Timber.i("Camera resolution is $resolution") } - // Other tutorials will use their own notification handler so unregister ours now - ble.unregisterListener(bleListeners) + Timber.i("Resolution Update Notification has been received.") + + // Unregister for notifications + Timber.i("Unregistering for resolution value updates") + val unregisterResolutionUpdates = ubyteArrayOf(0x02U, 0x72U, RESOLUTION_ID) + ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, unregisterResolutionUpdates) + receivedResponses.receive() } @RequiresPermission(allOf = ["android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT"]) @@ -211,10 +179,15 @@ class Tutorial4BleQueries(number: Int, name: String, prerequisites: List.toInt(): Int { + var value = 0 + this.forEachIndexed { index, byte -> + value += (byte.toInt() shl (index * 8)) + } + return value +} + + fun ByteArray.toHexString(): String = joinToString(separator = ":") { String.format("%02X", it) } @OptIn(ExperimentalUnsignedTypes::class) @@ -23,6 +41,9 @@ fun UByteArray.toHexString(): String = this.toByteArray().toHexString() @OptIn(ExperimentalUnsignedTypes::class) fun UByteArray.decodeToString(): String = this.toByteArray().decodeToString() +@OptIn(ExperimentalUnsignedTypes::class) +fun List.decodeToString() = this.toUByteArray().decodeToString() + fun BluetoothGatt.findCharacteristic(uuid: UUID): BluetoothGattCharacteristic? { services?.forEach { service -> service.characteristics?.firstOrNull { characteristic -> diff --git a/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/util/GoProData.kt b/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/util/GoProData.kt index 0201017b..ec503f75 100644 --- a/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/util/GoProData.kt +++ b/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/util/GoProData.kt @@ -19,8 +19,10 @@ enum class GoProUUID(val uuid: UUID) { CQ_QUERY_RSP(UUID.fromString(GOPRO_BASE_UUID.format("0077"))); companion object { - private val map: Map by lazy { GoProUUID.values().associateBy { it.uuid } } - fun fromUuid(uuid: UUID): GoProUUID? = map[uuid] + private val uuidToGoProUUID: Map by lazy { GoProUUID.values().associateBy { it.uuid } } + fun fromUuid(uuid: UUID): GoProUUID? = uuidToGoProUUID[uuid] + fun mapByUuid(valueCreator: ((GoProUUID) -> T)): MutableMap = + values().associateWith { valueCreator(it) }.toMutableMap() } } diff --git a/demos/kotlin/tutorial/app/src/test/java/com/example/open_gopro_tutorial/ExampleUnitTest.kt b/demos/kotlin/tutorial/app/src/test/java/com/example/open_gopro_tutorial/ExampleUnitTest.kt index 043773ff..c962b195 100644 --- a/demos/kotlin/tutorial/app/src/test/java/com/example/open_gopro_tutorial/ExampleUnitTest.kt +++ b/demos/kotlin/tutorial/app/src/test/java/com/example/open_gopro_tutorial/ExampleUnitTest.kt @@ -1,11 +1,15 @@ + /* ExampleUnitTest.kt/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */ /* This copyright was auto-generated on Mon Mar 6 17:45:15 UTC 2023 */ package com.example.open_gopro_tutorial +import com.example.open_gopro_tutorial.tutorials.Response +import com.example.open_gopro_tutorial.util.GoProUUID import org.junit.Test import org.junit.Assert.* +import timber.log.Timber /** * Example local unit test, which will execute on the development machine (host). @@ -17,4 +21,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/demos/kotlin/tutorial/build.gradle b/demos/kotlin/tutorial/build.gradle index c7cf3d98..571c806c 100644 --- a/demos/kotlin/tutorial/build.gradle +++ b/demos/kotlin/tutorial/build.gradle @@ -4,7 +4,7 @@ buildscript { } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.3.1' apply false - id 'com.android.library' version '7.3.1' apply false + id 'com.android.application' version '8.3.1' apply false + id 'com.android.library' version '8.3.1' apply false id 'org.jetbrains.kotlin.android' version '1.7.0' apply false } \ No newline at end of file diff --git a/demos/kotlin/tutorial/gradle.properties b/demos/kotlin/tutorial/gradle.properties index 3c5031eb..f19c7b9b 100644 --- a/demos/kotlin/tutorial/gradle.properties +++ b/demos/kotlin/tutorial/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/demos/kotlin/tutorial/gradle/wrapper/gradle-wrapper.properties b/demos/kotlin/tutorial/gradle/wrapper/gradle-wrapper.properties index 4c783d53..39e0b57b 100644 --- a/demos/kotlin/tutorial/gradle/wrapper/gradle-wrapper.properties +++ b/demos/kotlin/tutorial/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Feb 15 10:09:42 PST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/demos/python/sdk_wireless_camera_control/.gitignore b/demos/python/sdk_wireless_camera_control/.gitignore index bfbe3baf..01e74e54 100644 --- a/demos/python/sdk_wireless_camera_control/.gitignore +++ b/demos/python/sdk_wireless_camera_control/.gitignore @@ -1,13 +1,13 @@ **/*.crt +**/*.jpg +**/*.mp4 **/temp -.reports/ # Docs output docs/build # Test artifacts -/*.jpg -/*.mp4 +.reports/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/demos/python/sdk_wireless_camera_control/docs/api.rst b/demos/python/sdk_wireless_camera_control/docs/api.rst index cb769bf4..cd685526 100644 --- a/demos/python/sdk_wireless_camera_control/docs/api.rst +++ b/demos/python/sdk_wireless_camera_control/docs/api.rst @@ -86,19 +86,19 @@ GoPro Enum BLE Setting ^^^^^^^^^^^ -.. inheritance-diagram:: open_gopro.api.builders.BleSetting +.. inheritance-diagram:: open_gopro.api.builders.BleSettingFacade :parts: 1 -.. autoclass:: open_gopro.api.builders.BleSetting +.. autoclass:: open_gopro.api.builders.BleSettingFacade :exclude-members: get_name, get_capabilities_names BLE Status ^^^^^^^^^^ -.. inheritance-diagram:: open_gopro.api.builders.BleStatus +.. inheritance-diagram:: open_gopro.api.builders.BleStatusFacade :parts: 1 -.. autoclass:: open_gopro.api.builders.BleStatus +.. autoclass:: open_gopro.api.builders.BleStatusFacade HTTP Setting ^^^^^^^^^^^^ @@ -108,6 +108,11 @@ HTTP Setting .. autoclass:: open_gopro.api.builders.HttpSetting +Method Protocols +^^^^^^^^^^^^^^^^ + +.. autoclass:: open_gopro.api.builders.BuilderProtocol + Message Bases ^^^^^^^^^^^^^ @@ -134,8 +139,6 @@ but the end user should never need to use these directly. .. autoclass:: open_gopro.communicator_interface.MessageRules -.. autoclass:: open_gopro.communicator_interface.RuleSignature - Parameters ---------- diff --git a/demos/python/sdk_wireless_camera_control/docs/conf.py b/demos/python/sdk_wireless_camera_control/docs/conf.py index ab6330bb..206edc0b 100644 --- a/demos/python/sdk_wireless_camera_control/docs/conf.py +++ b/demos/python/sdk_wireless_camera_control/docs/conf.py @@ -90,6 +90,7 @@ (r".*", r".*response.T*"), ] + # This is the expected signature of the handler for this event, cf doc def autodoc_skip_member_handler(app, what, name, *_): for skip in ("internal", "deprecated"): diff --git a/demos/python/sdk_wireless_camera_control/docs/usage.rst b/demos/python/sdk_wireless_camera_control/docs/usage.rst index 94fd9152..851db2f6 100644 --- a/demos/python/sdk_wireless_camera_control/docs/usage.rst +++ b/demos/python/sdk_wireless_camera_control/docs/usage.rst @@ -248,7 +248,7 @@ Commands are callable instance attributes of a Messages class instance Statuses ^^^^^^^^ -Statuses are instances of a BleStatus(:class:`~open_gopro.api.builders.BleStatus`). They can be read +Statuses are instances of a BleStatus(:class:`~open_gopro.api.builders.BleStatusFacade`). They can be read synchronously using their `get_value` method as such: .. code-block:: python @@ -272,7 +272,7 @@ It is also possible to read all statuses at once via: Settings ^^^^^^^^ -Settings are instances of a BleSetting(:class:`~open_gopro.api.builders.BleSetting`) +Settings are instances of a BleSetting(:class:`~open_gopro.api.builders.BleSettingFacade`) or HttpSetting(:class:`~open_gopro.api.builders.HttpSetting`). They can be interacted synchronously in several ways. @@ -319,9 +319,9 @@ This section describes how to register for and handle asynchronous push notifica It is possible to enable push notifications for any of the following: -- setting values via :meth:`~open_gopro.api.builders.BleSetting.register_value_update` -- setting capabilities via :meth:`~open_gopro.api.builders.BleSetting.register_capability_update` -- status values via :meth:`~open_gopro.api.builders.BleStatus.register_value_update` +- setting values via :meth:`~open_gopro.api.builders.BleSettingFacade.register_value_update` +- setting capabilities via :meth:`~open_gopro.api.builders.BleSettingFacade.register_capability_update` +- status values via :meth:`~open_gopro.api.builders.BleStatusFacade.register_value_update` Firstly, the desired settings / ID must be registered for and given a callback to handle received notifications. @@ -332,9 +332,9 @@ 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.: -- setting values via :meth:`~open_gopro.api.builders.BleSetting.unregister_value_update` -- setting capabilities via :meth:`~open_gopro.api.builders.BleSetting.unregister_capability_update` -- status values via :meth:`~open_gopro.api.builders.BleStatus.unregister_value_update` +- setting values via :meth:`~open_gopro.api.builders.BleSettingFacade.unregister_value_update` +- setting capabilities via :meth:`~open_gopro.api.builders.BleSettingFacade.unregister_capability_update` +- status values via :meth:`~open_gopro.api.builders.BleStatusFacade.unregister_value_update` Here is an example of registering for and receiving FOV updates: 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 863a6ce2..2a7e681a 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 @@ -10,16 +10,13 @@ BleAsyncResponse, BleProtoCommand, BleReadCommand, - BleSetting, - BleStatus, + BleSettingFacade, + BleStatusFacade, BleWriteCommand, - HttpGetBinary, - HttpGetJsonCommand, HttpSetting, RegisterUnregisterAll, ) from .http_commands import HttpCommands, HttpSettings -# TODO find a better way to set up parsers, etc besides instantiating - +# We need to ensure the API instantiated so that all parsers are set up. WirelessApi(None) # type: ignore 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 bb9ecee0..179d87c0 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 @@ -28,10 +28,10 @@ ) from open_gopro import proto, types +from open_gopro.api.builders import BleAsyncResponse +from open_gopro.api.builders import BleSettingFacade as BleSetting +from open_gopro.api.builders import BleStatusFacade as BleStatus from open_gopro.api.builders import ( - BleAsyncResponse, - BleSetting, - BleStatus, RegisterUnregisterAll, ble_proto_command, ble_read_command, @@ -50,7 +50,6 @@ CmdId, FeatureId, GoProUUIDs, - QueryCmdId, SettingId, StatusId, ) @@ -64,7 +63,7 @@ logger = logging.getLogger(__name__) -class BleCommands(BleMessages[BleMessage, CmdId]): +class BleCommands(BleMessages[BleMessage]): """All of the BLE commands. To be used as a delegate for a GoProBle instance to build commands @@ -78,10 +77,10 @@ class BleCommands(BleMessages[BleMessage, CmdId]): 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, - }, + rules=MessageRules( + fastpass_analyzer=lambda **kwargs: kwargs["shutter"] == Params.Toggle.DISABLE, + wait_for_encoding_analyzer=lambda **kwargs: kwargs["shutter"] == Params.Toggle.ENABLE, + ), ) async def set_shutter(self, *, shutter: Params.Toggle) -> GoProResp[None]: """Set the Shutter to start / stop encoding @@ -343,7 +342,6 @@ async def get_wifi_password(self) -> GoProResp[str]: GoProUUIDs.CQ_QUERY, CmdId.REGISTER_ALL_STATUSES, update_set=StatusId, - responded_cmd=QueryCmdId.STATUS_VAL_PUSH, # TODO probably remove this action=RegisterUnregisterAll.Action.REGISTER, ) async def register_for_all_statuses(self, callback: types.UpdateCb) -> GoProResp[None]: @@ -360,7 +358,6 @@ async def register_for_all_statuses(self, callback: types.UpdateCb) -> GoProResp GoProUUIDs.CQ_QUERY, CmdId.UNREGISTER_ALL_STATUSES, update_set=StatusId, - responded_cmd=QueryCmdId.STATUS_VAL_PUSH, action=RegisterUnregisterAll.Action.UNREGISTER, ) async def unregister_for_all_statuses(self, callback: types.UpdateCb) -> GoProResp[None]: @@ -377,7 +374,6 @@ async def unregister_for_all_statuses(self, callback: types.UpdateCb) -> GoProRe GoProUUIDs.CQ_QUERY, CmdId.REGISTER_ALL_SETTINGS, update_set=SettingId, - responded_cmd=QueryCmdId.SETTING_VAL_PUSH, action=RegisterUnregisterAll.Action.REGISTER, ) async def register_for_all_settings(self, callback: types.UpdateCb) -> GoProResp[None]: @@ -394,7 +390,6 @@ async def register_for_all_settings(self, callback: types.UpdateCb) -> GoProResp GoProUUIDs.CQ_QUERY, CmdId.UNREGISTER_ALL_SETTINGS, update_set=SettingId, - responded_cmd=QueryCmdId.SETTING_VAL_PUSH, action=RegisterUnregisterAll.Action.UNREGISTER, ) async def unregister_for_all_settings(self, callback: types.UpdateCb) -> GoProResp[None]: @@ -411,7 +406,6 @@ async def unregister_for_all_settings(self, callback: types.UpdateCb) -> GoProRe GoProUUIDs.CQ_QUERY, CmdId.REGISTER_ALL_CAPABILITIES, update_set=SettingId, - responded_cmd=QueryCmdId.SETTING_CAPABILITY_PUSH, action=RegisterUnregisterAll.Action.REGISTER, ) async def register_for_all_capabilities(self, callback: types.UpdateCb) -> GoProResp[None]: @@ -428,7 +422,6 @@ async def register_for_all_capabilities(self, callback: types.UpdateCb) -> GoPro GoProUUIDs.CQ_QUERY, CmdId.UNREGISTER_ALL_CAPABILITIES, update_set=SettingId, - responded_cmd=QueryCmdId.SETTING_CAPABILITY_PUSH, action=RegisterUnregisterAll.Action.UNREGISTER, ) async def unregister_for_all_capabilities(self, callback: types.UpdateCb) -> GoProResp[None]: @@ -838,7 +831,7 @@ async def cohn_set_setting(self, *, mode: Params.Toggle) -> GoProResp[None]: return {"cohn_active": mode} # type: ignore -class BleSettings(BleMessages[BleSetting, SettingId]): +class BleSettings(BleMessages[BleSetting.BleSettingMessageBase]): # pylint: disable=missing-class-docstring, unused-argument """The collection of all BLE Settings. @@ -917,12 +910,12 @@ def __init__(self, communicator: GoProBle): ) """Camera controls configuration.""" - self.video_easy_mode: BleSetting[Params.Speed] = BleSetting[Params.Speed]( + self.video_easy_mode: BleSetting[int] = BleSetting[int]( communicator, SettingId.VIDEO_EASY_MODE, - Params.Speed, + Int8ub, ) - """Video easy mode speed.""" + """Video easy mode speed. It is not feasible to maintain this setting without code generation so just read as int.""" self.photo_easy_mode: BleSetting[Params.PhotoEasyMode] = BleSetting[Params.PhotoEasyMode]( communicator, @@ -1114,7 +1107,7 @@ def add_parsers(cls) -> None: GlobalParsers.add_feature_action_id_mapping(response.feature_id, response.action_id) -class BleStatuses(BleMessages[BleStatus, StatusId]): +class BleStatuses(BleMessages[BleStatus.BleStatusMessageBase]): """All of the BLE Statuses. To be used by a GoProBle delegate to build status messages. @@ -1131,7 +1124,6 @@ def __init__(self, communicator: GoProBle) -> None: 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[Any] = BleStatus( communicator, StatusId.DEPRECATED_3, ByteParserBuilders.DeprecatedMarker() ) 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 a79611ab..4df7263c 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 @@ -10,14 +10,14 @@ 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 typing import Any, Callable, Final, Generic, Protocol, TypeVar, Union import construct import wrapt from open_gopro import types -from open_gopro.api.parsers import ByteParserBuilders, JsonParsers +from open_gopro.api.parsers import ByteParserBuilders +from open_gopro.ble import BleUUID from open_gopro.communicator_interface import ( BleMessage, BleMessages, @@ -26,11 +26,9 @@ HttpMessage, HttpMessages, MessageRules, - RuleSignature, ) from open_gopro.constants import ( ActionId, - BleUUID, CmdId, FeatureId, GoProUUIDs, @@ -40,15 +38,12 @@ ) from open_gopro.enum import GoProIntEnum 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") QueryParserType = Union[construct.Construct, type[GoProIntEnum], BytesParserBuilder] @@ -58,7 +53,7 @@ T = TypeVar("T") -class BleReadCommand(BleMessage[BleUUID]): +class BleReadCommand(BleMessage): """A BLE command that reads data from a BleUUID""" def __init__(self, uuid: BleUUID, parser: Parser) -> None: @@ -70,30 +65,35 @@ def __init__(self, uuid: BleUUID, parser: Parser) -> None: """ super().__init__(uuid=uuid, parser=parser, identifier=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 _build_data(self, **kwargs: Any) -> bytearray: + # Read commands do not have data + raise NotImplementedError def __str__(self) -> str: return f"Read {self._uuid.name.lower().replace('_', ' ').title()}" - def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: + def _as_dict(self, **kwargs: Any) -> types.JsonDict: """Return the attributes of the command as a dict Args: - *_ (Any): unused **kwargs (Any): additional entries for the dict Returns: types.JsonDict: command as dict """ - return {"id": "Read " + self._uuid.name, **self._base_dict} | kwargs + return {"id": self._uuid, **self._base_dict} | kwargs + +class BleWriteCommand(BleMessage): + """A BLE command that writes to a BleUUID and retrieves responses by accumulating notifications -class BleWriteCommand(BleMessage[CmdId]): - """A BLE command that writes to a BleUUID and does not accept any parameters""" + Args: + uuid (BleUUID): UUID to write to + cmd (CmdId): command identifier + param_builder (BytesBuilder | None, optional): builds bytes from params. Defaults to None. + parser (Parser | None, optional): response parser to parse received bytes. Defaults to None. + rules (MessageRules): rules this Message must obey. Defaults to MessageRules(). + """ def __init__( self, @@ -101,34 +101,14 @@ def __init__( cmd: CmdId, param_builder: BytesBuilder | None = None, parser: Parser | None = None, - rules: dict[MessageRules, RuleSignature] | None = None, + rules: MessageRules = MessageRules(), ) -> None: - """Constructor - - Args: - 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 - rules (Optional[dict[MessageRules, RuleSignature]], optional): rules to apply when executing this - message. Defaults to None. - """ self.param_builder = param_builder self.cmd = cmd - super().__init__(uuid, parser, cmd, rules) - - async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp: - """Execute the command by sending it via BLE - - Args: - __communicator__ (GoProBle): BLE communicator to send the message - **kwargs (Any): arguments to BLE write command - - Returns: - GoProResp: Response received via BLE - """ - logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(**kwargs)))) + self.rules = rules + super().__init__(uuid, cmd, parser) + def _build_data(self, **kwargs: Any) -> bytearray: data = bytearray([self.cmd.value]) params = bytearray() if self.param_builder: @@ -139,20 +119,15 @@ async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> GoProResp if params: data.append(len(params)) data.extend(params) - response = await __communicator__._send_ble_message( - self._uuid, data, self._identifier, rules=self._evaluate_rules(**kwargs) - ) - # logger.info(Logger.build_log_rx_str(response)) - return response + return data def __str__(self) -> str: return self.cmd.name.lower().replace("_", " ").removeprefix("cmdid").title() - def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: + def _as_dict(self, **kwargs: Any) -> types.JsonDict: """Return the attributes of the command as a dict Args: - *_ (Any): unused **kwargs (Any): additional entries for the dict Returns: @@ -180,7 +155,6 @@ def __init__( uuid: BleUUID, cmd: CmdId, update_set: type[SettingId] | type[StatusId], - responded_cmd: QueryCmdId, action: Action, parser: Parser | None = None, ) -> None: @@ -190,28 +164,15 @@ def __init__( uuid (BleUUID): UUID to write to cmd (CmdId): Command ID that is being sent 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.update_set = update_set - self.responded_cmd = responded_cmd super().__init__(uuid=uuid, cmd=cmd, parser=parser) - 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_update - )(kwargs["callback"], update) - return response - -class BleProtoCommand(BleMessage[ActionId]): +class BleProtoCommand(BleMessage): """A BLE command that is sent and received as using the Protobuf protocol""" def __init__( @@ -241,7 +202,7 @@ def __init__( """ p = parser or Parser() p.byte_json_adapter = ByteParserBuilders.Protobuf(response_proto) - super().__init__(uuid=uuid, parser=p, identifier=action_id) + super().__init__(uuid=uuid, parser=p, identifier=response_action_id) self.feature_id = feature_id self.action_id = action_id self.response_action_id = response_action_id @@ -253,7 +214,7 @@ def __init__( 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: + def _build_data(self, **kwargs: Any) -> bytearray: """Build the byte data to prepare for command sending Args: @@ -278,23 +239,13 @@ def build_data(self, **kwargs: Any) -> bytearray: # Prepend headers and serialize return bytearray([self.feature_id.value, self.action_id.value, *proto.SerializeToString()]) - 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(pretty_print(self._as_dict(**kwargs)))) - data = self.build_data(**kwargs) - # Allow exception to pass through if protobuf not completely initialized - 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) -> types.JsonDict: + def _as_dict(self, **kwargs: Any) -> types.JsonDict: """Return the attributes of the command as a dict Args: - *_ (Any): unused **kwargs (Any): additional entries for the dict Returns: @@ -308,31 +259,31 @@ def ble_write_command( cmd: CmdId, param_builder: BytesBuilder | None = None, parser: Parser | None = None, - rules: dict[MessageRules, RuleSignature] | None = None, + rules: MessageRules = MessageRules(), ) -> Callable: - """Factory to build a BleWriteCommand and wrapper to execute it + """Decorator to build and encapsulate a BleWriteCommand in a Callable Args: - 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 (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 + uuid (BleUUID): UUID to write to + cmd (CmdId): command identifier + param_builder (BytesBuilder | None, optional): builds bytes from params. Defaults to None. + parser (Parser | None, optional): response parser to parse received bytes. Defaults to None. + rules (MessageRules): rules this Message must obey. Defaults to MessageRules(). Returns: - Callable: Generated method to perform command + Callable: built callable to perform operation """ - message = BleWriteCommand(uuid, cmd, param_builder, parser, rules=rules) + message = BleWriteCommand(uuid, cmd, param_builder, parser) @wrapt.decorator async def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: - return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) + return await instance._communicator._send_ble_message(message, rules, **(await wrapped(**kwargs) or kwargs)) return wrapper def ble_read_command(uuid: BleUUID, parser: Parser) -> Callable: - """Factory to build a BleReadCommand and wrapper to execute it + """Decorator to build a BleReadCommand and wrapper to execute it Args: uuid (BleUUID): BleUUID to read from @@ -345,7 +296,7 @@ def ble_read_command(uuid: BleUUID, parser: Parser) -> Callable: @wrapt.decorator async def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: - return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) + return await instance._communicator._read_ble_characteristic(message, **(await wrapped(**kwargs) or kwargs)) return wrapper @@ -354,28 +305,26 @@ def ble_register_command( uuid: BleUUID, cmd: CmdId, update_set: type[SettingId] | type[StatusId], - responded_cmd: QueryCmdId, action: RegisterUnregisterAll.Action, parser: Parser | None = None, ) -> Callable: - """Factory to build a RegisterUnregisterAll command and wrapper to execute it + """Decorator 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 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 (Parser, optional): Optional response parser. Defaults to None. Returns: Callable: Generated method to perform command """ - message = RegisterUnregisterAll(uuid, cmd, update_set, responded_cmd, action, parser) + message = RegisterUnregisterAll(uuid, cmd, update_set, action, parser) @wrapt.decorator async def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: - return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) + return await instance._communicator._send_ble_message(message, **(await wrapped(**kwargs) or kwargs)) return wrapper @@ -390,7 +339,7 @@ def ble_proto_command( 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 + """Decorator to build a BLE Protobuf command and wrapper to execute it Args: uuid (BleUUID): BleUUID to write to @@ -399,10 +348,10 @@ def ble_proto_command( response_action_id (ActionId): the action ID that will be in the response to this command 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. + parser (Parser | None, optional): Response parser to transform received Protobuf bytes. 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. + synchronous response. Defaults to None. Returns: Callable: Generated method to perform command @@ -420,7 +369,7 @@ def ble_proto_command( @wrapt.decorator async def wrapper(wrapped: Callable, instance: BleMessages, _: Any, kwargs: Any) -> GoProResp: - return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) + return await instance._communicator._send_ble_message(message, **(await wrapped(**kwargs) or kwargs)) return wrapper @@ -442,23 +391,50 @@ def __str__(self) -> str: return self.action_id.name.lower().replace("_", " ").removeprefix("actionid").title() -class BleSetting(BleMessage[SettingId], Generic[ValueType]): - """An individual camera setting that is interacted with via BLE.""" +class BuilderProtocol(Protocol): + """Protocol definition of data building methods""" + + def __call__(self, **kwargs: Any) -> bytearray: # noqa: D102 + ... + + +class BleSettingFacade(Generic[ValueType]): + """Wrapper around BleSetting since a BleSetting's message definition changes based on how it is being operated on. + + Args: + communicator (GoProBle): BLE communicator that will operate on this object. + identifier (SettingId): Setting Identifier + parser_builder (QueryParserType): Parses responses from bytes and builds requests to bytes. + """ SETTER_UUID: Final[BleUUID] = GoProUUIDs.CQ_SETTINGS READER_UUID: Final[BleUUID] = GoProUUIDs.CQ_QUERY - def __init__(self, communicator: GoProBle, identifier: SettingId, parser_builder: QueryParserType) -> None: - """Constructor + class BleSettingMessageBase(BleMessage): + """Actual BLE Setting Message that is wrapped by the facade. Args: - communicator (GoProBle): BLE communicator to interact with setting - identifier (SettingId): ID of setting - parser_builder (QueryParserType): object to both parse and build setting - - Raises: - TypeError: Invalid parser_builder type + uuid (BleUUID): UUID to access this setting. + identifier (SettingId | QueryCmdId): How responses to operations on this message will be identified. + setting_id: (SettingId): Setting identifier. May match identifier in some cases. + builder (BuilderProtocol): Build request bytes from the current message. """ + + def __init__( + self, uuid: BleUUID, identifier: SettingId | QueryCmdId, setting_id: SettingId, builder: BuilderProtocol + ) -> None: + self._build = builder + self._setting_id = setting_id + super().__init__(uuid, identifier, None) # type: ignore + + def _build_data(self, **kwargs: Any) -> bytearray: + return self._build(**kwargs) + + def _as_dict(self, **kwargs: Any) -> types.JsonDict: + d = {"id": self._identifier, "setting_id": self._setting_id, **self._base_dict} | kwargs + return d + + def __init__(self, communicator: GoProBle, identifier: SettingId, parser_builder: QueryParserType) -> None: # TODO abstract this parser = Parser[types.CameraState]() if isinstance(parser_builder, construct.Construct): @@ -469,40 +445,22 @@ def __init__(self, communicator: GoProBle, identifier: SettingId, parser_builder parser.byte_json_adapter = ByteParserBuilders.GoProEnum(parser_builder) else: raise TypeError(f"Unexpected {parser_builder=}") + GlobalParsers.add(identifier, parser) + self._identifier = identifier 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() - - async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> Any: - """Not applicable for a BLE setting - - Args: - __communicator__ (GoProBle): BLE communicator - **kwargs (Any): not used - Raises: - NotImplementedError: Not applicable - """ - raise NotImplementedError - - def _as_dict( # pylint: disable = arguments-differ - self, identifier: QueryCmdId | SettingId | str, *_: Any, **kwargs: Any - ) -> types.JsonDict: - """Return the attributes of the message as a dict + def _build_cmd(self, cmd: QueryCmdId) -> bytearray: + """Build the data Args: - identifier (Union[QueryCmdId, SettingId, str]): identifier of the message for this send - *_ (Any): unused - **kwargs (Any): additional entries for the dict + cmd (QueryCmdId): query command Returns: - types.JsonDict: setting as dict + bytearray: built data """ - return {"id": identifier, **self._base_dict} | kwargs + return bytearray([cmd.value, int(self._identifier)]) async def set(self, value: ValueType) -> GoProResp[None]: """Set the value of the setting. @@ -513,33 +471,24 @@ async def set(self, value: ValueType) -> GoProResp[None]: Returns: GoProResp: Status of set """ - 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: - param = self._builder.build(value) - data.extend([len(param), *param]) - except IndexError: - pass - - response = await self._communicator._send_ble_message(self.SETTER_UUID, data, self._identifier) - logger.info(Logger.build_log_rx_str(response)) - return response - - async def _send_query(self, response_id: QueryCmdId) -> GoProResp[types.CameraState | None]: - """Build the byte data and query setting information - Args: - response_id (QueryCmdId): expected identifier of response + def _build_data(**kwargs: Any) -> bytearray: + # Special case. Can't use _send_query + data = bytearray([int(self._identifier)]) + try: + param = self._builder.build(kwargs["value"]) + data.extend([len(param), *param]) + except IndexError: + pass + return data - Returns: - GoProResp: query response - """ - data = self._build_cmd(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 + message = BleSettingFacade.BleSettingMessageBase( + BleSettingFacade.SETTER_UUID, + self._identifier, + self._identifier, + lambda **_: _build_data(value=value), + ) + return await self._communicator._send_ble_message(message) async def get_value(self) -> GoProResp[ValueType]: """Get the settings value. @@ -547,7 +496,13 @@ async def get_value(self) -> GoProResp[ValueType]: Returns: GoProResp: settings value """ - return await self._send_query(QueryCmdId.GET_SETTING_VAL) # type: ignore + message = BleSettingFacade.BleSettingMessageBase( + BleSettingFacade.READER_UUID, + QueryCmdId.GET_SETTING_VAL, + self._identifier, + lambda **_: self._build_cmd(QueryCmdId.GET_SETTING_VAL), + ) + return await self._communicator._send_ble_message(message) async def get_name(self) -> GoProResp[str]: """Get the settings name. @@ -563,7 +518,13 @@ async def get_capabilities_values(self) -> GoProResp[list[ValueType]]: Returns: GoProResp: settings capabilities values """ - return await self._send_query(QueryCmdId.GET_CAPABILITIES_VAL) # type: ignore + message = BleSettingFacade.BleSettingMessageBase( + BleSettingFacade.READER_UUID, + QueryCmdId.GET_CAPABILITIES_VAL, + self._identifier, + lambda **_: self._build_cmd(QueryCmdId.GET_CAPABILITIES_VAL), + ) + return await self._communicator._send_ble_message(message) async def get_capabilities_names(self) -> GoProResp[list[str]]: """Get currently supported settings capabilities names. @@ -582,9 +543,15 @@ async def register_value_update(self, callback: types.UpdateCb) -> GoProResp[Non Returns: GoProResp: Current value of respective setting ID """ - if (response := await self._send_query(QueryCmdId.REG_SETTING_VAL_UPDATE)).ok: + message = BleSettingFacade.BleSettingMessageBase( + BleSettingFacade.READER_UUID, + QueryCmdId.REG_SETTING_VAL_UPDATE, + self._identifier, + lambda **_: self._build_cmd(QueryCmdId.REG_SETTING_VAL_UPDATE), + ) + if (response := await self._communicator._send_ble_message(message)).ok: self._communicator.register_update(callback, self._identifier) - return response # type: ignore + return response async def unregister_value_update(self, callback: types.UpdateCb) -> GoProResp[None]: """Stop receiving notifications when a given setting ID's value updates. @@ -595,9 +562,15 @@ async def unregister_value_update(self, callback: types.UpdateCb) -> GoProResp[N Returns: GoProResp: Status of unregister """ - if (response := await self._send_query(QueryCmdId.UNREG_SETTING_VAL_UPDATE)).ok: + message = BleSettingFacade.BleSettingMessageBase( + BleSettingFacade.READER_UUID, + QueryCmdId.UNREG_SETTING_VAL_UPDATE, + self._identifier, + lambda **_: self._build_cmd(QueryCmdId.UNREG_SETTING_VAL_UPDATE), + ) + if (response := await self._communicator._send_ble_message(message)).ok: self._communicator.unregister_update(callback, self._identifier) - return response # type: ignore + return response async def register_capability_update(self, callback: types.UpdateCb) -> GoProResp[None]: """Register for asynchronous notifications when a given setting ID's capabilities update. @@ -608,9 +581,15 @@ async def register_capability_update(self, callback: types.UpdateCb) -> GoProRes Returns: GoProResp: Current capabilities of respective setting ID """ - if (response := await self._send_query(QueryCmdId.REG_CAPABILITIES_UPDATE)).ok: - self._communicator.register_update(callback, self._identifier) - return response # type: ignore + message = BleSettingFacade.BleSettingMessageBase( + BleSettingFacade.READER_UUID, + QueryCmdId.REG_CAPABILITIES_UPDATE, + self._identifier, + lambda **_: self._build_cmd(QueryCmdId.REG_CAPABILITIES_UPDATE), + ) + if (response := await self._communicator._send_ble_message(message)).ok: + self._communicator.unregister_update(callback, self._identifier) + return response async def unregister_capability_update(self, callback: types.UpdateCb) -> GoProResp[None]: """Stop receiving notifications when a given setting ID's capabilities change. @@ -621,39 +600,62 @@ async def unregister_capability_update(self, callback: types.UpdateCb) -> GoProR Returns: GoProResp: Status of unregister """ - if (response := await self._send_query(QueryCmdId.UNREG_CAPABILITIES_UPDATE)).ok: + message = BleSettingFacade.BleSettingMessageBase( + BleSettingFacade.READER_UUID, + QueryCmdId.UNREG_CAPABILITIES_UPDATE, + self._identifier, + lambda **_: self._build_cmd(QueryCmdId.UNREG_CAPABILITIES_UPDATE), + ) + if (response := await self._communicator._send_ble_message(message)).ok: self._communicator.unregister_update(callback, self._identifier) - return response # type: ignore + return response - def _build_cmd(self, cmd: QueryCmdId) -> bytearray: - """Build the data to send a settings query over-the-air. + def __str__(self) -> str: + return str(self._identifier).lower().replace("_", " ").title() - Args: - cmd (QueryCmdId): command to build - Returns: - bytearray: data to send over-the-air - """ - ret = bytearray([cmd.value, int(self._identifier)]) - return ret +class BleStatusFacade(Generic[ValueType]): + """Wrapper around BleStatus since a BleStatus's message definition changes based on how it is being operated on. + Args: + communicator (GoProBle): BLE communicator that will operate on this object. + identifier (StatusId): Status identifier + parser (QueryParserType): Parser responses from bytes -class BleStatus(BleMessage[StatusId], Generic[ValueType]): - """An individual camera status that is interacted with via BLE.""" + Raises: + TypeError: Attempted to pass an invalid parser type + """ UUID: Final[BleUUID] = GoProUUIDs.CQ_QUERY - def __init__(self, communicator: GoProBle, identifier: StatusId, parser: QueryParserType) -> None: - """Constructor + class BleStatusMessageBase(BleMessage): + """An individual camera status that is interacted with via BLE. Args: - communicator (GoProBle): Adapter to read status data - identifier (StatusId): ID of status - parser (QueryParserType): construct to parse or enum to represent status value - - Raises: - TypeError: Invalid parser type + uuid (BleUUID): UUID to access this status. + identifier (StatusId | QueryCmdId): How responses to operations on this message will be identified. + status_id (StatusId): Status identifier. May match identifier in some cases. + builder (Callable[[Any], bytearray]): Build request bytes from the current message. """ + + def __init__( + self, + uuid: BleUUID, + identifier: StatusId | QueryCmdId, + status_id: StatusId, + builder: Callable[[Any], bytearray], + ) -> None: + self._build = builder + self._status_id = status_id + super().__init__(uuid, identifier, None) # type: ignore + + def _build_data(self, **kwargs: Any) -> bytearray: + return self._build(self, **kwargs) + + def _as_dict(self, **kwargs: Any) -> types.JsonDict: + return {"id": self._identifier, "status_id": self._status_id, **self._base_dict} | kwargs + + def __init__(self, communicator: GoProBle, identifier: StatusId, parser: QueryParserType) -> None: # TODO abstract this parser_builder = Parser[types.CameraState]() # Is it a protobuf enum? @@ -665,66 +667,27 @@ def __init__(self, communicator: GoProBle, identifier: StatusId, parser: QueryPa parser_builder.byte_json_adapter = ByteParserBuilders.GoProEnum(parser) else: raise TypeError(f"Unexpected {parser_builder=}") + GlobalParsers.add(identifier, parser_builder) self._communicator = communicator - BleMessage.__init__(self, uuid=self.UUID, parser=parser_builder, identifier=identifier) self._identifier = identifier - async def __call__(self, __communicator__: GoProBle, **kwargs: Any) -> Any: - """Not applicable for a BLE status - - Args: - __communicator__ (GoProBle): BLE communicator - **kwargs (Any): not used - - Raises: - NotImplementedError: Not applicable - """ - raise NotImplementedError - def __str__(self) -> str: return str(self._identifier).lower().replace("_", " ").title() - async def _send_query(self, response_id: QueryCmdId) -> GoProResp: - """Build the byte data and query setting information - - Args: - response_id (QueryCmdId): expected identifier of response - - Returns: - GoProResp: query response - """ - data = self._build_cmd(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: QueryCmdId | SettingId | str, - *_: Any, - **kwargs: Any, - ) -> types.JsonDict: - """Return the attributes of the command as a dict - - Args: - identifier (Union[QueryCmdId, SettingId, str]): identifier of the command for this send - *_ (Any): unused - **kwargs (Any): additional entries for the dict - - Returns: - types.JsonDict: command as dict - """ - return {"id": identifier, **self._base_dict} | kwargs - async def get_value(self) -> GoProResp[ValueType]: """Get the current value of a status. Returns: GoProResp: current status value """ - return await self._send_query(QueryCmdId.GET_STATUS_VAL) + message = BleStatusFacade.BleStatusMessageBase( + BleStatusFacade.UUID, + QueryCmdId.GET_STATUS_VAL, + self._identifier, + lambda *args: self._build_cmd(QueryCmdId.GET_STATUS_VAL), + ) + return await self._communicator._send_ble_message(message) async def register_value_update(self, callback: types.UpdateCb) -> GoProResp[ValueType]: """Register for asynchronous notifications when a status changes. @@ -735,7 +698,13 @@ async def register_value_update(self, callback: types.UpdateCb) -> GoProResp[Val Returns: GoProResp: current status value """ - if (response := await self._send_query(QueryCmdId.REG_STATUS_VAL_UPDATE)).ok: + message = BleStatusFacade.BleStatusMessageBase( + BleStatusFacade.UUID, + QueryCmdId.REG_STATUS_VAL_UPDATE, + self._identifier, + lambda *args: self._build_cmd(QueryCmdId.REG_STATUS_VAL_UPDATE), + ) + if (response := await self._communicator._send_ble_message(message)).ok: self._communicator.register_update(callback, self._identifier) return response @@ -748,8 +717,14 @@ async def unregister_value_update(self, callback: types.UpdateCb) -> GoProResp: Returns: GoProResp: Status of unregister """ - if (response := await self._send_query(QueryCmdId.UNREG_STATUS_VAL_UPDATE)).ok: - self._communicator.unregister_update(callback, self._identifier) + message = BleStatusFacade.BleStatusMessageBase( + BleStatusFacade.UUID, + QueryCmdId.UNREG_STATUS_VAL_UPDATE, + self._identifier, + lambda *args: self._build_cmd(QueryCmdId.UNREG_STATUS_VAL_UPDATE), + ) + if (response := await self._communicator._send_ble_message(message)).ok: + self._communicator.register_update(callback, self._identifier) return response def _build_cmd(self, cmd: QueryCmdId) -> bytearray: @@ -761,226 +736,142 @@ def _build_cmd(self, cmd: QueryCmdId) -> bytearray: Returns: bytearray: data to send over-the-air """ - ret = bytearray([cmd.value, int(self._identifier)]) - return ret + return bytearray([cmd.value, int(self._identifier)]) ######################################################## HTTP ################################################# -class HttpCommand(HttpMessage[str]): - """The base class for HTTP Commands""" - - def __init__( - self, - endpoint: str, - 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 - - Args: - endpoint (str): base 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. - identifier (Optional[IdType]): explicitly set message identifier. Defaults to None (generated from endpoint). - rules (Optional[dict[MessageRules, RuleSignature]], optional): rules to apply when executing this - message. Defaults to None. - """ - if not identifier: - # Build human-readable name from endpoint - identifier = endpoint.lower().removeprefix("gopro/").replace("/", " ").replace("_", " ").title() - try: - identifier = identifier.split("?")[0].strip("{}") - except IndexError: - pass - - super().__init__(endpoint, identifier, components, arguments, parser, rules) - - def build_url(self, **kwargs: Any) -> str: - """Build the URL string from the passed in components and arguments - - Args: - **kwargs (Any): additional entries for the dict - - Returns: - str: built URL - """ - url = self._endpoint - for component in self._components or []: - url += "/" + kwargs.pop(component) - # Append parameters - if self._args and ( - arg_part := urlencode( - { - k: kwargs[k].value if isinstance(kwargs[k], enum.Enum) else kwargs[k] - for k in self._args - if kwargs[k] is not None - }, - safe="/", - ) - ): - url += "?" + arg_part - return url - - -class HttpGetJsonCommand(HttpCommand): - """An HTTP command that performs a GET operation and receives JSON as response""" - - async def __call__( - self, - __communicator__: GoProHttp, - rules: list[MessageRules] | None = None, - **kwargs: Any, - ) -> GoProResp: - """Execute the command by sending it via HTTP - - Args: - __communicator__ (GoProHttp): HTTP communicator - rules (Optional[dict[MessageRules, RuleSignature]], optional): rules to apply when executing this - message. Defaults to None. - **kwargs (Any): arguments to message - - Returns: - GoProResp: Response received via HTTP - """ - url = self.build_url(**kwargs) - # Send to camera - 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 - +def http_get_json_command( + endpoint: str, + components: list[str] | None = None, + arguments: list[str] | None = None, + parser: Parser | None = None, + identifier: str | None = None, + rules: MessageRules = MessageRules(), +) -> Callable: + """Decorator to build and encapsulate a an Http Message that performs a GET to return JSON. -# pylint: disable = missing-class-docstring, arguments-differ -class HttpGetBinary(HttpCommand): - """An HTTP command that performs a GET operation and receives a binary stream as response""" + Args: + endpoint (str): base endpoint + components (list[str] | None): Additional path components (i.e. endpoint/{COMPONENT}). Defaults to None. + arguments (list[str] | None): Any arguments to be appended after endpoint (i.e. endpoint?{ARGUMENT}). Defaults to None. + parser (Parser | None, optional): Parser to handle received JSON. Defaults to None. + identifier (types.IdType | None): explicit message identifier. If None, will be generated from endpoint. + rules (MessageRules): rules this Message must obey. Defaults to MessageRules(). - 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 + Returns: + Callable: built callable to perform operation + """ + message = HttpMessage( + endpoint=endpoint, identifier=identifier, components=components, arguments=arguments, parser=parser + ) - Args: - __communicator__ (GoProHttp): HTTP communicator to query - camera_file (str): file on camera to access - local_file (Optional[Path], optional): file on local device to write to. Defaults to None - (camera-file will be used). + @wrapt.decorator + async def wrapper(wrapped: Callable, instance: HttpMessages, _: Any, kwargs: Any) -> GoProResp: + return await instance._communicator._get_json(message, rules=rules, **(await wrapped(**kwargs) or kwargs)) - Returns: - GoProResp: location on local device that file was written to - """ - # The method that will actually send the command and receive the stream - local_file = local_file or Path(".") / Path(camera_file).name - url = self.build_url(path=camera_file) - logger.info( - Logger.build_log_tx_str( - pretty_print(self._as_dict(endpoint=url, camera_file=camera_file, local_file=local_file)) - ) - ) - # Send to camera - response = await __communicator__._stream_to_file(url, local_file) - logger.info( - Logger.build_log_rx_str(pretty_print(self._as_dict(status="SUCCESS", endpoint=url, local_file=local_file))) - ) - return response + return wrapper -def http_get_json_command( +def http_get_binary_command( endpoint: str, components: list[str] | None = None, arguments: list[str] | None = None, parser: Parser | None = None, identifier: str | None = None, - rules: dict[MessageRules, RuleSignature] | None = None, + rules: MessageRules = MessageRules(), ) -> Callable: - """Factory to build an HttpGetJson command and wrapper to execute it + """Decorator to build and encapsulate a an Http Message that performs a GET to return a binary. Args: endpoint (str): base 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. - identifier (Optional[str]): explicitly set message identifier. Defaults to None (generated from endpoint). - rules (dict[MessageRules, RuleSignature], optional): Rules to be applied to message execution + components (list[str] | None): Additional path components (i.e. endpoint/{COMPONENT}). Defaults to None. + arguments (list[str] | None): Any arguments to be appended after endpoint (i.e. endpoint?{ARGUMENT}). Defaults to None. + parser (Parser | None, optional): Parser to handle received JSON. Defaults to None. + identifier (types.IdType | None): explicit message identifier. If None, will be generated from endpoint. + rules (MessageRules): rules this Message must obey. Defaults to MessageRules(). Returns: - Callable: Generated method to perform command + Callable: built callable to perform operation """ - message = HttpGetJsonCommand(endpoint, components, arguments, parser, identifier, rules=rules) + message = HttpMessage( + endpoint=endpoint, identifier=identifier, components=components, arguments=arguments, parser=parser + ) @wrapt.decorator 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) + kwargs = await wrapped(**kwargs) or kwargs + # If no local file was passed, used the file name of the camera file + kwargs["local_file"] = ( + kwargs.pop("local_file") if "local_file" in kwargs else Path(kwargs["camera_file"].split("/")[-1]) ) + return await instance._communicator._get_stream(message, rules=rules, **kwargs) return wrapper -def http_get_binary_command( +def http_put_json_command( endpoint: str, components: list[str] | None = None, arguments: list[str] | None = None, + body_args: list[str] | None = None, parser: Parser | None = None, identifier: str | None = None, + rules: MessageRules = MessageRules(), ) -> Callable: - """Factory to build an HttpGetBinary command and wrapper to execute it + """Decorator to build and encapsulate a an Http Message that performs a PUT to return JSON. Args: endpoint (str): base 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. - identifier (Optional[IdType]): explicitly set message identifier. Defaults to None (generated from endpoint). + components (list[str] | None): Additional path components (i.e. endpoint/{COMPONENT}). Defaults to None. + arguments (list[str] | None): Any arguments to be appended after endpoint (i.e. endpoint?{ARGUMENT}). Defaults to None. + body_args (list[str] | None, optional): Arguments to be added to the body JSON. Defaults to None. + parser (Parser | None, optional): Parser to handle received JSON. Defaults to None. + identifier (types.IdType | None): explicit message identifier. If None, will be generated from endpoint. + rules (MessageRules): rules this Message must obey. Defaults to MessageRules(). Returns: - Callable: Generated method to perform command + Callable: built callable to perform operation """ - message = HttpGetBinary(endpoint, components, arguments, parser, identifier) + message = HttpMessage( + endpoint=endpoint, + identifier=identifier, + body_args=body_args, + arguments=arguments, + components=components, + parser=parser, + ) @wrapt.decorator async def wrapper(wrapped: Callable, instance: HttpMessages, _: Any, kwargs: Any) -> GoProResp: - return await message(instance._communicator, **(await wrapped(**kwargs) or kwargs)) + return await instance._communicator._put_json(message, rules=rules, **(await wrapped(**kwargs) or kwargs)) return wrapper -class HttpSetting(HttpMessage[SettingId], Generic[ValueType]): +class HttpSetting(HttpMessage, Generic[ValueType]): """An individual camera setting that is interacted with via Wifi.""" def __init__(self, communicator: GoProHttp, identifier: SettingId) -> None: - super().__init__( - "gopro/camera/setting?setting={}&option={}", - identifier=identifier, - ) + super().__init__("gopro/camera/setting?setting={setting}&option={option}", identifier) self._communicator = communicator # Note! It is assumed that BLE and HTTP settings are symmetric so we only add to the communicator's # parser in the BLE Setting. - async def __call__(self, __communicator__: GoProHttp, **kwargs: Any) -> Any: - """Not applicable for settings + def __str__(self) -> str: + return str(self._identifier).lower().replace("_", " ").title() + + def build_url(self, **kwargs: Any) -> str: + """Build the endpoint from the current arguments Args: - __communicator__ (GoProHttp): HTTP communicator - **kwargs (Any): not used + kwargs (Any): run-time arguments - Raises: - NotImplementedError: not applicable + Returns: + str: built URL """ - raise NotImplementedError - - def __str__(self) -> str: - return str(self._identifier).lower().replace("_", " ").title() + return self._endpoint.format(setting=int(self._identifier), option=int(kwargs["value"])) async def set(self, value: ValueType) -> GoProResp: """Set the value of the setting. @@ -991,16 +882,7 @@ async def set(self, value: ValueType) -> GoProResp: Returns: GoProResp: Status of set """ - value = value.value if isinstance(value, enum.Enum) else value - url = self._endpoint.format(int(self._identifier), value) - logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(value=value, endpoint=url)))) - # Send to camera - 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)) + response = await self._communicator._get_json(self, value=value) + 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 7d601575..26843b1d 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 @@ -16,6 +16,7 @@ HttpSetting, http_get_binary_command, http_get_json_command, + http_put_json_command, ) from open_gopro.api.parsers import JsonParsers from open_gopro.communicator_interface import ( @@ -24,7 +25,7 @@ HttpMessages, MessageRules, ) -from open_gopro.constants import CmdId, SettingId +from open_gopro.constants import SettingId from open_gopro.models import CameraInfo, MediaList, MediaMetadata, MediaPath from open_gopro.models.general import WebcamResponse from open_gopro.models.response import GoProResp @@ -35,16 +36,12 @@ logger = logging.getLogger(__name__) -class HttpCommands(HttpMessages[HttpMessage, CmdId]): +class HttpCommands(HttpMessages[HttpMessage]): """All of the HTTP commands. To be used as a delegate for a GoProHttp to build commands """ - ##################################################################################################### - # HTTP GET JSON COMMANDS - ##################################################################################################### - @http_get_json_command( endpoint="gopro/media/last_captured", parser=Parser(json_parser=JsonParsers.PydanticAdapter(MediaPath)), @@ -56,25 +53,28 @@ async def get_last_captured_media(self) -> GoProResp[MediaPath]: GoProResp[MediaPath]: path of last captured media file """ - # async def update_custom_preset( - # self, - # icon_id: proto.EnumPresetIcon.ValueType | None = None, - # title: str | proto.EnumPresetTitle.ValueType | None = None, - # ) -> GoProResp[MediaPath]: - # """Update a custom preset title and / or icon - - # Args: - # icon_id (proto.EnumPresetIcon.ValueType | None, optional): Icon ID. Defaults to None. - # title (str | proto.EnumPresetTitle.ValueType | None, optional): Custom Preset name or Factory Title ID. Defaults to None. + @http_put_json_command( + endpoint="gopro/camera/presets/update_custom", + body_args=["custom_name", "icon_id", "title_id"], + ) + async def update_custom_preset( + self, + *, + icon_id: proto.EnumPresetIcon.ValueType | None = None, + title_id: str | proto.EnumPresetTitle.ValueType | None = None, + custom_name: str | None = None, + ) -> GoProResp[None]: + """For a custom preset, update the Icon and / or the Title - # Raises: - # ValueError: Did not set a parameter - # TypeError: Title was not proto.EnumPresetTitle.ValueType or string + Args: + icon_id (proto.EnumPresetIcon.ValueType | None): Icon to use. Defaults to None. + title_id (str | proto.EnumPresetTitle.ValueType | None): Title to use. Defaults to None. + custom_name (str | None): Custom name to use if title_id is set to + `proto.EnumPresetTitle.PRESET_TITLE_USER_DEFINED_CUSTOM_NAME`. Defaults to None. - # Returns: - # GoProResp[proto.ResponseGeneric]: status of preset update - # """ - # raise NotImplementedError("Need to add support for PUT requests") + Returns: + GoProResp: command status + """ @http_get_json_command(endpoint="gopro/camera/digital_zoom", arguments=["percent"]) async def set_digital_zoom(self, *, percent: int) -> GoProResp[None]: @@ -90,7 +90,7 @@ async def set_digital_zoom(self, *, percent: int) -> GoProResp[None]: @http_get_json_command( endpoint="gopro/camera/state", parser=Parser(json_parser=JsonParsers.CameraStateParser()), - rules={MessageRules.FASTPASS: lambda **kwargs: True}, + rules=MessageRules(fastpass_analyzer=MessageRules.always_true), ) async def get_camera_state(self) -> GoProResp[types.CameraState]: """Get all camera statuses and settings @@ -122,11 +122,11 @@ async def set_keep_alive(self) -> GoProResp[None]: arguments=["path"], parser=Parser(json_parser=JsonParsers.PydanticAdapter(MediaMetadata)), ) - async def get_media_metadata(self, *, file: str) -> GoProResp[MediaMetadata]: + async def get_media_metadata(self, *, path: str) -> GoProResp[MediaMetadata]: """Get media metadata for a file. Args: - file (str): Media file to get metadata for + path (str): Path on camera of media file to get metadata for Returns: GoProResp: Media metadata JSON structure @@ -166,7 +166,7 @@ async def get_open_gopro_api_version(self) -> GoProResp[str]: GoProResp: Open GoPro Version """ - # TODO make pydantic + # TODO make pydantic model of preset status @http_get_json_command(endpoint="gopro/camera/presets/get") async def get_preset_status(self) -> GoProResp[types.JsonDict]: """Get status of current presets @@ -226,10 +226,10 @@ async def set_third_party_client_info(self) -> GoProResp[None]: @http_get_json_command( endpoint="gopro/camera/shutter", components=["mode"], - rules={ - MessageRules.FASTPASS: lambda **kwargs: kwargs["shutter"] == Params.Toggle.DISABLE, - MessageRules.WAIT_FOR_ENCODING_START: lambda **kwargs: kwargs["shutter"] == Params.Toggle.ENABLE, - }, + rules=MessageRules( + fastpass_analyzer=lambda **kwargs: kwargs["mode"] == "stop", + wait_for_encoding_analyzer=lambda **kwargs: kwargs["mode"] == "start", + ), ) async def set_shutter(self, *, shutter: Params.Toggle) -> GoProResp[None]: """Set the shutter on or off @@ -340,6 +340,7 @@ async def remove_file_hilight( @http_get_json_command( endpoint="gopro/webcam/exit", parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + rules=MessageRules(fastpass_analyzer=MessageRules.always_true), ) async def webcam_exit(self) -> GoProResp[WebcamResponse]: """Exit the webcam. @@ -351,6 +352,7 @@ async def webcam_exit(self) -> GoProResp[WebcamResponse]: @http_get_json_command( endpoint="gopro/webcam/preview", parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + rules=MessageRules(fastpass_analyzer=MessageRules.always_true), ) async def webcam_preview(self) -> GoProResp[WebcamResponse]: """Start the webcam preview. @@ -363,6 +365,7 @@ async def webcam_preview(self) -> GoProResp[WebcamResponse]: endpoint="gopro/webcam/start", arguments=["res", "fov", "port", "protocol"], parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + rules=MessageRules(fastpass_analyzer=MessageRules.always_true), ) async def webcam_start( self, @@ -390,7 +393,7 @@ async def webcam_start( @http_get_json_command( endpoint="gopro/webcam/stop", - rules={MessageRules.FASTPASS: lambda **kwargs: True}, + rules=MessageRules(fastpass_analyzer=MessageRules.always_true), parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), ) async def webcam_stop(self) -> GoProResp[WebcamResponse]: @@ -403,6 +406,7 @@ async def webcam_stop(self) -> GoProResp[WebcamResponse]: @http_get_json_command( endpoint="gopro/webcam/status", parser=Parser(json_parser=JsonParsers.PydanticAdapter(WebcamResponse)), + rules=MessageRules(fastpass_analyzer=MessageRules.always_true), ) async def webcam_status(self) -> GoProResp[WebcamResponse]: """Get the current status of the webcam @@ -426,10 +430,6 @@ async def wired_usb_control(self, *, control: Params.Toggle) -> GoProResp[None]: """ return {"p": control} # type: ignore - ###################################################################################################### - # HTTP GET BINARY COMMANDS - ###################################################################################################### - @http_get_binary_command(endpoint="gopro/media/gpmf", arguments=["path"]) async def get_gpmf_data(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Get GPMF data for a file. @@ -501,7 +501,7 @@ async def download_file(self, *, camera_file: str, local_file: Path | None = Non """ -class HttpSettings(HttpMessages[HttpSetting, SettingId]): +class HttpSettings(HttpMessages[HttpSetting]): # pylint: disable=missing-class-docstring, unused-argument """The collection of all HTTP Settings @@ -566,9 +566,7 @@ def __init__(self, communicator: GoProHttp): ) """Camera controls configuration.""" - self.video_easy_mode: HttpSetting[Params.Speed] = HttpSetting[Params.Speed]( - communicator, SettingId.VIDEO_EASY_MODE - ) + self.video_easy_mode: HttpSetting[int] = HttpSetting[int](communicator, SettingId.VIDEO_EASY_MODE) """Video easy mode speed.""" self.photo_easy_mode: HttpSetting[Params.PhotoEasyMode] = HttpSetting[Params.PhotoEasyMode]( 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 d7451fc4..1926b5cb 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 @@ -20,9 +20,9 @@ class Resolution(GoProIntEnum): RES_4K_4_3 = 18 RES_5K = 24 RES_5K_4_3 = 25 - RES_5_3K_8_7 = 26 + RES_5_3K_8_7_LEGACY = 26 RES_5_3K_4_3 = 27 - RES_4K_8_7 = 28 + RES_4K_8_7_LEGACY = 28 RES_4K_9_16 = 29 RES_1080_9_16 = 30 RES_5_3K = 100 @@ -32,6 +32,11 @@ class Resolution(GoProIntEnum): RES_2_7K_16_9 = 104 RES_2_7K_4_3_TODO = 105 RES_1080_16_9 = 106 + RES_5_3K_8_7 = 107 + RES_4K_8_7 = 108 + RES_4K_8_7_ = 109 + RES_1080_8_7 = 110 + RES_2_7_K_8_7 = 11 class WebcamResolution(GoProIntEnum): @@ -302,58 +307,6 @@ class HorizonLeveling(GoProIntEnum): LOCKED = 2 -class Speed(GoProIntEnum): - ULTRA_SLO_MO_8X = 0 - SUPER_SLO_MO_4X = 1 - SLO_MO_2X = 2 - LOW_LIGHT_1X = 3 - SUPER_SLO_MO_4X_EXT_BATT = 4 - SLO_MO_2X_EXT_BATT = 5 - LOW_LIGHT_1X_EXT_BATT = 6 - ULTRA_SLO_MO_8X_50_HZ = 7 - SUPER_SLO_MO_4X_50_HZ = 8 - SLO_MO_2X_50_HZ = 9 - LOW_LIGHT_1X_50_HZ = 10 - SUPER_SLO_MO_4X_EXT_BATT_50_HZ = 11 - SLO_MO_2X_EXT_BATT_50_HZ = 12 - LOW_LIGHT_1X_EXT_BATT_50_HZ = 13 - ULTRA_SLO_MO_8X_EXT_BATT = 14 - ULTRA_SLO_MO_8X_EXT_BATT_50_HZ = 15 - ULTRA_SLO_MO_8X_LONG_BATT = 16 - SUPER_SLO_MO_4X_LONG_BATT = 17 - SLO_MO_2X_LONG_BATT = 18 - LOW_LIGHT_1X_LONG_BATT = 19 - ULTRA_SLO_MO_8X_LONG_BATT_50_HZ = 20 - SUPER_SLO_MO_4X_LONG_BATT_50_HZ = 21 - SLO_MO_2X_LONG_BATT_50_HZ = 22 - LOW_LIGHT_1X_LONG_BATT_50_HZ = 23 - SLO_MO_2X_4K = 24 - 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(GoProIntEnum): OFF = 0 ON = 1 @@ -468,3 +421,9 @@ class PhotoDuration(GoProIntEnum): HOUR_1 = 7 HOUR_2 = 8 HOUR_3 = 9 + + +class PresetGroup(GoProIntEnum): + VIDEO = 1000 + PHOTO = 1001 + TIMELAPSE = 1002 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 index 41e51a62..d165c032 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/api/parsers.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/parsers.py @@ -33,32 +33,6 @@ 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") @@ -250,14 +224,14 @@ class ProtobufByteParser(BytesParser[dict]): 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)) + # TODO can translate from Protobuf enums without relying on Protobuf internal implementation? # Monkey patch the field-to-json function to use our enum translation - ProtobufPrinter._FieldToJsonObject = ( - lambda self, field, value: enum_factory(field.enum_type)(value) + 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) ) @@ -331,7 +305,31 @@ class Construct(BytesParserBuilder): """ def __init__(self, construct: Construct) -> None: - self._construct = construct_adapter_factory(construct) + self._construct = self._construct_adapter_factory(construct) + + @classmethod + def _construct_adapter_factory(cls, 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() def parse(self, data: bytes) -> Construct: """Parse bytes into construct container diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/communicator_interface.py b/demos/python/sdk_wireless_camera_control/open_gopro/communicator_interface.py index feeed018..4379d1de 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/communicator_interface.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/communicator_interface.py @@ -11,7 +11,8 @@ import re from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, Generator, Generic, Pattern, Protocol, TypeVar, Union +from typing import Any, Generator, Generic, Pattern, Protocol, TypeVar +from urllib.parse import urlencode from construct import Bit, BitsInteger, BitStruct, Const, Construct, Padding @@ -25,7 +26,7 @@ DisconnectHandlerType, NotiHandlerType, ) -from open_gopro.constants import ActionId, CmdId, GoProUUIDs, SettingId, StatusId +from open_gopro.constants import GoProUUIDs from open_gopro.models.response import GoProResp, Header from open_gopro.parser_interface import ( BytesParser, @@ -39,6 +40,60 @@ logger = logging.getLogger(__name__) + +class MessageRules: + """Message Rules Manager + + Args: + fastpass_analyzer (Analyzer): used to check if message is fastpass. Defaults to always_false. + wait_for_encoding_analyer (Analyzer): Used to check if message should wait for encoding. Defaults to always_false. + """ + + class Analyzer(Protocol): + """Protocol definition of message rules analyzer""" + + def __call__(self, **kwargs: Any) -> bool: + """Analyze the current inputs to see if the rule should be applied + + Args: + kwargs (Any): input arguments + + Returns: + bool: Should the rule be applied? + """ + + always_false: Analyzer = lambda **kwargs: False + always_true: Analyzer = lambda **kwargs: True + + def __init__( + self, fastpass_analyzer: Analyzer = always_false, wait_for_encoding_analyzer: Analyzer = always_false + ) -> None: + self._analyze_fastpass = fastpass_analyzer + self._analyze_wait_for_encoding = wait_for_encoding_analyzer + + def is_fastpass(self, **kwargs: Any) -> bool: + """Is this command fastpass? + + Args: + kwargs (Any) : Arguments passed into the message + + Returns: + bool: result of rule check + """ + return self._analyze_fastpass(**kwargs) + + def should_wait_for_encoding_start(self, **kwargs: Any) -> bool: + """Should this message wait for encoding to start? + + Args: + kwargs (Any) : Arguments passed into the message + + Returns: + bool: result of rule check + """ + return self._analyze_wait_for_encoding(**kwargs) + + ############################################################################################################## ####### Communicators / Clients ############################################################################################################## @@ -70,33 +125,55 @@ def unregister_update(self, callback: types.UpdateCb, update: types.UpdateType | class GoProHttp(BaseGoProCommunicator): - """Base class interface for all HTTP commands""" + """Interface definition for all HTTP communicators""" @abstractmethod - async def _http_get(self, url: str, parser: Parser | None = None, **kwargs: Any) -> GoProResp: - """Send an HTTP GET request to a string endpoint. + async def _get_json( + self, message: HttpMessage, *, timeout: int = 0, rules: MessageRules | None = MessageRules(), **kwargs: Any + ) -> GoProResp: + """Perform a GET operation that returns JSON Args: - url (str): endpoint not including GoPro base path - parser (Optional[JsonParser]): Optional parser to further parse received JSON dict. Defaults to - None. - **kwargs (Any): - - rules (list[MessageRules]): rules to be enforced for this message + message (HttpMessage): operation description + timeout (int): time (in seconds) to wait to receive response before returning error. Defaults to 0. + rules (MessageRules | None): message rules that this operation will obey. Defaults to MessageRules(). + kwargs (Any) : any run-time arguments to apply to the operation Returns: - GoProResp: GoPro response + GoProResp: response parsed from received JSON """ - raise NotImplementedError @abstractmethod - 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. + async def _get_stream( + self, message: HttpMessage, *, timeout: int = 0, rules: MessageRules | None = MessageRules(), **kwargs: Any + ) -> GoProResp: + """Perform a GET operation that returns a binary stream Args: - url (str): endpoint URL - file (Path): location where file should be downloaded to + message (HttpMessage): operation description + timeout (int): time (in seconds) to wait to receive response before returning error. Defaults to 0. + rules (MessageRules | None): message rules that this operation will obey. Defaults to MessageRules(). + kwargs (Any) : any run-time arguments to apply to the operation + + Returns: + GoProResp: response wrapper around downloaded binary + """ + + @abstractmethod + async def _put_json( + self, message: HttpMessage, *, timeout: int = 0, rules: MessageRules | None = MessageRules(), **kwargs: Any + ) -> GoProResp: + """Perform a PUT operation that returns JSON + + Args: + message (HttpMessage): operation description + timeout (int): time (in seconds) to wait to receive response before returning error. Defaults to 0. + rules (MessageRules | None): message rules that this operation will obey. Defaults to MessageRules(). + kwargs (Any) : any run-time arguments to apply to the operation + + Returns: + GoProResp: response parsed from received JSON """ - raise NotImplementedError class GoProWifi(GoProHttp): @@ -107,7 +184,6 @@ class GoProWifi(GoProHttp): """ def __init__(self, controller: WifiController): - GoProHttp.__init__(self) self._wifi: WifiClient = WifiClient(controller) @property @@ -156,34 +232,35 @@ def __init__( @abstractmethod async def _send_ble_message( - self, uuid: BleUUID, data: bytearray, response_id: types.ResponseType, **kwargs: Any + self, message: BleMessage, rules: MessageRules = MessageRules(), **kwargs: Any ) -> GoProResp: - """Write a characteristic and block until its corresponding notification response is received. + """Perform a GATT write with response and accumulate received notifications into a response. Args: - uuid (BleUUID): characteristic to write to - data (bytearray): bytes to write - response_id (types.ResponseType): identifier to claim parsed response in notification handler - **kwargs (Any): - - rules (list[MessageRules]): rules to be enforced for this message + message (BleMessage): BLE operation description + rules (MessageRules): message rules that this operation will obey. Defaults to MessageRules(). + kwargs (Any) : any run-time arguments to apply to the operation Returns: - GoProResp: received response + GoProResp: response parsed from accumulated BLE notifications """ - raise NotImplementedError @abstractmethod - async def _read_characteristic(self, uuid: BleUUID) -> GoProResp: - """Read a characteristic and block until its corresponding notification response is received. + async def _read_ble_characteristic( + self, message: BleMessage, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + """Perform a direct GATT read of a characteristic Args: - uuid (BleUUID): characteristic ro read + message (BleMessage): BLE operation description + rules (MessageRules): message rules that this operation will obey. Defaults to MessageRules(). + kwargs (Any) : any run-time arguments to apply to the operation Returns: - GoProResp: data read from characteristic + GoProResp: response parsed from bytes read from characteristic """ - raise NotImplementedError + # TODO this should be somewhere else @classmethod def _fragment(cls, data: bytearray) -> Generator[bytearray, None, None]: """Fragment data in to MAX_BLE_PKT_LEN length packets @@ -246,7 +323,7 @@ def _fragment(cls, data: bytearray) -> Generator[bytearray, None, None]: yield packet -class GoProWiredInterface(GoProHttp): +class GoProWiredInterface(BaseGoProCommunicator): """The top-level interface for a Wired Open GoPro controller""" @@ -279,8 +356,6 @@ def __init__( GoProWifi.__init__(self, wifi_controller) -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) @@ -290,82 +365,22 @@ def __init__( ############################################################################################################## -class RuleSignature(Protocol): - """Protocol definition for a rule evaluation function""" - - def __call__(self, **kwargs: Any) -> bool: - """Function signature to evaluate a message rule - - Args: - **kwargs (Any): arguments to user-facing message method - - Returns: - bool: Whether or not message rule is currently enforced - """ - - -class MessageRules(enum.Enum): - """Rules to be applied when message is executed""" - - FASTPASS = enum.auto() #: Message can be sent when the camera is busy and / or encoding - WAIT_FOR_ENCODING_START = enum.auto() #: Message must not complete until encoding has started - - -class Message(Generic[CommunicatorType, IdType], ABC): +class Message(ABC): """Base class for all messages that will be contained in a Messages class""" def __init__( self, - identifier: IdType, + identifier: types.IdType, 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 (dict[MessageRules, RuleSignature] | None): rules to apply when executing this - message. Defaults to None. - """ - self._identifier: IdType = identifier + self._identifier: types.IdType = identifier self._parser = parser - self._rules = rules or {} - - def _evaluate_rules(self, **kwargs: Any) -> list[MessageRules]: - """Given the arguments for the current message execution, which rules should be enforced? - - Args: - **kwargs (Any): user-facing arguments to the current message execution - - Returns: - list[MessageRules]: list of rules to be enforced - """ - enforced_rules = [] - for rule, evaluator in self._rules.items(): - if evaluator(**kwargs): - enforced_rules.append(rule) - return enforced_rules - - @abstractmethod - async def __call__(self, __communicator__: CommunicatorType, **kwargs: Any) -> Any: - """Execute the message by sending it to the target device - - Args: - __communicator__ (CommunicatorType): communicator to send the message - **kwargs (Any): not used - - Returns: - Any: Value received from the device - """ - raise NotImplementedError @abstractmethod - def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: + def _as_dict(self, **kwargs: Any) -> types.JsonDict: """Return the attributes of the message as a dict Args: - *_ (Any): unused **kwargs (Any): additional entries for the dict Returns: @@ -374,7 +389,7 @@ def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: raise NotImplementedError -class BleMessage(Message[GoProBle, IdType]): +class BleMessage(Message): """The base class for all BLE messages to store common info Args: @@ -385,59 +400,81 @@ class BleMessage(Message[GoProBle, IdType]): def __init__( self, uuid: BleUUID, + identifier: types.IdType, parser: Parser | None, - identifier: IdType, - rules: dict[MessageRules, RuleSignature] | None = None, ) -> None: - Message.__init__(self, identifier, parser, rules) + Message.__init__(self, identifier, parser) self._uuid = uuid - self._base_dict = {"protocol": "BLE", "uuid": self._uuid} + self._base_dict = {"protocol": GoProResp.Protocol.BLE, "uuid": self._uuid} if parser: GlobalParsers.add(identifier, parser) + @abstractmethod + def _build_data(self, **kwargs: Any) -> bytearray: + """Build the raw write request from operation description and run-time arguments -class HttpMessage(Message[GoProHttp, IdType]): - """The base class for all HTTP messages. Stores common information.""" + Args: + kwargs (Any) : run-time arguments + + Returns: + bytearray: raw bytes request + """ + + +class HttpMessage(Message): + """The base class for all HTTP messages. Stores common information. + + Args: + endpoint (str): base endpoint + identifier (types.IdType | None): explicit message identifier. If None, will be generated from endpoint. + components (list[str] | None): Additional path components (i.e. endpoint/{COMPONENT}). Defaults to None. + arguments (list[str] | None): Any arguments to be appended after endpoint (i.e. endpoint?{ARGUMENT}). Defaults to None. + body_args (list[str] | None): Arguments to be added to the body JSON. Defaults to None. + headers (dict[str, Any] | None): A dict of values to be set in the HTTP operation headers. Defaults to None. + certificate (Path | None): Path to SSL certificate bundle. Defaults to None. + parser (Parser | None): Parser to interpret HTTP responses. Defaults to None. + """ def __init__( self, endpoint: str, - identifier: IdType, + identifier: types.IdType | None, components: list[str] | None = None, arguments: list[str] | None = None, + body_args: list[str] | None = None, + headers: dict[str, Any] | None = None, + certificate: Path | 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 (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. - """ + if not identifier: + # Build human-readable name from endpoint + identifier = endpoint.lower().removeprefix("gopro/").replace("/", " ").replace("_", " ").title() + try: + identifier = identifier.split("?")[0].strip("{}") + except IndexError: + pass + + self._headers = headers or {} self._endpoint = endpoint - self._components = components - self._args = arguments - Message.__init__(self, identifier, parser, rules=rules) + self._components = components or [] + self._arguments = arguments or [] + self._body_args = body_args or [] + self._certificate = certificate + Message.__init__(self, identifier, parser) self._base_dict: types.JsonDict = { "id": self._identifier, - "protocol": "HTTP", + "protocol": GoProResp.Protocol.HTTP, "endpoint": self._endpoint, } def __str__(self) -> str: return str(self._identifier).title() - def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: + def _as_dict(self, **kwargs: Any) -> types.JsonDict: """Return the attributes of the message as a dict Args: - *_ (Any): unused **kwargs (Any): additional entries for the dict Returns: @@ -446,11 +483,53 @@ def _as_dict(self, *_: Any, **kwargs: Any) -> types.JsonDict: # 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()} + def build_body(self, **kwargs: Any) -> dict[str, Any]: + """Build JSON body from run-time body arguments + + Args: + **kwargs (Any): run-time arguments to check to see if each should be added to the body + + Returns: + dict[str, Any]: built JSON body + """ + body: dict[str, Any] = {} + for name, value in kwargs.items(): + if name in self._body_args: + body[name] = value + return body + + def build_url(self, **kwargs: Any) -> str: + """Build the URL string from the passed in components and arguments + + Args: + **kwargs (Any): additional entries for the dict + + Returns: + str: built URL + """ + url = self._endpoint + for component in self._components: + url += "/" + kwargs.pop(component) + # Append parameters + if self._arguments and ( + arg_part := urlencode( + { + k: kwargs[k].value if isinstance(kwargs[k], enum.Enum) else kwargs[k] + for k in self._arguments + if kwargs[k] is not None + }, + safe="/", + ) + ): + url += "?" + arg_part + return url + MessageType = TypeVar("MessageType", bound=Message) +CommunicatorType = TypeVar("CommunicatorType", bound=BaseGoProCommunicator) -class Messages(ABC, dict, Generic[MessageType, IdType, CommunicatorType]): +class Messages(ABC, dict, Generic[MessageType, CommunicatorType]): """Base class for setting and status containers Allows message groups to be iterable and supports dict-like access. @@ -467,10 +546,10 @@ def __init__(self, communicator: CommunicatorType) -> None: """ self._communicator = communicator # Append any automatically discovered instance attributes (i.e. for settings and statuses) - message_map: dict[IdType | str, MessageType] = {} + message_map: dict[types.IdType, MessageType] = {} for message in self.__dict__.values(): - if isinstance(message, Message): - message_map[message._identifier] = message # type: ignore + if hasattr(message, "_identifier"): + message_map[message._identifier] = message # Append any automatically discovered methods (i.e. for commands) for name, method in inspect.getmembers(self, predicate=inspect.ismethod): if not name.startswith("_"): @@ -478,14 +557,14 @@ def __init__(self, communicator: CommunicatorType) -> None: dict.__init__(self, message_map) -class BleMessages(Messages[MessageType, IdType, GoProBle]): +class BleMessages(Messages[MessageType, GoProBle]): """A container of BLE Messages. Identical to Messages and it just used for typing """ -class HttpMessages(Messages[MessageType, IdType, GoProHttp]): +class HttpMessages(Messages[MessageType, GoProHttp]): """A container of HTTP Messages. Identical to Messages and it just used for typing diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/cohn.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/cohn.py index 2bf126c6..e0fac91a 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/cohn.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/cohn.py @@ -1,13 +1,12 @@ -# cohn.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Tue Oct 24 19:08:07 UTC 2023 - +# cohn.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Tue Oct 24 19:08:07 UTC 2023 + """Entrypoint for configuring and demonstrating Camera On the Home Network (COHN).""" from __future__ import annotations import argparse import asyncio -import logging from rich.console import Console @@ -17,19 +16,18 @@ console = Console() # rich consoler printer -logger: logging.Logger - MDNS_SERVICE = "_gopro-web._tcp.local." async def main(args: argparse.Namespace) -> None: - global logger logger = setup_logging(__name__, args.log) gopro: WirelessGoPro | None = None try: # Start with Wifi Disabled (i.e. don't allow camera in AP mode). async with WirelessGoPro(args.identifier, enable_wifi=False) as gopro: + await gopro.ble_command.cohn_clear_certificate() + if await gopro.is_cohn_provisioned: console.print("[yellow]COHN is already provisioned") else: @@ -42,6 +40,7 @@ async def main(args: argparse.Namespace) -> None: # Prove we can communicate via the COHN HTTP channel without a BLE or Wifi connection assert (await gopro.http_command.get_camera_state()).ok + console.print("Successfully communicated via COHN!!") except Exception as e: # pylint: disable = broad-except logger.error(repr(e)) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/custom_preset_udpate_demo.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/custom_preset_udpate_demo.py index 737d36de..17de6b68 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/custom_preset_udpate_demo.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/custom_preset_udpate_demo.py @@ -1,63 +1,57 @@ -# photo.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:45 PM +# custom_preset_udpate_demo.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Thu Mar 28 20:25:41 UTC 2024 + +"""Simple demo to modify a currently accessible custom preset's title and icon.""" -"""Entrypoint for taking a picture demo.""" - -import argparse import asyncio from pathlib import Path from rich.console import Console -from open_gopro import WiredGoPro, WirelessGoPro, proto -from open_gopro.gopro_base import GoProBase +from open_gopro import WiredGoPro, proto 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: - logger = setup_logging(__name__, args.log) - gopro: GoProBase | None = None +async def main() -> None: + logger = setup_logging(__name__, Path("custom_preset.log")) + gopro: WiredGoPro | None = None try: - async with ( - WiredGoPro(args.identifier) # type: ignore - if args.wired - else WirelessGoPro(args.identifier, wifi_interface=args.wifi_interface) - ) as gopro: - assert gopro - ble_last_file = (await gopro.ble_command.get_last_captured_media()).data - http_last_file = (await gopro.http_command.get_last_captured_media()).data - assert ble_last_file.media.folder == http_last_file.folder - assert ble_last_file.media.file == http_last_file.file - - presets = (await gopro.ble_command.get_preset_status()).data + async with WiredGoPro() as gopro: + presets = (await gopro.http_command.get_preset_status()).data custom_preset_id: int | None = None - for group in presets.preset_group_array: - for preset in group.preset_array: - if preset.user_defined: - custom_preset_id = preset.id + for group in presets["presetGroupArray"]: + for preset in group["presetArray"]: + if preset["userDefined"]: + custom_preset_id = preset["id"] if not custom_preset_id: raise RuntimeError("Could not find a custom preset.") # Ensure we can load it - assert (await gopro.ble_command.load_preset(preset=custom_preset_id)).ok + assert (await gopro.http_command.load_preset(preset=custom_preset_id)).ok # Now try to update it assert ( - await gopro.ble_command.custom_preset_update( - icon_id=proto.EnumPresetTitle.PRESET_TITLE_BIKE, - title="custom title", + await gopro.http_command.update_custom_preset( + icon_id=proto.EnumPresetIcon.PRESET_ICON_SNOW, + title_id=proto.EnumPresetTitle.PRESET_TITLE_SNOW, + ) + ).ok + input("press enter to continue") + assert ( + await gopro.http_command.update_custom_preset( + icon_id=proto.EnumPresetIcon.PRESET_ICON_MOTOR, + title_id=proto.EnumPresetTitle.PRESET_TITLE_MOTOR, ) ).ok input("press enter to continue") assert ( - await gopro.ble_command.custom_preset_update( - icon_id=proto.EnumPresetTitle.PRESET_TITLE_MOTOR, - title=proto.EnumPresetTitle.PRESET_TITLE_MOTOR, + await gopro.http_command.update_custom_preset( + custom_name="Custom Name", + icon_id=proto.EnumPresetIcon.PRESET_ICON_BIKE, + title_id=proto.EnumPresetTitle.PRESET_TITLE_USER_DEFINED_CUSTOM_NAME, ) ).ok - print("cheese") except Exception as e: # pylint: disable = broad-except logger.error(repr(e)) @@ -66,27 +60,5 @@ async def main(args: argparse.Namespace) -> None: await gopro.close() -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Connect to a GoPro camera, take a photo, then download it.") - parser.add_argument( - "--output", - type=Path, - help="Where to write the photo to. If not set, write to 'photo.jpg'", - default=Path("photo.jpg"), - ) - parser.add_argument( - "--wired", - action="store_true", - help="Set to use wired (USB) instead of wireless (BLE / WIFI) interface", - ) - - return add_cli_args_and_parse(parser) - - -# Needed for poetry scripts defined in pyproject.toml -def entrypoint() -> None: - asyncio.run(main(parse_arguments())) - - if __name__ == "__main__": - entrypoint() + asyncio.run(main()) 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 32735bc5..a67a6b44 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 @@ -50,7 +50,7 @@ async def wait_for_livestream_start(_: Any, update: proto.NotifyLiveStreamStatus console.print("[yellow]Waiting for livestream to be ready...\n") await livestream_is_ready.wait() - # TODO Is this still needed + # TODO Is this still needed? await asyncio.sleep(2) console.print("[yellow]Starting livestream") 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 72853aa4..24233237 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,7 +1,7 @@ # 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 webcam demo""" +"""USB / wireless webcam demo""" import argparse import asyncio 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 cf3117d9..0239cc4e 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 @@ -119,6 +119,7 @@ async def log_battery() -> None: ).data # Append initial sample SAMPLES.append(Sample(index=SAMPLE_INDEX, percentage=last_percentage, bars=last_bars)) + SAMPLE_INDEX += 1 console.print(str(SAMPLES[-1])) console.print("[bold green]Receiving battery notifications until it dies...") 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 67ff68ed..5034b237 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 @@ -19,6 +19,7 @@ async def main(args: argparse.Namespace) -> None: logger = setup_logging(__name__, args.log) + gopro: GoProBase | None = None try: @@ -28,33 +29,24 @@ async def main(args: argparse.Namespace) -> None: else WirelessGoPro(args.identifier, wifi_interface=args.wifi_interface) ) as gopro: assert gopro - # Configure settings to prepare for photo - 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((await gopro.http_command.get_media_list()).data.files) + # Get the media list before + media_set_before = set((await gopro.http_command.get_media_list()).data.files) + # Take a photo - # console.print("Capturing a photo...") - # assert (await gopro.http_command.set_shutter(shutter=Params.Toggle.ENABLE)).ok - - # # Get the media list after - # 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() - - # Get the last captured media - test = await gopro.http_command.get_last_captured_media() - # test = await gopro.ble_command.get_last_captured_media() - print(f"Photo from new command ==> {test.data.folder} ::: {test.data.file}") - - # # Download the photo - # console.print(f"Downloading {photo.filename}...") - # 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}") + assert (await gopro.http_command.set_shutter(shutter=Params.Toggle.ENABLE)).ok + + # Get the media list after + media_set_after = set((await gopro.http_command.get_media_list()).data.files) + # The video (is most likely) the difference between the two sets + photo = media_set_after.difference(media_set_before).pop() + + # Download the photo + console.print(f"Downloading {photo.filename}...") + await gopro.http_command.download_file(camera_file=photo.filename, local_file=args.output) + console.print(f"Success!! :smiley: File has been downloaded to {Path(args.output).absolute()}") + except Exception as e: # pylint: disable = broad-except logger.error(repr(e)) 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 04c8e99d..ef80585b 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 @@ -27,12 +27,6 @@ async def main(args: argparse.Namespace) -> None: else WirelessGoPro(args.identifier, wifi_interface=args.wifi_interface) ) as gopro: assert gopro - # Configure settings to prepare for video - 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 @@ -47,8 +41,9 @@ async def main(args: argparse.Namespace) -> None: 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...") + console.print(f"Downloading {video.filename}...") 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 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 35db422f..3ac17141 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 @@ -11,9 +11,8 @@ import logging import threading import traceback -from abc import ABC, abstractmethod -from pathlib import Path -from typing import Any, Awaitable, Callable, Final, Generic, Optional, TypeVar +from abc import abstractmethod +from typing import Any, Awaitable, Callable, Final, Generic, TypeVar import requests import wrapt @@ -29,9 +28,16 @@ WiredApi, WirelessApi, ) +from open_gopro.communicator_interface import ( + GoProHttp, + HttpMessage, + Message, + MessageRules, +) from open_gopro.constants import ErrorCode +from open_gopro.logger import Logger from open_gopro.models.response import GoProResp, RequestsHttpRespBuilderDirector -from open_gopro.parser_interface import Parser +from open_gopro.util import pretty_print logger = logging.getLogger(__name__) @@ -48,7 +54,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) -> Callable | None: """Catch any exceptions from this method and pass them to the exception handler identifier by thread name Args: @@ -88,10 +94,26 @@ def wrapper(wrapped: Callable, instance: GoProBase, args: Any, kwargs: Any) -> C return wrapper -class GoProBase(ABC, Generic[ApiType]): +@wrapt.decorator +async def enforce_message_rules(wrapped: MessageMethodType, instance: GoProBase, args: Any, kwargs: Any) -> GoProResp: + """Decorator proxy to call the GoProBase's _enforce_message_rules method. + + Args: + wrapped (MessageMethodType): Operation to enforce + instance (GoProBase): GoProBase instance to use + args (Any): positional arguments to wrapped + kwargs (Any): keyword arguments to wrapped + + Returns: + GoProResp: common response object + """ + return await instance._enforce_message_rules(wrapped, *args, **kwargs) + + +class GoProBase(GoProHttp, Generic[ApiType]): """The base class for communicating with all GoPro Clients""" - GET_TIMEOUT: Final = 5 + HTTP_TIMEOUT: Final = 5 HTTP_GET_RETRIES: Final = 5 def __init__(self, **kwargs: Any) -> None: @@ -262,6 +284,22 @@ async def is_cohn_provisioned(self) -> bool: # End Public API ########################################################################################################## + @abstractmethod + async def _enforce_message_rules( + self, wrapped: Callable, message: Message, rules: MessageRules, **kwargs: Any + ) -> GoProResp: + """Rule Enforcer. Called by enforce_message_rules decorator. + + Args: + wrapped (Callable): operation to enforce + message (Message): message passed to operation + rules (MessageRules): rules to enforce + kwargs (Any) : arguments passed to operation + + Returns: + GoProResp: Operation response + """ + def _handle_exception(self, source: Any, context: types.JsonDict) -> None: """Gather exceptions from module threads and send through callback if registered. @@ -320,7 +358,7 @@ def _ensure_opened(interface: tuple[GoProMessageInterface]) -> Callable: return ensure_opened(interface) @staticmethod - def _catch_thread_exception(*args: Any, **kwargs: Any) -> Optional[Callable]: + def _catch_thread_exception(*args: Any, **kwargs: Any) -> Callable | None: """Catch any exceptions from this method and pass them to the exception handler identifier by thread name Args: @@ -332,54 +370,38 @@ 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,)) - 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 - called from the instance's delegates (i.e. self.wifi_command and self.wifi_status) + def _build_http_request_args(self, message: HttpMessage) -> dict[str, Any]: + """Helper method to build request kwargs from message Args: - url (str): endpoint URL - 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 timeout seconds + message (HttpMessage): message to build args from Returns: - GoProResp: response + dict[str, Any]: built args """ - 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 + if message._headers: + request_args["headers"] = message._headers + if message._certificate: + request_args["verify"] = str(message._certificate) + return request_args + + @enforce_message_rules + async def _get_json( + self, message: HttpMessage, *, timeout: int = HTTP_TIMEOUT, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + url = self._base_url + message.build_url(**kwargs) + logger.debug(f"Sending: {url}") + logger.info(Logger.build_log_tx_str(pretty_print(message._as_dict(**kwargs)))) + response: GoProResp | None = None for retry in range(1, GoProBase.HTTP_GET_RETRIES + 1): try: - 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 - if not request.ok: - logger.warning(f"Received non-success status {request.status_code}: {request.reason}") - response = RequestsHttpRespBuilderDirector(request, parser)() + http_response = requests.get(url, timeout=timeout, **self._build_http_request_args(message)) + logger.trace(f"received raw json: {json.dumps(http_response.json() if http_response.text else {}, indent=4)}") # type: ignore + if not http_response.ok: + logger.warning(f"Received non-success status {http_response.status_code}: {http_response.reason}") + response = RequestsHttpRespBuilderDirector(http_response, message._parser)() break except requests.exceptions.ConnectionError as e: # This appears to only occur after initial connection after pairing @@ -393,36 +415,51 @@ async def _http_get( # pylint: disable=unused-argument raise GpException.ResponseTimeout(GoProBase.HTTP_GET_RETRIES) assert response is not None + logger.info(Logger.build_log_rx_str(pretty_print(response._as_dict()))) return response - @ensure_opened((GoProMessageInterface.HTTP,)) - 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 - called from the instance's delegates (i.e. self.wifi_command and self.wifi_status) - - Args: - url (str): endpoint URL - file (Path): location where file should be downloaded to - - Returns: - GoProResp: location of file that was written - """ - assert self.is_http_connected - - url = self._base_url + url - logger.debug(f"Sending: {url}") - with requests.get(url, stream=True, timeout=GoProBase.GET_TIMEOUT) as request: + @enforce_message_rules + async def _get_stream( + self, message: HttpMessage, *, timeout: int = HTTP_TIMEOUT, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + url = self._base_url + message.build_url(path=kwargs["camera_file"]) + logger.debug(f"Sending: {url}") + with requests.get(url, stream=True, timeout=timeout, **self._build_http_request_args(message)) as request: request.raise_for_status() + file = kwargs["local_file"] 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( - protocol=GoProResp.Protocol.HTTP, - status=ErrorCode.SUCCESS, - data=file, - identifier=url, - ) + return GoProResp(protocol=GoProResp.Protocol.HTTP, status=ErrorCode.SUCCESS, data=file, identifier=url) + + @enforce_message_rules + async def _put_json( + self, message: HttpMessage, *, timeout: int = HTTP_TIMEOUT, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + url = self._base_url + message.build_url(**kwargs) + body = message.build_body(**kwargs) + logger.debug(f"Sending: {url} with body: {json.dumps(body, indent=4)}") + response: GoProResp | None = None + for retry in range(1, GoProBase.HTTP_GET_RETRIES + 1): + try: + http_response = requests.put(url, timeout=timeout, json=body, **self._build_http_request_args(message)) + logger.trace(f"received raw json: {json.dumps(http_response.json() if http_response.text else {}, indent=4)}") # type: ignore + if not http_response.ok: + logger.warning(f"Received non-success status {http_response.status_code}: {http_response.reason}") + response = RequestsHttpRespBuilderDirector(http_response, message._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. 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)}") + logger.warning(f"Retrying #{retry} to send the command...") + else: + raise GpException.ResponseTimeout(GoProBase.HTTP_GET_RETRIES) + + assert response is not None + return response 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 ef1db4ce..e0370991 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 @@ -7,10 +7,7 @@ import asyncio import logging -from pathlib import Path -from typing import Any, Final - -import wrapt +from typing import Any, Callable, Final import open_gopro.exceptions as GpException import open_gopro.wifi.mdns_scanner # Imported this way for pytest monkeypatching @@ -24,9 +21,9 @@ Params, WiredApi, ) -from open_gopro.communicator_interface import GoProWiredInterface, MessageRules +from open_gopro.communicator_interface import GoProWiredInterface, Message, MessageRules from open_gopro.constants import StatusId -from open_gopro.gopro_base import GoProBase, MessageMethodType +from open_gopro.gopro_base import GoProBase from open_gopro.models import GoProResp logger = logging.getLogger(__name__) @@ -35,42 +32,6 @@ HTTP_GET_RETRIES: Final = 5 -@wrapt.decorator -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: - wrapped (MessageMethodType): Method that will be wrapped - instance (WiredGoPro): owner of method - args (Any): positional arguments - kwargs (Any): keyword arguments - - Returns: - GoProResp: forward response of message method - """ - rules: list[MessageRules] = kwargs.pop("rules", []) - - # Acquire ready lock unless we are initializing or this is a Set Shutter Off command - 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 - await instance._wait_for_state({StatusId.ENCODING: False, StatusId.SYSTEM_BUSY: False}) - logger.trace("Camera is ready to receive messages") # type: ignore - response = await wrapped(*args, **kwargs) - else: # Either we're not maintaining state, we're not opened yet, or this is a fastpass message - response = await wrapped(*args, **kwargs) - - # Release the lock if we acquired it - if instance._should_maintain_state: - 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 - await instance._wait_for_state({StatusId.ENCODING: True}) - return response - - class WiredGoPro(GoProBase[WiredApi], GoProWiredInterface): """The top-level USB interface to a Wired GoPro device. @@ -263,7 +224,7 @@ def is_http_connected(self) -> bool: Returns: bool: True if yes, False if no """ - return True # TODO find a better way to do this + return self.is_open def register_update(self, callback: types.UpdateCb, update: types.UpdateType) -> None: """Register for callbacks when an update occurs @@ -325,6 +286,29 @@ async def is_cohn_provisioned(self) -> bool: # End Public API ########################################################################################################## + async def _enforce_message_rules( + self, wrapped: Callable, message: Message, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + # Acquire ready lock unless we are initializing or this is a Set Shutter Off command + if self._should_maintain_state and self.is_open and not rules.is_fastpass(**kwargs): + # Wait for not encoding and not busy + logger.trace("Waiting for camera to be ready to receive messages.") # type: ignore + await self._wait_for_state({StatusId.ENCODING: False, StatusId.SYSTEM_BUSY: False}) + logger.trace("Camera is ready to receive messages") # type: ignore + response = await wrapped(message, **kwargs) + else: # Either we're not maintaining state, we're not opened yet, or this is a fastpass message + response = await wrapped(message, **kwargs) + + # Release the lock if we acquired it + if self._should_maintain_state: + if response.ok: + # Is there any special handling required after receiving the response? + if rules.should_wait_for_encoding_start(**kwargs): + logger.trace("Waiting to receive encoding started.") # type: ignore + # Wait for encoding to start + await self._wait_for_state({StatusId.ENCODING: True}) + return response + 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 @@ -337,6 +321,7 @@ async def _wait_for_state(self, check: types.CameraState) -> None: state = (await self.http_command.get_camera_state()).data for key, value in check.items(): if state.get(key) != value: + logger.trace(f"Not ready ==> {key} != {value}") # type: ignore await asyncio.sleep(self._poll_period) break # Get new state and try again else: @@ -359,7 +344,3 @@ def _base_url(self) -> str: if not self._serial: raise GpException.GoProNotOpened("Serial / IP has not yet been discovered") return WiredGoPro._BASE_ENDPOINT.format(ip=WiredGoPro._BASE_IP.format(*self._serial[-3:])) - - @enforce_message_rules - 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 5ecea303..7467a81a 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 @@ -6,14 +6,12 @@ from __future__ import annotations import asyncio +import enum import logging import queue from collections import defaultdict from copy import deepcopy -from pathlib import Path -from typing import Any, Final, Pattern - -import wrapt +from typing import Any, Callable, Final, Pattern import open_gopro.exceptions as GpException from open_gopro import proto, types @@ -27,14 +25,23 @@ WirelessApi, ) from open_gopro.ble import BleakWrapperController, BleUUID -from open_gopro.communicator_interface import GoProWirelessInterface, MessageRules +from open_gopro.communicator_interface import ( + BleMessage, + GoProWirelessInterface, + HttpMessage, + Message, + MessageRules, +) from open_gopro.constants import ActionId, GoProUUIDs, StatusId -from open_gopro.gopro_base import GoProBase, GoProMessageInterface, MessageMethodType +from open_gopro.gopro_base import ( + GoProBase, + GoProMessageInterface, + enforce_message_rules, +) from open_gopro.logger import Logger from open_gopro.models.general import CohnInfo 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.util import SnapshotQueue, get_current_dst_aware_time, pretty_print from open_gopro.wifi import WifiCli logger = logging.getLogger(__name__) @@ -42,47 +49,6 @@ KEEP_ALIVE_INTERVAL: Final = 28 -@wrapt.decorator -async def enforce_message_rules( - wrapped: MessageMethodType, instance: WirelessGoPro, args: Any, kwargs: Any -) -> GoProResp: - """Wrap the input message method, applying any message rules (MessageRules) - - Args: - wrapped (MessageMethodType): Method that will be wrapped - instance (WirelessGoPro): owner of method - args (Any): positional arguments - kwargs (Any): keyword arguments - - Returns: - GoProResp: forward response of message method - """ - rules: list[MessageRules] = kwargs.pop("rules", []) - - # Acquire ready lock unless we are initializing or this is a Set Shutter Off command - 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 - await instance._ready_lock.acquire() - logger.trace(f"{wrapped.__name__} has the lock") # type: ignore - have_lock = True - response = await wrapped(*args, **kwargs) - else: # Either we're not maintaining state, we're not opened yet, or this is a fastpass message - response = await wrapped(*args, **kwargs) - - # Release the lock if we acquired it - if instance._should_maintain_state: - if have_lock: - instance._ready_lock.release() - logger.trace(f"{wrapped.__name__} released the lock") # type: ignore - # 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 - - class WirelessGoPro(GoProBase[WirelessApi], GoProWirelessInterface): """The top-level BLE and Wifi interface to a Wireless GoPro device. @@ -145,6 +111,12 @@ class WirelessGoPro(GoProBase[WirelessApi], GoProWirelessInterface): WRITE_TIMEOUT: Final = 5 + class _LockOwner(enum.Enum): + """Current owner of the communication lock""" + + RULE_ENFORCER = enum.auto() + STATE_MANAGER = enum.auto() + def __init__( self, target: Pattern | None = None, @@ -193,6 +165,8 @@ def __init__( self._ble_disconnect_event: asyncio.Event if self._should_maintain_state: + self._state_tasks: list[asyncio.Task] = [] + self._lock_owner: WirelessGoPro._LockOwner | None = WirelessGoPro._LockOwner.STATE_MANAGER self._ready_lock: asyncio.Lock self._keep_alive_task: asyncio.Task self._encoding: bool @@ -486,10 +460,6 @@ async def is_cohn_provisioned(self) -> bool: """ return (await self.ble_command.cohn_get_status(register=False)).data.enabled - ########################################################################################################## - # End Public API - ########################################################################################################## - @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) async def keep_alive(self) -> bool: """Send a heartbeat to prevent the BLE connection from dropping. @@ -557,6 +527,34 @@ async def wait_for_provisioning(_: Any, result: proto.NotifProvisioningState) -> # End Public API ########################################################################################################## + async def _enforce_message_rules( + self, wrapped: Callable, message: Message, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + # Acquire ready lock unless we are initializing or this is a Set Shutter Off command + response: GoProResp + if self._should_maintain_state and self.is_open and not rules.is_fastpass(**kwargs): + logger.trace(f"{wrapped.__name__} acquiring lock") # type: ignore + await self._ready_lock.acquire() + logger.trace(f"{wrapped.__name__} has the lock") # type: ignore + self._lock_owner = WirelessGoPro._LockOwner.RULE_ENFORCER + response = await wrapped(message, **kwargs) + else: # Either we're not maintaining state, we're not opened yet, or this is a fastpass message + response = await wrapped(message, **kwargs) + + # Release the lock if we acquired it + if self._should_maintain_state: + if self._lock_owner is WirelessGoPro._LockOwner.RULE_ENFORCER: + logger.trace(f"{wrapped.__name__} releasing the lock") # type: ignore + self._lock_owner = None + self._ready_lock.release() + logger.trace(f"{wrapped.__name__} released the lock") # type: ignore + # Is there any special handling required after receiving the response? + if rules.should_wait_for_encoding_start(**kwargs): + logger.trace("Waiting to receive encoding started.") # type: ignore + await self._encoding_started.wait() + self._encoding_started.clear() + return response + async def _notify_listeners(self, update: types.UpdateType, value: Any) -> None: """Notify all registered listeners of this update @@ -595,13 +593,20 @@ async def _open_ble(self, timeout: int = 10, retries: int = 5) -> None: async def _update_internal_state(self, update: types.UpdateType, value: int) -> None: """Update the internal state based on a status update. + # Note!!! This needs to be reentrant-safe + Used to update encoding and / or busy status Args: update (types.UpdateType): type of update (status ID) value (int): updated value """ - have_lock = not await self.is_ready + # Cancel any currently pending state update tasks + for task in self._state_tasks: + logger.trace("Cancelling pending acquire task.") # type: ignore + task.cancel() + self._state_tasks = [] + logger.trace(f"State update received {update.name} ==> {value}") # type: ignore should_notify_encoding = False if update == StatusId.ENCODING: @@ -612,29 +617,29 @@ async def _update_internal_state(self, update: types.UpdateType, value: int) -> self._busy = bool(value) logger.trace(f"Current internal states: {self._encoding=} {self._busy=}") # type: ignore - ready_now = await self.is_ready - if have_lock and ready_now: + if self._lock_owner is WirelessGoPro._LockOwner.STATE_MANAGER and await self.is_ready: + logger.trace("Control releasing lock") # type: ignore + self._lock_owner = None self._ready_lock.release() logger.trace("Control released lock") # type: ignore - elif not have_lock and not ready_now: + elif not (self._lock_owner is WirelessGoPro._LockOwner.STATE_MANAGER) and not await self.is_ready: logger.trace("Control acquiring lock") # type: ignore - await self._ready_lock.acquire() + task = asyncio.create_task(self._ready_lock.acquire()) + self._state_tasks.append(task) + await task logger.trace("Control has lock") # type: ignore + self._lock_owner = WirelessGoPro._LockOwner.STATE_MANAGER if should_notify_encoding and self.is_open: logger.trace("Control setting encoded started") # type: ignore self._encoding_started.set() - # TODO this needs unit testing async def _route_response(self, response: GoProResp) -> None: """After parsing response, route it to any stakeholders (such as registered listeners) Args: - response (GoProResp): parsed response + response (GoProResp): parsed response to route """ - # Flatten data if possible - # from copy import copy - original_response = deepcopy(response) if response._is_query and not response._is_push: response.data = list(response.data.values())[0] @@ -675,22 +680,21 @@ async def _async_notification_handler() -> None: # 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) + logger.trace(f"Finished accumulating on {uuid}") # type: ignore # Clear active response from response dict del self._active_builders[uuid] + await self._route_response(builder.build()) 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() if self._should_maintain_state: self._keep_alive_task.cancel() await self._ble.close() await self._ble_disconnect_event.wait() + # TODO this event is never cleared since this object is not designed to be re-opened. def _disconnect_handler(self, _: Any) -> None: """Disconnect callback from BLE controller @@ -705,29 +709,16 @@ def _disconnect_handler(self, _: Any) -> None: @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) @enforce_message_rules async def _send_ble_message( - self, uuid: BleUUID, data: bytearray, response_id: types.ResponseType, **_: Any + self, message: BleMessage, rules: MessageRules = MessageRules(), **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 (types.ResponseType): identifier to claim parsed response in notification handler - **_ (Any): not used - - Raises: - ResponseTimeout: did not receive a response before timing out - - Returns: - GoProResp: received response - """ # Store information on the response we are expecting - await self._sync_resp_wait_q.put(response_id) + await self._sync_resp_wait_q.put(message._identifier) + logger.info(Logger.build_log_tx_str(pretty_print(message._as_dict(**kwargs)))) # Fragment data and write it - for packet in self._fragment(data): - logger.debug(f"Writing to [{uuid.name}] UUID: {packet.hex(':')}") - await self._ble.write(uuid, packet) + for packet in self._fragment(message._build_data(**kwargs)): + logger.debug(f"Writing to [{message._uuid.name}] UUID: {packet.hex(':')}") + await self._ble.write(message._uuid, packet) # Wait to be notified that response was received try: @@ -744,24 +735,42 @@ async def _send_ble_message( @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) @enforce_message_rules - async def _read_characteristic(self, uuid: BleUUID) -> GoProResp: - """Read a characteristic's data by GoProUUIDs. + async def _read_ble_characteristic( + self, message: BleMessage, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + received_data = await self._ble.read(message._uuid) + logger.debug(f"Reading from {message._uuid.name}") + builder = BleRespBuilder() + builder.set_uuid(message._uuid) + builder.set_packet(received_data) + return builder.build() - There should hopefully not be a scenario where this needs to be called directly as it is generally - called from the instance's delegates (i.e. self.command, self.setting, self.ble_status) + # TODO make decorator? + def _handle_cohn(self, message: HttpMessage) -> HttpMessage: + """Prepend COHN headers if COHN is enabled Args: - uuid (BleUUID): characteristic data to read + message (HttpMessage): HTTP message to append headers to Returns: - GoProResp: response from UUID read + HttpMessage: potentially modified HTTP message """ - received_data = await self._ble.read(uuid) - logger.debug(f"Reading from {uuid.name}") - builder = BleRespBuilder() - builder.set_uuid(uuid) - builder.set_packet(received_data) - return builder.build() + if self._cohn: + message._headers["Authorization"] = self._cohn.auth_token + message._certificate = self._cohn.cert_path + return message + + async def _get_json(self, message: HttpMessage, *args: Any, **kwargs: Any) -> GoProResp: + message = self._handle_cohn(message) + return await super()._get_json(*args, message=message, **kwargs) + + async def _get_stream(self, message: HttpMessage, *args: Any, **kwargs: Any) -> GoProResp: + message = self._handle_cohn(message) + return await super()._get_stream(*args, message=message, **kwargs) + + async def _put_json(self, message: HttpMessage, *args: Any, **kwargs: Any) -> GoProResp: + message = self._handle_cohn(message) + return await super()._put_json(*args, message=message, **kwargs) @GoProBase._ensure_opened((GoProMessageInterface.BLE,)) async def _open_wifi(self, timeout: int = 10, retries: int = 5) -> None: @@ -794,31 +803,6 @@ async def _close_wifi(self) -> None: if hasattr(self, "_wifi"): # Corner case where instantiation fails before superclass is initialized self._wifi.close() - @enforce_message_rules - 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: - if self._cohn: - return await super()._http_get( - url, - parser, - headers=headers or {"Authorization": self._cohn.auth_token}, - certificate=certificate or self._cohn.cert_path, - timeout=timeout, - **kwargs, - ) - return await super()._http_get(url, parser, **kwargs) - - @enforce_message_rules - 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: return f"https://{self._cohn.ip_address}/" if self._cohn else "http://10.5.5.9:8080/" diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/logger.py b/demos/python/sdk_wireless_camera_control/open_gopro/logger.py index 6c863180..4762238f 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/logger.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/logger.py @@ -46,14 +46,15 @@ def __init__( "open_gopro.gopro_wired", "open_gopro.gopro_wireless", "open_gopro.api.builders", + "open_gopro.api.parsers", "open_gopro.api.http_commands", "open_gopro.api.ble_commands", - "open_gopro.communication_client", + "open_gopro.communicator_interface", "open_gopro.ble.adapters.bleak_wrapper", "open_gopro.ble.client", "open_gopro.wifi.adapters.wireless", "open_gopro.wifi.mdns_scanner", - "open_gopro.responses", + "open_gopro.models.response", "open_gopro.util", "bleak", "urllib3", 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 index 4c2497ea..3a5ded56 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/models/general.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/models/general.py @@ -62,7 +62,6 @@ class HttpInvalidSettingResponse(CustomBaseModel): supported_options: Optional[list[SupportedOption]] = Field(default=None) -# TODO add to / from json methods @dataclass class CohnInfo: """Data model to store Camera on the Home Network connection info""" @@ -76,6 +75,5 @@ class CohnInfo: def __post_init__(self) -> None: token = b64encode(f"{self.username}:{self.password}".encode("utf-8")).decode("ascii") self.auth_token = f"Basic {token}" - # self.token = f"Basic {token}" with open(self.cert_path, "w") as fp: fp.write(self.certificate) 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 index f00966ef..3077b226 100644 --- 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 @@ -24,6 +24,18 @@ class MediaPath(ABC, CustomBaseModel): folder: str #: directory that media lives in file: str #: media file name (including file extension) + @property + def as_path(self) -> str: + """Return the model as a camera path (folder/file) + + Returns: + str: camera path + """ + return f"{self.folder}/{self.file}" + + def __str__(self) -> str: + return self.as_path + ############################################################################################################## # Metadata @@ -155,13 +167,17 @@ class MediaList(CustomBaseModel): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - # self.thing = 1 # Modify each file name to use full path for directory in self.media: for media in directory.file_system: media.filename = f"{directory.directory}/{media.filename}" self._files.append(media) + def __contains__(self, key: MediaItem | MediaPath | str) -> bool: + if isinstance(key, MediaItem): + return key in self.files + return str(key) in [m.filename for m in self.files] + @property def files(self) -> list[MediaItem]: """Helper method to get list of media items 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 index dc9a022d..4a20ff5b 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/models/response.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/models/response.py @@ -304,23 +304,31 @@ def is_response_protobuf(self) -> bool: """ return isinstance(self._identifier, (ActionId, FeatureId)) - def _set_response_meta(self) -> None: - """Set the identifier based on what is currently known about the packet""" + @classmethod + def get_response_identifier(cls, uuid: BleUUID, packet: bytearray) -> types.ResponseType: + """Get the identifier based on what is currently known about the packet + + Args: + uuid (BleUUID): UUID packet was received on + packet (bytearray): raw bytes contained in packet + + Returns: + types.ResponseType: identifier of this response + """ # If it's a protobuf command - identifier = self._packet[0] + identifier = packet[0] try: FeatureId(identifier) - self._identifier = ActionId(self._packet[1]) + return ActionId(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 + if uuid is GoProUUIDs.CQ_SETTINGS_RESP: + return SettingId(identifier) + if uuid is GoProUUIDs.CQ_QUERY_RESP: + return QueryCmdId(identifier) + if uuid in [GoProUUIDs.CQ_COMMAND_RESP, GoProUUIDs.CN_NET_MGMT_RESP]: + return CmdId(identifier) + return uuid def set_parser(self, parser: Parser) -> None: """Store a parser. This is optional. @@ -429,15 +437,15 @@ def build(self) -> GoProResp: Returns: GoProResp: built response """ - self._set_response_meta() - buf = self._packet + try: + self._identifier = self.get_response_identifier(self._uuid, self._packet) + buf = self._packet - if not self._is_direct_read: # length byte - buf.pop(0) - if self._is_protobuf: # feature ID byte - buf.pop(0) + 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 @@ -480,7 +488,7 @@ def build(self) -> GoProResp: 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 + camera_state[param_id] = param_val.hex(":") continue # These can be more than 1 value so use a list if self._identifier in [ @@ -527,7 +535,7 @@ def build(self) -> GoProResp: if parsed.get("result") == EnumResultGeneric.RESULT_SUCCESS else ErrorCode.ERROR ) - except KeyError as e: + except Exception as e: self._state = RespBuilder._State.ERROR raise ResponseParseError(str(self._identifier), buf) from e 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 index 7f136c7a..ebaad7f7 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/parser_interface.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/parser_interface.py @@ -120,7 +120,7 @@ def parse(self, data: bytes | bytearray | types.JsonDict) -> T: RuntimeError: attempted to parse bytes when a byte-json adapter does not exist Returns: - T: TODO + T: final parsed output """ parsed_json: types.JsonDict if isinstance(data, (bytes, bytearray)): @@ -196,9 +196,6 @@ 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) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/cohn_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/cohn_pb2.py index d783ef5e..96c6a6b7 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/cohn_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/cohn_pb2.py @@ -1,37 +1,38 @@ # cohn_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:47 UTC 2024 -"""Generated protocol buffer code.""" -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 - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\ncohn.proto\x12\nopen_gopro\x1a\x16response_generic.proto"4\n\x14RequestGetCOHNStatus\x12\x1c\n\x14register_cohn_status\x18\x01 \x01(\x08"\xd9\x01\n\x10NotifyCOHNStatus\x12*\n\x06status\x18\x01 \x01(\x0e2\x1a.open_gopro.EnumCOHNStatus\x12/\n\x05state\x18\x02 \x01(\x0e2 .open_gopro.EnumCOHNNetworkState\x12\x10\n\x08username\x18\x03 \x01(\t\x12\x10\n\x08password\x18\x04 \x01(\t\x12\x11\n\tipaddress\x18\x05 \x01(\t\x12\x0f\n\x07enabled\x18\x06 \x01(\x08\x12\x0c\n\x04ssid\x18\x07 \x01(\t\x12\x12\n\nmacaddress\x18\x08 \x01(\t")\n\x15RequestCreateCOHNCert\x12\x10\n\x08override\x18\x01 \x01(\x08"\x16\n\x14RequestClearCOHNCert"\x11\n\x0fRequestCOHNCert"O\n\x10ResponseCOHNCert\x12-\n\x06result\x18\x01 \x01(\x0e2\x1d.open_gopro.EnumResultGeneric\x12\x0c\n\x04cert\x18\x02 \x01(\t",\n\x15RequestSetCOHNSetting\x12\x13\n\x0bcohn_active\x18\x01 \x01(\x08*>\n\x0eEnumCOHNStatus\x12\x16\n\x12COHN_UNPROVISIONED\x10\x00\x12\x14\n\x10COHN_PROVISIONED\x10\x01*\xec\x01\n\x14EnumCOHNNetworkState\x12\x13\n\x0fCOHN_STATE_Init\x10\x00\x12\x14\n\x10COHN_STATE_Error\x10\x01\x12\x13\n\x0fCOHN_STATE_Exit\x10\x02\x12\x13\n\x0fCOHN_STATE_Idle\x10\x05\x12\x1f\n\x1bCOHN_STATE_NetworkConnected\x10\x1b\x12"\n\x1eCOHN_STATE_NetworkDisconnected\x10\x1c\x12"\n\x1eCOHN_STATE_ConnectingToNetwork\x10\x1d\x12\x16\n\x12COHN_STATE_Invalid\x10\x1e' -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "cohn_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _ENUMCOHNSTATUS._serialized_start = 537 - _ENUMCOHNSTATUS._serialized_end = 599 - _ENUMCOHNNETWORKSTATE._serialized_start = 602 - _ENUMCOHNNETWORKSTATE._serialized_end = 838 - _REQUESTGETCOHNSTATUS._serialized_start = 50 - _REQUESTGETCOHNSTATUS._serialized_end = 102 - _NOTIFYCOHNSTATUS._serialized_start = 105 - _NOTIFYCOHNSTATUS._serialized_end = 322 - _REQUESTCREATECOHNCERT._serialized_start = 324 - _REQUESTCREATECOHNCERT._serialized_end = 365 - _REQUESTCLEARCOHNCERT._serialized_start = 367 - _REQUESTCLEARCOHNCERT._serialized_end = 389 - _REQUESTCOHNCERT._serialized_start = 391 - _REQUESTCOHNCERT._serialized_end = 408 - _RESPONSECOHNCERT._serialized_start = 410 - _RESPONSECOHNCERT._serialized_end = 489 - _REQUESTSETCOHNSETTING._serialized_start = 491 - _REQUESTSETCOHNSETTING._serialized_end = 535 +"""Generated protocol buffer code.""" + +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 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\ncohn.proto\x12\nopen_gopro\x1a\x16response_generic.proto"4\n\x14RequestGetCOHNStatus\x12\x1c\n\x14register_cohn_status\x18\x01 \x01(\x08"\xd9\x01\n\x10NotifyCOHNStatus\x12*\n\x06status\x18\x01 \x01(\x0e2\x1a.open_gopro.EnumCOHNStatus\x12/\n\x05state\x18\x02 \x01(\x0e2 .open_gopro.EnumCOHNNetworkState\x12\x10\n\x08username\x18\x03 \x01(\t\x12\x10\n\x08password\x18\x04 \x01(\t\x12\x11\n\tipaddress\x18\x05 \x01(\t\x12\x0f\n\x07enabled\x18\x06 \x01(\x08\x12\x0c\n\x04ssid\x18\x07 \x01(\t\x12\x12\n\nmacaddress\x18\x08 \x01(\t")\n\x15RequestCreateCOHNCert\x12\x10\n\x08override\x18\x01 \x01(\x08"\x16\n\x14RequestClearCOHNCert"\x11\n\x0fRequestCOHNCert"O\n\x10ResponseCOHNCert\x12-\n\x06result\x18\x01 \x01(\x0e2\x1d.open_gopro.EnumResultGeneric\x12\x0c\n\x04cert\x18\x02 \x01(\t",\n\x15RequestSetCOHNSetting\x12\x13\n\x0bcohn_active\x18\x01 \x01(\x08*>\n\x0eEnumCOHNStatus\x12\x16\n\x12COHN_UNPROVISIONED\x10\x00\x12\x14\n\x10COHN_PROVISIONED\x10\x01*\xec\x01\n\x14EnumCOHNNetworkState\x12\x13\n\x0fCOHN_STATE_Init\x10\x00\x12\x14\n\x10COHN_STATE_Error\x10\x01\x12\x13\n\x0fCOHN_STATE_Exit\x10\x02\x12\x13\n\x0fCOHN_STATE_Idle\x10\x05\x12\x1f\n\x1bCOHN_STATE_NetworkConnected\x10\x1b\x12"\n\x1eCOHN_STATE_NetworkDisconnected\x10\x1c\x12"\n\x1eCOHN_STATE_ConnectingToNetwork\x10\x1d\x12\x16\n\x12COHN_STATE_Invalid\x10\x1e' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "cohn_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMCOHNSTATUS._serialized_start = 537 + _ENUMCOHNSTATUS._serialized_end = 599 + _ENUMCOHNNETWORKSTATE._serialized_start = 602 + _ENUMCOHNNETWORKSTATE._serialized_end = 838 + _REQUESTGETCOHNSTATUS._serialized_start = 50 + _REQUESTGETCOHNSTATUS._serialized_end = 102 + _NOTIFYCOHNSTATUS._serialized_start = 105 + _NOTIFYCOHNSTATUS._serialized_end = 322 + _REQUESTCREATECOHNCERT._serialized_start = 324 + _REQUESTCREATECOHNCERT._serialized_end = 365 + _REQUESTCLEARCOHNCERT._serialized_start = 367 + _REQUESTCLEARCOHNCERT._serialized_end = 389 + _REQUESTCOHNCERT._serialized_start = 391 + _REQUESTCOHNCERT._serialized_end = 408 + _RESPONSECOHNCERT._serialized_start = 410 + _RESPONSECOHNCERT._serialized_end = 489 + _REQUESTSETCOHNSETTING._serialized_start = 491 + _REQUESTSETCOHNSETTING._serialized_end = 535 diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/cohn_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/cohn_pb2.pyi index e0a2d73e..03516118 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/cohn_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/cohn_pb2.pyi @@ -1,263 +1,279 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Defines the structure of protobuf messages for Camera On the Home Network -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -from . import response_generic_pb2 -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _EnumCOHNStatus: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumCOHNStatusEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCOHNStatus.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - COHN_UNPROVISIONED: _EnumCOHNStatus.ValueType - COHN_PROVISIONED: _EnumCOHNStatus.ValueType - -class EnumCOHNStatus(_EnumCOHNStatus, metaclass=_EnumCOHNStatusEnumTypeWrapper): ... - -COHN_UNPROVISIONED: EnumCOHNStatus.ValueType -COHN_PROVISIONED: EnumCOHNStatus.ValueType -global___EnumCOHNStatus = EnumCOHNStatus - -class _EnumCOHNNetworkState: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumCOHNNetworkStateEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCOHNNetworkState.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - COHN_STATE_Init: _EnumCOHNNetworkState.ValueType - COHN_STATE_Error: _EnumCOHNNetworkState.ValueType - COHN_STATE_Exit: _EnumCOHNNetworkState.ValueType - COHN_STATE_Idle: _EnumCOHNNetworkState.ValueType - COHN_STATE_NetworkConnected: _EnumCOHNNetworkState.ValueType - COHN_STATE_NetworkDisconnected: _EnumCOHNNetworkState.ValueType - COHN_STATE_ConnectingToNetwork: _EnumCOHNNetworkState.ValueType - COHN_STATE_Invalid: _EnumCOHNNetworkState.ValueType - -class EnumCOHNNetworkState(_EnumCOHNNetworkState, metaclass=_EnumCOHNNetworkStateEnumTypeWrapper): ... - -COHN_STATE_Init: EnumCOHNNetworkState.ValueType -COHN_STATE_Error: EnumCOHNNetworkState.ValueType -COHN_STATE_Exit: EnumCOHNNetworkState.ValueType -COHN_STATE_Idle: EnumCOHNNetworkState.ValueType -COHN_STATE_NetworkConnected: EnumCOHNNetworkState.ValueType -COHN_STATE_NetworkDisconnected: EnumCOHNNetworkState.ValueType -COHN_STATE_ConnectingToNetwork: EnumCOHNNetworkState.ValueType -COHN_STATE_Invalid: EnumCOHNNetworkState.ValueType -global___EnumCOHNNetworkState = EnumCOHNNetworkState - -class RequestGetCOHNStatus(google.protobuf.message.Message): - """* - Get the current COHN status. - - This always returns a @ref NotifyCOHNStatus - - Additionally, asynchronous updates can also be registerd to return more @ref NotifyCOHNStatus when a value - changes. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - REGISTER_COHN_STATUS_FIELD_NUMBER: builtins.int - register_cohn_status: builtins.bool - "1 to register, 0 to unregister" - - def __init__(self, *, register_cohn_status: builtins.bool | None = ...) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["register_cohn_status", b"register_cohn_status"] - ) -> builtins.bool: ... - def ClearField( - self, field_name: typing_extensions.Literal["register_cohn_status", b"register_cohn_status"] - ) -> None: ... - -global___RequestGetCOHNStatus = RequestGetCOHNStatus - -class NotifyCOHNStatus(google.protobuf.message.Message): - """ - Current COHN status triggered by a RequestGetCOHNStatus - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - STATUS_FIELD_NUMBER: builtins.int - STATE_FIELD_NUMBER: builtins.int - USERNAME_FIELD_NUMBER: builtins.int - PASSWORD_FIELD_NUMBER: builtins.int - IPADDRESS_FIELD_NUMBER: builtins.int - ENABLED_FIELD_NUMBER: builtins.int - SSID_FIELD_NUMBER: builtins.int - MACADDRESS_FIELD_NUMBER: builtins.int - status: global___EnumCOHNStatus.ValueType - "Current COHN status" - state: global___EnumCOHNNetworkState.ValueType - "Current COHN network state" - username: builtins.str - "Username used for http basic auth header" - password: builtins.str - "Password used for http basic auth header" - ipaddress: builtins.str - "Camera's IP address on the local network" - enabled: builtins.bool - "Is COHN currently enabled" - ssid: builtins.str - "Currently connected SSID" - macaddress: builtins.str - "MAC address of the wifi adapter" - - def __init__( - self, - *, - status: global___EnumCOHNStatus.ValueType | None = ..., - state: global___EnumCOHNNetworkState.ValueType | None = ..., - username: builtins.str | None = ..., - password: builtins.str | None = ..., - ipaddress: builtins.str | None = ..., - enabled: builtins.bool | None = ..., - ssid: builtins.str | None = ..., - macaddress: builtins.str | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "enabled", - b"enabled", - "ipaddress", - b"ipaddress", - "macaddress", - b"macaddress", - "password", - b"password", - "ssid", - b"ssid", - "state", - b"state", - "status", - b"status", - "username", - b"username", - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "enabled", - b"enabled", - "ipaddress", - b"ipaddress", - "macaddress", - b"macaddress", - "password", - b"password", - "ssid", - b"ssid", - "state", - b"state", - "status", - b"status", - "username", - b"username", - ], - ) -> None: ... - -global___NotifyCOHNStatus = NotifyCOHNStatus - -class RequestCreateCOHNCert(google.protobuf.message.Message): - """* - Create the COHN certificate. - - Returns a @ref ResponseGeneric with the status of the creation - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - OVERRIDE_FIELD_NUMBER: builtins.int - override: builtins.bool - "Override current provisioning and create new cert" - - def __init__(self, *, override: builtins.bool | None = ...) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["override", b"override"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["override", b"override"]) -> None: ... - -global___RequestCreateCOHNCert = RequestCreateCOHNCert - -class RequestClearCOHNCert(google.protobuf.message.Message): - """* - Clear the COHN certificate. - - Returns a @ref ResponseGeneric with the status of the clear - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - def __init__(self) -> None: ... - -global___RequestClearCOHNCert = RequestClearCOHNCert - -class RequestCOHNCert(google.protobuf.message.Message): - """* - Get the COHN certificate. - - Returns a @ref ResponseCOHNCert - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - def __init__(self) -> None: ... - -global___RequestCOHNCert = RequestCOHNCert - -class ResponseCOHNCert(google.protobuf.message.Message): - """ - COHN Certificate response triggered by RequestCOHNCert - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - RESULT_FIELD_NUMBER: builtins.int - CERT_FIELD_NUMBER: builtins.int - result: response_generic_pb2.EnumResultGeneric.ValueType - "Was request successful?" - cert: builtins.str - "Root CA cert (ASCII text)" - - def __init__( - self, *, result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., cert: builtins.str | None = ... - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["cert", b"cert", "result", b"result"] - ) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["cert", b"cert", "result", b"result"]) -> None: ... - -global___ResponseCOHNCert = ResponseCOHNCert - -class RequestSetCOHNSetting(google.protobuf.message.Message): - """* - Enable and disable COHN if provisioned - - Returns a @ref ResponseGeneric - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - COHN_ACTIVE_FIELD_NUMBER: builtins.int - cohn_active: builtins.bool - "1 to enable, 0 to disable" - - def __init__(self, *, cohn_active: builtins.bool | None = ...) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["cohn_active", b"cohn_active"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["cohn_active", b"cohn_active"]) -> None: ... - -global___RequestSetCOHNSetting = RequestSetCOHNSetting +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for Camera On the Home Network +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +from . import response_generic_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumCOHNStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumCOHNStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCOHNStatus.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + COHN_UNPROVISIONED: _EnumCOHNStatus.ValueType + COHN_PROVISIONED: _EnumCOHNStatus.ValueType + +class EnumCOHNStatus(_EnumCOHNStatus, metaclass=_EnumCOHNStatusEnumTypeWrapper): ... + +COHN_UNPROVISIONED: EnumCOHNStatus.ValueType +COHN_PROVISIONED: EnumCOHNStatus.ValueType +global___EnumCOHNStatus = EnumCOHNStatus + +class _EnumCOHNNetworkState: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumCOHNNetworkStateEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCOHNNetworkState.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + COHN_STATE_Init: _EnumCOHNNetworkState.ValueType + COHN_STATE_Error: _EnumCOHNNetworkState.ValueType + COHN_STATE_Exit: _EnumCOHNNetworkState.ValueType + COHN_STATE_Idle: _EnumCOHNNetworkState.ValueType + COHN_STATE_NetworkConnected: _EnumCOHNNetworkState.ValueType + COHN_STATE_NetworkDisconnected: _EnumCOHNNetworkState.ValueType + COHN_STATE_ConnectingToNetwork: _EnumCOHNNetworkState.ValueType + COHN_STATE_Invalid: _EnumCOHNNetworkState.ValueType + +class EnumCOHNNetworkState(_EnumCOHNNetworkState, metaclass=_EnumCOHNNetworkStateEnumTypeWrapper): ... + +COHN_STATE_Init: EnumCOHNNetworkState.ValueType +COHN_STATE_Error: EnumCOHNNetworkState.ValueType +COHN_STATE_Exit: EnumCOHNNetworkState.ValueType +COHN_STATE_Idle: EnumCOHNNetworkState.ValueType +COHN_STATE_NetworkConnected: EnumCOHNNetworkState.ValueType +COHN_STATE_NetworkDisconnected: EnumCOHNNetworkState.ValueType +COHN_STATE_ConnectingToNetwork: EnumCOHNNetworkState.ValueType +COHN_STATE_Invalid: EnumCOHNNetworkState.ValueType +global___EnumCOHNNetworkState = EnumCOHNNetworkState + +@typing_extensions.final +class RequestGetCOHNStatus(google.protobuf.message.Message): + """* + Get the current COHN status. + + Response: @ref NotifyCOHNStatus + + Additionally, asynchronous updates can also be registered to return more @ref NotifyCOHNStatus when a value + changes. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + REGISTER_COHN_STATUS_FIELD_NUMBER: builtins.int + register_cohn_status: builtins.bool + "1 to register, 0 to unregister" + + def __init__(self, *, register_cohn_status: builtins.bool | None = ...) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["register_cohn_status", b"register_cohn_status"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["register_cohn_status", b"register_cohn_status"], + ) -> None: ... + +global___RequestGetCOHNStatus = RequestGetCOHNStatus + +@typing_extensions.final +class NotifyCOHNStatus(google.protobuf.message.Message): + """ + Current COHN status triggered by a @ref RequestGetCOHNStatus + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + STATUS_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + USERNAME_FIELD_NUMBER: builtins.int + PASSWORD_FIELD_NUMBER: builtins.int + IPADDRESS_FIELD_NUMBER: builtins.int + ENABLED_FIELD_NUMBER: builtins.int + SSID_FIELD_NUMBER: builtins.int + MACADDRESS_FIELD_NUMBER: builtins.int + status: global___EnumCOHNStatus.ValueType + "Current COHN status" + state: global___EnumCOHNNetworkState.ValueType + "Current COHN network state" + username: builtins.str + "Username used for http basic auth header" + password: builtins.str + "Password used for http basic auth header" + ipaddress: builtins.str + "Camera's IP address on the local network" + enabled: builtins.bool + "Is COHN currently enabled?" + ssid: builtins.str + "Currently connected SSID" + macaddress: builtins.str + "MAC address of the wifi adapter" + + def __init__( + self, + *, + status: global___EnumCOHNStatus.ValueType | None = ..., + state: global___EnumCOHNNetworkState.ValueType | None = ..., + username: builtins.str | None = ..., + password: builtins.str | None = ..., + ipaddress: builtins.str | None = ..., + enabled: builtins.bool | None = ..., + ssid: builtins.str | None = ..., + macaddress: builtins.str | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "enabled", + b"enabled", + "ipaddress", + b"ipaddress", + "macaddress", + b"macaddress", + "password", + b"password", + "ssid", + b"ssid", + "state", + b"state", + "status", + b"status", + "username", + b"username", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "enabled", + b"enabled", + "ipaddress", + b"ipaddress", + "macaddress", + b"macaddress", + "password", + b"password", + "ssid", + b"ssid", + "state", + b"state", + "status", + b"status", + "username", + b"username", + ], + ) -> None: ... + +global___NotifyCOHNStatus = NotifyCOHNStatus + +@typing_extensions.final +class RequestCreateCOHNCert(google.protobuf.message.Message): + """* + Create the Camera On the Home Network SSL/TLS certificate. + + Returns a @ref ResponseGeneric with the status of the creation + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + OVERRIDE_FIELD_NUMBER: builtins.int + override: builtins.bool + "Override current provisioning and create new cert" + + def __init__(self, *, override: builtins.bool | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["override", b"override"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["override", b"override"]) -> None: ... + +global___RequestCreateCOHNCert = RequestCreateCOHNCert + +@typing_extensions.final +class RequestClearCOHNCert(google.protobuf.message.Message): + """* + Clear the COHN certificate. + + Returns a @ref ResponseGeneric with the status of the clear + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestClearCOHNCert = RequestClearCOHNCert + +@typing_extensions.final +class RequestCOHNCert(google.protobuf.message.Message): + """* + Get the COHN certificate. + + Returns a @ref ResponseCOHNCert + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestCOHNCert = RequestCOHNCert + +@typing_extensions.final +class ResponseCOHNCert(google.protobuf.message.Message): + """ + COHN Certificate response triggered by @ref RequestCOHNCert + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + CERT_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Was request successful?" + cert: builtins.str + "Root CA cert (ASCII text)" + + def __init__( + self, *, result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., cert: builtins.str | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["cert", b"cert", "result", b"result"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["cert", b"cert", "result", b"result"], + ) -> None: ... + +global___ResponseCOHNCert = ResponseCOHNCert + +@typing_extensions.final +class RequestSetCOHNSetting(google.protobuf.message.Message): + """* + Configure a COHN Setting + + Returns a @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + COHN_ACTIVE_FIELD_NUMBER: builtins.int + cohn_active: builtins.bool + "*\n 1 to enable COHN, 0 to disable COHN\n\n When set to 1, STA Mode connection will be dropped and camera will not automatically re-connect for COHN.\n " + + def __init__(self, *, cohn_active: builtins.bool | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["cohn_active", b"cohn_active"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["cohn_active", b"cohn_active"]) -> None: ... + +global___RequestSetCOHNSetting = RequestSetCOHNSetting 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 09aa4b94..9accf644 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,33 +1,34 @@ # 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 Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:47 UTC 2024 -"""Generated protocol buffer code.""" -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( - b'\n\x14live_streaming.proto\x12\nopen_gopro"\xa4\x04\n\x16NotifyLiveStreamStatus\x12<\n\x12live_stream_status\x18\x01 \x01(\x0e2 .open_gopro.EnumLiveStreamStatus\x12:\n\x11live_stream_error\x18\x02 \x01(\x0e2\x1f.open_gopro.EnumLiveStreamError\x12\x1a\n\x12live_stream_encode\x18\x03 \x01(\x08\x12\x1b\n\x13live_stream_bitrate\x18\x04 \x01(\x05\x12K\n\'live_stream_window_size_supported_array\x18\x05 \x03(\x0e2\x1a.open_gopro.EnumWindowSize\x12$\n\x1clive_stream_encode_supported\x18\x06 \x01(\x08\x12(\n live_stream_max_lens_unsupported\x18\x07 \x01(\x08\x12*\n"live_stream_minimum_stream_bitrate\x18\x08 \x01(\x05\x12*\n"live_stream_maximum_stream_bitrate\x18\t \x01(\x05\x12"\n\x1alive_stream_lens_supported\x18\n \x01(\x08\x12>\n live_stream_lens_supported_array\x18\x0b \x03(\x0e2\x14.open_gopro.EnumLens"\xbc\x01\n\x1aRequestGetLiveStreamStatus\x12M\n\x1bregister_live_stream_status\x18\x01 \x03(\x0e2(.open_gopro.EnumRegisterLiveStreamStatus\x12O\n\x1dunregister_live_stream_status\x18\x02 \x03(\x0e2(.open_gopro.EnumRegisterLiveStreamStatus"\xe6\x01\n\x18RequestSetLiveStreamMode\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0e\n\x06encode\x18\x02 \x01(\x08\x12/\n\x0bwindow_size\x18\x03 \x01(\x0e2\x1a.open_gopro.EnumWindowSize\x12\x0c\n\x04cert\x18\x06 \x01(\x0c\x12\x17\n\x0fminimum_bitrate\x18\x07 \x01(\x05\x12\x17\n\x0fmaximum_bitrate\x18\x08 \x01(\x05\x12\x18\n\x10starting_bitrate\x18\t \x01(\x05\x12"\n\x04lens\x18\n \x01(\x0e2\x14.open_gopro.EnumLens*>\n\x08EnumLens\x12\r\n\tLENS_WIDE\x10\x00\x12\x0f\n\x0bLENS_LINEAR\x10\x04\x12\x12\n\x0eLENS_SUPERVIEW\x10\x03*\xde\x03\n\x13EnumLiveStreamError\x12\x1a\n\x16LIVE_STREAM_ERROR_NONE\x10\x00\x12\x1d\n\x19LIVE_STREAM_ERROR_NETWORK\x10\x01\x12"\n\x1eLIVE_STREAM_ERROR_CREATESTREAM\x10\x02\x12!\n\x1dLIVE_STREAM_ERROR_OUTOFMEMORY\x10\x03\x12!\n\x1dLIVE_STREAM_ERROR_INPUTSTREAM\x10\x04\x12\x1e\n\x1aLIVE_STREAM_ERROR_INTERNET\x10\x05\x12\x1f\n\x1bLIVE_STREAM_ERROR_OSNETWORK\x10\x06\x12,\n(LIVE_STREAM_ERROR_SELECTEDNETWORKTIMEOUT\x10\x07\x12#\n\x1fLIVE_STREAM_ERROR_SSL_HANDSHAKE\x10\x08\x12$\n LIVE_STREAM_ERROR_CAMERA_BLOCKED\x10\t\x12\x1d\n\x19LIVE_STREAM_ERROR_UNKNOWN\x10\n\x12"\n\x1eLIVE_STREAM_ERROR_SD_CARD_FULL\x10(\x12%\n!LIVE_STREAM_ERROR_SD_CARD_REMOVED\x10)*\x80\x02\n\x14EnumLiveStreamStatus\x12\x1a\n\x16LIVE_STREAM_STATE_IDLE\x10\x00\x12\x1c\n\x18LIVE_STREAM_STATE_CONFIG\x10\x01\x12\x1b\n\x17LIVE_STREAM_STATE_READY\x10\x02\x12\x1f\n\x1bLIVE_STREAM_STATE_STREAMING\x10\x03\x12&\n"LIVE_STREAM_STATE_COMPLETE_STAY_ON\x10\x04\x12$\n LIVE_STREAM_STATE_FAILED_STAY_ON\x10\x05\x12"\n\x1eLIVE_STREAM_STATE_RECONNECTING\x10\x06*\xbc\x01\n\x1cEnumRegisterLiveStreamStatus\x12&\n"REGISTER_LIVE_STREAM_STATUS_STATUS\x10\x01\x12%\n!REGISTER_LIVE_STREAM_STATUS_ERROR\x10\x02\x12$\n REGISTER_LIVE_STREAM_STATUS_MODE\x10\x03\x12\'\n#REGISTER_LIVE_STREAM_STATUS_BITRATE\x10\x04*P\n\x0eEnumWindowSize\x12\x13\n\x0fWINDOW_SIZE_480\x10\x04\x12\x13\n\x0fWINDOW_SIZE_720\x10\x07\x12\x14\n\x10WINDOW_SIZE_1080\x10\x0c' -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "live_streaming_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _ENUMLENS._serialized_start = 1011 - _ENUMLENS._serialized_end = 1073 - _ENUMLIVESTREAMERROR._serialized_start = 1076 - _ENUMLIVESTREAMERROR._serialized_end = 1554 - _ENUMLIVESTREAMSTATUS._serialized_start = 1557 - _ENUMLIVESTREAMSTATUS._serialized_end = 1813 - _ENUMREGISTERLIVESTREAMSTATUS._serialized_start = 1816 - _ENUMREGISTERLIVESTREAMSTATUS._serialized_end = 2004 - _ENUMWINDOWSIZE._serialized_start = 2006 - _ENUMWINDOWSIZE._serialized_end = 2086 - _NOTIFYLIVESTREAMSTATUS._serialized_start = 37 - _NOTIFYLIVESTREAMSTATUS._serialized_end = 585 - _REQUESTGETLIVESTREAMSTATUS._serialized_start = 588 - _REQUESTGETLIVESTREAMSTATUS._serialized_end = 776 - _REQUESTSETLIVESTREAMMODE._serialized_start = 779 - _REQUESTSETLIVESTREAMMODE._serialized_end = 1009 +"""Generated protocol buffer code.""" + +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( + b'\n\x14live_streaming.proto\x12\nopen_gopro"\xa4\x04\n\x16NotifyLiveStreamStatus\x12<\n\x12live_stream_status\x18\x01 \x01(\x0e2 .open_gopro.EnumLiveStreamStatus\x12:\n\x11live_stream_error\x18\x02 \x01(\x0e2\x1f.open_gopro.EnumLiveStreamError\x12\x1a\n\x12live_stream_encode\x18\x03 \x01(\x08\x12\x1b\n\x13live_stream_bitrate\x18\x04 \x01(\x05\x12K\n\'live_stream_window_size_supported_array\x18\x05 \x03(\x0e2\x1a.open_gopro.EnumWindowSize\x12$\n\x1clive_stream_encode_supported\x18\x06 \x01(\x08\x12(\n live_stream_max_lens_unsupported\x18\x07 \x01(\x08\x12*\n"live_stream_minimum_stream_bitrate\x18\x08 \x01(\x05\x12*\n"live_stream_maximum_stream_bitrate\x18\t \x01(\x05\x12"\n\x1alive_stream_lens_supported\x18\n \x01(\x08\x12>\n live_stream_lens_supported_array\x18\x0b \x03(\x0e2\x14.open_gopro.EnumLens"\xbc\x01\n\x1aRequestGetLiveStreamStatus\x12M\n\x1bregister_live_stream_status\x18\x01 \x03(\x0e2(.open_gopro.EnumRegisterLiveStreamStatus\x12O\n\x1dunregister_live_stream_status\x18\x02 \x03(\x0e2(.open_gopro.EnumRegisterLiveStreamStatus"\xe6\x01\n\x18RequestSetLiveStreamMode\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0e\n\x06encode\x18\x02 \x01(\x08\x12/\n\x0bwindow_size\x18\x03 \x01(\x0e2\x1a.open_gopro.EnumWindowSize\x12\x0c\n\x04cert\x18\x06 \x01(\x0c\x12\x17\n\x0fminimum_bitrate\x18\x07 \x01(\x05\x12\x17\n\x0fmaximum_bitrate\x18\x08 \x01(\x05\x12\x18\n\x10starting_bitrate\x18\t \x01(\x05\x12"\n\x04lens\x18\n \x01(\x0e2\x14.open_gopro.EnumLens*>\n\x08EnumLens\x12\r\n\tLENS_WIDE\x10\x00\x12\x0f\n\x0bLENS_LINEAR\x10\x04\x12\x12\n\x0eLENS_SUPERVIEW\x10\x03*\xde\x03\n\x13EnumLiveStreamError\x12\x1a\n\x16LIVE_STREAM_ERROR_NONE\x10\x00\x12\x1d\n\x19LIVE_STREAM_ERROR_NETWORK\x10\x01\x12"\n\x1eLIVE_STREAM_ERROR_CREATESTREAM\x10\x02\x12!\n\x1dLIVE_STREAM_ERROR_OUTOFMEMORY\x10\x03\x12!\n\x1dLIVE_STREAM_ERROR_INPUTSTREAM\x10\x04\x12\x1e\n\x1aLIVE_STREAM_ERROR_INTERNET\x10\x05\x12\x1f\n\x1bLIVE_STREAM_ERROR_OSNETWORK\x10\x06\x12,\n(LIVE_STREAM_ERROR_SELECTEDNETWORKTIMEOUT\x10\x07\x12#\n\x1fLIVE_STREAM_ERROR_SSL_HANDSHAKE\x10\x08\x12$\n LIVE_STREAM_ERROR_CAMERA_BLOCKED\x10\t\x12\x1d\n\x19LIVE_STREAM_ERROR_UNKNOWN\x10\n\x12"\n\x1eLIVE_STREAM_ERROR_SD_CARD_FULL\x10(\x12%\n!LIVE_STREAM_ERROR_SD_CARD_REMOVED\x10)*\x80\x02\n\x14EnumLiveStreamStatus\x12\x1a\n\x16LIVE_STREAM_STATE_IDLE\x10\x00\x12\x1c\n\x18LIVE_STREAM_STATE_CONFIG\x10\x01\x12\x1b\n\x17LIVE_STREAM_STATE_READY\x10\x02\x12\x1f\n\x1bLIVE_STREAM_STATE_STREAMING\x10\x03\x12&\n"LIVE_STREAM_STATE_COMPLETE_STAY_ON\x10\x04\x12$\n LIVE_STREAM_STATE_FAILED_STAY_ON\x10\x05\x12"\n\x1eLIVE_STREAM_STATE_RECONNECTING\x10\x06*\xbc\x01\n\x1cEnumRegisterLiveStreamStatus\x12&\n"REGISTER_LIVE_STREAM_STATUS_STATUS\x10\x01\x12%\n!REGISTER_LIVE_STREAM_STATUS_ERROR\x10\x02\x12$\n REGISTER_LIVE_STREAM_STATUS_MODE\x10\x03\x12\'\n#REGISTER_LIVE_STREAM_STATUS_BITRATE\x10\x04*P\n\x0eEnumWindowSize\x12\x13\n\x0fWINDOW_SIZE_480\x10\x04\x12\x13\n\x0fWINDOW_SIZE_720\x10\x07\x12\x14\n\x10WINDOW_SIZE_1080\x10\x0c' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "live_streaming_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMLENS._serialized_start = 1011 + _ENUMLENS._serialized_end = 1073 + _ENUMLIVESTREAMERROR._serialized_start = 1076 + _ENUMLIVESTREAMERROR._serialized_end = 1554 + _ENUMLIVESTREAMSTATUS._serialized_start = 1557 + _ENUMLIVESTREAMSTATUS._serialized_end = 1813 + _ENUMREGISTERLIVESTREAMSTATUS._serialized_start = 1816 + _ENUMREGISTERLIVESTREAMSTATUS._serialized_end = 2004 + _ENUMWINDOWSIZE._serialized_start = 2006 + _ENUMWINDOWSIZE._serialized_end = 2086 + _NOTIFYLIVESTREAMSTATUS._serialized_start = 37 + _NOTIFYLIVESTREAMSTATUS._serialized_end = 585 + _REQUESTGETLIVESTREAMSTATUS._serialized_start = 588 + _REQUESTGETLIVESTREAMSTATUS._serialized_end = 776 + _REQUESTSETLIVESTREAMMODE._serialized_start = 779 + _REQUESTSETLIVESTREAMMODE._serialized_end = 1009 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 2aeb277e..d658661e 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 @@ -1,444 +1,458 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Defines the structure of protobuf messages for working with Live Streams -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _EnumLens: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumLensEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLens.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - LENS_WIDE: _EnumLens.ValueType - LENS_LINEAR: _EnumLens.ValueType - LENS_SUPERVIEW: _EnumLens.ValueType - -class EnumLens(_EnumLens, metaclass=_EnumLensEnumTypeWrapper): ... - -LENS_WIDE: EnumLens.ValueType -LENS_LINEAR: EnumLens.ValueType -LENS_SUPERVIEW: EnumLens.ValueType -global___EnumLens = EnumLens - -class _EnumLiveStreamError: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumLiveStreamErrorEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLiveStreamError.ValueType], builtins.type -): - 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: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumLiveStreamStatusEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLiveStreamStatus.ValueType], builtins.type -): - 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 - "\n Livestream has finished configuration and is ready to start streaming\n " - 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 -"\nLivestream has finished configuration and is ready to start streaming\n" -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: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumRegisterLiveStreamStatusEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumRegisterLiveStreamStatus.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - REGISTER_LIVE_STREAM_STATUS_STATUS: _EnumRegisterLiveStreamStatus.ValueType - REGISTER_LIVE_STREAM_STATUS_ERROR: _EnumRegisterLiveStreamStatus.ValueType - REGISTER_LIVE_STREAM_STATUS_MODE: _EnumRegisterLiveStreamStatus.ValueType - REGISTER_LIVE_STREAM_STATUS_BITRATE: _EnumRegisterLiveStreamStatus.ValueType - -class EnumRegisterLiveStreamStatus( - _EnumRegisterLiveStreamStatus, metaclass=_EnumRegisterLiveStreamStatusEnumTypeWrapper -): ... - -REGISTER_LIVE_STREAM_STATUS_STATUS: EnumRegisterLiveStreamStatus.ValueType -REGISTER_LIVE_STREAM_STATUS_ERROR: EnumRegisterLiveStreamStatus.ValueType -REGISTER_LIVE_STREAM_STATUS_MODE: EnumRegisterLiveStreamStatus.ValueType -REGISTER_LIVE_STREAM_STATUS_BITRATE: EnumRegisterLiveStreamStatus.ValueType -global___EnumRegisterLiveStreamStatus = EnumRegisterLiveStreamStatus - -class _EnumWindowSize: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumWindowSizeEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumWindowSize.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - WINDOW_SIZE_480: _EnumWindowSize.ValueType - WINDOW_SIZE_720: _EnumWindowSize.ValueType - WINDOW_SIZE_1080: _EnumWindowSize.ValueType - -class EnumWindowSize(_EnumWindowSize, metaclass=_EnumWindowSizeEnumTypeWrapper): ... - -WINDOW_SIZE_480: EnumWindowSize.ValueType -WINDOW_SIZE_720: EnumWindowSize.ValueType -WINDOW_SIZE_1080: EnumWindowSize.ValueType -global___EnumWindowSize = EnumWindowSize - -class NotifyLiveStreamStatus(google.protobuf.message.Message): - """* - Live Stream status - - Sent either: - - as a syncrhonous response to initial @ref RequestGetLiveStreamStatus - - as asynchronous notifications registered for via @ref RequestGetLiveStreamStatus - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int - LIVE_STREAM_ERROR_FIELD_NUMBER: builtins.int - LIVE_STREAM_ENCODE_FIELD_NUMBER: builtins.int - LIVE_STREAM_BITRATE_FIELD_NUMBER: builtins.int - LIVE_STREAM_WINDOW_SIZE_SUPPORTED_ARRAY_FIELD_NUMBER: builtins.int - LIVE_STREAM_ENCODE_SUPPORTED_FIELD_NUMBER: builtins.int - LIVE_STREAM_MAX_LENS_UNSUPPORTED_FIELD_NUMBER: builtins.int - LIVE_STREAM_MINIMUM_STREAM_BITRATE_FIELD_NUMBER: builtins.int - LIVE_STREAM_MAXIMUM_STREAM_BITRATE_FIELD_NUMBER: builtins.int - LIVE_STREAM_LENS_SUPPORTED_FIELD_NUMBER: builtins.int - LIVE_STREAM_LENS_SUPPORTED_ARRAY_FIELD_NUMBER: builtins.int - live_stream_status: global___EnumLiveStreamStatus.ValueType - "Live stream status" - live_stream_error: global___EnumLiveStreamError.ValueType - "Live stream error" - live_stream_encode: builtins.bool - "Is live stream encoding?" - live_stream_bitrate: builtins.int - "Live stream bitrate (Kbps)" - - @property - def live_stream_window_size_supported_array( - self, - ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___EnumWindowSize.ValueType]: - """Set of currently supported resolutions""" - live_stream_encode_supported: builtins.bool - "Does the camera support encoding while live streaming?" - live_stream_max_lens_unsupported: builtins.bool - "Is the Max Lens feature NOT supported?" - live_stream_minimum_stream_bitrate: builtins.int - "Camera-defined minimum bitrate (static) (Kbps)" - live_stream_maximum_stream_bitrate: builtins.int - "Camera-defined maximum bitrate (static) (Kbps)" - live_stream_lens_supported: builtins.bool - "Does camera support setting lens for live streaming?" - - @property - def live_stream_lens_supported_array( - self, - ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___EnumLens.ValueType]: - """Set of currently supported FOV options""" - def __init__( - self, - *, - live_stream_status: global___EnumLiveStreamStatus.ValueType | None = ..., - live_stream_error: global___EnumLiveStreamError.ValueType | None = ..., - live_stream_encode: builtins.bool | None = ..., - live_stream_bitrate: builtins.int | None = ..., - live_stream_window_size_supported_array: collections.abc.Iterable[global___EnumWindowSize.ValueType] - | None = ..., - live_stream_encode_supported: builtins.bool | None = ..., - live_stream_max_lens_unsupported: builtins.bool | None = ..., - live_stream_minimum_stream_bitrate: builtins.int | None = ..., - live_stream_maximum_stream_bitrate: builtins.int | None = ..., - live_stream_lens_supported: builtins.bool | None = ..., - live_stream_lens_supported_array: collections.abc.Iterable[global___EnumLens.ValueType] | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "live_stream_bitrate", - b"live_stream_bitrate", - "live_stream_encode", - b"live_stream_encode", - "live_stream_encode_supported", - b"live_stream_encode_supported", - "live_stream_error", - b"live_stream_error", - "live_stream_lens_supported", - b"live_stream_lens_supported", - "live_stream_max_lens_unsupported", - b"live_stream_max_lens_unsupported", - "live_stream_maximum_stream_bitrate", - b"live_stream_maximum_stream_bitrate", - "live_stream_minimum_stream_bitrate", - b"live_stream_minimum_stream_bitrate", - "live_stream_status", - b"live_stream_status", - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "live_stream_bitrate", - b"live_stream_bitrate", - "live_stream_encode", - b"live_stream_encode", - "live_stream_encode_supported", - b"live_stream_encode_supported", - "live_stream_error", - b"live_stream_error", - "live_stream_lens_supported", - b"live_stream_lens_supported", - "live_stream_lens_supported_array", - b"live_stream_lens_supported_array", - "live_stream_max_lens_unsupported", - b"live_stream_max_lens_unsupported", - "live_stream_maximum_stream_bitrate", - b"live_stream_maximum_stream_bitrate", - "live_stream_minimum_stream_bitrate", - b"live_stream_minimum_stream_bitrate", - "live_stream_status", - b"live_stream_status", - "live_stream_window_size_supported_array", - b"live_stream_window_size_supported_array", - ], - ) -> None: ... - -global___NotifyLiveStreamStatus = NotifyLiveStreamStatus - -class RequestGetLiveStreamStatus(google.protobuf.message.Message): - """* - Get the current livestream status (and optionally register for future status changes) - - Both current status and future status changes are sent via @ref NotifyLiveStreamStatus - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - REGISTER_LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int - UNREGISTER_LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int - - @property - def register_live_stream_status( - self, - ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ - global___EnumRegisterLiveStreamStatus.ValueType - ]: - """Array of live stream statuses to be notified about""" - @property - def unregister_live_stream_status( - self, - ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ - global___EnumRegisterLiveStreamStatus.ValueType - ]: - """Array of live stream statuses to stop being notified about""" - def __init__( - self, - *, - register_live_stream_status: collections.abc.Iterable[global___EnumRegisterLiveStreamStatus.ValueType] - | None = ..., - unregister_live_stream_status: collections.abc.Iterable[global___EnumRegisterLiveStreamStatus.ValueType] - | None = ... - ) -> None: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "register_live_stream_status", - b"register_live_stream_status", - "unregister_live_stream_status", - b"unregister_live_stream_status", - ], - ) -> None: ... - -global___RequestGetLiveStreamStatus = RequestGetLiveStreamStatus - -class RequestSetLiveStreamMode(google.protobuf.message.Message): - """* - Configure lives streaming - - The current livestream status can be queried via @ref RequestGetLiveStreamStatus - - Response: @ref ResponseGeneric - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - URL_FIELD_NUMBER: builtins.int - ENCODE_FIELD_NUMBER: builtins.int - WINDOW_SIZE_FIELD_NUMBER: builtins.int - CERT_FIELD_NUMBER: builtins.int - MINIMUM_BITRATE_FIELD_NUMBER: builtins.int - MAXIMUM_BITRATE_FIELD_NUMBER: builtins.int - STARTING_BITRATE_FIELD_NUMBER: builtins.int - LENS_FIELD_NUMBER: builtins.int - url: builtins.str - "RTMP(S) URL used for live stream" - encode: builtins.bool - "Save media to sdcard while streaming?" - window_size: global___EnumWindowSize.ValueType - "*\n Resolution to use for live stream\n\n The set of supported lenses is only available from the `live_stream_window_size_supported_array` in @ref NotifyLiveStreamStatus)\n " - cert: builtins.bytes - "Certificate for servers that require it" - minimum_bitrate: builtins.int - "Minimum desired bitrate (may or may not be honored)" - maximum_bitrate: builtins.int - "Maximum desired bitrate (may or may not be honored)" - starting_bitrate: builtins.int - "Starting bitrate" - lens: global___EnumLens.ValueType - "*\n Lens to use for live stream\n\n The set of supported lenses is only available from the `live_stream_lens_supported_array` in @ref NotifyLiveStreamStatus)\n " - - def __init__( - self, - *, - url: builtins.str | None = ..., - encode: builtins.bool | None = ..., - window_size: global___EnumWindowSize.ValueType | None = ..., - cert: builtins.bytes | None = ..., - minimum_bitrate: builtins.int | None = ..., - maximum_bitrate: builtins.int | None = ..., - starting_bitrate: builtins.int | None = ..., - lens: global___EnumLens.ValueType | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "cert", - b"cert", - "encode", - b"encode", - "lens", - b"lens", - "maximum_bitrate", - b"maximum_bitrate", - "minimum_bitrate", - b"minimum_bitrate", - "starting_bitrate", - b"starting_bitrate", - "url", - b"url", - "window_size", - b"window_size", - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "cert", - b"cert", - "encode", - b"encode", - "lens", - b"lens", - "maximum_bitrate", - b"maximum_bitrate", - "minimum_bitrate", - b"minimum_bitrate", - "starting_bitrate", - b"starting_bitrate", - "url", - b"url", - "window_size", - b"window_size", - ], - ) -> None: ... - -global___RequestSetLiveStreamMode = RequestSetLiveStreamMode +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for working with Live Streams +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumLens: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumLensEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLens.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + LENS_WIDE: _EnumLens.ValueType + LENS_LINEAR: _EnumLens.ValueType + LENS_SUPERVIEW: _EnumLens.ValueType + +class EnumLens(_EnumLens, metaclass=_EnumLensEnumTypeWrapper): ... + +LENS_WIDE: EnumLens.ValueType +LENS_LINEAR: EnumLens.ValueType +LENS_SUPERVIEW: EnumLens.ValueType +global___EnumLens = EnumLens + +class _EnumLiveStreamError: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumLiveStreamErrorEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLiveStreamError.ValueType], + builtins.type, +): + 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: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumLiveStreamStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLiveStreamStatus.ValueType], + builtins.type, +): + 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 + "\n Livestream has finished configuration and is ready to start streaming\n " + 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 +"\nLivestream has finished configuration and is ready to start streaming\n" +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: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumRegisterLiveStreamStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumRegisterLiveStreamStatus.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + REGISTER_LIVE_STREAM_STATUS_STATUS: _EnumRegisterLiveStreamStatus.ValueType + REGISTER_LIVE_STREAM_STATUS_ERROR: _EnumRegisterLiveStreamStatus.ValueType + REGISTER_LIVE_STREAM_STATUS_MODE: _EnumRegisterLiveStreamStatus.ValueType + REGISTER_LIVE_STREAM_STATUS_BITRATE: _EnumRegisterLiveStreamStatus.ValueType + +class EnumRegisterLiveStreamStatus( + _EnumRegisterLiveStreamStatus, + metaclass=_EnumRegisterLiveStreamStatusEnumTypeWrapper, +): ... + +REGISTER_LIVE_STREAM_STATUS_STATUS: EnumRegisterLiveStreamStatus.ValueType +REGISTER_LIVE_STREAM_STATUS_ERROR: EnumRegisterLiveStreamStatus.ValueType +REGISTER_LIVE_STREAM_STATUS_MODE: EnumRegisterLiveStreamStatus.ValueType +REGISTER_LIVE_STREAM_STATUS_BITRATE: EnumRegisterLiveStreamStatus.ValueType +global___EnumRegisterLiveStreamStatus = EnumRegisterLiveStreamStatus + +class _EnumWindowSize: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumWindowSizeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumWindowSize.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + WINDOW_SIZE_480: _EnumWindowSize.ValueType + WINDOW_SIZE_720: _EnumWindowSize.ValueType + WINDOW_SIZE_1080: _EnumWindowSize.ValueType + +class EnumWindowSize(_EnumWindowSize, metaclass=_EnumWindowSizeEnumTypeWrapper): ... + +WINDOW_SIZE_480: EnumWindowSize.ValueType +WINDOW_SIZE_720: EnumWindowSize.ValueType +WINDOW_SIZE_1080: EnumWindowSize.ValueType +global___EnumWindowSize = EnumWindowSize + +@typing_extensions.final +class NotifyLiveStreamStatus(google.protobuf.message.Message): + """* + Live Stream status + + Sent either: + + - As a synchronous response to initial @ref RequestGetLiveStreamStatus + - As an asynchronous notifications registered for via @ref RequestGetLiveStreamStatus + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int + LIVE_STREAM_ERROR_FIELD_NUMBER: builtins.int + LIVE_STREAM_ENCODE_FIELD_NUMBER: builtins.int + LIVE_STREAM_BITRATE_FIELD_NUMBER: builtins.int + LIVE_STREAM_WINDOW_SIZE_SUPPORTED_ARRAY_FIELD_NUMBER: builtins.int + LIVE_STREAM_ENCODE_SUPPORTED_FIELD_NUMBER: builtins.int + LIVE_STREAM_MAX_LENS_UNSUPPORTED_FIELD_NUMBER: builtins.int + LIVE_STREAM_MINIMUM_STREAM_BITRATE_FIELD_NUMBER: builtins.int + LIVE_STREAM_MAXIMUM_STREAM_BITRATE_FIELD_NUMBER: builtins.int + LIVE_STREAM_LENS_SUPPORTED_FIELD_NUMBER: builtins.int + LIVE_STREAM_LENS_SUPPORTED_ARRAY_FIELD_NUMBER: builtins.int + live_stream_status: global___EnumLiveStreamStatus.ValueType + "Live stream status" + live_stream_error: global___EnumLiveStreamError.ValueType + "Live stream error" + live_stream_encode: builtins.bool + "Is live stream encoding?" + live_stream_bitrate: builtins.int + "Live stream bitrate (Kbps)" + + @property + def live_stream_window_size_supported_array( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___EnumWindowSize.ValueType]: + """Set of currently supported resolutions""" + live_stream_encode_supported: builtins.bool + "Does the camera support encoding while live streaming?" + live_stream_max_lens_unsupported: builtins.bool + "Is the Max Lens feature NOT supported?" + live_stream_minimum_stream_bitrate: builtins.int + "Camera-defined minimum bitrate (static) (Kbps)" + live_stream_maximum_stream_bitrate: builtins.int + "Camera-defined maximum bitrate (static) (Kbps)" + live_stream_lens_supported: builtins.bool + "Does camera support setting lens for live streaming?" + + @property + def live_stream_lens_supported_array( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___EnumLens.ValueType]: + """Set of currently supported FOV options""" + def __init__( + self, + *, + live_stream_status: global___EnumLiveStreamStatus.ValueType | None = ..., + live_stream_error: global___EnumLiveStreamError.ValueType | None = ..., + live_stream_encode: builtins.bool | None = ..., + live_stream_bitrate: builtins.int | None = ..., + live_stream_window_size_supported_array: ( + collections.abc.Iterable[global___EnumWindowSize.ValueType] | None + ) = ..., + live_stream_encode_supported: builtins.bool | None = ..., + live_stream_max_lens_unsupported: builtins.bool | None = ..., + live_stream_minimum_stream_bitrate: builtins.int | None = ..., + live_stream_maximum_stream_bitrate: builtins.int | None = ..., + live_stream_lens_supported: builtins.bool | None = ..., + live_stream_lens_supported_array: (collections.abc.Iterable[global___EnumLens.ValueType] | None) = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "live_stream_bitrate", + b"live_stream_bitrate", + "live_stream_encode", + b"live_stream_encode", + "live_stream_encode_supported", + b"live_stream_encode_supported", + "live_stream_error", + b"live_stream_error", + "live_stream_lens_supported", + b"live_stream_lens_supported", + "live_stream_max_lens_unsupported", + b"live_stream_max_lens_unsupported", + "live_stream_maximum_stream_bitrate", + b"live_stream_maximum_stream_bitrate", + "live_stream_minimum_stream_bitrate", + b"live_stream_minimum_stream_bitrate", + "live_stream_status", + b"live_stream_status", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "live_stream_bitrate", + b"live_stream_bitrate", + "live_stream_encode", + b"live_stream_encode", + "live_stream_encode_supported", + b"live_stream_encode_supported", + "live_stream_error", + b"live_stream_error", + "live_stream_lens_supported", + b"live_stream_lens_supported", + "live_stream_lens_supported_array", + b"live_stream_lens_supported_array", + "live_stream_max_lens_unsupported", + b"live_stream_max_lens_unsupported", + "live_stream_maximum_stream_bitrate", + b"live_stream_maximum_stream_bitrate", + "live_stream_minimum_stream_bitrate", + b"live_stream_minimum_stream_bitrate", + "live_stream_status", + b"live_stream_status", + "live_stream_window_size_supported_array", + b"live_stream_window_size_supported_array", + ], + ) -> None: ... + +global___NotifyLiveStreamStatus = NotifyLiveStreamStatus + +@typing_extensions.final +class RequestGetLiveStreamStatus(google.protobuf.message.Message): + """* + Get the current livestream status (and optionally register for future status changes) + + Response: @ref NotifyLiveStreamStatus + + Notification: @ref NotifyLiveStreamStatus + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + REGISTER_LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int + UNREGISTER_LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int + + @property + def register_live_stream_status( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + global___EnumRegisterLiveStreamStatus.ValueType + ]: + """Array of live stream statuses to be notified about""" + @property + def unregister_live_stream_status( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + global___EnumRegisterLiveStreamStatus.ValueType + ]: + """Array of live stream statuses to stop being notified about""" + def __init__( + self, + *, + register_live_stream_status: ( + collections.abc.Iterable[global___EnumRegisterLiveStreamStatus.ValueType] | None + ) = ..., + unregister_live_stream_status: ( + collections.abc.Iterable[global___EnumRegisterLiveStreamStatus.ValueType] | None + ) = ... + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "register_live_stream_status", + b"register_live_stream_status", + "unregister_live_stream_status", + b"unregister_live_stream_status", + ], + ) -> None: ... + +global___RequestGetLiveStreamStatus = RequestGetLiveStreamStatus + +@typing_extensions.final +class RequestSetLiveStreamMode(google.protobuf.message.Message): + """* + Configure Live Streaming + + Response: @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + URL_FIELD_NUMBER: builtins.int + ENCODE_FIELD_NUMBER: builtins.int + WINDOW_SIZE_FIELD_NUMBER: builtins.int + CERT_FIELD_NUMBER: builtins.int + MINIMUM_BITRATE_FIELD_NUMBER: builtins.int + MAXIMUM_BITRATE_FIELD_NUMBER: builtins.int + STARTING_BITRATE_FIELD_NUMBER: builtins.int + LENS_FIELD_NUMBER: builtins.int + url: builtins.str + "RTMP(S) URL used for live stream" + encode: builtins.bool + "Save media to sdcard while streaming?" + window_size: global___EnumWindowSize.ValueType + "*\n Resolution to use for live stream\n\n The set of supported lenses is only available from the `live_stream_window_size_supported_array` in @ref NotifyLiveStreamStatus)\n " + cert: builtins.bytes + "Certificate for servers that require it in PEM format" + minimum_bitrate: builtins.int + "Minimum desired bitrate (may or may not be honored)" + maximum_bitrate: builtins.int + "Maximum desired bitrate (may or may not be honored)" + starting_bitrate: builtins.int + "Starting bitrate" + lens: global___EnumLens.ValueType + "*\n Lens to use for live stream\n\n The set of supported lenses is only available from the `live_stream_lens_supported_array` in @ref NotifyLiveStreamStatus)\n " + + def __init__( + self, + *, + url: builtins.str | None = ..., + encode: builtins.bool | None = ..., + window_size: global___EnumWindowSize.ValueType | None = ..., + cert: builtins.bytes | None = ..., + minimum_bitrate: builtins.int | None = ..., + maximum_bitrate: builtins.int | None = ..., + starting_bitrate: builtins.int | None = ..., + lens: global___EnumLens.ValueType | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "cert", + b"cert", + "encode", + b"encode", + "lens", + b"lens", + "maximum_bitrate", + b"maximum_bitrate", + "minimum_bitrate", + b"minimum_bitrate", + "starting_bitrate", + b"starting_bitrate", + "url", + b"url", + "window_size", + b"window_size", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "cert", + b"cert", + "encode", + b"encode", + "lens", + b"lens", + "maximum_bitrate", + b"maximum_bitrate", + "minimum_bitrate", + b"minimum_bitrate", + "starting_bitrate", + b"starting_bitrate", + "url", + b"url", + "window_size", + b"window_size", + ], + ) -> None: ... + +global___RequestSetLiveStreamMode = RequestSetLiveStreamMode diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/media_pb2.py b/demos/python/sdk_wireless_camera_control/open_gopro/proto/media_pb2.py index 05ef3d94..e9241b14 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/media_pb2.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/media_pb2.py @@ -1,23 +1,24 @@ # media_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). -# This copyright was auto-generated on Mon Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:47 UTC 2024 -"""Generated protocol buffer code.""" -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 - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\x0bmedia.proto\x12\nopen_gopro\x1a\x16response_generic.proto"\x1d\n\x1bRequestGetLastCapturedMedia"l\n\x19ResponseLastCapturedMedia\x12-\n\x06result\x18\x01 \x01(\x0e2\x1d.open_gopro.EnumResultGeneric\x12 \n\x05media\x18\x02 \x01(\x0b2\x11.open_gopro.Media' -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "media_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _REQUESTGETLASTCAPTUREDMEDIA._serialized_start = 51 - _REQUESTGETLASTCAPTUREDMEDIA._serialized_end = 80 - _RESPONSELASTCAPTUREDMEDIA._serialized_start = 82 - _RESPONSELASTCAPTUREDMEDIA._serialized_end = 190 +"""Generated protocol buffer code.""" + +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 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0bmedia.proto\x12\nopen_gopro\x1a\x16response_generic.proto"\x1d\n\x1bRequestGetLastCapturedMedia"l\n\x19ResponseLastCapturedMedia\x12-\n\x06result\x18\x01 \x01(\x0e2\x1d.open_gopro.EnumResultGeneric\x12 \n\x05media\x18\x02 \x01(\x0b2\x11.open_gopro.Media' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "media_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _REQUESTGETLASTCAPTUREDMEDIA._serialized_start = 51 + _REQUESTGETLASTCAPTUREDMEDIA._serialized_end = 80 + _RESPONSELASTCAPTUREDMEDIA._serialized_start = 82 + _RESPONSELASTCAPTUREDMEDIA._serialized_end = 190 diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/media_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/media_pb2.pyi index 7e0584ba..845282f3 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/media_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/media_pb2.pyi @@ -1,64 +1,74 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Commands to query and manipulate media files -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.message -from . import response_generic_pb2 -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class RequestGetLastCapturedMedia(google.protobuf.message.Message): - """* - Get the last captured media filename - - Returns a @ref ResponseLastCapturedMedia - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - def __init__(self) -> None: ... - -global___RequestGetLastCapturedMedia = RequestGetLastCapturedMedia - -class ResponseLastCapturedMedia(google.protobuf.message.Message): - """* - Message sent in response to a @ref RequestGetLastCapturedMedia - - This contains the complete path of the last captured media. Depending on the type of media captured, it will return: - - - Single photo / video: The single media path - - Any grouped media: The path to the first captured media in the group - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - RESULT_FIELD_NUMBER: builtins.int - MEDIA_FIELD_NUMBER: builtins.int - result: response_generic_pb2.EnumResultGeneric.ValueType - "Was the request successful?" - - @property - def media(self) -> response_generic_pb2.Media: - """* - Last captured media if result is RESULT_SUCCESS. Invalid if result is RESULT_RESOURCE_NOT_AVAILBLE. - """ - def __init__( - self, - *, - result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., - media: response_generic_pb2.Media | None = ... - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["media", b"media", "result", b"result"] - ) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["media", b"media", "result", b"result"]) -> None: ... - -global___ResponseLastCapturedMedia = ResponseLastCapturedMedia +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Commands to query and manipulate media files +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.message +from . import response_generic_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing_extensions.final +class RequestGetLastCapturedMedia(google.protobuf.message.Message): + """* + Get the last captured media filename + + Returns a @ref ResponseLastCapturedMedia + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestGetLastCapturedMedia = RequestGetLastCapturedMedia + +@typing_extensions.final +class ResponseLastCapturedMedia(google.protobuf.message.Message): + """* + The Last Captured Media + + Message is sent in response to a @ref RequestGetLastCapturedMedia. + + This contains the relative path of the last captured media starting from the `DCIM` directory on the SDCard. Depending + on the type of media captured, it will return: + + - The single media path for single photo/video media + - The path to the first captured media in the group for grouped media + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + MEDIA_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Was the request successful?" + + @property + def media(self) -> response_generic_pb2.Media: + """* + Last captured media if result is RESULT_SUCCESS. Invalid if result is RESULT_RESOURCE_NOT_AVAILBLE. + """ + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + media: response_generic_pb2.Media | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["media", b"media", "result", b"result"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["media", b"media", "result", b"result"], + ) -> None: ... + +global___ResponseLastCapturedMedia = ResponseLastCapturedMedia 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 be49fe85..9c00171d 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,49 +1,50 @@ # 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 Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:47 UTC 2024 -"""Generated protocol buffer code.""" -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 - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\x18network_management.proto\x12\nopen_gopro\x1a\x16response_generic.proto"R\n\x16NotifProvisioningState\x128\n\x12provisioning_state\x18\x01 \x02(\x0e2\x1c.open_gopro.EnumProvisioning"\x8d\x01\n\x12NotifStartScanning\x120\n\x0escanning_state\x18\x01 \x02(\x0e2\x18.open_gopro.EnumScanning\x12\x0f\n\x07scan_id\x18\x02 \x01(\x05\x12\x15\n\rtotal_entries\x18\x03 \x01(\x05\x12\x1d\n\x15total_configured_ssid\x18\x04 \x02(\x05"\x1e\n\x0eRequestConnect\x12\x0c\n\x04ssid\x18\x01 \x02(\t"\x93\x01\n\x11RequestConnectNew\x12\x0c\n\x04ssid\x18\x01 \x02(\t\x12\x10\n\x08password\x18\x02 \x02(\t\x12\x11\n\tstatic_ip\x18\x03 \x01(\x0c\x12\x0f\n\x07gateway\x18\x04 \x01(\x0c\x12\x0e\n\x06subnet\x18\x05 \x01(\x0c\x12\x13\n\x0bdns_primary\x18\x06 \x01(\x0c\x12\x15\n\rdns_secondary\x18\x07 \x01(\x0c"P\n\x13RequestGetApEntries\x12\x13\n\x0bstart_index\x18\x01 \x02(\x05\x12\x13\n\x0bmax_entries\x18\x02 \x02(\x05\x12\x0f\n\x07scan_id\x18\x03 \x02(\x05"\x17\n\x15RequestReleaseNetwork"\x12\n\x10RequestStartScan"\x93\x01\n\x0fResponseConnect\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x128\n\x12provisioning_state\x18\x02 \x02(\x0e2\x1c.open_gopro.EnumProvisioning\x12\x17\n\x0ftimeout_seconds\x18\x03 \x02(\x05"\x96\x01\n\x12ResponseConnectNew\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x128\n\x12provisioning_state\x18\x02 \x02(\x0e2\x1c.open_gopro.EnumProvisioning\x12\x17\n\x0ftimeout_seconds\x18\x03 \x02(\x05"\x84\x02\n\x14ResponseGetApEntries\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x12\x0f\n\x07scan_id\x18\x02 \x02(\x05\x12;\n\x07entries\x18\x03 \x03(\x0b2*.open_gopro.ResponseGetApEntries.ScanEntry\x1ao\n\tScanEntry\x12\x0c\n\x04ssid\x18\x01 \x02(\t\x12\x1c\n\x14signal_strength_bars\x18\x02 \x02(\x05\x12\x1c\n\x14signal_frequency_mhz\x18\x04 \x02(\x05\x12\x18\n\x10scan_entry_flags\x18\x05 \x02(\x05"x\n\x15ResponseStartScanning\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x120\n\x0escanning_state\x18\x02 \x02(\x0e2\x18.open_gopro.EnumScanning*\xb5\x03\n\x10EnumProvisioning\x12\x18\n\x14PROVISIONING_UNKNOWN\x10\x00\x12\x1e\n\x1aPROVISIONING_NEVER_STARTED\x10\x01\x12\x18\n\x14PROVISIONING_STARTED\x10\x02\x12"\n\x1ePROVISIONING_ABORTED_BY_SYSTEM\x10\x03\x12"\n\x1ePROVISIONING_CANCELLED_BY_USER\x10\x04\x12\x1f\n\x1bPROVISIONING_SUCCESS_NEW_AP\x10\x05\x12\x1f\n\x1bPROVISIONING_SUCCESS_OLD_AP\x10\x06\x12*\n&PROVISIONING_ERROR_FAILED_TO_ASSOCIATE\x10\x07\x12$\n PROVISIONING_ERROR_PASSWORD_AUTH\x10\x08\x12$\n PROVISIONING_ERROR_EULA_BLOCKING\x10\t\x12"\n\x1ePROVISIONING_ERROR_NO_INTERNET\x10\n\x12\'\n#PROVISIONING_ERROR_UNSUPPORTED_TYPE\x10\x0b*\xac\x01\n\x0cEnumScanning\x12\x14\n\x10SCANNING_UNKNOWN\x10\x00\x12\x1a\n\x16SCANNING_NEVER_STARTED\x10\x01\x12\x14\n\x10SCANNING_STARTED\x10\x02\x12\x1e\n\x1aSCANNING_ABORTED_BY_SYSTEM\x10\x03\x12\x1e\n\x1aSCANNING_CANCELLED_BY_USER\x10\x04\x12\x14\n\x10SCANNING_SUCCESS\x10\x05*\xb2\x01\n\x12EnumScanEntryFlags\x12\x12\n\x0eSCAN_FLAG_OPEN\x10\x00\x12\x1b\n\x17SCAN_FLAG_AUTHENTICATED\x10\x01\x12\x18\n\x14SCAN_FLAG_CONFIGURED\x10\x02\x12\x17\n\x13SCAN_FLAG_BEST_SSID\x10\x04\x12\x18\n\x14SCAN_FLAG_ASSOCIATED\x10\x08\x12\x1e\n\x1aSCAN_FLAG_UNSUPPORTED_TYPE\x10\x10' -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "network_management_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _ENUMPROVISIONING._serialized_start = 1290 - _ENUMPROVISIONING._serialized_end = 1727 - _ENUMSCANNING._serialized_start = 1730 - _ENUMSCANNING._serialized_end = 1902 - _ENUMSCANENTRYFLAGS._serialized_start = 1905 - _ENUMSCANENTRYFLAGS._serialized_end = 2083 - _NOTIFPROVISIONINGSTATE._serialized_start = 64 - _NOTIFPROVISIONINGSTATE._serialized_end = 146 - _NOTIFSTARTSCANNING._serialized_start = 149 - _NOTIFSTARTSCANNING._serialized_end = 290 - _REQUESTCONNECT._serialized_start = 292 - _REQUESTCONNECT._serialized_end = 322 - _REQUESTCONNECTNEW._serialized_start = 325 - _REQUESTCONNECTNEW._serialized_end = 472 - _REQUESTGETAPENTRIES._serialized_start = 474 - _REQUESTGETAPENTRIES._serialized_end = 554 - _REQUESTRELEASENETWORK._serialized_start = 556 - _REQUESTRELEASENETWORK._serialized_end = 579 - _REQUESTSTARTSCAN._serialized_start = 581 - _REQUESTSTARTSCAN._serialized_end = 599 - _RESPONSECONNECT._serialized_start = 602 - _RESPONSECONNECT._serialized_end = 749 - _RESPONSECONNECTNEW._serialized_start = 752 - _RESPONSECONNECTNEW._serialized_end = 902 - _RESPONSEGETAPENTRIES._serialized_start = 905 - _RESPONSEGETAPENTRIES._serialized_end = 1165 - _RESPONSEGETAPENTRIES_SCANENTRY._serialized_start = 1054 - _RESPONSEGETAPENTRIES_SCANENTRY._serialized_end = 1165 - _RESPONSESTARTSCANNING._serialized_start = 1167 - _RESPONSESTARTSCANNING._serialized_end = 1287 +"""Generated protocol buffer code.""" + +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 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x18network_management.proto\x12\nopen_gopro\x1a\x16response_generic.proto"R\n\x16NotifProvisioningState\x128\n\x12provisioning_state\x18\x01 \x02(\x0e2\x1c.open_gopro.EnumProvisioning"\x8d\x01\n\x12NotifStartScanning\x120\n\x0escanning_state\x18\x01 \x02(\x0e2\x18.open_gopro.EnumScanning\x12\x0f\n\x07scan_id\x18\x02 \x01(\x05\x12\x15\n\rtotal_entries\x18\x03 \x01(\x05\x12\x1d\n\x15total_configured_ssid\x18\x04 \x02(\x05"\x1e\n\x0eRequestConnect\x12\x0c\n\x04ssid\x18\x01 \x02(\t"\x93\x01\n\x11RequestConnectNew\x12\x0c\n\x04ssid\x18\x01 \x02(\t\x12\x10\n\x08password\x18\x02 \x02(\t\x12\x11\n\tstatic_ip\x18\x03 \x01(\x0c\x12\x0f\n\x07gateway\x18\x04 \x01(\x0c\x12\x0e\n\x06subnet\x18\x05 \x01(\x0c\x12\x13\n\x0bdns_primary\x18\x06 \x01(\x0c\x12\x15\n\rdns_secondary\x18\x07 \x01(\x0c"P\n\x13RequestGetApEntries\x12\x13\n\x0bstart_index\x18\x01 \x02(\x05\x12\x13\n\x0bmax_entries\x18\x02 \x02(\x05\x12\x0f\n\x07scan_id\x18\x03 \x02(\x05"\x17\n\x15RequestReleaseNetwork"\x12\n\x10RequestStartScan"\x93\x01\n\x0fResponseConnect\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x128\n\x12provisioning_state\x18\x02 \x02(\x0e2\x1c.open_gopro.EnumProvisioning\x12\x17\n\x0ftimeout_seconds\x18\x03 \x02(\x05"\x96\x01\n\x12ResponseConnectNew\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x128\n\x12provisioning_state\x18\x02 \x02(\x0e2\x1c.open_gopro.EnumProvisioning\x12\x17\n\x0ftimeout_seconds\x18\x03 \x02(\x05"\x84\x02\n\x14ResponseGetApEntries\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x12\x0f\n\x07scan_id\x18\x02 \x02(\x05\x12;\n\x07entries\x18\x03 \x03(\x0b2*.open_gopro.ResponseGetApEntries.ScanEntry\x1ao\n\tScanEntry\x12\x0c\n\x04ssid\x18\x01 \x02(\t\x12\x1c\n\x14signal_strength_bars\x18\x02 \x02(\x05\x12\x1c\n\x14signal_frequency_mhz\x18\x04 \x02(\x05\x12\x18\n\x10scan_entry_flags\x18\x05 \x02(\x05"x\n\x15ResponseStartScanning\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x120\n\x0escanning_state\x18\x02 \x02(\x0e2\x18.open_gopro.EnumScanning*\xb5\x03\n\x10EnumProvisioning\x12\x18\n\x14PROVISIONING_UNKNOWN\x10\x00\x12\x1e\n\x1aPROVISIONING_NEVER_STARTED\x10\x01\x12\x18\n\x14PROVISIONING_STARTED\x10\x02\x12"\n\x1ePROVISIONING_ABORTED_BY_SYSTEM\x10\x03\x12"\n\x1ePROVISIONING_CANCELLED_BY_USER\x10\x04\x12\x1f\n\x1bPROVISIONING_SUCCESS_NEW_AP\x10\x05\x12\x1f\n\x1bPROVISIONING_SUCCESS_OLD_AP\x10\x06\x12*\n&PROVISIONING_ERROR_FAILED_TO_ASSOCIATE\x10\x07\x12$\n PROVISIONING_ERROR_PASSWORD_AUTH\x10\x08\x12$\n PROVISIONING_ERROR_EULA_BLOCKING\x10\t\x12"\n\x1ePROVISIONING_ERROR_NO_INTERNET\x10\n\x12\'\n#PROVISIONING_ERROR_UNSUPPORTED_TYPE\x10\x0b*\xac\x01\n\x0cEnumScanning\x12\x14\n\x10SCANNING_UNKNOWN\x10\x00\x12\x1a\n\x16SCANNING_NEVER_STARTED\x10\x01\x12\x14\n\x10SCANNING_STARTED\x10\x02\x12\x1e\n\x1aSCANNING_ABORTED_BY_SYSTEM\x10\x03\x12\x1e\n\x1aSCANNING_CANCELLED_BY_USER\x10\x04\x12\x14\n\x10SCANNING_SUCCESS\x10\x05*\xb2\x01\n\x12EnumScanEntryFlags\x12\x12\n\x0eSCAN_FLAG_OPEN\x10\x00\x12\x1b\n\x17SCAN_FLAG_AUTHENTICATED\x10\x01\x12\x18\n\x14SCAN_FLAG_CONFIGURED\x10\x02\x12\x17\n\x13SCAN_FLAG_BEST_SSID\x10\x04\x12\x18\n\x14SCAN_FLAG_ASSOCIATED\x10\x08\x12\x1e\n\x1aSCAN_FLAG_UNSUPPORTED_TYPE\x10\x10' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "network_management_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMPROVISIONING._serialized_start = 1290 + _ENUMPROVISIONING._serialized_end = 1727 + _ENUMSCANNING._serialized_start = 1730 + _ENUMSCANNING._serialized_end = 1902 + _ENUMSCANENTRYFLAGS._serialized_start = 1905 + _ENUMSCANENTRYFLAGS._serialized_end = 2083 + _NOTIFPROVISIONINGSTATE._serialized_start = 64 + _NOTIFPROVISIONINGSTATE._serialized_end = 146 + _NOTIFSTARTSCANNING._serialized_start = 149 + _NOTIFSTARTSCANNING._serialized_end = 290 + _REQUESTCONNECT._serialized_start = 292 + _REQUESTCONNECT._serialized_end = 322 + _REQUESTCONNECTNEW._serialized_start = 325 + _REQUESTCONNECTNEW._serialized_end = 472 + _REQUESTGETAPENTRIES._serialized_start = 474 + _REQUESTGETAPENTRIES._serialized_end = 554 + _REQUESTRELEASENETWORK._serialized_start = 556 + _REQUESTRELEASENETWORK._serialized_end = 579 + _REQUESTSTARTSCAN._serialized_start = 581 + _REQUESTSTARTSCAN._serialized_end = 599 + _RESPONSECONNECT._serialized_start = 602 + _RESPONSECONNECT._serialized_end = 749 + _RESPONSECONNECTNEW._serialized_start = 752 + _RESPONSECONNECTNEW._serialized_end = 902 + _RESPONSEGETAPENTRIES._serialized_start = 905 + _RESPONSEGETAPENTRIES._serialized_end = 1165 + _RESPONSEGETAPENTRIES_SCANENTRY._serialized_start = 1054 + _RESPONSEGETAPENTRIES_SCANENTRY._serialized_end = 1165 + _RESPONSESTARTSCANNING._serialized_start = 1167 + _RESPONSESTARTSCANNING._serialized_end = 1287 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 3694b6ec..1c81fec4 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 @@ -1,571 +1,632 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Defines the structure of protobuf messages for network management -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -from . import response_generic_pb2 -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _EnumProvisioning: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumProvisioningEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumProvisioning.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - PROVISIONING_UNKNOWN: _EnumProvisioning.ValueType - PROVISIONING_NEVER_STARTED: _EnumProvisioning.ValueType - PROVISIONING_STARTED: _EnumProvisioning.ValueType - PROVISIONING_ABORTED_BY_SYSTEM: _EnumProvisioning.ValueType - PROVISIONING_CANCELLED_BY_USER: _EnumProvisioning.ValueType - PROVISIONING_SUCCESS_NEW_AP: _EnumProvisioning.ValueType - PROVISIONING_SUCCESS_OLD_AP: _EnumProvisioning.ValueType - PROVISIONING_ERROR_FAILED_TO_ASSOCIATE: _EnumProvisioning.ValueType - PROVISIONING_ERROR_PASSWORD_AUTH: _EnumProvisioning.ValueType - PROVISIONING_ERROR_EULA_BLOCKING: _EnumProvisioning.ValueType - PROVISIONING_ERROR_NO_INTERNET: _EnumProvisioning.ValueType - PROVISIONING_ERROR_UNSUPPORTED_TYPE: _EnumProvisioning.ValueType - -class EnumProvisioning(_EnumProvisioning, metaclass=_EnumProvisioningEnumTypeWrapper): ... - -PROVISIONING_UNKNOWN: EnumProvisioning.ValueType -PROVISIONING_NEVER_STARTED: EnumProvisioning.ValueType -PROVISIONING_STARTED: EnumProvisioning.ValueType -PROVISIONING_ABORTED_BY_SYSTEM: EnumProvisioning.ValueType -PROVISIONING_CANCELLED_BY_USER: EnumProvisioning.ValueType -PROVISIONING_SUCCESS_NEW_AP: EnumProvisioning.ValueType -PROVISIONING_SUCCESS_OLD_AP: EnumProvisioning.ValueType -PROVISIONING_ERROR_FAILED_TO_ASSOCIATE: EnumProvisioning.ValueType -PROVISIONING_ERROR_PASSWORD_AUTH: EnumProvisioning.ValueType -PROVISIONING_ERROR_EULA_BLOCKING: EnumProvisioning.ValueType -PROVISIONING_ERROR_NO_INTERNET: EnumProvisioning.ValueType -PROVISIONING_ERROR_UNSUPPORTED_TYPE: EnumProvisioning.ValueType -global___EnumProvisioning = EnumProvisioning - -class _EnumScanning: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumScanningEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumScanning.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - SCANNING_UNKNOWN: _EnumScanning.ValueType - SCANNING_NEVER_STARTED: _EnumScanning.ValueType - SCANNING_STARTED: _EnumScanning.ValueType - SCANNING_ABORTED_BY_SYSTEM: _EnumScanning.ValueType - SCANNING_CANCELLED_BY_USER: _EnumScanning.ValueType - SCANNING_SUCCESS: _EnumScanning.ValueType - -class EnumScanning(_EnumScanning, metaclass=_EnumScanningEnumTypeWrapper): ... - -SCANNING_UNKNOWN: EnumScanning.ValueType -SCANNING_NEVER_STARTED: EnumScanning.ValueType -SCANNING_STARTED: EnumScanning.ValueType -SCANNING_ABORTED_BY_SYSTEM: EnumScanning.ValueType -SCANNING_CANCELLED_BY_USER: EnumScanning.ValueType -SCANNING_SUCCESS: EnumScanning.ValueType -global___EnumScanning = EnumScanning - -class _EnumScanEntryFlags: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumScanEntryFlagsEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumScanEntryFlags.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - SCAN_FLAG_OPEN: _EnumScanEntryFlags.ValueType - "This network does not require authentication" - SCAN_FLAG_AUTHENTICATED: _EnumScanEntryFlags.ValueType - "This network requires authentication" - SCAN_FLAG_CONFIGURED: _EnumScanEntryFlags.ValueType - "This network has been previously provisioned" - SCAN_FLAG_BEST_SSID: _EnumScanEntryFlags.ValueType - SCAN_FLAG_ASSOCIATED: _EnumScanEntryFlags.ValueType - "camera is connected to this AP" - SCAN_FLAG_UNSUPPORTED_TYPE: _EnumScanEntryFlags.ValueType - -class EnumScanEntryFlags(_EnumScanEntryFlags, metaclass=_EnumScanEntryFlagsEnumTypeWrapper): ... - -SCAN_FLAG_OPEN: EnumScanEntryFlags.ValueType -"This network does not require authentication" -SCAN_FLAG_AUTHENTICATED: EnumScanEntryFlags.ValueType -"This network requires authentication" -SCAN_FLAG_CONFIGURED: EnumScanEntryFlags.ValueType -"This network has been previously provisioned" -SCAN_FLAG_BEST_SSID: EnumScanEntryFlags.ValueType -SCAN_FLAG_ASSOCIATED: EnumScanEntryFlags.ValueType -"camera is connected to this AP" -SCAN_FLAG_UNSUPPORTED_TYPE: EnumScanEntryFlags.ValueType -global___EnumScanEntryFlags = EnumScanEntryFlags - -class NotifProvisioningState(google.protobuf.message.Message): - """ - Provision state notification - - TODO refernce where this is triggered - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - PROVISIONING_STATE_FIELD_NUMBER: builtins.int - provisioning_state: global___EnumProvisioning.ValueType - "Provisioning / connection state" - - def __init__(self, *, provisioning_state: global___EnumProvisioning.ValueType | None = ...) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["provisioning_state", b"provisioning_state"] - ) -> builtins.bool: ... - def ClearField( - self, field_name: typing_extensions.Literal["provisioning_state", b"provisioning_state"] - ) -> None: ... - -global___NotifProvisioningState = NotifProvisioningState - -class NotifStartScanning(google.protobuf.message.Message): - """ - Scanning state notification - - Triggered via @ref RequestStartScan - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - SCANNING_STATE_FIELD_NUMBER: builtins.int - SCAN_ID_FIELD_NUMBER: builtins.int - TOTAL_ENTRIES_FIELD_NUMBER: builtins.int - TOTAL_CONFIGURED_SSID_FIELD_NUMBER: builtins.int - scanning_state: global___EnumScanning.ValueType - "Scanning state" - scan_id: builtins.int - "ID associated with scan results (included if scan was successful)" - total_entries: builtins.int - "Number of APs found during scan (included if scan was successful)" - total_configured_ssid: builtins.int - "Total count of camera's provisioned SSIDs" - - def __init__( - self, - *, - scanning_state: global___EnumScanning.ValueType | None = ..., - scan_id: builtins.int | None = ..., - total_entries: builtins.int | None = ..., - total_configured_ssid: builtins.int | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "scan_id", - b"scan_id", - "scanning_state", - b"scanning_state", - "total_configured_ssid", - b"total_configured_ssid", - "total_entries", - b"total_entries", - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "scan_id", - b"scan_id", - "scanning_state", - b"scanning_state", - "total_configured_ssid", - b"total_configured_ssid", - "total_entries", - b"total_entries", - ], - ) -> None: ... - -global___NotifStartScanning = NotifStartScanning - -class RequestConnect(google.protobuf.message.Message): - """* - Connect to (but do not authenticate with) an Access Point - - This is intended to be used to connect to a previously-connected Access Point - - Response: @ref ResponseConnect - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - SSID_FIELD_NUMBER: builtins.int - ssid: builtins.str - "AP SSID" - - def __init__(self, *, ssid: builtins.str | None = ...) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["ssid", b"ssid"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["ssid", b"ssid"]) -> None: ... - -global___RequestConnect = RequestConnect - -class RequestConnectNew(google.protobuf.message.Message): - """* - Connect to and authenticate with an Access Point - - This is only intended to be used if the AP is not previously provisioned. - - Response: @ref ResponseConnectNew sent immediately - - Notification: @ref NotifProvisioningState sent periodically as provisioning state changes - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - SSID_FIELD_NUMBER: builtins.int - PASSWORD_FIELD_NUMBER: builtins.int - STATIC_IP_FIELD_NUMBER: builtins.int - GATEWAY_FIELD_NUMBER: builtins.int - SUBNET_FIELD_NUMBER: builtins.int - DNS_PRIMARY_FIELD_NUMBER: builtins.int - DNS_SECONDARY_FIELD_NUMBER: builtins.int - ssid: builtins.str - "AP SSID" - password: builtins.str - "AP password" - static_ip: builtins.bytes - "Static IP address" - gateway: builtins.bytes - "Gateway IP address" - subnet: builtins.bytes - "Subnet mask" - dns_primary: builtins.bytes - "Primary DNS" - dns_secondary: builtins.bytes - "Secondary DNS" - - def __init__( - self, - *, - ssid: builtins.str | None = ..., - password: builtins.str | None = ..., - static_ip: builtins.bytes | None = ..., - gateway: builtins.bytes | None = ..., - subnet: builtins.bytes | None = ..., - dns_primary: builtins.bytes | None = ..., - dns_secondary: builtins.bytes | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "dns_primary", - b"dns_primary", - "dns_secondary", - b"dns_secondary", - "gateway", - b"gateway", - "password", - b"password", - "ssid", - b"ssid", - "static_ip", - b"static_ip", - "subnet", - b"subnet", - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "dns_primary", - b"dns_primary", - "dns_secondary", - b"dns_secondary", - "gateway", - b"gateway", - "password", - b"password", - "ssid", - b"ssid", - "static_ip", - b"static_ip", - "subnet", - b"subnet", - ], - ) -> None: ... - -global___RequestConnectNew = RequestConnectNew - -class RequestGetApEntries(google.protobuf.message.Message): - """* - Get a list of Access Points found during a @ref RequestStartScan - - Response: @ref ResponseGetApEntries - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - START_INDEX_FIELD_NUMBER: builtins.int - MAX_ENTRIES_FIELD_NUMBER: builtins.int - SCAN_ID_FIELD_NUMBER: builtins.int - start_index: builtins.int - "Used for paging. 0 <= start_index < @ref ResponseGetApEntries .total_entries" - max_entries: builtins.int - "Used for paging. Value must be < @ref ResponseGetApEntries .total_entries" - scan_id: builtins.int - "ID corresponding to a set of scan results (i.e. @ref ResponseGetApEntries .scan_id)" - - def __init__( - self, - *, - start_index: builtins.int | None = ..., - max_entries: builtins.int | None = ..., - scan_id: builtins.int | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "max_entries", b"max_entries", "scan_id", b"scan_id", "start_index", b"start_index" - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "max_entries", b"max_entries", "scan_id", b"scan_id", "start_index", b"start_index" - ], - ) -> None: ... - -global___RequestGetApEntries = RequestGetApEntries - -class RequestReleaseNetwork(google.protobuf.message.Message): - """* - Request to disconnect from current AP network - - Response: @ref ResponseGeneric - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - def __init__(self) -> None: ... - -global___RequestReleaseNetwork = RequestReleaseNetwork - -class RequestStartScan(google.protobuf.message.Message): - """* - Start scanning for Access Points - - @note Serialization of this object is zero bytes. - - Response: @ref ResponseStartScanning are sent immediately after the camera receives this command - - Notifications: @ref NotifStartScanning are sent periodically as scanning state changes. Use to detect scan complete. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - def __init__(self) -> None: ... - -global___RequestStartScan = RequestStartScan - -class ResponseConnect(google.protobuf.message.Message): - """* - The status of an attempt to connect to an Access Point - - Sent as the initial response to @ref RequestConnect - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - RESULT_FIELD_NUMBER: builtins.int - PROVISIONING_STATE_FIELD_NUMBER: builtins.int - TIMEOUT_SECONDS_FIELD_NUMBER: builtins.int - result: response_generic_pb2.EnumResultGeneric.ValueType - "Generic pass/fail/error info" - provisioning_state: global___EnumProvisioning.ValueType - "Provisioning/connection state" - timeout_seconds: builtins.int - "Network connection timeout (seconds)" - - def __init__( - self, - *, - result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., - provisioning_state: global___EnumProvisioning.ValueType | None = ..., - timeout_seconds: builtins.int | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "provisioning_state", b"provisioning_state", "result", b"result", "timeout_seconds", b"timeout_seconds" - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "provisioning_state", b"provisioning_state", "result", b"result", "timeout_seconds", b"timeout_seconds" - ], - ) -> None: ... - -global___ResponseConnect = ResponseConnect - -class ResponseConnectNew(google.protobuf.message.Message): - """* - The status of an attempt to connect to an Access Point - - Sent as the initial response to @ref RequestConnectNew - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - RESULT_FIELD_NUMBER: builtins.int - PROVISIONING_STATE_FIELD_NUMBER: builtins.int - TIMEOUT_SECONDS_FIELD_NUMBER: builtins.int - result: response_generic_pb2.EnumResultGeneric.ValueType - "Status of Connect New request" - provisioning_state: global___EnumProvisioning.ValueType - "Current provisioning state of the network" - timeout_seconds: builtins.int - "*\n number of seconds camera will wait before declaring a network connection attempt failed.\n " - - def __init__( - self, - *, - result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., - provisioning_state: global___EnumProvisioning.ValueType | None = ..., - timeout_seconds: builtins.int | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "provisioning_state", b"provisioning_state", "result", b"result", "timeout_seconds", b"timeout_seconds" - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "provisioning_state", b"provisioning_state", "result", b"result", "timeout_seconds", b"timeout_seconds" - ], - ) -> None: ... - -global___ResponseConnectNew = ResponseConnectNew - -class ResponseGetApEntries(google.protobuf.message.Message): - """* - A list of scan entries describing a scanned Access Point - - This is sent in response to a @ref RequestGetApEntries - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - class ScanEntry(google.protobuf.message.Message): - """The individual Scan Entry model""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - SSID_FIELD_NUMBER: builtins.int - SIGNAL_STRENGTH_BARS_FIELD_NUMBER: builtins.int - SIGNAL_FREQUENCY_MHZ_FIELD_NUMBER: builtins.int - SCAN_ENTRY_FLAGS_FIELD_NUMBER: builtins.int - ssid: builtins.str - "AP SSID" - signal_strength_bars: builtins.int - "Signal strength (3 bars: >-70 dBm; 2 bars: >-85 dBm; 1 bar: <=-85 dBm)" - signal_frequency_mhz: builtins.int - "Signal frequency (MHz)" - scan_entry_flags: builtins.int - "Bitmasked value from @ref EnumScanEntryFlags" - - def __init__( - self, - *, - ssid: builtins.str | None = ..., - signal_strength_bars: builtins.int | None = ..., - signal_frequency_mhz: builtins.int | None = ..., - scan_entry_flags: builtins.int | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "scan_entry_flags", - b"scan_entry_flags", - "signal_frequency_mhz", - b"signal_frequency_mhz", - "signal_strength_bars", - b"signal_strength_bars", - "ssid", - b"ssid", - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "scan_entry_flags", - b"scan_entry_flags", - "signal_frequency_mhz", - b"signal_frequency_mhz", - "signal_strength_bars", - b"signal_strength_bars", - "ssid", - b"ssid", - ], - ) -> None: ... - RESULT_FIELD_NUMBER: builtins.int - SCAN_ID_FIELD_NUMBER: builtins.int - ENTRIES_FIELD_NUMBER: builtins.int - result: response_generic_pb2.EnumResultGeneric.ValueType - "Generic pass/fail/error info" - scan_id: builtins.int - "ID associated with this batch of results" - - @property - def entries( - self, - ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResponseGetApEntries.ScanEntry]: - """Array containing details about discovered APs""" - def __init__( - self, - *, - result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., - scan_id: builtins.int | None = ..., - entries: collections.abc.Iterable[global___ResponseGetApEntries.ScanEntry] | None = ... - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["result", b"result", "scan_id", b"scan_id"] - ) -> builtins.bool: ... - def ClearField( - self, field_name: typing_extensions.Literal["entries", b"entries", "result", b"result", "scan_id", b"scan_id"] - ) -> None: ... - -global___ResponseGetApEntries = ResponseGetApEntries - -class ResponseStartScanning(google.protobuf.message.Message): - """* - The current scanning state. - - This is the initial response to a @ref RequestStartScan - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - RESULT_FIELD_NUMBER: builtins.int - SCANNING_STATE_FIELD_NUMBER: builtins.int - result: response_generic_pb2.EnumResultGeneric.ValueType - "Generic pass/fail/error info" - scanning_state: global___EnumScanning.ValueType - "Scanning state" - - def __init__( - self, - *, - result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., - scanning_state: global___EnumScanning.ValueType | None = ... - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["result", b"result", "scanning_state", b"scanning_state"] - ) -> builtins.bool: ... - def ClearField( - self, field_name: typing_extensions.Literal["result", b"result", "scanning_state", b"scanning_state"] - ) -> None: ... - -global___ResponseStartScanning = ResponseStartScanning +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for network management +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +from . import response_generic_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumProvisioning: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumProvisioningEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumProvisioning.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PROVISIONING_UNKNOWN: _EnumProvisioning.ValueType + PROVISIONING_NEVER_STARTED: _EnumProvisioning.ValueType + PROVISIONING_STARTED: _EnumProvisioning.ValueType + PROVISIONING_ABORTED_BY_SYSTEM: _EnumProvisioning.ValueType + PROVISIONING_CANCELLED_BY_USER: _EnumProvisioning.ValueType + PROVISIONING_SUCCESS_NEW_AP: _EnumProvisioning.ValueType + PROVISIONING_SUCCESS_OLD_AP: _EnumProvisioning.ValueType + PROVISIONING_ERROR_FAILED_TO_ASSOCIATE: _EnumProvisioning.ValueType + PROVISIONING_ERROR_PASSWORD_AUTH: _EnumProvisioning.ValueType + PROVISIONING_ERROR_EULA_BLOCKING: _EnumProvisioning.ValueType + PROVISIONING_ERROR_NO_INTERNET: _EnumProvisioning.ValueType + PROVISIONING_ERROR_UNSUPPORTED_TYPE: _EnumProvisioning.ValueType + +class EnumProvisioning(_EnumProvisioning, metaclass=_EnumProvisioningEnumTypeWrapper): ... + +PROVISIONING_UNKNOWN: EnumProvisioning.ValueType +PROVISIONING_NEVER_STARTED: EnumProvisioning.ValueType +PROVISIONING_STARTED: EnumProvisioning.ValueType +PROVISIONING_ABORTED_BY_SYSTEM: EnumProvisioning.ValueType +PROVISIONING_CANCELLED_BY_USER: EnumProvisioning.ValueType +PROVISIONING_SUCCESS_NEW_AP: EnumProvisioning.ValueType +PROVISIONING_SUCCESS_OLD_AP: EnumProvisioning.ValueType +PROVISIONING_ERROR_FAILED_TO_ASSOCIATE: EnumProvisioning.ValueType +PROVISIONING_ERROR_PASSWORD_AUTH: EnumProvisioning.ValueType +PROVISIONING_ERROR_EULA_BLOCKING: EnumProvisioning.ValueType +PROVISIONING_ERROR_NO_INTERNET: EnumProvisioning.ValueType +PROVISIONING_ERROR_UNSUPPORTED_TYPE: EnumProvisioning.ValueType +global___EnumProvisioning = EnumProvisioning + +class _EnumScanning: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumScanningEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumScanning.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + SCANNING_UNKNOWN: _EnumScanning.ValueType + SCANNING_NEVER_STARTED: _EnumScanning.ValueType + SCANNING_STARTED: _EnumScanning.ValueType + SCANNING_ABORTED_BY_SYSTEM: _EnumScanning.ValueType + SCANNING_CANCELLED_BY_USER: _EnumScanning.ValueType + SCANNING_SUCCESS: _EnumScanning.ValueType + +class EnumScanning(_EnumScanning, metaclass=_EnumScanningEnumTypeWrapper): ... + +SCANNING_UNKNOWN: EnumScanning.ValueType +SCANNING_NEVER_STARTED: EnumScanning.ValueType +SCANNING_STARTED: EnumScanning.ValueType +SCANNING_ABORTED_BY_SYSTEM: EnumScanning.ValueType +SCANNING_CANCELLED_BY_USER: EnumScanning.ValueType +SCANNING_SUCCESS: EnumScanning.ValueType +global___EnumScanning = EnumScanning + +class _EnumScanEntryFlags: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumScanEntryFlagsEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumScanEntryFlags.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + SCAN_FLAG_OPEN: _EnumScanEntryFlags.ValueType + "This network does not require authentication" + SCAN_FLAG_AUTHENTICATED: _EnumScanEntryFlags.ValueType + "This network requires authentication" + SCAN_FLAG_CONFIGURED: _EnumScanEntryFlags.ValueType + "This network has been previously provisioned" + SCAN_FLAG_BEST_SSID: _EnumScanEntryFlags.ValueType + SCAN_FLAG_ASSOCIATED: _EnumScanEntryFlags.ValueType + "Camera is connected to this AP" + SCAN_FLAG_UNSUPPORTED_TYPE: _EnumScanEntryFlags.ValueType + +class EnumScanEntryFlags(_EnumScanEntryFlags, metaclass=_EnumScanEntryFlagsEnumTypeWrapper): ... + +SCAN_FLAG_OPEN: EnumScanEntryFlags.ValueType +"This network does not require authentication" +SCAN_FLAG_AUTHENTICATED: EnumScanEntryFlags.ValueType +"This network requires authentication" +SCAN_FLAG_CONFIGURED: EnumScanEntryFlags.ValueType +"This network has been previously provisioned" +SCAN_FLAG_BEST_SSID: EnumScanEntryFlags.ValueType +SCAN_FLAG_ASSOCIATED: EnumScanEntryFlags.ValueType +"Camera is connected to this AP" +SCAN_FLAG_UNSUPPORTED_TYPE: EnumScanEntryFlags.ValueType +global___EnumScanEntryFlags = EnumScanEntryFlags + +@typing_extensions.final +class NotifProvisioningState(google.protobuf.message.Message): + """ + Provision state notification + + Sent during provisioning triggered via @ref RequestConnect or @ref RequestConnectNew + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + PROVISIONING_STATE_FIELD_NUMBER: builtins.int + provisioning_state: global___EnumProvisioning.ValueType + "Provisioning / connection state" + + def __init__(self, *, provisioning_state: global___EnumProvisioning.ValueType | None = ...) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["provisioning_state", b"provisioning_state"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["provisioning_state", b"provisioning_state"], + ) -> None: ... + +global___NotifProvisioningState = NotifProvisioningState + +@typing_extensions.final +class NotifStartScanning(google.protobuf.message.Message): + """ + Scanning state notification + + Triggered via @ref RequestStartScan + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SCANNING_STATE_FIELD_NUMBER: builtins.int + SCAN_ID_FIELD_NUMBER: builtins.int + TOTAL_ENTRIES_FIELD_NUMBER: builtins.int + TOTAL_CONFIGURED_SSID_FIELD_NUMBER: builtins.int + scanning_state: global___EnumScanning.ValueType + "Scanning state" + scan_id: builtins.int + "ID associated with scan results (included if scan was successful)" + total_entries: builtins.int + "Number of APs found during scan (included if scan was successful)" + total_configured_ssid: builtins.int + "Total count of camera's provisioned SSIDs" + + def __init__( + self, + *, + scanning_state: global___EnumScanning.ValueType | None = ..., + scan_id: builtins.int | None = ..., + total_entries: builtins.int | None = ..., + total_configured_ssid: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "scan_id", + b"scan_id", + "scanning_state", + b"scanning_state", + "total_configured_ssid", + b"total_configured_ssid", + "total_entries", + b"total_entries", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "scan_id", + b"scan_id", + "scanning_state", + b"scanning_state", + "total_configured_ssid", + b"total_configured_ssid", + "total_entries", + b"total_entries", + ], + ) -> None: ... + +global___NotifStartScanning = NotifStartScanning + +@typing_extensions.final +class RequestConnect(google.protobuf.message.Message): + """* + Connect to (but do not authenticate with) an Access Point + + This is intended to be used to connect to a previously-connected Access Point + + Response: @ref ResponseConnect + + Notification: @ref NotifProvisioningState sent periodically as provisioning state changes + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SSID_FIELD_NUMBER: builtins.int + ssid: builtins.str + "AP SSID" + + def __init__(self, *, ssid: builtins.str | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["ssid", b"ssid"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["ssid", b"ssid"]) -> None: ... + +global___RequestConnect = RequestConnect + +@typing_extensions.final +class RequestConnectNew(google.protobuf.message.Message): + """* + Connect to and authenticate with an Access Point + + This is only intended to be used if the AP is not previously provisioned. + + Response: @ref ResponseConnectNew sent immediately + + Notification: @ref NotifProvisioningState sent periodically as provisioning state changes + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SSID_FIELD_NUMBER: builtins.int + PASSWORD_FIELD_NUMBER: builtins.int + STATIC_IP_FIELD_NUMBER: builtins.int + GATEWAY_FIELD_NUMBER: builtins.int + SUBNET_FIELD_NUMBER: builtins.int + DNS_PRIMARY_FIELD_NUMBER: builtins.int + DNS_SECONDARY_FIELD_NUMBER: builtins.int + ssid: builtins.str + "AP SSID" + password: builtins.str + "AP password" + static_ip: builtins.bytes + "Static IP address" + gateway: builtins.bytes + "Gateway IP address" + subnet: builtins.bytes + "Subnet mask" + dns_primary: builtins.bytes + "Primary DNS" + dns_secondary: builtins.bytes + "Secondary DNS" + + def __init__( + self, + *, + ssid: builtins.str | None = ..., + password: builtins.str | None = ..., + static_ip: builtins.bytes | None = ..., + gateway: builtins.bytes | None = ..., + subnet: builtins.bytes | None = ..., + dns_primary: builtins.bytes | None = ..., + dns_secondary: builtins.bytes | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "dns_primary", + b"dns_primary", + "dns_secondary", + b"dns_secondary", + "gateway", + b"gateway", + "password", + b"password", + "ssid", + b"ssid", + "static_ip", + b"static_ip", + "subnet", + b"subnet", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "dns_primary", + b"dns_primary", + "dns_secondary", + b"dns_secondary", + "gateway", + b"gateway", + "password", + b"password", + "ssid", + b"ssid", + "static_ip", + b"static_ip", + "subnet", + b"subnet", + ], + ) -> None: ... + +global___RequestConnectNew = RequestConnectNew + +@typing_extensions.final +class RequestGetApEntries(google.protobuf.message.Message): + """* + Get a list of Access Points found during a @ref RequestStartScan + + Response: @ref ResponseGetApEntries + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + START_INDEX_FIELD_NUMBER: builtins.int + MAX_ENTRIES_FIELD_NUMBER: builtins.int + SCAN_ID_FIELD_NUMBER: builtins.int + start_index: builtins.int + "Used for paging. 0 <= start_index < @ref ResponseGetApEntries .total_entries" + max_entries: builtins.int + "Used for paging. Value must be < @ref ResponseGetApEntries .total_entries" + scan_id: builtins.int + "ID corresponding to a set of scan results (i.e. @ref ResponseGetApEntries .scan_id)" + + def __init__( + self, + *, + start_index: builtins.int | None = ..., + max_entries: builtins.int | None = ..., + scan_id: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "max_entries", + b"max_entries", + "scan_id", + b"scan_id", + "start_index", + b"start_index", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "max_entries", + b"max_entries", + "scan_id", + b"scan_id", + "start_index", + b"start_index", + ], + ) -> None: ... + +global___RequestGetApEntries = RequestGetApEntries + +@typing_extensions.final +class RequestReleaseNetwork(google.protobuf.message.Message): + """* + Request to disconnect from currently-connected AP + + This drops the camera out of Station (STA) Mode and returns it to Access Point (AP) mode. + + Response: @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestReleaseNetwork = RequestReleaseNetwork + +@typing_extensions.final +class RequestStartScan(google.protobuf.message.Message): + """* + Start scanning for Access Points + + @note Serialization of this object is zero bytes. + + Response: @ref ResponseStartScanning are sent immediately after the camera receives this command + + Notifications: @ref NotifStartScanning are sent periodically as scanning state changes. Use to detect scan complete. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestStartScan = RequestStartScan + +@typing_extensions.final +class ResponseConnect(google.protobuf.message.Message): + """* + The status of an attempt to connect to an Access Point + + Sent as the initial response to @ref RequestConnect + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + PROVISIONING_STATE_FIELD_NUMBER: builtins.int + TIMEOUT_SECONDS_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Generic pass/fail/error info" + provisioning_state: global___EnumProvisioning.ValueType + "Provisioning/connection state" + timeout_seconds: builtins.int + "Network connection timeout (seconds)" + + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + provisioning_state: global___EnumProvisioning.ValueType | None = ..., + timeout_seconds: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "provisioning_state", + b"provisioning_state", + "result", + b"result", + "timeout_seconds", + b"timeout_seconds", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "provisioning_state", + b"provisioning_state", + "result", + b"result", + "timeout_seconds", + b"timeout_seconds", + ], + ) -> None: ... + +global___ResponseConnect = ResponseConnect + +@typing_extensions.final +class ResponseConnectNew(google.protobuf.message.Message): + """* + The status of an attempt to connect to an Access Point + + Sent as the initial response to @ref RequestConnectNew + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + PROVISIONING_STATE_FIELD_NUMBER: builtins.int + TIMEOUT_SECONDS_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Status of Connect New request" + provisioning_state: global___EnumProvisioning.ValueType + "Current provisioning state of the network" + timeout_seconds: builtins.int + "*\n Number of seconds camera will wait before declaring a network connection attempt failed\n " + + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + provisioning_state: global___EnumProvisioning.ValueType | None = ..., + timeout_seconds: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "provisioning_state", + b"provisioning_state", + "result", + b"result", + "timeout_seconds", + b"timeout_seconds", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "provisioning_state", + b"provisioning_state", + "result", + b"result", + "timeout_seconds", + b"timeout_seconds", + ], + ) -> None: ... + +global___ResponseConnectNew = ResponseConnectNew + +@typing_extensions.final +class ResponseGetApEntries(google.protobuf.message.Message): + """* + A list of scan entries describing a scanned Access Point + + This is sent in response to a @ref RequestGetApEntries + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class ScanEntry(google.protobuf.message.Message): + """* + An individual Scan Entry in a @ref ResponseGetApEntries response + + @note When `scan_entry_flags` contains `SCAN_FLAG_CONFIGURED`, it is an indication that this network has already been provisioned. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SSID_FIELD_NUMBER: builtins.int + SIGNAL_STRENGTH_BARS_FIELD_NUMBER: builtins.int + SIGNAL_FREQUENCY_MHZ_FIELD_NUMBER: builtins.int + SCAN_ENTRY_FLAGS_FIELD_NUMBER: builtins.int + ssid: builtins.str + "AP SSID" + signal_strength_bars: builtins.int + "Signal strength (3 bars: >-70 dBm; 2 bars: >-85 dBm; 1 bar: <=-85 dBm)" + signal_frequency_mhz: builtins.int + "Signal frequency (MHz)" + scan_entry_flags: builtins.int + "Bitmasked value from @ref EnumScanEntryFlags" + + def __init__( + self, + *, + ssid: builtins.str | None = ..., + signal_strength_bars: builtins.int | None = ..., + signal_frequency_mhz: builtins.int | None = ..., + scan_entry_flags: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "scan_entry_flags", + b"scan_entry_flags", + "signal_frequency_mhz", + b"signal_frequency_mhz", + "signal_strength_bars", + b"signal_strength_bars", + "ssid", + b"ssid", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "scan_entry_flags", + b"scan_entry_flags", + "signal_frequency_mhz", + b"signal_frequency_mhz", + "signal_strength_bars", + b"signal_strength_bars", + "ssid", + b"ssid", + ], + ) -> None: ... + + RESULT_FIELD_NUMBER: builtins.int + SCAN_ID_FIELD_NUMBER: builtins.int + ENTRIES_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Generic pass/fail/error info" + scan_id: builtins.int + "ID associated with this batch of results" + + @property + def entries( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResponseGetApEntries.ScanEntry]: + """Array containing details about discovered APs""" + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + scan_id: builtins.int | None = ..., + entries: (collections.abc.Iterable[global___ResponseGetApEntries.ScanEntry] | None) = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["result", b"result", "scan_id", b"scan_id"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["entries", b"entries", "result", b"result", "scan_id", b"scan_id"], + ) -> None: ... + +global___ResponseGetApEntries = ResponseGetApEntries + +@typing_extensions.final +class ResponseStartScanning(google.protobuf.message.Message): + """* + The current scanning state. + + This is the initial response to a @ref RequestStartScan + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + SCANNING_STATE_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Generic pass/fail/error info" + scanning_state: global___EnumScanning.ValueType + "Scanning state" + + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + scanning_state: global___EnumScanning.ValueType | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["result", b"result", "scanning_state", b"scanning_state"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["result", b"result", "scanning_state", b"scanning_state"], + ) -> None: ... + +global___ResponseStartScanning = ResponseStartScanning 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 555abb0f..ae660544 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,39 +1,40 @@ # 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 Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:47 UTC 2024 -"""Generated protocol buffer code.""" -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 - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\x13preset_status.proto\x12\nopen_gopro\x1a\x16response_generic.proto"I\n\x12NotifyPresetStatus\x123\n\x12preset_group_array\x18\x01 \x03(\x0b2\x17.open_gopro.PresetGroup"\xaf\x02\n\x06Preset\x12\n\n\x02id\x18\x01 \x01(\x05\x12&\n\x04mode\x18\x02 \x01(\x0e2\x18.open_gopro.EnumFlatMode\x12-\n\x08title_id\x18\x03 \x01(\x0e2\x1b.open_gopro.EnumPresetTitle\x12\x14\n\x0ctitle_number\x18\x04 \x01(\x05\x12\x14\n\x0cuser_defined\x18\x05 \x01(\x08\x12(\n\x04icon\x18\x06 \x01(\x0e2\x1a.open_gopro.EnumPresetIcon\x120\n\rsetting_array\x18\x07 \x03(\x0b2\x19.open_gopro.PresetSetting\x12\x13\n\x0bis_modified\x18\x08 \x01(\x08\x12\x10\n\x08is_fixed\x18\t \x01(\x08\x12\x13\n\x0bcustom_name\x18\n \x01(\t"\x8c\x01\n\x19RequestCustomPresetUpdate\x12-\n\x08title_id\x18\x01 \x01(\x0e2\x1b.open_gopro.EnumPresetTitle\x12\x13\n\x0bcustom_name\x18\x02 \x01(\t\x12+\n\x07icon_id\x18\x03 \x01(\x0e2\x1a.open_gopro.EnumPresetIcon"\xa7\x01\n\x0bPresetGroup\x12\'\n\x02id\x18\x01 \x01(\x0e2\x1b.open_gopro.EnumPresetGroup\x12(\n\x0cpreset_array\x18\x02 \x03(\x0b2\x12.open_gopro.Preset\x12\x16\n\x0ecan_add_preset\x18\x03 \x01(\x08\x12-\n\x04icon\x18\x04 \x01(\x0e2\x1f.open_gopro.EnumPresetGroupIcon">\n\rPresetSetting\x12\n\n\x02id\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05\x12\x12\n\nis_caption\x18\x03 \x01(\x08*\xfa\x04\n\x0cEnumFlatMode\x12\x1e\n\x11FLAT_MODE_UNKNOWN\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01\x12\x16\n\x12FLAT_MODE_PLAYBACK\x10\x04\x12\x13\n\x0fFLAT_MODE_SETUP\x10\x05\x12\x13\n\x0fFLAT_MODE_VIDEO\x10\x0c\x12\x1e\n\x1aFLAT_MODE_TIME_LAPSE_VIDEO\x10\r\x12\x15\n\x11FLAT_MODE_LOOPING\x10\x0f\x12\x1a\n\x16FLAT_MODE_PHOTO_SINGLE\x10\x10\x12\x13\n\x0fFLAT_MODE_PHOTO\x10\x11\x12\x19\n\x15FLAT_MODE_PHOTO_NIGHT\x10\x12\x12\x19\n\x15FLAT_MODE_PHOTO_BURST\x10\x13\x12\x1e\n\x1aFLAT_MODE_TIME_LAPSE_PHOTO\x10\x14\x12\x1f\n\x1bFLAT_MODE_NIGHT_LAPSE_PHOTO\x10\x15\x12\x1e\n\x1aFLAT_MODE_BROADCAST_RECORD\x10\x16\x12!\n\x1dFLAT_MODE_BROADCAST_BROADCAST\x10\x17\x12\x1d\n\x19FLAT_MODE_TIME_WARP_VIDEO\x10\x18\x12\x18\n\x14FLAT_MODE_LIVE_BURST\x10\x19\x12\x1f\n\x1bFLAT_MODE_NIGHT_LAPSE_VIDEO\x10\x1a\x12\x13\n\x0fFLAT_MODE_SLOMO\x10\x1b\x12\x12\n\x0eFLAT_MODE_IDLE\x10\x1c\x12\x1e\n\x1aFLAT_MODE_VIDEO_STAR_TRAIL\x10\x1d\x12"\n\x1eFLAT_MODE_VIDEO_LIGHT_PAINTING\x10\x1e\x12\x1f\n\x1bFLAT_MODE_VIDEO_LIGHT_TRAIL\x10\x1f*i\n\x0fEnumPresetGroup\x12\x1a\n\x15PRESET_GROUP_ID_VIDEO\x10\xe8\x07\x12\x1a\n\x15PRESET_GROUP_ID_PHOTO\x10\xe9\x07\x12\x1e\n\x19PRESET_GROUP_ID_TIMELAPSE\x10\xea\x07*\xbc\x02\n\x13EnumPresetGroupIcon\x12\x1e\n\x1aPRESET_GROUP_VIDEO_ICON_ID\x10\x00\x12\x1e\n\x1aPRESET_GROUP_PHOTO_ICON_ID\x10\x01\x12"\n\x1ePRESET_GROUP_TIMELAPSE_ICON_ID\x10\x02\x12\'\n#PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID\x10\x03\x12(\n$PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID\x10\x04\x12"\n\x1ePRESET_GROUP_MAX_VIDEO_ICON_ID\x10\x05\x12"\n\x1ePRESET_GROUP_MAX_PHOTO_ICON_ID\x10\x06\x12&\n"PRESET_GROUP_MAX_TIMELAPSE_ICON_ID\x10\x07*\xec\x10\n\x0eEnumPresetIcon\x12\x15\n\x11PRESET_ICON_VIDEO\x10\x00\x12\x18\n\x14PRESET_ICON_ACTIVITY\x10\x01\x12\x19\n\x15PRESET_ICON_CINEMATIC\x10\x02\x12\x15\n\x11PRESET_ICON_PHOTO\x10\x03\x12\x1a\n\x16PRESET_ICON_LIVE_BURST\x10\x04\x12\x15\n\x11PRESET_ICON_BURST\x10\x05\x12\x1b\n\x17PRESET_ICON_PHOTO_NIGHT\x10\x06\x12\x18\n\x14PRESET_ICON_TIMEWARP\x10\x07\x12\x19\n\x15PRESET_ICON_TIMELAPSE\x10\x08\x12\x1a\n\x16PRESET_ICON_NIGHTLAPSE\x10\t\x12\x15\n\x11PRESET_ICON_SNAIL\x10\n\x12\x17\n\x13PRESET_ICON_VIDEO_2\x10\x0b\x12\x19\n\x15PRESET_ICON_360_VIDEO\x10\x0c\x12\x17\n\x13PRESET_ICON_PHOTO_2\x10\r\x12\x18\n\x14PRESET_ICON_PANORAMA\x10\x0e\x12\x17\n\x13PRESET_ICON_BURST_2\x10\x0f\x12\x1a\n\x16PRESET_ICON_TIMEWARP_2\x10\x10\x12\x1b\n\x17PRESET_ICON_TIMELAPSE_2\x10\x11\x12\x16\n\x12PRESET_ICON_CUSTOM\x10\x12\x12\x13\n\x0fPRESET_ICON_AIR\x10\x13\x12\x14\n\x10PRESET_ICON_BIKE\x10\x14\x12\x14\n\x10PRESET_ICON_EPIC\x10\x15\x12\x16\n\x12PRESET_ICON_INDOOR\x10\x16\x12\x15\n\x11PRESET_ICON_MOTOR\x10\x17\x12\x17\n\x13PRESET_ICON_MOUNTED\x10\x18\x12\x17\n\x13PRESET_ICON_OUTDOOR\x10\x19\x12\x13\n\x0fPRESET_ICON_POV\x10\x1a\x12\x16\n\x12PRESET_ICON_SELFIE\x10\x1b\x12\x15\n\x11PRESET_ICON_SKATE\x10\x1c\x12\x14\n\x10PRESET_ICON_SNOW\x10\x1d\x12\x15\n\x11PRESET_ICON_TRAIL\x10\x1e\x12\x16\n\x12PRESET_ICON_TRAVEL\x10\x1f\x12\x15\n\x11PRESET_ICON_WATER\x10 \x12\x17\n\x13PRESET_ICON_LOOPING\x10!\x12\x15\n\x11PRESET_ICON_STARS\x10"\x12\x16\n\x12PRESET_ICON_ACTION\x10#\x12\x1a\n\x16PRESET_ICON_FOLLOW_CAM\x10$\x12\x14\n\x10PRESET_ICON_SURF\x10%\x12\x14\n\x10PRESET_ICON_CITY\x10&\x12\x15\n\x11PRESET_ICON_SHAKY\x10\'\x12\x16\n\x12PRESET_ICON_CHESTY\x10(\x12\x16\n\x12PRESET_ICON_HELMET\x10)\x12\x14\n\x10PRESET_ICON_BITE\x10*\x12\x19\n\x15PRESET_ICON_MAX_VIDEO\x107\x12\x19\n\x15PRESET_ICON_MAX_PHOTO\x108\x12\x1c\n\x18PRESET_ICON_MAX_TIMEWARP\x109\x12\x15\n\x11PRESET_ICON_BASIC\x10:\x12\x1c\n\x18PRESET_ICON_ULTRA_SLO_MO\x10;\x12"\n\x1ePRESET_ICON_STANDARD_ENDURANCE\x10<\x12"\n\x1ePRESET_ICON_ACTIVITY_ENDURANCE\x10=\x12#\n\x1fPRESET_ICON_CINEMATIC_ENDURANCE\x10>\x12\x1f\n\x1bPRESET_ICON_SLOMO_ENDURANCE\x10?\x12\x1c\n\x18PRESET_ICON_STATIONARY_1\x10@\x12\x1c\n\x18PRESET_ICON_STATIONARY_2\x10A\x12\x1c\n\x18PRESET_ICON_STATIONARY_3\x10B\x12\x1c\n\x18PRESET_ICON_STATIONARY_4\x10C\x12"\n\x1ePRESET_ICON_SIMPLE_SUPER_PHOTO\x10F\x12"\n\x1ePRESET_ICON_SIMPLE_NIGHT_PHOTO\x10G\x12%\n!PRESET_ICON_HIGHEST_QUALITY_VIDEO\x10I\x12&\n"PRESET_ICON_STANDARD_QUALITY_VIDEO\x10J\x12#\n\x1fPRESET_ICON_BASIC_QUALITY_VIDEO\x10K\x12\x1a\n\x16PRESET_ICON_STAR_TRAIL\x10L\x12\x1e\n\x1aPRESET_ICON_LIGHT_PAINTING\x10M\x12\x1b\n\x17PRESET_ICON_LIGHT_TRAIL\x10N\x12\x1a\n\x16PRESET_ICON_FULL_FRAME\x10O\x12\x1e\n\x1aPRESET_ICON_EASY_MAX_VIDEO\x10P\x12\x1e\n\x1aPRESET_ICON_EASY_MAX_PHOTO\x10Q\x12!\n\x1dPRESET_ICON_EASY_MAX_TIMEWARP\x10R\x12#\n\x1fPRESET_ICON_EASY_MAX_STAR_TRAIL\x10S\x12\'\n#PRESET_ICON_EASY_MAX_LIGHT_PAINTING\x10T\x12$\n PRESET_ICON_EASY_MAX_LIGHT_TRAIL\x10U\x12\x1e\n\x1aPRESET_ICON_MAX_STAR_TRAIL\x10Y\x12"\n\x1ePRESET_ICON_MAX_LIGHT_PAINTING\x10Z\x12\x1f\n\x1bPRESET_ICON_MAX_LIGHT_TRAIL\x10[\x12 \n\x1bPRESET_ICON_TIMELAPSE_PHOTO\x10\xe8\x07\x12!\n\x1cPRESET_ICON_NIGHTLAPSE_PHOTO\x10\xe9\x07*\xbd\x14\n\x0fEnumPresetTitle\x12\x19\n\x15PRESET_TITLE_ACTIVITY\x10\x00\x12\x19\n\x15PRESET_TITLE_STANDARD\x10\x01\x12\x1a\n\x16PRESET_TITLE_CINEMATIC\x10\x02\x12\x16\n\x12PRESET_TITLE_PHOTO\x10\x03\x12\x1b\n\x17PRESET_TITLE_LIVE_BURST\x10\x04\x12\x16\n\x12PRESET_TITLE_BURST\x10\x05\x12\x16\n\x12PRESET_TITLE_NIGHT\x10\x06\x12\x1a\n\x16PRESET_TITLE_TIME_WARP\x10\x07\x12\x1b\n\x17PRESET_TITLE_TIME_LAPSE\x10\x08\x12\x1c\n\x18PRESET_TITLE_NIGHT_LAPSE\x10\t\x12\x16\n\x12PRESET_TITLE_VIDEO\x10\n\x12\x16\n\x12PRESET_TITLE_SLOMO\x10\x0b\x12\x1a\n\x16PRESET_TITLE_360_VIDEO\x10\x0c\x12\x18\n\x14PRESET_TITLE_PHOTO_2\x10\r\x12\x19\n\x15PRESET_TITLE_PANORAMA\x10\x0e\x12\x1a\n\x16PRESET_TITLE_360_PHOTO\x10\x0f\x12\x1c\n\x18PRESET_TITLE_TIME_WARP_2\x10\x10\x12\x1e\n\x1aPRESET_TITLE_360_TIME_WARP\x10\x11\x12\x17\n\x13PRESET_TITLE_CUSTOM\x10\x12\x12\x14\n\x10PRESET_TITLE_AIR\x10\x13\x12\x15\n\x11PRESET_TITLE_BIKE\x10\x14\x12\x15\n\x11PRESET_TITLE_EPIC\x10\x15\x12\x17\n\x13PRESET_TITLE_INDOOR\x10\x16\x12\x16\n\x12PRESET_TITLE_MOTOR\x10\x17\x12\x18\n\x14PRESET_TITLE_MOUNTED\x10\x18\x12\x18\n\x14PRESET_TITLE_OUTDOOR\x10\x19\x12\x14\n\x10PRESET_TITLE_POV\x10\x1a\x12\x17\n\x13PRESET_TITLE_SELFIE\x10\x1b\x12\x16\n\x12PRESET_TITLE_SKATE\x10\x1c\x12\x15\n\x11PRESET_TITLE_SNOW\x10\x1d\x12\x16\n\x12PRESET_TITLE_TRAIL\x10\x1e\x12\x17\n\x13PRESET_TITLE_TRAVEL\x10\x1f\x12\x16\n\x12PRESET_TITLE_WATER\x10 \x12\x18\n\x14PRESET_TITLE_LOOPING\x10!\x12\x16\n\x12PRESET_TITLE_STARS\x10"\x12\x17\n\x13PRESET_TITLE_ACTION\x10#\x12\x1b\n\x17PRESET_TITLE_FOLLOW_CAM\x10$\x12\x15\n\x11PRESET_TITLE_SURF\x10%\x12\x15\n\x11PRESET_TITLE_CITY\x10&\x12\x16\n\x12PRESET_TITLE_SHAKY\x10\'\x12\x17\n\x13PRESET_TITLE_CHESTY\x10(\x12\x17\n\x13PRESET_TITLE_HELMET\x10)\x12\x15\n\x11PRESET_TITLE_BITE\x10*\x12\x1e\n\x1aPRESET_TITLE_360_TIMELAPSE\x103\x12 \n\x1cPRESET_TITLE_360_NIGHT_LAPSE\x104\x12 \n\x1cPRESET_TITLE_360_NIGHT_PHOTO\x105\x12 \n\x1cPRESET_TITLE_PANO_TIME_LAPSE\x106\x12\x1a\n\x16PRESET_TITLE_MAX_VIDEO\x107\x12\x1a\n\x16PRESET_TITLE_MAX_PHOTO\x108\x12\x1d\n\x19PRESET_TITLE_MAX_TIMEWARP\x109\x12\x16\n\x12PRESET_TITLE_BASIC\x10:\x12\x1d\n\x19PRESET_TITLE_ULTRA_SLO_MO\x10;\x12#\n\x1fPRESET_TITLE_STANDARD_ENDURANCE\x10<\x12#\n\x1fPRESET_TITLE_ACTIVITY_ENDURANCE\x10=\x12$\n PRESET_TITLE_CINEMATIC_ENDURANCE\x10>\x12 \n\x1cPRESET_TITLE_SLOMO_ENDURANCE\x10?\x12\x1d\n\x19PRESET_TITLE_STATIONARY_1\x10@\x12\x1d\n\x19PRESET_TITLE_STATIONARY_2\x10A\x12\x1d\n\x19PRESET_TITLE_STATIONARY_3\x10B\x12\x1d\n\x19PRESET_TITLE_STATIONARY_4\x10C\x12\x1d\n\x19PRESET_TITLE_SIMPLE_VIDEO\x10D\x12!\n\x1dPRESET_TITLE_SIMPLE_TIME_WARP\x10E\x12#\n\x1fPRESET_TITLE_SIMPLE_SUPER_PHOTO\x10F\x12#\n\x1fPRESET_TITLE_SIMPLE_NIGHT_PHOTO\x10G\x12\'\n#PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE\x10H\x12 \n\x1cPRESET_TITLE_HIGHEST_QUALITY\x10I\x12!\n\x1dPRESET_TITLE_EXTENDED_BATTERY\x10J\x12 \n\x1cPRESET_TITLE_LONGEST_BATTERY\x10K\x12\x1b\n\x17PRESET_TITLE_STAR_TRAIL\x10L\x12\x1f\n\x1bPRESET_TITLE_LIGHT_PAINTING\x10M\x12\x1c\n\x18PRESET_TITLE_LIGHT_TRAIL\x10N\x12\x1b\n\x17PRESET_TITLE_FULL_FRAME\x10O\x12\x1f\n\x1bPRESET_TITLE_MAX_LENS_VIDEO\x10P\x12"\n\x1ePRESET_TITLE_MAX_LENS_TIMEWARP\x10Q\x12\'\n#PRESET_TITLE_STANDARD_QUALITY_VIDEO\x10R\x12$\n PRESET_TITLE_BASIC_QUALITY_VIDEO\x10S\x12\x1f\n\x1bPRESET_TITLE_EASY_MAX_VIDEO\x10T\x12\x1f\n\x1bPRESET_TITLE_EASY_MAX_PHOTO\x10U\x12"\n\x1ePRESET_TITLE_EASY_MAX_TIMEWARP\x10V\x12$\n PRESET_TITLE_EASY_MAX_STAR_TRAIL\x10W\x12(\n$PRESET_TITLE_EASY_MAX_LIGHT_PAINTING\x10X\x12%\n!PRESET_TITLE_EASY_MAX_LIGHT_TRAIL\x10Y\x12\x1f\n\x1bPRESET_TITLE_MAX_STAR_TRAIL\x10Z\x12#\n\x1fPRESET_TITLE_MAX_LIGHT_PAINTING\x10[\x12 \n\x1cPRESET_TITLE_MAX_LIGHT_TRAIL\x10\\\x12&\n"PRESET_TITLE_HIGHEST_QUALITY_VIDEO\x10]\x12)\n%PRESET_TITLE_USER_DEFINED_CUSTOM_NAME\x10^' -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "preset_status_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _ENUMFLATMODE._serialized_start = 818 - _ENUMFLATMODE._serialized_end = 1452 - _ENUMPRESETGROUP._serialized_start = 1454 - _ENUMPRESETGROUP._serialized_end = 1559 - _ENUMPRESETGROUPICON._serialized_start = 1562 - _ENUMPRESETGROUPICON._serialized_end = 1878 - _ENUMPRESETICON._serialized_start = 1881 - _ENUMPRESETICON._serialized_end = 4037 - _ENUMPRESETTITLE._serialized_start = 4040 - _ENUMPRESETTITLE._serialized_end = 6661 - _NOTIFYPRESETSTATUS._serialized_start = 59 - _NOTIFYPRESETSTATUS._serialized_end = 132 - _PRESET._serialized_start = 135 - _PRESET._serialized_end = 438 - _REQUESTCUSTOMPRESETUPDATE._serialized_start = 441 - _REQUESTCUSTOMPRESETUPDATE._serialized_end = 581 - _PRESETGROUP._serialized_start = 584 - _PRESETGROUP._serialized_end = 751 - _PRESETSETTING._serialized_start = 753 - _PRESETSETTING._serialized_end = 815 +"""Generated protocol buffer code.""" + +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 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x13preset_status.proto\x12\nopen_gopro\x1a\x16response_generic.proto"I\n\x12NotifyPresetStatus\x123\n\x12preset_group_array\x18\x01 \x03(\x0b2\x17.open_gopro.PresetGroup"\xaf\x02\n\x06Preset\x12\n\n\x02id\x18\x01 \x01(\x05\x12&\n\x04mode\x18\x02 \x01(\x0e2\x18.open_gopro.EnumFlatMode\x12-\n\x08title_id\x18\x03 \x01(\x0e2\x1b.open_gopro.EnumPresetTitle\x12\x14\n\x0ctitle_number\x18\x04 \x01(\x05\x12\x14\n\x0cuser_defined\x18\x05 \x01(\x08\x12(\n\x04icon\x18\x06 \x01(\x0e2\x1a.open_gopro.EnumPresetIcon\x120\n\rsetting_array\x18\x07 \x03(\x0b2\x19.open_gopro.PresetSetting\x12\x13\n\x0bis_modified\x18\x08 \x01(\x08\x12\x10\n\x08is_fixed\x18\t \x01(\x08\x12\x13\n\x0bcustom_name\x18\n \x01(\t"\x8c\x01\n\x19RequestCustomPresetUpdate\x12-\n\x08title_id\x18\x01 \x01(\x0e2\x1b.open_gopro.EnumPresetTitle\x12\x13\n\x0bcustom_name\x18\x02 \x01(\t\x12+\n\x07icon_id\x18\x03 \x01(\x0e2\x1a.open_gopro.EnumPresetIcon"\xa7\x01\n\x0bPresetGroup\x12\'\n\x02id\x18\x01 \x01(\x0e2\x1b.open_gopro.EnumPresetGroup\x12(\n\x0cpreset_array\x18\x02 \x03(\x0b2\x12.open_gopro.Preset\x12\x16\n\x0ecan_add_preset\x18\x03 \x01(\x08\x12-\n\x04icon\x18\x04 \x01(\x0e2\x1f.open_gopro.EnumPresetGroupIcon">\n\rPresetSetting\x12\n\n\x02id\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05\x12\x12\n\nis_caption\x18\x03 \x01(\x08*\x9b\x05\n\x0cEnumFlatMode\x12\x1e\n\x11FLAT_MODE_UNKNOWN\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01\x12\x16\n\x12FLAT_MODE_PLAYBACK\x10\x04\x12\x13\n\x0fFLAT_MODE_SETUP\x10\x05\x12\x13\n\x0fFLAT_MODE_VIDEO\x10\x0c\x12\x1e\n\x1aFLAT_MODE_TIME_LAPSE_VIDEO\x10\r\x12\x15\n\x11FLAT_MODE_LOOPING\x10\x0f\x12\x1a\n\x16FLAT_MODE_PHOTO_SINGLE\x10\x10\x12\x13\n\x0fFLAT_MODE_PHOTO\x10\x11\x12\x19\n\x15FLAT_MODE_PHOTO_NIGHT\x10\x12\x12\x19\n\x15FLAT_MODE_PHOTO_BURST\x10\x13\x12\x1e\n\x1aFLAT_MODE_TIME_LAPSE_PHOTO\x10\x14\x12\x1f\n\x1bFLAT_MODE_NIGHT_LAPSE_PHOTO\x10\x15\x12\x1e\n\x1aFLAT_MODE_BROADCAST_RECORD\x10\x16\x12!\n\x1dFLAT_MODE_BROADCAST_BROADCAST\x10\x17\x12\x1d\n\x19FLAT_MODE_TIME_WARP_VIDEO\x10\x18\x12\x18\n\x14FLAT_MODE_LIVE_BURST\x10\x19\x12\x1f\n\x1bFLAT_MODE_NIGHT_LAPSE_VIDEO\x10\x1a\x12\x13\n\x0fFLAT_MODE_SLOMO\x10\x1b\x12\x12\n\x0eFLAT_MODE_IDLE\x10\x1c\x12\x1e\n\x1aFLAT_MODE_VIDEO_STAR_TRAIL\x10\x1d\x12"\n\x1eFLAT_MODE_VIDEO_LIGHT_PAINTING\x10\x1e\x12\x1f\n\x1bFLAT_MODE_VIDEO_LIGHT_TRAIL\x10\x1f\x12\x1f\n\x1bFLAT_MODE_VIDEO_BURST_SLOMO\x10 *i\n\x0fEnumPresetGroup\x12\x1a\n\x15PRESET_GROUP_ID_VIDEO\x10\xe8\x07\x12\x1a\n\x15PRESET_GROUP_ID_PHOTO\x10\xe9\x07\x12\x1e\n\x19PRESET_GROUP_ID_TIMELAPSE\x10\xea\x07*\xbc\x02\n\x13EnumPresetGroupIcon\x12\x1e\n\x1aPRESET_GROUP_VIDEO_ICON_ID\x10\x00\x12\x1e\n\x1aPRESET_GROUP_PHOTO_ICON_ID\x10\x01\x12"\n\x1ePRESET_GROUP_TIMELAPSE_ICON_ID\x10\x02\x12\'\n#PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID\x10\x03\x12(\n$PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID\x10\x04\x12"\n\x1ePRESET_GROUP_MAX_VIDEO_ICON_ID\x10\x05\x12"\n\x1ePRESET_GROUP_MAX_PHOTO_ICON_ID\x10\x06\x12&\n"PRESET_GROUP_MAX_TIMELAPSE_ICON_ID\x10\x07*\xc1\r\n\x0eEnumPresetIcon\x12\x15\n\x11PRESET_ICON_VIDEO\x10\x00\x12\x18\n\x14PRESET_ICON_ACTIVITY\x10\x01\x12\x19\n\x15PRESET_ICON_CINEMATIC\x10\x02\x12\x15\n\x11PRESET_ICON_PHOTO\x10\x03\x12\x1a\n\x16PRESET_ICON_LIVE_BURST\x10\x04\x12\x15\n\x11PRESET_ICON_BURST\x10\x05\x12\x1b\n\x17PRESET_ICON_PHOTO_NIGHT\x10\x06\x12\x18\n\x14PRESET_ICON_TIMEWARP\x10\x07\x12\x19\n\x15PRESET_ICON_TIMELAPSE\x10\x08\x12\x1a\n\x16PRESET_ICON_NIGHTLAPSE\x10\t\x12\x15\n\x11PRESET_ICON_SNAIL\x10\n\x12\x17\n\x13PRESET_ICON_VIDEO_2\x10\x0b\x12\x17\n\x13PRESET_ICON_PHOTO_2\x10\r\x12\x18\n\x14PRESET_ICON_PANORAMA\x10\x0e\x12\x17\n\x13PRESET_ICON_BURST_2\x10\x0f\x12\x1a\n\x16PRESET_ICON_TIMEWARP_2\x10\x10\x12\x1b\n\x17PRESET_ICON_TIMELAPSE_2\x10\x11\x12\x16\n\x12PRESET_ICON_CUSTOM\x10\x12\x12\x13\n\x0fPRESET_ICON_AIR\x10\x13\x12\x14\n\x10PRESET_ICON_BIKE\x10\x14\x12\x14\n\x10PRESET_ICON_EPIC\x10\x15\x12\x16\n\x12PRESET_ICON_INDOOR\x10\x16\x12\x15\n\x11PRESET_ICON_MOTOR\x10\x17\x12\x17\n\x13PRESET_ICON_MOUNTED\x10\x18\x12\x17\n\x13PRESET_ICON_OUTDOOR\x10\x19\x12\x13\n\x0fPRESET_ICON_POV\x10\x1a\x12\x16\n\x12PRESET_ICON_SELFIE\x10\x1b\x12\x15\n\x11PRESET_ICON_SKATE\x10\x1c\x12\x14\n\x10PRESET_ICON_SNOW\x10\x1d\x12\x15\n\x11PRESET_ICON_TRAIL\x10\x1e\x12\x16\n\x12PRESET_ICON_TRAVEL\x10\x1f\x12\x15\n\x11PRESET_ICON_WATER\x10 \x12\x17\n\x13PRESET_ICON_LOOPING\x10!\x12\x15\n\x11PRESET_ICON_STARS\x10"\x12\x16\n\x12PRESET_ICON_ACTION\x10#\x12\x1a\n\x16PRESET_ICON_FOLLOW_CAM\x10$\x12\x14\n\x10PRESET_ICON_SURF\x10%\x12\x14\n\x10PRESET_ICON_CITY\x10&\x12\x15\n\x11PRESET_ICON_SHAKY\x10\'\x12\x16\n\x12PRESET_ICON_CHESTY\x10(\x12\x16\n\x12PRESET_ICON_HELMET\x10)\x12\x14\n\x10PRESET_ICON_BITE\x10*\x12\x15\n\x11PRESET_ICON_BASIC\x10:\x12\x1c\n\x18PRESET_ICON_ULTRA_SLO_MO\x10;\x12"\n\x1ePRESET_ICON_STANDARD_ENDURANCE\x10<\x12"\n\x1ePRESET_ICON_ACTIVITY_ENDURANCE\x10=\x12#\n\x1fPRESET_ICON_CINEMATIC_ENDURANCE\x10>\x12\x1f\n\x1bPRESET_ICON_SLOMO_ENDURANCE\x10?\x12\x1c\n\x18PRESET_ICON_STATIONARY_1\x10@\x12\x1c\n\x18PRESET_ICON_STATIONARY_2\x10A\x12\x1c\n\x18PRESET_ICON_STATIONARY_3\x10B\x12\x1c\n\x18PRESET_ICON_STATIONARY_4\x10C\x12"\n\x1ePRESET_ICON_SIMPLE_SUPER_PHOTO\x10F\x12"\n\x1ePRESET_ICON_SIMPLE_NIGHT_PHOTO\x10G\x12%\n!PRESET_ICON_HIGHEST_QUALITY_VIDEO\x10I\x12&\n"PRESET_ICON_STANDARD_QUALITY_VIDEO\x10J\x12#\n\x1fPRESET_ICON_BASIC_QUALITY_VIDEO\x10K\x12\x1a\n\x16PRESET_ICON_STAR_TRAIL\x10L\x12\x1e\n\x1aPRESET_ICON_LIGHT_PAINTING\x10M\x12\x1b\n\x17PRESET_ICON_LIGHT_TRAIL\x10N\x12\x1a\n\x16PRESET_ICON_FULL_FRAME\x10O\x12 \n\x1bPRESET_ICON_TIMELAPSE_PHOTO\x10\xe8\x07\x12!\n\x1cPRESET_ICON_NIGHTLAPSE_PHOTO\x10\xe9\x07*\xfe\x0e\n\x0fEnumPresetTitle\x12\x19\n\x15PRESET_TITLE_ACTIVITY\x10\x00\x12\x19\n\x15PRESET_TITLE_STANDARD\x10\x01\x12\x1a\n\x16PRESET_TITLE_CINEMATIC\x10\x02\x12\x16\n\x12PRESET_TITLE_PHOTO\x10\x03\x12\x1b\n\x17PRESET_TITLE_LIVE_BURST\x10\x04\x12\x16\n\x12PRESET_TITLE_BURST\x10\x05\x12\x16\n\x12PRESET_TITLE_NIGHT\x10\x06\x12\x1a\n\x16PRESET_TITLE_TIME_WARP\x10\x07\x12\x1b\n\x17PRESET_TITLE_TIME_LAPSE\x10\x08\x12\x1c\n\x18PRESET_TITLE_NIGHT_LAPSE\x10\t\x12\x16\n\x12PRESET_TITLE_VIDEO\x10\n\x12\x16\n\x12PRESET_TITLE_SLOMO\x10\x0b\x12\x18\n\x14PRESET_TITLE_PHOTO_2\x10\r\x12\x19\n\x15PRESET_TITLE_PANORAMA\x10\x0e\x12\x1c\n\x18PRESET_TITLE_TIME_WARP_2\x10\x10\x12\x17\n\x13PRESET_TITLE_CUSTOM\x10\x12\x12\x14\n\x10PRESET_TITLE_AIR\x10\x13\x12\x15\n\x11PRESET_TITLE_BIKE\x10\x14\x12\x15\n\x11PRESET_TITLE_EPIC\x10\x15\x12\x17\n\x13PRESET_TITLE_INDOOR\x10\x16\x12\x16\n\x12PRESET_TITLE_MOTOR\x10\x17\x12\x18\n\x14PRESET_TITLE_MOUNTED\x10\x18\x12\x18\n\x14PRESET_TITLE_OUTDOOR\x10\x19\x12\x14\n\x10PRESET_TITLE_POV\x10\x1a\x12\x17\n\x13PRESET_TITLE_SELFIE\x10\x1b\x12\x16\n\x12PRESET_TITLE_SKATE\x10\x1c\x12\x15\n\x11PRESET_TITLE_SNOW\x10\x1d\x12\x16\n\x12PRESET_TITLE_TRAIL\x10\x1e\x12\x17\n\x13PRESET_TITLE_TRAVEL\x10\x1f\x12\x16\n\x12PRESET_TITLE_WATER\x10 \x12\x18\n\x14PRESET_TITLE_LOOPING\x10!\x12\x16\n\x12PRESET_TITLE_STARS\x10"\x12\x17\n\x13PRESET_TITLE_ACTION\x10#\x12\x1b\n\x17PRESET_TITLE_FOLLOW_CAM\x10$\x12\x15\n\x11PRESET_TITLE_SURF\x10%\x12\x15\n\x11PRESET_TITLE_CITY\x10&\x12\x16\n\x12PRESET_TITLE_SHAKY\x10\'\x12\x17\n\x13PRESET_TITLE_CHESTY\x10(\x12\x17\n\x13PRESET_TITLE_HELMET\x10)\x12\x15\n\x11PRESET_TITLE_BITE\x10*\x12\x16\n\x12PRESET_TITLE_BASIC\x10:\x12\x1d\n\x19PRESET_TITLE_ULTRA_SLO_MO\x10;\x12#\n\x1fPRESET_TITLE_STANDARD_ENDURANCE\x10<\x12#\n\x1fPRESET_TITLE_ACTIVITY_ENDURANCE\x10=\x12$\n PRESET_TITLE_CINEMATIC_ENDURANCE\x10>\x12 \n\x1cPRESET_TITLE_SLOMO_ENDURANCE\x10?\x12\x1d\n\x19PRESET_TITLE_STATIONARY_1\x10@\x12\x1d\n\x19PRESET_TITLE_STATIONARY_2\x10A\x12\x1d\n\x19PRESET_TITLE_STATIONARY_3\x10B\x12\x1d\n\x19PRESET_TITLE_STATIONARY_4\x10C\x12\x1d\n\x19PRESET_TITLE_SIMPLE_VIDEO\x10D\x12!\n\x1dPRESET_TITLE_SIMPLE_TIME_WARP\x10E\x12#\n\x1fPRESET_TITLE_SIMPLE_SUPER_PHOTO\x10F\x12#\n\x1fPRESET_TITLE_SIMPLE_NIGHT_PHOTO\x10G\x12\'\n#PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE\x10H\x12 \n\x1cPRESET_TITLE_HIGHEST_QUALITY\x10I\x12!\n\x1dPRESET_TITLE_EXTENDED_BATTERY\x10J\x12 \n\x1cPRESET_TITLE_LONGEST_BATTERY\x10K\x12\x1b\n\x17PRESET_TITLE_STAR_TRAIL\x10L\x12\x1f\n\x1bPRESET_TITLE_LIGHT_PAINTING\x10M\x12\x1c\n\x18PRESET_TITLE_LIGHT_TRAIL\x10N\x12\x1b\n\x17PRESET_TITLE_FULL_FRAME\x10O\x12\'\n#PRESET_TITLE_STANDARD_QUALITY_VIDEO\x10R\x12$\n PRESET_TITLE_BASIC_QUALITY_VIDEO\x10S\x12&\n"PRESET_TITLE_HIGHEST_QUALITY_VIDEO\x10]\x12)\n%PRESET_TITLE_USER_DEFINED_CUSTOM_NAME\x10^' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "preset_status_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMFLATMODE._serialized_start = 818 + _ENUMFLATMODE._serialized_end = 1485 + _ENUMPRESETGROUP._serialized_start = 1487 + _ENUMPRESETGROUP._serialized_end = 1592 + _ENUMPRESETGROUPICON._serialized_start = 1595 + _ENUMPRESETGROUPICON._serialized_end = 1911 + _ENUMPRESETICON._serialized_start = 1914 + _ENUMPRESETICON._serialized_end = 3643 + _ENUMPRESETTITLE._serialized_start = 3646 + _ENUMPRESETTITLE._serialized_end = 5564 + _NOTIFYPRESETSTATUS._serialized_start = 59 + _NOTIFYPRESETSTATUS._serialized_end = 132 + _PRESET._serialized_start = 135 + _PRESET._serialized_end = 438 + _REQUESTCUSTOMPRESETUPDATE._serialized_start = 441 + _REQUESTCUSTOMPRESETUPDATE._serialized_end = 581 + _PRESETGROUP._serialized_start = 584 + _PRESETGROUP._serialized_end = 751 + _PRESETSETTING._serialized_start = 753 + _PRESETSETTING._serialized_end = 815 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 4f5035dd..d5145f88 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 @@ -1,740 +1,701 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Defines the structure of protobuf message received from camera containing preset status -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _EnumFlatMode: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumFlatModeEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumFlatMode.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - FLAT_MODE_UNKNOWN: _EnumFlatMode.ValueType - FLAT_MODE_PLAYBACK: _EnumFlatMode.ValueType - FLAT_MODE_SETUP: _EnumFlatMode.ValueType - FLAT_MODE_VIDEO: _EnumFlatMode.ValueType - FLAT_MODE_TIME_LAPSE_VIDEO: _EnumFlatMode.ValueType - FLAT_MODE_LOOPING: _EnumFlatMode.ValueType - FLAT_MODE_PHOTO_SINGLE: _EnumFlatMode.ValueType - FLAT_MODE_PHOTO: _EnumFlatMode.ValueType - FLAT_MODE_PHOTO_NIGHT: _EnumFlatMode.ValueType - FLAT_MODE_PHOTO_BURST: _EnumFlatMode.ValueType - FLAT_MODE_TIME_LAPSE_PHOTO: _EnumFlatMode.ValueType - FLAT_MODE_NIGHT_LAPSE_PHOTO: _EnumFlatMode.ValueType - FLAT_MODE_BROADCAST_RECORD: _EnumFlatMode.ValueType - FLAT_MODE_BROADCAST_BROADCAST: _EnumFlatMode.ValueType - FLAT_MODE_TIME_WARP_VIDEO: _EnumFlatMode.ValueType - FLAT_MODE_LIVE_BURST: _EnumFlatMode.ValueType - FLAT_MODE_NIGHT_LAPSE_VIDEO: _EnumFlatMode.ValueType - FLAT_MODE_SLOMO: _EnumFlatMode.ValueType - FLAT_MODE_IDLE: _EnumFlatMode.ValueType - FLAT_MODE_VIDEO_STAR_TRAIL: _EnumFlatMode.ValueType - FLAT_MODE_VIDEO_LIGHT_PAINTING: _EnumFlatMode.ValueType - FLAT_MODE_VIDEO_LIGHT_TRAIL: _EnumFlatMode.ValueType - -class EnumFlatMode(_EnumFlatMode, metaclass=_EnumFlatModeEnumTypeWrapper): ... - -FLAT_MODE_UNKNOWN: EnumFlatMode.ValueType -FLAT_MODE_PLAYBACK: EnumFlatMode.ValueType -FLAT_MODE_SETUP: EnumFlatMode.ValueType -FLAT_MODE_VIDEO: EnumFlatMode.ValueType -FLAT_MODE_TIME_LAPSE_VIDEO: EnumFlatMode.ValueType -FLAT_MODE_LOOPING: EnumFlatMode.ValueType -FLAT_MODE_PHOTO_SINGLE: EnumFlatMode.ValueType -FLAT_MODE_PHOTO: EnumFlatMode.ValueType -FLAT_MODE_PHOTO_NIGHT: EnumFlatMode.ValueType -FLAT_MODE_PHOTO_BURST: EnumFlatMode.ValueType -FLAT_MODE_TIME_LAPSE_PHOTO: EnumFlatMode.ValueType -FLAT_MODE_NIGHT_LAPSE_PHOTO: EnumFlatMode.ValueType -FLAT_MODE_BROADCAST_RECORD: EnumFlatMode.ValueType -FLAT_MODE_BROADCAST_BROADCAST: EnumFlatMode.ValueType -FLAT_MODE_TIME_WARP_VIDEO: EnumFlatMode.ValueType -FLAT_MODE_LIVE_BURST: EnumFlatMode.ValueType -FLAT_MODE_NIGHT_LAPSE_VIDEO: EnumFlatMode.ValueType -FLAT_MODE_SLOMO: EnumFlatMode.ValueType -FLAT_MODE_IDLE: EnumFlatMode.ValueType -FLAT_MODE_VIDEO_STAR_TRAIL: EnumFlatMode.ValueType -FLAT_MODE_VIDEO_LIGHT_PAINTING: EnumFlatMode.ValueType -FLAT_MODE_VIDEO_LIGHT_TRAIL: EnumFlatMode.ValueType -global___EnumFlatMode = EnumFlatMode - -class _EnumPresetGroup: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumPresetGroupEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetGroup.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - PRESET_GROUP_ID_VIDEO: _EnumPresetGroup.ValueType - PRESET_GROUP_ID_PHOTO: _EnumPresetGroup.ValueType - PRESET_GROUP_ID_TIMELAPSE: _EnumPresetGroup.ValueType - -class EnumPresetGroup(_EnumPresetGroup, metaclass=_EnumPresetGroupEnumTypeWrapper): ... - -PRESET_GROUP_ID_VIDEO: EnumPresetGroup.ValueType -PRESET_GROUP_ID_PHOTO: EnumPresetGroup.ValueType -PRESET_GROUP_ID_TIMELAPSE: EnumPresetGroup.ValueType -global___EnumPresetGroup = EnumPresetGroup - -class _EnumPresetGroupIcon: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumPresetGroupIconEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetGroupIcon.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - PRESET_GROUP_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType - PRESET_GROUP_PHOTO_ICON_ID: _EnumPresetGroupIcon.ValueType - PRESET_GROUP_TIMELAPSE_ICON_ID: _EnumPresetGroupIcon.ValueType - PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType - PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType - PRESET_GROUP_MAX_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType - PRESET_GROUP_MAX_PHOTO_ICON_ID: _EnumPresetGroupIcon.ValueType - PRESET_GROUP_MAX_TIMELAPSE_ICON_ID: _EnumPresetGroupIcon.ValueType - -class EnumPresetGroupIcon(_EnumPresetGroupIcon, metaclass=_EnumPresetGroupIconEnumTypeWrapper): ... - -PRESET_GROUP_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType -PRESET_GROUP_PHOTO_ICON_ID: EnumPresetGroupIcon.ValueType -PRESET_GROUP_TIMELAPSE_ICON_ID: EnumPresetGroupIcon.ValueType -PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType -PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType -PRESET_GROUP_MAX_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType -PRESET_GROUP_MAX_PHOTO_ICON_ID: EnumPresetGroupIcon.ValueType -PRESET_GROUP_MAX_TIMELAPSE_ICON_ID: EnumPresetGroupIcon.ValueType -global___EnumPresetGroupIcon = EnumPresetGroupIcon - -class _EnumPresetIcon: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumPresetIconEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetIcon.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - PRESET_ICON_VIDEO: _EnumPresetIcon.ValueType - PRESET_ICON_ACTIVITY: _EnumPresetIcon.ValueType - PRESET_ICON_CINEMATIC: _EnumPresetIcon.ValueType - PRESET_ICON_PHOTO: _EnumPresetIcon.ValueType - PRESET_ICON_LIVE_BURST: _EnumPresetIcon.ValueType - PRESET_ICON_BURST: _EnumPresetIcon.ValueType - PRESET_ICON_PHOTO_NIGHT: _EnumPresetIcon.ValueType - PRESET_ICON_TIMEWARP: _EnumPresetIcon.ValueType - PRESET_ICON_TIMELAPSE: _EnumPresetIcon.ValueType - PRESET_ICON_NIGHTLAPSE: _EnumPresetIcon.ValueType - PRESET_ICON_SNAIL: _EnumPresetIcon.ValueType - PRESET_ICON_VIDEO_2: _EnumPresetIcon.ValueType - PRESET_ICON_360_VIDEO: _EnumPresetIcon.ValueType - PRESET_ICON_PHOTO_2: _EnumPresetIcon.ValueType - PRESET_ICON_PANORAMA: _EnumPresetIcon.ValueType - PRESET_ICON_BURST_2: _EnumPresetIcon.ValueType - PRESET_ICON_TIMEWARP_2: _EnumPresetIcon.ValueType - PRESET_ICON_TIMELAPSE_2: _EnumPresetIcon.ValueType - PRESET_ICON_CUSTOM: _EnumPresetIcon.ValueType - PRESET_ICON_AIR: _EnumPresetIcon.ValueType - PRESET_ICON_BIKE: _EnumPresetIcon.ValueType - PRESET_ICON_EPIC: _EnumPresetIcon.ValueType - PRESET_ICON_INDOOR: _EnumPresetIcon.ValueType - PRESET_ICON_MOTOR: _EnumPresetIcon.ValueType - PRESET_ICON_MOUNTED: _EnumPresetIcon.ValueType - PRESET_ICON_OUTDOOR: _EnumPresetIcon.ValueType - PRESET_ICON_POV: _EnumPresetIcon.ValueType - PRESET_ICON_SELFIE: _EnumPresetIcon.ValueType - PRESET_ICON_SKATE: _EnumPresetIcon.ValueType - PRESET_ICON_SNOW: _EnumPresetIcon.ValueType - PRESET_ICON_TRAIL: _EnumPresetIcon.ValueType - PRESET_ICON_TRAVEL: _EnumPresetIcon.ValueType - PRESET_ICON_WATER: _EnumPresetIcon.ValueType - PRESET_ICON_LOOPING: _EnumPresetIcon.ValueType - PRESET_ICON_STARS: _EnumPresetIcon.ValueType - "*\n New custom icon (34 - 42)added for HERO 12\n " - PRESET_ICON_ACTION: _EnumPresetIcon.ValueType - PRESET_ICON_FOLLOW_CAM: _EnumPresetIcon.ValueType - PRESET_ICON_SURF: _EnumPresetIcon.ValueType - PRESET_ICON_CITY: _EnumPresetIcon.ValueType - PRESET_ICON_SHAKY: _EnumPresetIcon.ValueType - PRESET_ICON_CHESTY: _EnumPresetIcon.ValueType - PRESET_ICON_HELMET: _EnumPresetIcon.ValueType - PRESET_ICON_BITE: _EnumPresetIcon.ValueType - PRESET_ICON_MAX_VIDEO: _EnumPresetIcon.ValueType - "*\n Reserved 43 - 50 for Custom presets. Add icons below for new presets starting from 51\n " - PRESET_ICON_MAX_PHOTO: _EnumPresetIcon.ValueType - PRESET_ICON_MAX_TIMEWARP: _EnumPresetIcon.ValueType - PRESET_ICON_BASIC: _EnumPresetIcon.ValueType - PRESET_ICON_ULTRA_SLO_MO: _EnumPresetIcon.ValueType - PRESET_ICON_STANDARD_ENDURANCE: _EnumPresetIcon.ValueType - PRESET_ICON_ACTIVITY_ENDURANCE: _EnumPresetIcon.ValueType - PRESET_ICON_CINEMATIC_ENDURANCE: _EnumPresetIcon.ValueType - PRESET_ICON_SLOMO_ENDURANCE: _EnumPresetIcon.ValueType - PRESET_ICON_STATIONARY_1: _EnumPresetIcon.ValueType - PRESET_ICON_STATIONARY_2: _EnumPresetIcon.ValueType - PRESET_ICON_STATIONARY_3: _EnumPresetIcon.ValueType - PRESET_ICON_STATIONARY_4: _EnumPresetIcon.ValueType - PRESET_ICON_SIMPLE_SUPER_PHOTO: _EnumPresetIcon.ValueType - PRESET_ICON_SIMPLE_NIGHT_PHOTO: _EnumPresetIcon.ValueType - PRESET_ICON_HIGHEST_QUALITY_VIDEO: _EnumPresetIcon.ValueType - PRESET_ICON_STANDARD_QUALITY_VIDEO: _EnumPresetIcon.ValueType - PRESET_ICON_BASIC_QUALITY_VIDEO: _EnumPresetIcon.ValueType - PRESET_ICON_STAR_TRAIL: _EnumPresetIcon.ValueType - PRESET_ICON_LIGHT_PAINTING: _EnumPresetIcon.ValueType - PRESET_ICON_LIGHT_TRAIL: _EnumPresetIcon.ValueType - PRESET_ICON_FULL_FRAME: _EnumPresetIcon.ValueType - PRESET_ICON_EASY_MAX_VIDEO: _EnumPresetIcon.ValueType - PRESET_ICON_EASY_MAX_PHOTO: _EnumPresetIcon.ValueType - PRESET_ICON_EASY_MAX_TIMEWARP: _EnumPresetIcon.ValueType - PRESET_ICON_EASY_MAX_STAR_TRAIL: _EnumPresetIcon.ValueType - PRESET_ICON_EASY_MAX_LIGHT_PAINTING: _EnumPresetIcon.ValueType - PRESET_ICON_EASY_MAX_LIGHT_TRAIL: _EnumPresetIcon.ValueType - PRESET_ICON_MAX_STAR_TRAIL: _EnumPresetIcon.ValueType - PRESET_ICON_MAX_LIGHT_PAINTING: _EnumPresetIcon.ValueType - PRESET_ICON_MAX_LIGHT_TRAIL: _EnumPresetIcon.ValueType - PRESET_ICON_TIMELAPSE_PHOTO: _EnumPresetIcon.ValueType - PRESET_ICON_NIGHTLAPSE_PHOTO: _EnumPresetIcon.ValueType - -class EnumPresetIcon(_EnumPresetIcon, metaclass=_EnumPresetIconEnumTypeWrapper): ... - -PRESET_ICON_VIDEO: EnumPresetIcon.ValueType -PRESET_ICON_ACTIVITY: EnumPresetIcon.ValueType -PRESET_ICON_CINEMATIC: EnumPresetIcon.ValueType -PRESET_ICON_PHOTO: EnumPresetIcon.ValueType -PRESET_ICON_LIVE_BURST: EnumPresetIcon.ValueType -PRESET_ICON_BURST: EnumPresetIcon.ValueType -PRESET_ICON_PHOTO_NIGHT: EnumPresetIcon.ValueType -PRESET_ICON_TIMEWARP: EnumPresetIcon.ValueType -PRESET_ICON_TIMELAPSE: EnumPresetIcon.ValueType -PRESET_ICON_NIGHTLAPSE: EnumPresetIcon.ValueType -PRESET_ICON_SNAIL: EnumPresetIcon.ValueType -PRESET_ICON_VIDEO_2: EnumPresetIcon.ValueType -PRESET_ICON_360_VIDEO: EnumPresetIcon.ValueType -PRESET_ICON_PHOTO_2: EnumPresetIcon.ValueType -PRESET_ICON_PANORAMA: EnumPresetIcon.ValueType -PRESET_ICON_BURST_2: EnumPresetIcon.ValueType -PRESET_ICON_TIMEWARP_2: EnumPresetIcon.ValueType -PRESET_ICON_TIMELAPSE_2: EnumPresetIcon.ValueType -PRESET_ICON_CUSTOM: EnumPresetIcon.ValueType -PRESET_ICON_AIR: EnumPresetIcon.ValueType -PRESET_ICON_BIKE: EnumPresetIcon.ValueType -PRESET_ICON_EPIC: EnumPresetIcon.ValueType -PRESET_ICON_INDOOR: EnumPresetIcon.ValueType -PRESET_ICON_MOTOR: EnumPresetIcon.ValueType -PRESET_ICON_MOUNTED: EnumPresetIcon.ValueType -PRESET_ICON_OUTDOOR: EnumPresetIcon.ValueType -PRESET_ICON_POV: EnumPresetIcon.ValueType -PRESET_ICON_SELFIE: EnumPresetIcon.ValueType -PRESET_ICON_SKATE: EnumPresetIcon.ValueType -PRESET_ICON_SNOW: EnumPresetIcon.ValueType -PRESET_ICON_TRAIL: EnumPresetIcon.ValueType -PRESET_ICON_TRAVEL: EnumPresetIcon.ValueType -PRESET_ICON_WATER: EnumPresetIcon.ValueType -PRESET_ICON_LOOPING: EnumPresetIcon.ValueType -PRESET_ICON_STARS: EnumPresetIcon.ValueType -"*\nNew custom icon (34 - 42)added for HERO 12\n" -PRESET_ICON_ACTION: EnumPresetIcon.ValueType -PRESET_ICON_FOLLOW_CAM: EnumPresetIcon.ValueType -PRESET_ICON_SURF: EnumPresetIcon.ValueType -PRESET_ICON_CITY: EnumPresetIcon.ValueType -PRESET_ICON_SHAKY: EnumPresetIcon.ValueType -PRESET_ICON_CHESTY: EnumPresetIcon.ValueType -PRESET_ICON_HELMET: EnumPresetIcon.ValueType -PRESET_ICON_BITE: EnumPresetIcon.ValueType -PRESET_ICON_MAX_VIDEO: EnumPresetIcon.ValueType -"*\nReserved 43 - 50 for Custom presets. Add icons below for new presets starting from 51\n" -PRESET_ICON_MAX_PHOTO: EnumPresetIcon.ValueType -PRESET_ICON_MAX_TIMEWARP: EnumPresetIcon.ValueType -PRESET_ICON_BASIC: EnumPresetIcon.ValueType -PRESET_ICON_ULTRA_SLO_MO: EnumPresetIcon.ValueType -PRESET_ICON_STANDARD_ENDURANCE: EnumPresetIcon.ValueType -PRESET_ICON_ACTIVITY_ENDURANCE: EnumPresetIcon.ValueType -PRESET_ICON_CINEMATIC_ENDURANCE: EnumPresetIcon.ValueType -PRESET_ICON_SLOMO_ENDURANCE: EnumPresetIcon.ValueType -PRESET_ICON_STATIONARY_1: EnumPresetIcon.ValueType -PRESET_ICON_STATIONARY_2: EnumPresetIcon.ValueType -PRESET_ICON_STATIONARY_3: EnumPresetIcon.ValueType -PRESET_ICON_STATIONARY_4: EnumPresetIcon.ValueType -PRESET_ICON_SIMPLE_SUPER_PHOTO: EnumPresetIcon.ValueType -PRESET_ICON_SIMPLE_NIGHT_PHOTO: EnumPresetIcon.ValueType -PRESET_ICON_HIGHEST_QUALITY_VIDEO: EnumPresetIcon.ValueType -PRESET_ICON_STANDARD_QUALITY_VIDEO: EnumPresetIcon.ValueType -PRESET_ICON_BASIC_QUALITY_VIDEO: EnumPresetIcon.ValueType -PRESET_ICON_STAR_TRAIL: EnumPresetIcon.ValueType -PRESET_ICON_LIGHT_PAINTING: EnumPresetIcon.ValueType -PRESET_ICON_LIGHT_TRAIL: EnumPresetIcon.ValueType -PRESET_ICON_FULL_FRAME: EnumPresetIcon.ValueType -PRESET_ICON_EASY_MAX_VIDEO: EnumPresetIcon.ValueType -PRESET_ICON_EASY_MAX_PHOTO: EnumPresetIcon.ValueType -PRESET_ICON_EASY_MAX_TIMEWARP: EnumPresetIcon.ValueType -PRESET_ICON_EASY_MAX_STAR_TRAIL: EnumPresetIcon.ValueType -PRESET_ICON_EASY_MAX_LIGHT_PAINTING: EnumPresetIcon.ValueType -PRESET_ICON_EASY_MAX_LIGHT_TRAIL: EnumPresetIcon.ValueType -PRESET_ICON_MAX_STAR_TRAIL: EnumPresetIcon.ValueType -PRESET_ICON_MAX_LIGHT_PAINTING: EnumPresetIcon.ValueType -PRESET_ICON_MAX_LIGHT_TRAIL: EnumPresetIcon.ValueType -PRESET_ICON_TIMELAPSE_PHOTO: EnumPresetIcon.ValueType -PRESET_ICON_NIGHTLAPSE_PHOTO: EnumPresetIcon.ValueType -global___EnumPresetIcon = EnumPresetIcon - -class _EnumPresetTitle: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumPresetTitleEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetTitle.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - PRESET_TITLE_ACTIVITY: _EnumPresetTitle.ValueType - PRESET_TITLE_STANDARD: _EnumPresetTitle.ValueType - PRESET_TITLE_CINEMATIC: _EnumPresetTitle.ValueType - PRESET_TITLE_PHOTO: _EnumPresetTitle.ValueType - PRESET_TITLE_LIVE_BURST: _EnumPresetTitle.ValueType - PRESET_TITLE_BURST: _EnumPresetTitle.ValueType - PRESET_TITLE_NIGHT: _EnumPresetTitle.ValueType - PRESET_TITLE_TIME_WARP: _EnumPresetTitle.ValueType - PRESET_TITLE_TIME_LAPSE: _EnumPresetTitle.ValueType - PRESET_TITLE_NIGHT_LAPSE: _EnumPresetTitle.ValueType - PRESET_TITLE_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_SLOMO: _EnumPresetTitle.ValueType - PRESET_TITLE_360_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_PHOTO_2: _EnumPresetTitle.ValueType - PRESET_TITLE_PANORAMA: _EnumPresetTitle.ValueType - PRESET_TITLE_360_PHOTO: _EnumPresetTitle.ValueType - PRESET_TITLE_TIME_WARP_2: _EnumPresetTitle.ValueType - PRESET_TITLE_360_TIME_WARP: _EnumPresetTitle.ValueType - PRESET_TITLE_CUSTOM: _EnumPresetTitle.ValueType - PRESET_TITLE_AIR: _EnumPresetTitle.ValueType - PRESET_TITLE_BIKE: _EnumPresetTitle.ValueType - PRESET_TITLE_EPIC: _EnumPresetTitle.ValueType - PRESET_TITLE_INDOOR: _EnumPresetTitle.ValueType - PRESET_TITLE_MOTOR: _EnumPresetTitle.ValueType - PRESET_TITLE_MOUNTED: _EnumPresetTitle.ValueType - PRESET_TITLE_OUTDOOR: _EnumPresetTitle.ValueType - PRESET_TITLE_POV: _EnumPresetTitle.ValueType - PRESET_TITLE_SELFIE: _EnumPresetTitle.ValueType - PRESET_TITLE_SKATE: _EnumPresetTitle.ValueType - PRESET_TITLE_SNOW: _EnumPresetTitle.ValueType - PRESET_TITLE_TRAIL: _EnumPresetTitle.ValueType - PRESET_TITLE_TRAVEL: _EnumPresetTitle.ValueType - PRESET_TITLE_WATER: _EnumPresetTitle.ValueType - PRESET_TITLE_LOOPING: _EnumPresetTitle.ValueType - PRESET_TITLE_STARS: _EnumPresetTitle.ValueType - "*\n New custom names (34 - 42)added for HERO 12\n " - PRESET_TITLE_ACTION: _EnumPresetTitle.ValueType - PRESET_TITLE_FOLLOW_CAM: _EnumPresetTitle.ValueType - PRESET_TITLE_SURF: _EnumPresetTitle.ValueType - PRESET_TITLE_CITY: _EnumPresetTitle.ValueType - PRESET_TITLE_SHAKY: _EnumPresetTitle.ValueType - PRESET_TITLE_CHESTY: _EnumPresetTitle.ValueType - PRESET_TITLE_HELMET: _EnumPresetTitle.ValueType - PRESET_TITLE_BITE: _EnumPresetTitle.ValueType - PRESET_TITLE_360_TIMELAPSE: _EnumPresetTitle.ValueType - "*\n Reserved 43 - 50 for custom presets.\n " - PRESET_TITLE_360_NIGHT_LAPSE: _EnumPresetTitle.ValueType - PRESET_TITLE_360_NIGHT_PHOTO: _EnumPresetTitle.ValueType - PRESET_TITLE_PANO_TIME_LAPSE: _EnumPresetTitle.ValueType - PRESET_TITLE_MAX_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_MAX_PHOTO: _EnumPresetTitle.ValueType - PRESET_TITLE_MAX_TIMEWARP: _EnumPresetTitle.ValueType - PRESET_TITLE_BASIC: _EnumPresetTitle.ValueType - PRESET_TITLE_ULTRA_SLO_MO: _EnumPresetTitle.ValueType - PRESET_TITLE_STANDARD_ENDURANCE: _EnumPresetTitle.ValueType - PRESET_TITLE_ACTIVITY_ENDURANCE: _EnumPresetTitle.ValueType - PRESET_TITLE_CINEMATIC_ENDURANCE: _EnumPresetTitle.ValueType - PRESET_TITLE_SLOMO_ENDURANCE: _EnumPresetTitle.ValueType - PRESET_TITLE_STATIONARY_1: _EnumPresetTitle.ValueType - PRESET_TITLE_STATIONARY_2: _EnumPresetTitle.ValueType - PRESET_TITLE_STATIONARY_3: _EnumPresetTitle.ValueType - PRESET_TITLE_STATIONARY_4: _EnumPresetTitle.ValueType - PRESET_TITLE_SIMPLE_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_SIMPLE_TIME_WARP: _EnumPresetTitle.ValueType - PRESET_TITLE_SIMPLE_SUPER_PHOTO: _EnumPresetTitle.ValueType - PRESET_TITLE_SIMPLE_NIGHT_PHOTO: _EnumPresetTitle.ValueType - PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE: _EnumPresetTitle.ValueType - PRESET_TITLE_HIGHEST_QUALITY: _EnumPresetTitle.ValueType - PRESET_TITLE_EXTENDED_BATTERY: _EnumPresetTitle.ValueType - PRESET_TITLE_LONGEST_BATTERY: _EnumPresetTitle.ValueType - PRESET_TITLE_STAR_TRAIL: _EnumPresetTitle.ValueType - PRESET_TITLE_LIGHT_PAINTING: _EnumPresetTitle.ValueType - PRESET_TITLE_LIGHT_TRAIL: _EnumPresetTitle.ValueType - PRESET_TITLE_FULL_FRAME: _EnumPresetTitle.ValueType - PRESET_TITLE_MAX_LENS_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_MAX_LENS_TIMEWARP: _EnumPresetTitle.ValueType - PRESET_TITLE_STANDARD_QUALITY_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_BASIC_QUALITY_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_EASY_MAX_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_EASY_MAX_PHOTO: _EnumPresetTitle.ValueType - PRESET_TITLE_EASY_MAX_TIMEWARP: _EnumPresetTitle.ValueType - PRESET_TITLE_EASY_MAX_STAR_TRAIL: _EnumPresetTitle.ValueType - PRESET_TITLE_EASY_MAX_LIGHT_PAINTING: _EnumPresetTitle.ValueType - PRESET_TITLE_EASY_MAX_LIGHT_TRAIL: _EnumPresetTitle.ValueType - PRESET_TITLE_MAX_STAR_TRAIL: _EnumPresetTitle.ValueType - PRESET_TITLE_MAX_LIGHT_PAINTING: _EnumPresetTitle.ValueType - PRESET_TITLE_MAX_LIGHT_TRAIL: _EnumPresetTitle.ValueType - PRESET_TITLE_HIGHEST_QUALITY_VIDEO: _EnumPresetTitle.ValueType - PRESET_TITLE_USER_DEFINED_CUSTOM_NAME: _EnumPresetTitle.ValueType - -class EnumPresetTitle(_EnumPresetTitle, metaclass=_EnumPresetTitleEnumTypeWrapper): ... - -PRESET_TITLE_ACTIVITY: EnumPresetTitle.ValueType -PRESET_TITLE_STANDARD: EnumPresetTitle.ValueType -PRESET_TITLE_CINEMATIC: EnumPresetTitle.ValueType -PRESET_TITLE_PHOTO: EnumPresetTitle.ValueType -PRESET_TITLE_LIVE_BURST: EnumPresetTitle.ValueType -PRESET_TITLE_BURST: EnumPresetTitle.ValueType -PRESET_TITLE_NIGHT: EnumPresetTitle.ValueType -PRESET_TITLE_TIME_WARP: EnumPresetTitle.ValueType -PRESET_TITLE_TIME_LAPSE: EnumPresetTitle.ValueType -PRESET_TITLE_NIGHT_LAPSE: EnumPresetTitle.ValueType -PRESET_TITLE_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_SLOMO: EnumPresetTitle.ValueType -PRESET_TITLE_360_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_PHOTO_2: EnumPresetTitle.ValueType -PRESET_TITLE_PANORAMA: EnumPresetTitle.ValueType -PRESET_TITLE_360_PHOTO: EnumPresetTitle.ValueType -PRESET_TITLE_TIME_WARP_2: EnumPresetTitle.ValueType -PRESET_TITLE_360_TIME_WARP: EnumPresetTitle.ValueType -PRESET_TITLE_CUSTOM: EnumPresetTitle.ValueType -PRESET_TITLE_AIR: EnumPresetTitle.ValueType -PRESET_TITLE_BIKE: EnumPresetTitle.ValueType -PRESET_TITLE_EPIC: EnumPresetTitle.ValueType -PRESET_TITLE_INDOOR: EnumPresetTitle.ValueType -PRESET_TITLE_MOTOR: EnumPresetTitle.ValueType -PRESET_TITLE_MOUNTED: EnumPresetTitle.ValueType -PRESET_TITLE_OUTDOOR: EnumPresetTitle.ValueType -PRESET_TITLE_POV: EnumPresetTitle.ValueType -PRESET_TITLE_SELFIE: EnumPresetTitle.ValueType -PRESET_TITLE_SKATE: EnumPresetTitle.ValueType -PRESET_TITLE_SNOW: EnumPresetTitle.ValueType -PRESET_TITLE_TRAIL: EnumPresetTitle.ValueType -PRESET_TITLE_TRAVEL: EnumPresetTitle.ValueType -PRESET_TITLE_WATER: EnumPresetTitle.ValueType -PRESET_TITLE_LOOPING: EnumPresetTitle.ValueType -PRESET_TITLE_STARS: EnumPresetTitle.ValueType -"*\nNew custom names (34 - 42)added for HERO 12\n" -PRESET_TITLE_ACTION: EnumPresetTitle.ValueType -PRESET_TITLE_FOLLOW_CAM: EnumPresetTitle.ValueType -PRESET_TITLE_SURF: EnumPresetTitle.ValueType -PRESET_TITLE_CITY: EnumPresetTitle.ValueType -PRESET_TITLE_SHAKY: EnumPresetTitle.ValueType -PRESET_TITLE_CHESTY: EnumPresetTitle.ValueType -PRESET_TITLE_HELMET: EnumPresetTitle.ValueType -PRESET_TITLE_BITE: EnumPresetTitle.ValueType -PRESET_TITLE_360_TIMELAPSE: EnumPresetTitle.ValueType -"*\nReserved 43 - 50 for custom presets.\n" -PRESET_TITLE_360_NIGHT_LAPSE: EnumPresetTitle.ValueType -PRESET_TITLE_360_NIGHT_PHOTO: EnumPresetTitle.ValueType -PRESET_TITLE_PANO_TIME_LAPSE: EnumPresetTitle.ValueType -PRESET_TITLE_MAX_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_MAX_PHOTO: EnumPresetTitle.ValueType -PRESET_TITLE_MAX_TIMEWARP: EnumPresetTitle.ValueType -PRESET_TITLE_BASIC: EnumPresetTitle.ValueType -PRESET_TITLE_ULTRA_SLO_MO: EnumPresetTitle.ValueType -PRESET_TITLE_STANDARD_ENDURANCE: EnumPresetTitle.ValueType -PRESET_TITLE_ACTIVITY_ENDURANCE: EnumPresetTitle.ValueType -PRESET_TITLE_CINEMATIC_ENDURANCE: EnumPresetTitle.ValueType -PRESET_TITLE_SLOMO_ENDURANCE: EnumPresetTitle.ValueType -PRESET_TITLE_STATIONARY_1: EnumPresetTitle.ValueType -PRESET_TITLE_STATIONARY_2: EnumPresetTitle.ValueType -PRESET_TITLE_STATIONARY_3: EnumPresetTitle.ValueType -PRESET_TITLE_STATIONARY_4: EnumPresetTitle.ValueType -PRESET_TITLE_SIMPLE_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_SIMPLE_TIME_WARP: EnumPresetTitle.ValueType -PRESET_TITLE_SIMPLE_SUPER_PHOTO: EnumPresetTitle.ValueType -PRESET_TITLE_SIMPLE_NIGHT_PHOTO: EnumPresetTitle.ValueType -PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE: EnumPresetTitle.ValueType -PRESET_TITLE_HIGHEST_QUALITY: EnumPresetTitle.ValueType -PRESET_TITLE_EXTENDED_BATTERY: EnumPresetTitle.ValueType -PRESET_TITLE_LONGEST_BATTERY: EnumPresetTitle.ValueType -PRESET_TITLE_STAR_TRAIL: EnumPresetTitle.ValueType -PRESET_TITLE_LIGHT_PAINTING: EnumPresetTitle.ValueType -PRESET_TITLE_LIGHT_TRAIL: EnumPresetTitle.ValueType -PRESET_TITLE_FULL_FRAME: EnumPresetTitle.ValueType -PRESET_TITLE_MAX_LENS_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_MAX_LENS_TIMEWARP: EnumPresetTitle.ValueType -PRESET_TITLE_STANDARD_QUALITY_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_BASIC_QUALITY_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_EASY_MAX_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_EASY_MAX_PHOTO: EnumPresetTitle.ValueType -PRESET_TITLE_EASY_MAX_TIMEWARP: EnumPresetTitle.ValueType -PRESET_TITLE_EASY_MAX_STAR_TRAIL: EnumPresetTitle.ValueType -PRESET_TITLE_EASY_MAX_LIGHT_PAINTING: EnumPresetTitle.ValueType -PRESET_TITLE_EASY_MAX_LIGHT_TRAIL: EnumPresetTitle.ValueType -PRESET_TITLE_MAX_STAR_TRAIL: EnumPresetTitle.ValueType -PRESET_TITLE_MAX_LIGHT_PAINTING: EnumPresetTitle.ValueType -PRESET_TITLE_MAX_LIGHT_TRAIL: EnumPresetTitle.ValueType -PRESET_TITLE_HIGHEST_QUALITY_VIDEO: EnumPresetTitle.ValueType -PRESET_TITLE_USER_DEFINED_CUSTOM_NAME: EnumPresetTitle.ValueType -global___EnumPresetTitle = EnumPresetTitle - -class NotifyPresetStatus(google.protobuf.message.Message): - """* - Current Preset status - - Sent either: - - synchronously via initial response to @ref RequestGetPresetStatus - - asynchronously when Preset change if registered in @rev RequestGetPresetStatus - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - PRESET_GROUP_ARRAY_FIELD_NUMBER: builtins.int - - @property - def preset_group_array( - self, - ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___PresetGroup]: - """Array of currently available Preset Groups""" - 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: ... - -global___NotifyPresetStatus = NotifyPresetStatus - -class Preset(google.protobuf.message.Message): - """* - An individual preset. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - ID_FIELD_NUMBER: builtins.int - MODE_FIELD_NUMBER: builtins.int - TITLE_ID_FIELD_NUMBER: builtins.int - TITLE_NUMBER_FIELD_NUMBER: builtins.int - USER_DEFINED_FIELD_NUMBER: builtins.int - ICON_FIELD_NUMBER: builtins.int - SETTING_ARRAY_FIELD_NUMBER: builtins.int - IS_MODIFIED_FIELD_NUMBER: builtins.int - IS_FIXED_FIELD_NUMBER: builtins.int - CUSTOM_NAME_FIELD_NUMBER: builtins.int - id: builtins.int - "Preset ID" - mode: global___EnumFlatMode.ValueType - "Preset flatmode ID" - title_id: global___EnumPresetTitle.ValueType - "Preset Title ID" - title_number: builtins.int - "Preset Title Number (e.g. 1/2/3 in Custom1, Custom2, Custom3)" - user_defined: builtins.bool - "Is the Preset custom/user-defined?" - icon: global___EnumPresetIcon.ValueType - "Preset Icon ID" - - @property - def setting_array( - self, - ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___PresetSetting]: - """Array of settings associated with this Preset""" - is_modified: builtins.bool - "Has Preset been modified from factory defaults? (False for user-defined Presets)" - is_fixed: builtins.bool - "Is this Preset mutable?" - custom_name: builtins.str - "Custom string name given to this preset via @ref RequestCustomPresetUpdate" - - def __init__( - self, - *, - id: builtins.int | None = ..., - mode: global___EnumFlatMode.ValueType | None = ..., - title_id: global___EnumPresetTitle.ValueType | None = ..., - title_number: builtins.int | None = ..., - user_defined: builtins.bool | None = ..., - icon: global___EnumPresetIcon.ValueType | None = ..., - setting_array: collections.abc.Iterable[global___PresetSetting] | None = ..., - is_modified: builtins.bool | None = ..., - is_fixed: builtins.bool | None = ..., - custom_name: builtins.str | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "custom_name", - b"custom_name", - "icon", - b"icon", - "id", - b"id", - "is_fixed", - b"is_fixed", - "is_modified", - b"is_modified", - "mode", - b"mode", - "title_id", - b"title_id", - "title_number", - b"title_number", - "user_defined", - b"user_defined", - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "custom_name", - b"custom_name", - "icon", - b"icon", - "id", - b"id", - "is_fixed", - b"is_fixed", - "is_modified", - b"is_modified", - "mode", - b"mode", - "setting_array", - b"setting_array", - "title_id", - b"title_id", - "title_number", - b"title_number", - "user_defined", - b"user_defined", - ], - ) -> None: ... - -global___Preset = Preset - -class RequestCustomPresetUpdate(google.protobuf.message.Message): - """* - Request to update the active custom preset - - This only operates on the currently active Preset and will fail if the current - Preset is not custom. - - The use cases are: - - 1. Update the Custom Preset Icon - - `icon_id` is always optional and can always be passed - - and / or - - 2. Update the Custom Preset Title to a... - - **Factory Preset Title**: Set `title_id` to a non-94 value - - **Custom Preset Name**: Set `title_id` to 94 and specify a `custom_name` - * - Preset Title ID - - The range of acceptable custom title ID's can be found in the initial @ref NotifyPresetStatus response - to @ref RequestGetPresetStatus - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - TITLE_ID_FIELD_NUMBER: builtins.int - CUSTOM_NAME_FIELD_NUMBER: builtins.int - ICON_ID_FIELD_NUMBER: builtins.int - title_id: global___EnumPresetTitle.ValueType - custom_name: builtins.str - "utf-8 encoded target custom preset name" - icon_id: global___EnumPresetIcon.ValueType - "*\n Preset Icon ID\n\n The range of acceptable custom icon ID's can be found in the initial @ref NotifyPresetStatus response to\n @ref RequestGetPresetStatus\n " - - def __init__( - self, - *, - title_id: global___EnumPresetTitle.ValueType | None = ..., - custom_name: builtins.str | None = ..., - icon_id: global___EnumPresetIcon.ValueType | None = ... - ) -> None: ... - def HasField( - self, - field_name: typing_extensions.Literal[ - "custom_name", b"custom_name", "icon_id", b"icon_id", "title_id", b"title_id" - ], - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "custom_name", b"custom_name", "icon_id", b"icon_id", "title_id", b"title_id" - ], - ) -> None: ... - -global___RequestCustomPresetUpdate = RequestCustomPresetUpdate - -class PresetGroup(google.protobuf.message.Message): - """ - Preset Group meta information and contained Presets - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - ID_FIELD_NUMBER: builtins.int - PRESET_ARRAY_FIELD_NUMBER: builtins.int - CAN_ADD_PRESET_FIELD_NUMBER: builtins.int - ICON_FIELD_NUMBER: builtins.int - id: global___EnumPresetGroup.ValueType - "Preset Group ID" - - @property - def preset_array(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Preset]: - """Array of Presets contained in this Preset Group""" - can_add_preset: builtins.bool - "Is there room in the group to add additional Presets?" - icon: global___EnumPresetGroupIcon.ValueType - "The icon to display for this preset group" - - def __init__( - self, - *, - id: global___EnumPresetGroup.ValueType | None = ..., - preset_array: collections.abc.Iterable[global___Preset] | None = ..., - can_add_preset: builtins.bool | None = ..., - icon: global___EnumPresetGroupIcon.ValueType | None = ... - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["can_add_preset", b"can_add_preset", "icon", b"icon", "id", b"id"] - ) -> builtins.bool: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "can_add_preset", b"can_add_preset", "icon", b"icon", "id", b"id", "preset_array", b"preset_array" - ], - ) -> None: ... - -global___PresetGroup = PresetGroup - -class PresetSetting(google.protobuf.message.Message): - """* - Setting representation that comprises a @ref Preset - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - ID_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - IS_CAPTION_FIELD_NUMBER: builtins.int - id: builtins.int - "Setting ID" - value: builtins.int - "Setting value" - is_caption: builtins.bool - '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 = ... - ) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["id", b"id", "is_caption", b"is_caption", "value", b"value"] - ) -> builtins.bool: ... - def ClearField( - self, field_name: typing_extensions.Literal["id", b"id", "is_caption", b"is_caption", "value", b"value"] - ) -> None: ... - -global___PresetSetting = PresetSetting +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf message received from camera containing preset status +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumFlatMode: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumFlatModeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumFlatMode.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + FLAT_MODE_UNKNOWN: _EnumFlatMode.ValueType + FLAT_MODE_PLAYBACK: _EnumFlatMode.ValueType + FLAT_MODE_SETUP: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO: _EnumFlatMode.ValueType + FLAT_MODE_TIME_LAPSE_VIDEO: _EnumFlatMode.ValueType + FLAT_MODE_LOOPING: _EnumFlatMode.ValueType + FLAT_MODE_PHOTO_SINGLE: _EnumFlatMode.ValueType + FLAT_MODE_PHOTO: _EnumFlatMode.ValueType + FLAT_MODE_PHOTO_NIGHT: _EnumFlatMode.ValueType + FLAT_MODE_PHOTO_BURST: _EnumFlatMode.ValueType + FLAT_MODE_TIME_LAPSE_PHOTO: _EnumFlatMode.ValueType + FLAT_MODE_NIGHT_LAPSE_PHOTO: _EnumFlatMode.ValueType + FLAT_MODE_BROADCAST_RECORD: _EnumFlatMode.ValueType + FLAT_MODE_BROADCAST_BROADCAST: _EnumFlatMode.ValueType + FLAT_MODE_TIME_WARP_VIDEO: _EnumFlatMode.ValueType + FLAT_MODE_LIVE_BURST: _EnumFlatMode.ValueType + FLAT_MODE_NIGHT_LAPSE_VIDEO: _EnumFlatMode.ValueType + FLAT_MODE_SLOMO: _EnumFlatMode.ValueType + FLAT_MODE_IDLE: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO_STAR_TRAIL: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO_LIGHT_PAINTING: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO_LIGHT_TRAIL: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO_BURST_SLOMO: _EnumFlatMode.ValueType + +class EnumFlatMode(_EnumFlatMode, metaclass=_EnumFlatModeEnumTypeWrapper): ... + +FLAT_MODE_UNKNOWN: EnumFlatMode.ValueType +FLAT_MODE_PLAYBACK: EnumFlatMode.ValueType +FLAT_MODE_SETUP: EnumFlatMode.ValueType +FLAT_MODE_VIDEO: EnumFlatMode.ValueType +FLAT_MODE_TIME_LAPSE_VIDEO: EnumFlatMode.ValueType +FLAT_MODE_LOOPING: EnumFlatMode.ValueType +FLAT_MODE_PHOTO_SINGLE: EnumFlatMode.ValueType +FLAT_MODE_PHOTO: EnumFlatMode.ValueType +FLAT_MODE_PHOTO_NIGHT: EnumFlatMode.ValueType +FLAT_MODE_PHOTO_BURST: EnumFlatMode.ValueType +FLAT_MODE_TIME_LAPSE_PHOTO: EnumFlatMode.ValueType +FLAT_MODE_NIGHT_LAPSE_PHOTO: EnumFlatMode.ValueType +FLAT_MODE_BROADCAST_RECORD: EnumFlatMode.ValueType +FLAT_MODE_BROADCAST_BROADCAST: EnumFlatMode.ValueType +FLAT_MODE_TIME_WARP_VIDEO: EnumFlatMode.ValueType +FLAT_MODE_LIVE_BURST: EnumFlatMode.ValueType +FLAT_MODE_NIGHT_LAPSE_VIDEO: EnumFlatMode.ValueType +FLAT_MODE_SLOMO: EnumFlatMode.ValueType +FLAT_MODE_IDLE: EnumFlatMode.ValueType +FLAT_MODE_VIDEO_STAR_TRAIL: EnumFlatMode.ValueType +FLAT_MODE_VIDEO_LIGHT_PAINTING: EnumFlatMode.ValueType +FLAT_MODE_VIDEO_LIGHT_TRAIL: EnumFlatMode.ValueType +FLAT_MODE_VIDEO_BURST_SLOMO: EnumFlatMode.ValueType +global___EnumFlatMode = EnumFlatMode + +class _EnumPresetGroup: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumPresetGroupEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetGroup.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PRESET_GROUP_ID_VIDEO: _EnumPresetGroup.ValueType + PRESET_GROUP_ID_PHOTO: _EnumPresetGroup.ValueType + PRESET_GROUP_ID_TIMELAPSE: _EnumPresetGroup.ValueType + +class EnumPresetGroup(_EnumPresetGroup, metaclass=_EnumPresetGroupEnumTypeWrapper): ... + +PRESET_GROUP_ID_VIDEO: EnumPresetGroup.ValueType +PRESET_GROUP_ID_PHOTO: EnumPresetGroup.ValueType +PRESET_GROUP_ID_TIMELAPSE: EnumPresetGroup.ValueType +global___EnumPresetGroup = EnumPresetGroup + +class _EnumPresetGroupIcon: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumPresetGroupIconEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetGroupIcon.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PRESET_GROUP_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_PHOTO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_TIMELAPSE_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_MAX_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_MAX_PHOTO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_MAX_TIMELAPSE_ICON_ID: _EnumPresetGroupIcon.ValueType + +class EnumPresetGroupIcon(_EnumPresetGroupIcon, metaclass=_EnumPresetGroupIconEnumTypeWrapper): ... + +PRESET_GROUP_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_PHOTO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_TIMELAPSE_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_MAX_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_MAX_PHOTO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_MAX_TIMELAPSE_ICON_ID: EnumPresetGroupIcon.ValueType +global___EnumPresetGroupIcon = EnumPresetGroupIcon + +class _EnumPresetIcon: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumPresetIconEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetIcon.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PRESET_ICON_VIDEO: _EnumPresetIcon.ValueType + PRESET_ICON_ACTIVITY: _EnumPresetIcon.ValueType + PRESET_ICON_CINEMATIC: _EnumPresetIcon.ValueType + PRESET_ICON_PHOTO: _EnumPresetIcon.ValueType + PRESET_ICON_LIVE_BURST: _EnumPresetIcon.ValueType + PRESET_ICON_BURST: _EnumPresetIcon.ValueType + PRESET_ICON_PHOTO_NIGHT: _EnumPresetIcon.ValueType + PRESET_ICON_TIMEWARP: _EnumPresetIcon.ValueType + PRESET_ICON_TIMELAPSE: _EnumPresetIcon.ValueType + PRESET_ICON_NIGHTLAPSE: _EnumPresetIcon.ValueType + PRESET_ICON_SNAIL: _EnumPresetIcon.ValueType + PRESET_ICON_VIDEO_2: _EnumPresetIcon.ValueType + PRESET_ICON_PHOTO_2: _EnumPresetIcon.ValueType + PRESET_ICON_PANORAMA: _EnumPresetIcon.ValueType + PRESET_ICON_BURST_2: _EnumPresetIcon.ValueType + PRESET_ICON_TIMEWARP_2: _EnumPresetIcon.ValueType + PRESET_ICON_TIMELAPSE_2: _EnumPresetIcon.ValueType + PRESET_ICON_CUSTOM: _EnumPresetIcon.ValueType + PRESET_ICON_AIR: _EnumPresetIcon.ValueType + PRESET_ICON_BIKE: _EnumPresetIcon.ValueType + PRESET_ICON_EPIC: _EnumPresetIcon.ValueType + PRESET_ICON_INDOOR: _EnumPresetIcon.ValueType + PRESET_ICON_MOTOR: _EnumPresetIcon.ValueType + PRESET_ICON_MOUNTED: _EnumPresetIcon.ValueType + PRESET_ICON_OUTDOOR: _EnumPresetIcon.ValueType + PRESET_ICON_POV: _EnumPresetIcon.ValueType + PRESET_ICON_SELFIE: _EnumPresetIcon.ValueType + PRESET_ICON_SKATE: _EnumPresetIcon.ValueType + PRESET_ICON_SNOW: _EnumPresetIcon.ValueType + PRESET_ICON_TRAIL: _EnumPresetIcon.ValueType + PRESET_ICON_TRAVEL: _EnumPresetIcon.ValueType + PRESET_ICON_WATER: _EnumPresetIcon.ValueType + PRESET_ICON_LOOPING: _EnumPresetIcon.ValueType + PRESET_ICON_STARS: _EnumPresetIcon.ValueType + PRESET_ICON_ACTION: _EnumPresetIcon.ValueType + PRESET_ICON_FOLLOW_CAM: _EnumPresetIcon.ValueType + PRESET_ICON_SURF: _EnumPresetIcon.ValueType + PRESET_ICON_CITY: _EnumPresetIcon.ValueType + PRESET_ICON_SHAKY: _EnumPresetIcon.ValueType + PRESET_ICON_CHESTY: _EnumPresetIcon.ValueType + PRESET_ICON_HELMET: _EnumPresetIcon.ValueType + PRESET_ICON_BITE: _EnumPresetIcon.ValueType + PRESET_ICON_BASIC: _EnumPresetIcon.ValueType + PRESET_ICON_ULTRA_SLO_MO: _EnumPresetIcon.ValueType + PRESET_ICON_STANDARD_ENDURANCE: _EnumPresetIcon.ValueType + PRESET_ICON_ACTIVITY_ENDURANCE: _EnumPresetIcon.ValueType + PRESET_ICON_CINEMATIC_ENDURANCE: _EnumPresetIcon.ValueType + PRESET_ICON_SLOMO_ENDURANCE: _EnumPresetIcon.ValueType + PRESET_ICON_STATIONARY_1: _EnumPresetIcon.ValueType + PRESET_ICON_STATIONARY_2: _EnumPresetIcon.ValueType + PRESET_ICON_STATIONARY_3: _EnumPresetIcon.ValueType + PRESET_ICON_STATIONARY_4: _EnumPresetIcon.ValueType + PRESET_ICON_SIMPLE_SUPER_PHOTO: _EnumPresetIcon.ValueType + PRESET_ICON_SIMPLE_NIGHT_PHOTO: _EnumPresetIcon.ValueType + PRESET_ICON_HIGHEST_QUALITY_VIDEO: _EnumPresetIcon.ValueType + PRESET_ICON_STANDARD_QUALITY_VIDEO: _EnumPresetIcon.ValueType + PRESET_ICON_BASIC_QUALITY_VIDEO: _EnumPresetIcon.ValueType + PRESET_ICON_STAR_TRAIL: _EnumPresetIcon.ValueType + PRESET_ICON_LIGHT_PAINTING: _EnumPresetIcon.ValueType + PRESET_ICON_LIGHT_TRAIL: _EnumPresetIcon.ValueType + PRESET_ICON_FULL_FRAME: _EnumPresetIcon.ValueType + PRESET_ICON_TIMELAPSE_PHOTO: _EnumPresetIcon.ValueType + PRESET_ICON_NIGHTLAPSE_PHOTO: _EnumPresetIcon.ValueType + +class EnumPresetIcon(_EnumPresetIcon, metaclass=_EnumPresetIconEnumTypeWrapper): ... + +PRESET_ICON_VIDEO: EnumPresetIcon.ValueType +PRESET_ICON_ACTIVITY: EnumPresetIcon.ValueType +PRESET_ICON_CINEMATIC: EnumPresetIcon.ValueType +PRESET_ICON_PHOTO: EnumPresetIcon.ValueType +PRESET_ICON_LIVE_BURST: EnumPresetIcon.ValueType +PRESET_ICON_BURST: EnumPresetIcon.ValueType +PRESET_ICON_PHOTO_NIGHT: EnumPresetIcon.ValueType +PRESET_ICON_TIMEWARP: EnumPresetIcon.ValueType +PRESET_ICON_TIMELAPSE: EnumPresetIcon.ValueType +PRESET_ICON_NIGHTLAPSE: EnumPresetIcon.ValueType +PRESET_ICON_SNAIL: EnumPresetIcon.ValueType +PRESET_ICON_VIDEO_2: EnumPresetIcon.ValueType +PRESET_ICON_PHOTO_2: EnumPresetIcon.ValueType +PRESET_ICON_PANORAMA: EnumPresetIcon.ValueType +PRESET_ICON_BURST_2: EnumPresetIcon.ValueType +PRESET_ICON_TIMEWARP_2: EnumPresetIcon.ValueType +PRESET_ICON_TIMELAPSE_2: EnumPresetIcon.ValueType +PRESET_ICON_CUSTOM: EnumPresetIcon.ValueType +PRESET_ICON_AIR: EnumPresetIcon.ValueType +PRESET_ICON_BIKE: EnumPresetIcon.ValueType +PRESET_ICON_EPIC: EnumPresetIcon.ValueType +PRESET_ICON_INDOOR: EnumPresetIcon.ValueType +PRESET_ICON_MOTOR: EnumPresetIcon.ValueType +PRESET_ICON_MOUNTED: EnumPresetIcon.ValueType +PRESET_ICON_OUTDOOR: EnumPresetIcon.ValueType +PRESET_ICON_POV: EnumPresetIcon.ValueType +PRESET_ICON_SELFIE: EnumPresetIcon.ValueType +PRESET_ICON_SKATE: EnumPresetIcon.ValueType +PRESET_ICON_SNOW: EnumPresetIcon.ValueType +PRESET_ICON_TRAIL: EnumPresetIcon.ValueType +PRESET_ICON_TRAVEL: EnumPresetIcon.ValueType +PRESET_ICON_WATER: EnumPresetIcon.ValueType +PRESET_ICON_LOOPING: EnumPresetIcon.ValueType +PRESET_ICON_STARS: EnumPresetIcon.ValueType +PRESET_ICON_ACTION: EnumPresetIcon.ValueType +PRESET_ICON_FOLLOW_CAM: EnumPresetIcon.ValueType +PRESET_ICON_SURF: EnumPresetIcon.ValueType +PRESET_ICON_CITY: EnumPresetIcon.ValueType +PRESET_ICON_SHAKY: EnumPresetIcon.ValueType +PRESET_ICON_CHESTY: EnumPresetIcon.ValueType +PRESET_ICON_HELMET: EnumPresetIcon.ValueType +PRESET_ICON_BITE: EnumPresetIcon.ValueType +PRESET_ICON_BASIC: EnumPresetIcon.ValueType +PRESET_ICON_ULTRA_SLO_MO: EnumPresetIcon.ValueType +PRESET_ICON_STANDARD_ENDURANCE: EnumPresetIcon.ValueType +PRESET_ICON_ACTIVITY_ENDURANCE: EnumPresetIcon.ValueType +PRESET_ICON_CINEMATIC_ENDURANCE: EnumPresetIcon.ValueType +PRESET_ICON_SLOMO_ENDURANCE: EnumPresetIcon.ValueType +PRESET_ICON_STATIONARY_1: EnumPresetIcon.ValueType +PRESET_ICON_STATIONARY_2: EnumPresetIcon.ValueType +PRESET_ICON_STATIONARY_3: EnumPresetIcon.ValueType +PRESET_ICON_STATIONARY_4: EnumPresetIcon.ValueType +PRESET_ICON_SIMPLE_SUPER_PHOTO: EnumPresetIcon.ValueType +PRESET_ICON_SIMPLE_NIGHT_PHOTO: EnumPresetIcon.ValueType +PRESET_ICON_HIGHEST_QUALITY_VIDEO: EnumPresetIcon.ValueType +PRESET_ICON_STANDARD_QUALITY_VIDEO: EnumPresetIcon.ValueType +PRESET_ICON_BASIC_QUALITY_VIDEO: EnumPresetIcon.ValueType +PRESET_ICON_STAR_TRAIL: EnumPresetIcon.ValueType +PRESET_ICON_LIGHT_PAINTING: EnumPresetIcon.ValueType +PRESET_ICON_LIGHT_TRAIL: EnumPresetIcon.ValueType +PRESET_ICON_FULL_FRAME: EnumPresetIcon.ValueType +PRESET_ICON_TIMELAPSE_PHOTO: EnumPresetIcon.ValueType +PRESET_ICON_NIGHTLAPSE_PHOTO: EnumPresetIcon.ValueType +global___EnumPresetIcon = EnumPresetIcon + +class _EnumPresetTitle: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumPresetTitleEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetTitle.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PRESET_TITLE_ACTIVITY: _EnumPresetTitle.ValueType + PRESET_TITLE_STANDARD: _EnumPresetTitle.ValueType + PRESET_TITLE_CINEMATIC: _EnumPresetTitle.ValueType + PRESET_TITLE_PHOTO: _EnumPresetTitle.ValueType + PRESET_TITLE_LIVE_BURST: _EnumPresetTitle.ValueType + PRESET_TITLE_BURST: _EnumPresetTitle.ValueType + PRESET_TITLE_NIGHT: _EnumPresetTitle.ValueType + PRESET_TITLE_TIME_WARP: _EnumPresetTitle.ValueType + PRESET_TITLE_TIME_LAPSE: _EnumPresetTitle.ValueType + PRESET_TITLE_NIGHT_LAPSE: _EnumPresetTitle.ValueType + PRESET_TITLE_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_SLOMO: _EnumPresetTitle.ValueType + PRESET_TITLE_PHOTO_2: _EnumPresetTitle.ValueType + PRESET_TITLE_PANORAMA: _EnumPresetTitle.ValueType + PRESET_TITLE_TIME_WARP_2: _EnumPresetTitle.ValueType + PRESET_TITLE_CUSTOM: _EnumPresetTitle.ValueType + PRESET_TITLE_AIR: _EnumPresetTitle.ValueType + PRESET_TITLE_BIKE: _EnumPresetTitle.ValueType + PRESET_TITLE_EPIC: _EnumPresetTitle.ValueType + PRESET_TITLE_INDOOR: _EnumPresetTitle.ValueType + PRESET_TITLE_MOTOR: _EnumPresetTitle.ValueType + PRESET_TITLE_MOUNTED: _EnumPresetTitle.ValueType + PRESET_TITLE_OUTDOOR: _EnumPresetTitle.ValueType + PRESET_TITLE_POV: _EnumPresetTitle.ValueType + PRESET_TITLE_SELFIE: _EnumPresetTitle.ValueType + PRESET_TITLE_SKATE: _EnumPresetTitle.ValueType + PRESET_TITLE_SNOW: _EnumPresetTitle.ValueType + PRESET_TITLE_TRAIL: _EnumPresetTitle.ValueType + PRESET_TITLE_TRAVEL: _EnumPresetTitle.ValueType + PRESET_TITLE_WATER: _EnumPresetTitle.ValueType + PRESET_TITLE_LOOPING: _EnumPresetTitle.ValueType + PRESET_TITLE_STARS: _EnumPresetTitle.ValueType + PRESET_TITLE_ACTION: _EnumPresetTitle.ValueType + PRESET_TITLE_FOLLOW_CAM: _EnumPresetTitle.ValueType + PRESET_TITLE_SURF: _EnumPresetTitle.ValueType + PRESET_TITLE_CITY: _EnumPresetTitle.ValueType + PRESET_TITLE_SHAKY: _EnumPresetTitle.ValueType + PRESET_TITLE_CHESTY: _EnumPresetTitle.ValueType + PRESET_TITLE_HELMET: _EnumPresetTitle.ValueType + PRESET_TITLE_BITE: _EnumPresetTitle.ValueType + PRESET_TITLE_BASIC: _EnumPresetTitle.ValueType + PRESET_TITLE_ULTRA_SLO_MO: _EnumPresetTitle.ValueType + PRESET_TITLE_STANDARD_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_ACTIVITY_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_CINEMATIC_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_SLOMO_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_STATIONARY_1: _EnumPresetTitle.ValueType + PRESET_TITLE_STATIONARY_2: _EnumPresetTitle.ValueType + PRESET_TITLE_STATIONARY_3: _EnumPresetTitle.ValueType + PRESET_TITLE_STATIONARY_4: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_TIME_WARP: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_SUPER_PHOTO: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_NIGHT_PHOTO: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_HIGHEST_QUALITY: _EnumPresetTitle.ValueType + PRESET_TITLE_EXTENDED_BATTERY: _EnumPresetTitle.ValueType + PRESET_TITLE_LONGEST_BATTERY: _EnumPresetTitle.ValueType + PRESET_TITLE_STAR_TRAIL: _EnumPresetTitle.ValueType + PRESET_TITLE_LIGHT_PAINTING: _EnumPresetTitle.ValueType + PRESET_TITLE_LIGHT_TRAIL: _EnumPresetTitle.ValueType + PRESET_TITLE_FULL_FRAME: _EnumPresetTitle.ValueType + PRESET_TITLE_STANDARD_QUALITY_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_BASIC_QUALITY_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_HIGHEST_QUALITY_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_USER_DEFINED_CUSTOM_NAME: _EnumPresetTitle.ValueType + +class EnumPresetTitle(_EnumPresetTitle, metaclass=_EnumPresetTitleEnumTypeWrapper): ... + +PRESET_TITLE_ACTIVITY: EnumPresetTitle.ValueType +PRESET_TITLE_STANDARD: EnumPresetTitle.ValueType +PRESET_TITLE_CINEMATIC: EnumPresetTitle.ValueType +PRESET_TITLE_PHOTO: EnumPresetTitle.ValueType +PRESET_TITLE_LIVE_BURST: EnumPresetTitle.ValueType +PRESET_TITLE_BURST: EnumPresetTitle.ValueType +PRESET_TITLE_NIGHT: EnumPresetTitle.ValueType +PRESET_TITLE_TIME_WARP: EnumPresetTitle.ValueType +PRESET_TITLE_TIME_LAPSE: EnumPresetTitle.ValueType +PRESET_TITLE_NIGHT_LAPSE: EnumPresetTitle.ValueType +PRESET_TITLE_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_SLOMO: EnumPresetTitle.ValueType +PRESET_TITLE_PHOTO_2: EnumPresetTitle.ValueType +PRESET_TITLE_PANORAMA: EnumPresetTitle.ValueType +PRESET_TITLE_TIME_WARP_2: EnumPresetTitle.ValueType +PRESET_TITLE_CUSTOM: EnumPresetTitle.ValueType +PRESET_TITLE_AIR: EnumPresetTitle.ValueType +PRESET_TITLE_BIKE: EnumPresetTitle.ValueType +PRESET_TITLE_EPIC: EnumPresetTitle.ValueType +PRESET_TITLE_INDOOR: EnumPresetTitle.ValueType +PRESET_TITLE_MOTOR: EnumPresetTitle.ValueType +PRESET_TITLE_MOUNTED: EnumPresetTitle.ValueType +PRESET_TITLE_OUTDOOR: EnumPresetTitle.ValueType +PRESET_TITLE_POV: EnumPresetTitle.ValueType +PRESET_TITLE_SELFIE: EnumPresetTitle.ValueType +PRESET_TITLE_SKATE: EnumPresetTitle.ValueType +PRESET_TITLE_SNOW: EnumPresetTitle.ValueType +PRESET_TITLE_TRAIL: EnumPresetTitle.ValueType +PRESET_TITLE_TRAVEL: EnumPresetTitle.ValueType +PRESET_TITLE_WATER: EnumPresetTitle.ValueType +PRESET_TITLE_LOOPING: EnumPresetTitle.ValueType +PRESET_TITLE_STARS: EnumPresetTitle.ValueType +PRESET_TITLE_ACTION: EnumPresetTitle.ValueType +PRESET_TITLE_FOLLOW_CAM: EnumPresetTitle.ValueType +PRESET_TITLE_SURF: EnumPresetTitle.ValueType +PRESET_TITLE_CITY: EnumPresetTitle.ValueType +PRESET_TITLE_SHAKY: EnumPresetTitle.ValueType +PRESET_TITLE_CHESTY: EnumPresetTitle.ValueType +PRESET_TITLE_HELMET: EnumPresetTitle.ValueType +PRESET_TITLE_BITE: EnumPresetTitle.ValueType +PRESET_TITLE_BASIC: EnumPresetTitle.ValueType +PRESET_TITLE_ULTRA_SLO_MO: EnumPresetTitle.ValueType +PRESET_TITLE_STANDARD_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_ACTIVITY_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_CINEMATIC_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_SLOMO_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_STATIONARY_1: EnumPresetTitle.ValueType +PRESET_TITLE_STATIONARY_2: EnumPresetTitle.ValueType +PRESET_TITLE_STATIONARY_3: EnumPresetTitle.ValueType +PRESET_TITLE_STATIONARY_4: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_TIME_WARP: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_SUPER_PHOTO: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_NIGHT_PHOTO: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_HIGHEST_QUALITY: EnumPresetTitle.ValueType +PRESET_TITLE_EXTENDED_BATTERY: EnumPresetTitle.ValueType +PRESET_TITLE_LONGEST_BATTERY: EnumPresetTitle.ValueType +PRESET_TITLE_STAR_TRAIL: EnumPresetTitle.ValueType +PRESET_TITLE_LIGHT_PAINTING: EnumPresetTitle.ValueType +PRESET_TITLE_LIGHT_TRAIL: EnumPresetTitle.ValueType +PRESET_TITLE_FULL_FRAME: EnumPresetTitle.ValueType +PRESET_TITLE_STANDARD_QUALITY_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_BASIC_QUALITY_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_HIGHEST_QUALITY_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_USER_DEFINED_CUSTOM_NAME: EnumPresetTitle.ValueType +global___EnumPresetTitle = EnumPresetTitle + +@typing_extensions.final +class NotifyPresetStatus(google.protobuf.message.Message): + """* + Current Preset status + + Sent either: + + - Synchronously via initial response to @ref RequestGetPresetStatus + - Asynchronously when Preset change if registered in @ref RequestGetPresetStatus + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + PRESET_GROUP_ARRAY_FIELD_NUMBER: builtins.int + + @property + def preset_group_array( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___PresetGroup]: + """List of currently available Preset Groups""" + 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: ... + +global___NotifyPresetStatus = NotifyPresetStatus + +@typing_extensions.final +class Preset(google.protobuf.message.Message): + """* + An individual preset. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + ID_FIELD_NUMBER: builtins.int + MODE_FIELD_NUMBER: builtins.int + TITLE_ID_FIELD_NUMBER: builtins.int + TITLE_NUMBER_FIELD_NUMBER: builtins.int + USER_DEFINED_FIELD_NUMBER: builtins.int + ICON_FIELD_NUMBER: builtins.int + SETTING_ARRAY_FIELD_NUMBER: builtins.int + IS_MODIFIED_FIELD_NUMBER: builtins.int + IS_FIXED_FIELD_NUMBER: builtins.int + CUSTOM_NAME_FIELD_NUMBER: builtins.int + id: builtins.int + "Preset ID" + mode: global___EnumFlatMode.ValueType + "Preset flatmode ID" + title_id: global___EnumPresetTitle.ValueType + "Preset Title ID" + title_number: builtins.int + "Preset Title Number (e.g. 1/2/3 in Custom1, Custom2, Custom3)" + user_defined: builtins.bool + "Is the Preset custom/user-defined?" + icon: global___EnumPresetIcon.ValueType + "Preset Icon ID" + + @property + def setting_array( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___PresetSetting]: + """Array of settings associated with this Preset""" + is_modified: builtins.bool + "Has Preset been modified from factory defaults? (False for user-defined Presets)" + is_fixed: builtins.bool + "Is this Preset mutable?" + custom_name: builtins.str + "Custom string name given to this preset via @ref RequestCustomPresetUpdate" + + def __init__( + self, + *, + id: builtins.int | None = ..., + mode: global___EnumFlatMode.ValueType | None = ..., + title_id: global___EnumPresetTitle.ValueType | None = ..., + title_number: builtins.int | None = ..., + user_defined: builtins.bool | None = ..., + icon: global___EnumPresetIcon.ValueType | None = ..., + setting_array: collections.abc.Iterable[global___PresetSetting] | None = ..., + is_modified: builtins.bool | None = ..., + is_fixed: builtins.bool | None = ..., + custom_name: builtins.str | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "custom_name", + b"custom_name", + "icon", + b"icon", + "id", + b"id", + "is_fixed", + b"is_fixed", + "is_modified", + b"is_modified", + "mode", + b"mode", + "title_id", + b"title_id", + "title_number", + b"title_number", + "user_defined", + b"user_defined", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "custom_name", + b"custom_name", + "icon", + b"icon", + "id", + b"id", + "is_fixed", + b"is_fixed", + "is_modified", + b"is_modified", + "mode", + b"mode", + "setting_array", + b"setting_array", + "title_id", + b"title_id", + "title_number", + b"title_number", + "user_defined", + b"user_defined", + ], + ) -> None: ... + +global___Preset = Preset + +@typing_extensions.final +class RequestCustomPresetUpdate(google.protobuf.message.Message): + """* + Request to Update the Title and / or Icon of the Active Custom Preset + + This only operates on the currently active Preset and will fail if the current + Preset is not custom. + + The use cases are: + + 1. Update the Custom Preset Icon + + - `icon_id` is always optional and can always be passed + + and / or + + 2. Update the Custom Preset Title to a... + + - **Factory Preset Title**: Set `title_id` to a non-PRESET_TITLE_USER_DEFINED_CUSTOM_NAME (94) value + - **Custom Preset Name**: Set `title_id` to PRESET_TITLE_USER_DEFINED_CUSTOM_NAME (94) and specify a `custom_name` + + Returns a @ref ResponseGeneric with the status of the preset update request. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TITLE_ID_FIELD_NUMBER: builtins.int + CUSTOM_NAME_FIELD_NUMBER: builtins.int + ICON_ID_FIELD_NUMBER: builtins.int + title_id: global___EnumPresetTitle.ValueType + "*\n Preset Title ID\n\n The range of acceptable custom title ID's can be found in the initial @ref NotifyPresetStatus response\n to @ref RequestGetPresetStatus\n " + custom_name: builtins.str + "*\n UTF-8 encoded custom preset name\n\n The name must obey the following:\n\n - Custom titles must be between 1 and 16 characters (inclusive)\n - No special characters outside of the following languages: English, French, Italian, German,\n Spanish, Portuguese, Swedish, Russian\n " + icon_id: global___EnumPresetIcon.ValueType + "*\n Preset Icon ID\n\n The range of acceptable custom icon ID's can be found in the initial @ref NotifyPresetStatus response to\n @ref RequestGetPresetStatus\n " + + def __init__( + self, + *, + title_id: global___EnumPresetTitle.ValueType | None = ..., + custom_name: builtins.str | None = ..., + icon_id: global___EnumPresetIcon.ValueType | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "custom_name", + b"custom_name", + "icon_id", + b"icon_id", + "title_id", + b"title_id", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "custom_name", + b"custom_name", + "icon_id", + b"icon_id", + "title_id", + b"title_id", + ], + ) -> None: ... + +global___RequestCustomPresetUpdate = RequestCustomPresetUpdate + +@typing_extensions.final +class PresetGroup(google.protobuf.message.Message): + """ + Preset Group meta information and contained Presets + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + ID_FIELD_NUMBER: builtins.int + PRESET_ARRAY_FIELD_NUMBER: builtins.int + CAN_ADD_PRESET_FIELD_NUMBER: builtins.int + ICON_FIELD_NUMBER: builtins.int + id: global___EnumPresetGroup.ValueType + "Preset Group ID" + + @property + def preset_array( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Preset]: + """Array of Presets contained in this Preset Group""" + can_add_preset: builtins.bool + "Is there room in the group to add additional Presets?" + icon: global___EnumPresetGroupIcon.ValueType + "The icon to display for this preset group" + + def __init__( + self, + *, + id: global___EnumPresetGroup.ValueType | None = ..., + preset_array: collections.abc.Iterable[global___Preset] | None = ..., + can_add_preset: builtins.bool | None = ..., + icon: global___EnumPresetGroupIcon.ValueType | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["can_add_preset", b"can_add_preset", "icon", b"icon", "id", b"id"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "can_add_preset", + b"can_add_preset", + "icon", + b"icon", + "id", + b"id", + "preset_array", + b"preset_array", + ], + ) -> None: ... + +global___PresetGroup = PresetGroup + +@typing_extensions.final +class PresetSetting(google.protobuf.message.Message): + """* + Setting representation that comprises a @ref Preset + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + ID_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + IS_CAPTION_FIELD_NUMBER: builtins.int + id: builtins.int + "Setting ID" + value: builtins.int + "Setting value" + is_caption: builtins.bool + '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 = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["id", b"id", "is_caption", b"is_caption", "value", b"value"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["id", b"id", "is_caption", b"is_caption", "value", b"value"], + ) -> None: ... + +global___PresetSetting = PresetSetting 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 318a73d8..91f1f5b8 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,21 +1,22 @@ # 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 Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:47 UTC 2024 -"""Generated protocol buffer code.""" -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( - b'\n\x1frequest_get_preset_status.proto\x12\nopen_gopro"\xa6\x01\n\x16RequestGetPresetStatus\x12D\n\x16register_preset_status\x18\x01 \x03(\x0e2$.open_gopro.EnumRegisterPresetStatus\x12F\n\x18unregister_preset_status\x18\x02 \x03(\x0e2$.open_gopro.EnumRegisterPresetStatus*l\n\x18EnumRegisterPresetStatus\x12!\n\x1dREGISTER_PRESET_STATUS_PRESET\x10\x01\x12-\n)REGISTER_PRESET_STATUS_PRESET_GROUP_ARRAY\x10\x02' -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "request_get_preset_status_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _ENUMREGISTERPRESETSTATUS._serialized_start = 216 - _ENUMREGISTERPRESETSTATUS._serialized_end = 324 - _REQUESTGETPRESETSTATUS._serialized_start = 48 - _REQUESTGETPRESETSTATUS._serialized_end = 214 +"""Generated protocol buffer code.""" + +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( + b'\n\x1frequest_get_preset_status.proto\x12\nopen_gopro"\xa6\x01\n\x16RequestGetPresetStatus\x12D\n\x16register_preset_status\x18\x01 \x03(\x0e2$.open_gopro.EnumRegisterPresetStatus\x12F\n\x18unregister_preset_status\x18\x02 \x03(\x0e2$.open_gopro.EnumRegisterPresetStatus*l\n\x18EnumRegisterPresetStatus\x12!\n\x1dREGISTER_PRESET_STATUS_PRESET\x10\x01\x12-\n)REGISTER_PRESET_STATUS_PRESET_GROUP_ARRAY\x10\x02' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "request_get_preset_status_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMREGISTERPRESETSTATUS._serialized_start = 216 + _ENUMREGISTERPRESETSTATUS._serialized_end = 324 + _REQUESTGETPRESETSTATUS._serialized_start = 48 + _REQUESTGETPRESETSTATUS._serialized_end = 214 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 8a5d9f05..ef0a0092 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 @@ -1,79 +1,91 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Defines the structure of protobuf messages for obtaining preset status -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _EnumRegisterPresetStatus: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumRegisterPresetStatusEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumRegisterPresetStatus.ValueType], builtins.type -): - 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): ... - -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): - """* - Get preset status (and optionally register to be notified when it changes) - - Response: @ref NotifyPresetStatus sent immediately - - Notification: @ref NotifyPresetStatus sent periodically as preset status changes, if registered. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - REGISTER_PRESET_STATUS_FIELD_NUMBER: builtins.int - UNREGISTER_PRESET_STATUS_FIELD_NUMBER: builtins.int - - @property - def register_preset_status( - self, - ) -> 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]: - """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 = ... - ) -> None: ... - def ClearField( - self, - field_name: typing_extensions.Literal[ - "register_preset_status", b"register_preset_status", "unregister_preset_status", b"unregister_preset_status" - ], - ) -> None: ... - -global___RequestGetPresetStatus = RequestGetPresetStatus +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for obtaining preset status +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumRegisterPresetStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumRegisterPresetStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumRegisterPresetStatus.ValueType], + builtins.type, +): + 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): ... + +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 + +@typing_extensions.final +class RequestGetPresetStatus(google.protobuf.message.Message): + """* + Get the set of currently available presets and optionally register to be notified when it changes. + + Response: @ref NotifyPresetStatus sent immediately + + Notification: @ref NotifyPresetStatus sent periodically as preset status changes, if registered. + + The preset status changes when: + + - A client changes one of a preset's captioned settings via the API + - The user exits from a preset's settings UI on the camera (e.g. long-press the preset pill and then press the back arrow) + - The user creates/deletes/reorders a preset within a group + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + REGISTER_PRESET_STATUS_FIELD_NUMBER: builtins.int + UNREGISTER_PRESET_STATUS_FIELD_NUMBER: builtins.int + + @property + def register_preset_status( + self, + ) -> 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]: + """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) = ... + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "register_preset_status", + b"register_preset_status", + "unregister_preset_status", + b"unregister_preset_status", + ], + ) -> None: ... + +global___RequestGetPresetStatus = RequestGetPresetStatus 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 b2e9890d..fd295a47 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,23 +1,24 @@ # 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 Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:48 UTC 2024 -"""Generated protocol buffer code.""" -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( - b'\n\x16response_generic.proto\x12\nopen_gopro"@\n\x0fResponseGeneric\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric"%\n\x05Media\x12\x0e\n\x06folder\x18\x01 \x01(\t\x12\x0c\n\x04file\x18\x02 \x01(\t*\xcf\x01\n\x11EnumResultGeneric\x12\x12\n\x0eRESULT_UNKNOWN\x10\x00\x12\x12\n\x0eRESULT_SUCCESS\x10\x01\x12\x15\n\x11RESULT_ILL_FORMED\x10\x02\x12\x18\n\x14RESULT_NOT_SUPPORTED\x10\x03\x12!\n\x1dRESULT_ARGUMENT_OUT_OF_BOUNDS\x10\x04\x12\x1b\n\x17RESULT_ARGUMENT_INVALID\x10\x05\x12!\n\x1dRESULT_RESOURCE_NOT_AVAILABLE\x10\x06' -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "response_generic_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _ENUMRESULTGENERIC._serialized_start = 144 - _ENUMRESULTGENERIC._serialized_end = 351 - _RESPONSEGENERIC._serialized_start = 38 - _RESPONSEGENERIC._serialized_end = 102 - _MEDIA._serialized_start = 104 - _MEDIA._serialized_end = 141 +"""Generated protocol buffer code.""" + +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( + b'\n\x16response_generic.proto\x12\nopen_gopro"@\n\x0fResponseGeneric\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric"%\n\x05Media\x12\x0e\n\x06folder\x18\x01 \x01(\t\x12\x0c\n\x04file\x18\x02 \x01(\t*\xcf\x01\n\x11EnumResultGeneric\x12\x12\n\x0eRESULT_UNKNOWN\x10\x00\x12\x12\n\x0eRESULT_SUCCESS\x10\x01\x12\x15\n\x11RESULT_ILL_FORMED\x10\x02\x12\x18\n\x14RESULT_NOT_SUPPORTED\x10\x03\x12!\n\x1dRESULT_ARGUMENT_OUT_OF_BOUNDS\x10\x04\x12\x1b\n\x17RESULT_ARGUMENT_INVALID\x10\x05\x12!\n\x1dRESULT_RESOURCE_NOT_AVAILABLE\x10\x06' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "response_generic_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMRESULTGENERIC._serialized_start = 144 + _ENUMRESULTGENERIC._serialized_end = 351 + _RESPONSEGENERIC._serialized_start = 38 + _RESPONSEGENERIC._serialized_end = 102 + _MEDIA._serialized_start = 104 + _MEDIA._serialized_end = 141 diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/response_generic_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/response_generic_pb2.pyi index eed7c045..85655c36 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/response_generic_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/response_generic_pb2.pyi @@ -1,84 +1,90 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Defines the structure of protobuf message containing generic response to a command -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _EnumResultGeneric: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumResultGenericEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumResultGeneric.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - RESULT_UNKNOWN: _EnumResultGeneric.ValueType - RESULT_SUCCESS: _EnumResultGeneric.ValueType - RESULT_ILL_FORMED: _EnumResultGeneric.ValueType - RESULT_NOT_SUPPORTED: _EnumResultGeneric.ValueType - RESULT_ARGUMENT_OUT_OF_BOUNDS: _EnumResultGeneric.ValueType - RESULT_ARGUMENT_INVALID: _EnumResultGeneric.ValueType - RESULT_RESOURCE_NOT_AVAILABLE: _EnumResultGeneric.ValueType - -class EnumResultGeneric(_EnumResultGeneric, metaclass=_EnumResultGenericEnumTypeWrapper): ... - -RESULT_UNKNOWN: EnumResultGeneric.ValueType -RESULT_SUCCESS: EnumResultGeneric.ValueType -RESULT_ILL_FORMED: EnumResultGeneric.ValueType -RESULT_NOT_SUPPORTED: EnumResultGeneric.ValueType -RESULT_ARGUMENT_OUT_OF_BOUNDS: EnumResultGeneric.ValueType -RESULT_ARGUMENT_INVALID: EnumResultGeneric.ValueType -RESULT_RESOURCE_NOT_AVAILABLE: EnumResultGeneric.ValueType -global___EnumResultGeneric = EnumResultGeneric - -class ResponseGeneric(google.protobuf.message.Message): - """ - Generic Response used across most response / notification messages - - @ref EnumResultGeneric - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - RESULT_FIELD_NUMBER: builtins.int - result: global___EnumResultGeneric.ValueType - "Generic pass/fail/error info" - - def __init__(self, *, result: global___EnumResultGeneric.ValueType | None = ...) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["result", b"result"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["result", b"result"]) -> None: ... - -global___ResponseGeneric = ResponseGeneric - -class Media(google.protobuf.message.Message): - """* - A reusable model to represent a media file - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - FOLDER_FIELD_NUMBER: builtins.int - FILE_FIELD_NUMBER: builtins.int - folder: builtins.str - "Directory that the media is contained in" - file: builtins.str - "Filename of media" - - def __init__(self, *, folder: builtins.str | None = ..., file: builtins.str | None = ...) -> None: ... - def HasField( - self, field_name: typing_extensions.Literal["file", b"file", "folder", b"folder"] - ) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["file", b"file", "folder", b"folder"]) -> None: ... - -global___Media = Media +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf message containing generic response to a command +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumResultGeneric: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumResultGenericEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumResultGeneric.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + RESULT_UNKNOWN: _EnumResultGeneric.ValueType + RESULT_SUCCESS: _EnumResultGeneric.ValueType + RESULT_ILL_FORMED: _EnumResultGeneric.ValueType + RESULT_NOT_SUPPORTED: _EnumResultGeneric.ValueType + RESULT_ARGUMENT_OUT_OF_BOUNDS: _EnumResultGeneric.ValueType + RESULT_ARGUMENT_INVALID: _EnumResultGeneric.ValueType + RESULT_RESOURCE_NOT_AVAILABLE: _EnumResultGeneric.ValueType + +class EnumResultGeneric(_EnumResultGeneric, metaclass=_EnumResultGenericEnumTypeWrapper): ... + +RESULT_UNKNOWN: EnumResultGeneric.ValueType +RESULT_SUCCESS: EnumResultGeneric.ValueType +RESULT_ILL_FORMED: EnumResultGeneric.ValueType +RESULT_NOT_SUPPORTED: EnumResultGeneric.ValueType +RESULT_ARGUMENT_OUT_OF_BOUNDS: EnumResultGeneric.ValueType +RESULT_ARGUMENT_INVALID: EnumResultGeneric.ValueType +RESULT_RESOURCE_NOT_AVAILABLE: EnumResultGeneric.ValueType +global___EnumResultGeneric = EnumResultGeneric + +@typing_extensions.final +class ResponseGeneric(google.protobuf.message.Message): + """ + Generic Response used across many response / notification messages + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + result: global___EnumResultGeneric.ValueType + "Generic pass/fail/error info" + + def __init__(self, *, result: global___EnumResultGeneric.ValueType | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["result", b"result"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["result", b"result"]) -> None: ... + +global___ResponseGeneric = ResponseGeneric + +@typing_extensions.final +class Media(google.protobuf.message.Message): + """* + A common model to represent a media file + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + FOLDER_FIELD_NUMBER: builtins.int + FILE_FIELD_NUMBER: builtins.int + folder: builtins.str + "Directory in which the media is contained" + file: builtins.str + "Filename of media" + + def __init__(self, *, folder: builtins.str | None = ..., file: builtins.str | None = ...) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["file", b"file", "folder", b"folder"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["file", b"file", "folder", b"folder"], + ) -> None: ... + +global___Media = Media 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 ee279177..1430451c 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,21 +1,22 @@ # 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 Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:47 UTC 2024 -"""Generated protocol buffer code.""" -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( - b'\n\x1fset_camera_control_status.proto\x12\nopen_gopro"c\n\x1dRequestSetCameraControlStatus\x12B\n\x15camera_control_status\x18\x01 \x02(\x0e2#.open_gopro.EnumCameraControlStatus*[\n\x17EnumCameraControlStatus\x12\x0f\n\x0bCAMERA_IDLE\x10\x00\x12\x12\n\x0eCAMERA_CONTROL\x10\x01\x12\x1b\n\x17CAMERA_EXTERNAL_CONTROL\x10\x02' -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "set_camera_control_status_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _ENUMCAMERACONTROLSTATUS._serialized_start = 148 - _ENUMCAMERACONTROLSTATUS._serialized_end = 239 - _REQUESTSETCAMERACONTROLSTATUS._serialized_start = 47 - _REQUESTSETCAMERACONTROLSTATUS._serialized_end = 146 +"""Generated protocol buffer code.""" + +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( + b'\n\x1fset_camera_control_status.proto\x12\nopen_gopro"c\n\x1dRequestSetCameraControlStatus\x12B\n\x15camera_control_status\x18\x01 \x02(\x0e2#.open_gopro.EnumCameraControlStatus*[\n\x17EnumCameraControlStatus\x12\x0f\n\x0bCAMERA_IDLE\x10\x00\x12\x12\n\x0eCAMERA_CONTROL\x10\x01\x12\x1b\n\x17CAMERA_EXTERNAL_CONTROL\x10\x02' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "set_camera_control_status_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMCAMERACONTROLSTATUS._serialized_start = 148 + _ENUMCAMERACONTROLSTATUS._serialized_end = 239 + _REQUESTSETCAMERACONTROLSTATUS._serialized_start = 47 + _REQUESTSETCAMERACONTROLSTATUS._serialized_end = 146 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 125b2005..37b27336 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 @@ -1,61 +1,74 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Defines the structure of protobuf messages for setting camera control status -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _EnumCameraControlStatus: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _EnumCameraControlStatusEnumTypeWrapper( - google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCameraControlStatus.ValueType], builtins.type -): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - CAMERA_IDLE: _EnumCameraControlStatus.ValueType - CAMERA_CONTROL: _EnumCameraControlStatus.ValueType - "Can only be set by camera, not by app or third party" - CAMERA_EXTERNAL_CONTROL: _EnumCameraControlStatus.ValueType - -class EnumCameraControlStatus(_EnumCameraControlStatus, metaclass=_EnumCameraControlStatusEnumTypeWrapper): ... - -CAMERA_IDLE: EnumCameraControlStatus.ValueType -CAMERA_CONTROL: EnumCameraControlStatus.ValueType -"Can only be set by camera, not by app or third party" -CAMERA_EXTERNAL_CONTROL: EnumCameraControlStatus.ValueType -global___EnumCameraControlStatus = EnumCameraControlStatus - -class RequestSetCameraControlStatus(google.protobuf.message.Message): - """* - Set Camera Control Status (as part of Global Behaviors feature) - - Response: @ref ResponseGeneric - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - CAMERA_CONTROL_STATUS_FIELD_NUMBER: builtins.int - 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 HasField( - self, field_name: typing_extensions.Literal["camera_control_status", b"camera_control_status"] - ) -> builtins.bool: ... - def ClearField( - self, field_name: typing_extensions.Literal["camera_control_status", b"camera_control_status"] - ) -> None: ... - -global___RequestSetCameraControlStatus = RequestSetCameraControlStatus +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for setting camera control status +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumCameraControlStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumCameraControlStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCameraControlStatus.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + CAMERA_IDLE: _EnumCameraControlStatus.ValueType + CAMERA_CONTROL: _EnumCameraControlStatus.ValueType + "Can only be set by camera, not by app or third party" + CAMERA_EXTERNAL_CONTROL: _EnumCameraControlStatus.ValueType + +class EnumCameraControlStatus(_EnumCameraControlStatus, metaclass=_EnumCameraControlStatusEnumTypeWrapper): ... + +CAMERA_IDLE: EnumCameraControlStatus.ValueType +CAMERA_CONTROL: EnumCameraControlStatus.ValueType +"Can only be set by camera, not by app or third party" +CAMERA_EXTERNAL_CONTROL: EnumCameraControlStatus.ValueType +global___EnumCameraControlStatus = EnumCameraControlStatus + +@typing_extensions.final +class RequestSetCameraControlStatus(google.protobuf.message.Message): + """* + Set Camera Control Status (as part of Global Behaviors feature) + + This command is used to tell the camera that the app (i.e. External Control) wishes to claim control of the camera. + This causes the camera to immediately exit most contextual menus and return to the idle screen. Any interaction with + the camera's physical buttons will cause the camera to reclaim control and update control status accordingly. If the + user returns the camera UI to the idle screen, the camera updates control status to Idle. + + The entity currently claiming control of the camera is advertised in camera status 114. Information about whether the + camera is in a contextual menu or not is advertised in camera status 63. + + Response: @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + CAMERA_CONTROL_STATUS_FIELD_NUMBER: builtins.int + 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 HasField( + self, + field_name: typing_extensions.Literal["camera_control_status", b"camera_control_status"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["camera_control_status", b"camera_control_status"], + ) -> None: ... + +global___RequestSetCameraControlStatus = RequestSetCameraControlStatus 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 8ce4f006..565e1e72 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,19 +1,20 @@ # 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 Dec 18 20:40:36 UTC 2023 +# This copyright was auto-generated on Wed Mar 27 22:05:47 UTC 2024 -"""Generated protocol buffer code.""" -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( - b"\n\x14turbo_transfer.proto\x12\nopen_gopro\"'\n\x15RequestSetTurboActive\x12\x0e\n\x06active\x18\x01 \x02(\x08" -) -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "turbo_transfer_pb2", globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - DESCRIPTOR._options = None - _REQUESTSETTURBOACTIVE._serialized_start = 36 - _REQUESTSETTURBOACTIVE._serialized_end = 75 +"""Generated protocol buffer code.""" + +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( + b"\n\x14turbo_transfer.proto\x12\nopen_gopro\"'\n\x15RequestSetTurboActive\x12\x0e\n\x06active\x18\x01 \x02(\x08" +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "turbo_transfer_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _REQUESTSETTURBOACTIVE._serialized_start = 36 + _REQUESTSETTURBOACTIVE._serialized_end = 75 diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/proto/turbo_transfer_pb2.pyi b/demos/python/sdk_wireless_camera_control/open_gopro/proto/turbo_transfer_pb2.pyi index cd34eb19..0c79e66a 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/proto/turbo_transfer_pb2.pyi +++ b/demos/python/sdk_wireless_camera_control/open_gopro/proto/turbo_transfer_pb2.pyi @@ -1,34 +1,36 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -* -Defines the structure of protobuf messages for enabling and disabling Turbo Transfer feature -""" -import builtins -import google.protobuf.descriptor -import google.protobuf.message -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class RequestSetTurboActive(google.protobuf.message.Message): - """* - Enable/disable display of "Transferring Media" UI - - Response: @ref ResponseGeneric - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - ACTIVE_FIELD_NUMBER: builtins.int - active: builtins.bool - "Enable or disable Turbo Transfer feature" - - def __init__(self, *, active: builtins.bool | None = ...) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["active", b"active"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["active", b"active"]) -> None: ... - -global___RequestSetTurboActive = RequestSetTurboActive +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for enabling and disabling Turbo Transfer feature +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing_extensions.final +class RequestSetTurboActive(google.protobuf.message.Message): + """* + Enable/disable display of "Transferring Media" UI + + Response: @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + ACTIVE_FIELD_NUMBER: builtins.int + active: builtins.bool + "Enable or disable Turbo Transfer feature" + + def __init__(self, *, active: builtins.bool | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["active", b"active"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["active", b"active"]) -> None: ... + +global___RequestSetTurboActive = RequestSetTurboActive diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/types.py b/demos/python/sdk_wireless_camera_control/open_gopro/types.py index dadaa14b..97df1aae 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/types.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/types.py @@ -36,3 +36,5 @@ UpdateType = Union[SettingId, StatusId, ActionId] UpdateCb = Callable[[UpdateType, Any], Coroutine[Any, Any, None]] + +IdType = Union[SettingId, StatusId, ActionId, CmdId, BleUUID, str] 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 4a370883..1d16bb16 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/util.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/util.py @@ -98,7 +98,7 @@ def pretty_print(obj: Any, stringify_all: bool = True, should_quote: bool = True 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. + should_quote (bool): Should each element be surrounded in quotes?. Defaults to True. Returns: str: pretty-printed string @@ -301,7 +301,7 @@ async def ainput(string: str, printer: Callable = sys.stdout.write) -> str: Returns: str: Input read from console """ - await asyncio.get_event_loop().run_in_executor(None, lambda s=string: printer(s + " ")) + await asyncio.get_event_loop().run_in_executor(None, lambda s=string: printer(s + " ")) # type: ignore return await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline) @@ -319,4 +319,6 @@ def get_current_dst_aware_time() -> tuple[datetime, int, bool]: except AttributeError: is_dst = False offset = now.utcoffset().total_seconds() / 60 # type: ignore + if is_dst: + offset += 60 return (now, int(offset), is_dst) diff --git a/demos/python/sdk_wireless_camera_control/poetry.lock b/demos/python/sdk_wireless_camera_control/poetry.lock index 59bab35e..b0ec87a9 100644 --- a/demos/python/sdk_wireless_camera_control/poetry.lock +++ b/demos/python/sdk_wireless_camera_control/poetry.lock @@ -2,14 +2,14 @@ [[package]] name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" +version = "0.7.16" +description = "A light, configurable Sphinx theme" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] @@ -58,14 +58,14 @@ files = [ [[package]] name = "autodoc-pydantic" -version = "2.0.1" +version = "2.1.0" description = "Seamlessly integrate pydantic models in your Sphinx documentation." category = "dev" optional = false -python-versions = ">=3.7.1,<4.0.0" +python-versions = ">=3.8,<4.0.0" files = [ - {file = "autodoc_pydantic-2.0.1-py3-none-any.whl", hash = "sha256:d3c302fdb6d37edb5b721f0f540252fa79cea7018bc1a9a85bf70f33a68b0ce4"}, - {file = "autodoc_pydantic-2.0.1.tar.gz", hash = "sha256:7a125a4ff18e4903e27be71e4ddb3269380860eacab4a584d6cc2e212fa96991"}, + {file = "autodoc_pydantic-2.1.0-py3-none-any.whl", hash = "sha256:9f1f82ee3667589dfa08b21697be8bbd80b15110e838cd765bb1bf3ce1b0ea8f"}, + {file = "autodoc_pydantic-2.1.0.tar.gz", hash = "sha256:3cf1b973e2f5ff0fbbe9b951c11827b5e32d3409e238f7f5782359426ab8d360"}, ] [package.dependencies] @@ -81,14 +81,14 @@ test = ["coverage (>=7,<8)", "pytest (>=7,<8)"] [[package]] name = "babel" -version = "2.13.1" +version = "2.14.0" description = "Internationalization utilities" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, - {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] [package.extras] @@ -174,14 +174,14 @@ files = [ [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -451,62 +451,63 @@ files = [ [[package]] name = "dbus-fast" -version = "2.20.0" +version = "2.21.1" description = "A faster version of dbus-next" category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:ecf22e22434bdd61bfb8b544eb58f5032b23dda5a7fc233afa1d3c9c3241f0a8"}, - {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70f4c1fe23c47a59d81c8fd8830c65307a1f089cc92949004df4c65c69f155"}, - {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:9963180456586d0e1b58075e0439a34ed8e9ee4266b35f76f3db6ffc1af17e27"}, - {file = "dbus_fast-2.20.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:eafbf4f0ac86fd959f86bbdf910bf64406b35315781014ef4a1cd2bb43985346"}, - {file = "dbus_fast-2.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bb668e2039e15f0e5af14bee7de8c8c082e3b292ed2ce2ceb3168c7068ff2856"}, - {file = "dbus_fast-2.20.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3f7966f835da1d8a77c55a7336313bd97e7f722b316f760077c55c1e9533b0cd"}, - {file = "dbus_fast-2.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff856cbb1508bcf6735ed1e3c04de1def6c400720765141d2470e39c8fd6f13"}, - {file = "dbus_fast-2.20.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7a1da4ed9880046403ddedb7b941fd981872fc883dc9925bbf269b551f12120d"}, - {file = "dbus_fast-2.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9084ded47761a43b2252879c6ebaddb7e3cf89377cbdc981de7e8ba87c845239"}, - {file = "dbus_fast-2.20.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d4b91b98cc1849f7d062d563d320594377b099ea9de53ebb789bf9fd6a0eeab4"}, - {file = "dbus_fast-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8ab58ef76575e6e00cf1c1f5747b24ce19e35d4966f1c5c3732cea2c3ed5e9"}, - {file = "dbus_fast-2.20.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1909addfad23d400d6f77c3665778a96003e32a1cddd1964de605d0ca400d829"}, - {file = "dbus_fast-2.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e591b218d4f327df29a89a922f199bbefb6f892ddc9b96aff21c05c15c0e5dc8"}, - {file = "dbus_fast-2.20.0-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f55f75ac3891c161daeabdb37d8a3407098482fe54013342a340cdd58f2be091"}, - {file = "dbus_fast-2.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d317dba76e904f75146ce0c5f219dae44e8060767b3adf78c94557bbcbea2cbe"}, - {file = "dbus_fast-2.20.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:88126343024f280c1fadd6599ac4cd7046ed550ddc942811dc3d290830cffd51"}, - {file = "dbus_fast-2.20.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecc07860e3014607a5293e1b87294148f96b1cc508f6496b27e40f64079ebb7a"}, - {file = "dbus_fast-2.20.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:e9cdf34f81320b36ce7f2b8c46169632730d9cdcafc52b55cada95096fce3457"}, - {file = "dbus_fast-2.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40ad43412f92373e4c74bb76d2129a7f0c38a1d883adcfc08f168535f7e7846"}, - {file = "dbus_fast-2.20.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4a5fdebcd8f79d417693536d3ed08bb5842917d373fbc3e9685feecd001accd7"}, - {file = "dbus_fast-2.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b134d40688ca7f27ab38bec99194e2551c82fc01f583f44ae66129c3d15db8a7"}, - {file = "dbus_fast-2.20.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:fdd4ece2c856e44b5fe9dec354ce5d8930f7ae9bb4b96b3a195157621fea6322"}, - {file = "dbus_fast-2.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e609309d5503a5eab91a7b0cef9dd158c3d8786ac38643a962e99a69d5eb7a66"}, - {file = "dbus_fast-2.20.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8fd806bf4676a28b2323d8529d51f86fec5a9d32923d53ba522a4c2bc3d55857"}, - {file = "dbus_fast-2.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8526ff5b27b7c689d97fe8a29e97d3cb7298419b4cb63ed9029331d08d423c55"}, - {file = "dbus_fast-2.20.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:562868206d774080c4131b124a407350ffb5d2b89442048350b83b5084f4e0e1"}, - {file = "dbus_fast-2.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707fc61b4f2de83c8f574061fdaf0ac6fc28b402f451951cf0a1ead11bfcac71"}, - {file = "dbus_fast-2.20.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4a13c7856459e849202165fd9e1adda8169107a591b083b95842c15b9e772be4"}, - {file = "dbus_fast-2.20.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1aba69c1dd694399124efbc6ce15930e4697a95d527f16b614100f1f1055a2"}, - {file = "dbus_fast-2.20.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9817bd32d7734766b073bb08525b9560b0b9501c68c43cc91d43684a2829ad86"}, - {file = "dbus_fast-2.20.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed226cccedee0c94b292e27fd1c7d24987d36b5ac1cde021031f9c77a76a423"}, - {file = "dbus_fast-2.20.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c11a2b4addb965e09a2d8d666265455f4a7e48916b7c6f43629b828de6682425"}, - {file = "dbus_fast-2.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88367c2a849234f134b9c98fdb16dc84d5ba9703fe995c67f7306900bfa13896"}, - {file = "dbus_fast-2.20.0.tar.gz", hash = "sha256:a38e837c5a8d0a1745ec8390f68ff57986ed2167b0aa2e4a79738a51dd6dfcc3"}, + {file = "dbus_fast-2.21.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b04b88be594dad81b33f6770283eed2125763632515c5112f8aa30f259cd334c"}, + {file = "dbus_fast-2.21.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7333896544a4d0a3d708bd092f8c05eb3599dc2b34ae6e4c4b44d04d5514b0ec"}, + {file = "dbus_fast-2.21.1-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:4591e0962c272d42d305ab3fb8889f13d47255e412fd3b9839620836662c91fe"}, + {file = "dbus_fast-2.21.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:52641305461660c8969c6bb12364206a108c5c9e014c9220c70b99c4f48b6750"}, + {file = "dbus_fast-2.21.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:237db4ab0b90e5284ea7659264630d693273cdbda323a40368f320869bf6470f"}, + {file = "dbus_fast-2.21.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:999fed45cb391126107b804be0e344e75556fceaee4cc30a0ca06d77309bdf3c"}, + {file = "dbus_fast-2.21.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2309b9cafba799e9d343fdfdd5ae46276adf3929fef60f296f23b97ed1aa2f6"}, + {file = "dbus_fast-2.21.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b7d1f35218549762e52a782c0b548e0681332beee773d3dfffe2efc38b2ee960"}, + {file = "dbus_fast-2.21.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47aa28520fe274414b655c74cbe2e91d8b76e22f40cd41a758bb6975e526827b"}, + {file = "dbus_fast-2.21.1-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0ff6c72bcd6539d798015bda33c7ce35c7de76276b9bd45e48db13672713521a"}, + {file = "dbus_fast-2.21.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d8cd43b3799e766158f1bb0b27cc4eef685fd892417b0382b7fdfdd94f1e6c"}, + {file = "dbus_fast-2.21.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d4da8d58064f0a3dd07bfc283ba912b9d5a4cb38f1c0fcd9ecb2b9d43111243c"}, + {file = "dbus_fast-2.21.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:66e160f496ac79248feb09a0acf4aab5d139d823330cbd9377f6e19ae007330a"}, + {file = "dbus_fast-2.21.1-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:670b5c4d78c9c2d25e7ba650d212d98bf24d40292f91fe4e2f3ad4f80dc6d7e5"}, + {file = "dbus_fast-2.21.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d62adfab7c6f4a491085f53f9634d24745ca5a2772549945b7e2de27c0d534"}, + {file = "dbus_fast-2.21.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:54e8771e31ee1deb01feef2475c12123cab770c371ecc97af98eb6ca10a2858e"}, + {file = "dbus_fast-2.21.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2db4d0d60a891a8b20a4c6de68a088efe73b29ab4a5949fe6aad2713c131e174"}, + {file = "dbus_fast-2.21.1-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:65e76b20099c33352d5e7734a219982858873cf66fe510951d9bd27cb690190f"}, + {file = "dbus_fast-2.21.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:927f294b1dc7cea9372ef8c7c46ebeb5c7e6c1c7345358f952e7499bdbdf7eb4"}, + {file = "dbus_fast-2.21.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9e9a43ea42b8a9f2c62ca50ce05582de7b4f1f7eb27091f904578c29124af246"}, + {file = "dbus_fast-2.21.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78c84ecf19459571784fd6a8ad8b3e9006cf96c3282e8220bc49098866ef4cc7"}, + {file = "dbus_fast-2.21.1-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:a5b3895ea12c4e636dfaacf75fa5bd1e8450b2ffb97507520991eaf1989d102e"}, + {file = "dbus_fast-2.21.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85be33bb04e918833ac6f28f68f83a1e83425eb6e08b9c482cc3318820dfd55f"}, + {file = "dbus_fast-2.21.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:13ab6a0f64d345cb42c489239962261f724bd441458bef245b39828ed94ea6f4"}, + {file = "dbus_fast-2.21.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c585e7a94bb723a70b4966677b882be8bda324cc41bd129765e3ceab428889bb"}, + {file = "dbus_fast-2.21.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:62331ee3871f6881f517ca65ae185fb2462a0bf2fe78acc4a4d621fc4da08396"}, + {file = "dbus_fast-2.21.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbfd6892fa092cbd6f52edcb24797af62fba8baa50995db856b0a342184c850d"}, + {file = "dbus_fast-2.21.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:a999e35628988ad4f81af36192cd592b8fd1e72e1bbc76a64d80808e6f4b9540"}, + {file = "dbus_fast-2.21.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae9a6b9bb54f3f89424fdd960b60ac53239b9e5d4a5d9a598d222fbf8d3173"}, + {file = "dbus_fast-2.21.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:39a3f3662391b49553bf9d9d2e9a6cb31e0d7d337557ee0c0be5c558a3c7d230"}, + {file = "dbus_fast-2.21.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffc2b6beb212d0d231816dcb7bd8bcdafccd04750ba8f5e915f40ad312f5adf2"}, + {file = "dbus_fast-2.21.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c938eb7130067ca3b74b248ee376228776d8f013a206ae78e6fc644c9db0f4f5"}, + {file = "dbus_fast-2.21.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fae9609d972f0c2b72017796a8140b8a6fb842426f0aed4f43f0fa7d780a16f"}, + {file = "dbus_fast-2.21.1.tar.gz", hash = "sha256:87b852d2005f1d59399ca51c5f3538f28a4742d739d7abe82b7ae8d01d8a5d02"}, ] [[package]] name = "dill" -version = "0.3.7" +version = "0.3.8" description = "serialize all of Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, - {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "docutils" @@ -573,23 +574,23 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.0.0" +version = "7.1.0" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"}, - {file = "importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "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"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" @@ -605,32 +606,29 @@ files = [ [[package]] name = "isort" -version = "5.12.0" +version = "5.13.2" 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"}, + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [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"] +colors = ["colorama (>=0.4.6)"] [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" 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"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -641,108 +639,119 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "lazy-object-proxy" -version = "1.9.0" +version = "1.10.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" 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"}, + {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, + {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, ] [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" 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-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"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -759,39 +768,39 @@ files = [ [[package]] name = "mypy" -version = "1.7.1" +version = "1.9.0" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, - {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, - {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, - {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, - {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, - {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, - {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, - {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, - {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, - {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, - {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, - {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, - {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, - {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, - {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, - {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, - {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, - {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, - {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] @@ -817,83 +826,67 @@ files = [ {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.26.2" +version = "1.26.4" description = "Fundamental package for array computing in Python" category = "main" optional = true python-versions = ">=3.9" files = [ - {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"}, - {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"}, - {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"}, - {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"}, - {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"}, - {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"}, - {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"}, - {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"}, - {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"}, - {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"}, - {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"}, - {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"}, - {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"}, - {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"}, - {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"}, - {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"}, - {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"}, - {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"}, - {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"}, - {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"}, - {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"}, - {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"}, - {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"}, - {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"}, - {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"}, - {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"}, - {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"}, - {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"}, - {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"}, - {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"}, - {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"}, - {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"}, - {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"}, - {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"}, - {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"}, - {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] name = "opencv-python" -version = "4.8.1.78" +version = "4.9.0.80" description = "Wrapper package for OpenCV python bindings." category = "main" optional = true python-versions = ">=3.6" files = [ - {file = "opencv-python-4.8.1.78.tar.gz", hash = "sha256:cc7adbbcd1112877a39274106cb2752e04984bc01a031162952e97450d6117f6"}, - {file = "opencv_python-4.8.1.78-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:91d5f6f5209dc2635d496f6b8ca6573ecdad051a09e6b5de4c399b8e673c60da"}, - {file = "opencv_python-4.8.1.78-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31f47e05447da8b3089faa0a07ffe80e114c91ce0b171e6424f9badbd1c5cd"}, - {file = "opencv_python-4.8.1.78-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9814beca408d3a0eca1bae7e3e5be68b07c17ecceb392b94170881216e09b319"}, - {file = "opencv_python-4.8.1.78-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c406bdb41eb21ea51b4e90dfbc989c002786c3f601c236a99c59a54670a394"}, - {file = "opencv_python-4.8.1.78-cp37-abi3-win32.whl", hash = "sha256:a7aac3900fbacf55b551e7b53626c3dad4c71ce85643645c43e91fcb19045e47"}, - {file = "opencv_python-4.8.1.78-cp37-abi3-win_amd64.whl", hash = "sha256:b983197f97cfa6fcb74e1da1802c7497a6f94ed561aba6980f1f33123f904956"}, + {file = "opencv-python-4.9.0.80.tar.gz", hash = "sha256:1a9f0e6267de3a1a1db0c54213d022c7c8b5b9ca4b580e80bdc58516c922c9e1"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:7e5f7aa4486651a6ebfa8ed4b594b65bd2d2f41beeb4241a3e4b1b85acbbbadb"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71dfb9555ccccdd77305fc3dcca5897fbf0cf28b297c51ee55e079c065d812a3"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b34a52e9da36dda8c151c6394aed602e4b17fa041df0b9f5b93ae10b0fcca2a"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4088cab82b66a3b37ffc452976b14a3c599269c247895ae9ceb4066d8188a57"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:dcf000c36dd1651118a2462257e3a9e76db789a78432e1f303c7bac54f63ef6c"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:3f16f08e02b2a2da44259c7cc712e779eff1dd8b55fdb0323e8cab09548086c0"}, ] [package.dependencies] @@ -901,10 +894,10 @@ 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.23.5", markers = "python_version >= \"3.11\""}, {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]] @@ -936,14 +929,14 @@ files = [ [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] @@ -1043,30 +1036,30 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.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.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [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)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -1075,14 +1068,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "poethepoet" -version = "0.24.4" +version = "0.25.0" description = "A task runner that works well with poetry." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "poethepoet-0.24.4-py3-none-any.whl", hash = "sha256:fb4ea35d7f40fe2081ea917d2e4102e2310fda2cde78974050ca83896e229075"}, - {file = "poethepoet-0.24.4.tar.gz", hash = "sha256:ff4220843a87c888cbcb5312c8905214701d0af60ac7271795baa8369b428fef"}, + {file = "poethepoet-0.25.0-py3-none-any.whl", hash = "sha256:42c0fd654f23e1b7c67aa8aa395c72e15eb275034bd5105171003daf679c1470"}, + {file = "poethepoet-0.25.0.tar.gz", hash = "sha256:ca8f1d8475aa10d2ceeb26331d2626fc4a6b51df1e7e70d3d0d6481a984faab6"}, ] [package.dependencies] @@ -1124,25 +1117,6 @@ files = [ {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, ] -[[package]] -name = "protoletariat" -version = "3.2.19" -description = "Python protocol buffers for the rest of us" -category = "dev" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "protoletariat-3.2.19-py3-none-any.whl", hash = "sha256:4bed510011cb352b26998008167a5a7ae697fb49d76fe4848bffa27856feab35"}, - {file = "protoletariat-3.2.19.tar.gz", hash = "sha256:3c23aa88bcceadde5a589bf0c1dd91e08636309e5b3d115ddebb38f5b1873d53"}, -] - -[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" @@ -1169,19 +1143,19 @@ files = [ [[package]] name = "pydantic" -version = "2.5.2" +version = "2.6.4" description = "Data validation using Python type hints" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, + {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, + {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.14.5" +pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" [package.extras] @@ -1189,117 +1163,91 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.14.5" +version = "2.16.3" description = "" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, - {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, - {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, - {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, - {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, - {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, - {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, - {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, - {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, - {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, - {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, - {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, - {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, - {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, - {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] @@ -1307,20 +1255,24 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.1.0" +version = "2.2.1" description = "Settings management using Pydantic" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, - {file = "pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, ] [package.dependencies] pydantic = ">=2.3.0" python-dotenv = ">=0.21.0" +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pydocstyle" version = "6.3.0" @@ -1466,14 +1418,14 @@ pyobjc-core = ">=9.2" [[package]] name = "pyparsing" -version = "3.1.1" +version = "3.1.2" 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"}, + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] [package.extras] @@ -1481,14 +1433,14 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -1558,14 +1510,14 @@ pytest-metadata = "*" [[package]] name = "pytest-metadata" -version = "3.0.0" +version = "3.1.1" description = "pytest plugin for test session metadata" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" 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"}, + {file = "pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b"}, + {file = "pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8"}, ] [package.dependencies] @@ -1576,29 +1528,29 @@ test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "tox (> [[package]] name = "pytest-timeout" -version = "2.2.0" +version = "2.3.1" description = "pytest plugin to abort hanging tests" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-timeout-2.2.0.tar.gz", hash = "sha256:3b0b95dabf3cb50bac9ef5ca912fa0cfc286526af17afc806824df20c2f72c90"}, - {file = "pytest_timeout-2.2.0-py3-none-any.whl", hash = "sha256:bde531e096466f49398a59f2dde76fa78429a09a12411466f88a07213e220de2"}, + {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, + {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, ] [package.dependencies] -pytest = ">=5.0.0" +pytest = ">=7.0.0" [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] @@ -1606,14 +1558,14 @@ cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" description = "World timezone definitions, modern and historical" category = "main" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] @@ -1640,23 +1592,21 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-mock" -version = "1.11.0" +version = "1.12.1" description = "Mock out responses from the requests package" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" 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"}, + {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, + {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, ] [package.dependencies] -requests = ">=2.3,<3" -six = "*" +requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] -test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testtools"] [[package]] name = "rich" @@ -1677,18 +1627,6 @@ 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" @@ -1759,59 +1697,53 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.7" +version = "1.0.8" 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"}, + {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, + {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.5" +version = "1.0.6" 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"}, + {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, + {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.4" +version = "2.0.5" 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"}, + {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, + {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] @@ -1846,55 +1778,52 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.6" +version = "1.0.7" 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"}, + {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, + {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.9" +version = "1.1.10" 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.9-py3-none-any.whl", hash = "sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1"}, - {file = "sphinxcontrib_serializinghtml-1.1.9.tar.gz", hash = "sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54"}, + {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, + {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxemoji" -version = "0.2.0" +version = "0.3.1" description = "An extension to use emoji codes in your Sphinx documentation" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.9" files = [ - {file = "sphinxemoji-0.2.0.tar.gz", hash = "sha256:27861d1dd7c6570f5e63020dac9a687263f7481f6d5d6409eb31ecebcc804e4c"}, + {file = "sphinxemoji-0.3.1-py3-none-any.whl", hash = "sha256:dae483695f8d1e90a28a6e9bbccc08d256202afcc1d0fbd33b51b3b4352d439e"}, + {file = "sphinxemoji-0.3.1.tar.gz", hash = "sha256:23ecff1f1e765393e49083b45386b7da81ced97c9a18a1dfd191460a97da3b11"}, ] [package.dependencies] -sphinx = ">=1.8" +sphinx = ">=5.0" [[package]] name = "tomli" @@ -1910,14 +1839,14 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.3" +version = "0.12.4" description = "Style preserving TOML library" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, - {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, ] [[package]] @@ -1933,38 +1862,38 @@ files = [ [[package]] name = "types-protobuf" -version = "4.24.0.4" +version = "4.24.0.20240408" description = "Typing stubs for protobuf" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-protobuf-4.24.0.4.tar.gz", hash = "sha256:57ab42cb171dfdba2c74bb5b50c250478538cc3c5ed95b8b368929ad0c9f90a5"}, - {file = "types_protobuf-4.24.0.4-py3-none-any.whl", hash = "sha256:131ab7d0cbc9e444bc89c994141327dcce7bcaeded72b1acb72a94827eb9c7af"}, + {file = "types-protobuf-4.24.0.20240408.tar.gz", hash = "sha256:c03a44357b03c233c8c5864ce3e07dd9c766a00497d271496923f7ae3cb9e1de"}, + {file = "types_protobuf-4.24.0.20240408-py3-none-any.whl", hash = "sha256:9b87cd279378693071247227f52e89738af7c8d6f06dbdd749b0cf473c4916ce"}, ] [[package]] name = "types-pytz" -version = "2023.3.1.1" +version = "2024.1.0.20240203" description = "Typing stubs for pytz" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-pytz-2023.3.1.1.tar.gz", hash = "sha256:cc23d0192cd49c8f6bba44ee0c81e4586a8f30204970fc0894d209a6b08dab9a"}, - {file = "types_pytz-2023.3.1.1-py3-none-any.whl", hash = "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf"}, + {file = "types-pytz-2024.1.0.20240203.tar.gz", hash = "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e"}, + {file = "types_pytz-2024.1.0.20240203-py3-none-any.whl", hash = "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3"}, ] [[package]] name = "types-requests" -version = "2.31.0.10" +version = "2.31.0.20240406" description = "Typing stubs for requests" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.10.tar.gz", hash = "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92"}, - {file = "types_requests-2.31.0.10-py3-none-any.whl", hash = "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc"}, + {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, + {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, ] [package.dependencies] @@ -1987,26 +1916,26 @@ types-pytz = "*" [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] name = "tzdata" -version = "2023.3" +version = "2024.1" 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"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [[package]] @@ -2029,18 +1958,19 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) [[package]] name = "urllib3" -version = "2.1.0" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2126,63 +2056,14 @@ files = [ [[package]] name = "zeroconf" -version = "0.128.0" +version = "0.132.0" description = "A pure python implementation of multicast DNS service discovery" category = "main" optional = false -python-versions = ">=3.7,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "zeroconf-0.128.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b4a196126162938b132a6baa46b8e0d7198252aab2e04744b3782ccc35be05"}, - {file = "zeroconf-0.128.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:23c40f972019643665a16f96d2874e649593efac633542b39cd705d23fc52508"}, - {file = "zeroconf-0.128.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b37b8bcc0510a16f1e045422cf7f062e9d6a2f599777614e11f157f11f85879"}, - {file = "zeroconf-0.128.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:10bcbc0789581a9dd7a60aa8f2e6c7ce6d8c574c004bbabd1b8a87f80cdc89ee"}, - {file = "zeroconf-0.128.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:81db09373f2283b9ed23a8de569ecbc6735822f317e4fa77793b7b368e8443b6"}, - {file = "zeroconf-0.128.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a044f04dfc01655f85934db860649295a7958808fed8641ef2e2af8d0546c1a4"}, - {file = "zeroconf-0.128.0-cp310-cp310-win32.whl", hash = "sha256:a9fe274dda792d89ee644fe18e050f92bff2634717c85dec775707896f0f9856"}, - {file = "zeroconf-0.128.0-cp310-cp310-win_amd64.whl", hash = "sha256:b69e4b12332f60feea49a14e3aa7b92a31d21d52de6ebe67de4fa1f183705029"}, - {file = "zeroconf-0.128.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:9035d647039cdf679e664d5ddb0277e3219131e22e29643b79d5f16324e99261"}, - {file = "zeroconf-0.128.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9084e621ea4dffe551f46dfcfc41f2e8854b4642e59aebae383ce0f4c7185254"}, - {file = "zeroconf-0.128.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:7340863fd214d025f5a27eb0d6fef7310aca42f7f031922ba21086fea5872e3a"}, - {file = "zeroconf-0.128.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e8cda1be369c587a25d6a8f5db5d0e1ae3d163c9ff1236719e1def0bd52c0e4"}, - {file = "zeroconf-0.128.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d5dc5f2d1613466c61627bb137613009452cf3be864b1344c1af9c1c0e39bb5"}, - {file = "zeroconf-0.128.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7906a820d54a18a8ee77940eea56e365bd770fc8c4f83d740f7f0c6087ec3c3e"}, - {file = "zeroconf-0.128.0-cp311-cp311-win32.whl", hash = "sha256:002cdec84b3a4c55dc877af0c7b99e0d8bdb3c92d9f294dd48b1f4b4eab2c893"}, - {file = "zeroconf-0.128.0-cp311-cp311-win_amd64.whl", hash = "sha256:367f0d1e8ad789c39c385f153b1ff0c924fbdf9983eb65579be67e74585b6b0b"}, - {file = "zeroconf-0.128.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:600d84ee75ad7a5e41b69f8f650af6d8551cb0ee9d67708aff0bee1ece8f3686"}, - {file = "zeroconf-0.128.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77b5cc3466c9bf09b88b123b68c3ca0fbc1af78dcbb001c2d684bf7440fdc1cb"}, - {file = "zeroconf-0.128.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:7d1b42e2067b9d1323a9626de5f31087ba3d7bcb8770cf82b65bf448fc1ff87a"}, - {file = "zeroconf-0.128.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96edbde95d81de2c618479338b34791abf4fa1e4bcf48fc669b05afa181fd3ea"}, - {file = "zeroconf-0.128.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9bb825df621fb70a82d59807e046876f85770457feb17e76118e91a4ceb58868"}, - {file = "zeroconf-0.128.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:774f6f4c4cdf87c7a09a55dbad8c7755a737e84ca8a0e1c2ef360c9ed6b49333"}, - {file = "zeroconf-0.128.0-cp312-cp312-win32.whl", hash = "sha256:4d891fc5fc2f8739934c3afb6ed667dda639daf76051e56d5b8d843bcb0e43e3"}, - {file = "zeroconf-0.128.0-cp312-cp312-win_amd64.whl", hash = "sha256:0f62e7842e874aa8dd2c3fd35319594a6a2e02e470c79be67eac6b85760cfa57"}, - {file = "zeroconf-0.128.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:eb500b4eb00b694214a68c229c6845cc66b7ab50e43648ee15d649775107bd5c"}, - {file = "zeroconf-0.128.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:548ad39fe7f30b2181dde26aff4dc1711226dd72719496c71c73f9f26e92ce0e"}, - {file = "zeroconf-0.128.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba341df450470895a69403d64636ceb309cf5c56f525d62e396818e3c71a78a0"}, - {file = "zeroconf-0.128.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:173a66bf776407cf5eb2d17a3d1fd1343ce078e5126019383d230be0769be0d8"}, - {file = "zeroconf-0.128.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fb33cf2ce028c94146876453044672f12f5f834fb1a76b9aaca46e316c3fdc81"}, - {file = "zeroconf-0.128.0-cp38-cp38-win32.whl", hash = "sha256:58853c89c8f533d53ba02fc94fa30b249da46ae19a557a987cd36d89095ffbc2"}, - {file = "zeroconf-0.128.0-cp38-cp38-win_amd64.whl", hash = "sha256:5966882787be03cb51442b8a8725a6c11c3b0e406aff75fb68c5307c504d1958"}, - {file = "zeroconf-0.128.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:03dcb14a1b377bcb9da90866c4d72291101b4dc8ea15ecdde7d448b298bb41aa"}, - {file = "zeroconf-0.128.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:156001fb366170d99cc735093b0eaba6b1cc113df5388b1016acb1630a7ef2ff"}, - {file = "zeroconf-0.128.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f09fa3a56393932f67ee12416cad3c70297bd1a66b70024fb9a7f63d97a215"}, - {file = "zeroconf-0.128.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6c5f4fbb5ba36f40d0968b3c7ec7ec13bbbb3bb09217bb70682cf48322803d57"}, - {file = "zeroconf-0.128.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1854ea933b3821172a371868f34cf94f9033f9f52e8523e08aa140bce00633f4"}, - {file = "zeroconf-0.128.0-cp39-cp39-win32.whl", hash = "sha256:eae387ff5ba80b66678653b00598b613859c7c4fc4ee8a38b1cc7a83cc1c7b91"}, - {file = "zeroconf-0.128.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8a99fea808f3f3a67ec516386408603e9b95b700794bfeabfdaff3d52227e67"}, - {file = "zeroconf-0.128.0-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:eca9d64becb1ecb5ae9af58243aa03b813b64c750fe9eba18b6fa641cb685eb7"}, - {file = "zeroconf-0.128.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:20eb9d0948a4cf92ff33cbfb2b6883cc995b449b4ef4c97cd2697b366ed62abc"}, - {file = "zeroconf-0.128.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2839f7374bbf3e07db19be51d6f64ab222071d70c4e20541d179550f6fde6e79"}, - {file = "zeroconf-0.128.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2713afa28747a4e58a6995b1df24cba1215ed03ce77cd5efbca39262fff4d593"}, - {file = "zeroconf-0.128.0-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:836ef8149f135d4c17d1745cd3774d531984063e173dfc77a12ef1e3ed509b3f"}, - {file = "zeroconf-0.128.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4b3f35deac8fc38ec9a4413fdf6d0fce8872e6320d0580120233661e7988e3b6"}, - {file = "zeroconf-0.128.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96be7af467fddd964794733ba2442608ce381bd4574f46b72e603b0834765cd7"}, - {file = "zeroconf-0.128.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:24d8f54e37e49691d834a20641a82adcbc2f601d1eca1bc86eb9f045f12aa379"}, - {file = "zeroconf-0.128.0-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5f4a70c129858c956fa28310aafa12776ebb093dea9c7d09b9d759aea02087e7"}, - {file = "zeroconf-0.128.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9b76bbbba4b1ba2d28aa0813e260b6bf7e9374a584902c75e60dac6a449a2926"}, - {file = "zeroconf-0.128.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0efddd1518e394cb97660eb63b97675761a83013701d528eb31b9175c535c7ee"}, - {file = "zeroconf-0.128.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c37f5f9379527d06b7e165792b916b122f8192bd9d91e8e9ab52344f3722c4e2"}, - {file = "zeroconf-0.128.0.tar.gz", hash = "sha256:f27877647f2afdf02f365d38d912bef0966783fb25fe0fba287f025969cf7349"}, + {file = "zeroconf-0.132.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:fb0a91b58b10d3a31b8324b2a8548e59c547a5c37055344c12d929f86c063d4e"}, + {file = "zeroconf-0.132.0.tar.gz", hash = "sha256:e2dddb9b8e6a9de3c43f943d8547300e6bd49b2043fd719ae830cfe0f2908a5c"}, ] [package.dependencies] @@ -2191,19 +2072,19 @@ ifaddr = ">=0.1.7" [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "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"] +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-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] gui = ["opencv-python", "Pillow"] @@ -2211,4 +2092,4 @@ gui = ["opencv-python", "Pillow"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "ee2cd93dd3871ec789b46a5fed7f46ccc66a127c69953cf70cb87bdbc1c9b562" +content-hash = "f2e9d2dd78a4da02ab1f6e1f1f0c487756c9cf1332e08c0a33ae2e3ea58966cf" diff --git a/demos/python/sdk_wireless_camera_control/pyproject.toml b/demos/python/sdk_wireless_camera_control/pyproject.toml index 362c7731..2cbee0f9 100644 --- a/demos/python/sdk_wireless_camera_control/pyproject.toml +++ b/demos/python/sdk_wireless_camera_control/pyproject.toml @@ -71,7 +71,6 @@ types-requests = "*" types-attrs = "*" types-pytz = "*" types-tzlocal = "*" -mypy-protobuf = "*" construct-typing = "*" sphinx = "^5" sphinx-rtd-theme = "^1" @@ -82,69 +81,69 @@ poethepoet = "^0" autodoc-pydantic = "^2" pytest-timeout = "^2" isort = "*" -protoletariat = "^3" +types-protobuf = "^4" [tool.poe.tasks.tests] cmd = "pytest tests --cov-fail-under=70" help = "Run unit tests" -[tool.poe.tasks.types] +[tool.poe.tasks._types] cmd = "mypy open_gopro" help = "Check types" -[tool.poe.tasks.lint] +[tool.poe.tasks._pylint] cmd = "pylint open_gopro" help = "Run pylint" -[tool.poe.tasks.format_code] +[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] +[tool.poe.tasks._sort_imports] cmd = "isort open_gopro tests" help = "Sort imports with isort" [tool.poe.tasks.format] -sequence = ["format_code", "sort_imports"] +sequence = ["_format_code", "_sort_imports"] help = "Format code and sort imports" -[tool.poe.tasks.pydocstyle] +[tool.poe.tasks.lint] +sequence = ["format", "_types", "_pylint"] +help = "Perform all static code analysis" + +[tool.poe.tasks._pydocstyle] cmd = "pydocstyle --config pyproject.toml -v open_gopro" help = "check docstrings style" -[tool.poe.tasks.darglint] +[tool.poe.tasks._darglint] cmd = "darglint -v 2 open_gopro" help = "validate docstrings" [tool.poe.tasks.docstrings] -sequence = ["pydocstyle", "darglint"] +sequence = ["_pydocstyle", "_darglint"] help = "Format, check types, lint, check docstrings, and run unit tests" [tool.poe.tasks.sphinx] cmd = "sphinx-build -W --keep-going -a -E -b html docs docs/build" help = "Build sphinx documentation." -[tool.poe.tasks.coverage] +[tool.poe.tasks._coverage] cmd = "coverage-badge -f -o docs/_static/coverage.svg" help = "update coverage badge" -[tool.poe.tasks.protobuf] -cmd = "bash ./tools/build_protos.sh" -help = "generate protobuf source from .proto (assumes protoc >= 3.20.1 available)" - -[tool.poe.tasks.clean_artifacts] +[tool.poe.tasks._clean_artifacts] cmd = "rm -rf **/__pycache__ *.csv *.mp4 *.jpg *.log .mypy_cache .nox" help = "Clean testing artifacts and pycache" -[tool.poe.tasks.clean_tests] +[tool.poe.tasks._clean_tests] cmd = "rm -rf .reports && rm -rf .pytest_cache" help = "Clean test reports" -[tool.poe.tasks.clean_docs] +[tool.poe.tasks._clean_docs] cmd = "rm -f docs/modules.rst && rm -rf docs/build" help = "Clean built docs output" -[tool.poe.tasks.clean_build] +[tool.poe.tasks._clean_build] cmd = "rm -rf dist" help = "Clean module build output" @@ -153,11 +152,11 @@ sequence = ["docstrings", "sphinx"] help = "Validate docstrings and build docs" [tool.poe.tasks.clean] -sequence = ["clean_artifacts", "clean_tests", "clean_docs", "clean_build"] +sequence = ["_clean_artifacts", "_clean_tests", "_clean_docs", "_clean_build"] help = "Clean everything" [tool.poe.tasks.all] -sequence = ["format", "types", "lint", "tests", "docs"] +sequence = ["format", "lint", "tests", "docs"] help = "Format, check types, lint, check docstrings, and run unit tests" [tool.mypy] @@ -217,6 +216,7 @@ ignore = ["tests", "proto"] [tool.pylint.'MESSAGES CONTROL'] disable = [ + "use-maxsplit-arg", "unnecessary-lambda", "unnecessary-lambda-assignment", "too-many-ancestors", diff --git a/demos/python/sdk_wireless_camera_control/tests/conftest.py b/demos/python/sdk_wireless_camera_control/tests/conftest.py index 10738c98..36105af0 100644 --- a/demos/python/sdk_wireless_camera_control/tests/conftest.py +++ b/demos/python/sdk_wireless_camera_control/tests/conftest.py @@ -6,7 +6,7 @@ import asyncio import logging import re -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import Any, Generic, Optional, Pattern @@ -37,8 +37,14 @@ ) from open_gopro.ble.adapters.bleak_wrapper import BleakWrapperController 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.communicator_interface import ( + BleMessage, + GoProBle, + GoProWifi, + HttpMessage, + MessageRules, +) +from open_gopro.constants import CmdId, GoProUUIDs, StatusId from open_gopro.exceptions import ConnectFailed, FailedToFindDevice from open_gopro.gopro_base import GoProBase from open_gopro.logger import set_logging_level, setup_logging @@ -193,7 +199,7 @@ def disconnection_handler(_) -> None: print("Entered test disconnect callback") -def notification_handler(handle: int, data: bytearray) -> None: +async def notification_handler(handle: int, data: bytearray) -> None: print("Entered test notification callback") @@ -222,12 +228,14 @@ def unregister_update(self, callback: types.UpdateCb, update: types.UpdateType = return async def _send_ble_message( - self, uuid: BleUUID, data: bytearray, response_id: types.ResponseType, **kwargs - ) -> dict: - return dict(uuid=uuid, packet=data) + self, message: BleMessage, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + return dict(uuid=message._uuid, packet=message._build_data(**kwargs)) - async def _read_characteristic(self, uuid: BleUUID) -> dict: - return dict(uuid=uuid) + async def _read_ble_characteristic( + self, message: BleMessage, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + return dict(uuid=message._uuid) @property def ble_command(self) -> BleCommands: @@ -287,6 +295,7 @@ async def mock_wifi_client(): @dataclass class MockWifiResponse: url: str + body: dict[str, Any] = field(default_factory=dict) class MockWifiCommunicator(GoProWifi): @@ -296,8 +305,20 @@ def __init__(self, test_version: str): super().__init__(MockWifiController()) self._api = api_versions[test_version](self) - async def _http_get(self, url: str, _=None, **kwargs): - return MockWifiResponse(url) + async def _get_json( + self, message: HttpMessage, *, timeout: int = 0, rules: MessageRules = MessageRules(), **kwargs + ) -> GoProResp: + return MockWifiResponse(message.build_url(**kwargs), message.build_body(**kwargs)) + + async def _get_stream( + self, message: HttpMessage, *, timeout: int = 0, rules: MessageRules = MessageRules(), **kwargs + ) -> GoProResp: + return MockWifiResponse(message.build_url(path=kwargs["camera_file"])), kwargs["local_file"] + + async def _put_json( + self, message: HttpMessage, *, timeout: int = 0, rules: MessageRules = MessageRules(), **kwargs + ) -> GoProResp: + return MockWifiResponse(message.build_url(**kwargs), message.build_body(**kwargs)) async def _stream_to_file(self, url: str, file: Path): return url, file @@ -391,23 +412,22 @@ async def _open_ble(self, timeout: int, retries: int) -> None: self._ble._gatt_table.handle2uuid = self._mock_uuid async def _send_ble_message( - self, - uuid: BleUUID, - data: bytearray, - response_id: types.ResponseType, - response_data: list[bytearray] = None, - response_uuid: BleUUID = None, - **kwargs + self, message: BleMessage, rules: MessageRules = MessageRules(), **kwargs: Any ) -> GoProResp: - if response_uuid is None: - return mock_good_response - else: + if response_data := kwargs.get("response_data"): self._test_response_data = response_data - self._test_response_uuid = response_uuid + self._test_response_uuid = message._uuid global _test_response_id - _test_response_id = response_id + _test_response_id = message._identifier self._ble.write = self._mock_write - return await super()._send_ble_message(uuid, data, response_id) + return await super()._send_ble_message(message, **kwargs) + else: + return mock_good_response + + async def _read_ble_characteristic( + self, message: BleMessage, rules: MessageRules = MessageRules(), **kwargs: Any + ) -> GoProResp: + raise NotImplementedError async def _mock_version(self) -> DataPatch: return DataPatch("2.0") @@ -423,8 +443,9 @@ def _mock_uuid(self, _) -> BleUUID: 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) + self._notification_handler(0, self._test_response_data) + # for packet in self._test_response_data: + # self._notification_handler(0, packet) @property def is_ble_connected(self) -> bool: @@ -441,7 +462,6 @@ def close(self) -> None: _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) diff --git a/demos/python/sdk_wireless_camera_control/tests/test_ble_commands.py b/demos/python/sdk_wireless_camera_control/tests/test_ble_commands.py index b1e946ad..285a8293 100644 --- a/demos/python/sdk_wireless_camera_control/tests/test_ble_commands.py +++ b/demos/python/sdk_wireless_camera_control/tests/test_ble_commands.py @@ -1,32 +1,25 @@ # test_ble_commands.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:54 PM -import inspect -import logging -from typing import cast import pytest from construct import Int32ub -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 import Params +from open_gopro.constants import GoProUUIDs, SettingId from open_gopro.gopro_base import GoProBase -from tests.conftest import MockBleCommunicator @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 + assert response["packet"] == bytearray([1, 1, 1]) @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 @@ -34,5 +27,17 @@ async def test_write_command_correct_parameter_data(mock_ble_communicator: GoPro @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 + + +@pytest.mark.asyncio +async def test_ble_setting(mock_ble_communicator: GoProBase): + response = await mock_ble_communicator.ble_setting.led.set(Params.LED.BLE_KEEP_ALIVE) + assert response["uuid"] == GoProUUIDs.CQ_SETTINGS + assert response["packet"] == bytearray([SettingId.LED, 1, Params.LED.BLE_KEEP_ALIVE]) + + +@pytest.mark.asyncio +async def test_fastpass_shutter(mock_ble_communicator: GoProBase): + response = await mock_ble_communicator.ble_command.set_shutter(shutter=Params.Toggle.ENABLE) + assert response["uuid"] == GoProUUIDs.CQ_COMMAND diff --git a/demos/python/sdk_wireless_camera_control/tests/test_http_commands.py b/demos/python/sdk_wireless_camera_control/tests/test_http_commands.py index 6f10c44f..c712313d 100644 --- a/demos/python/sdk_wireless_camera_control/tests/test_http_commands.py +++ b/demos/python/sdk_wireless_camera_control/tests/test_http_commands.py @@ -7,12 +7,27 @@ import pytest -from open_gopro import Params +from open_gopro import Params, proto from open_gopro.gopro_base import GoProBase camera_file = "100GOPRO/XXX.mp4" +@pytest.mark.asyncio +async def test_put_with_body_args(mock_wifi_communicator: GoProBase): + response = await mock_wifi_communicator.http_command.update_custom_preset( + custom_name="Custom Name", + icon_id=proto.EnumPresetIcon.PRESET_ICON_ACTION, + title_id=proto.EnumPresetTitle.PRESET_TITLE_ACTION, + ) + assert response.url == "gopro/camera/presets/update_custom" + assert response.body == { + "custom_name": "Custom Name", + "icon_id": proto.EnumPresetIcon.PRESET_ICON_ACTION, + "title_id": proto.EnumPresetTitle.PRESET_TITLE_ACTION, + } + + @pytest.mark.asyncio async def test_get_with_no_params(mock_wifi_communicator: GoProBase): response = await mock_wifi_communicator.http_command.get_media_list() @@ -34,16 +49,18 @@ async def test_get_with_params(mock_wifi_communicator: GoProBase): @pytest.mark.asyncio async def test_get_binary(mock_wifi_communicator: GoProBase): - url, file = await mock_wifi_communicator.http_command.get_gpmf_data(camera_file=camera_file) - assert url == f"gopro/media/gpmf?path={camera_file}" + response, file = await mock_wifi_communicator.http_command.get_gpmf_data(camera_file=camera_file) + assert response.url == f"gopro/media/gpmf?path={camera_file}" assert file == Path("XXX.mp4") @pytest.mark.asyncio async def test_get_binary_with_component(mock_wifi_communicator: GoProBase): - url, file = await mock_wifi_communicator.http_command.download_file(camera_file=camera_file) - assert url == f"videos/DCIM/{camera_file}" - assert file == Path("XXX.mp4") + response, file = await mock_wifi_communicator.http_command.download_file( + camera_file=camera_file, local_file=Path("cheese.mp4") + ) + assert response.url == f"videos/DCIM/{camera_file}" + assert file == Path("cheese.mp4") @pytest.mark.asyncio diff --git a/demos/python/sdk_wireless_camera_control/tests/test_logging.py b/demos/python/sdk_wireless_camera_control/tests/test_logging.py new file mode 100644 index 00000000..880ba861 --- /dev/null +++ b/demos/python/sdk_wireless_camera_control/tests/test_logging.py @@ -0,0 +1,248 @@ +# test_logging.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +from typing import Generic, TypeVar + +import construct +import pytest + +from open_gopro import GoProResp +from open_gopro.api.builders import ( + BleProtoCommand, + BleReadCommand, + BleSettingFacade, + BleStatusFacade, + BleWriteCommand, + HttpSetting, +) +from open_gopro.communicator_interface import HttpMessage, Message +from open_gopro.constants import ( + ActionId, + CmdId, + ErrorCode, + FeatureId, + GoProUUIDs, + QueryCmdId, + SettingId, + StatusId, +) + +dummy_kwargs = {"first": 1, "second": 2} + + +def assert_kwargs(message: dict): + assert message.pop("first") == 1 + assert message.pop("second") == 2 + + +@pytest.mark.asyncio +async def test_ble_read_command(): + message = BleReadCommand(uuid=GoProUUIDs.ACC_APPEARANCE, parser=None) + d = message._as_dict(**dummy_kwargs) + assert d.pop("id") == GoProUUIDs.ACC_APPEARANCE + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.ACC_APPEARANCE + assert_kwargs(d) + assert not d + + +@pytest.mark.asyncio +async def test_ble_write_command(): + message = BleWriteCommand(uuid=GoProUUIDs.ACC_APPEARANCE, cmd=CmdId.GET_CAMERA_CAPABILITIES) + d = message._as_dict(**dummy_kwargs) + assert d.pop("id") == CmdId.GET_CAMERA_CAPABILITIES + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.ACC_APPEARANCE + assert_kwargs(d) + assert not d + + +@pytest.mark.asyncio +async def test_ble_proto_command(): + message = BleProtoCommand( + uuid=GoProUUIDs.ACC_APPEARANCE, + feature_id=FeatureId.COMMAND, + action_id=ActionId.GET_AP_ENTRIES, + response_action_id=ActionId.GET_AP_ENTRIES_RSP, + request_proto=None, + response_proto=None, + parser=None, + ) + d = message._as_dict(**dummy_kwargs) + assert d.pop("id") == ActionId.GET_AP_ENTRIES + assert d.pop("feature_id") == FeatureId.COMMAND + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.ACC_APPEARANCE + assert_kwargs(d) + assert not d + + +T = TypeVar("T", bound=Message) + + +class MockCommunicator(Generic[T]): + def __init__(self) -> None: + self.message: T + + async def _send_ble_message(self, message: T) -> GoProResp: + self.message = message + return GoProResp( + protocol=GoProResp.Protocol.BLE, status=ErrorCode.SUCCESS, data=bytes(), identifier=message._identifier + ) + + async def _get_json(self, message, **kwargs) -> GoProResp: + self.message = message + return GoProResp(protocol=GoProResp.Protocol.BLE, status=ErrorCode.SUCCESS, data=bytes(), identifier="unknown") + + def register_update(self, *args, **kwargs): + ... + + def unregister_update(self, *args, **kwargs): + ... + + +@pytest.mark.asyncio +async def test_ble_setting(): + class Communicator(MockCommunicator[BleSettingFacade.BleSettingMessageBase]): + ... + + communicator = Communicator() + message = BleSettingFacade( + communicator=communicator, identifier=SettingId.ADDON_MAX_LENS_MOD, parser_builder=construct.Flag + ) + + # Set Setting Value + await message.set(bytes()) + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("setting_id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_SETTINGS + assert_kwargs(d) + assert not d + + # Get Setting Value + await message.get_value() + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.GET_SETTING_VAL + assert d.pop("setting_id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + # Get Setting Capabilities Values + await message.get_capabilities_values() + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.GET_CAPABILITIES_VAL + assert d.pop("setting_id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + # Register value updates + await message.register_value_update(None) + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.REG_SETTING_VAL_UPDATE + assert d.pop("setting_id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + # Unregister value updates + await message.unregister_value_update(None) + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.UNREG_SETTING_VAL_UPDATE + assert d.pop("setting_id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + # Register capability updates + await message.register_capability_update(None) + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.REG_CAPABILITIES_UPDATE + assert d.pop("setting_id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + # Unregister capability updates + await message.unregister_capability_update(None) + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.UNREG_CAPABILITIES_UPDATE + assert d.pop("setting_id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + +@pytest.mark.asyncio +async def test_ble_status(): + class Communicator(MockCommunicator[BleStatusFacade.BleStatusMessageBase]): + ... + + communicator = Communicator() + message = BleStatusFacade(communicator=communicator, identifier=StatusId.ACC_MIC_STAT, parser=construct.Flag) + + # Get Status Value + await message.get_value() + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.GET_STATUS_VAL + assert d.pop("status_id") == StatusId.ACC_MIC_STAT + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + # Register value updates + await message.register_value_update(None) + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.REG_STATUS_VAL_UPDATE + assert d.pop("status_id") == StatusId.ACC_MIC_STAT + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + # Unregister value updates + await message.unregister_value_update(None) + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == QueryCmdId.UNREG_STATUS_VAL_UPDATE + assert d.pop("status_id") == StatusId.ACC_MIC_STAT + assert d.pop("protocol") == GoProResp.Protocol.BLE + assert d.pop("uuid") == GoProUUIDs.CQ_QUERY + assert_kwargs(d) + assert not d + + +@pytest.mark.asyncio +async def test_http_command(): + message = HttpMessage("endpoint", identifier=None) + d = message._as_dict(**dummy_kwargs) + assert d.pop("id") == "Endpoint" + assert d.pop("protocol") == GoProResp.Protocol.HTTP + assert d.pop("endpoint") == "endpoint" + assert_kwargs(d) + assert not d + + +@pytest.mark.asyncio +async def test_http_setting(): + class Communicator(MockCommunicator[HttpMessage]): + ... + + communicator = Communicator() + message = HttpSetting(communicator=communicator, identifier=SettingId.ADDON_MAX_LENS_MOD) + await message.set(1) + d = communicator.message._as_dict(**dummy_kwargs) + assert d.pop("id") == SettingId.ADDON_MAX_LENS_MOD + assert d.pop("protocol") == GoProResp.Protocol.HTTP + assert d.pop("endpoint") == r"gopro/camera/setting?setting={setting}&option={option}" + assert_kwargs(d) + assert not d diff --git a/demos/python/sdk_wireless_camera_control/tests/test_parsers.py b/demos/python/sdk_wireless_camera_control/tests/test_parsers.py index 188c92f5..9d144e59 100644 --- a/demos/python/sdk_wireless_camera_control/tests/test_parsers.py +++ b/demos/python/sdk_wireless_camera_control/tests/test_parsers.py @@ -1,13 +1,11 @@ # 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.models.response import BleRespBuilder, GlobalParsers from open_gopro.parser_interface import Parser from open_gopro.proto import EnumResultGeneric, ResponseGetApEntries diff --git a/demos/python/sdk_wireless_camera_control/tests/test_wireless_gopro.py b/demos/python/sdk_wireless_camera_control/tests/test_wireless_gopro.py index b56269fc..81f303b8 100644 --- a/demos/python/sdk_wireless_camera_control/tests/test_wireless_gopro.py +++ b/demos/python/sdk_wireless_camera_control/tests/test_wireless_gopro.py @@ -13,6 +13,7 @@ import requests import requests_mock +from open_gopro.communicator_interface import HttpMessage from open_gopro.constants import SettingId, StatusId from open_gopro.exceptions import GoProNotOpened, ResponseTimeout from open_gopro.gopro_wireless import Params, WirelessGoPro, types @@ -67,58 +68,58 @@ async def test_gopro_open(mock_wireless_gopro_basic: WirelessGoPro): @pytest.mark.asyncio async def test_http_get(mock_wireless_gopro_basic: WirelessGoPro, monkeypatch): - endpoint = "gopro/camera/stream/start" + message = HttpMessage("gopro/camera/stream/start", None) 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="{}") + session.mount(mock_wireless_gopro_basic._base_url + message._endpoint, adapter) + adapter.register_uri("GET", mock_wireless_gopro_basic._base_url + message._endpoint, json="{}") monkeypatch.setattr("open_gopro.gopro_base.requests.get", session.get) - response = await mock_wireless_gopro_basic._http_get(endpoint) + response = await mock_wireless_gopro_basic._get_json(message) assert response.ok @pytest.mark.asyncio async def test_http_file(mock_wireless_gopro_basic: WirelessGoPro, monkeypatch): + message = HttpMessage("videos/DCIM/100GOPRO/dummy.MP4", None) 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") + session.mount(mock_wireless_gopro_basic._base_url + message._endpoint, adapter) + adapter.register_uri("GET", mock_wireless_gopro_basic._base_url + message._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) + await mock_wireless_gopro_basic._get_stream(message, camera_file=out_file, local_file=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" + message = HttpMessage("gopro/camera/stream/start", None) session = requests.Session() adapter = requests_mock.Adapter() - session.mount(mock_wireless_gopro_basic._base_url + endpoint, adapter) + session.mount(mock_wireless_gopro_basic._base_url + message._endpoint, adapter) adapter.register_uri( - "GET", mock_wireless_gopro_basic._base_url + endpoint, exc=requests.exceptions.ConnectTimeout + "GET", mock_wireless_gopro_basic._base_url + message._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) + await mock_wireless_gopro_basic._get_json(message, timeout=1) @pytest.mark.asyncio async def test_http_response_error(mock_wireless_gopro_basic: WirelessGoPro, monkeypatch): - endpoint = "gopro/camera/stream/start" + message = HttpMessage("gopro/camera/stream/start", None) session = requests.Session() adapter = requests_mock.Adapter() - session.mount(mock_wireless_gopro_basic._base_url + endpoint, adapter) + session.mount(mock_wireless_gopro_basic._base_url + message._endpoint, adapter) adapter.register_uri( "GET", - mock_wireless_gopro_basic._base_url + endpoint, + mock_wireless_gopro_basic._base_url + message._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) + response = await mock_wireless_gopro_basic._get_json(message) assert not response.ok diff --git a/demos/python/sdk_wireless_camera_control/tools/build_protos.sh b/demos/python/sdk_wireless_camera_control/tools/build_protos.sh deleted file mode 100755 index 8e20751d..00000000 --- a/demos/python/sdk_wireless_camera_control/tools/build_protos.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -# build_protos.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 - - -CWD=$(pwd) -PROTO_SRC_DIR=$CWD/../../../protobuf -PROTO_OUT_DIR=$CWD/open_gopro/proto - -# Clean all current protobuf files and stubs -rm -f $PROTO_OUT_DIR/*pb2.py* >/dev/null 2>&1 - -echo -echo "Building protobuf python files and stubs from .proto source files..." -pushd $PROTO_SRC_DIR -protoc --include_imports --descriptor_set_out=$CWD/descriptors --python_out=$PROTO_OUT_DIR --mypy_out=$PROTO_OUT_DIR * -popd - -echo -echo "Converting relative imports to absolute..." -poetry run protol -o ./open_gopro/proto/ --in-place raw descriptors -rm descriptors - -# Format generated files -echo -echo "Formatting..." -poetry run black open_gopro/proto diff --git a/demos/python/tutorial/.python-version b/demos/python/tutorial/.python-version index 0f57b2b6..1d44b9e0 100644 --- a/demos/python/tutorial/.python-version +++ b/demos/python/tutorial/.python-version @@ -1 +1 @@ -3.10.8 +3.11.4 diff --git a/demos/python/tutorial/README.md b/demos/python/tutorial/README.md index 0e450726..9ede5dcc 100644 --- a/demos/python/tutorial/README.md +++ b/demos/python/tutorial/README.md @@ -8,10 +8,10 @@ If you are a user of the tutorials, please visit the above link. Developer's of ## Development Environment Setup -With Python >= 3.8 and < 3.11, perform: +With Python >= 3.9 and < 3.12, perform: ``` -pip install -r requirements-dev.txt +poetry install ``` ## Tutorial Documentation @@ -29,7 +29,7 @@ shall be tested as detailed below. There should be no static typing or linting errors as checked via: ``` -make lint +poetry run poe lint ``` ## Testing @@ -39,7 +39,7 @@ Each script shall be tested via pytest by adding a case to the `tests/testtutori Tests can be run via: ``` -make tests +poetry run poe tests ``` -After running th tests, test logs and a summary test report can be found in the `reports` folder. \ No newline at end of file +After running the tests, test logs and a summary test report can be found in the `.reports` folder. \ No newline at end of file diff --git a/demos/python/tutorial/poetry.lock b/demos/python/tutorial/poetry.lock index 40545c9c..2a8bc8e0 100644 --- a/demos/python/tutorial/poetry.lock +++ b/demos/python/tutorial/poetry.lock @@ -15,7 +15,10 @@ files = [ [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\""} +wrapt = [ + {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, +] [[package]] name = "async-timeout" @@ -31,30 +34,34 @@ files = [ [[package]] name = "black" -version = "23.11.0" +version = "24.3.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, - {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, - {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, - {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, - {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, - {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, - {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, - {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, - {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, - {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, - {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, - {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, - {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, - {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, - {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, - {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, - {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, - {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -68,7 +75,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -116,14 +123,14 @@ files = [ [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -321,62 +328,63 @@ toml = ["tomli"] [[package]] name = "dbus-fast" -version = "2.20.0" +version = "2.21.1" description = "A faster version of dbus-next" category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:ecf22e22434bdd61bfb8b544eb58f5032b23dda5a7fc233afa1d3c9c3241f0a8"}, - {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70f4c1fe23c47a59d81c8fd8830c65307a1f089cc92949004df4c65c69f155"}, - {file = "dbus_fast-2.20.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:9963180456586d0e1b58075e0439a34ed8e9ee4266b35f76f3db6ffc1af17e27"}, - {file = "dbus_fast-2.20.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:eafbf4f0ac86fd959f86bbdf910bf64406b35315781014ef4a1cd2bb43985346"}, - {file = "dbus_fast-2.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bb668e2039e15f0e5af14bee7de8c8c082e3b292ed2ce2ceb3168c7068ff2856"}, - {file = "dbus_fast-2.20.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3f7966f835da1d8a77c55a7336313bd97e7f722b316f760077c55c1e9533b0cd"}, - {file = "dbus_fast-2.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff856cbb1508bcf6735ed1e3c04de1def6c400720765141d2470e39c8fd6f13"}, - {file = "dbus_fast-2.20.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7a1da4ed9880046403ddedb7b941fd981872fc883dc9925bbf269b551f12120d"}, - {file = "dbus_fast-2.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9084ded47761a43b2252879c6ebaddb7e3cf89377cbdc981de7e8ba87c845239"}, - {file = "dbus_fast-2.20.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d4b91b98cc1849f7d062d563d320594377b099ea9de53ebb789bf9fd6a0eeab4"}, - {file = "dbus_fast-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8ab58ef76575e6e00cf1c1f5747b24ce19e35d4966f1c5c3732cea2c3ed5e9"}, - {file = "dbus_fast-2.20.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1909addfad23d400d6f77c3665778a96003e32a1cddd1964de605d0ca400d829"}, - {file = "dbus_fast-2.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e591b218d4f327df29a89a922f199bbefb6f892ddc9b96aff21c05c15c0e5dc8"}, - {file = "dbus_fast-2.20.0-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f55f75ac3891c161daeabdb37d8a3407098482fe54013342a340cdd58f2be091"}, - {file = "dbus_fast-2.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d317dba76e904f75146ce0c5f219dae44e8060767b3adf78c94557bbcbea2cbe"}, - {file = "dbus_fast-2.20.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:88126343024f280c1fadd6599ac4cd7046ed550ddc942811dc3d290830cffd51"}, - {file = "dbus_fast-2.20.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecc07860e3014607a5293e1b87294148f96b1cc508f6496b27e40f64079ebb7a"}, - {file = "dbus_fast-2.20.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:e9cdf34f81320b36ce7f2b8c46169632730d9cdcafc52b55cada95096fce3457"}, - {file = "dbus_fast-2.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40ad43412f92373e4c74bb76d2129a7f0c38a1d883adcfc08f168535f7e7846"}, - {file = "dbus_fast-2.20.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4a5fdebcd8f79d417693536d3ed08bb5842917d373fbc3e9685feecd001accd7"}, - {file = "dbus_fast-2.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b134d40688ca7f27ab38bec99194e2551c82fc01f583f44ae66129c3d15db8a7"}, - {file = "dbus_fast-2.20.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:fdd4ece2c856e44b5fe9dec354ce5d8930f7ae9bb4b96b3a195157621fea6322"}, - {file = "dbus_fast-2.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e609309d5503a5eab91a7b0cef9dd158c3d8786ac38643a962e99a69d5eb7a66"}, - {file = "dbus_fast-2.20.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8fd806bf4676a28b2323d8529d51f86fec5a9d32923d53ba522a4c2bc3d55857"}, - {file = "dbus_fast-2.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8526ff5b27b7c689d97fe8a29e97d3cb7298419b4cb63ed9029331d08d423c55"}, - {file = "dbus_fast-2.20.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:562868206d774080c4131b124a407350ffb5d2b89442048350b83b5084f4e0e1"}, - {file = "dbus_fast-2.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707fc61b4f2de83c8f574061fdaf0ac6fc28b402f451951cf0a1ead11bfcac71"}, - {file = "dbus_fast-2.20.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4a13c7856459e849202165fd9e1adda8169107a591b083b95842c15b9e772be4"}, - {file = "dbus_fast-2.20.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1aba69c1dd694399124efbc6ce15930e4697a95d527f16b614100f1f1055a2"}, - {file = "dbus_fast-2.20.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9817bd32d7734766b073bb08525b9560b0b9501c68c43cc91d43684a2829ad86"}, - {file = "dbus_fast-2.20.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed226cccedee0c94b292e27fd1c7d24987d36b5ac1cde021031f9c77a76a423"}, - {file = "dbus_fast-2.20.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c11a2b4addb965e09a2d8d666265455f4a7e48916b7c6f43629b828de6682425"}, - {file = "dbus_fast-2.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88367c2a849234f134b9c98fdb16dc84d5ba9703fe995c67f7306900bfa13896"}, - {file = "dbus_fast-2.20.0.tar.gz", hash = "sha256:a38e837c5a8d0a1745ec8390f68ff57986ed2167b0aa2e4a79738a51dd6dfcc3"}, + {file = "dbus_fast-2.21.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b04b88be594dad81b33f6770283eed2125763632515c5112f8aa30f259cd334c"}, + {file = "dbus_fast-2.21.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7333896544a4d0a3d708bd092f8c05eb3599dc2b34ae6e4c4b44d04d5514b0ec"}, + {file = "dbus_fast-2.21.1-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:4591e0962c272d42d305ab3fb8889f13d47255e412fd3b9839620836662c91fe"}, + {file = "dbus_fast-2.21.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:52641305461660c8969c6bb12364206a108c5c9e014c9220c70b99c4f48b6750"}, + {file = "dbus_fast-2.21.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:237db4ab0b90e5284ea7659264630d693273cdbda323a40368f320869bf6470f"}, + {file = "dbus_fast-2.21.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:999fed45cb391126107b804be0e344e75556fceaee4cc30a0ca06d77309bdf3c"}, + {file = "dbus_fast-2.21.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2309b9cafba799e9d343fdfdd5ae46276adf3929fef60f296f23b97ed1aa2f6"}, + {file = "dbus_fast-2.21.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b7d1f35218549762e52a782c0b548e0681332beee773d3dfffe2efc38b2ee960"}, + {file = "dbus_fast-2.21.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47aa28520fe274414b655c74cbe2e91d8b76e22f40cd41a758bb6975e526827b"}, + {file = "dbus_fast-2.21.1-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0ff6c72bcd6539d798015bda33c7ce35c7de76276b9bd45e48db13672713521a"}, + {file = "dbus_fast-2.21.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d8cd43b3799e766158f1bb0b27cc4eef685fd892417b0382b7fdfdd94f1e6c"}, + {file = "dbus_fast-2.21.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d4da8d58064f0a3dd07bfc283ba912b9d5a4cb38f1c0fcd9ecb2b9d43111243c"}, + {file = "dbus_fast-2.21.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:66e160f496ac79248feb09a0acf4aab5d139d823330cbd9377f6e19ae007330a"}, + {file = "dbus_fast-2.21.1-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:670b5c4d78c9c2d25e7ba650d212d98bf24d40292f91fe4e2f3ad4f80dc6d7e5"}, + {file = "dbus_fast-2.21.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d62adfab7c6f4a491085f53f9634d24745ca5a2772549945b7e2de27c0d534"}, + {file = "dbus_fast-2.21.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:54e8771e31ee1deb01feef2475c12123cab770c371ecc97af98eb6ca10a2858e"}, + {file = "dbus_fast-2.21.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2db4d0d60a891a8b20a4c6de68a088efe73b29ab4a5949fe6aad2713c131e174"}, + {file = "dbus_fast-2.21.1-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:65e76b20099c33352d5e7734a219982858873cf66fe510951d9bd27cb690190f"}, + {file = "dbus_fast-2.21.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:927f294b1dc7cea9372ef8c7c46ebeb5c7e6c1c7345358f952e7499bdbdf7eb4"}, + {file = "dbus_fast-2.21.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9e9a43ea42b8a9f2c62ca50ce05582de7b4f1f7eb27091f904578c29124af246"}, + {file = "dbus_fast-2.21.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78c84ecf19459571784fd6a8ad8b3e9006cf96c3282e8220bc49098866ef4cc7"}, + {file = "dbus_fast-2.21.1-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:a5b3895ea12c4e636dfaacf75fa5bd1e8450b2ffb97507520991eaf1989d102e"}, + {file = "dbus_fast-2.21.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85be33bb04e918833ac6f28f68f83a1e83425eb6e08b9c482cc3318820dfd55f"}, + {file = "dbus_fast-2.21.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:13ab6a0f64d345cb42c489239962261f724bd441458bef245b39828ed94ea6f4"}, + {file = "dbus_fast-2.21.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c585e7a94bb723a70b4966677b882be8bda324cc41bd129765e3ceab428889bb"}, + {file = "dbus_fast-2.21.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:62331ee3871f6881f517ca65ae185fb2462a0bf2fe78acc4a4d621fc4da08396"}, + {file = "dbus_fast-2.21.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbfd6892fa092cbd6f52edcb24797af62fba8baa50995db856b0a342184c850d"}, + {file = "dbus_fast-2.21.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:a999e35628988ad4f81af36192cd592b8fd1e72e1bbc76a64d80808e6f4b9540"}, + {file = "dbus_fast-2.21.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae9a6b9bb54f3f89424fdd960b60ac53239b9e5d4a5d9a598d222fbf8d3173"}, + {file = "dbus_fast-2.21.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:39a3f3662391b49553bf9d9d2e9a6cb31e0d7d337557ee0c0be5c558a3c7d230"}, + {file = "dbus_fast-2.21.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffc2b6beb212d0d231816dcb7bd8bcdafccd04750ba8f5e915f40ad312f5adf2"}, + {file = "dbus_fast-2.21.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c938eb7130067ca3b74b248ee376228776d8f013a206ae78e6fc644c9db0f4f5"}, + {file = "dbus_fast-2.21.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fae9609d972f0c2b72017796a8140b8a6fb842426f0aed4f43f0fa7d780a16f"}, + {file = "dbus_fast-2.21.1.tar.gz", hash = "sha256:87b852d2005f1d59399ca51c5f3538f28a4742d739d7abe82b7ae8d01d8a5d02"}, ] [[package]] name = "dill" -version = "0.3.7" +version = "0.3.8" description = "serialize all of Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, - {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "exceptiongroup" @@ -419,66 +427,64 @@ files = [ [[package]] name = "isort" -version = "5.12.0" +version = "5.13.2" 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"}, + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [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"] +colors = ["colorama (>=0.4.6)"] [[package]] name = "lazy-object-proxy" -version = "1.9.0" +version = "1.10.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" 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"}, + {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, + {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, ] [[package]] @@ -532,39 +538,39 @@ files = [ [[package]] name = "mypy" -version = "1.7.1" +version = "1.9.0" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, - {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, - {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, - {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, - {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, - {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, - {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, - {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, - {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, - {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, - {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, - {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, - {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, - {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, - {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, - {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, - {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, - {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, - {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] @@ -590,16 +596,32 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "mypy-protobuf" +version = "3.6.0" +description = "Generate mypy stub files from protobuf specs" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-protobuf-3.6.0.tar.gz", hash = "sha256:02f242eb3409f66889f2b1a3aa58356ec4d909cdd0f93115622e9e70366eca3c"}, + {file = "mypy_protobuf-3.6.0-py3-none-any.whl", hash = "sha256:56176e4d569070e7350ea620262478b49b7efceba4103d468448f1d21492fd6c"}, +] + +[package.dependencies] +protobuf = ">=4.25.3" +types-protobuf = ">=4.24" + [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -616,42 +638,42 @@ files = [ [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.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.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [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)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -660,14 +682,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "poethepoet" -version = "0.24.4" +version = "0.25.0" description = "A task runner that works well with poetry." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "poethepoet-0.24.4-py3-none-any.whl", hash = "sha256:fb4ea35d7f40fe2081ea917d2e4102e2310fda2cde78974050ca83896e229075"}, - {file = "poethepoet-0.24.4.tar.gz", hash = "sha256:ff4220843a87c888cbcb5312c8905214701d0af60ac7271795baa8369b428fef"}, + {file = "poethepoet-0.25.0-py3-none-any.whl", hash = "sha256:42c0fd654f23e1b7c67aa8aa395c72e15eb275034bd5105171003daf679c1470"}, + {file = "poethepoet-0.25.0.tar.gz", hash = "sha256:ca8f1d8475aa10d2ceeb26331d2626fc4a6b51df1e7e70d3d0d6481a984faab6"}, ] [package.dependencies] @@ -677,6 +699,46 @@ tomli = ">=1.2.2" [package.extras] poetry-plugin = ["poetry (>=1.0,<2.0)"] +[[package]] +name = "protobuf" +version = "4.25.3" +description = "" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, +] + +[[package]] +name = "protoletariat" +version = "3.2.19" +description = "Python protocol buffers for the rest of us" +category = "dev" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "protoletariat-3.2.19-py3-none-any.whl", hash = "sha256:4bed510011cb352b26998008167a5a7ae697fb49d76fe4848bffa27856feab35"}, + {file = "protoletariat-3.2.19.tar.gz", hash = "sha256:3c23aa88bcceadde5a589bf0c1dd91e08636309e5b3d115ddebb38f5b1873d53"}, +] + +[package.dependencies] +click = ">=8,<9" +protobuf = ">=3.19.1,<5" + +[package.extras] +grpcio-tools = ["grpcio-tools (>=1.42.0,<2)"] + [[package]] name = "py" version = "1.11.0" @@ -720,7 +782,10 @@ files = [ [package.dependencies] astroid = ">=2.15.8,<=2.17.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = {version = ">=0.2", markers = "python_version < \"3.11\""} +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" @@ -812,14 +877,14 @@ pyobjc-core = ">=9.2" [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -835,18 +900,18 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.23.2" +version = "0.23.6" description = "Pytest support for asyncio" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.2.tar.gz", hash = "sha256:c16052382554c7b22d48782ab3438d5b10f8cf7a4bdcae7f0f67f097d95beecc"}, - {file = "pytest_asyncio-0.23.2-py3-none-any.whl", hash = "sha256:ea9021364e32d58f0be43b91c6233fb8d2224ccef2398d6837559e587682808f"}, + {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, + {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, ] [package.dependencies] -pytest = ">=7.0.0" +pytest = ">=7.0.0,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -890,14 +955,14 @@ pytest-metadata = "*" [[package]] name = "pytest-metadata" -version = "3.0.0" +version = "3.1.1" description = "pytest plugin for test session metadata" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" 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"}, + {file = "pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b"}, + {file = "pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8"}, ] [package.dependencies] @@ -906,6 +971,18 @@ 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 = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + [[package]] name = "requests" version = "2.31.0" @@ -930,20 +1007,19 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.0" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -962,26 +1038,50 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.3" +version = "0.12.4" description = "Style preserving TOML library" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, - {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, +] + +[[package]] +name = "types-protobuf" +version = "4.24.0.20240408" +description = "Typing stubs for protobuf" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-protobuf-4.24.0.20240408.tar.gz", hash = "sha256:c03a44357b03c233c8c5864ce3e07dd9c766a00497d271496923f7ae3cb9e1de"}, + {file = "types_protobuf-4.24.0.20240408-py3-none-any.whl", hash = "sha256:9b87cd279378693071247227f52e89738af7c8d6f06dbdd749b0cf473c4916ce"}, +] + +[[package]] +name = "types-pytz" +version = "2024.1.0.20240203" +description = "Typing stubs for pytz" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pytz-2024.1.0.20240203.tar.gz", hash = "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e"}, + {file = "types_pytz-2024.1.0.20240203-py3-none-any.whl", hash = "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3"}, ] [[package]] name = "types-requests" -version = "2.31.0.10" +version = "2.31.0.20240406" description = "Typing stubs for requests" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.10.tar.gz", hash = "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92"}, - {file = "types_requests-2.31.0.10-py3-none-any.whl", hash = "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc"}, + {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, + {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, ] [package.dependencies] @@ -989,30 +1089,61 @@ urllib3 = ">=2" [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, ] +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "urllib3" -version = "2.1.0" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1098,5 +1229,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = ">=3.8,<3.11" -content-hash = "56ab342d644d11266f452ce4f3ae66ee054b4366839ea554cb3b94d79828edf6" +python-versions = ">=3.9,<3.12" +content-hash = "d7f680ddab9cf52fa424bcb3a374e4782acbf42221405e834dc40d3675670a6a" diff --git a/demos/python/tutorial/pyproject.toml b/demos/python/tutorial/pyproject.toml index 9c5d2e21..2385318b 100644 --- a/demos/python/tutorial/pyproject.toml +++ b/demos/python/tutorial/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "open-gopro-tutorials" -version = "0.2.0" +version = "0.3.0" description = "Open GoPro Python Tutorials" authors = ["Tim Camise "] license = "MIT" @@ -18,20 +18,25 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] [tool.poetry.dependencies] -python = ">=3.8,<3.11" +python = ">=3.9,<3.12" bleak = "0.21.1" requests = "^2" rich = "^13" +pytz = "^2024.1" +tzlocal = "^5.2" +mypy-protobuf = "*" [tool.poetry.group.dev.dependencies] +types-pytz = "^2024.1.0.20240203" poethepoet = "^0" black = "*" +isort = "*" mypy = "*" pytest = "^7" pytest-asyncio = "^0" @@ -40,6 +45,7 @@ pytest-cov = "^4" coverage = { extras = ["toml"], version = "^6" } pylint = "^2" types-requests = "*" +protoletariat = "^3" [build-system] requires = ["poetry-core"] @@ -47,39 +53,47 @@ build-backend = "poetry.core.masonry.api" [tool.poe.tasks.tests] cmd = "pytest tests --cov-fail-under=60" -help = "Run end-to-end tests" +help = "Run end-to-end tests. Requires ssid=${SSID} and password=${ssid} args to connect to AP" -[tool.poe.tasks.types] +[tool.poe.tasks._types] cmd = "mypy tutorial_modules" help = "Check types" -[tool.poe.tasks.lint] -cmd = "pylint --no-docstring-rgx=main tutorial_modules" +[tool.poe.tasks._pylint] +cmd = "pylint --no-docstring-rgx=main|_ tutorial_modules" help = "Run pylint" +[tool.poe.tasks._sort_imports] +cmd = "isort open_gopro tests" +help = "Sort imports with isort" + [tool.poe.tasks.format] cmd = "black tutorial_modules tests" help = "Apply black formatting to source code" -[tool.poe.tasks.clean_artifacts] +[tool.poe.tasks._clean_artifacts] cmd = "rm -rf **/__pycache__ *.csv *.mp4 *.jpg *.log .mypy_cache .nox" help = "Clean testing artifacts and pycache" -[tool.poe.tasks.clean_tests] +[tool.poe.tasks._clean_tests] cmd = "rm -rf .reports && rm -rf .pytest_cache" help = "Clean test reports" -[tool.poe.tasks.clean_build] +[tool.poe.tasks._clean_build] cmd = "rm -rf dist" help = "Clean module build output" [tool.poe.tasks.clean] -sequence = ["clean_artifacts", "clean_tests", "clean_build"] +sequence = ["_clean_artifacts", "_clean_tests", "_clean_build"] help = "Clean everything" -[tool.poe.tasks.all] -sequence = ["format", "types", "lint", "tests"] -help = "Format, check types, lint, check docstrings, and run end-to-end tests" +[tool.poe.tasks.protobuf] +cmd = "bash ../../../tools/build_python_protos.sh tutorial_modules" +help = "generate protobuf source from .proto (assumes protoc >= 3.20.1 available)" + +[tool.poe.tasks.lint] +sequence = ["format", "_sort_imports", "_types", "_pylint",] +help = "Format, check types, lint, and check docstrings" [tool.mypy] ignore_missing_imports = true @@ -126,7 +140,6 @@ directory = ".reports/coverage" exclude_lines = ["raise NotImplementedError"] [tool.pylint.'MASTER'] -extension-pkg-whitelist = "cv2" # TODO this isn't working load-plugins = "pylint.extensions.docparams" accept-no-param-doc = "yes" accept-no-return-doc = "yes" @@ -165,8 +178,11 @@ disable = [ ] [tool.pylint.'FORMAT'] -max-line-length = 160 +max-line-length = 300 # Handled by black [tool.black] -line-length = 111 +line-length = 120 exclude = ".venv" + +[tool.isort] +profile = "black" diff --git a/demos/python/tutorial/tests/conftest.py b/demos/python/tutorial/tests/conftest.py index cd065bdd..6788e510 100644 --- a/demos/python/tutorial/tests/conftest.py +++ b/demos/python/tutorial/tests/conftest.py @@ -7,6 +7,12 @@ import pytest + +def pytest_addoption(parser): + parser.addoption("--ssid", action="store", required=True) + parser.addoption("--password", action="store", required=True) + + ############################################################################################################## # Log Management ############################################################################################################## @@ -15,7 +21,7 @@ @pytest.fixture(scope="class", autouse=True) def manage_logs(request): log_file = Path(request.node.name + ".log") - request.config.pluginmanager.get_plugin("logging-plugin").set_log_path(Path("reports") / "logs" / log_file) + request.config.pluginmanager.get_plugin("logging-plugin").set_log_path(Path(".reports") / "logs" / log_file) @pytest.fixture(scope="function", autouse=True) diff --git a/demos/python/tutorial/tests/test_tutorials.py b/demos/python/tutorial/tests/test_tutorials.py index 66ff099f..7a828644 100644 --- a/demos/python/tutorial/tests/test_tutorials.py +++ b/demos/python/tutorial/tests/test_tutorials.py @@ -4,20 +4,25 @@ # Simply run each demo for some naive sanity checking import time +from pathlib import Path import pytest from tutorial_modules.tutorial_1_connect_ble.ble_connect import main as ble_connect -from tutorial_modules.tutorial_2_send_ble_commands.ble_command_load_group import main as ble_command_load_group -from tutorial_modules.tutorial_2_send_ble_commands.ble_command_set_fps import main as ble_command_set_fps +from tutorial_modules.tutorial_2_send_ble_commands.ble_command_load_group import ( + main as ble_command_load_group, +) +from tutorial_modules.tutorial_2_send_ble_commands.ble_command_set_fps import ( + main as ble_command_set_fps, +) from tutorial_modules.tutorial_2_send_ble_commands.ble_command_set_resolution import ( main as ble_command_set_resolution, ) from tutorial_modules.tutorial_2_send_ble_commands.ble_command_set_shutter import ( main as ble_command_set_shutter, ) -from tutorial_modules.tutorial_3_parse_ble_tlv_responses.ble_command_get_state import ( - main as ble_command_get_state, +from tutorial_modules.tutorial_3_parse_ble_tlv_responses.ble_command_get_hardware_info import ( + main as ble_get_hardware_info, ) from tutorial_modules.tutorial_3_parse_ble_tlv_responses.ble_command_get_version import ( main as ble_command_get_version, @@ -31,35 +36,45 @@ from tutorial_modules.tutorial_4_ble_queries.ble_query_register_resolution_value_updates import ( main as ble_query_register_resolution_value_updates, ) -from tutorial_modules.tutorial_5_connect_wifi.wifi_enable import main as wifi_enable -from tutorial_modules.tutorial_6_send_wifi_commands.wifi_command_get_media_list import ( +from tutorial_modules.tutorial_5_ble_protobuf.set_turbo_mode import ( + main as set_turbo_mode, +) +from tutorial_modules.tutorial_6_connect_wifi.connect_as_sta import main as connect_sta +from tutorial_modules.tutorial_6_connect_wifi.enable_wifi_ap import main as wifi_enable +from tutorial_modules.tutorial_7_send_wifi_commands.wifi_command_get_media_list import ( main as wifi_command_get_media_list, ) -from tutorial_modules.tutorial_6_send_wifi_commands.wifi_command_get_state import ( +from tutorial_modules.tutorial_7_send_wifi_commands.wifi_command_get_state import ( main as wifi_command_get_state, ) -from tutorial_modules.tutorial_6_send_wifi_commands.wifi_command_load_group import ( +from tutorial_modules.tutorial_7_send_wifi_commands.wifi_command_load_group import ( main as wifi_command_load_group, ) -from tutorial_modules.tutorial_6_send_wifi_commands.wifi_command_preview_stream import ( +from tutorial_modules.tutorial_7_send_wifi_commands.wifi_command_preview_stream import ( main as wifi_command_preview_stream, ) -from tutorial_modules.tutorial_6_send_wifi_commands.wifi_command_set_resolution import ( +from tutorial_modules.tutorial_7_send_wifi_commands.wifi_command_set_resolution import ( main as wifi_command_set_resolution, ) -from tutorial_modules.tutorial_6_send_wifi_commands.wifi_command_set_shutter import ( +from tutorial_modules.tutorial_7_send_wifi_commands.wifi_command_set_shutter import ( main as wifi_command_set_shutter, ) -from tutorial_modules.tutorial_7_camera_media_list.wifi_media_download_file import ( +from tutorial_modules.tutorial_8_camera_media_list.wifi_media_download_file import ( main as wifi_media_download_file, ) -from tutorial_modules.tutorial_7_camera_media_list.wifi_media_get_gpmf import main as wifi_media_get_gpmf -from tutorial_modules.tutorial_7_camera_media_list.wifi_media_get_screennail import ( +from tutorial_modules.tutorial_8_camera_media_list.wifi_media_get_gpmf import ( + main as wifi_media_get_gpmf, +) +from tutorial_modules.tutorial_8_camera_media_list.wifi_media_get_screennail import ( main as wifi_media_get_screennail, ) -from tutorial_modules.tutorial_7_camera_media_list.wifi_media_get_thumbnail import ( +from tutorial_modules.tutorial_8_camera_media_list.wifi_media_get_thumbnail import ( main as wifi_media_get_thumbnail, ) +from tutorial_modules.tutorial_9_cohn.communicate_via_cohn import ( + main as communicate_via_cohn, +) +from tutorial_modules.tutorial_9_cohn.provision_cohn import main as provision_cohn @pytest.fixture(scope="module") @@ -80,28 +95,28 @@ class TestTutorial2SendBleCommands: async def test_ble_command_load_group(self): await ble_command_load_group(None) - @pytest.mark.asyncio - async def test_ble_command_set_fps(self): - await ble_command_set_fps(None) - @pytest.mark.asyncio async def test_ble_command_set_resolution(self): await ble_command_set_resolution(None) + @pytest.mark.asyncio + async def test_ble_command_set_fps(self): + await ble_command_set_fps(None) + @pytest.mark.asyncio async def test_ble_command_set_shutter(self): await ble_command_set_shutter(None) class TestTutorial3ParseBleTlvResponses: - @pytest.mark.asyncio - async def test_ble_command_get_state(self): - await ble_command_get_state(None) - @pytest.mark.asyncio async def test_ble_command_get_version(self): await ble_command_get_version(None) + @pytest.mark.asyncio + async def test_ble_get_hardware_info(self): + await ble_get_hardware_info(None) + class TestTutorial4BleQueries: @pytest.mark.asyncio @@ -123,7 +138,27 @@ async def test_wifi_enable(self): await wifi_enable(None, timeout=1) -class TestTutorial6SendWifiCommands: +class TestTutorial6BleProtobuf: + @pytest.mark.asyncio + async def test_set_turbo_mode(self): + await set_turbo_mode(None) + + +class TestTutorial9Cohn: + @pytest.mark.asyncio + async def test_connect_sta(self, pytestconfig): + await connect_sta(pytestconfig.getoption("ssid"), pytestconfig.getoption("password"), None) + + @pytest.mark.asyncio + async def test_cohn(self, pytestconfig): + credentials = await provision_cohn( + pytestconfig.getoption("ssid"), pytestconfig.getoption("password"), None, Path("cohn.crt") + ) + assert credentials + await communicate_via_cohn(credentials.ip_address, credentials.username, credentials.password, Path("cohn.crt")) + + +class TestTutorial7SendWifiCommands: def test_wifi_command_get_media_list(self, connect_wifi): wifi_command_get_media_list() @@ -144,7 +179,7 @@ def test_wifi_command_set_shutter(self, connect_wifi): time.sleep(2) # wait for camera to be ready -class TestTutorial7CameraMediaList: +class TestTutorial8CameraMediaList: def test_wifi_media_download_file(self, connect_wifi): wifi_media_download_file() diff --git a/demos/python/tutorial/tutorial_modules/__init__.py b/demos/python/tutorial/tutorial_modules/__init__.py index f262bc0a..01f65f95 100644 --- a/demos/python/tutorial/tutorial_modules/__init__.py +++ b/demos/python/tutorial/tutorial_modules/__init__.py @@ -3,7 +3,7 @@ # pylint: disable=wrong-import-position -from typing import Callable +from typing import Awaitable, Callable, Any import logging from bleak.backends.characteristic import BleakGATTCharacteristic @@ -17,7 +17,7 @@ sh.setFormatter(stream_formatter) sh.setLevel(logging.DEBUG) logger.addHandler(sh) -logger.setLevel(logging.INFO) +logger.setLevel(logging.DEBUG) bleak_logger = logging.getLogger("bleak") bleak_logger.setLevel(logging.WARNING) @@ -28,9 +28,15 @@ GOPRO_BASE_UUID = "b5f9{}-aa8d-11e3-9046-0002a5d5c51b" GOPRO_BASE_URL = "http://10.5.5.9:8080" -noti_handler_T = Callable[[BleakGATTCharacteristic, bytearray], None] +noti_handler_T = Callable[[BleakGATTCharacteristic, bytearray], Awaitable[None]] from tutorial_modules.tutorial_1_connect_ble.ble_connect import connect_ble -from tutorial_modules.tutorial_3_parse_ble_tlv_responses.ble_command_get_state import Response -from tutorial_modules.tutorial_5_connect_wifi.wifi_enable import enable_wifi -from tutorial_modules.tutorial_6_send_wifi_commands.wifi_command_get_media_list import get_media_list +from tutorial_modules.tutorial_2_send_ble_commands.ble_command_set_shutter import GoProUuid +from tutorial_modules.tutorial_3_parse_ble_tlv_responses.ble_command_get_hardware_info import Response, TlvResponse +from tutorial_modules.tutorial_4_ble_queries.ble_query_poll_resolution_value import QueryResponse, Resolution +from tutorial_modules.tutorial_6_connect_wifi.enable_wifi_ap import enable_wifi +from tutorial_modules.tutorial_7_send_wifi_commands.wifi_command_get_media_list import get_media_list +from tutorial_modules.tutorial_5_ble_protobuf import proto +from tutorial_modules.tutorial_5_ble_protobuf.set_turbo_mode import ProtobufResponse +from tutorial_modules.tutorial_5_ble_protobuf.decipher_response import ResponseManager +from tutorial_modules.tutorial_6_connect_wifi.connect_as_sta import connect_to_access_point diff --git a/demos/python/tutorial/tutorial_modules/tutorial_1_connect_ble/ble_connect.py b/demos/python/tutorial/tutorial_modules/tutorial_1_connect_ble/ble_connect.py index 5abf9e7e..292635a0 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_1_connect_ble/ble_connect.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_1_connect_ble/ble_connect.py @@ -5,7 +5,7 @@ import sys import asyncio import argparse -from typing import Dict, Any, List, Optional +from typing import Any from bleak import BleakScanner, BleakClient from bleak.backends.device import BLEDevice as BleakDevice @@ -13,7 +13,7 @@ from tutorial_modules import logger, noti_handler_T -def exception_handler(loop: asyncio.AbstractEventLoop, context: Dict[str, Any]) -> None: +def exception_handler(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None: """Catch exceptions from non-main thread Args: @@ -25,7 +25,7 @@ def exception_handler(loop: asyncio.AbstractEventLoop, context: Dict[str, Any]) logger.critical("This is unexpected and unrecoverable.") -async def connect_ble(notification_handler: noti_handler_T, identifier: Optional[str] = None) -> BleakClient: +async def connect_ble(notification_handler: noti_handler_T, identifier: str | None = None) -> BleakClient: """Connect to a GoPro, then pair, and enable notifications If identifier is None, the first discovered GoPro will be connected to. @@ -49,7 +49,7 @@ async def connect_ble(notification_handler: noti_handler_T, identifier: Optional for retry in range(RETRIES): try: # Map of discovered devices indexed by name - devices: Dict[str, BleakDevice] = {} + devices: dict[str, BleakDevice] = {} # Scan for devices logger.info("Scanning for bluetooth devices...") @@ -62,17 +62,17 @@ def _scan_callback(device: BleakDevice, _: Any) -> None: devices[device.name] = device # Scan until we find devices - matched_devices: List[BleakDevice] = [] + matched_devices: list[BleakDevice] = [] while len(matched_devices) == 0: # Now get list of connectable advertisements for device in await BleakScanner.discover(timeout=5, detection_callback=_scan_callback): - if device.name != "Unknown" and device.name is not None: + if device.name and device.name != "Unknown": devices[device.name] = device # Log every device we discovered for d in devices: logger.info(f"\tDiscovered: {d}") # Now look for our matching device(s) - token = re.compile(r"GoPro [A-Z0-9]{4}" if identifier is None else f"GoPro {identifier}") + token = re.compile(identifier or r"GoPro [A-Z0-9]{4}") matched_devices = [device for name, device in devices.items() if token.match(name)] logger.info(f"Found {len(matched_devices)} matching devices.") @@ -101,6 +101,7 @@ def _scan_callback(device: BleakDevice, _: Any) -> None: logger.info(f"Enabling notification on char {char.uuid}") await client.start_notify(char, notification_handler) logger.info("Done enabling notifications") + logger.info("BLE Connection is ready for communication.") return client except Exception as exc: # pylint: disable=broad-exception-caught @@ -110,9 +111,8 @@ def _scan_callback(device: BleakDevice, _: Any) -> None: raise RuntimeError(f"Couldn't establish BLE connection after {RETRIES} retries") -async def main(identifier: Optional[str]) -> None: - def dummy_notification_handler(*_: Any) -> None: - ... +async def main(identifier: str | None) -> None: + async def dummy_notification_handler(*_: Any) -> None: ... client = await connect_ble(dummy_notification_handler, identifier) await client.disconnect() diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_connect_wifi/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/__init__.py similarity index 58% rename from demos/python/tutorial/tutorial_modules/tutorial_5_connect_wifi/__init__.py rename to demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/__init__.py index 005ca0cb..e3af0028 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_5_connect_wifi/__init__.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/__init__.py @@ -1,2 +1,2 @@ # __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:06:01 PM +# This copyright was auto-generated on Thu Apr 4 21:50:02 UTC 2024 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_load_group.py b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_load_group.py index 8d864905..fff08620 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_load_group.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_load_group.py @@ -4,30 +4,26 @@ import sys import asyncio import argparse -from typing import Optional from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, logger, noti_handler_T +from tutorial_modules import connect_ble, logger, GoProUuid -async def main(identifier: Optional[str]) -> None: +async def main(identifier: str | None) -> None: # Synchronization event to wait until notification response is received event = asyncio.Event() - - # UUIDs to write to and receive responses from - COMMAND_REQ_UUID = GOPRO_BASE_UUID.format("0072") - COMMAND_RSP_UUID = GOPRO_BASE_UUID.format("0073") - response_uuid = COMMAND_RSP_UUID - client: BleakClient + request_uuid = GoProUuid.COMMAND_REQ_UUID + response_uuid = GoProUuid.COMMAND_RSP_UUID - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') # If this is the correct handle and the status is success, the command was a success - if client.services.characteristics[characteristic.handle].uuid == response_uuid and data[2] == 0x00: + if uuid is response_uuid and data[2] == 0x00: logger.info("Command sent successfully") # Anything else is unexpected. This shouldn't happen else: @@ -40,16 +36,16 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - # Write to command request BleUUID to load the video preset group logger.info("Loading the video preset group...") + request = bytes([0x04, 0x3E, 0x02, 0x03, 0xE8]) + logger.debug(f"Sending to {request_uuid}: {request.hex(':')}") event.clear() - await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x04, 0x3E, 0x02, 0x03, 0xE8]), response=True) + await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response await client.disconnect() if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Connect to a GoPro camera, then change the Preset Group to Video." - ) + parser = argparse.ArgumentParser(description="Connect to a GoPro camera, then change the Preset Group to Video.") parser.add_argument( "-i", "--identifier", @@ -61,7 +57,7 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - try: asyncio.run(main(args.identifier)) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(e) sys.exit(-1) else: diff --git a/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_fps.py b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_fps.py index cee7e0d1..03cd8d20 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_fps.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_fps.py @@ -4,31 +4,28 @@ import sys import asyncio import argparse -from typing import Optional -from binascii import hexlify from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, logger +from tutorial_modules import connect_ble, logger, GoProUuid -async def main(identifier: Optional[str]) -> None: +async def main(identifier: str | None) -> None: # Synchronization event to wait until notification response is received event = asyncio.Event() - # UUIDs to write to and receive responses from - SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") - SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") - response_uuid = SETTINGS_RSP_UUID + request_uuid = GoProUuid.SETTINGS_REQ_UUID + response_uuid = GoProUuid.SETTINGS_RSP_UUID client: BleakClient - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') # If this is the correct handle and the status is success, the command was a success - if client.services.characteristics[characteristic.handle].uuid == response_uuid and data[2] == 0x00: + if uuid is response_uuid and data[2] == 0x00: logger.info("Command sent successfully") # Anything else is unexpected. This shouldn't happen else: @@ -41,16 +38,16 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - # Write to command request BleUUID to change the fps to 240 logger.info("Setting the fps to 240") + request = bytes([0x03, 0x03, 0x01, 0x00]) + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") event.clear() - await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x03, 0x01, 0x00]), response=True) + await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response await client.disconnect() if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Connect to a GoPro camera, then attempt to change the fps to 240." - ) + parser = argparse.ArgumentParser(description="Connect to a GoPro camera, then attempt to change the fps to 240.") parser.add_argument( "-i", "--identifier", @@ -62,7 +59,7 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - try: asyncio.run(main(args.identifier)) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(e) sys.exit(-1) else: diff --git a/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_resolution.py b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_resolution.py index 98157f25..c9030ab3 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_resolution.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_resolution.py @@ -4,31 +4,27 @@ import sys import asyncio import argparse -from typing import Optional -from binascii import hexlify from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, logger +from tutorial_modules import GoProUuid, connect_ble, logger -async def main(identifier: Optional[str]) -> None: +async def main(identifier: str | None) -> None: # Synchronization event to wait until notification response is received event = asyncio.Event() - - # UUIDs to write to and receive responses from - SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") - SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") - response_uuid = SETTINGS_RSP_UUID + response_uuid = GoProUuid.SETTINGS_RSP_UUID + request_uuid = GoProUuid.SETTINGS_REQ_UUID client: BleakClient - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') # If this is the correct handle and the status is success, the command was a success - if client.services.characteristics[characteristic.handle].uuid == response_uuid and data[2] == 0x00: + if uuid == response_uuid and data[2] == 0x00: logger.info("Command sent successfully") # Anything else is unexpected. This shouldn't happen else: @@ -41,16 +37,16 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - # Write to command request BleUUID to change the video resolution to 1080 logger.info("Setting the video resolution to 1080") + request = bytes([0x03, 0x02, 0x01, 0x09]) + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") event.clear() - await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x02, 0x01, 0x09]), response=True) + await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response await client.disconnect() if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Connect to a GoPro camera, then change the resolution to 1080." - ) + parser = argparse.ArgumentParser(description="Connect to a GoPro camera, then change the resolution to 1080.") parser.add_argument( "-i", "--identifier", @@ -62,7 +58,7 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - try: asyncio.run(main(args.identifier)) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(e) sys.exit(-1) else: diff --git a/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_shutter.py b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_shutter.py index 6c984e2a..006fd939 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_shutter.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/ble_command_set_shutter.py @@ -1,11 +1,12 @@ # ble_command_set_shutter.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:58 PM +from __future__ import annotations import sys -import time +import enum import asyncio import argparse -from typing import Optional +from typing import Callable, TypeVar from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic @@ -13,22 +14,52 @@ from tutorial_modules import GOPRO_BASE_UUID, connect_ble, logger -async def main(identifier: Optional[str]) -> None: - # Synchronization event to wait until notification response is received - event = asyncio.Event() +T = TypeVar("T") + + +class GoProUuid(str, enum.Enum): + """UUIDs to write to and receive responses from""" - # UUIDs to write to and receive responses from COMMAND_REQ_UUID = GOPRO_BASE_UUID.format("0072") COMMAND_RSP_UUID = GOPRO_BASE_UUID.format("0073") - response_uuid = COMMAND_RSP_UUID + SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") + SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") + CONTROL_QUERY_SERVICE_UUID = "0000fea6-0000-1000-8000-00805f9b34fb" + INTERNAL_UUID = "00002a19-0000-1000-8000-00805f9b34fb" + QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") + QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077") + WIFI_AP_SSID_UUID = GOPRO_BASE_UUID.format("0002") + WIFI_AP_PASSWORD_UUID = GOPRO_BASE_UUID.format("0003") + NETWORK_MANAGEMENT_REQ_UUID = GOPRO_BASE_UUID.format("0091") + NETWORK_MANAGEMENT_RSP_UUID = GOPRO_BASE_UUID.format("0092") + + @classmethod + def dict_by_uuid(cls, value_creator: Callable[[GoProUuid], T]) -> dict[GoProUuid, T]: + """Build a dict where the keys are each UUID defined here and the values are built from the input value_creator. + + Args: + value_creator (Callable[[GoProUuid], T]): callable to create the values from each UUID + + Returns: + dict[GoProUuid, T]: uuid-to-value mapping. + """ + return {uuid: value_creator(uuid) for uuid in cls} + + +async def main(identifier: str | None) -> None: + # Synchronization event to wait until notification response is received + event = asyncio.Event() + request_uuid = GoProUuid.COMMAND_REQ_UUID + response_uuid = GoProUuid.COMMAND_RSP_UUID client: BleakClient - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') # If this is the correct handle and the status is success, the command was a success - if client.services.characteristics[characteristic.handle].uuid == response_uuid and data[2] == 0x00: + if uuid is response_uuid and data[2] == 0x00: logger.info("Command sent successfully") # Anything else is unexpected. This shouldn't happen else: @@ -42,14 +73,18 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - # Write to command request BleUUID to turn the shutter on logger.info("Setting the shutter on") event.clear() - await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 1]), response=True) + request = bytes([3, 1, 1, 1]) + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") + await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response - time.sleep(2) # If we're recording, let's wait 2 seconds (i.e. take a 2 second video) + await asyncio.sleep(2) # If we're recording, let's wait 2 seconds (i.e. take a 2 second video) # Write to command request BleUUID to turn the shutter off logger.info("Setting the shutter off") - # event.clear() - await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 0]), response=True) + request = bytes([3, 1, 1, 0]) + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") + event.clear() + await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response await client.disconnect() @@ -69,7 +104,7 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - try: asyncio.run(main(args.identifier)) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(e) sys.exit(-1) else: diff --git a/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/__init__.py new file mode 100644 index 00000000..e3af0028 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/__init__.py @@ -0,0 +1,2 @@ +# __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Thu Apr 4 21:50:02 UTC 2024 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_hardware_info.py b/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_hardware_info.py new file mode 100644 index 00000000..8a870e7b --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_hardware_info.py @@ -0,0 +1,238 @@ +# ble_command_get_state.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:59 PM + +from __future__ import annotations +import sys +import json +import enum +import asyncio +import argparse +from typing import TypeVar +from dataclasses import dataclass, asdict + +from bleak import BleakClient +from bleak.backends.characteristic import BleakGATTCharacteristic + +from tutorial_modules import GoProUuid, connect_ble, logger + +T = TypeVar("T", bound="Response") + + +class Response: + """The base class to encapsulate all BLE Responses + + Args: + uuid (GoProUuid): UUID that this response was received on. + """ + + def __init__(self, uuid: GoProUuid) -> None: + """Constructor""" + self.bytes_remaining = 0 + self.uuid = uuid + self.raw_bytes = bytearray() + + @classmethod + def from_received_response(cls: type[T], received_response: Response) -> T: + """Build a new response from a received response. + + Can be used by subclasses for essentially casting into their derived type. + + Args: + cls (type[T]): type of response to build + received_response (Response): received response to build from + + Returns: + T: built response. + """ + response = cls(received_response.uuid) + response.bytes_remaining = 0 + response.raw_bytes = received_response.raw_bytes + return response + + @property + def is_received(self) -> bool: + """Have all of the bytes identified by the length header been received? + + Returns: + bool: True if received, False otherwise. + """ + return len(self.raw_bytes) > 0 and self.bytes_remaining == 0 + + def accumulate(self, data: bytes) -> None: + """Accumulate a current packet in to the received response. + + Args: + data (bytes): bytes to accumulate. + """ + CONT_MASK = 0b10000000 + HDR_MASK = 0b01100000 + GEN_LEN_MASK = 0b00011111 + EXT_13_BYTE0_MASK = 0b00011111 + + class Header(enum.Enum): + """Header Type Identifiers""" + + GENERAL = 0b00 + EXT_13 = 0b01 + EXT_16 = 0b10 + RESERVED = 0b11 + + 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_bytes = 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.raw_bytes.extend(buf) + self.bytes_remaining -= len(buf) + logger.debug(f"{self.bytes_remaining=}") + + +class TlvResponse(Response): + """A Type Length Value TLV Response. + + TLV response all have an ID, status, and payload. + """ + + def __init__(self, uuid: GoProUuid) -> None: + super().__init__(uuid) + self.id: int + self.status: int + self.payload: bytes + + def parse(self) -> None: + """Extract the ID, status, and payload""" + self.id = self.raw_bytes[0] + self.status = self.raw_bytes[1] + self.payload = bytes(self.raw_bytes[2:]) + + +@dataclass +class HardwareInfo: + """The meaningful values from a hardware info response""" + + model_number: int + model_name: str + firmware_version: str + serial_number: str + ap_ssid: str + ap_mac_address: str + + def __str__(self) -> str: + return json.dumps(asdict(self), indent=4) + + @classmethod + def from_bytes(cls, data: bytes) -> HardwareInfo: + """Parse and build from a raw hardware info response bytestream + + Args: + data (bytes): bytestream to parse + + Returns: + HardwareInfo: Parsed response. + """ + buf = bytearray(data) + # Get model number + model_num_length = buf.pop(0) + model = int.from_bytes(buf[:model_num_length], "big", signed=False) + buf = buf[model_num_length:] + # Get model name + model_name_length = buf.pop(0) + model_name = (buf[:model_name_length]).decode() + buf = buf[model_name_length:] + # Advance past deprecated bytes + deprecated_length = buf.pop(0) + buf = buf[deprecated_length:] + # Get firmware version + firmware_length = buf.pop(0) + firmware = (buf[:firmware_length]).decode() + buf = buf[firmware_length:] + # Get serial number + serial_length = buf.pop(0) + serial = (buf[:serial_length]).decode() + buf = buf[serial_length:] + # Get AP SSID + ssid_length = buf.pop(0) + ssid = (buf[:ssid_length]).decode() + buf = buf[ssid_length:] + # Get MAC address + mac_length = buf.pop(0) + mac = (buf[:mac_length]).decode() + buf = buf[mac_length:] + + return cls(model, model_name, firmware, serial, ssid, mac) + + +async def main(identifier: str | None) -> None: + client: BleakClient + responses_by_uuid = GoProUuid.dict_by_uuid(TlvResponse) + received_responses: asyncio.Queue[TlvResponse] = asyncio.Queue() + + request_uuid = GoProUuid.COMMAND_REQ_UUID + response_uuid = GoProUuid.COMMAND_RSP_UUID + + async def tlv_notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') + + response = responses_by_uuid[uuid] + response.accumulate(data) + + if response.is_received: + # If this is the correct handle, enqueue it for processing + if uuid is response_uuid: + logger.info("Received the get hardware info response") + await received_responses.put(response) + # Anything else is unexpected. This shouldn't happen + else: + logger.error("Unexpected response") + # Reset the per-UUID response + responses_by_uuid[uuid] = TlvResponse(uuid) + + client = await connect_ble(tlv_notification_handler, identifier) + + # Write to command request BleUUID to get the hardware info + logger.info("Getting the camera's hardware info...") + request = bytearray([0x01, 0x3C]) + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") + await client.write_gatt_char(request_uuid.value, request, response=True) + response = await received_responses.get() + # Parse TLV headers and Payload + response.parse() + # Now parse payload into human readable object + hardware_info = HardwareInfo.from_bytes(response.payload) + logger.info(f"Parsed hardware info: {hardware_info}") + + await client.disconnect() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Connect to a GoPro camera via BLE, then get its hardware info.") + parser.add_argument( + "-i", + "--identifier", + type=str, + help="Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to", + default=None, + ) + args = parser.parse_args() + + try: + asyncio.run(main(args.identifier)) + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(e) + sys.exit(-1) + else: + sys.exit(0) diff --git a/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_state.py b/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_state.py deleted file mode 100644 index 17251b96..00000000 --- a/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_state.py +++ /dev/null @@ -1,157 +0,0 @@ -# ble_command_get_state.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:59 PM - -import sys -import json -import enum -import asyncio -import argparse -from binascii import hexlify -from typing import Dict, Optional - -from bleak import BleakClient -from bleak.backends.characteristic import BleakGATTCharacteristic - -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, logger - - -class Response: - def __init__(self) -> None: - self.bytes_remaining = 0 - self.bytes = bytearray() - self.data: Dict[int, bytes] = {} - self.id: int - self.status: int - - def __str__(self) -> str: - return json.dumps(self.data, indent=4, default=lambda x: x.hex(":")) - - @property - def is_received(self) -> bool: - return len(self.bytes) > 0 and self.bytes_remaining == 0 - - def accumulate(self, data: bytes) -> None: - CONT_MASK = 0b10000000 - HDR_MASK = 0b01100000 - GEN_LEN_MASK = 0b00011111 - EXT_13_BYTE0_MASK = 0b00011111 - - class Header(enum.Enum): - GENERAL = 0b00 - EXT_13 = 0b01 - EXT_16 = 0b10 - RESERVED = 0b11 - - 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.bytes = 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.bytes.extend(buf) - self.bytes_remaining -= len(buf) - logger.info(f"{self.bytes_remaining=}") - - def parse(self) -> None: - self.id = self.bytes[0] - self.status = self.bytes[1] - buf = self.bytes[2:] - while len(buf) > 0: - # Get ID and Length - param_id = buf[0] - param_len = buf[1] - buf = buf[2:] - # Get the value - value = buf[:param_len] - - # Store in dict for later access - self.data[param_id] = value - - # Advance the buffer - buf = buf[param_len:] - - -async def main(identifier: Optional[str]) -> None: - # Synchronization event to wait until notification response is received - event = asyncio.Event() - - # UUIDs to write to and receive responses from - QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") - QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077") - response_uuid = QUERY_RSP_UUID - - client: BleakClient - response = Response() - - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') - - response.accumulate(data) - - if response.is_received: - response.parse() - - # If this is the correct handle and the status is success, the command was a success - if ( - client.services.characteristics[characteristic.handle].uuid == response_uuid - and response.status == 0 - ): - logger.info("Successfully received the response") - # Anything else is unexpected. This shouldn't happen - else: - logger.error("Unexpected response") - - # Notify writer that procedure is complete - event.set() - - client = await connect_ble(notification_handler, identifier) - - # Write to command request BleUUID to put the camera to sleep - logger.info("Getting the camera's settings...") - event.clear() - await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x01, 0x12]), response=True) - await event.wait() # Wait to receive the notification response - logger.info(f"Received settings\n: {response}") - - # Write to command request BleUUID to put the camera to sleep - logger.info("Getting the camera's statuses...") - event.clear() - await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x01, 0x13]), response=True) - await event.wait() # Wait to receive the notification response - logger.info(f"Received statuses\n: {response}") - - await client.disconnect() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Connect to a GoPro camera via BLE, then get its statuses and settings." - ) - parser.add_argument( - "-i", - "--identifier", - type=str, - help="Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to", - default=None, - ) - args = parser.parse_args() - - try: - asyncio.run(main(args.identifier)) - except Exception as e: - logger.error(e) - sys.exit(-1) - else: - sys.exit(0) diff --git a/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_version.py b/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_version.py index ed46bb9f..400168d3 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_version.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/ble_command_get_version.py @@ -4,47 +4,46 @@ import sys import asyncio import argparse -from typing import Dict, Optional from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, logger +from tutorial_modules import GoProUuid, connect_ble, logger -async def main(identifier: Optional[str]) -> None: +async def main(identifier: str | None) -> None: # Synchronization event to wait until notification response is received event = asyncio.Event() - # UUIDs to write to and receive responses from - COMMAND_REQ_UUID = GOPRO_BASE_UUID.format("0072") - COMMAND_RSP_UUID = GOPRO_BASE_UUID.format("0073") - response_uuid = COMMAND_RSP_UUID - client: BleakClient - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + request_uuid = GoProUuid.COMMAND_REQ_UUID + response_uuid = GoProUuid.COMMAND_RSP_UUID + + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response {uuid}: {data.hex(":")}') # If this is the correct handle and the status is success, the command was a success - if client.services.characteristics[characteristic.handle].uuid == response_uuid: - # First byte is the length for this command. + if uuid is response_uuid: + # First byte is the length of this response. length = data[0] # Second byte is the ID command_id = data[1] # Third byte is the status status = data[2] - index = 3 - params = [] - # Remaining bytes are individual values of (length...length bytes) - while index <= length: - param_len = data[index] - index += 1 - params.append(data[index : index + param_len]) - index += param_len - major, minor = params - - logger.info(f"Received a response to {command_id=} with {status=}") + # The remainder is the payload + payload = data[3 : length + 1] + logger.info(f"Received a response to {command_id=} with {status=}, payload={payload.hex(':')}") + + # Now parse the payload from the response documentation + major_length = payload[0] + payload.pop(0) + major = payload[:major_length] + payload.pop(major_length) + minor_length = payload[0] + payload.pop(0) + minor = payload[:minor_length] logger.info(f"The version is Open GoPro {major[0]}.{minor[0]}") # Anything else is unexpected. This shouldn't happen @@ -59,16 +58,15 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - # Write to command request BleUUID to get the Open GoPro Version logger.info("Getting the Open GoPro version...") event.clear() - await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x01, 0x51]), response=True) + request = bytes([0x01, 0x51]) + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") + await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response - await client.disconnect() if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Connect to a GoPro camera via BLE, then get the Open GoPro version." - ) + parser = argparse.ArgumentParser(description="Connect to a GoPro camera via BLE, then get the Open GoPro version.") parser.add_argument( "-i", "--identifier", @@ -80,7 +78,7 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - try: asyncio.run(main(args.identifier)) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(e) sys.exit(-1) else: diff --git a/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/__init__.py new file mode 100644 index 00000000..e3af0028 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/__init__.py @@ -0,0 +1,2 @@ +# __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Thu Apr 4 21:50:02 UTC 2024 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_poll_multiple_setting_values.py b/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_poll_multiple_setting_values.py index bc2b6c6a..4c69192e 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_poll_multiple_setting_values.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_poll_multiple_setting_values.py @@ -5,29 +5,17 @@ import enum import asyncio import argparse -from typing import Optional from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, Response - -from tutorial_modules import logger - - -# Note these may change based on the Open GoPro version! -class Resolution(enum.Enum): - RES_4K = 1 - RES_2_7K = 4 - RES_2_7K_4_3 = 6 - RES_1440 = 7 - RES_1080 = 9 - RES_4K_4_3 = 18 - RES_5K = 24 +from tutorial_modules import GoProUuid, connect_ble, QueryResponse, logger, Resolution # Note these may change based on the Open GoPro version! class FPS(enum.Enum): + """Common Frames-per-second values""" + FPS_240 = 0 FPS_120 = 1 FPS_100 = 2 @@ -40,6 +28,8 @@ class FPS(enum.Enum): # Note these may change based on the Open GoPro version! class VideoFOV(enum.Enum): + """Common Video Field of View values""" + FOV_WIDE = 0 FOV_NARROW = 2 FOV_SUPERVIEW = 3 @@ -48,67 +38,50 @@ class VideoFOV(enum.Enum): FOV_LINEAR_HORIZON_LEVELING = 8 -resolution: Resolution -fps: FPS -video_fov: VideoFOV - - -async def main(identifier: Optional[str]) -> None: - # Synchronization event to wait until notification response is received - event = asyncio.Event() - - # UUIDs to write to and receive responses from - QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") - QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077") - SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") - SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") - +async def main(identifier: str | None) -> None: RESOLUTION_ID = 2 FPS_ID = 3 FOV_ID = 121 client: BleakClient - response = Response() + responses_by_uuid = GoProUuid.dict_by_uuid(value_creator=QueryResponse) + received_responses: asyncio.Queue[QueryResponse] = asyncio.Queue() - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + query_request_uuid = GoProUuid.QUERY_REQ_UUID + query_response_uuid = GoProUuid.QUERY_RSP_UUID + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') + + response = responses_by_uuid[uuid] response.accumulate(data) # Notify the writer if we have received the entire response if response.is_received: - response.parse() - - # If this is query response, it must contain a resolution value - if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: - global resolution - global fps - global video_fov - resolution = Resolution(response.data[RESOLUTION_ID][0]) - fps = FPS(response.data[FPS_ID][0]) - video_fov = VideoFOV(response.data[FOV_ID][0]) - # If this is a setting response, it will just show the status - elif client.services.characteristics[characteristic.handle].uuid == SETTINGS_RSP_UUID: - logger.info("Command sent successfully") + # If this is query response, enqueue it + if uuid is query_response_uuid: + logger.info("Received the Query Response") + await received_responses.put(response) # Anything else is unexpected. This shouldn't happen else: logger.error("Unexpected response") - - # Notify writer that the procedure is complete - event.set() + # Reset the per-uuuid response + responses_by_uuid[uuid] = QueryResponse(uuid) client = await connect_ble(notification_handler, identifier) # Write to query BleUUID to poll the current resolution, fps, and fov - logger.info("Getting the current resolution, fps, and fov,") - event.clear() - await client.write_gatt_char( - QUERY_REQ_UUID, bytearray([0x04, 0x12, RESOLUTION_ID, FPS_ID, FOV_ID]), response=True - ) - await event.wait() # Wait to receive the notification response - logger.info(f"Resolution is currently {resolution}") - logger.info(f"Video FOV is currently {video_fov}") - logger.info(f"FPS is currently {fps}") + logger.info("Getting the current resolution, fps, and fov.") + request = bytes([0x04, 0x12, RESOLUTION_ID, FPS_ID, FOV_ID]) + logger.debug(f"Writing to {query_request_uuid}: {request.hex(':')}") + await client.write_gatt_char(query_request_uuid.value, request, response=True) + response = await received_responses.get() # Wait to receive the notification response + # Parse Query headers and query items + response.parse() + logger.info(f"Resolution is currently {Resolution(response.data[RESOLUTION_ID][0])}") + logger.info(f"Video FOV is currently {VideoFOV(response.data[FOV_ID][0])}") + logger.info(f"FPS is currently {FPS(response.data[FPS_ID][0])}") await client.disconnect() @@ -128,7 +101,7 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - try: asyncio.run(main(args.identifier)) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(e) sys.exit(-1) else: diff --git a/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_poll_resolution_value.py b/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_poll_resolution_value.py index 0e48088a..b559ffea 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_poll_resolution_value.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_poll_resolution_value.py @@ -5,92 +5,131 @@ import enum import asyncio import argparse -from typing import Optional from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, Response +from tutorial_modules import GoProUuid, connect_ble, TlvResponse from tutorial_modules import logger +# Note these may change based on the Open GoPro version! class Resolution(enum.Enum): + """Common Resolution Values""" + RES_4K = 1 RES_2_7K = 4 RES_2_7K_4_3 = 6 + RES_1440 = 7 RES_1080 = 9 RES_4K_4_3 = 18 RES_5K = 24 -resolution: Resolution +class QueryResponse(TlvResponse): + """A TLV Response to a Query Operation. + + Args: + uuid (GoProUuid): _description_ + """ + def __init__(self, uuid: GoProUuid) -> None: + """Constructor""" + super().__init__(uuid) + self.data: dict[int, bytes] = {} -async def main(identifier: Optional[str]) -> None: - # Synchronization event to wait until notification response is received - event = asyncio.Event() + def parse(self) -> None: + """Perform common TLV parsing. Then also parse all Query elements into the data property""" + super().parse() + buf = bytearray(self.payload) + while len(buf) > 0: + # Get ID and Length of query parameter + param_id = buf[0] + param_len = buf[1] + buf = buf[2:] + # Get the value + value = buf[:param_len] + # Store in dict for later access + self.data[param_id] = bytes(value) - # UUIDs to write to and receive responses from - QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") - QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077") - SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") - SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") + # Advance the buffer + buf = buf[param_len:] + +async def main(identifier: str | None) -> None: RESOLUTION_ID = 2 client: BleakClient - response = Response() + responses_by_uuid = GoProUuid.dict_by_uuid(QueryResponse) + received_responses: asyncio.Queue[QueryResponse] = asyncio.Queue() + + query_request_uuid = GoProUuid.QUERY_REQ_UUID + query_response_uuid = GoProUuid.QUERY_RSP_UUID + setting_request_uuid = GoProUuid.SETTINGS_REQ_UUID + setting_response_uuid = GoProUuid.SETTINGS_RSP_UUID - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') + response = responses_by_uuid[uuid] response.accumulate(data) # Notify the writer if we have received the entire response if response.is_received: - response.parse() - # If this is query response, it must contain a resolution value - if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: - global resolution - resolution = Resolution(response.data[RESOLUTION_ID][0]) + if uuid is query_response_uuid: + logger.info("Received the Resolution Query response") + await received_responses.put(response) # If this is a setting response, it will just show the status - elif client.services.characteristics[characteristic.handle].uuid == SETTINGS_RSP_UUID: - logger.info("Command sent successfully") + elif uuid is setting_response_uuid: + logger.info("Received Set Setting command response.") + await received_responses.put(response) # Anything else is unexpected. This shouldn't happen else: logger.error("Unexpected response") - - # Notify writer that the procedure is complete - event.set() + # Reset per-uuid response + responses_by_uuid[uuid] = QueryResponse(uuid) client = await connect_ble(notification_handler, identifier) # Write to query BleUUID to poll the current resolution logger.info("Getting the current resolution") - event.clear() - await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x12, RESOLUTION_ID]), response=True) - await event.wait() # Wait to receive the notification response + request = bytes([0x02, 0x12, RESOLUTION_ID]) + logger.debug(f"Writing to {query_request_uuid}: {request.hex(':')}") + await client.write_gatt_char(query_request_uuid.value, request, response=True) + # Wait to receive the notification response + response = await received_responses.get() + response.parse() + resolution = Resolution(response.data[RESOLUTION_ID][0]) logger.info(f"Resolution is currently {resolution}") # Write to command request BleUUID to change the video resolution (either to 1080 or 2.7K) - new_resolution = Resolution.RES_2_7K if resolution is Resolution.RES_1080 else Resolution.RES_1080 - logger.info(f"Changing the resolution to {new_resolution}...") - event.clear() - await client.write_gatt_char( - SETTINGS_REQ_UUID, bytearray([0x03, 0x02, 0x01, new_resolution.value]), response=True - ) - await event.wait() # Wait to receive the notification response + target_resolution = Resolution.RES_2_7K if resolution is Resolution.RES_1080 else Resolution.RES_1080 + logger.info(f"Changing the resolution to {target_resolution}...") + request = bytes([0x03, 0x02, 0x01, target_resolution.value]) + logger.debug(f"Writing to {setting_request_uuid}: {request.hex(':')}") + await client.write_gatt_char(setting_request_uuid.value, request, response=True) + # Wait to receive the notification response + response = await received_responses.get() + response.parse() + # Ensure the setting was successful + assert response.status == 0x00 # Now let's poll again until we see the update occur - while resolution is not new_resolution: + while resolution is not target_resolution: logger.info("Polling the resolution to see if it has changed...") - event.clear() - await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x12, RESOLUTION_ID]), response=True) - await event.wait() # Wait to receive the notification response + request = bytes([0x02, 0x12, RESOLUTION_ID]) + logger.debug(f"Writing to {query_request_uuid}: {request.hex(':')}") + await client.write_gatt_char(query_request_uuid.value, request, response=True) + response = await received_responses.get() # Wait to receive the notification response + response.parse() + resolution = Resolution(response.data[RESOLUTION_ID][0]) logger.info(f"Resolution is currently {resolution}") + logger.info("Resolution has changed as expected. Exiting...") + await client.disconnect() @@ -109,7 +148,7 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - try: asyncio.run(main(args.identifier)) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(e) sys.exit(-1) else: diff --git a/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_register_resolution_value_updates.py b/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_register_resolution_value_updates.py index ad5d05af..1af98270 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_register_resolution_value_updates.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/ble_query_register_resolution_value_updates.py @@ -2,95 +2,86 @@ # This copyright was auto-generated on Wed, Sep 1, 2021 5:06:00 PM import sys -import enum import asyncio import argparse -from typing import Optional from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, Response +from tutorial_modules import GoProUuid, connect_ble, QueryResponse, Resolution from tutorial_modules import logger -# Note that this may change based on the Open GoPro version! -class Resolution(enum.Enum): - RES_4K = 1 - RES_2_7K = 4 - RES_2_7K_4_3 = 6 - RES_1080 = 9 - RES_4K_4_3 = 18 - RES_5K = 24 - - -resolution: Resolution - - -async def main(identifier: Optional[str]) -> None: - # Synchronization event to wait until notification response is received - event = asyncio.Event() - - # UUIDs to write to and receive responses from - SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") - SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") - QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") - QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077") - +async def main(identifier: str | None) -> None: RESOLUTION_ID = 2 client: BleakClient - response = Response() + responses_by_uuid = GoProUuid.dict_by_uuid(QueryResponse) + received_responses: asyncio.Queue[QueryResponse] = asyncio.Queue() - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + query_request_uuid = GoProUuid.QUERY_REQ_UUID + query_response_uuid = GoProUuid.QUERY_RSP_UUID + setting_request_uuid = GoProUuid.SETTINGS_REQ_UUID + setting_response_uuid = GoProUuid.SETTINGS_RSP_UUID + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') + + response = responses_by_uuid[uuid] response.accumulate(data) # Notify the writer if we have received the entire response if response.is_received: - response.parse() - # If this is query response, it must contain a resolution value - if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: - global resolution - resolution = Resolution(response.data[RESOLUTION_ID][0]) + if uuid is query_response_uuid: + logger.info("Received the Resolution Query response") + await received_responses.put(response) # If this is a setting response, it will just show the status - elif client.services.characteristics[characteristic.handle].uuid == SETTINGS_RSP_UUID: - logger.info("Command sent successfully") + elif uuid is setting_response_uuid: + logger.info("Received Set Setting command response.") + await received_responses.put(response) # Anything else is unexpected. This shouldn't happen else: logger.error("Unexpected response") - - # Notify writer that the procedure is complete - event.set() + # Reset the per-uuid response + responses_by_uuid[uuid] = QueryResponse(uuid) client = await connect_ble(notification_handler, identifier) # Register for updates when resolution value changes logger.info("Registering for resolution updates") - event.clear() - await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x52, RESOLUTION_ID]), response=True) - await event.wait() # Wait to receive the notification response + request = bytes([0x02, 0x52, RESOLUTION_ID]) + logger.debug(f"Writing to {query_request_uuid}: {request.hex(':')}") + await client.write_gatt_char(query_request_uuid.value, request, response=True) + # Wait to receive the notification response + response = await received_responses.get() + response.parse() logger.info("Successfully registered for resolution value updates.") + resolution = Resolution(response.data[RESOLUTION_ID][0]) logger.info(f"Resolution is currently {resolution}") # Write to command request BleUUID to change the video resolution (either to 1080 or 2.7K) new_resolution = Resolution.RES_2_7K if resolution is Resolution.RES_1080 else Resolution.RES_1080 logger.info(f"Changing the resolution to {new_resolution}...") - event.clear() - await client.write_gatt_char( - SETTINGS_REQ_UUID, bytearray([0x03, 0x02, 0x01, new_resolution.value]), response=True - ) - await event.wait() # Wait to receive the notification response - logger.info("Successfully changed the resolution") + request = bytes([0x03, 0x02, 0x01, new_resolution.value]) + logger.debug(f"Writing to {setting_request_uuid}: {request.hex(':')}") + await client.write_gatt_char(setting_request_uuid.value, request, response=True) + # Wait to receive the notification response + response = await received_responses.get() + response.parse() + # Ensure the setting was successful + assert response.status == 0x00 # Let's verify we got the update - while resolution is not new_resolution: - event.clear() - await event.wait() - logger.info(f"Resolution is now {resolution}") + logger.info("Waiting to receive new resolution") + while resolution is not new_resolution and (response := await received_responses.get()): + response.parse() + resolution = Resolution(response.data[RESOLUTION_ID][0]) + logger.info(f"Resolution is currently {resolution}") + + logger.info("Resolution has changed as expected. Exiting...") await client.disconnect() @@ -110,7 +101,7 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - try: asyncio.run(main(args.identifier)) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught logger.error(e) sys.exit(-1) else: diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/__init__.py new file mode 100644 index 00000000..290990ae --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/__init__.py @@ -0,0 +1,2 @@ +# __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/decipher_response.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/decipher_response.py new file mode 100644 index 00000000..98e8a921 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/decipher_response.py @@ -0,0 +1,348 @@ +# set_turbo_mode.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +from __future__ import annotations +import sys +import asyncio +import argparse +from dataclasses import dataclass +from typing import TypeAlias, cast + +from bleak import BleakClient +from bleak.backends.characteristic import BleakGATTCharacteristic +from google.protobuf.message import Message as ProtobufMessage + +from tutorial_modules import logger, proto +from tutorial_modules import GoProUuid, connect_ble, Response, QueryResponse, TlvResponse, ProtobufResponse, Resolution + +RESOLUTION_ID = 2 + + +@dataclass(frozen=True) +class ProtobufId: + """Protobuf feature / action identifier pair.""" + + feature_id: int + action_id: int + + +# From https://gopro.github.io/OpenGoPro/ble/protocol/id_tables.html#protobuf-ids +# TODO automatically generate this and fill out all messages. +ProtobufIdToMessage: dict[ProtobufId, type[ProtobufMessage] | None] = { + ProtobufId(0x02, 0x02): None, + ProtobufId(0x02, 0x04): None, + ProtobufId(0x02, 0x05): None, + ProtobufId(0x02, 0x0B): proto.NotifStartScanning, + ProtobufId(0x02, 0x0C): proto.NotifProvisioningState, + ProtobufId(0x02, 0x82): proto.ResponseStartScanning, + ProtobufId(0x02, 0x83): proto.ResponseGetApEntries, + ProtobufId(0x02, 0x84): proto.ResponseConnect, + ProtobufId(0x02, 0x85): proto.ResponseConnectNew, + ProtobufId(0xF1, 0x64): None, + ProtobufId(0xF1, 0x65): None, + ProtobufId(0xF1, 0x66): None, + ProtobufId(0xF1, 0x67): None, + ProtobufId(0xF1, 0x69): None, + ProtobufId(0xF1, 0x6B): None, + ProtobufId(0xF1, 0x79): None, + ProtobufId(0xF1, 0xE4): None, + ProtobufId(0xF1, 0xE5): None, + ProtobufId(0xF1, 0xE6): proto.ResponseGeneric, + ProtobufId(0xF1, 0xE7): proto.ResponseGeneric, + ProtobufId(0xF1, 0xE9): None, + ProtobufId(0xF1, 0xEB): proto.ResponseGeneric, + ProtobufId(0xF1, 0xF9): None, + ProtobufId(0xF5, 0x6D): None, + ProtobufId(0xF5, 0x6E): None, + ProtobufId(0xF5, 0x6F): None, + ProtobufId(0xF5, 0x72): None, + ProtobufId(0xF5, 0x74): None, + ProtobufId(0xF5, 0xED): None, + ProtobufId(0xF5, 0xEE): proto.ResponseCOHNCert, + ProtobufId(0xF5, 0xEF): proto.ResponseGeneric, + ProtobufId(0xF5, 0xEF): proto.NotifyCOHNStatus, + ProtobufId(0xF5, 0xF2): None, + ProtobufId(0xF5, 0xF3): None, + ProtobufId(0xF5, 0xF4): None, + ProtobufId(0xF5, 0xF5): None, +} + +ConcreteResponse: TypeAlias = ProtobufResponse | QueryResponse | TlvResponse + + +class ResponseManager: + """A wrapper around a BleakClient to manage accumulating, parsing, and retrieving responses. + + Before use, the client must be set via the `set_client` method. + """ + + def __init__(self) -> None: + """Constructor""" + + self._responses_by_uuid = GoProUuid.dict_by_uuid(Response) + self._q: asyncio.Queue[ConcreteResponse] = asyncio.Queue() + self._client: BleakClient | None = None + + def set_client(self, client: BleakClient) -> None: + """Set the client. This is required before use. + + Args: + client (BleakClient): bleak client to use for this manager instance. + """ + self._client = client + + @property + def is_initialized(self) -> bool: + """Has the client been set yet? + + Returns: + bool: True if the client is set. False otherwise. + """ + return self._client is not None + + @property + def client(self) -> BleakClient: + """Get the client. This property assumes that the client has already been set + + Raises: + RuntimeError: Client has not yet been set. + + Returns: + BleakClient: Client associated with this manager. + """ + if not self.is_initialized: + raise RuntimeError("Client has not been set") + return self._client # type: ignore + + def decipher_response(self, undeciphered_response: Response) -> ConcreteResponse: + """Given an undeciphered and unparsed response, decipher its type and parse as much of its payload as is feasible. + + Args: + undeciphered_response (Response): input response to decipher + + Raises: + RuntimeError: Found a Protobuf Response that does not have a defined message for its Feature / Action ID + + Returns: + ConcreteResponse: deciphered and parsed response + """ + payload = undeciphered_response.raw_bytes + # Are the first two payload bytes a real Fetaure / Action ID pair? + response: Response + if (index := ProtobufId(payload[0], payload[1])) in ProtobufIdToMessage: + if not (proto_message := ProtobufIdToMessage.get(index)): + # We've only added protobuf messages for operations used in this tutorial. + raise RuntimeError( + f"{index} is a valid Protobuf identifier but does not currently have a defined message." + ) + # Now use the protobuf messaged identified by the Feature / Action ID pair to parse the remaining payload + response = ProtobufResponse.from_received_response(undeciphered_response) + response.parse(proto_message) + return response + # TLV. Should it be parsed as Command or Query? + if undeciphered_response.uuid is GoProUuid.QUERY_RSP_UUID: + # It's a TLV query + response = QueryResponse.from_received_response(undeciphered_response) + else: + # It's a TLV command / setting. + response = TlvResponse.from_received_response(undeciphered_response) + # Parse the TLV payload (query, command, or setting) + response.parse() + return response + + async def notification_handler(self, characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + """Notification handler to use for the bleak client. + + Args: + characteristic (BleakGATTCharacteristic): characteristic notification was received on + data (bytearray): byte data of notification. + """ + uuid = GoProUuid(self.client.services.characteristics[characteristic.handle].uuid) + logger.debug(f'Received response at {uuid}: {data.hex(":")}') + + response = self._responses_by_uuid[uuid] + response.accumulate(data) + + # Enqueue if we have received the entire response + if response.is_received: + await self._q.put(self.decipher_response(response)) + # Reset the accumulating response + self._responses_by_uuid[uuid] = Response(uuid) + + @classmethod + def assert_generic_protobuf_success(cls, response: ProtobufMessage) -> None: + """Helper method to assert that a ResponseGeneric is successful + + Args: + response (ProtobufMessage): GenericResponse. This must be of type proto.ResponseGeneric + + Raises: + TypeError: response is not of type proto.ResponseGeneric + """ + generic_response = cast(proto.ResponseGeneric, response) + if (result := int(generic_response.result)) != int(proto.EnumResultGeneric.RESULT_SUCCESS): + raise TypeError(f"Received non-success status: {str(result)}") + + async def get_next_response(self) -> ConcreteResponse: + """Get the next received, deciphered, and parsed response from the queue. + + Note! If you know the type of response that you are expecting, use one of the more narrow-typed get methods. + + Returns: + ConcreteResponse: Dequeued response. + """ + return await self._q.get() + + # Helper methods to aid with typing. They are the same at run-time. + + async def get_next_response_as_tlv(self) -> TlvResponse: + """Get the next received, deciphered, and parsed response, casted as a TlvResponse. + + Returns: + TlvResponse: dequeued response + """ + return cast(TlvResponse, await self.get_next_response()) + + async def get_next_response_as_query(self) -> QueryResponse: + """Get the next received, deciphered, and parsed response, casted as a QueryResponse. + + Returns: + QueryResponse: dequeued response + """ + return cast(QueryResponse, await self.get_next_response()) + + async def get_next_response_as_protobuf(self) -> ProtobufResponse: + """Get the next received, deciphered, and parsed response, casted as a ProtobufResponse. + + Returns: + ProtobufResponse: dequeued response + """ + return cast(ProtobufResponse, await self.get_next_response()) + + +async def set_resolution(manager: ResponseManager) -> bool: + """Set the video resolution to 1080 + + Args: + manager (ResponseManager): manager used to perform the operation + + Returns: + bool: True if the setting was successfully set. False otherwise. + """ + logger.info("Setting the video resolution to 1080") + request = bytes([0x03, 0x02, 0x01, 0x09]) + request_uuid = GoProUuid.SETTINGS_REQ_UUID + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") + await manager.client.write_gatt_char(request_uuid.value, request, response=True) + tlv_response = await manager.get_next_response_as_tlv() + logger.info(f"Set resolution status: {tlv_response.status}") + return tlv_response.status == 0x00 + + +async def set_shutter_off(manager: ResponseManager) -> bool: + """Set the shutter off. + + Args: + manager (ResponseManager): manager used to perform the operation + + Returns: + bool: True if the shutter was successfully set off. False otherwise. + """ + # Write to command request BleUUID to turn the shutter on + logger.info("Setting the shutter on") + request = bytes([3, 1, 1, 0]) + request_uuid = GoProUuid.COMMAND_REQ_UUID + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") + await manager.client.write_gatt_char(request_uuid.value, request, response=True) + tlv_response = await manager.get_next_response_as_tlv() + logger.info(f"Set shutter status: {tlv_response.status}") + return tlv_response.status == 0x00 + + +async def get_resolution(manager: ResponseManager) -> Resolution: + """Get the current resolution. + + Args: + manager (ResponseManager): manager used to perform the operation + + Returns: + Resolution: The current resolution. + """ + logger.info("Getting the current resolution") + request = bytes([0x02, 0x12, 0x02]) + request_uuid = GoProUuid.QUERY_REQ_UUID + logger.debug(f"Writing to {request_uuid}: {request.hex(':')}") + await manager.client.write_gatt_char(request_uuid.value, request, response=True) + query_response = await manager.get_next_response_as_query() + resolution = Resolution(query_response.data[RESOLUTION_ID][0]) + logger.info(f"Received current resolution: {resolution}") + return resolution + + +async def set_turbo_mode(manager: ResponseManager) -> bool: + """Set the turbo mode off. + + Args: + manager (ResponseManager): manager used to perform the operation + + Returns: + bool: True if the turbo mode was successfully set off. False otherwise. + """ + request = bytearray( + [ + 0xF1, # Feature ID + 0x6B, # Action ID + *proto.RequestSetTurboActive(active=False).SerializeToString(), + ] + ) + request.insert(0, len(request)) + request_uuid = GoProUuid.COMMAND_REQ_UUID + # Write to command request UUID to enable turbo mode + logger.info(f"Writing {request.hex(':')} to {request_uuid}") + await manager.client.write_gatt_char(request_uuid.value, request, response=True) + protobuf_response = await manager.get_next_response_as_protobuf() + generic_response: proto.ResponseGeneric = protobuf_response.data # type: ignore + logger.info(f"Set Turbo Mode Status: {generic_response}") + return generic_response.result == proto.EnumResultGeneric.RESULT_SUCCESS + + +async def main(identifier: str | None) -> None: + manager = ResponseManager() + + try: + manager.set_client(await connect_ble(manager.notification_handler, identifier)) + # TLV Command (Setting) + await set_resolution(manager) + # TLV Command + await get_resolution(manager) + # TLV Query + await set_shutter_off(manager) + # Protobuf + await set_turbo_mode(manager) + except Exception as exc: # pylint: disable=broad-exception-caught + logger.error(repr(exc)) + finally: + if manager.is_initialized: + await manager.client.disconnect() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Connect to a GoPro camera, perform operations to demonstrate various responses types." + ) + parser.add_argument( + "-i", + "--identifier", + type=str, + help="Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to", + default=None, + ) + args = parser.parse_args() + + try: + asyncio.run(main(args.identifier)) + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(e) + sys.exit(-1) + else: + sys.exit(0) diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/__init__.py new file mode 100644 index 00000000..39239c83 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/__init__.py @@ -0,0 +1,31 @@ +# __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +from .cohn_pb2 import ( + ResponseCOHNCert, + NotifyCOHNStatus, + RequestClearCOHNCert, + RequestCOHNCert, + RequestCreateCOHNCert, + RequestGetCOHNStatus, + RequestSetCOHNSetting, + EnumCOHNNetworkState, + EnumCOHNStatus, +) +from .network_management_pb2 import ( + NotifProvisioningState, + NotifStartScanning, + ResponseGetApEntries, + ResponseConnectNew, + RequestConnect, + RequestConnectNew, + RequestGetApEntries, + RequestStartScan, + ResponseConnect, + ResponseStartScanning, + EnumProvisioning, + EnumScanning, + EnumScanEntryFlags, +) +from .response_generic_pb2 import ResponseGeneric, EnumResultGeneric +from .turbo_transfer_pb2 import RequestSetTurboActive diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/cohn_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/cohn_pb2.py new file mode 100644 index 00000000..2620e29c --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/cohn_pb2.py @@ -0,0 +1,38 @@ +# cohn_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +from . import response_generic_pb2 as response__generic__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\ncohn.proto\x12\nopen_gopro\x1a\x16response_generic.proto"4\n\x14RequestGetCOHNStatus\x12\x1c\n\x14register_cohn_status\x18\x01 \x01(\x08"\xd9\x01\n\x10NotifyCOHNStatus\x12*\n\x06status\x18\x01 \x01(\x0e2\x1a.open_gopro.EnumCOHNStatus\x12/\n\x05state\x18\x02 \x01(\x0e2 .open_gopro.EnumCOHNNetworkState\x12\x10\n\x08username\x18\x03 \x01(\t\x12\x10\n\x08password\x18\x04 \x01(\t\x12\x11\n\tipaddress\x18\x05 \x01(\t\x12\x0f\n\x07enabled\x18\x06 \x01(\x08\x12\x0c\n\x04ssid\x18\x07 \x01(\t\x12\x12\n\nmacaddress\x18\x08 \x01(\t")\n\x15RequestCreateCOHNCert\x12\x10\n\x08override\x18\x01 \x01(\x08"\x16\n\x14RequestClearCOHNCert"\x11\n\x0fRequestCOHNCert"O\n\x10ResponseCOHNCert\x12-\n\x06result\x18\x01 \x01(\x0e2\x1d.open_gopro.EnumResultGeneric\x12\x0c\n\x04cert\x18\x02 \x01(\t",\n\x15RequestSetCOHNSetting\x12\x13\n\x0bcohn_active\x18\x01 \x01(\x08*>\n\x0eEnumCOHNStatus\x12\x16\n\x12COHN_UNPROVISIONED\x10\x00\x12\x14\n\x10COHN_PROVISIONED\x10\x01*\xec\x01\n\x14EnumCOHNNetworkState\x12\x13\n\x0fCOHN_STATE_Init\x10\x00\x12\x14\n\x10COHN_STATE_Error\x10\x01\x12\x13\n\x0fCOHN_STATE_Exit\x10\x02\x12\x13\n\x0fCOHN_STATE_Idle\x10\x05\x12\x1f\n\x1bCOHN_STATE_NetworkConnected\x10\x1b\x12"\n\x1eCOHN_STATE_NetworkDisconnected\x10\x1c\x12"\n\x1eCOHN_STATE_ConnectingToNetwork\x10\x1d\x12\x16\n\x12COHN_STATE_Invalid\x10\x1e' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "cohn_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMCOHNSTATUS._serialized_start = 537 + _ENUMCOHNSTATUS._serialized_end = 599 + _ENUMCOHNNETWORKSTATE._serialized_start = 602 + _ENUMCOHNNETWORKSTATE._serialized_end = 838 + _REQUESTGETCOHNSTATUS._serialized_start = 50 + _REQUESTGETCOHNSTATUS._serialized_end = 102 + _NOTIFYCOHNSTATUS._serialized_start = 105 + _NOTIFYCOHNSTATUS._serialized_end = 322 + _REQUESTCREATECOHNCERT._serialized_start = 324 + _REQUESTCREATECOHNCERT._serialized_end = 365 + _REQUESTCLEARCOHNCERT._serialized_start = 367 + _REQUESTCLEARCOHNCERT._serialized_end = 389 + _REQUESTCOHNCERT._serialized_start = 391 + _REQUESTCOHNCERT._serialized_end = 408 + _RESPONSECOHNCERT._serialized_start = 410 + _RESPONSECOHNCERT._serialized_end = 489 + _REQUESTSETCOHNSETTING._serialized_start = 491 + _REQUESTSETCOHNSETTING._serialized_end = 535 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/cohn_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/cohn_pb2.pyi new file mode 100644 index 00000000..03516118 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/cohn_pb2.pyi @@ -0,0 +1,279 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for Camera On the Home Network +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +from . import response_generic_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumCOHNStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumCOHNStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCOHNStatus.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + COHN_UNPROVISIONED: _EnumCOHNStatus.ValueType + COHN_PROVISIONED: _EnumCOHNStatus.ValueType + +class EnumCOHNStatus(_EnumCOHNStatus, metaclass=_EnumCOHNStatusEnumTypeWrapper): ... + +COHN_UNPROVISIONED: EnumCOHNStatus.ValueType +COHN_PROVISIONED: EnumCOHNStatus.ValueType +global___EnumCOHNStatus = EnumCOHNStatus + +class _EnumCOHNNetworkState: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumCOHNNetworkStateEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCOHNNetworkState.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + COHN_STATE_Init: _EnumCOHNNetworkState.ValueType + COHN_STATE_Error: _EnumCOHNNetworkState.ValueType + COHN_STATE_Exit: _EnumCOHNNetworkState.ValueType + COHN_STATE_Idle: _EnumCOHNNetworkState.ValueType + COHN_STATE_NetworkConnected: _EnumCOHNNetworkState.ValueType + COHN_STATE_NetworkDisconnected: _EnumCOHNNetworkState.ValueType + COHN_STATE_ConnectingToNetwork: _EnumCOHNNetworkState.ValueType + COHN_STATE_Invalid: _EnumCOHNNetworkState.ValueType + +class EnumCOHNNetworkState(_EnumCOHNNetworkState, metaclass=_EnumCOHNNetworkStateEnumTypeWrapper): ... + +COHN_STATE_Init: EnumCOHNNetworkState.ValueType +COHN_STATE_Error: EnumCOHNNetworkState.ValueType +COHN_STATE_Exit: EnumCOHNNetworkState.ValueType +COHN_STATE_Idle: EnumCOHNNetworkState.ValueType +COHN_STATE_NetworkConnected: EnumCOHNNetworkState.ValueType +COHN_STATE_NetworkDisconnected: EnumCOHNNetworkState.ValueType +COHN_STATE_ConnectingToNetwork: EnumCOHNNetworkState.ValueType +COHN_STATE_Invalid: EnumCOHNNetworkState.ValueType +global___EnumCOHNNetworkState = EnumCOHNNetworkState + +@typing_extensions.final +class RequestGetCOHNStatus(google.protobuf.message.Message): + """* + Get the current COHN status. + + Response: @ref NotifyCOHNStatus + + Additionally, asynchronous updates can also be registered to return more @ref NotifyCOHNStatus when a value + changes. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + REGISTER_COHN_STATUS_FIELD_NUMBER: builtins.int + register_cohn_status: builtins.bool + "1 to register, 0 to unregister" + + def __init__(self, *, register_cohn_status: builtins.bool | None = ...) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["register_cohn_status", b"register_cohn_status"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["register_cohn_status", b"register_cohn_status"], + ) -> None: ... + +global___RequestGetCOHNStatus = RequestGetCOHNStatus + +@typing_extensions.final +class NotifyCOHNStatus(google.protobuf.message.Message): + """ + Current COHN status triggered by a @ref RequestGetCOHNStatus + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + STATUS_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + USERNAME_FIELD_NUMBER: builtins.int + PASSWORD_FIELD_NUMBER: builtins.int + IPADDRESS_FIELD_NUMBER: builtins.int + ENABLED_FIELD_NUMBER: builtins.int + SSID_FIELD_NUMBER: builtins.int + MACADDRESS_FIELD_NUMBER: builtins.int + status: global___EnumCOHNStatus.ValueType + "Current COHN status" + state: global___EnumCOHNNetworkState.ValueType + "Current COHN network state" + username: builtins.str + "Username used for http basic auth header" + password: builtins.str + "Password used for http basic auth header" + ipaddress: builtins.str + "Camera's IP address on the local network" + enabled: builtins.bool + "Is COHN currently enabled?" + ssid: builtins.str + "Currently connected SSID" + macaddress: builtins.str + "MAC address of the wifi adapter" + + def __init__( + self, + *, + status: global___EnumCOHNStatus.ValueType | None = ..., + state: global___EnumCOHNNetworkState.ValueType | None = ..., + username: builtins.str | None = ..., + password: builtins.str | None = ..., + ipaddress: builtins.str | None = ..., + enabled: builtins.bool | None = ..., + ssid: builtins.str | None = ..., + macaddress: builtins.str | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "enabled", + b"enabled", + "ipaddress", + b"ipaddress", + "macaddress", + b"macaddress", + "password", + b"password", + "ssid", + b"ssid", + "state", + b"state", + "status", + b"status", + "username", + b"username", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "enabled", + b"enabled", + "ipaddress", + b"ipaddress", + "macaddress", + b"macaddress", + "password", + b"password", + "ssid", + b"ssid", + "state", + b"state", + "status", + b"status", + "username", + b"username", + ], + ) -> None: ... + +global___NotifyCOHNStatus = NotifyCOHNStatus + +@typing_extensions.final +class RequestCreateCOHNCert(google.protobuf.message.Message): + """* + Create the Camera On the Home Network SSL/TLS certificate. + + Returns a @ref ResponseGeneric with the status of the creation + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + OVERRIDE_FIELD_NUMBER: builtins.int + override: builtins.bool + "Override current provisioning and create new cert" + + def __init__(self, *, override: builtins.bool | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["override", b"override"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["override", b"override"]) -> None: ... + +global___RequestCreateCOHNCert = RequestCreateCOHNCert + +@typing_extensions.final +class RequestClearCOHNCert(google.protobuf.message.Message): + """* + Clear the COHN certificate. + + Returns a @ref ResponseGeneric with the status of the clear + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestClearCOHNCert = RequestClearCOHNCert + +@typing_extensions.final +class RequestCOHNCert(google.protobuf.message.Message): + """* + Get the COHN certificate. + + Returns a @ref ResponseCOHNCert + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestCOHNCert = RequestCOHNCert + +@typing_extensions.final +class ResponseCOHNCert(google.protobuf.message.Message): + """ + COHN Certificate response triggered by @ref RequestCOHNCert + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + CERT_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Was request successful?" + cert: builtins.str + "Root CA cert (ASCII text)" + + def __init__( + self, *, result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., cert: builtins.str | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["cert", b"cert", "result", b"result"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["cert", b"cert", "result", b"result"], + ) -> None: ... + +global___ResponseCOHNCert = ResponseCOHNCert + +@typing_extensions.final +class RequestSetCOHNSetting(google.protobuf.message.Message): + """* + Configure a COHN Setting + + Returns a @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + COHN_ACTIVE_FIELD_NUMBER: builtins.int + cohn_active: builtins.bool + "*\n 1 to enable COHN, 0 to disable COHN\n\n When set to 1, STA Mode connection will be dropped and camera will not automatically re-connect for COHN.\n " + + def __init__(self, *, cohn_active: builtins.bool | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["cohn_active", b"cohn_active"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["cohn_active", b"cohn_active"]) -> None: ... + +global___RequestSetCOHNSetting = RequestSetCOHNSetting diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/live_streaming_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/live_streaming_pb2.py new file mode 100644 index 00000000..8319589e --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/live_streaming_pb2.py @@ -0,0 +1,34 @@ +# live_streaming_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x14live_streaming.proto\x12\nopen_gopro"\xa4\x04\n\x16NotifyLiveStreamStatus\x12<\n\x12live_stream_status\x18\x01 \x01(\x0e2 .open_gopro.EnumLiveStreamStatus\x12:\n\x11live_stream_error\x18\x02 \x01(\x0e2\x1f.open_gopro.EnumLiveStreamError\x12\x1a\n\x12live_stream_encode\x18\x03 \x01(\x08\x12\x1b\n\x13live_stream_bitrate\x18\x04 \x01(\x05\x12K\n\'live_stream_window_size_supported_array\x18\x05 \x03(\x0e2\x1a.open_gopro.EnumWindowSize\x12$\n\x1clive_stream_encode_supported\x18\x06 \x01(\x08\x12(\n live_stream_max_lens_unsupported\x18\x07 \x01(\x08\x12*\n"live_stream_minimum_stream_bitrate\x18\x08 \x01(\x05\x12*\n"live_stream_maximum_stream_bitrate\x18\t \x01(\x05\x12"\n\x1alive_stream_lens_supported\x18\n \x01(\x08\x12>\n live_stream_lens_supported_array\x18\x0b \x03(\x0e2\x14.open_gopro.EnumLens"\xbc\x01\n\x1aRequestGetLiveStreamStatus\x12M\n\x1bregister_live_stream_status\x18\x01 \x03(\x0e2(.open_gopro.EnumRegisterLiveStreamStatus\x12O\n\x1dunregister_live_stream_status\x18\x02 \x03(\x0e2(.open_gopro.EnumRegisterLiveStreamStatus"\xe6\x01\n\x18RequestSetLiveStreamMode\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0e\n\x06encode\x18\x02 \x01(\x08\x12/\n\x0bwindow_size\x18\x03 \x01(\x0e2\x1a.open_gopro.EnumWindowSize\x12\x0c\n\x04cert\x18\x06 \x01(\x0c\x12\x17\n\x0fminimum_bitrate\x18\x07 \x01(\x05\x12\x17\n\x0fmaximum_bitrate\x18\x08 \x01(\x05\x12\x18\n\x10starting_bitrate\x18\t \x01(\x05\x12"\n\x04lens\x18\n \x01(\x0e2\x14.open_gopro.EnumLens*>\n\x08EnumLens\x12\r\n\tLENS_WIDE\x10\x00\x12\x0f\n\x0bLENS_LINEAR\x10\x04\x12\x12\n\x0eLENS_SUPERVIEW\x10\x03*\xde\x03\n\x13EnumLiveStreamError\x12\x1a\n\x16LIVE_STREAM_ERROR_NONE\x10\x00\x12\x1d\n\x19LIVE_STREAM_ERROR_NETWORK\x10\x01\x12"\n\x1eLIVE_STREAM_ERROR_CREATESTREAM\x10\x02\x12!\n\x1dLIVE_STREAM_ERROR_OUTOFMEMORY\x10\x03\x12!\n\x1dLIVE_STREAM_ERROR_INPUTSTREAM\x10\x04\x12\x1e\n\x1aLIVE_STREAM_ERROR_INTERNET\x10\x05\x12\x1f\n\x1bLIVE_STREAM_ERROR_OSNETWORK\x10\x06\x12,\n(LIVE_STREAM_ERROR_SELECTEDNETWORKTIMEOUT\x10\x07\x12#\n\x1fLIVE_STREAM_ERROR_SSL_HANDSHAKE\x10\x08\x12$\n LIVE_STREAM_ERROR_CAMERA_BLOCKED\x10\t\x12\x1d\n\x19LIVE_STREAM_ERROR_UNKNOWN\x10\n\x12"\n\x1eLIVE_STREAM_ERROR_SD_CARD_FULL\x10(\x12%\n!LIVE_STREAM_ERROR_SD_CARD_REMOVED\x10)*\x80\x02\n\x14EnumLiveStreamStatus\x12\x1a\n\x16LIVE_STREAM_STATE_IDLE\x10\x00\x12\x1c\n\x18LIVE_STREAM_STATE_CONFIG\x10\x01\x12\x1b\n\x17LIVE_STREAM_STATE_READY\x10\x02\x12\x1f\n\x1bLIVE_STREAM_STATE_STREAMING\x10\x03\x12&\n"LIVE_STREAM_STATE_COMPLETE_STAY_ON\x10\x04\x12$\n LIVE_STREAM_STATE_FAILED_STAY_ON\x10\x05\x12"\n\x1eLIVE_STREAM_STATE_RECONNECTING\x10\x06*\xbc\x01\n\x1cEnumRegisterLiveStreamStatus\x12&\n"REGISTER_LIVE_STREAM_STATUS_STATUS\x10\x01\x12%\n!REGISTER_LIVE_STREAM_STATUS_ERROR\x10\x02\x12$\n REGISTER_LIVE_STREAM_STATUS_MODE\x10\x03\x12\'\n#REGISTER_LIVE_STREAM_STATUS_BITRATE\x10\x04*P\n\x0eEnumWindowSize\x12\x13\n\x0fWINDOW_SIZE_480\x10\x04\x12\x13\n\x0fWINDOW_SIZE_720\x10\x07\x12\x14\n\x10WINDOW_SIZE_1080\x10\x0c' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "live_streaming_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMLENS._serialized_start = 1011 + _ENUMLENS._serialized_end = 1073 + _ENUMLIVESTREAMERROR._serialized_start = 1076 + _ENUMLIVESTREAMERROR._serialized_end = 1554 + _ENUMLIVESTREAMSTATUS._serialized_start = 1557 + _ENUMLIVESTREAMSTATUS._serialized_end = 1813 + _ENUMREGISTERLIVESTREAMSTATUS._serialized_start = 1816 + _ENUMREGISTERLIVESTREAMSTATUS._serialized_end = 2004 + _ENUMWINDOWSIZE._serialized_start = 2006 + _ENUMWINDOWSIZE._serialized_end = 2086 + _NOTIFYLIVESTREAMSTATUS._serialized_start = 37 + _NOTIFYLIVESTREAMSTATUS._serialized_end = 585 + _REQUESTGETLIVESTREAMSTATUS._serialized_start = 588 + _REQUESTGETLIVESTREAMSTATUS._serialized_end = 776 + _REQUESTSETLIVESTREAMMODE._serialized_start = 779 + _REQUESTSETLIVESTREAMMODE._serialized_end = 1009 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/live_streaming_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/live_streaming_pb2.pyi new file mode 100644 index 00000000..1eb9200f --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/live_streaming_pb2.pyi @@ -0,0 +1,461 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for working with Live Streams +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumLens: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumLensEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLens.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + LENS_WIDE: _EnumLens.ValueType + LENS_LINEAR: _EnumLens.ValueType + LENS_SUPERVIEW: _EnumLens.ValueType + +class EnumLens(_EnumLens, metaclass=_EnumLensEnumTypeWrapper): ... + +LENS_WIDE: EnumLens.ValueType +LENS_LINEAR: EnumLens.ValueType +LENS_SUPERVIEW: EnumLens.ValueType +global___EnumLens = EnumLens + +class _EnumLiveStreamError: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumLiveStreamErrorEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLiveStreamError.ValueType], + builtins.type, +): + 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: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumLiveStreamStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumLiveStreamStatus.ValueType], + builtins.type, +): + 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 + "\n Livestream has finished configuration and is ready to start streaming\n " + 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 +"\nLivestream has finished configuration and is ready to start streaming\n" +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: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumRegisterLiveStreamStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumRegisterLiveStreamStatus.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + REGISTER_LIVE_STREAM_STATUS_STATUS: _EnumRegisterLiveStreamStatus.ValueType + REGISTER_LIVE_STREAM_STATUS_ERROR: _EnumRegisterLiveStreamStatus.ValueType + REGISTER_LIVE_STREAM_STATUS_MODE: _EnumRegisterLiveStreamStatus.ValueType + REGISTER_LIVE_STREAM_STATUS_BITRATE: _EnumRegisterLiveStreamStatus.ValueType + +class EnumRegisterLiveStreamStatus( + _EnumRegisterLiveStreamStatus, + metaclass=_EnumRegisterLiveStreamStatusEnumTypeWrapper, +): ... + +REGISTER_LIVE_STREAM_STATUS_STATUS: EnumRegisterLiveStreamStatus.ValueType +REGISTER_LIVE_STREAM_STATUS_ERROR: EnumRegisterLiveStreamStatus.ValueType +REGISTER_LIVE_STREAM_STATUS_MODE: EnumRegisterLiveStreamStatus.ValueType +REGISTER_LIVE_STREAM_STATUS_BITRATE: EnumRegisterLiveStreamStatus.ValueType +global___EnumRegisterLiveStreamStatus = EnumRegisterLiveStreamStatus + +class _EnumWindowSize: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumWindowSizeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumWindowSize.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + WINDOW_SIZE_480: _EnumWindowSize.ValueType + WINDOW_SIZE_720: _EnumWindowSize.ValueType + WINDOW_SIZE_1080: _EnumWindowSize.ValueType + +class EnumWindowSize(_EnumWindowSize, metaclass=_EnumWindowSizeEnumTypeWrapper): ... + +WINDOW_SIZE_480: EnumWindowSize.ValueType +WINDOW_SIZE_720: EnumWindowSize.ValueType +WINDOW_SIZE_1080: EnumWindowSize.ValueType +global___EnumWindowSize = EnumWindowSize + +@typing_extensions.final +class NotifyLiveStreamStatus(google.protobuf.message.Message): + """* + Live Stream status + + Sent either: + + - As a synchronous response to initial @ref RequestGetLiveStreamStatus + - As an asynchronous notifications registered for via @ref RequestGetLiveStreamStatus + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int + LIVE_STREAM_ERROR_FIELD_NUMBER: builtins.int + LIVE_STREAM_ENCODE_FIELD_NUMBER: builtins.int + LIVE_STREAM_BITRATE_FIELD_NUMBER: builtins.int + LIVE_STREAM_WINDOW_SIZE_SUPPORTED_ARRAY_FIELD_NUMBER: builtins.int + LIVE_STREAM_ENCODE_SUPPORTED_FIELD_NUMBER: builtins.int + LIVE_STREAM_MAX_LENS_UNSUPPORTED_FIELD_NUMBER: builtins.int + LIVE_STREAM_MINIMUM_STREAM_BITRATE_FIELD_NUMBER: builtins.int + LIVE_STREAM_MAXIMUM_STREAM_BITRATE_FIELD_NUMBER: builtins.int + LIVE_STREAM_LENS_SUPPORTED_FIELD_NUMBER: builtins.int + LIVE_STREAM_LENS_SUPPORTED_ARRAY_FIELD_NUMBER: builtins.int + live_stream_status: global___EnumLiveStreamStatus.ValueType + "Live stream status" + live_stream_error: global___EnumLiveStreamError.ValueType + "Live stream error" + live_stream_encode: builtins.bool + "Is live stream encoding?" + live_stream_bitrate: builtins.int + "Live stream bitrate (Kbps)" + + @property + def live_stream_window_size_supported_array( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___EnumWindowSize.ValueType]: + """Set of currently supported resolutions""" + live_stream_encode_supported: builtins.bool + "Does the camera support encoding while live streaming?" + live_stream_max_lens_unsupported: builtins.bool + "Is the Max Lens feature NOT supported?" + live_stream_minimum_stream_bitrate: builtins.int + "Camera-defined minimum bitrate (static) (Kbps)" + live_stream_maximum_stream_bitrate: builtins.int + "Camera-defined maximum bitrate (static) (Kbps)" + live_stream_lens_supported: builtins.bool + "Does camera support setting lens for live streaming?" + + @property + def live_stream_lens_supported_array( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___EnumLens.ValueType]: + """Set of currently supported FOV options""" + + def __init__( + self, + *, + live_stream_status: global___EnumLiveStreamStatus.ValueType | None = ..., + live_stream_error: global___EnumLiveStreamError.ValueType | None = ..., + live_stream_encode: builtins.bool | None = ..., + live_stream_bitrate: builtins.int | None = ..., + live_stream_window_size_supported_array: ( + collections.abc.Iterable[global___EnumWindowSize.ValueType] | None + ) = ..., + live_stream_encode_supported: builtins.bool | None = ..., + live_stream_max_lens_unsupported: builtins.bool | None = ..., + live_stream_minimum_stream_bitrate: builtins.int | None = ..., + live_stream_maximum_stream_bitrate: builtins.int | None = ..., + live_stream_lens_supported: builtins.bool | None = ..., + live_stream_lens_supported_array: collections.abc.Iterable[global___EnumLens.ValueType] | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "live_stream_bitrate", + b"live_stream_bitrate", + "live_stream_encode", + b"live_stream_encode", + "live_stream_encode_supported", + b"live_stream_encode_supported", + "live_stream_error", + b"live_stream_error", + "live_stream_lens_supported", + b"live_stream_lens_supported", + "live_stream_max_lens_unsupported", + b"live_stream_max_lens_unsupported", + "live_stream_maximum_stream_bitrate", + b"live_stream_maximum_stream_bitrate", + "live_stream_minimum_stream_bitrate", + b"live_stream_minimum_stream_bitrate", + "live_stream_status", + b"live_stream_status", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "live_stream_bitrate", + b"live_stream_bitrate", + "live_stream_encode", + b"live_stream_encode", + "live_stream_encode_supported", + b"live_stream_encode_supported", + "live_stream_error", + b"live_stream_error", + "live_stream_lens_supported", + b"live_stream_lens_supported", + "live_stream_lens_supported_array", + b"live_stream_lens_supported_array", + "live_stream_max_lens_unsupported", + b"live_stream_max_lens_unsupported", + "live_stream_maximum_stream_bitrate", + b"live_stream_maximum_stream_bitrate", + "live_stream_minimum_stream_bitrate", + b"live_stream_minimum_stream_bitrate", + "live_stream_status", + b"live_stream_status", + "live_stream_window_size_supported_array", + b"live_stream_window_size_supported_array", + ], + ) -> None: ... + +global___NotifyLiveStreamStatus = NotifyLiveStreamStatus + +@typing_extensions.final +class RequestGetLiveStreamStatus(google.protobuf.message.Message): + """* + Get the current livestream status (and optionally register for future status changes) + + Response: @ref NotifyLiveStreamStatus + + Notification: @ref NotifyLiveStreamStatus + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + REGISTER_LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int + UNREGISTER_LIVE_STREAM_STATUS_FIELD_NUMBER: builtins.int + + @property + def register_live_stream_status( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + global___EnumRegisterLiveStreamStatus.ValueType + ]: + """Array of live stream statuses to be notified about""" + + @property + def unregister_live_stream_status( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[ + global___EnumRegisterLiveStreamStatus.ValueType + ]: + """Array of live stream statuses to stop being notified about""" + + def __init__( + self, + *, + register_live_stream_status: ( + collections.abc.Iterable[global___EnumRegisterLiveStreamStatus.ValueType] | None + ) = ..., + unregister_live_stream_status: ( + collections.abc.Iterable[global___EnumRegisterLiveStreamStatus.ValueType] | None + ) = ... + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "register_live_stream_status", + b"register_live_stream_status", + "unregister_live_stream_status", + b"unregister_live_stream_status", + ], + ) -> None: ... + +global___RequestGetLiveStreamStatus = RequestGetLiveStreamStatus + +@typing_extensions.final +class RequestSetLiveStreamMode(google.protobuf.message.Message): + """* + Configure Live Streaming + + Response: @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + URL_FIELD_NUMBER: builtins.int + ENCODE_FIELD_NUMBER: builtins.int + WINDOW_SIZE_FIELD_NUMBER: builtins.int + CERT_FIELD_NUMBER: builtins.int + MINIMUM_BITRATE_FIELD_NUMBER: builtins.int + MAXIMUM_BITRATE_FIELD_NUMBER: builtins.int + STARTING_BITRATE_FIELD_NUMBER: builtins.int + LENS_FIELD_NUMBER: builtins.int + url: builtins.str + "RTMP(S) URL used for live stream" + encode: builtins.bool + "Save media to sdcard while streaming?" + window_size: global___EnumWindowSize.ValueType + "*\n Resolution to use for live stream\n\n The set of supported lenses is only available from the `live_stream_window_size_supported_array` in @ref NotifyLiveStreamStatus)\n " + cert: builtins.bytes + "Certificate for servers that require it in PEM format" + minimum_bitrate: builtins.int + "Minimum desired bitrate (may or may not be honored)" + maximum_bitrate: builtins.int + "Maximum desired bitrate (may or may not be honored)" + starting_bitrate: builtins.int + "Starting bitrate" + lens: global___EnumLens.ValueType + "*\n Lens to use for live stream\n\n The set of supported lenses is only available from the `live_stream_lens_supported_array` in @ref NotifyLiveStreamStatus)\n " + + def __init__( + self, + *, + url: builtins.str | None = ..., + encode: builtins.bool | None = ..., + window_size: global___EnumWindowSize.ValueType | None = ..., + cert: builtins.bytes | None = ..., + minimum_bitrate: builtins.int | None = ..., + maximum_bitrate: builtins.int | None = ..., + starting_bitrate: builtins.int | None = ..., + lens: global___EnumLens.ValueType | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "cert", + b"cert", + "encode", + b"encode", + "lens", + b"lens", + "maximum_bitrate", + b"maximum_bitrate", + "minimum_bitrate", + b"minimum_bitrate", + "starting_bitrate", + b"starting_bitrate", + "url", + b"url", + "window_size", + b"window_size", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "cert", + b"cert", + "encode", + b"encode", + "lens", + b"lens", + "maximum_bitrate", + b"maximum_bitrate", + "minimum_bitrate", + b"minimum_bitrate", + "starting_bitrate", + b"starting_bitrate", + "url", + b"url", + "window_size", + b"window_size", + ], + ) -> None: ... + +global___RequestSetLiveStreamMode = RequestSetLiveStreamMode diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/media_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/media_pb2.py new file mode 100644 index 00000000..94d1fef3 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/media_pb2.py @@ -0,0 +1,24 @@ +# media_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +from . import response_generic_pb2 as response__generic__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0bmedia.proto\x12\nopen_gopro\x1a\x16response_generic.proto"\x1d\n\x1bRequestGetLastCapturedMedia"l\n\x19ResponseLastCapturedMedia\x12-\n\x06result\x18\x01 \x01(\x0e2\x1d.open_gopro.EnumResultGeneric\x12 \n\x05media\x18\x02 \x01(\x0b2\x11.open_gopro.Media' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "media_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _REQUESTGETLASTCAPTUREDMEDIA._serialized_start = 51 + _REQUESTGETLASTCAPTUREDMEDIA._serialized_end = 80 + _RESPONSELASTCAPTUREDMEDIA._serialized_start = 82 + _RESPONSELASTCAPTUREDMEDIA._serialized_end = 190 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/media_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/media_pb2.pyi new file mode 100644 index 00000000..6de33cc0 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/media_pb2.pyi @@ -0,0 +1,75 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Commands to query and manipulate media files +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.message +from . import response_generic_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing_extensions.final +class RequestGetLastCapturedMedia(google.protobuf.message.Message): + """* + Get the last captured media filename + + Returns a @ref ResponseLastCapturedMedia + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestGetLastCapturedMedia = RequestGetLastCapturedMedia + +@typing_extensions.final +class ResponseLastCapturedMedia(google.protobuf.message.Message): + """* + The Last Captured Media + + Message is sent in response to a @ref RequestGetLastCapturedMedia. + + This contains the relative path of the last captured media starting from the `DCIM` directory on the SDCard. Depending + on the type of media captured, it will return: + + - The single media path for single photo/video media + - The path to the first captured media in the group for grouped media + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + MEDIA_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Was the request successful?" + + @property + def media(self) -> response_generic_pb2.Media: + """* + Last captured media if result is RESULT_SUCCESS. Invalid if result is RESULT_RESOURCE_NOT_AVAILBLE. + """ + + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + media: response_generic_pb2.Media | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["media", b"media", "result", b"result"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["media", b"media", "result", b"result"], + ) -> None: ... + +global___ResponseLastCapturedMedia = ResponseLastCapturedMedia diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/network_management_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/network_management_pb2.py new file mode 100644 index 00000000..770915ca --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/network_management_pb2.py @@ -0,0 +1,50 @@ +# network_management_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +from . import response_generic_pb2 as response__generic__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x18network_management.proto\x12\nopen_gopro\x1a\x16response_generic.proto"R\n\x16NotifProvisioningState\x128\n\x12provisioning_state\x18\x01 \x02(\x0e2\x1c.open_gopro.EnumProvisioning"\x8d\x01\n\x12NotifStartScanning\x120\n\x0escanning_state\x18\x01 \x02(\x0e2\x18.open_gopro.EnumScanning\x12\x0f\n\x07scan_id\x18\x02 \x01(\x05\x12\x15\n\rtotal_entries\x18\x03 \x01(\x05\x12\x1d\n\x15total_configured_ssid\x18\x04 \x02(\x05"\x1e\n\x0eRequestConnect\x12\x0c\n\x04ssid\x18\x01 \x02(\t"\x93\x01\n\x11RequestConnectNew\x12\x0c\n\x04ssid\x18\x01 \x02(\t\x12\x10\n\x08password\x18\x02 \x02(\t\x12\x11\n\tstatic_ip\x18\x03 \x01(\x0c\x12\x0f\n\x07gateway\x18\x04 \x01(\x0c\x12\x0e\n\x06subnet\x18\x05 \x01(\x0c\x12\x13\n\x0bdns_primary\x18\x06 \x01(\x0c\x12\x15\n\rdns_secondary\x18\x07 \x01(\x0c"P\n\x13RequestGetApEntries\x12\x13\n\x0bstart_index\x18\x01 \x02(\x05\x12\x13\n\x0bmax_entries\x18\x02 \x02(\x05\x12\x0f\n\x07scan_id\x18\x03 \x02(\x05"\x17\n\x15RequestReleaseNetwork"\x12\n\x10RequestStartScan"\x93\x01\n\x0fResponseConnect\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x128\n\x12provisioning_state\x18\x02 \x02(\x0e2\x1c.open_gopro.EnumProvisioning\x12\x17\n\x0ftimeout_seconds\x18\x03 \x02(\x05"\x96\x01\n\x12ResponseConnectNew\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x128\n\x12provisioning_state\x18\x02 \x02(\x0e2\x1c.open_gopro.EnumProvisioning\x12\x17\n\x0ftimeout_seconds\x18\x03 \x02(\x05"\x84\x02\n\x14ResponseGetApEntries\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x12\x0f\n\x07scan_id\x18\x02 \x02(\x05\x12;\n\x07entries\x18\x03 \x03(\x0b2*.open_gopro.ResponseGetApEntries.ScanEntry\x1ao\n\tScanEntry\x12\x0c\n\x04ssid\x18\x01 \x02(\t\x12\x1c\n\x14signal_strength_bars\x18\x02 \x02(\x05\x12\x1c\n\x14signal_frequency_mhz\x18\x04 \x02(\x05\x12\x18\n\x10scan_entry_flags\x18\x05 \x02(\x05"x\n\x15ResponseStartScanning\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric\x120\n\x0escanning_state\x18\x02 \x02(\x0e2\x18.open_gopro.EnumScanning*\xb5\x03\n\x10EnumProvisioning\x12\x18\n\x14PROVISIONING_UNKNOWN\x10\x00\x12\x1e\n\x1aPROVISIONING_NEVER_STARTED\x10\x01\x12\x18\n\x14PROVISIONING_STARTED\x10\x02\x12"\n\x1ePROVISIONING_ABORTED_BY_SYSTEM\x10\x03\x12"\n\x1ePROVISIONING_CANCELLED_BY_USER\x10\x04\x12\x1f\n\x1bPROVISIONING_SUCCESS_NEW_AP\x10\x05\x12\x1f\n\x1bPROVISIONING_SUCCESS_OLD_AP\x10\x06\x12*\n&PROVISIONING_ERROR_FAILED_TO_ASSOCIATE\x10\x07\x12$\n PROVISIONING_ERROR_PASSWORD_AUTH\x10\x08\x12$\n PROVISIONING_ERROR_EULA_BLOCKING\x10\t\x12"\n\x1ePROVISIONING_ERROR_NO_INTERNET\x10\n\x12\'\n#PROVISIONING_ERROR_UNSUPPORTED_TYPE\x10\x0b*\xac\x01\n\x0cEnumScanning\x12\x14\n\x10SCANNING_UNKNOWN\x10\x00\x12\x1a\n\x16SCANNING_NEVER_STARTED\x10\x01\x12\x14\n\x10SCANNING_STARTED\x10\x02\x12\x1e\n\x1aSCANNING_ABORTED_BY_SYSTEM\x10\x03\x12\x1e\n\x1aSCANNING_CANCELLED_BY_USER\x10\x04\x12\x14\n\x10SCANNING_SUCCESS\x10\x05*\xb2\x01\n\x12EnumScanEntryFlags\x12\x12\n\x0eSCAN_FLAG_OPEN\x10\x00\x12\x1b\n\x17SCAN_FLAG_AUTHENTICATED\x10\x01\x12\x18\n\x14SCAN_FLAG_CONFIGURED\x10\x02\x12\x17\n\x13SCAN_FLAG_BEST_SSID\x10\x04\x12\x18\n\x14SCAN_FLAG_ASSOCIATED\x10\x08\x12\x1e\n\x1aSCAN_FLAG_UNSUPPORTED_TYPE\x10\x10' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "network_management_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMPROVISIONING._serialized_start = 1290 + _ENUMPROVISIONING._serialized_end = 1727 + _ENUMSCANNING._serialized_start = 1730 + _ENUMSCANNING._serialized_end = 1902 + _ENUMSCANENTRYFLAGS._serialized_start = 1905 + _ENUMSCANENTRYFLAGS._serialized_end = 2083 + _NOTIFPROVISIONINGSTATE._serialized_start = 64 + _NOTIFPROVISIONINGSTATE._serialized_end = 146 + _NOTIFSTARTSCANNING._serialized_start = 149 + _NOTIFSTARTSCANNING._serialized_end = 290 + _REQUESTCONNECT._serialized_start = 292 + _REQUESTCONNECT._serialized_end = 322 + _REQUESTCONNECTNEW._serialized_start = 325 + _REQUESTCONNECTNEW._serialized_end = 472 + _REQUESTGETAPENTRIES._serialized_start = 474 + _REQUESTGETAPENTRIES._serialized_end = 554 + _REQUESTRELEASENETWORK._serialized_start = 556 + _REQUESTRELEASENETWORK._serialized_end = 579 + _REQUESTSTARTSCAN._serialized_start = 581 + _REQUESTSTARTSCAN._serialized_end = 599 + _RESPONSECONNECT._serialized_start = 602 + _RESPONSECONNECT._serialized_end = 749 + _RESPONSECONNECTNEW._serialized_start = 752 + _RESPONSECONNECTNEW._serialized_end = 902 + _RESPONSEGETAPENTRIES._serialized_start = 905 + _RESPONSEGETAPENTRIES._serialized_end = 1165 + _RESPONSEGETAPENTRIES_SCANENTRY._serialized_start = 1054 + _RESPONSEGETAPENTRIES_SCANENTRY._serialized_end = 1165 + _RESPONSESTARTSCANNING._serialized_start = 1167 + _RESPONSESTARTSCANNING._serialized_end = 1287 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/network_management_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/network_management_pb2.pyi new file mode 100644 index 00000000..c9f74c3d --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/network_management_pb2.pyi @@ -0,0 +1,633 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for network management +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +from . import response_generic_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumProvisioning: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumProvisioningEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumProvisioning.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PROVISIONING_UNKNOWN: _EnumProvisioning.ValueType + PROVISIONING_NEVER_STARTED: _EnumProvisioning.ValueType + PROVISIONING_STARTED: _EnumProvisioning.ValueType + PROVISIONING_ABORTED_BY_SYSTEM: _EnumProvisioning.ValueType + PROVISIONING_CANCELLED_BY_USER: _EnumProvisioning.ValueType + PROVISIONING_SUCCESS_NEW_AP: _EnumProvisioning.ValueType + PROVISIONING_SUCCESS_OLD_AP: _EnumProvisioning.ValueType + PROVISIONING_ERROR_FAILED_TO_ASSOCIATE: _EnumProvisioning.ValueType + PROVISIONING_ERROR_PASSWORD_AUTH: _EnumProvisioning.ValueType + PROVISIONING_ERROR_EULA_BLOCKING: _EnumProvisioning.ValueType + PROVISIONING_ERROR_NO_INTERNET: _EnumProvisioning.ValueType + PROVISIONING_ERROR_UNSUPPORTED_TYPE: _EnumProvisioning.ValueType + +class EnumProvisioning(_EnumProvisioning, metaclass=_EnumProvisioningEnumTypeWrapper): ... + +PROVISIONING_UNKNOWN: EnumProvisioning.ValueType +PROVISIONING_NEVER_STARTED: EnumProvisioning.ValueType +PROVISIONING_STARTED: EnumProvisioning.ValueType +PROVISIONING_ABORTED_BY_SYSTEM: EnumProvisioning.ValueType +PROVISIONING_CANCELLED_BY_USER: EnumProvisioning.ValueType +PROVISIONING_SUCCESS_NEW_AP: EnumProvisioning.ValueType +PROVISIONING_SUCCESS_OLD_AP: EnumProvisioning.ValueType +PROVISIONING_ERROR_FAILED_TO_ASSOCIATE: EnumProvisioning.ValueType +PROVISIONING_ERROR_PASSWORD_AUTH: EnumProvisioning.ValueType +PROVISIONING_ERROR_EULA_BLOCKING: EnumProvisioning.ValueType +PROVISIONING_ERROR_NO_INTERNET: EnumProvisioning.ValueType +PROVISIONING_ERROR_UNSUPPORTED_TYPE: EnumProvisioning.ValueType +global___EnumProvisioning = EnumProvisioning + +class _EnumScanning: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumScanningEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumScanning.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + SCANNING_UNKNOWN: _EnumScanning.ValueType + SCANNING_NEVER_STARTED: _EnumScanning.ValueType + SCANNING_STARTED: _EnumScanning.ValueType + SCANNING_ABORTED_BY_SYSTEM: _EnumScanning.ValueType + SCANNING_CANCELLED_BY_USER: _EnumScanning.ValueType + SCANNING_SUCCESS: _EnumScanning.ValueType + +class EnumScanning(_EnumScanning, metaclass=_EnumScanningEnumTypeWrapper): ... + +SCANNING_UNKNOWN: EnumScanning.ValueType +SCANNING_NEVER_STARTED: EnumScanning.ValueType +SCANNING_STARTED: EnumScanning.ValueType +SCANNING_ABORTED_BY_SYSTEM: EnumScanning.ValueType +SCANNING_CANCELLED_BY_USER: EnumScanning.ValueType +SCANNING_SUCCESS: EnumScanning.ValueType +global___EnumScanning = EnumScanning + +class _EnumScanEntryFlags: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumScanEntryFlagsEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumScanEntryFlags.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + SCAN_FLAG_OPEN: _EnumScanEntryFlags.ValueType + "This network does not require authentication" + SCAN_FLAG_AUTHENTICATED: _EnumScanEntryFlags.ValueType + "This network requires authentication" + SCAN_FLAG_CONFIGURED: _EnumScanEntryFlags.ValueType + "This network has been previously provisioned" + SCAN_FLAG_BEST_SSID: _EnumScanEntryFlags.ValueType + SCAN_FLAG_ASSOCIATED: _EnumScanEntryFlags.ValueType + "Camera is connected to this AP" + SCAN_FLAG_UNSUPPORTED_TYPE: _EnumScanEntryFlags.ValueType + +class EnumScanEntryFlags(_EnumScanEntryFlags, metaclass=_EnumScanEntryFlagsEnumTypeWrapper): ... + +SCAN_FLAG_OPEN: EnumScanEntryFlags.ValueType +"This network does not require authentication" +SCAN_FLAG_AUTHENTICATED: EnumScanEntryFlags.ValueType +"This network requires authentication" +SCAN_FLAG_CONFIGURED: EnumScanEntryFlags.ValueType +"This network has been previously provisioned" +SCAN_FLAG_BEST_SSID: EnumScanEntryFlags.ValueType +SCAN_FLAG_ASSOCIATED: EnumScanEntryFlags.ValueType +"Camera is connected to this AP" +SCAN_FLAG_UNSUPPORTED_TYPE: EnumScanEntryFlags.ValueType +global___EnumScanEntryFlags = EnumScanEntryFlags + +@typing_extensions.final +class NotifProvisioningState(google.protobuf.message.Message): + """ + Provision state notification + + Sent during provisioning triggered via @ref RequestConnect or @ref RequestConnectNew + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + PROVISIONING_STATE_FIELD_NUMBER: builtins.int + provisioning_state: global___EnumProvisioning.ValueType + "Provisioning / connection state" + + def __init__(self, *, provisioning_state: global___EnumProvisioning.ValueType | None = ...) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["provisioning_state", b"provisioning_state"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["provisioning_state", b"provisioning_state"], + ) -> None: ... + +global___NotifProvisioningState = NotifProvisioningState + +@typing_extensions.final +class NotifStartScanning(google.protobuf.message.Message): + """ + Scanning state notification + + Triggered via @ref RequestStartScan + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SCANNING_STATE_FIELD_NUMBER: builtins.int + SCAN_ID_FIELD_NUMBER: builtins.int + TOTAL_ENTRIES_FIELD_NUMBER: builtins.int + TOTAL_CONFIGURED_SSID_FIELD_NUMBER: builtins.int + scanning_state: global___EnumScanning.ValueType + "Scanning state" + scan_id: builtins.int + "ID associated with scan results (included if scan was successful)" + total_entries: builtins.int + "Number of APs found during scan (included if scan was successful)" + total_configured_ssid: builtins.int + "Total count of camera's provisioned SSIDs" + + def __init__( + self, + *, + scanning_state: global___EnumScanning.ValueType | None = ..., + scan_id: builtins.int | None = ..., + total_entries: builtins.int | None = ..., + total_configured_ssid: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "scan_id", + b"scan_id", + "scanning_state", + b"scanning_state", + "total_configured_ssid", + b"total_configured_ssid", + "total_entries", + b"total_entries", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "scan_id", + b"scan_id", + "scanning_state", + b"scanning_state", + "total_configured_ssid", + b"total_configured_ssid", + "total_entries", + b"total_entries", + ], + ) -> None: ... + +global___NotifStartScanning = NotifStartScanning + +@typing_extensions.final +class RequestConnect(google.protobuf.message.Message): + """* + Connect to (but do not authenticate with) an Access Point + + This is intended to be used to connect to a previously-connected Access Point + + Response: @ref ResponseConnect + + Notification: @ref NotifProvisioningState sent periodically as provisioning state changes + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SSID_FIELD_NUMBER: builtins.int + ssid: builtins.str + "AP SSID" + + def __init__(self, *, ssid: builtins.str | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["ssid", b"ssid"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["ssid", b"ssid"]) -> None: ... + +global___RequestConnect = RequestConnect + +@typing_extensions.final +class RequestConnectNew(google.protobuf.message.Message): + """* + Connect to and authenticate with an Access Point + + This is only intended to be used if the AP is not previously provisioned. + + Response: @ref ResponseConnectNew sent immediately + + Notification: @ref NotifProvisioningState sent periodically as provisioning state changes + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SSID_FIELD_NUMBER: builtins.int + PASSWORD_FIELD_NUMBER: builtins.int + STATIC_IP_FIELD_NUMBER: builtins.int + GATEWAY_FIELD_NUMBER: builtins.int + SUBNET_FIELD_NUMBER: builtins.int + DNS_PRIMARY_FIELD_NUMBER: builtins.int + DNS_SECONDARY_FIELD_NUMBER: builtins.int + ssid: builtins.str + "AP SSID" + password: builtins.str + "AP password" + static_ip: builtins.bytes + "Static IP address" + gateway: builtins.bytes + "Gateway IP address" + subnet: builtins.bytes + "Subnet mask" + dns_primary: builtins.bytes + "Primary DNS" + dns_secondary: builtins.bytes + "Secondary DNS" + + def __init__( + self, + *, + ssid: builtins.str | None = ..., + password: builtins.str | None = ..., + static_ip: builtins.bytes | None = ..., + gateway: builtins.bytes | None = ..., + subnet: builtins.bytes | None = ..., + dns_primary: builtins.bytes | None = ..., + dns_secondary: builtins.bytes | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "dns_primary", + b"dns_primary", + "dns_secondary", + b"dns_secondary", + "gateway", + b"gateway", + "password", + b"password", + "ssid", + b"ssid", + "static_ip", + b"static_ip", + "subnet", + b"subnet", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "dns_primary", + b"dns_primary", + "dns_secondary", + b"dns_secondary", + "gateway", + b"gateway", + "password", + b"password", + "ssid", + b"ssid", + "static_ip", + b"static_ip", + "subnet", + b"subnet", + ], + ) -> None: ... + +global___RequestConnectNew = RequestConnectNew + +@typing_extensions.final +class RequestGetApEntries(google.protobuf.message.Message): + """* + Get a list of Access Points found during a @ref RequestStartScan + + Response: @ref ResponseGetApEntries + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + START_INDEX_FIELD_NUMBER: builtins.int + MAX_ENTRIES_FIELD_NUMBER: builtins.int + SCAN_ID_FIELD_NUMBER: builtins.int + start_index: builtins.int + "Used for paging. 0 <= start_index < @ref ResponseGetApEntries .total_entries" + max_entries: builtins.int + "Used for paging. Value must be < @ref ResponseGetApEntries .total_entries" + scan_id: builtins.int + "ID corresponding to a set of scan results (i.e. @ref ResponseGetApEntries .scan_id)" + + def __init__( + self, + *, + start_index: builtins.int | None = ..., + max_entries: builtins.int | None = ..., + scan_id: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "max_entries", + b"max_entries", + "scan_id", + b"scan_id", + "start_index", + b"start_index", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "max_entries", + b"max_entries", + "scan_id", + b"scan_id", + "start_index", + b"start_index", + ], + ) -> None: ... + +global___RequestGetApEntries = RequestGetApEntries + +@typing_extensions.final +class RequestReleaseNetwork(google.protobuf.message.Message): + """* + Request to disconnect from currently-connected AP + + This drops the camera out of Station (STA) Mode and returns it to Access Point (AP) mode. + + Response: @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestReleaseNetwork = RequestReleaseNetwork + +@typing_extensions.final +class RequestStartScan(google.protobuf.message.Message): + """* + Start scanning for Access Points + + @note Serialization of this object is zero bytes. + + Response: @ref ResponseStartScanning are sent immediately after the camera receives this command + + Notifications: @ref NotifStartScanning are sent periodically as scanning state changes. Use to detect scan complete. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__(self) -> None: ... + +global___RequestStartScan = RequestStartScan + +@typing_extensions.final +class ResponseConnect(google.protobuf.message.Message): + """* + The status of an attempt to connect to an Access Point + + Sent as the initial response to @ref RequestConnect + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + PROVISIONING_STATE_FIELD_NUMBER: builtins.int + TIMEOUT_SECONDS_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Generic pass/fail/error info" + provisioning_state: global___EnumProvisioning.ValueType + "Provisioning/connection state" + timeout_seconds: builtins.int + "Network connection timeout (seconds)" + + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + provisioning_state: global___EnumProvisioning.ValueType | None = ..., + timeout_seconds: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "provisioning_state", + b"provisioning_state", + "result", + b"result", + "timeout_seconds", + b"timeout_seconds", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "provisioning_state", + b"provisioning_state", + "result", + b"result", + "timeout_seconds", + b"timeout_seconds", + ], + ) -> None: ... + +global___ResponseConnect = ResponseConnect + +@typing_extensions.final +class ResponseConnectNew(google.protobuf.message.Message): + """* + The status of an attempt to connect to an Access Point + + Sent as the initial response to @ref RequestConnectNew + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + PROVISIONING_STATE_FIELD_NUMBER: builtins.int + TIMEOUT_SECONDS_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Status of Connect New request" + provisioning_state: global___EnumProvisioning.ValueType + "Current provisioning state of the network" + timeout_seconds: builtins.int + "*\n Number of seconds camera will wait before declaring a network connection attempt failed\n " + + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + provisioning_state: global___EnumProvisioning.ValueType | None = ..., + timeout_seconds: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "provisioning_state", + b"provisioning_state", + "result", + b"result", + "timeout_seconds", + b"timeout_seconds", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "provisioning_state", + b"provisioning_state", + "result", + b"result", + "timeout_seconds", + b"timeout_seconds", + ], + ) -> None: ... + +global___ResponseConnectNew = ResponseConnectNew + +@typing_extensions.final +class ResponseGetApEntries(google.protobuf.message.Message): + """* + A list of scan entries describing a scanned Access Point + + This is sent in response to a @ref RequestGetApEntries + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class ScanEntry(google.protobuf.message.Message): + """* + An individual Scan Entry in a @ref ResponseGetApEntries response + + @note When `scan_entry_flags` contains `SCAN_FLAG_CONFIGURED`, it is an indication that this network has already been provisioned. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + SSID_FIELD_NUMBER: builtins.int + SIGNAL_STRENGTH_BARS_FIELD_NUMBER: builtins.int + SIGNAL_FREQUENCY_MHZ_FIELD_NUMBER: builtins.int + SCAN_ENTRY_FLAGS_FIELD_NUMBER: builtins.int + ssid: builtins.str + "AP SSID" + signal_strength_bars: builtins.int + "Signal strength (3 bars: >-70 dBm; 2 bars: >-85 dBm; 1 bar: <=-85 dBm)" + signal_frequency_mhz: builtins.int + "Signal frequency (MHz)" + scan_entry_flags: builtins.int + "Bitmasked value from @ref EnumScanEntryFlags" + + def __init__( + self, + *, + ssid: builtins.str | None = ..., + signal_strength_bars: builtins.int | None = ..., + signal_frequency_mhz: builtins.int | None = ..., + scan_entry_flags: builtins.int | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "scan_entry_flags", + b"scan_entry_flags", + "signal_frequency_mhz", + b"signal_frequency_mhz", + "signal_strength_bars", + b"signal_strength_bars", + "ssid", + b"ssid", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "scan_entry_flags", + b"scan_entry_flags", + "signal_frequency_mhz", + b"signal_frequency_mhz", + "signal_strength_bars", + b"signal_strength_bars", + "ssid", + b"ssid", + ], + ) -> None: ... + + RESULT_FIELD_NUMBER: builtins.int + SCAN_ID_FIELD_NUMBER: builtins.int + ENTRIES_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Generic pass/fail/error info" + scan_id: builtins.int + "ID associated with this batch of results" + + @property + def entries( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResponseGetApEntries.ScanEntry]: + """Array containing details about discovered APs""" + + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + scan_id: builtins.int | None = ..., + entries: collections.abc.Iterable[global___ResponseGetApEntries.ScanEntry] | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["result", b"result", "scan_id", b"scan_id"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["entries", b"entries", "result", b"result", "scan_id", b"scan_id"], + ) -> None: ... + +global___ResponseGetApEntries = ResponseGetApEntries + +@typing_extensions.final +class ResponseStartScanning(google.protobuf.message.Message): + """* + The current scanning state. + + This is the initial response to a @ref RequestStartScan + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + SCANNING_STATE_FIELD_NUMBER: builtins.int + result: response_generic_pb2.EnumResultGeneric.ValueType + "Generic pass/fail/error info" + scanning_state: global___EnumScanning.ValueType + "Scanning state" + + def __init__( + self, + *, + result: response_generic_pb2.EnumResultGeneric.ValueType | None = ..., + scanning_state: global___EnumScanning.ValueType | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["result", b"result", "scanning_state", b"scanning_state"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["result", b"result", "scanning_state", b"scanning_state"], + ) -> None: ... + +global___ResponseStartScanning = ResponseStartScanning diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/preset_status_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/preset_status_pb2.py new file mode 100644 index 00000000..666abc10 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/preset_status_pb2.py @@ -0,0 +1,40 @@ +# preset_status_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +from . import response_generic_pb2 as response__generic__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x13preset_status.proto\x12\nopen_gopro\x1a\x16response_generic.proto"I\n\x12NotifyPresetStatus\x123\n\x12preset_group_array\x18\x01 \x03(\x0b2\x17.open_gopro.PresetGroup"\xaf\x02\n\x06Preset\x12\n\n\x02id\x18\x01 \x01(\x05\x12&\n\x04mode\x18\x02 \x01(\x0e2\x18.open_gopro.EnumFlatMode\x12-\n\x08title_id\x18\x03 \x01(\x0e2\x1b.open_gopro.EnumPresetTitle\x12\x14\n\x0ctitle_number\x18\x04 \x01(\x05\x12\x14\n\x0cuser_defined\x18\x05 \x01(\x08\x12(\n\x04icon\x18\x06 \x01(\x0e2\x1a.open_gopro.EnumPresetIcon\x120\n\rsetting_array\x18\x07 \x03(\x0b2\x19.open_gopro.PresetSetting\x12\x13\n\x0bis_modified\x18\x08 \x01(\x08\x12\x10\n\x08is_fixed\x18\t \x01(\x08\x12\x13\n\x0bcustom_name\x18\n \x01(\t"\x8c\x01\n\x19RequestCustomPresetUpdate\x12-\n\x08title_id\x18\x01 \x01(\x0e2\x1b.open_gopro.EnumPresetTitle\x12\x13\n\x0bcustom_name\x18\x02 \x01(\t\x12+\n\x07icon_id\x18\x03 \x01(\x0e2\x1a.open_gopro.EnumPresetIcon"\xa7\x01\n\x0bPresetGroup\x12\'\n\x02id\x18\x01 \x01(\x0e2\x1b.open_gopro.EnumPresetGroup\x12(\n\x0cpreset_array\x18\x02 \x03(\x0b2\x12.open_gopro.Preset\x12\x16\n\x0ecan_add_preset\x18\x03 \x01(\x08\x12-\n\x04icon\x18\x04 \x01(\x0e2\x1f.open_gopro.EnumPresetGroupIcon">\n\rPresetSetting\x12\n\n\x02id\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05\x12\x12\n\nis_caption\x18\x03 \x01(\x08*\x9b\x05\n\x0cEnumFlatMode\x12\x1e\n\x11FLAT_MODE_UNKNOWN\x10\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01\x12\x16\n\x12FLAT_MODE_PLAYBACK\x10\x04\x12\x13\n\x0fFLAT_MODE_SETUP\x10\x05\x12\x13\n\x0fFLAT_MODE_VIDEO\x10\x0c\x12\x1e\n\x1aFLAT_MODE_TIME_LAPSE_VIDEO\x10\r\x12\x15\n\x11FLAT_MODE_LOOPING\x10\x0f\x12\x1a\n\x16FLAT_MODE_PHOTO_SINGLE\x10\x10\x12\x13\n\x0fFLAT_MODE_PHOTO\x10\x11\x12\x19\n\x15FLAT_MODE_PHOTO_NIGHT\x10\x12\x12\x19\n\x15FLAT_MODE_PHOTO_BURST\x10\x13\x12\x1e\n\x1aFLAT_MODE_TIME_LAPSE_PHOTO\x10\x14\x12\x1f\n\x1bFLAT_MODE_NIGHT_LAPSE_PHOTO\x10\x15\x12\x1e\n\x1aFLAT_MODE_BROADCAST_RECORD\x10\x16\x12!\n\x1dFLAT_MODE_BROADCAST_BROADCAST\x10\x17\x12\x1d\n\x19FLAT_MODE_TIME_WARP_VIDEO\x10\x18\x12\x18\n\x14FLAT_MODE_LIVE_BURST\x10\x19\x12\x1f\n\x1bFLAT_MODE_NIGHT_LAPSE_VIDEO\x10\x1a\x12\x13\n\x0fFLAT_MODE_SLOMO\x10\x1b\x12\x12\n\x0eFLAT_MODE_IDLE\x10\x1c\x12\x1e\n\x1aFLAT_MODE_VIDEO_STAR_TRAIL\x10\x1d\x12"\n\x1eFLAT_MODE_VIDEO_LIGHT_PAINTING\x10\x1e\x12\x1f\n\x1bFLAT_MODE_VIDEO_LIGHT_TRAIL\x10\x1f\x12\x1f\n\x1bFLAT_MODE_VIDEO_BURST_SLOMO\x10 *i\n\x0fEnumPresetGroup\x12\x1a\n\x15PRESET_GROUP_ID_VIDEO\x10\xe8\x07\x12\x1a\n\x15PRESET_GROUP_ID_PHOTO\x10\xe9\x07\x12\x1e\n\x19PRESET_GROUP_ID_TIMELAPSE\x10\xea\x07*\xbc\x02\n\x13EnumPresetGroupIcon\x12\x1e\n\x1aPRESET_GROUP_VIDEO_ICON_ID\x10\x00\x12\x1e\n\x1aPRESET_GROUP_PHOTO_ICON_ID\x10\x01\x12"\n\x1ePRESET_GROUP_TIMELAPSE_ICON_ID\x10\x02\x12\'\n#PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID\x10\x03\x12(\n$PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID\x10\x04\x12"\n\x1ePRESET_GROUP_MAX_VIDEO_ICON_ID\x10\x05\x12"\n\x1ePRESET_GROUP_MAX_PHOTO_ICON_ID\x10\x06\x12&\n"PRESET_GROUP_MAX_TIMELAPSE_ICON_ID\x10\x07*\xc1\r\n\x0eEnumPresetIcon\x12\x15\n\x11PRESET_ICON_VIDEO\x10\x00\x12\x18\n\x14PRESET_ICON_ACTIVITY\x10\x01\x12\x19\n\x15PRESET_ICON_CINEMATIC\x10\x02\x12\x15\n\x11PRESET_ICON_PHOTO\x10\x03\x12\x1a\n\x16PRESET_ICON_LIVE_BURST\x10\x04\x12\x15\n\x11PRESET_ICON_BURST\x10\x05\x12\x1b\n\x17PRESET_ICON_PHOTO_NIGHT\x10\x06\x12\x18\n\x14PRESET_ICON_TIMEWARP\x10\x07\x12\x19\n\x15PRESET_ICON_TIMELAPSE\x10\x08\x12\x1a\n\x16PRESET_ICON_NIGHTLAPSE\x10\t\x12\x15\n\x11PRESET_ICON_SNAIL\x10\n\x12\x17\n\x13PRESET_ICON_VIDEO_2\x10\x0b\x12\x17\n\x13PRESET_ICON_PHOTO_2\x10\r\x12\x18\n\x14PRESET_ICON_PANORAMA\x10\x0e\x12\x17\n\x13PRESET_ICON_BURST_2\x10\x0f\x12\x1a\n\x16PRESET_ICON_TIMEWARP_2\x10\x10\x12\x1b\n\x17PRESET_ICON_TIMELAPSE_2\x10\x11\x12\x16\n\x12PRESET_ICON_CUSTOM\x10\x12\x12\x13\n\x0fPRESET_ICON_AIR\x10\x13\x12\x14\n\x10PRESET_ICON_BIKE\x10\x14\x12\x14\n\x10PRESET_ICON_EPIC\x10\x15\x12\x16\n\x12PRESET_ICON_INDOOR\x10\x16\x12\x15\n\x11PRESET_ICON_MOTOR\x10\x17\x12\x17\n\x13PRESET_ICON_MOUNTED\x10\x18\x12\x17\n\x13PRESET_ICON_OUTDOOR\x10\x19\x12\x13\n\x0fPRESET_ICON_POV\x10\x1a\x12\x16\n\x12PRESET_ICON_SELFIE\x10\x1b\x12\x15\n\x11PRESET_ICON_SKATE\x10\x1c\x12\x14\n\x10PRESET_ICON_SNOW\x10\x1d\x12\x15\n\x11PRESET_ICON_TRAIL\x10\x1e\x12\x16\n\x12PRESET_ICON_TRAVEL\x10\x1f\x12\x15\n\x11PRESET_ICON_WATER\x10 \x12\x17\n\x13PRESET_ICON_LOOPING\x10!\x12\x15\n\x11PRESET_ICON_STARS\x10"\x12\x16\n\x12PRESET_ICON_ACTION\x10#\x12\x1a\n\x16PRESET_ICON_FOLLOW_CAM\x10$\x12\x14\n\x10PRESET_ICON_SURF\x10%\x12\x14\n\x10PRESET_ICON_CITY\x10&\x12\x15\n\x11PRESET_ICON_SHAKY\x10\'\x12\x16\n\x12PRESET_ICON_CHESTY\x10(\x12\x16\n\x12PRESET_ICON_HELMET\x10)\x12\x14\n\x10PRESET_ICON_BITE\x10*\x12\x15\n\x11PRESET_ICON_BASIC\x10:\x12\x1c\n\x18PRESET_ICON_ULTRA_SLO_MO\x10;\x12"\n\x1ePRESET_ICON_STANDARD_ENDURANCE\x10<\x12"\n\x1ePRESET_ICON_ACTIVITY_ENDURANCE\x10=\x12#\n\x1fPRESET_ICON_CINEMATIC_ENDURANCE\x10>\x12\x1f\n\x1bPRESET_ICON_SLOMO_ENDURANCE\x10?\x12\x1c\n\x18PRESET_ICON_STATIONARY_1\x10@\x12\x1c\n\x18PRESET_ICON_STATIONARY_2\x10A\x12\x1c\n\x18PRESET_ICON_STATIONARY_3\x10B\x12\x1c\n\x18PRESET_ICON_STATIONARY_4\x10C\x12"\n\x1ePRESET_ICON_SIMPLE_SUPER_PHOTO\x10F\x12"\n\x1ePRESET_ICON_SIMPLE_NIGHT_PHOTO\x10G\x12%\n!PRESET_ICON_HIGHEST_QUALITY_VIDEO\x10I\x12&\n"PRESET_ICON_STANDARD_QUALITY_VIDEO\x10J\x12#\n\x1fPRESET_ICON_BASIC_QUALITY_VIDEO\x10K\x12\x1a\n\x16PRESET_ICON_STAR_TRAIL\x10L\x12\x1e\n\x1aPRESET_ICON_LIGHT_PAINTING\x10M\x12\x1b\n\x17PRESET_ICON_LIGHT_TRAIL\x10N\x12\x1a\n\x16PRESET_ICON_FULL_FRAME\x10O\x12 \n\x1bPRESET_ICON_TIMELAPSE_PHOTO\x10\xe8\x07\x12!\n\x1cPRESET_ICON_NIGHTLAPSE_PHOTO\x10\xe9\x07*\xfe\x0e\n\x0fEnumPresetTitle\x12\x19\n\x15PRESET_TITLE_ACTIVITY\x10\x00\x12\x19\n\x15PRESET_TITLE_STANDARD\x10\x01\x12\x1a\n\x16PRESET_TITLE_CINEMATIC\x10\x02\x12\x16\n\x12PRESET_TITLE_PHOTO\x10\x03\x12\x1b\n\x17PRESET_TITLE_LIVE_BURST\x10\x04\x12\x16\n\x12PRESET_TITLE_BURST\x10\x05\x12\x16\n\x12PRESET_TITLE_NIGHT\x10\x06\x12\x1a\n\x16PRESET_TITLE_TIME_WARP\x10\x07\x12\x1b\n\x17PRESET_TITLE_TIME_LAPSE\x10\x08\x12\x1c\n\x18PRESET_TITLE_NIGHT_LAPSE\x10\t\x12\x16\n\x12PRESET_TITLE_VIDEO\x10\n\x12\x16\n\x12PRESET_TITLE_SLOMO\x10\x0b\x12\x18\n\x14PRESET_TITLE_PHOTO_2\x10\r\x12\x19\n\x15PRESET_TITLE_PANORAMA\x10\x0e\x12\x1c\n\x18PRESET_TITLE_TIME_WARP_2\x10\x10\x12\x17\n\x13PRESET_TITLE_CUSTOM\x10\x12\x12\x14\n\x10PRESET_TITLE_AIR\x10\x13\x12\x15\n\x11PRESET_TITLE_BIKE\x10\x14\x12\x15\n\x11PRESET_TITLE_EPIC\x10\x15\x12\x17\n\x13PRESET_TITLE_INDOOR\x10\x16\x12\x16\n\x12PRESET_TITLE_MOTOR\x10\x17\x12\x18\n\x14PRESET_TITLE_MOUNTED\x10\x18\x12\x18\n\x14PRESET_TITLE_OUTDOOR\x10\x19\x12\x14\n\x10PRESET_TITLE_POV\x10\x1a\x12\x17\n\x13PRESET_TITLE_SELFIE\x10\x1b\x12\x16\n\x12PRESET_TITLE_SKATE\x10\x1c\x12\x15\n\x11PRESET_TITLE_SNOW\x10\x1d\x12\x16\n\x12PRESET_TITLE_TRAIL\x10\x1e\x12\x17\n\x13PRESET_TITLE_TRAVEL\x10\x1f\x12\x16\n\x12PRESET_TITLE_WATER\x10 \x12\x18\n\x14PRESET_TITLE_LOOPING\x10!\x12\x16\n\x12PRESET_TITLE_STARS\x10"\x12\x17\n\x13PRESET_TITLE_ACTION\x10#\x12\x1b\n\x17PRESET_TITLE_FOLLOW_CAM\x10$\x12\x15\n\x11PRESET_TITLE_SURF\x10%\x12\x15\n\x11PRESET_TITLE_CITY\x10&\x12\x16\n\x12PRESET_TITLE_SHAKY\x10\'\x12\x17\n\x13PRESET_TITLE_CHESTY\x10(\x12\x17\n\x13PRESET_TITLE_HELMET\x10)\x12\x15\n\x11PRESET_TITLE_BITE\x10*\x12\x16\n\x12PRESET_TITLE_BASIC\x10:\x12\x1d\n\x19PRESET_TITLE_ULTRA_SLO_MO\x10;\x12#\n\x1fPRESET_TITLE_STANDARD_ENDURANCE\x10<\x12#\n\x1fPRESET_TITLE_ACTIVITY_ENDURANCE\x10=\x12$\n PRESET_TITLE_CINEMATIC_ENDURANCE\x10>\x12 \n\x1cPRESET_TITLE_SLOMO_ENDURANCE\x10?\x12\x1d\n\x19PRESET_TITLE_STATIONARY_1\x10@\x12\x1d\n\x19PRESET_TITLE_STATIONARY_2\x10A\x12\x1d\n\x19PRESET_TITLE_STATIONARY_3\x10B\x12\x1d\n\x19PRESET_TITLE_STATIONARY_4\x10C\x12\x1d\n\x19PRESET_TITLE_SIMPLE_VIDEO\x10D\x12!\n\x1dPRESET_TITLE_SIMPLE_TIME_WARP\x10E\x12#\n\x1fPRESET_TITLE_SIMPLE_SUPER_PHOTO\x10F\x12#\n\x1fPRESET_TITLE_SIMPLE_NIGHT_PHOTO\x10G\x12\'\n#PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE\x10H\x12 \n\x1cPRESET_TITLE_HIGHEST_QUALITY\x10I\x12!\n\x1dPRESET_TITLE_EXTENDED_BATTERY\x10J\x12 \n\x1cPRESET_TITLE_LONGEST_BATTERY\x10K\x12\x1b\n\x17PRESET_TITLE_STAR_TRAIL\x10L\x12\x1f\n\x1bPRESET_TITLE_LIGHT_PAINTING\x10M\x12\x1c\n\x18PRESET_TITLE_LIGHT_TRAIL\x10N\x12\x1b\n\x17PRESET_TITLE_FULL_FRAME\x10O\x12\'\n#PRESET_TITLE_STANDARD_QUALITY_VIDEO\x10R\x12$\n PRESET_TITLE_BASIC_QUALITY_VIDEO\x10S\x12&\n"PRESET_TITLE_HIGHEST_QUALITY_VIDEO\x10]\x12)\n%PRESET_TITLE_USER_DEFINED_CUSTOM_NAME\x10^' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "preset_status_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMFLATMODE._serialized_start = 818 + _ENUMFLATMODE._serialized_end = 1485 + _ENUMPRESETGROUP._serialized_start = 1487 + _ENUMPRESETGROUP._serialized_end = 1592 + _ENUMPRESETGROUPICON._serialized_start = 1595 + _ENUMPRESETGROUPICON._serialized_end = 1911 + _ENUMPRESETICON._serialized_start = 1914 + _ENUMPRESETICON._serialized_end = 3643 + _ENUMPRESETTITLE._serialized_start = 3646 + _ENUMPRESETTITLE._serialized_end = 5564 + _NOTIFYPRESETSTATUS._serialized_start = 59 + _NOTIFYPRESETSTATUS._serialized_end = 132 + _PRESET._serialized_start = 135 + _PRESET._serialized_end = 438 + _REQUESTCUSTOMPRESETUPDATE._serialized_start = 441 + _REQUESTCUSTOMPRESETUPDATE._serialized_end = 581 + _PRESETGROUP._serialized_start = 584 + _PRESETGROUP._serialized_end = 751 + _PRESETSETTING._serialized_start = 753 + _PRESETSETTING._serialized_end = 815 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/preset_status_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/preset_status_pb2.pyi new file mode 100644 index 00000000..73bd5fc7 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/preset_status_pb2.pyi @@ -0,0 +1,702 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf message received from camera containing preset status +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumFlatMode: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumFlatModeEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumFlatMode.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + FLAT_MODE_UNKNOWN: _EnumFlatMode.ValueType + FLAT_MODE_PLAYBACK: _EnumFlatMode.ValueType + FLAT_MODE_SETUP: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO: _EnumFlatMode.ValueType + FLAT_MODE_TIME_LAPSE_VIDEO: _EnumFlatMode.ValueType + FLAT_MODE_LOOPING: _EnumFlatMode.ValueType + FLAT_MODE_PHOTO_SINGLE: _EnumFlatMode.ValueType + FLAT_MODE_PHOTO: _EnumFlatMode.ValueType + FLAT_MODE_PHOTO_NIGHT: _EnumFlatMode.ValueType + FLAT_MODE_PHOTO_BURST: _EnumFlatMode.ValueType + FLAT_MODE_TIME_LAPSE_PHOTO: _EnumFlatMode.ValueType + FLAT_MODE_NIGHT_LAPSE_PHOTO: _EnumFlatMode.ValueType + FLAT_MODE_BROADCAST_RECORD: _EnumFlatMode.ValueType + FLAT_MODE_BROADCAST_BROADCAST: _EnumFlatMode.ValueType + FLAT_MODE_TIME_WARP_VIDEO: _EnumFlatMode.ValueType + FLAT_MODE_LIVE_BURST: _EnumFlatMode.ValueType + FLAT_MODE_NIGHT_LAPSE_VIDEO: _EnumFlatMode.ValueType + FLAT_MODE_SLOMO: _EnumFlatMode.ValueType + FLAT_MODE_IDLE: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO_STAR_TRAIL: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO_LIGHT_PAINTING: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO_LIGHT_TRAIL: _EnumFlatMode.ValueType + FLAT_MODE_VIDEO_BURST_SLOMO: _EnumFlatMode.ValueType + +class EnumFlatMode(_EnumFlatMode, metaclass=_EnumFlatModeEnumTypeWrapper): ... + +FLAT_MODE_UNKNOWN: EnumFlatMode.ValueType +FLAT_MODE_PLAYBACK: EnumFlatMode.ValueType +FLAT_MODE_SETUP: EnumFlatMode.ValueType +FLAT_MODE_VIDEO: EnumFlatMode.ValueType +FLAT_MODE_TIME_LAPSE_VIDEO: EnumFlatMode.ValueType +FLAT_MODE_LOOPING: EnumFlatMode.ValueType +FLAT_MODE_PHOTO_SINGLE: EnumFlatMode.ValueType +FLAT_MODE_PHOTO: EnumFlatMode.ValueType +FLAT_MODE_PHOTO_NIGHT: EnumFlatMode.ValueType +FLAT_MODE_PHOTO_BURST: EnumFlatMode.ValueType +FLAT_MODE_TIME_LAPSE_PHOTO: EnumFlatMode.ValueType +FLAT_MODE_NIGHT_LAPSE_PHOTO: EnumFlatMode.ValueType +FLAT_MODE_BROADCAST_RECORD: EnumFlatMode.ValueType +FLAT_MODE_BROADCAST_BROADCAST: EnumFlatMode.ValueType +FLAT_MODE_TIME_WARP_VIDEO: EnumFlatMode.ValueType +FLAT_MODE_LIVE_BURST: EnumFlatMode.ValueType +FLAT_MODE_NIGHT_LAPSE_VIDEO: EnumFlatMode.ValueType +FLAT_MODE_SLOMO: EnumFlatMode.ValueType +FLAT_MODE_IDLE: EnumFlatMode.ValueType +FLAT_MODE_VIDEO_STAR_TRAIL: EnumFlatMode.ValueType +FLAT_MODE_VIDEO_LIGHT_PAINTING: EnumFlatMode.ValueType +FLAT_MODE_VIDEO_LIGHT_TRAIL: EnumFlatMode.ValueType +FLAT_MODE_VIDEO_BURST_SLOMO: EnumFlatMode.ValueType +global___EnumFlatMode = EnumFlatMode + +class _EnumPresetGroup: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumPresetGroupEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetGroup.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PRESET_GROUP_ID_VIDEO: _EnumPresetGroup.ValueType + PRESET_GROUP_ID_PHOTO: _EnumPresetGroup.ValueType + PRESET_GROUP_ID_TIMELAPSE: _EnumPresetGroup.ValueType + +class EnumPresetGroup(_EnumPresetGroup, metaclass=_EnumPresetGroupEnumTypeWrapper): ... + +PRESET_GROUP_ID_VIDEO: EnumPresetGroup.ValueType +PRESET_GROUP_ID_PHOTO: EnumPresetGroup.ValueType +PRESET_GROUP_ID_TIMELAPSE: EnumPresetGroup.ValueType +global___EnumPresetGroup = EnumPresetGroup + +class _EnumPresetGroupIcon: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumPresetGroupIconEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetGroupIcon.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PRESET_GROUP_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_PHOTO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_TIMELAPSE_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_MAX_VIDEO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_MAX_PHOTO_ICON_ID: _EnumPresetGroupIcon.ValueType + PRESET_GROUP_MAX_TIMELAPSE_ICON_ID: _EnumPresetGroupIcon.ValueType + +class EnumPresetGroupIcon(_EnumPresetGroupIcon, metaclass=_EnumPresetGroupIconEnumTypeWrapper): ... + +PRESET_GROUP_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_PHOTO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_TIMELAPSE_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_LONG_BAT_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_ENDURANCE_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_MAX_VIDEO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_MAX_PHOTO_ICON_ID: EnumPresetGroupIcon.ValueType +PRESET_GROUP_MAX_TIMELAPSE_ICON_ID: EnumPresetGroupIcon.ValueType +global___EnumPresetGroupIcon = EnumPresetGroupIcon + +class _EnumPresetIcon: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumPresetIconEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetIcon.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PRESET_ICON_VIDEO: _EnumPresetIcon.ValueType + PRESET_ICON_ACTIVITY: _EnumPresetIcon.ValueType + PRESET_ICON_CINEMATIC: _EnumPresetIcon.ValueType + PRESET_ICON_PHOTO: _EnumPresetIcon.ValueType + PRESET_ICON_LIVE_BURST: _EnumPresetIcon.ValueType + PRESET_ICON_BURST: _EnumPresetIcon.ValueType + PRESET_ICON_PHOTO_NIGHT: _EnumPresetIcon.ValueType + PRESET_ICON_TIMEWARP: _EnumPresetIcon.ValueType + PRESET_ICON_TIMELAPSE: _EnumPresetIcon.ValueType + PRESET_ICON_NIGHTLAPSE: _EnumPresetIcon.ValueType + PRESET_ICON_SNAIL: _EnumPresetIcon.ValueType + PRESET_ICON_VIDEO_2: _EnumPresetIcon.ValueType + PRESET_ICON_PHOTO_2: _EnumPresetIcon.ValueType + PRESET_ICON_PANORAMA: _EnumPresetIcon.ValueType + PRESET_ICON_BURST_2: _EnumPresetIcon.ValueType + PRESET_ICON_TIMEWARP_2: _EnumPresetIcon.ValueType + PRESET_ICON_TIMELAPSE_2: _EnumPresetIcon.ValueType + PRESET_ICON_CUSTOM: _EnumPresetIcon.ValueType + PRESET_ICON_AIR: _EnumPresetIcon.ValueType + PRESET_ICON_BIKE: _EnumPresetIcon.ValueType + PRESET_ICON_EPIC: _EnumPresetIcon.ValueType + PRESET_ICON_INDOOR: _EnumPresetIcon.ValueType + PRESET_ICON_MOTOR: _EnumPresetIcon.ValueType + PRESET_ICON_MOUNTED: _EnumPresetIcon.ValueType + PRESET_ICON_OUTDOOR: _EnumPresetIcon.ValueType + PRESET_ICON_POV: _EnumPresetIcon.ValueType + PRESET_ICON_SELFIE: _EnumPresetIcon.ValueType + PRESET_ICON_SKATE: _EnumPresetIcon.ValueType + PRESET_ICON_SNOW: _EnumPresetIcon.ValueType + PRESET_ICON_TRAIL: _EnumPresetIcon.ValueType + PRESET_ICON_TRAVEL: _EnumPresetIcon.ValueType + PRESET_ICON_WATER: _EnumPresetIcon.ValueType + PRESET_ICON_LOOPING: _EnumPresetIcon.ValueType + PRESET_ICON_STARS: _EnumPresetIcon.ValueType + PRESET_ICON_ACTION: _EnumPresetIcon.ValueType + PRESET_ICON_FOLLOW_CAM: _EnumPresetIcon.ValueType + PRESET_ICON_SURF: _EnumPresetIcon.ValueType + PRESET_ICON_CITY: _EnumPresetIcon.ValueType + PRESET_ICON_SHAKY: _EnumPresetIcon.ValueType + PRESET_ICON_CHESTY: _EnumPresetIcon.ValueType + PRESET_ICON_HELMET: _EnumPresetIcon.ValueType + PRESET_ICON_BITE: _EnumPresetIcon.ValueType + PRESET_ICON_BASIC: _EnumPresetIcon.ValueType + PRESET_ICON_ULTRA_SLO_MO: _EnumPresetIcon.ValueType + PRESET_ICON_STANDARD_ENDURANCE: _EnumPresetIcon.ValueType + PRESET_ICON_ACTIVITY_ENDURANCE: _EnumPresetIcon.ValueType + PRESET_ICON_CINEMATIC_ENDURANCE: _EnumPresetIcon.ValueType + PRESET_ICON_SLOMO_ENDURANCE: _EnumPresetIcon.ValueType + PRESET_ICON_STATIONARY_1: _EnumPresetIcon.ValueType + PRESET_ICON_STATIONARY_2: _EnumPresetIcon.ValueType + PRESET_ICON_STATIONARY_3: _EnumPresetIcon.ValueType + PRESET_ICON_STATIONARY_4: _EnumPresetIcon.ValueType + PRESET_ICON_SIMPLE_SUPER_PHOTO: _EnumPresetIcon.ValueType + PRESET_ICON_SIMPLE_NIGHT_PHOTO: _EnumPresetIcon.ValueType + PRESET_ICON_HIGHEST_QUALITY_VIDEO: _EnumPresetIcon.ValueType + PRESET_ICON_STANDARD_QUALITY_VIDEO: _EnumPresetIcon.ValueType + PRESET_ICON_BASIC_QUALITY_VIDEO: _EnumPresetIcon.ValueType + PRESET_ICON_STAR_TRAIL: _EnumPresetIcon.ValueType + PRESET_ICON_LIGHT_PAINTING: _EnumPresetIcon.ValueType + PRESET_ICON_LIGHT_TRAIL: _EnumPresetIcon.ValueType + PRESET_ICON_FULL_FRAME: _EnumPresetIcon.ValueType + PRESET_ICON_TIMELAPSE_PHOTO: _EnumPresetIcon.ValueType + PRESET_ICON_NIGHTLAPSE_PHOTO: _EnumPresetIcon.ValueType + +class EnumPresetIcon(_EnumPresetIcon, metaclass=_EnumPresetIconEnumTypeWrapper): ... + +PRESET_ICON_VIDEO: EnumPresetIcon.ValueType +PRESET_ICON_ACTIVITY: EnumPresetIcon.ValueType +PRESET_ICON_CINEMATIC: EnumPresetIcon.ValueType +PRESET_ICON_PHOTO: EnumPresetIcon.ValueType +PRESET_ICON_LIVE_BURST: EnumPresetIcon.ValueType +PRESET_ICON_BURST: EnumPresetIcon.ValueType +PRESET_ICON_PHOTO_NIGHT: EnumPresetIcon.ValueType +PRESET_ICON_TIMEWARP: EnumPresetIcon.ValueType +PRESET_ICON_TIMELAPSE: EnumPresetIcon.ValueType +PRESET_ICON_NIGHTLAPSE: EnumPresetIcon.ValueType +PRESET_ICON_SNAIL: EnumPresetIcon.ValueType +PRESET_ICON_VIDEO_2: EnumPresetIcon.ValueType +PRESET_ICON_PHOTO_2: EnumPresetIcon.ValueType +PRESET_ICON_PANORAMA: EnumPresetIcon.ValueType +PRESET_ICON_BURST_2: EnumPresetIcon.ValueType +PRESET_ICON_TIMEWARP_2: EnumPresetIcon.ValueType +PRESET_ICON_TIMELAPSE_2: EnumPresetIcon.ValueType +PRESET_ICON_CUSTOM: EnumPresetIcon.ValueType +PRESET_ICON_AIR: EnumPresetIcon.ValueType +PRESET_ICON_BIKE: EnumPresetIcon.ValueType +PRESET_ICON_EPIC: EnumPresetIcon.ValueType +PRESET_ICON_INDOOR: EnumPresetIcon.ValueType +PRESET_ICON_MOTOR: EnumPresetIcon.ValueType +PRESET_ICON_MOUNTED: EnumPresetIcon.ValueType +PRESET_ICON_OUTDOOR: EnumPresetIcon.ValueType +PRESET_ICON_POV: EnumPresetIcon.ValueType +PRESET_ICON_SELFIE: EnumPresetIcon.ValueType +PRESET_ICON_SKATE: EnumPresetIcon.ValueType +PRESET_ICON_SNOW: EnumPresetIcon.ValueType +PRESET_ICON_TRAIL: EnumPresetIcon.ValueType +PRESET_ICON_TRAVEL: EnumPresetIcon.ValueType +PRESET_ICON_WATER: EnumPresetIcon.ValueType +PRESET_ICON_LOOPING: EnumPresetIcon.ValueType +PRESET_ICON_STARS: EnumPresetIcon.ValueType +PRESET_ICON_ACTION: EnumPresetIcon.ValueType +PRESET_ICON_FOLLOW_CAM: EnumPresetIcon.ValueType +PRESET_ICON_SURF: EnumPresetIcon.ValueType +PRESET_ICON_CITY: EnumPresetIcon.ValueType +PRESET_ICON_SHAKY: EnumPresetIcon.ValueType +PRESET_ICON_CHESTY: EnumPresetIcon.ValueType +PRESET_ICON_HELMET: EnumPresetIcon.ValueType +PRESET_ICON_BITE: EnumPresetIcon.ValueType +PRESET_ICON_BASIC: EnumPresetIcon.ValueType +PRESET_ICON_ULTRA_SLO_MO: EnumPresetIcon.ValueType +PRESET_ICON_STANDARD_ENDURANCE: EnumPresetIcon.ValueType +PRESET_ICON_ACTIVITY_ENDURANCE: EnumPresetIcon.ValueType +PRESET_ICON_CINEMATIC_ENDURANCE: EnumPresetIcon.ValueType +PRESET_ICON_SLOMO_ENDURANCE: EnumPresetIcon.ValueType +PRESET_ICON_STATIONARY_1: EnumPresetIcon.ValueType +PRESET_ICON_STATIONARY_2: EnumPresetIcon.ValueType +PRESET_ICON_STATIONARY_3: EnumPresetIcon.ValueType +PRESET_ICON_STATIONARY_4: EnumPresetIcon.ValueType +PRESET_ICON_SIMPLE_SUPER_PHOTO: EnumPresetIcon.ValueType +PRESET_ICON_SIMPLE_NIGHT_PHOTO: EnumPresetIcon.ValueType +PRESET_ICON_HIGHEST_QUALITY_VIDEO: EnumPresetIcon.ValueType +PRESET_ICON_STANDARD_QUALITY_VIDEO: EnumPresetIcon.ValueType +PRESET_ICON_BASIC_QUALITY_VIDEO: EnumPresetIcon.ValueType +PRESET_ICON_STAR_TRAIL: EnumPresetIcon.ValueType +PRESET_ICON_LIGHT_PAINTING: EnumPresetIcon.ValueType +PRESET_ICON_LIGHT_TRAIL: EnumPresetIcon.ValueType +PRESET_ICON_FULL_FRAME: EnumPresetIcon.ValueType +PRESET_ICON_TIMELAPSE_PHOTO: EnumPresetIcon.ValueType +PRESET_ICON_NIGHTLAPSE_PHOTO: EnumPresetIcon.ValueType +global___EnumPresetIcon = EnumPresetIcon + +class _EnumPresetTitle: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumPresetTitleEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumPresetTitle.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PRESET_TITLE_ACTIVITY: _EnumPresetTitle.ValueType + PRESET_TITLE_STANDARD: _EnumPresetTitle.ValueType + PRESET_TITLE_CINEMATIC: _EnumPresetTitle.ValueType + PRESET_TITLE_PHOTO: _EnumPresetTitle.ValueType + PRESET_TITLE_LIVE_BURST: _EnumPresetTitle.ValueType + PRESET_TITLE_BURST: _EnumPresetTitle.ValueType + PRESET_TITLE_NIGHT: _EnumPresetTitle.ValueType + PRESET_TITLE_TIME_WARP: _EnumPresetTitle.ValueType + PRESET_TITLE_TIME_LAPSE: _EnumPresetTitle.ValueType + PRESET_TITLE_NIGHT_LAPSE: _EnumPresetTitle.ValueType + PRESET_TITLE_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_SLOMO: _EnumPresetTitle.ValueType + PRESET_TITLE_PHOTO_2: _EnumPresetTitle.ValueType + PRESET_TITLE_PANORAMA: _EnumPresetTitle.ValueType + PRESET_TITLE_TIME_WARP_2: _EnumPresetTitle.ValueType + PRESET_TITLE_CUSTOM: _EnumPresetTitle.ValueType + PRESET_TITLE_AIR: _EnumPresetTitle.ValueType + PRESET_TITLE_BIKE: _EnumPresetTitle.ValueType + PRESET_TITLE_EPIC: _EnumPresetTitle.ValueType + PRESET_TITLE_INDOOR: _EnumPresetTitle.ValueType + PRESET_TITLE_MOTOR: _EnumPresetTitle.ValueType + PRESET_TITLE_MOUNTED: _EnumPresetTitle.ValueType + PRESET_TITLE_OUTDOOR: _EnumPresetTitle.ValueType + PRESET_TITLE_POV: _EnumPresetTitle.ValueType + PRESET_TITLE_SELFIE: _EnumPresetTitle.ValueType + PRESET_TITLE_SKATE: _EnumPresetTitle.ValueType + PRESET_TITLE_SNOW: _EnumPresetTitle.ValueType + PRESET_TITLE_TRAIL: _EnumPresetTitle.ValueType + PRESET_TITLE_TRAVEL: _EnumPresetTitle.ValueType + PRESET_TITLE_WATER: _EnumPresetTitle.ValueType + PRESET_TITLE_LOOPING: _EnumPresetTitle.ValueType + PRESET_TITLE_STARS: _EnumPresetTitle.ValueType + PRESET_TITLE_ACTION: _EnumPresetTitle.ValueType + PRESET_TITLE_FOLLOW_CAM: _EnumPresetTitle.ValueType + PRESET_TITLE_SURF: _EnumPresetTitle.ValueType + PRESET_TITLE_CITY: _EnumPresetTitle.ValueType + PRESET_TITLE_SHAKY: _EnumPresetTitle.ValueType + PRESET_TITLE_CHESTY: _EnumPresetTitle.ValueType + PRESET_TITLE_HELMET: _EnumPresetTitle.ValueType + PRESET_TITLE_BITE: _EnumPresetTitle.ValueType + PRESET_TITLE_BASIC: _EnumPresetTitle.ValueType + PRESET_TITLE_ULTRA_SLO_MO: _EnumPresetTitle.ValueType + PRESET_TITLE_STANDARD_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_ACTIVITY_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_CINEMATIC_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_SLOMO_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_STATIONARY_1: _EnumPresetTitle.ValueType + PRESET_TITLE_STATIONARY_2: _EnumPresetTitle.ValueType + PRESET_TITLE_STATIONARY_3: _EnumPresetTitle.ValueType + PRESET_TITLE_STATIONARY_4: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_TIME_WARP: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_SUPER_PHOTO: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_NIGHT_PHOTO: _EnumPresetTitle.ValueType + PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE: _EnumPresetTitle.ValueType + PRESET_TITLE_HIGHEST_QUALITY: _EnumPresetTitle.ValueType + PRESET_TITLE_EXTENDED_BATTERY: _EnumPresetTitle.ValueType + PRESET_TITLE_LONGEST_BATTERY: _EnumPresetTitle.ValueType + PRESET_TITLE_STAR_TRAIL: _EnumPresetTitle.ValueType + PRESET_TITLE_LIGHT_PAINTING: _EnumPresetTitle.ValueType + PRESET_TITLE_LIGHT_TRAIL: _EnumPresetTitle.ValueType + PRESET_TITLE_FULL_FRAME: _EnumPresetTitle.ValueType + PRESET_TITLE_STANDARD_QUALITY_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_BASIC_QUALITY_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_HIGHEST_QUALITY_VIDEO: _EnumPresetTitle.ValueType + PRESET_TITLE_USER_DEFINED_CUSTOM_NAME: _EnumPresetTitle.ValueType + +class EnumPresetTitle(_EnumPresetTitle, metaclass=_EnumPresetTitleEnumTypeWrapper): ... + +PRESET_TITLE_ACTIVITY: EnumPresetTitle.ValueType +PRESET_TITLE_STANDARD: EnumPresetTitle.ValueType +PRESET_TITLE_CINEMATIC: EnumPresetTitle.ValueType +PRESET_TITLE_PHOTO: EnumPresetTitle.ValueType +PRESET_TITLE_LIVE_BURST: EnumPresetTitle.ValueType +PRESET_TITLE_BURST: EnumPresetTitle.ValueType +PRESET_TITLE_NIGHT: EnumPresetTitle.ValueType +PRESET_TITLE_TIME_WARP: EnumPresetTitle.ValueType +PRESET_TITLE_TIME_LAPSE: EnumPresetTitle.ValueType +PRESET_TITLE_NIGHT_LAPSE: EnumPresetTitle.ValueType +PRESET_TITLE_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_SLOMO: EnumPresetTitle.ValueType +PRESET_TITLE_PHOTO_2: EnumPresetTitle.ValueType +PRESET_TITLE_PANORAMA: EnumPresetTitle.ValueType +PRESET_TITLE_TIME_WARP_2: EnumPresetTitle.ValueType +PRESET_TITLE_CUSTOM: EnumPresetTitle.ValueType +PRESET_TITLE_AIR: EnumPresetTitle.ValueType +PRESET_TITLE_BIKE: EnumPresetTitle.ValueType +PRESET_TITLE_EPIC: EnumPresetTitle.ValueType +PRESET_TITLE_INDOOR: EnumPresetTitle.ValueType +PRESET_TITLE_MOTOR: EnumPresetTitle.ValueType +PRESET_TITLE_MOUNTED: EnumPresetTitle.ValueType +PRESET_TITLE_OUTDOOR: EnumPresetTitle.ValueType +PRESET_TITLE_POV: EnumPresetTitle.ValueType +PRESET_TITLE_SELFIE: EnumPresetTitle.ValueType +PRESET_TITLE_SKATE: EnumPresetTitle.ValueType +PRESET_TITLE_SNOW: EnumPresetTitle.ValueType +PRESET_TITLE_TRAIL: EnumPresetTitle.ValueType +PRESET_TITLE_TRAVEL: EnumPresetTitle.ValueType +PRESET_TITLE_WATER: EnumPresetTitle.ValueType +PRESET_TITLE_LOOPING: EnumPresetTitle.ValueType +PRESET_TITLE_STARS: EnumPresetTitle.ValueType +PRESET_TITLE_ACTION: EnumPresetTitle.ValueType +PRESET_TITLE_FOLLOW_CAM: EnumPresetTitle.ValueType +PRESET_TITLE_SURF: EnumPresetTitle.ValueType +PRESET_TITLE_CITY: EnumPresetTitle.ValueType +PRESET_TITLE_SHAKY: EnumPresetTitle.ValueType +PRESET_TITLE_CHESTY: EnumPresetTitle.ValueType +PRESET_TITLE_HELMET: EnumPresetTitle.ValueType +PRESET_TITLE_BITE: EnumPresetTitle.ValueType +PRESET_TITLE_BASIC: EnumPresetTitle.ValueType +PRESET_TITLE_ULTRA_SLO_MO: EnumPresetTitle.ValueType +PRESET_TITLE_STANDARD_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_ACTIVITY_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_CINEMATIC_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_SLOMO_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_STATIONARY_1: EnumPresetTitle.ValueType +PRESET_TITLE_STATIONARY_2: EnumPresetTitle.ValueType +PRESET_TITLE_STATIONARY_3: EnumPresetTitle.ValueType +PRESET_TITLE_STATIONARY_4: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_TIME_WARP: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_SUPER_PHOTO: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_NIGHT_PHOTO: EnumPresetTitle.ValueType +PRESET_TITLE_SIMPLE_VIDEO_ENDURANCE: EnumPresetTitle.ValueType +PRESET_TITLE_HIGHEST_QUALITY: EnumPresetTitle.ValueType +PRESET_TITLE_EXTENDED_BATTERY: EnumPresetTitle.ValueType +PRESET_TITLE_LONGEST_BATTERY: EnumPresetTitle.ValueType +PRESET_TITLE_STAR_TRAIL: EnumPresetTitle.ValueType +PRESET_TITLE_LIGHT_PAINTING: EnumPresetTitle.ValueType +PRESET_TITLE_LIGHT_TRAIL: EnumPresetTitle.ValueType +PRESET_TITLE_FULL_FRAME: EnumPresetTitle.ValueType +PRESET_TITLE_STANDARD_QUALITY_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_BASIC_QUALITY_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_HIGHEST_QUALITY_VIDEO: EnumPresetTitle.ValueType +PRESET_TITLE_USER_DEFINED_CUSTOM_NAME: EnumPresetTitle.ValueType +global___EnumPresetTitle = EnumPresetTitle + +@typing_extensions.final +class NotifyPresetStatus(google.protobuf.message.Message): + """* + Current Preset status + + Sent either: + + - Synchronously via initial response to @ref RequestGetPresetStatus + - Asynchronously when Preset change if registered in @ref RequestGetPresetStatus + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + PRESET_GROUP_ARRAY_FIELD_NUMBER: builtins.int + + @property + def preset_group_array( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___PresetGroup]: + """List of currently available Preset Groups""" + + 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: ... + +global___NotifyPresetStatus = NotifyPresetStatus + +@typing_extensions.final +class Preset(google.protobuf.message.Message): + """* + An individual preset. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + ID_FIELD_NUMBER: builtins.int + MODE_FIELD_NUMBER: builtins.int + TITLE_ID_FIELD_NUMBER: builtins.int + TITLE_NUMBER_FIELD_NUMBER: builtins.int + USER_DEFINED_FIELD_NUMBER: builtins.int + ICON_FIELD_NUMBER: builtins.int + SETTING_ARRAY_FIELD_NUMBER: builtins.int + IS_MODIFIED_FIELD_NUMBER: builtins.int + IS_FIXED_FIELD_NUMBER: builtins.int + CUSTOM_NAME_FIELD_NUMBER: builtins.int + id: builtins.int + "Preset ID" + mode: global___EnumFlatMode.ValueType + "Preset flatmode ID" + title_id: global___EnumPresetTitle.ValueType + "Preset Title ID" + title_number: builtins.int + "Preset Title Number (e.g. 1/2/3 in Custom1, Custom2, Custom3)" + user_defined: builtins.bool + "Is the Preset custom/user-defined?" + icon: global___EnumPresetIcon.ValueType + "Preset Icon ID" + + @property + def setting_array( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___PresetSetting]: + """Array of settings associated with this Preset""" + is_modified: builtins.bool + "Has Preset been modified from factory defaults? (False for user-defined Presets)" + is_fixed: builtins.bool + "Is this Preset mutable?" + custom_name: builtins.str + "Custom string name given to this preset via @ref RequestCustomPresetUpdate" + + def __init__( + self, + *, + id: builtins.int | None = ..., + mode: global___EnumFlatMode.ValueType | None = ..., + title_id: global___EnumPresetTitle.ValueType | None = ..., + title_number: builtins.int | None = ..., + user_defined: builtins.bool | None = ..., + icon: global___EnumPresetIcon.ValueType | None = ..., + setting_array: collections.abc.Iterable[global___PresetSetting] | None = ..., + is_modified: builtins.bool | None = ..., + is_fixed: builtins.bool | None = ..., + custom_name: builtins.str | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "custom_name", + b"custom_name", + "icon", + b"icon", + "id", + b"id", + "is_fixed", + b"is_fixed", + "is_modified", + b"is_modified", + "mode", + b"mode", + "title_id", + b"title_id", + "title_number", + b"title_number", + "user_defined", + b"user_defined", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "custom_name", + b"custom_name", + "icon", + b"icon", + "id", + b"id", + "is_fixed", + b"is_fixed", + "is_modified", + b"is_modified", + "mode", + b"mode", + "setting_array", + b"setting_array", + "title_id", + b"title_id", + "title_number", + b"title_number", + "user_defined", + b"user_defined", + ], + ) -> None: ... + +global___Preset = Preset + +@typing_extensions.final +class RequestCustomPresetUpdate(google.protobuf.message.Message): + """* + Request to Update the Title and / or Icon of the Active Custom Preset + + This only operates on the currently active Preset and will fail if the current + Preset is not custom. + + The use cases are: + + 1. Update the Custom Preset Icon + + - `icon_id` is always optional and can always be passed + + and / or + + 2. Update the Custom Preset Title to a... + + - **Factory Preset Title**: Set `title_id` to a non-PRESET_TITLE_USER_DEFINED_CUSTOM_NAME (94) value + - **Custom Preset Name**: Set `title_id` to PRESET_TITLE_USER_DEFINED_CUSTOM_NAME (94) and specify a `custom_name` + + Returns a @ref ResponseGeneric with the status of the preset update request. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TITLE_ID_FIELD_NUMBER: builtins.int + CUSTOM_NAME_FIELD_NUMBER: builtins.int + ICON_ID_FIELD_NUMBER: builtins.int + title_id: global___EnumPresetTitle.ValueType + "*\n Preset Title ID\n\n The range of acceptable custom title ID's can be found in the initial @ref NotifyPresetStatus response\n to @ref RequestGetPresetStatus\n " + custom_name: builtins.str + "*\n UTF-8 encoded custom preset name\n\n The name must obey the following:\n\n - Custom titles must be between 1 and 16 characters (inclusive)\n - No special characters outside of the following languages: English, French, Italian, German,\n Spanish, Portuguese, Swedish, Russian\n " + icon_id: global___EnumPresetIcon.ValueType + "*\n Preset Icon ID\n\n The range of acceptable custom icon ID's can be found in the initial @ref NotifyPresetStatus response to\n @ref RequestGetPresetStatus\n " + + def __init__( + self, + *, + title_id: global___EnumPresetTitle.ValueType | None = ..., + custom_name: builtins.str | None = ..., + icon_id: global___EnumPresetIcon.ValueType | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal[ + "custom_name", + b"custom_name", + "icon_id", + b"icon_id", + "title_id", + b"title_id", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "custom_name", + b"custom_name", + "icon_id", + b"icon_id", + "title_id", + b"title_id", + ], + ) -> None: ... + +global___RequestCustomPresetUpdate = RequestCustomPresetUpdate + +@typing_extensions.final +class PresetGroup(google.protobuf.message.Message): + """ + Preset Group meta information and contained Presets + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + ID_FIELD_NUMBER: builtins.int + PRESET_ARRAY_FIELD_NUMBER: builtins.int + CAN_ADD_PRESET_FIELD_NUMBER: builtins.int + ICON_FIELD_NUMBER: builtins.int + id: global___EnumPresetGroup.ValueType + "Preset Group ID" + + @property + def preset_array( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Preset]: + """Array of Presets contained in this Preset Group""" + can_add_preset: builtins.bool + "Is there room in the group to add additional Presets?" + icon: global___EnumPresetGroupIcon.ValueType + "The icon to display for this preset group" + + def __init__( + self, + *, + id: global___EnumPresetGroup.ValueType | None = ..., + preset_array: collections.abc.Iterable[global___Preset] | None = ..., + can_add_preset: builtins.bool | None = ..., + icon: global___EnumPresetGroupIcon.ValueType | None = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["can_add_preset", b"can_add_preset", "icon", b"icon", "id", b"id"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "can_add_preset", + b"can_add_preset", + "icon", + b"icon", + "id", + b"id", + "preset_array", + b"preset_array", + ], + ) -> None: ... + +global___PresetGroup = PresetGroup + +@typing_extensions.final +class PresetSetting(google.protobuf.message.Message): + """* + Setting representation that comprises a @ref Preset + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + ID_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + IS_CAPTION_FIELD_NUMBER: builtins.int + id: builtins.int + "Setting ID" + value: builtins.int + "Setting value" + is_caption: builtins.bool + '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 = ... + ) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["id", b"id", "is_caption", b"is_caption", "value", b"value"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["id", b"id", "is_caption", b"is_caption", "value", b"value"], + ) -> None: ... + +global___PresetSetting = PresetSetting diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/request_get_preset_status_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/request_get_preset_status_pb2.py new file mode 100644 index 00000000..28fe041f --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/request_get_preset_status_pb2.py @@ -0,0 +1,22 @@ +# 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 Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x1frequest_get_preset_status.proto\x12\nopen_gopro"\xa6\x01\n\x16RequestGetPresetStatus\x12D\n\x16register_preset_status\x18\x01 \x03(\x0e2$.open_gopro.EnumRegisterPresetStatus\x12F\n\x18unregister_preset_status\x18\x02 \x03(\x0e2$.open_gopro.EnumRegisterPresetStatus*l\n\x18EnumRegisterPresetStatus\x12!\n\x1dREGISTER_PRESET_STATUS_PRESET\x10\x01\x12-\n)REGISTER_PRESET_STATUS_PRESET_GROUP_ARRAY\x10\x02' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "request_get_preset_status_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMREGISTERPRESETSTATUS._serialized_start = 216 + _ENUMREGISTERPRESETSTATUS._serialized_end = 324 + _REQUESTGETPRESETSTATUS._serialized_start = 48 + _REQUESTGETPRESETSTATUS._serialized_end = 214 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/request_get_preset_status_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/request_get_preset_status_pb2.pyi new file mode 100644 index 00000000..13e7d1c8 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/request_get_preset_status_pb2.pyi @@ -0,0 +1,93 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for obtaining preset status +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumRegisterPresetStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumRegisterPresetStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumRegisterPresetStatus.ValueType], + builtins.type, +): + 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): ... + +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 + +@typing_extensions.final +class RequestGetPresetStatus(google.protobuf.message.Message): + """* + Get the set of currently available presets and optionally register to be notified when it changes. + + Response: @ref NotifyPresetStatus sent immediately + + Notification: @ref NotifyPresetStatus sent periodically as preset status changes, if registered. + + The preset status changes when: + + - A client changes one of a preset's captioned settings via the API + - The user exits from a preset's settings UI on the camera (e.g. long-press the preset pill and then press the back arrow) + - The user creates/deletes/reorders a preset within a group + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + REGISTER_PRESET_STATUS_FIELD_NUMBER: builtins.int + UNREGISTER_PRESET_STATUS_FIELD_NUMBER: builtins.int + + @property + def register_preset_status( + self, + ) -> 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]: + """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 = ... + ) -> None: ... + def ClearField( + self, + field_name: typing_extensions.Literal[ + "register_preset_status", + b"register_preset_status", + "unregister_preset_status", + b"unregister_preset_status", + ], + ) -> None: ... + +global___RequestGetPresetStatus = RequestGetPresetStatus diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/response_generic_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/response_generic_pb2.py new file mode 100644 index 00000000..a61b782b --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/response_generic_pb2.py @@ -0,0 +1,24 @@ +# response_generic_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x16response_generic.proto\x12\nopen_gopro"@\n\x0fResponseGeneric\x12-\n\x06result\x18\x01 \x02(\x0e2\x1d.open_gopro.EnumResultGeneric"%\n\x05Media\x12\x0e\n\x06folder\x18\x01 \x01(\t\x12\x0c\n\x04file\x18\x02 \x01(\t*\xcf\x01\n\x11EnumResultGeneric\x12\x12\n\x0eRESULT_UNKNOWN\x10\x00\x12\x12\n\x0eRESULT_SUCCESS\x10\x01\x12\x15\n\x11RESULT_ILL_FORMED\x10\x02\x12\x18\n\x14RESULT_NOT_SUPPORTED\x10\x03\x12!\n\x1dRESULT_ARGUMENT_OUT_OF_BOUNDS\x10\x04\x12\x1b\n\x17RESULT_ARGUMENT_INVALID\x10\x05\x12!\n\x1dRESULT_RESOURCE_NOT_AVAILABLE\x10\x06' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "response_generic_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMRESULTGENERIC._serialized_start = 144 + _ENUMRESULTGENERIC._serialized_end = 351 + _RESPONSEGENERIC._serialized_start = 38 + _RESPONSEGENERIC._serialized_end = 102 + _MEDIA._serialized_start = 104 + _MEDIA._serialized_end = 141 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/response_generic_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/response_generic_pb2.pyi new file mode 100644 index 00000000..85655c36 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/response_generic_pb2.pyi @@ -0,0 +1,90 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf message containing generic response to a command +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumResultGeneric: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumResultGenericEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumResultGeneric.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + RESULT_UNKNOWN: _EnumResultGeneric.ValueType + RESULT_SUCCESS: _EnumResultGeneric.ValueType + RESULT_ILL_FORMED: _EnumResultGeneric.ValueType + RESULT_NOT_SUPPORTED: _EnumResultGeneric.ValueType + RESULT_ARGUMENT_OUT_OF_BOUNDS: _EnumResultGeneric.ValueType + RESULT_ARGUMENT_INVALID: _EnumResultGeneric.ValueType + RESULT_RESOURCE_NOT_AVAILABLE: _EnumResultGeneric.ValueType + +class EnumResultGeneric(_EnumResultGeneric, metaclass=_EnumResultGenericEnumTypeWrapper): ... + +RESULT_UNKNOWN: EnumResultGeneric.ValueType +RESULT_SUCCESS: EnumResultGeneric.ValueType +RESULT_ILL_FORMED: EnumResultGeneric.ValueType +RESULT_NOT_SUPPORTED: EnumResultGeneric.ValueType +RESULT_ARGUMENT_OUT_OF_BOUNDS: EnumResultGeneric.ValueType +RESULT_ARGUMENT_INVALID: EnumResultGeneric.ValueType +RESULT_RESOURCE_NOT_AVAILABLE: EnumResultGeneric.ValueType +global___EnumResultGeneric = EnumResultGeneric + +@typing_extensions.final +class ResponseGeneric(google.protobuf.message.Message): + """ + Generic Response used across many response / notification messages + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RESULT_FIELD_NUMBER: builtins.int + result: global___EnumResultGeneric.ValueType + "Generic pass/fail/error info" + + def __init__(self, *, result: global___EnumResultGeneric.ValueType | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["result", b"result"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["result", b"result"]) -> None: ... + +global___ResponseGeneric = ResponseGeneric + +@typing_extensions.final +class Media(google.protobuf.message.Message): + """* + A common model to represent a media file + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + FOLDER_FIELD_NUMBER: builtins.int + FILE_FIELD_NUMBER: builtins.int + folder: builtins.str + "Directory in which the media is contained" + file: builtins.str + "Filename of media" + + def __init__(self, *, folder: builtins.str | None = ..., file: builtins.str | None = ...) -> None: ... + def HasField( + self, + field_name: typing_extensions.Literal["file", b"file", "folder", b"folder"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["file", b"file", "folder", b"folder"], + ) -> None: ... + +global___Media = Media diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/set_camera_control_status_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/set_camera_control_status_pb2.py new file mode 100644 index 00000000..543ced5f --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/set_camera_control_status_pb2.py @@ -0,0 +1,22 @@ +# 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 Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x1fset_camera_control_status.proto\x12\nopen_gopro"c\n\x1dRequestSetCameraControlStatus\x12B\n\x15camera_control_status\x18\x01 \x02(\x0e2#.open_gopro.EnumCameraControlStatus*[\n\x17EnumCameraControlStatus\x12\x0f\n\x0bCAMERA_IDLE\x10\x00\x12\x12\n\x0eCAMERA_CONTROL\x10\x01\x12\x1b\n\x17CAMERA_EXTERNAL_CONTROL\x10\x02' +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "set_camera_control_status_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _ENUMCAMERACONTROLSTATUS._serialized_start = 148 + _ENUMCAMERACONTROLSTATUS._serialized_end = 239 + _REQUESTSETCAMERACONTROLSTATUS._serialized_start = 47 + _REQUESTSETCAMERACONTROLSTATUS._serialized_end = 146 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/set_camera_control_status_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/set_camera_control_status_pb2.pyi new file mode 100644 index 00000000..37b27336 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/set_camera_control_status_pb2.pyi @@ -0,0 +1,74 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for setting camera control status +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _EnumCameraControlStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _EnumCameraControlStatusEnumTypeWrapper( + google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_EnumCameraControlStatus.ValueType], + builtins.type, +): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + CAMERA_IDLE: _EnumCameraControlStatus.ValueType + CAMERA_CONTROL: _EnumCameraControlStatus.ValueType + "Can only be set by camera, not by app or third party" + CAMERA_EXTERNAL_CONTROL: _EnumCameraControlStatus.ValueType + +class EnumCameraControlStatus(_EnumCameraControlStatus, metaclass=_EnumCameraControlStatusEnumTypeWrapper): ... + +CAMERA_IDLE: EnumCameraControlStatus.ValueType +CAMERA_CONTROL: EnumCameraControlStatus.ValueType +"Can only be set by camera, not by app or third party" +CAMERA_EXTERNAL_CONTROL: EnumCameraControlStatus.ValueType +global___EnumCameraControlStatus = EnumCameraControlStatus + +@typing_extensions.final +class RequestSetCameraControlStatus(google.protobuf.message.Message): + """* + Set Camera Control Status (as part of Global Behaviors feature) + + This command is used to tell the camera that the app (i.e. External Control) wishes to claim control of the camera. + This causes the camera to immediately exit most contextual menus and return to the idle screen. Any interaction with + the camera's physical buttons will cause the camera to reclaim control and update control status accordingly. If the + user returns the camera UI to the idle screen, the camera updates control status to Idle. + + The entity currently claiming control of the camera is advertised in camera status 114. Information about whether the + camera is in a contextual menu or not is advertised in camera status 63. + + Response: @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + CAMERA_CONTROL_STATUS_FIELD_NUMBER: builtins.int + 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 HasField( + self, + field_name: typing_extensions.Literal["camera_control_status", b"camera_control_status"], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing_extensions.Literal["camera_control_status", b"camera_control_status"], + ) -> None: ... + +global___RequestSetCameraControlStatus = RequestSetCameraControlStatus diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/turbo_transfer_pb2.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/turbo_transfer_pb2.py new file mode 100644 index 00000000..97d913a0 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/turbo_transfer_pb2.py @@ -0,0 +1,20 @@ +# turbo_transfer_pb2.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +"""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 + +_sym_db = _symbol_database.Default() +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b"\n\x14turbo_transfer.proto\x12\nopen_gopro\"'\n\x15RequestSetTurboActive\x12\x0e\n\x06active\x18\x01 \x02(\x08" +) +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "turbo_transfer_pb2", globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _REQUESTSETTURBOACTIVE._serialized_start = 36 + _REQUESTSETTURBOACTIVE._serialized_end = 75 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/turbo_transfer_pb2.pyi b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/turbo_transfer_pb2.pyi new file mode 100644 index 00000000..0c79e66a --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto/turbo_transfer_pb2.pyi @@ -0,0 +1,36 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +* +Defines the structure of protobuf messages for enabling and disabling Turbo Transfer feature +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing_extensions.final +class RequestSetTurboActive(google.protobuf.message.Message): + """* + Enable/disable display of "Transferring Media" UI + + Response: @ref ResponseGeneric + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + ACTIVE_FIELD_NUMBER: builtins.int + active: builtins.bool + "Enable or disable Turbo Transfer feature" + + def __init__(self, *, active: builtins.bool | None = ...) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["active", b"active"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["active", b"active"]) -> None: ... + +global___RequestSetTurboActive = RequestSetTurboActive diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/protobuf_example.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/protobuf_example.py new file mode 100644 index 00000000..bf7cab06 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/protobuf_example.py @@ -0,0 +1,32 @@ +# protobuf_example.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +import sys +import argparse + +from tutorial_modules import logger, proto + + +def main() -> None: + request = proto.RequestSetTurboActive(active=False) + logger.info(f"Sending ==> {request}") + logger.info(request.SerializeToString().hex(":")) + + # We're not hard-coding serialized bytes here since it may not be constant across Protobuf versions + response_bytes = proto.ResponseGeneric(result=proto.EnumResultGeneric.RESULT_SUCCESS).SerializeToString() + logger.info(f"Received bytes ==> {response_bytes.hex(':')}") + response = proto.ResponseGeneric.FromString(response_bytes) + logger.info(f"Received ==> {response}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Perform some basic protobuf manipulation.") + args = parser.parse_args() + + try: + main() + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(e) + sys.exit(-1) + else: + sys.exit(0) diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/set_turbo_mode.py b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/set_turbo_mode.py new file mode 100644 index 00000000..ff85efea --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/set_turbo_mode.py @@ -0,0 +1,114 @@ +# set_turbo_mode.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +import sys +import asyncio +import argparse + +from bleak import BleakClient +from bleak.backends.characteristic import BleakGATTCharacteristic +from google.protobuf.message import Message as ProtobufMessage + +from tutorial_modules import logger +from tutorial_modules import GoProUuid, connect_ble, Response, proto + + +class ProtobufResponse(Response): + """Accumulate and parse protobuf responses""" + + def __init__(self, uuid: GoProUuid) -> None: + super().__init__(uuid) + self.feature_id: int + self.action_id: int + self.uuid = uuid + self.data: ProtobufMessage + + def parse(self, proto_message: type[ProtobufMessage]) -> None: + """Set the responses data by parsing using the passed in protobuf container + + Args: + proto_message (type[ProtobufMessage]): protobuf container to use for parsing + """ + self.feature_id = self.raw_bytes[0] + self.action_id = self.raw_bytes[1] + self.data = proto_message.FromString(bytes(self.raw_bytes[2:])) + + +async def main(identifier: str | None) -> None: + client: BleakClient + responses_by_uuid = GoProUuid.dict_by_uuid(ProtobufResponse) + received_responses: asyncio.Queue[ProtobufResponse] = asyncio.Queue() + + request_uuid = GoProUuid.COMMAND_REQ_UUID + response_uuid = GoProUuid.COMMAND_RSP_UUID + + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f"Received response at UUID {uuid}: {data.hex(':')}") + + response = responses_by_uuid[uuid] + response.accumulate(data) + + # Notify the writer if we have received the entire response + if response.is_received: + # The turbo mode response will come on the Command Response characteristic + if uuid is response_uuid: + logger.info("Set Turbo Mode response complete received.") + # Notify writer that the procedure is complete + await received_responses.put(response) + # Anything else is unexpected. This shouldn't happen + else: + logger.error("Unexpected response") + # Reset the per-uuid Response + responses_by_uuid[uuid] = ProtobufResponse(uuid) + + client = await connect_ble(notification_handler, identifier) + + logger.info("Setting Turbo Mode off.") + + # Build raw bytes request from feature / action IDs and serialized protobuf message + turbo_mode_request = bytearray( + [ + 0xF1, # Feature ID + 0x6B, # Action ID + *proto.RequestSetTurboActive(active=False).SerializeToString(), + ] + ) + turbo_mode_request.insert(0, len(turbo_mode_request)) + + # Write to command request UUID to enable turbo mode + logger.info(f"Writing {turbo_mode_request.hex(':')} to {request_uuid}") + await client.write_gatt_char(request_uuid.value, turbo_mode_request, response=True) + + # Wait to receive the response, then parse it + response = await received_responses.get() + response.parse(proto.ResponseGeneric) + # Deserialize into protobuf message + assert response.feature_id == 0xF1 + assert response.action_id == 0xEB + logger.info("Successfully set turbo mode") + logger.info(response.data) + + await client.disconnect() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Connect to a GoPro camera, send Set Turbo Mode and parse the response" + ) + parser.add_argument( + "-i", + "--identifier", + type=str, + help="Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to", + default=None, + ) + args = parser.parse_args() + + try: + asyncio.run(main(args.identifier)) + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(e) + sys.exit(-1) + else: + sys.exit(0) diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/__init__.py new file mode 100644 index 00000000..e3af0028 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/__init__.py @@ -0,0 +1,2 @@ +# __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Thu Apr 4 21:50:02 UTC 2024 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/connect_as_sta.py b/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/connect_as_sta.py new file mode 100644 index 00000000..be97f628 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/connect_as_sta.py @@ -0,0 +1,258 @@ +# connect_sta.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +import sys +import asyncio +import argparse +from typing import Generator, Final + +from bleak import BleakClient +from tutorial_modules import GoProUuid, connect_ble, proto, logger, ResponseManager + + +def yield_fragmented_packets(payload: bytes) -> Generator[bytes, None, None]: + """Generate fragmented packets from a monolithic payload to accommodate the max BLE packet size of 20 bytes. + + Args: + payload (bytes): input payload to fragment + + Raises: + ValueError: Input payload is too large. + + Yields: + Generator[bytes, None, None]: fragmented packets. + """ + length = len(payload) + + CONTINUATION_HEADER: Final = bytearray([0x80]) + MAX_PACKET_SIZE: Final = 20 + is_first_packet = True + + # Build initial length header + if length < (2**5 - 1): + header = bytearray([length]) + elif length < (2**13 - 1): + header = bytearray((length | 0x2000).to_bytes(2, "big", signed=False)) + elif length < (2**16 - 1): + header = bytearray((length | 0x6400).to_bytes(2, "big", signed=False)) + else: + raise ValueError(f"Data length {length} is too big for this protocol.") + + byte_index = 0 + while bytes_remaining := length - byte_index: + # If this is the first packet, use the appropriate header. Else use the continuation header + if is_first_packet: + packet = bytearray(header) + is_first_packet = False + else: + packet = bytearray(CONTINUATION_HEADER) + # Build the current packet + packet_size = min(MAX_PACKET_SIZE - len(packet), bytes_remaining) + packet.extend(bytearray(payload[byte_index : byte_index + packet_size])) + yield bytes(packet) + # Increment byte_index for continued processing + byte_index += packet_size + + +async def fragment_and_write_gatt_char(client: BleakClient, char_specifier: str, data: bytes) -> None: + """Fragment the data into BLE packets and send each packet via GATT write. + + Args: + client (BleakClient): Bleak client to perform GATT Writes with + char_specifier (str): BLE characteristic to write to + data (bytes): data to fragment and write. + """ + for packet in yield_fragmented_packets(data): + await client.write_gatt_char(char_specifier, packet, response=True) + + +async def scan_for_networks(manager: ResponseManager) -> int: + """Scan for WiFi networks + + Args: + manager (ResponseManager): manager used to perform the operation + + Raises: + RuntimeError: Received unexpected response. + + Returns: + int: Scan ID to use to retrieve scan results + """ + logger.info(msg="Scanning for available Wifi Networks") + + start_scan_request = bytearray( + [ + 0x02, # Feature ID + 0x02, # Action ID + *proto.RequestStartScan().SerializePartialToString(), + ] + ) + start_scan_request.insert(0, len(start_scan_request)) + + # Send the scan request + logger.debug(f"Writing: {start_scan_request.hex(':')}") + await manager.client.write_gatt_char(GoProUuid.NETWORK_MANAGEMENT_REQ_UUID.value, start_scan_request, response=True) + while response := await manager.get_next_response_as_protobuf(): + if response.feature_id != 0x02: + raise RuntimeError("Only expect to receive Feature ID 0x02 responses after scan request") + if response.action_id == 0x82: # Initial Scan Response + manager.assert_generic_protobuf_success(response.data) + elif response.action_id == 0x0B: # Scan Notifications + scan_notification: proto.NotifStartScanning = response.data # type: ignore + logger.info(f"Received scan notification: {scan_notification}") + if scan_notification.scanning_state == proto.EnumScanning.SCANNING_SUCCESS: + return scan_notification.scan_id + else: + raise RuntimeError("Only expect to receive Action ID 0x02 or 0x0B responses after scan request") + raise RuntimeError("Loop should not exit without return") + + +async def get_scan_results(manager: ResponseManager, scan_id: int) -> list[proto.ResponseGetApEntries.ScanEntry]: + """Retrieve the results from a completed Wifi Network scan + + Args: + manager (ResponseManager): manager used to perform the operation + scan_id (int): identifier returned from completed scan + + Raises: + RuntimeError: Received unexpected response. + + Returns: + list[proto.ResponseGetApEntries.ScanEntry]: list of scan entries + """ + logger.info("Getting the scanned networks.") + + results_request = bytearray( + [ + 0x02, # Feature ID + 0x03, # Action ID + *proto.RequestGetApEntries(start_index=0, max_entries=100, scan_id=scan_id).SerializePartialToString(), + ] + ) + results_request.insert(0, len(results_request)) + + # Send the request + logger.debug(f"Writing: {results_request.hex(':')}") + await manager.client.write_gatt_char(GoProUuid.NETWORK_MANAGEMENT_REQ_UUID.value, results_request, response=True) + while response := await manager.get_next_response_as_protobuf(): + if response.feature_id != 0x02 or response.action_id != 0x83: + raise RuntimeError("Only expect to receive Feature ID 0x02 Action ID 0x83 responses after scan request") + entries_response: proto.ResponseGetApEntries = response.data # type: ignore + manager.assert_generic_protobuf_success(entries_response) + logger.info("Found the following networks:") + for entry in entries_response.entries: + logger.info(str(entry)) + return list(entries_response.entries) + raise RuntimeError("Loop should not exit without return") + + +async def connect_to_network( + manager: ResponseManager, entry: proto.ResponseGetApEntries.ScanEntry, password: str +) -> None: + """Connect to a WiFi network + + Args: + manager (ResponseManager): manager used to perform the operation + entry (proto.ResponseGetApEntries.ScanEntry): scan entry that contains network (and its metadata) to connect to + password (str): password corresponding to network from `entry` + + Raises: + RuntimeError: Received unexpected response. + """ + logger.info(f"Connecting to {entry.ssid}") + + if entry.scan_entry_flags & proto.EnumScanEntryFlags.SCAN_FLAG_CONFIGURED: + connect_request = bytearray( + [ + 0x02, # Feature ID + 0x04, # Action ID + *proto.RequestConnect(ssid=entry.ssid).SerializePartialToString(), + ] + ) + else: + connect_request = bytearray( + [ + 0x02, # Feature ID + 0x05, # Action ID + *proto.RequestConnectNew(ssid=entry.ssid, password=password).SerializePartialToString(), + ] + ) + + # Send the request + logger.debug(f"Writing: {connect_request.hex(':')}") + await fragment_and_write_gatt_char(manager.client, GoProUuid.NETWORK_MANAGEMENT_REQ_UUID.value, connect_request) + while response := await manager.get_next_response_as_protobuf(): + if response.feature_id != 0x02: + raise RuntimeError("Only expect to receive Feature ID 0x02 responses after connect request") + if response.action_id == 0x84: # RequestConnect Response + manager.assert_generic_protobuf_success(response.data) + elif response.action_id == 0x85: # RequestConnectNew Response + manager.assert_generic_protobuf_success(response.data) + elif response.action_id == 0x0C: # NotifProvisioningState Notifications + provisioning_notification: proto.NotifProvisioningState = response.data # type: ignore + logger.info(f"Received network provisioning status: {provisioning_notification}") + if provisioning_notification.provisioning_state == proto.EnumProvisioning.PROVISIONING_SUCCESS_NEW_AP: + return + if provisioning_notification.provisioning_state != proto.EnumProvisioning.PROVISIONING_STARTED: + raise RuntimeError(f"Unexpected provisioning state: {provisioning_notification.provisioning_state}") + else: + raise RuntimeError("Only expect to receive Action ID 0x84, 0x85, or 0x0C responses after scan request") + raise RuntimeError("Loop should not exit without return") + + +async def connect_to_access_point(manager: ResponseManager, ssid: str, password: str) -> None: + """Top level method to connect to an access point. + + Args: + manager (ResponseManager): manager used to perform the operation + ssid (str): SSID of WiFi network to connect to + password (str): password of WiFi network to connect to + + Raises: + RuntimeError: Received unexpected response. + """ + entries = await get_scan_results(manager, await scan_for_networks(manager)) + try: + entry = [entry for entry in entries if entry.ssid == ssid][0] + except IndexError as exc: + raise RuntimeError(f"Did not find {ssid}") from exc + + await connect_to_network(manager, entry, password) + logger.info(f"Successfully connected to {ssid}") + + +async def main(ssid: str, password: str, identifier: str | None) -> None: + manager = ResponseManager() + try: + client = await connect_ble(manager.notification_handler, identifier) + manager.set_client(client) + await connect_to_access_point(manager, ssid, password) + except Exception as exc: # pylint: disable=broad-exception-caught + logger.error(repr(exc)) + finally: + if manager.is_initialized: + await manager.client.disconnect() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Connect the GoPro to a Wifi network where the GoPro is in Station Mode (STA)." + ) + parser.add_argument("ssid", type=str, help="SSID of network to connect to") + parser.add_argument("password", type=str, help="Password of network to connect to") + parser.add_argument( + "-i", + "--identifier", + type=str, + help="Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to", + default=None, + ) + args = parser.parse_args() + + try: + asyncio.run(main(args.ssid, args.password, args.identifier)) + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(e) + sys.exit(-1) + else: + sys.exit(0) diff --git a/demos/python/tutorial/tutorial_modules/tutorial_5_connect_wifi/wifi_enable.py b/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/enable_wifi_ap.py similarity index 63% rename from demos/python/tutorial/tutorial_modules/tutorial_5_connect_wifi/wifi_enable.py rename to demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/enable_wifi_ap.py index 16207f26..75a32ba4 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_5_connect_wifi/wifi_enable.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/enable_wifi_ap.py @@ -2,18 +2,16 @@ # This copyright was auto-generated on Wed, Sep 1, 2021 5:06:01 PM import sys -import time import asyncio import argparse -from typing import Tuple, Optional from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from tutorial_modules import GOPRO_BASE_UUID, connect_ble, logger +from tutorial_modules import GoProUuid, connect_ble, logger -async def enable_wifi(identifier: Optional[str] = None) -> Tuple[str, str, BleakClient]: +async def enable_wifi(identifier: str | None = None) -> tuple[str, str, BleakClient]: """Connect to a GoPro via BLE, find its WiFi AP SSID and password, and enable its WiFI AP If identifier is None, the first discovered GoPro will be connected to. @@ -26,20 +24,14 @@ async def enable_wifi(identifier: Optional[str] = None) -> Tuple[str, str, Bleak """ # Synchronization event to wait until notification response is received event = asyncio.Event() - - # UUIDs to write to and receive responses from, and read from - COMMAND_REQ_UUID = GOPRO_BASE_UUID.format("0072") - COMMAND_RSP_UUID = GOPRO_BASE_UUID.format("0073") - WIFI_AP_SSID_UUID = GOPRO_BASE_UUID.format("0002") - WIFI_AP_PASSWORD_UUID = GOPRO_BASE_UUID.format("0003") - client: BleakClient - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') + async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + logger.info(f'Received response at {uuid}: {data.hex(":")}') # If this is the correct handle and the status is success, the command was a success - if client.services.characteristics[characteristic.handle].uuid == COMMAND_RSP_UUID and data[2] == 0x00: + if uuid is GoProUuid.COMMAND_RSP_UUID and data[2] == 0x00: logger.info("Command sent successfully") # Anything else is unexpected. This shouldn't happen else: @@ -51,42 +43,46 @@ def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) - client = await connect_ble(notification_handler, identifier) # Read from WiFi AP SSID BleUUID - logger.info("Reading the WiFi AP SSID") - ssid = (await client.read_gatt_char(WIFI_AP_SSID_UUID)).decode() + ssid_uuid = GoProUuid.WIFI_AP_SSID_UUID + logger.info(f"Reading the WiFi AP SSID at {ssid_uuid}") + ssid = (await client.read_gatt_char(ssid_uuid.value)).decode() logger.info(f"SSID is {ssid}") # Read from WiFi AP Password BleUUID - logger.info("Reading the WiFi AP password") - password = (await client.read_gatt_char(WIFI_AP_PASSWORD_UUID)).decode() + password_uuid = GoProUuid.WIFI_AP_PASSWORD_UUID + logger.info(f"Reading the WiFi AP password at {password_uuid}") + password = (await client.read_gatt_char(password_uuid.value)).decode() logger.info(f"Password is {password}") # Write to the Command Request BleUUID to enable WiFi logger.info("Enabling the WiFi AP") event.clear() - await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x03, 0x17, 0x01, 0x01]), response=True) + request = bytes([0x03, 0x17, 0x01, 0x01]) + command_request_uuid = GoProUuid.COMMAND_REQ_UUID + logger.debug(f"Writing to {command_request_uuid}: {request.hex(':')}") + await client.write_gatt_char(command_request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response logger.info("WiFi AP is enabled") return ssid, password, client -async def main(identifier: Optional[str], timeout: Optional[int]) -> None: +async def main(identifier: str | None, timeout: int | None) -> None: *_, client = await enable_wifi(identifier) - if not timeout: - logger.info("Maintaining BLE Connection indefinitely. Send keyboard interrupt to exit.") - while True: - time.sleep(1) - else: + if timeout: logger.info(f"Maintaining BLE connection for {timeout} seconds") - time.sleep(timeout) + await asyncio.sleep(timeout) + else: + input("Maintaining BLE Connection indefinitely. Press enter to exit.") + logger.info("Disconnect from BLE...") await client.disconnect() if __name__ == "__main__": parser = argparse.ArgumentParser( - description="Connect to a GoPro camera via BLE, get WiFi info, and enable WiFi." + description="Connect to a GoPro camera via BLE, get its WiFi Access Point (AP) info, and enable its AP." ) parser.add_argument( "-i", diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/__init__.py similarity index 100% rename from demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/__init__.py rename to demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/__init__.py diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_get_media_list.py b/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_get_media_list.py similarity index 100% rename from demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_get_media_list.py rename to demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_get_media_list.py diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_get_state.py b/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_get_state.py similarity index 100% rename from demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_get_state.py rename to demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_get_state.py diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_load_group.py b/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_load_group.py similarity index 100% rename from demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_load_group.py rename to demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_load_group.py diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_preview_stream.py b/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_preview_stream.py similarity index 100% rename from demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_preview_stream.py rename to demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_preview_stream.py diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_set_resolution.py b/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_set_resolution.py similarity index 97% rename from demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_set_resolution.py rename to demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_set_resolution.py index 2888711b..960513c9 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_set_resolution.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_set_resolution.py @@ -13,8 +13,6 @@ def main() -> None: # Note!! The endpoint below changed between Open GoPro version 1.0 and 2.0 # This endpoint supports >= 2.0 - - # Build the HTTP GET request url = GOPRO_BASE_URL + "/gopro/camera/setting?setting=2&option=9" logger.info(f"Setting the video resolution to 1080: sending {url}") diff --git a/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_set_shutter.py b/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_set_shutter.py similarity index 100% rename from demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/wifi_command_set_shutter.py rename to demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/wifi_command_set_shutter.py diff --git a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/__init__.py similarity index 100% rename from demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/__init__.py rename to demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/__init__.py diff --git a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_download_file.py b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_download_file.py similarity index 69% rename from demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_download_file.py rename to demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_download_file.py index 27c35333..5ebb857f 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_download_file.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_download_file.py @@ -3,7 +3,6 @@ import sys import argparse -from typing import Optional import requests @@ -15,19 +14,28 @@ def main() -> None: media_list = get_media_list() # Find a photo. We're just taking the first one we find. - photo: Optional[str] = None - for media_file in [x["n"] for x in media_list["media"][0]["fs"]]: - if media_file.lower().endswith(".jpg"): - logger.info(f"found a photo: {media_file}") - photo = media_file + photo: str | None = None + directory: str | None = None + found_photo = False + # TODO update tutorial docs to get directory + for media in media_list["media"]: + for media_file in [x["n"] for x in media["fs"]]: + if media_file.lower().endswith(".jpg"): + logger.info(f"found a photo: {media_file}") + photo = media_file + directory = media["d"] + found_photo = True + break + if found_photo: break else: raise RuntimeError("Couldn't find a photo on the GoPro") - assert photo is not None + assert photo + assert directory # Build the url to get the thumbnail data for the photo logger.info(f"Downloading {photo}") - url = GOPRO_BASE_URL + f"/videos/DCIM/100GOPRO/{photo}" + url = GOPRO_BASE_URL + f"/videos/DCIM/{directory}/{photo}" logger.info(f"Sending: {url}") with requests.get(url, stream=True, timeout=10) as request: request.raise_for_status() diff --git a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_gpmf.py b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_gpmf.py similarity index 68% rename from demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_gpmf.py rename to demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_gpmf.py index bbb25f94..0dc289d7 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_gpmf.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_gpmf.py @@ -3,7 +3,6 @@ import sys import argparse -from typing import Optional import requests @@ -15,19 +14,28 @@ def main() -> None: media_list = get_media_list() # Find a photo. We're just taking the first one we find. - photo: Optional[str] = None - for media_file in [x["n"] for x in media_list["media"][0]["fs"]]: - if media_file.lower().endswith(".jpg"): - logger.info(f"found a photo: {media_file}") - photo = media_file + photo: str | None = None + directory: str | None = None + found_photo = False + # TODO update tutorial docs to get directory + for media in media_list["media"]: + for media_file in [x["n"] for x in media["fs"]]: + if media_file.lower().endswith(".jpg"): + logger.info(f"found a photo: {media_file}") + photo = media_file + directory = media["d"] + found_photo = True + break + if found_photo: break else: raise RuntimeError("Couldn't find a photo on the GoPro") - assert photo is not None + assert photo + assert directory # Build the url to get the GPMF data for the photo logger.info(f"Getting the GPMF for {photo}") - url = GOPRO_BASE_URL + f"/gopro/media/gpmf?path=100GOPRO/{photo}" + url = GOPRO_BASE_URL + f"/gopro/media/gpmf?path={directory}/{photo}" logger.info(f"Sending: {url}") with requests.get(url, stream=True, timeout=10) as request: request.raise_for_status() diff --git a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_screennail.py b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_screennail.py similarity index 68% rename from demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_screennail.py rename to demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_screennail.py index b9146dc4..f4338833 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_screennail.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_screennail.py @@ -3,7 +3,6 @@ import sys import argparse -from typing import Optional import requests @@ -15,19 +14,28 @@ def main() -> None: media_list = get_media_list() # Find a photo. We're just taking the first one we find. - photo: Optional[str] = None - for media_file in [x["n"] for x in media_list["media"][0]["fs"]]: - if media_file.lower().endswith(".jpg"): - logger.info(f"found a photo: {media_file}") - photo = media_file + photo: str | None = None + directory: str | None = None + found_photo = False + # TODO update tutorial docs to get directory + for media in media_list["media"]: + for media_file in [x["n"] for x in media["fs"]]: + if media_file.lower().endswith(".jpg"): + logger.info(f"found a photo: {media_file}") + photo = media_file + directory = media["d"] + found_photo = True + break + if found_photo: break else: raise RuntimeError("Couldn't find a photo on the GoPro") - assert photo is not None + assert photo + assert directory # Build the url to get the screennail data for the photo logger.info(f"Getting the screennail for {photo}") - url = GOPRO_BASE_URL + f"/gopro/media/screennail?path=100GOPRO/{photo}" + url = GOPRO_BASE_URL + f"/gopro/media/screennail?path={directory}/{photo}" logger.info(f"Sending: {url}") with requests.get(url, stream=True, timeout=10) as request: request.raise_for_status() diff --git a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_thumbnail.py b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_thumbnail.py similarity index 68% rename from demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_thumbnail.py rename to demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_thumbnail.py index 2c26c31b..89759beb 100644 --- a/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/wifi_media_get_thumbnail.py +++ b/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/wifi_media_get_thumbnail.py @@ -3,7 +3,6 @@ import sys import argparse -from typing import Optional import requests @@ -15,19 +14,28 @@ def main() -> None: media_list = get_media_list() # Find a photo. We're just taking the first one we find. - photo: Optional[str] = None - for media_file in [x["n"] for x in media_list["media"][0]["fs"]]: - if media_file.lower().endswith(".jpg"): - logger.info(f"found a photo: {media_file}") - photo = media_file + photo: str | None = None + directory: str | None = None + found_photo = False + # TODO update tutorial docs to get directory + for media in media_list["media"]: + for media_file in [x["n"] for x in media["fs"]]: + if media_file.lower().endswith(".jpg"): + logger.info(f"found a photo: {media_file}") + photo = media_file + directory = media["d"] + found_photo = True + break + if found_photo: break else: raise RuntimeError("Couldn't find a photo on the GoPro") - assert photo is not None + assert photo + assert directory # Build the url to get the thumbnail data for the photo logger.info(f"Getting the thumbnail for {photo}") - url = GOPRO_BASE_URL + f"/gopro/media/thumbnail?path=100GOPRO/{photo}" + url = GOPRO_BASE_URL + f"/gopro/media/thumbnail?path={directory}/{photo}" logger.info(f"Sending: {url}") with requests.get(url, stream=True, timeout=10) as request: request.raise_for_status() diff --git a/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/__init__.py b/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/__init__.py new file mode 100644 index 00000000..e3af0028 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/__init__.py @@ -0,0 +1,2 @@ +# __init__.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Thu Apr 4 21:50:02 UTC 2024 diff --git a/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/communicate_via_cohn.py b/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/communicate_via_cohn.py new file mode 100644 index 00000000..802c26d6 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/communicate_via_cohn.py @@ -0,0 +1,48 @@ +# communicate_via_cohn.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +import sys +import json +import argparse +import asyncio +from base64 import b64encode +from pathlib import Path + +import requests + +from tutorial_modules import logger + + +async def main(ip_address: str, username: str, password: str, certificate: Path) -> None: + url = f"https://{ip_address}" + "/gopro/camera/state" + logger.debug(f"Sending: {url}") + + token = b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") + response = requests.get( + url, + timeout=10, + headers={"Authorization": f"Basic {token}"}, + verify=str(certificate), + ) + # Check for errors (if an error is found, an exception will be raised) + response.raise_for_status() + logger.info("Command sent successfully") + # Log response as json + logger.info(f"Response: {json.dumps(response.json(), indent=4)}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Demonstrate HTTPS communication via COHN.") + parser.add_argument("ip_address", type=str, help="IP Address of camera on the home network") + parser.add_argument("username", type=str, help="COHN username") + parser.add_argument("password", type=str, help="COHN password") + parser.add_argument("certificate", type=Path, help="Path to read COHN cert from.", default=Path("cohn.crt")) + args = parser.parse_args() + + try: + asyncio.run(main(args.ip_address, args.username, args.password, args.certificate)) + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(e) + sys.exit(-1) + else: + sys.exit(0) diff --git a/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/provision_cohn.py b/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/provision_cohn.py new file mode 100644 index 00000000..13e56ce0 --- /dev/null +++ b/demos/python/tutorial/tutorial_modules/tutorial_9_cohn/provision_cohn.py @@ -0,0 +1,292 @@ +# provision_cohn.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). +# This copyright was auto-generated on Wed Mar 27 22:05:49 UTC 2024 + +import sys +import json +import asyncio +import argparse +from pathlib import Path +from dataclasses import dataclass, asdict +from datetime import datetime + +import pytz +from tzlocal import get_localzone + +from tutorial_modules import GoProUuid, connect_ble, proto, connect_to_access_point, ResponseManager, logger + + +async def set_date_time(manager: ResponseManager) -> None: + """Get and then set the camera's date, time, timezone, and daylight savings time status + + Args: + manager (ResponseManager): manager used to perform the operation + """ + # First find the current time, timezone and is_dst + tz = pytz.timezone(get_localzone().key) + 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 + if is_dst: + offset += 60 # Handle daylight savings time + offset = int(offset) + logger.info(f"Setting the camera's date and time to {now}:{offset} {is_dst=}") + + # Build the request bytes + datetime_request = bytearray( + [ + 0x0F, # Command ID + 10, # Length of following datetime parameter + *now.year.to_bytes(2, "big", signed=False), # uint16 year + now.month, + now.day, + now.hour, + now.minute, + now.second, + *offset.to_bytes(2, "big", signed=True), # int16 offset in minutes + is_dst, + ] + ) + datetime_request.insert(0, len(datetime_request)) + + # Send the request + logger.debug(f"Writing: {datetime_request.hex(':')}") + await manager.client.write_gatt_char(GoProUuid.COMMAND_REQ_UUID.value, datetime_request, response=True) + response = await manager.get_next_response_as_tlv() + assert response.id == 0x0F + assert response.status == 0x00 + logger.info("Successfully set the date time.") + + +async def clear_certificate(manager: ResponseManager) -> None: + """Clear the camera's COHN certificate. + + Args: + manager (ResponseManager): manager used to perform the operation + + Raises: + RuntimeError: Received unexpected response + """ + logger.info("Clearing any preexisting COHN certificate.") + + clear_request = bytearray( + [ + 0xF1, # Feature ID + 0x66, # Action ID + *proto.RequestClearCOHNCert().SerializePartialToString(), + ] + ) + clear_request.insert(0, len(clear_request)) + + # Send the request + logger.debug(f"Writing: {clear_request.hex(':')}") + await manager.client.write_gatt_char(GoProUuid.COMMAND_REQ_UUID.value, clear_request, response=True) + while response := await manager.get_next_response_as_protobuf(): + if response.feature_id != 0xF1 or response.action_id != 0xE6: + raise RuntimeError( + "Only expect to receive Feature ID 0xF1 Action ID 0xE6 responses after clear cert request" + ) + manager.assert_generic_protobuf_success(response.data) + logger.info("COHN certificate successfully cleared") + return + raise RuntimeError("Loop should not exit without return") + + +async def create_certificate(manager: ResponseManager) -> None: + """Instruct the camera to create the COHN certificate. + + Args: + manager (ResponseManager): manager used to perform the operation + + Raises: + RuntimeError: Received unexpected response + """ + logger.info("Creating a new COHN certificate.") + + create_request = bytearray( + [ + 0xF1, # Feature ID + 0x67, # Action ID + *proto.RequestCreateCOHNCert().SerializePartialToString(), + ] + ) + create_request.insert(0, len(create_request)) + + # Send the request + logger.debug(f"Writing: {create_request.hex(':')}") + await manager.client.write_gatt_char(GoProUuid.COMMAND_REQ_UUID.value, create_request, response=True) + while response := await manager.get_next_response_as_protobuf(): + if response.feature_id != 0xF1 or response.action_id != 0xE7: + raise RuntimeError( + "Only expect to receive Feature ID 0xF1 Action ID 0xE7 responses after create cert request" + ) + manager.assert_generic_protobuf_success(response.data) + logger.info("COHN certificate successfully created") + return + raise RuntimeError("Loop should not exit without return") + + +@dataclass(frozen=True) +class Credentials: + """COHN credentials.""" + + certificate: str + username: str + password: str + ip_address: str + + def __str__(self) -> str: + return json.dumps(asdict(self), indent=4) + + +async def get_cohn_certificate(manager: ResponseManager) -> str: + """Get the camera's COHN certificate + + Args: + manager (ResponseManager): manager used to perform the operation + + Raises: + RuntimeError: Received unexpected response + + Returns: + str: certificate in string form. + """ + logger.info("Getting the current COHN certificate.") + + cert_request = bytearray( + [ + 0xF5, # Feature ID + 0x6E, # Action ID + *proto.RequestCOHNCert().SerializePartialToString(), + ] + ) + cert_request.insert(0, len(cert_request)) + + # Send the request + logger.debug(f"Writing: {cert_request.hex(':')}") + await manager.client.write_gatt_char(GoProUuid.QUERY_REQ_UUID.value, cert_request, response=True) + while response := await manager.get_next_response_as_protobuf(): + if response.feature_id != 0xF5 or response.action_id != 0xEE: + raise RuntimeError("Only expect to receive Feature ID 0xF5 Action ID 0xEE responses after get cert request") + cert_response: proto.ResponseCOHNCert = response.data # type: ignore + manager.assert_generic_protobuf_success(cert_response) + logger.info("COHN certificate successfully retrieved") + return cert_response.cert + raise RuntimeError("Loop should not exit without return") + + +async def get_cohn_status(manager: ResponseManager) -> proto.NotifyCOHNStatus: + """Get the COHN status until it is provisioned and connected. + + Args: + manager (ResponseManager): manager used to perform the operation + + Raises: + RuntimeError: Received unexpected response + + Returns: + proto.NotifyCOHNStatus: Connected COHN status that includes the credentials. + """ + logger.info("Checking COHN status until provisioning is complete") + + status_request = bytearray( + [ + 0xF5, # Feature ID + 0x6F, # Action ID + *proto.RequestGetCOHNStatus(register_cohn_status=True).SerializePartialToString(), + ] + ) + status_request.insert(0, len(status_request)) + + # Send the scan request + logger.debug(f"Writing: {status_request.hex(':')}") + await manager.client.write_gatt_char(GoProUuid.QUERY_REQ_UUID.value, status_request, response=True) + while response := await manager.get_next_response_as_protobuf(): + if response.feature_id != 0xF5 or response.action_id != 0xEF: + raise RuntimeError( + "Only expect to receive Feature ID 0xF5, Action ID 0xEF responses after COHN status request" + ) + cohn_status: proto.NotifyCOHNStatus = response.data # type: ignore + logger.info(f"Received COHN Status: {cohn_status}") + if cohn_status.state == proto.EnumCOHNNetworkState.COHN_STATE_NetworkConnected: + return cohn_status + raise RuntimeError("Loop should not exit without return") + + +async def provision_cohn(manager: ResponseManager) -> Credentials: + """Helper method to provision COHN. + + Args: + manager (ResponseManager): manager used to perform the operation + + Returns: + Credentials: COHN credentials to use for future COHN communication. + """ + logger.info("Provisioning COHN") + await clear_certificate(manager) + await create_certificate(manager) + certificate = await get_cohn_certificate(manager) + # Wait for COHN to be provisioned and get the provisioned status + status = await get_cohn_status(manager) + logger.info("Successfully provisioned COHN.") + credentials = Credentials( + certificate=certificate, + username=status.username, + password=status.password, + ip_address=status.ipaddress, + ) + logger.info(credentials) + return credentials + + +async def main(ssid: str, password: str, identifier: str | None, certificate: Path) -> Credentials | None: + manager = ResponseManager() + credentials: Credentials | None = None + try: + client = await connect_ble(manager.notification_handler, identifier) + manager.set_client(client) + await set_date_time(manager) + await connect_to_access_point(manager, ssid, password) + credentials = await provision_cohn(manager) + with open(certificate, "w") as fp: + fp.write(credentials.certificate) + logger.info(f"Certificate written to {certificate.resolve()}") + + except Exception as exc: # pylint: disable=broad-exception-caught + logger.error(repr(exc)) + finally: + if manager.is_initialized: + await manager.client.disconnect() + return credentials + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Provision COHN via BLE to be ready for communication.") + parser.add_argument("ssid", type=str, help="SSID of network to connect to") + parser.add_argument("password", type=str, help="Password of network to connect to") + parser.add_argument( + "-i", + "--identifier", + type=str, + help="Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to", + default=None, + ) + parser.add_argument( + "-c", + "--certificate", + type=Path, + help="Path to write retrieved COHN certificate.", + default=Path("cohn.crt"), + ) + args = parser.parse_args() + + try: + asyncio.run(main(args.ssid, args.password, args.identifier, args.certificate)) + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(e) + sys.exit(-1) + else: + sys.exit(0) diff --git a/docker-compose.yml b/docker-compose.yml index 90674770..1ab800d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,10 +27,10 @@ services: networks: - open_gopro depends_on: - - plant_uml + - plant-uml - plant_uml: - container_name: plant_uml + plant-uml: + container_name: plant-uml # Note! v1.2023.7 seems to be broken image: plantuml/plantuml-server:jetty-v1.2023.6 ports: @@ -39,6 +39,16 @@ services: networks: - open_gopro + proto-build: + build: + context: .admin/proto_build + container_name: proto-build + profiles: + - ephemeral + volumes: + - ./protobuf:/proto_in + - ./.build/protobuf/python:/proto_python_out + linkchecker: image: ghcr.io/gopro/opengopro/gplinkchecker:main container_name: linkchecker diff --git a/docs/_config.yml b/docs/_config.yml index 0eb1fbf0..203625f2 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -222,7 +222,7 @@ jekyll-spaceship: syntax: code: 'plantuml!' custom: ['@startuml', '@enduml'] - src: http://plant_uml:8080/svg/ # TODO make this run-time configurable + src: http://plant-uml:8080/svg/ # TODO make this run-time configurable mermaid-processor: mode: pre-fetch # fetch image at build-time css: diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml index 5ac4b82e..8b4bdd71 100644 --- a/docs/_data/navigation.yml +++ b/docs/_data/navigation.yml @@ -24,11 +24,15 @@ tutorials: url: /tutorials/parse-ble-responses - title: '4: BLE Queries' url: /tutorials/ble-queries - - title: '5: Connect WiFi' + - title: '5: BLE Protobuf' + url: /tutorials/ble-protobuf + - title: '6: Connect WiFi' url: /tutorials/connect-wifi - - title: '6: Send WiFi Commands' + - title: '7: Send WiFi Commands' url: /tutorials/send-wifi-commands - - title: '7: Camera Media List' + - title: '8: Camera Media List' url: /tutorials/camera-media-list + - title: '9: Camera on the Home Network' + url: /tutorials/cohn diff --git a/docs/_tutorials/tutorial_1_connect_ble/tutorial.md b/docs/_tutorials/tutorial_1_connect_ble/tutorial.md index 7f92486d..c9d4090f 100644 --- a/docs/_tutorials/tutorial_1_connect_ble/tutorial.md +++ b/docs/_tutorials/tutorial_1_connect_ble/tutorial.md @@ -13,17 +13,21 @@ This tutorial will provide a walk-through to connect to the GoPro camera via Blu ## Hardware -* A GoPro camera that is [supported by Open GoPro](/ble/index.html#supported-cameras) +- A GoPro camera that is [supported by Open GoPro]({{site.baseurl}}/ble/index.html#supported-cameras) + {% linkedTabs hardware_prereqs %} {% tab hardware_prereqs python %} -* One of the following systems: - - Windows 10, version 16299 (Fall Creators Update) or greater - - Linux distribution with [BlueZ](http://www.bluez.org/) >= 5.43 - - OS X/macOS support via Core Bluetooth API, from at least OS X version 10.11 +- One of the following systems: + - Windows 10, version 16299 (Fall Creators Update) or greater + - Linux distribution with [BlueZ](http://www.bluez.org/) >= 5.43 + - OS X/macOS support via Core Bluetooth API, from at least OS X version 10.11 + {% endtab %} {% tab hardware_prereqs kotlin %} -* An Android Device supporting SDK >= 33 + +- An Android Device supporting SDK >= 33 + {% endtab %} {% endlinkedTabs %} @@ -31,12 +35,13 @@ This tutorial will provide a walk-through to connect to the GoPro camera via Blu {% linkedTabs software_prereqs %} {% tab software_prereqs python %} -- Python >= 3.8.x must be installed. See this [Python installation guide](https://docs.python-guide.org/starting/installation/). -{% endtab %} -{% tab software_prereqs kotlin %} -- [Android Studio](https://developer.android.com/studio) >= 2022.1.1 (Electric Eel) -{% endtab %} -{% endlinkedTabs %} + +- Python >= 3.9 and < 3.12 must be installed. See this [Python installation guide](https://docs.python-guide.org/starting/installation/). + {% endtab %} + {% tab software_prereqs kotlin %} +- [Android Studio](https://developer.android.com/studio) >= 2022.1.1 (Electric Eel) + {% endtab %} + {% endlinkedTabs %} # Overview / Assumptions @@ -47,8 +52,7 @@ This tutorial will use [bleak](https://pypi.org/project/bleak/) to control the O {% warning %} The Bleak BLE controller does not currently support autonomous pairing for the BlueZ backend. So if you are using BlueZ (i.e. Ubuntu, RaspberryPi, etc.), you need to first pair the camera from the command line as shown in the -[BlueZ tutorial](https://gopro.github.io/OpenGoPro/tutorials/bash/bluez). There is work to add this feature -and progress can be tracked on the [Github Issue](https://github.com/hbldh/bleak/pull/1100). +[BlueZ tutorial](https://gopro.github.io/OpenGoPro/tutorials/bash/bluez). {% endwarning %} The bleak module is based on asyncio which means that its awaitable functions need to @@ -93,10 +97,10 @@ do not prioritize the following: These tutorials assume familiarity and a base level of competence with: -- Android Studio -- Bluetooth Low Energy -- JSON -- HTTP +- [Android Studio](https://developer.android.com/studio) +- [Bluetooth Low Energy](https://www.bluetooth.com/bluetooth-resources/intro-to-bluetooth-low-energy/) +- [JSON](https://www.w3schools.com/js/js_json_intro.asp) +- [HTTP](https://www.tutorialspoint.com/http/index.htm) {% endtab %} {% endlinkedTabs %} @@ -144,7 +148,7 @@ Required-by: {% tab setup kotlin %} This set of tutorials is accompanied by an Android Studio project consisting of, among other project infrastructure, Kotlin files separated by tutorial module. -The project can be found on [Github](https://github.com/gopro/OpenGoPro/tree/main/demos/kotlin/tutorial/). Once the +The project can be found on [Github](https://github.com/gopro/OpenGoPro/tree/main/demos/kotlin/tutorial/). Once the Github repo has been cloned or downloaded to your local machine, open the project in Android studio. At this point you should be able to build and load the project to your Android device. @@ -160,10 +164,10 @@ The project will not work on an emulated device since BLE can not be emulated. {% linkedTabs demo %} {% tab demo python %} Each of the scripts for this tutorial can be found in the Tutorial 1 -[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_1_connect_ble).. +[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_1_connect_ble). {% warning %} -Python >= 3.8.x must be used as specified in the requirements +Python >= 3.9 and < 3.12 must be used as specified in the requirements {% endwarning %} You can test connecting to your camera through BLE using the following script: @@ -261,9 +265,10 @@ We are keeping any devices that have a device name. # Scan callback to also catch nonconnectable scan responses def _scan_callback(device: BleakDevice, _: Any) -> None: # Add to the dict if not unknown - if device.name != "Unknown" and device.name is not None: + if device.name and device.name != "Unknown": devices[device.name] = device + # Now discover and add connectable advertisements for device in await BleakScanner.discover(timeout=5, detection_callback=_scan_callback): if device.name != "Unknown" and device.name is not None: @@ -280,11 +285,10 @@ steps accordingly. {% endwarning %} First, we define a regex which is either "GoPro " followed by any four alphanumeric characters if no identifier was passed, -or "GoPro " concatenated with the identifier if it exists. In the demo `ble_connect.py`, the identifier is taken -from the command-line arguments. +or the identifier if it exists. In the demo `ble_connect.py`, the identifier is taken from the command-line arguments. ```python -token = re.compile(r"GoPro [A-Z0-9]{4}" if identifier is None else f"GoPro {identifier}") +token = re.compile(identifier or r"GoPro [A-Z0-9]{4}") ``` Now we build a list of matched devices by checking if each device's name includes the token regex. @@ -324,8 +328,8 @@ your camera's serial number. {% endtab %} {% tab scan kotlin %} -First let's define a filter to find the GoPro. We do this by filtering on the GoPro Service UUID that is -included in all GoPro advertisements: +First let's define a filter that will be used to find GoPro device advertisements. We do this by filtering on the GoPro +Service UUID that is included in all GoPro advertisements: ```kotlin private val scanFilters = listOf( @@ -353,7 +357,7 @@ ble.startScan(scanFilters).onSuccess { scanResults -> } ``` -At this point, the GoPro's BLE address is stored (as a String) in `goproAddress`. +At this point, the GoPro's BLE address is stored (as a string) in `goproAddress`. Here is an example log output from this process: @@ -362,6 +366,7 @@ Scanning for GoPro's Received scan result: GoPro 0992 Found GoPro: GoPro 0992 ``` + {% endtab %} {% endlinkedTabs %} @@ -372,6 +377,7 @@ establish a BLE connection to the camera. {% linkedTabs connect %} {% tab connect python %} + ```python # We're just taking the first device if there are multiple. device = matched_devices[0] @@ -387,8 +393,10 @@ INFO:root:Establishing BLE connection to EF:5A:F6:13:E6:5A: GoPro 0456... INFO:bleak.backends.dotnet.client:Services resolved for BleakClientDotNet (EF:5A:F6:13:E6:5A) INFO:root:BLE Connected! ``` + {% endtab %} {% tab connect kotlin %} + ```kotlin ble.connect(goproAddress) ``` @@ -406,6 +414,7 @@ writing to them. Therefore now that we are connected, we need to attempt to pair {% linkedTabs pair %} {% tab pair python %} + ```python try: await client.pair() @@ -421,7 +430,7 @@ catching the exception when it fails. {% tab pair kotlin %} Rather than explicitly request pairing, we rely on the fact that Android will automatically start the pairing process if you try to read a characteristic that requires encryption. To do this, we read the -[Wifi AP Password characteristic](https://gopro.github.io/OpenGoPro/ble/protocol/ble_setup.html#ble-characteristics). +[Wifi AP Password characteristic](https://gopro.github.io/OpenGoPro/ble_2_0#services-and-characteristics). First we discover all characteristics (this will also be needed later when enabling notifications): @@ -429,6 +438,11 @@ First we discover all characteristics (this will also be needed later when enabl ble.discoverCharacteristics(goproAddress) ``` +{% note %} +This API will discover the characteristics over-the-air but not return them here. They are stored to the `ble` object +for later access via the `servicesOf` method. +{% endnote %} + Then we read the relevant characteristic to trigger pairing: ```kotlin @@ -450,50 +464,7 @@ Characteristics: |--00002a00-0000-1000-8000-00805f9b34fb: READABLE |--00002a01-0000-1000-8000-00805f9b34fb: READABLE |--00002a04-0000-1000-8000-00805f9b34fb: READABLE -Service 0000180f-0000-1000-8000-00805f9b34fb -Characteristics: -|--00002a19-0000-1000-8000-00805f9b34fb: READABLE, NOTIFIABLE -|------00002902-0000-1000-8000-00805f9b34fb: EMPTY -Service 0000180a-0000-1000-8000-00805f9b34fb -Characteristics: -|--00002a29-0000-1000-8000-00805f9b34fb: READABLE -|--00002a24-0000-1000-8000-00805f9b34fb: READABLE -|--00002a25-0000-1000-8000-00805f9b34fb: READABLE -|--00002a27-0000-1000-8000-00805f9b34fb: READABLE -|--00002a26-0000-1000-8000-00805f9b34fb: READABLE -|--00002a28-0000-1000-8000-00805f9b34fb: READABLE -|--00002a23-0000-1000-8000-00805f9b34fb: READABLE -|--00002a50-0000-1000-8000-00805f9b34fb: READABLE -Service b5f90001-aa8d-11e3-9046-0002a5d5c51b -Characteristics: -|--b5f90002-aa8d-11e3-9046-0002a5d5c51b: READABLE, WRITABLE -|--b5f90003-aa8d-11e3-9046-0002a5d5c51b: READABLE, WRITABLE -|--b5f90004-aa8d-11e3-9046-0002a5d5c51b: WRITABLE -|--b5f90005-aa8d-11e3-9046-0002a5d5c51b: READABLE, INDICATABLE -|------00002902-0000-1000-8000-00805f9b34fb: EMPTY -|--b5f90006-aa8d-11e3-9046-0002a5d5c51b: READABLE -Service 0000fea6-0000-1000-8000-00805f9b34fb -Characteristics: -|--b5f90072-aa8d-11e3-9046-0002a5d5c51b: WRITABLE -|--b5f90073-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE -|------00002902-0000-1000-8000-00805f9b34fb: EMPTY -|--b5f90074-aa8d-11e3-9046-0002a5d5c51b: WRITABLE -|--b5f90075-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE -|------00002902-0000-1000-8000-00805f9b34fb: EMPTY -|--b5f90076-aa8d-11e3-9046-0002a5d5c51b: WRITABLE -|--b5f90077-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE -|------00002902-0000-1000-8000-00805f9b34fb: EMPTY -|--b5f90078-aa8d-11e3-9046-0002a5d5c51b: WRITABLE -|--b5f90079-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE -|------00002902-0000-1000-8000-00805f9b34fb: EMPTY -Service b5f90090-aa8d-11e3-9046-0002a5d5c51b -Characteristics: -|--b5f90091-aa8d-11e3-9046-0002a5d5c51b: WRITABLE -|--b5f90092-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE -|------00002902-0000-1000-8000-00805f9b34fb: EMPTY -Service b5f90080-aa8d-11e3-9046-0002a5d5c51b -Characteristics: -|--b5f90081-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE +... |------00002902-0000-1000-8000-00805f9b34fb: EMPTY |--b5f90082-aa8d-11e3-9046-0002a5d5c51b: WRITABLE |--b5f90083-aa8d-11e3-9046-0002a5d5c51b: NOTIFIABLE @@ -506,6 +477,7 @@ Characteristics: Pairing Read characteristic b5f90003-aa8d-11e3-9046-0002a5d5c51b : value: 66:3F:54:2D:38:35:72:2D:4E:35:63 ``` + {% endtab %} {% endlinkedTabs %} @@ -518,8 +490,9 @@ re-establish encryption using stored keys. That is, they are "bonded." ## Enable Notifications -As specified in the [Open GoPro Bluetooth API](/ble/index.html#sending-and-receiving-messages), -we must enable notifications for a given characteristic to receive responses from it. +As specified in the Open GoPRo BLE Spec, we must +[enable notifications]({{site.baseurl}}/ble/protocol/ble_setup.html#configure-gatt-characteristics) for a given +characteristic to receive responses from it. To enable notifications, we loop over each characteristic in each service and enable the characteristic for notification if it has `notify` properties: @@ -554,9 +527,12 @@ INFO:root:Enabling notification on char b5f90081-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90083-aa8d-11e3-9046-0002a5d5c51b INFO:root:Enabling notification on char b5f90084-aa8d-11e3-9046-0002a5d5c51b INFO:root:Done enabling notifications +INFO:root:BLE Connection is ready for communication. ``` + {% endtab %} {% tab enable_notifications kotlin %} + ```kotlin ble.servicesOf(goproAddress).onSuccess { services -> services.forEach { service -> @@ -593,11 +569,12 @@ Enabling notifications for b5f90084-aa8d-11e3-9046-0002a5d5c51b Wrote to descriptor 00002902-0000-1000-8000-00805f9b34fb Bluetooth is ready for communication! ``` + {% endtab %} {% endlinkedTabs %} The characteristics that correspond to each UUID listed in the log can be found in the -[Open GoPro API](/ble/index.html#services-and-characteristics). These +[Open GoPro API]({{site.baseurl}}/ble/protocol/ble_setup.html#ble-characteristics). These will be used in a future tutorial to send data. Once the notifications are enabled, the GoPro BLE initialization is complete and it is ready to communicate via diff --git a/docs/_tutorials/tutorial_2_send_ble_commands/tutorial.md b/docs/_tutorials/tutorial_2_send_ble_commands/tutorial.md index b717a06e..e03d6064 100644 --- a/docs/_tutorials/tutorial_2_send_ble_commands/tutorial.md +++ b/docs/_tutorials/tutorial_2_send_ble_commands/tutorial.md @@ -5,31 +5,36 @@ sidebar: lesson: 2 --- -# Tutorial 2: Send BLE Commands +# Tutorial 2: Send BLE TLV Commands This document will provide a walk-through tutorial to use the -[Open GoPro BLE Interface](/ble/index.html) to send commands and receive responses. +[Open GoPro BLE Interface]({{site.baseurl}}/ble/index.html) to send [Type-Length-Value](https://en.wikipedia.org/wiki/Type-length-value) +(TLV)commands and receive TLV responses. -"Commands" in this sense are specifically procedures that are initiated by either: +[Commands]({{site.baseurl}}/ble/protocol/data_protocol.html#commands) in this sense are operations that are initiated by either: -- Writing to the Command Request UUID and receiving responses via the Command Response UUID. They are - listed [here](/ble/index.html#commands). -- Writing to the Setting UUID and receiving responses via the Setting Response UUID. They are listed - [here](/ble/index.html#settings). +- Writing to the Command Request UUID and receiving responses via the Command Response + [UUID]({{site.baseurl}}/ble/protocol/ble_setup.html#ble-characteristics). +- Writing to the Setting UUID and receiving responses via the Setting Response + [UUID]({{site.baseurl}}/ble/protocol/ble_setup.html#ble-characteristics) -{% tip %} -It is suggested that you have first completed the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) before going through -this tutorial. -{% endtip %} +A list of TLV commands can be found in the [Command ID Table](http://localhost:4998/ble/protocol/id_tables.html#command-ids). -This tutorial only considers sending these commands as one-off commands. That is, it does not consider state +{% note %} +This tutorial only considers sending these as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future lab. +{% endnote %} # Requirements It is assumed that the hardware and software requirements from the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}) are present and configured correctly. +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}) are present and configured correctly. + +{% tip %} +It is suggested that you have first completed the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) before going through +this tutorial. +{% endtip %} # Just Show me the Demo(s)!! @@ -39,12 +44,13 @@ Each of the scripts for this tutorial can be found in the Tutorial 2 [directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/). {% warning %} -Python >= 3.8.x must be used as specified in the requirements +Python >= 3.9 and < 3.12 must be used as specified in the requirements {% endwarning %} {% accordion Set Shutter %} You can test sending the Set Shutter command to your camera through BLE using the following script: + ```console $ python ble_command_set_shutter.py ``` @@ -63,11 +69,13 @@ optional arguments: Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to ``` + {% endaccordion %} {% accordion Load Preset Group %} You can test sending the Load Preset Group command to your camera through BLE using the following script: + ```console $ python ble_command_load_group.py ``` @@ -86,12 +94,13 @@ optional arguments: Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to ``` -{% endaccordion %} +{% endaccordion %} {% accordion Set the Video Resolution %} You can test sending the Set Video Resolution command to your camera through BLE using the following script: + ```console $ python ble_command_set_resolution.py ``` @@ -110,12 +119,13 @@ optional arguments: Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to ``` -{% endaccordion %} +{% endaccordion %} {% accordion Set the Frames Per Second (FPS) %} You can test sending the Set FPS command to your camera through BLE using the following script: + ```console $ python ble_command_set_fps.py ``` @@ -134,6 +144,7 @@ optional arguments: Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to ``` + {% endaccordion %} {% endtab %} {% tab demo kotlin %} @@ -144,7 +155,7 @@ To perform the tutorial, run the Android Studio project, select "Tutorial 2" fro This requires that a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. You can check the BLE status at the top of the app. -{% include figure image_path="/assets/images/tutorials/kotlin/tutorial_1.png" alt="kotlin_tutorial_2" size="40%" caption="Perform Tutorial 2" %} +{% include figure image_path="/assets/images/tutorials/kotlin/tutorial_2.png" alt="kotlin_tutorial_2" size="40%" caption="Perform Tutorial 2" %} This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click "Exit Tutorial" to return to the Tutorial selection screen. @@ -154,20 +165,21 @@ This will start the tutorial and log to the screen as it executes. When the tuto # Setup -We must first connect as was discussed in the [connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). In -this case, however, we are defining a meaningful (albeit naive) notification handler that will: +We must first connect as was discussed in the [connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). +In this case, however, we are defining a functional (albeit naive) notification handler that will: -1. print byte data and handle that the notification was received on -1. check if the response is what we expected -1. set an event to notify the writer that the response was received +1. Log byte data and handle that the notification was received on +1. Check if the response is what we expected +1. Set an event to notify the writer that the response was received This is a very simple handler; response parsing will be expanded upon in the [next tutorial]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}). {% linkedTabs response_parsing %} {% tab response_parsing python %} + ```python -def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: +async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') # If this is the correct handle and the status is success, the command was a success @@ -186,6 +198,7 @@ received. For now, we're just checking that the handle matches what is expected that the status (third byte) is success (0x00). {% endtab %} {% tab response_parsing kotlin %} + ```kotlin private val receivedData: Channel = Channel() @@ -209,6 +222,7 @@ We are registering this notification handler with the BLE API before sending any ```kotlin ble.registerListener(goproAddress, bleListeners) ``` + {% endtab %} {% endlinkedTabs %} @@ -217,16 +231,16 @@ discussed in future tutorials. # Command Overview -Both Command Requests and Setting Requests follow the same procedure: +All commands follow the same procedure: -1. Write to relevant request UUID +1. Write to the relevant request UUID 1. Receive confirmation from GoPro (via notification from relevant response UUID) that request was received. 1. GoPro reacts to command -{% note %} +{% warning %} The notification response only indicates that the request was received and whether it was accepted or rejected. The relevant behavior of the GoPro must be observed to verify when the command's effects have been applied. -{% endnote %} +{% endwarning %} Here is the procedure from power-on to finish: @@ -247,25 +261,26 @@ sequenceDiagram Now that we are are connected, paired, and have enabled notifications (registered to our defined callback), we can send some commands. -First, we need to define the attributes to write to / receive responses from, which are: - -- For commands - - "Command Request" characteristic (UUID `b5f90072-aa8d-11e3-9046-0002a5d5c51b`) - - "Command Response" characteristic (UUID `b5f90073-aa8d-11e3-9046-0002a5d5c51b`) -- For settings - - "Settings" characteristic (UUID `b5f90074-aa8d-11e3-9046-0002a5d5c51b`) - - "Settings Response" (UUID `b5f90075-aa8d-11e3-9046-0002a5d5c51b`) +First, we need to define the [UUIDs](http://localhost:4998/ble/protocol/ble_setup.html#configure-gatt-characteristics) +to write to / receive responses from, which are: {% linkedTabs uuid %} {% tab uuid python %} -```python -COMMAND_REQ_UUID = GOPRO_BASE_UUID.format("0072") -COMMAND_RSP_UUID = GOPRO_BASE_UUID.format("0073") -``` + +We'll define these and any others used throughout the tutorials and store them in a `GoProUUID` class: ```python -SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") -SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") +class GoProUuid: + COMMAND_REQ_UUID = GOPRO_BASE_UUID.format("0072") + COMMAND_RSP_UUID = GOPRO_BASE_UUID.format("0073") + SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") + SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") + QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") + QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077") + WIFI_AP_SSID_UUID = GOPRO_BASE_UUID.format("0002") + WIFI_AP_PASSWORD_UUID = GOPRO_BASE_UUID.format("0003") + NETWORK_MANAGEMENT_REQ_UUID = GOPRO_BASE_UUID.format("0091") + NETWORK_MANAGEMENT_RSP_UUID = GOPRO_BASE_UUID.format("0092") ``` {% tip %} @@ -296,7 +311,7 @@ enum class GoProUUID(val uuid: UUID) { ## Set Shutter -The first command we will be sending is [Set Shutter](/ble/index.html#commands-quick-reference), +The first command we will be sending is [Set Shutter]({{site.baseurl}}/ble/features/control.html#set-shutter), which at byte level is: | Command | Bytes | @@ -308,10 +323,13 @@ Now, let's write the bytes to the "Command Request" UUID to turn the shutter on {% linkedTabs set_shutter_on %} {% tab set_shutter_on python %} + ```python +request_uuid = GoProUuid.COMMAND_REQ_UUID event.clear() -await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 1])) -await event.wait() # Wait to receive the notification response +request = bytes([3, 1, 1, 1]) +await client.write_gatt_char(request_uuid.value, request, response=True) +await event.wait() # Wait to receive the notification response ``` {% success %} @@ -320,12 +338,18 @@ the notification callback. {% endsuccess %} {% endtab %} {% tab set_shutter_on kotlin %} + ```kotlin val setShutterOnCmd = ubyteArrayOf(0x03U, 0x01U, 0x01U, 0x01U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setShutterOnCmd) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) ``` + +{% success %} +We're waiting to receive the data from the queue that is posted to in the notification handler when the response is received. +{% endsuccess %} + {% endtab %} {% endlinkedTabs %} @@ -339,13 +363,17 @@ This can be seen in the demo log: {% linkedTabs set_shutter_on %} {% tab set_shutter_on python %} + ```console -INFO:root:Setting the shutter on -INFO:root:Received response at handle=52: b'02:01:00' -INFO:root:Shutter command sent successfully +Setting the shutter on +Writing to GoProUuid.COMMAND_REQ_UUID: 03:01:01:01 +Received response at GoProUuid.COMMAND_RSP_UUID: 02:01:00 +Command sent successfully ``` + {% endtab %} {% tab set_shutter_on kotlin %} + ```console Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:01:01:01 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b @@ -353,14 +381,13 @@ Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:01:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:01:00 Command sent successfully ``` + {% endtab %} {% endlinkedTabs %} -As expected, the response was received on the correct handle and the status was "success". - -If you are recording a video, continue reading to set the shutter off. +As expected, the response was received on the correct UUID and the status was "success" (third byte == 0x00). -We can now set the shutter off: +If you are recording a video, continue reading to set the shutter off: {% tip %} We're waiting 2 seconds in case you are in video mode so that we can capture a 2 second video. @@ -368,22 +395,28 @@ We're waiting 2 seconds in case you are in video mode so that we can capture a 2 {% linkedTabs set_shutter_off %} {% tab set_shutter_off python %} + ```python -time.sleep(2) +await asyncio.sleep(2) +request_uuid = GoProUuid.COMMAND_REQ_UUID +request = bytes([3, 1, 1, 0]) event.clear() -await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 0])) -await event.wait() # Wait to receive the notification response +await client.write_gatt_char(request_uuid.value, request, response=True) +await event.wait() # Wait to receive the notification response ``` This will log in the console as follows: ```python -INFO:root:Setting the shutter off -INFO:root:Received response at handle=52: b'02:01:00' -INFO:root:Shutter command sent successfully +Setting the shutter off +Writing to GoProUuid.COMMAND_REQ_UUID: 03:01:01:00 +Received response at GoProUuid.COMMAND_RSP_UUID: 02:01:00 +Command sent successfully ``` + {% endtab %} {% tab set_shutter_off kotlin %} + ```kotlin delay(2000) val setShutterOffCmd = ubyteArrayOf(0x03U, 0x01U, 0x01U, 0x00U) @@ -391,6 +424,10 @@ val setShutterOffCmd = ubyteArrayOf(0x03U, 0x01U, 0x01U, 0x00U) checkStatus(receivedData.receive()) ``` +{% success %} +We're waiting to receive the data from the queue that is posted to in the notification handler when the response is received. +{% endsuccess %} + This will log as such: ```console @@ -401,13 +438,14 @@ Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:01:00 Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:01:00 Command sent successfully ``` + {% endtab %} {% endlinkedTabs %} ## Load Preset Group The next command we will be sending is -[Load Preset Group](/ble/index.html#commands-quick-reference), which is used +[Load Preset Group]({{site.baseurl}}/ble/features/presets.html#load-preset-group), which is used to toggle between the 3 groups of presets (video, photo, and timelapse). At byte level, the commands are: | Command | Bytes | @@ -416,19 +454,16 @@ to toggle between the 3 groups of presets (video, photo, and timelapse). At byte | Load Photo Preset Group | 0x04 0x3E 0x02 0x03 0xE9 | | Load Timelapse Preset Group | 0x04 0x3E 0x02 0x03 0xEA | -{% note %} -It is possible that the preset GroupID values will vary in future cameras. The only absolutely correct way to know -the preset ID is to read them from the "Get Preset Status" protobuf command. A future lab will discuss protobuf -commands. -{% endnote %} - Now, let's write the bytes to the "Command Request" UUID to change the preset group to Video! {% linkedTabs load_preset_group_send %} {% tab load_preset_group_send python %} + ```python +request_uuid = GoProUuid.COMMAND_REQ_UUID +request = bytes([0x04, 0x3E, 0x02, 0x03, 0xE8]) event.clear() -await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x04, 0x3E, 0x02, 0x03, 0xE8])) +await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response ``` @@ -438,12 +473,18 @@ the notification callback. {% endsuccess %} {% endtab %} {% tab load_preset_group_send kotlin %} + ```kotlin val loadPreset = ubyteArrayOf(0x04U, 0x3EU, 0x02U, 0x03U, 0xE8U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, loadPreset) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) ``` + +{% success %} +We're waiting to receive the data from the queue that is posted to in the notification handler when the response is received. +{% endsuccess %} + {% endtab %} {% endlinkedTabs %} @@ -459,13 +500,17 @@ be seen in the demo log: {% linkedTabs load_preset_group_receive %} {% tab load_preset_group_receive python %} + ```console -INFO:root:Loading the video preset group... -INFO:root:Received response at handle=52: b'02:3e:00' -INFO:root:Command sent successfully +Loading the video preset group... +Sending to GoProUuid.COMMAND_REQ_UUID: 04:3e:02:03:e8 +Received response at GoProUuid.COMMAND_RSP_UUID: 02:3e:00 +Command sent successfully ``` + {% endtab %} {% tab load_preset_group_receive kotlin %} + ```console Loading Video Preset Group Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 04:3E:02:03:E8 @@ -475,16 +520,18 @@ Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:3E:00 Command status received Command sent successfully ``` + {% endtab %} {% endlinkedTabs %} -As expected, the response was received on the correct handle and the status was "success". +As expected, the response was received on the correct UUID and the status was "success" (third byte == 0x00). ## Set the Video Resolution The next command we will be sending is -[Set Video Resolution](/ble/index.html#commands-quick-reference). This is -used to change the value of the Video Resolution setting. It is important to note that this only affects +[Set Setting]({{site.baseurl}}/ble/features/settings.html#set-setting) to set the +[Video Resolution]({{site.baseurl}}/ble/features/settings.html#setting-2). +This is used to change the value of the Video Resolution setting. It is important to note that this only affects **video** resolution (not photo). Therefore, the Video Preset Group must be active in order for it to succeed. This can be done either manually through the camera UI or by sending [Load Preset Group](#load-preset-group). @@ -505,9 +552,12 @@ Now, let's write the bytes to the "Setting Request" UUID to change the video res {% linkedTabs set_resolution %} {% tab set_resolution python %} + ```python +request_uuid = GoProUuid.COMMAND_REQ_UUID +request = bytes([0x03, 0x02, 0x01, 0x09]) event.clear() -await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x02, 0x01, 0x09])) +await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response ``` @@ -518,12 +568,18 @@ the notification callback. {% endtab %} {% tab set_resolution kotlin %} + ```kotlin val setResolution = ubyteArrayOf(0x03U, 0x02U, 0x01U, 0x09U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setResolution) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) ``` + +{% success %} +We're waiting to receive the data from the queue that is posted to in the notification handler when the response is received. +{% endsuccess %} + {% endtab %} {% endlinkedTabs %} @@ -534,18 +590,22 @@ screen: Also note that we have received the "Command Status" notification response from the Command Response characteristic since we enabled its notifications in -[Enable Notifications]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#enable-notifications).. This can +[Enable Notifications]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#enable-notifications). This can be seen in the demo log: {% linkedTabs set_resolution %} {% tab set_resolution python %} + ```console -INFO:root:Loading the video preset group... -INFO:root:Received response at handle=52: b'02:3e:00' -INFO:root:Command sent successfully +Setting the video resolution to 1080 +Writing to GoProUuid.SETTINGS_REQ_UUID: 03:02:01:09 +Received response at GoProUuid.SETTINGS_RSP_UUID: 02:02:00 +Command sent successfully ``` + {% endtab %} {% tab set_resolution kotlin %} + ```console Setting resolution to 1080 Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:02:01:09 @@ -555,21 +615,24 @@ Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:02:00 Command status received Command sent successfully ``` + {% endtab %} {% endlinkedTabs %} -As expected, the response was received on the correct handle and the status was "success". If the Preset +As expected, the response was received on the correct UUID and the status was "success" (third byte == 0x00). If the Preset Group was not Video, the status will not be success. ## Set the Frames Per Second (FPS) The next command we will be sending is -[Set FPS](/ble/index.html#commands-quick-reference). This is +[Set Setting]({{site.baseurl}}/ble/features/settings.html#set-setting) to set the +[FPS]({{site.baseurl}}/ble/features/settings.html#setting-3). This is used to change the value of the FPS setting. It is important to note that this setting is dependent on the video resolution. That is, certain FPS values are not valid with certain resolutions. In general, higher -resolutions only allow lower FPS values. Also, the current anti-flicker value may further limit possible FPS -values. Check the [camera capabilities ](/ble/index.html#camera-capabilities) to see which FPS -values are valid for given use cases. +resolutions only allow lower FPS values. Other settings such as the current anti-flicker value may further limit possible +FPS values. Futhermore, these capabilities all vary by camera. Check the +[camera capabilities ]({{site.baseurl}}/ble/index.html#camera-capabilities) to see which FPS values are valid for +given use cases. Therefore, for this step of the tutorial, it is assumed that the resolution has been set to 1080 as in [Set the Video Resolution](#set-the-video-resolution). @@ -582,17 +645,18 @@ Here are some of the byte level commands for various FPS values. | Set FPS to 60 | 0x03 0x03 0x01 0x05 | | Set FPS to 240 | 0x03 0x03 0x01 0x00 | -Note that the possible FPS values can vary based on the Open GoPro version that the camera supports. -Therefore, it is necessary to -[check the version]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}#complex-command-response). +Note that the possible FPS values can vary based on the Camera that is being operated on. Now, let's write the bytes to the "Setting Request" UUID to change the FPS to 240! {% linkedTabs set_fps %} {% tab set_fps python %} + ```python +request_uuid = GoProUuid.COMMAND_REQ_UUID +request = bytes([0x03, 0x03, 0x01, 0x00]) event.clear() -await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x03, 0x01, 0x00])) +await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response ``` @@ -602,12 +666,18 @@ the notification callback. {% endsuccess %} {% endtab %} {% tab set_fps kotlin %} + ```kotlin val setFps = ubyteArrayOf(0x03U, 0x03U, 0x01U, 0x00U) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, setFps) // Wait to receive the notification response, then check its status checkStatus(receivedData.receive()) ``` + +{% success %} +We're waiting to receive the data from the queue that is posted to in the notification handler when the response is received. +{% endsuccess %} + {% endtab %} {% endlinkedTabs %} @@ -623,13 +693,17 @@ be seen in the demo log: {% linkedTabs set_fps %} {% tab set_fps python %} + ```console -INFO:root:Setting the fps to 240 -INFO:root:Received response at handle=57: b'02:03:00' -INFO:root:Command sent successfully +Setting the fps to 240 +Writing to GoProUuid.SETTINGS_REQ_UUID: 03:03:01:00 +Received response at GoProUuid.SETTINGS_RSP_UUID: 02:03:00 +Command sent successfully ``` + {% endtab %} {% tab set_fps kotlin %} + ```console Setting the FPS to 240 Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:03:01:00 @@ -639,11 +713,12 @@ Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:03:00 Command status received Command sent successfully ``` + {% endtab %} {% endlinkedTabs %} -As expected, the response was received on the correct handle and the status was "success". If the video resolution -was higher, for example 5K, this would fail. +As expected, the response was received on the correct UUID and the status was "success" (third byte == 0x00). If the video +resolution was higher, for example 5K, this would fail. **Quiz time! 📚 ✏️** @@ -663,7 +738,7 @@ was higher, for example 5K, this would fail. option="A:::True" option="B:::False" correct="B" - info="Each resolution can support all or only some FPS values. You can find out which resolutions support which fps values by consulting the [capabilities section of the spec](https://gopro.github.io/OpenGoPro/ble/features/settings.html#camera-capabilities)." + info="Each resolution can support all or only some FPS values. You can find out which resolutions support which FPS values by consulting the [capabilities section of the spec](https://gopro.github.io/OpenGoPro/ble_2_0#camera-capabilities)." %} {% quiz @@ -686,7 +761,7 @@ See the first tutorial's Congratulations 🤙 {% endsuccess %} -You can now send any of the other BLE commands detailed in the Open GoPro documentation in -a similar manner. +You can now send any of the other BLE [commands]({{site.baseurl}}/ble/protocol/data_protocol.html#commands) detailed in the +Open GoPro documentation in a similar manner. -To see how to parse more complicate responses, proceed to the next tutorial. +To see how to parse responses, proceed to the next tutorial. diff --git a/docs/_tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md b/docs/_tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md index a2254efa..c9978749 100644 --- a/docs/_tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md +++ b/docs/_tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md @@ -8,11 +8,23 @@ lesson: 3 # Tutorial 3: Parse BLE TLV Responses This document will provide a walk-through tutorial to implement -the [Open GoPro Interface](/ble/index.html) to parse BLE +the [Open GoPro Interface]({{site.baseurl}}/ble/index.html) to parse BLE [Type-Length-Value](https://en.wikipedia.org/wiki/Type-length-value) (TLV) Responses. -Besides TLV, some BLE commands instead return protobuf responses. These are not considered here and will be -discussed in a future tutorial. +{% note %} +Besides TLV, some BLE operations instead return protobuf responses. These are not considered here and will be +discussed in a [future tutorial]({% link _tutorials/tutorial_5_ble_protobuf/tutorial.md %}) +{% endnote %} + +This tutorial will provide an overview of how to handle responses of both single and multiple packets lengths, then +give parsing examples for each case, and finally create `Response` and `TlvResponse` classes that will be reused in +future tutorials. + +# Requirements + +It is assumed that the hardware and software requirements from the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}) +are present and configured correctly. {% tip %} It is suggested that you have first completed the @@ -21,29 +33,21 @@ and [sending commands]({% link _tutorials/tutorial_2_send_ble_commands/tutorial. through this tutorial. {% endtip %} -This tutorial will give an overview of types of responses, then give examples of parsing each type -before finally providing a **Response** class that will be used in future tutorials. - -# Requirements - -It is assumed that the hardware and software requirements from the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}) -are present and configured correctly. - # Just Show me the Demo(s)!! {% linkedTabs demo %} {% tab demo python %} -Each of the scripts for this tutorial can be found in the Tutorial 2 +Each of the scripts for this tutorial can be found in the Tutorial 3 [directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_3_parse_ble_tlv_responses/). {% warning %} -Python >= 3.8.x must be used as specified in the requirements +Python >= 3.9 and < 3.12 must be used as specified in the requirements {% endwarning %} {% accordion Parsing a One Packet TLV Response %} You can test parsing a one packet TLV response with your camera through BLE using the following script: + ```console $ python ble_command_get_version.py ``` @@ -62,30 +66,33 @@ optional arguments: Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to ``` -{% endaccordion %} +{% endaccordion %} {% accordion Parsing Multiple Packet TLV Responses %} You can test parsing multiple packet TVL responses with your camera through BLE using the following script: + ```console -$ python ble_command_get_state.py +$ python ble_command_get_hardware_info.py ``` See the help for parameter definitions: ```console -$ python ble_command_get_state.py --help -usage: ble_command_get_state.py [-h] [-i IDENTIFIER] +$ python ble_command_get_hardware_info.py --help +usage: ble_command_get_hardware_info.py [-h] [-i IDENTIFIER] -Connect to a GoPro camera via BLE, then get its statuses and settings. +Connect to a GoPro camera via BLE, then get its hardware info. -optional arguments: +options: -h, --help show this help message and exit -i IDENTIFIER, --identifier IDENTIFIER - Last 4 digits of GoPro serial number, which is the last 4 digits of the - default camera SSID. If not used, first discovered GoPro will be connected to + Last 4 digits of GoPro serial number, which is the last 4 digits of + the default camera SSID. If not used, first discovered GoPro will be + connected to ``` + {% endaccordion %} {% endtab %} @@ -108,14 +115,14 @@ This will start the tutorial and log to the screen as it executes. When the tuto # Setup We must first connect as was discussed in the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). When enabling notifications, +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). When enabling notifications, one of the notification handlers described in the following sections will be used. # Response Overview In the preceding tutorials, we have been using a very simple response handling procedure where the notification handler simply checks that the UUID is the expected UUID and that the status byte of the response is 0 (Success). -This has been fine since we were only sending specific commands where this works and we know that the sequence +This has been fine since we were only performing specific operations where this works and we know that the sequence always appears as such (connection sequence left out for brevity): ```mermaid! @@ -128,8 +135,8 @@ sequenceDiagram ``` In actuality, responses can be more complicated. As described in the -[Open GoPro Interface](/ble/index.html#packet-headers), responses can be -be comprised of multiple packets where each packet is <= 20 bytes such as: +[BLE Spec]({{site.baseurl}}/ble/protocol/data_protocol.html#packetization), responses can be be comprised of multiple +packets where each packet is <= 20 bytes such as: ```mermaid! sequenceDiagram @@ -143,19 +150,19 @@ sequenceDiagram GoPro ->> PC: Notification Response (MSB == 1 (continuation)) ``` -This requires the implementation of accumulating and parsing algorithms which will be described in -[Parsing Multiple Packet TLV Responses]. +This requires the implementation of accumulating and parsing algorithms which will be described +[below](#parsing-multiple-packet-tlv-responses). # Parsing a One Packet TLV Response This section will describe how to parse one packet (<= 20 byte) responses. A one-packet response is formatted as such: -| Header (length) | Command / Setting ID | Status | Response | -| --------------- | -------------------- | ------- | ---------------- | -| 1 byte | 1 byte | 1 bytes | Length - 2 bytes | +| Header (length) | Operation ID | Status | Response | +| --------------- | ------------ | ------- | ---------------- | +| 1 byte | 1 byte | 1 bytes | Length - 2 bytes | -## Command / Setting Responses with Response Length 0 +## Responses with Payload Length 0 These are the only responses that we have seen thus far through the first 2 tutorials. They return a status but have a 0 length additional response. For example, consider @@ -168,186 +175,202 @@ of: This equates to: -| Header (length) | Command / Setting / Status ID | Status | Response | -| --------------- | ----------------------------- | --------------- | ---------------- | -| 1 byte | 1 byte | 1 bytes | Length - 2 bytes | -| 0x02 | 0x01 == Set Shutter | 0x00 == Success | (2 -2 = 0 bytes) | +| Header (length) | Command ID | Status | Response | +| --------------- | ------------------- | --------------- | ---------------- | +| 1 byte | 1 byte | 1 bytes | Length - 2 bytes | +| 0x02 | 0x01 == Set Shutter | 0x00 == Success | (2 -2 = 0 bytes) | We can see how this response includes the status but no additional response data. This type of response will be used for most Commands and Setting Responses as seen in the [previous tutorial]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}). -## Complex Command Response +## Responses with Payload -There are some commands that do return additional response data. These are called "complex responses." -From the [commands reference](/ble/index.html#commands-quick-reference), we can see that these are: +However, there are some operations that do return additional response data. These are identified by the presence of +`parameters` in their Response documentation as shown in the red box here: -- Get Open GoPro Version (ID == 0x51) -- Get Hardware Info (ID == 0x3C) +{% include figure image_path="/assets/images/tutorials/complex_response_doc.png" alt="complex response example" size="40%" caption="Response With Payload" %} -In this tutorial, we will walk through creating a simple parser to parse the Open GoPro Get Version Command. +In this tutorial, we will walk through creating a simple parser to parse the +[Open GoPro Get Version Command]({{site.baseurl}}/ble/features/query.html#get-open-gopro-version) which is an example +of such an operation. {% tip %} It is important to always query the version after connecting in order to know which API is supported. See the relevant version of the BLE and / or WiFi spec for more details about each version. {% endtip %} -First, we send the command to the Command Request [UUID](/ble/index.html#services-and-characteristics): +First, we send the Get Version Command to the Command Request +[UUID]({{site.baseurl}}/ble/protocol/ble_setup.html#configure-gatt-characteristics) in the same manner as commands +were sent in the previous tutorial: {% linkedTabs send_command %} {% tab send_command python %} + ```python -COMMAND_REQ_UUID = GOPRO_BASE_UUID.format("0072") -event.clear() -await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x01, 0x51])) +request_uuid = GoProUuid.COMMAND_REQ_UUID +request = bytes([0x01, 0x51]) +await client.write_gatt_char(request_uuid.value, request, response=True) await event.wait() # Wait to receive the notification response ``` -We then receive a response at the expected handle. This is logged as: +We receive a response at the expected handle (as a TLV Response). This is logged as: ```console -INFO:root:Getting the Open GoPro version... -INFO:root:Received response at handle=52: b'06:51:00:01:02:01:00' +Getting the Open GoPro version... +Writing to GoProUuid.COMMAND_REQ_UUID: 01:51 +Received response GoProUuid.COMMAND_RSP_UUID: 06:51:00:01:02:01:00 ``` + {% endtab %} {% tab send_command kotlin %} + ```kotlin - val getVersion = ubyteArrayOf(0x01U, 0x51U) - ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, getVersion) - val version = receivedResponse.receive() as Response.Complex // Wait to receive response +val versionRequest = ubyteArrayOf(0x01U, 0x51U) +ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, versionRequest) +var tlvResponse = receivedResponses.receive() as Response.Tlv ``` -This is loged as such: +We then receive a response at the expected handle. This is logged as: + +This is logged as such: ```console Getting the Open GoPro version Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 01:51 Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 06:51:00:01:02:01:00 -Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 06:51:00:01:02:01:00 +Received response on CQ_COMMAND_RSP +Received packet of length 6. 0 bytes remaining ``` + {% endtab %} {% endlinkedTabs %} This response equates to: -| Header (length) | Command / Setting / Status ID | Status | Response | -| --------------- | ----------------------------- | --------------- | ------------------- | -| 1 byte | 1 byte | 1 bytes | Length - 2 bytes | -| 0x06 | 0x51 == Get Version | 0x00 == Success | 0x01 0x02 0x01 0x00 | +| Header (length) | Command ID | Status | Response | +| --------------- | ------------------- | --------------- | ------------------- | +| 1 byte | 1 byte | 1 bytes | Length - 2 bytes | +| 0x06 | 0x51 == Get Version | 0x00 == Success | 0x01 0x02 0x01 0x00 | -We can see that this "complex response" contains 4 additional bytes that need to be parsed. Using the information -from the [interface description](/ble/index.html#complex-command-responses), -we know to parse this as: +We can see that this response payload contains 4 additional bytes that need to be parsed. Using the information +from the [Get Version Documentation]({{site.baseurl}}/ble/features/query.html#get-open-gopro-version), we know to +parse this as: -| Byte | Meaning | -| ---- | ------------------------------ | -| 0x01 | Length of Major Version Number | -| 0x02 | Major Version Number | -| 0x01 | Length of Minor Version Number | -| 0x00 | Minor Version Number | +| Byte | Meaning | +| ---- | ------------------------------------- | +| 0x01 | Length of Major Version Number | +| 0x02 | Major Version Number of length 1 byte | +| 0x01 | Length of Minor Version Number | +| 0x00 | Minor Version Number of length 1 byte | -We implement this in the notification handler as follows. First, we parse the length, command ID, and status -from the first 3 bytes of the response. Then we parse the remaining four bytes of the response as individual -values formatted as such: - -| Length | Value | -| ------ | ------------ | -| 1 byte | Length bytes | +We implement this as follows. First, we parse the length, command ID, and status from the first 3 bytes of the response. +The remainder is stored as the payload. This is all of the common parsing across TLV Responses. Each individual +response will document how to further parse the payload. {% linkedTabs parse_response %} {% tab parse_response python %} + {% note %} The snippets of code included in this section are taken from the `notification handler` {% endnote %} - ```python -# Parse first 3 bytes +# First byte is the length of this response. length = data[0] +# Second byte is the ID command_id = data[1] +# Third byte is the status status = data[2] +# The remainder is the payload +payload = data[3 : length + 1] ``` -```python -# Parse remaining four bytes -index = 3 -params = [] -while index <= length: - param_len = data[index] - index += 1 - params.append(data[index : index + param_len]) - index += param_len -``` {% endtab %} {% tab parse_response kotlin %} + {% note %} -The snippets of code included in this section are taken from the `Response.Complex` `parse` method. For the -contrived code in this tutorial, we have separate `Response` sealed classes to handle each use case. +The snippets of code included in this section are taken from the `Response.Tlv.Parse` method {% endnote %} ```kotlin // Parse header bytes -id = packet[0].toInt() -status = packet[1].toInt() -var buf = packet.drop(2) -``` +tlvResponse.parse() -```kotlin -// Parse remaining packet -while (buf.isNotEmpty()) { - // Get each parameter's ID and length - val paramLen = buf[0].toInt() - buf = buf.drop(1) - // Get the parameter's value - val paramVal = buf.take(paramLen) - // Store in data list - data += paramVal.toUByteArray() - // Advance the buffer for continued parsing - buf = buf.drop(paramLen) +... + +open fun parse() { + require(isReceived) + id = rawBytes[0].toInt() + status = rawBytes[1].toInt() + // Store remainder as payload + payload = rawBytes.drop(2).toUByteArray() } + ``` + {% endtab %} {% endlinkedTabs %} -From the complex response definition, we know these parameters are one byte each and equate to the major and +From the response definition, we know these parameters are one byte each and equate to the major and the minor version so let's print them (and all of the other response information) as such: {% linkedTabs print_response %} {% tab print_response python %} + ```python -major, minor = params +major_length = payload[0] +payload.pop(0) +major = payload[:major_length] +payload.pop(major_length) +minor_length = payload[0] +payload.pop(0) +minor = payload[:minor_length] +logger.info(f"The version is Open GoPro {major[0]}.{minor[0]}") logger.info(f"Received a response to {command_id=} with {status=}: version={major[0]}.{minor[0]}") ``` which shows on the log as: ```console -INFO:root:Received a response to command_id=81 with status=0: version=2.0 +Received a response to command_id=81 with status=0, payload=01:02:01:00 +The version is Open GoPro 2.0 ``` + {% endtab %} {% tab print_response kotlin %} + +{% note %} +The snippets of code included in this section are taken from the `OpenGoProVersion` `from_bytes` method. This class +is a simple data class to contain the Get Version information. +{% endnote %} + ```kotlin -val version = receivedResponse.receive() as Response.Complex // Wait to receive response -val major = version.data[0].first().toInt() -val minor = version.data[1].first().toInt() -Timber.i("Got the Open GoPro version successfully: $major.$minor") +var buf = data.toUByteArray() +val minorLen = buf[0].toInt() +buf = buf.drop(1).toUByteArray() +val minor = buf.take(minorLen).toInt() +val majorLen = buf[0].toInt() +buf = buf.drop(1).toUByteArray() +val major = buf.take(majorLen).toInt() +return OpenGoProVersion(minor, major) ``` which shows on the log as such: ```console +Received response: ID: 81, Status: 0, Payload: 01:02:01:00 Got the Open GoPro version successfully: 2.0 ``` - {% endtab %} {% endlinkedTabs %} **Quiz time! 📚 ✏️** {% quiz - question="What is the maximum size of an individual notification response packet?" + question="What is the maximum size of an individual notification response packet at the Open GoPro application layer?" option="A:::20 bytes" option="B:::256 bytes" option="C:::There is no maximum size" @@ -357,7 +380,7 @@ Got the Open GoPro version successfully: 2.0 %} {% quiz - question="What is the maximum amount of packets that one response can be composed of?" + question="What is the maximum amount of bytes that one response can be composed of?" option="A:::20 bytes" option="B:::256 bytes" option="C:::There is no maximum size" @@ -366,38 +389,36 @@ Got the Open GoPro version successfully: 2.0 %} {% quiz - question="What is the maximum amount of packets that one response can be composed of?" + question="How many packets are command responses composed of?" option="A:::Always 1 packet" option="B:::Always multiple packets." - option="C:::Always 1 packet except for complex responses." + option="C:::A variable amount of packets depending on the payload size" correct="C" - info="Command responses are almost always 1 packet (just returning the status). - The exception are complex responses which can be multiple packets (in the case of Get Hardware - Info)" + info="Command responses are sometimes 1 packet (just returning the status). + Other times, command responses also contain a payload and can thus be multiple packets if the payload is big enough + (i.e. in the case of Get Hardware Info). This is described in the per-command documentation in the BLE spec." %} {% quiz question="How many packets are setting responses comprised of?" option="A:::Always 1 packet" option="B:::Always multiple packets." - option="C:::Always 1 packet except for complex responses." + option="C:::A variable amount of packets depending on the payload size" correct="A" - info="Settings Responses only ever contain the command status. Furthermore, there - is no concept of complex responses for setting commands." + info="Settings Responses only ever contain the response status." %} # Parsing Multiple Packet TLV Responses This section will describe parsing TLV responses that contain more than one packet. It will first describe how -to accumulate such responses and then provide a parsing example. The example script that will be walked through -for this section is `ble_command_get_state.py`. We will be creating a small _Response_ class that will be -re-used for future tutorials. +to accumulate such responses and then provide a parsing example. We will be creating small _Response_ and _TlvResponse_ +classes that will be re-used for future tutorials. ## Accumulating the Response The first step is to accumulate the multiple packets into one response. Whereas for all tutorials until now, we -have just used the header bytes of the response as the length, we now must completely parse the header as it is -defined: +have just used the header bytes of the response as the length, we now must completely parse the headers as they are +[defined]({{site.baseurl}}/ble/protocol/data_protocol.html#packet-headers), reproduced for reference here: @@ -462,37 +483,46 @@ defined:
-The basic algorithm here (which is implemented in the _Message.accumulate_ method) is as follows: +The basic accumulation algorithm (which is implemented in the _Response.Accumulate_ method) is as follows: ---
-Continuation bit set? +Is the continuation bit set? {% linkedTabs is_cont_set %} {% tab is_cont_set python %} + +{% note %} +The example script that will be walked through for this section is `ble_command_get_hardware_info.py`. +{% endnote %} + ```python if buf[0] & CONT_MASK: buf.pop(0) else: ... ``` + {% endtab %} {% tab is_cont_set kotlin %} + ```kotlin if (data.first().and(Mask.Continuation.value) == Mask.Continuation.value) { buf = buf.drop(1).toUByteArray() // Pop the header byte } else { // This is a new packet ... ``` + {% endtab %} {% endlinkedTabs %} -No, continuation bit was not set. So create new response, then get its length. +No, the continuation bit was not set. Therefore create new response, then get its length. {% linkedTabs cont_not_set %} {% tab cont_not_set python %} + ```python # This is a new packet so start with an empty byte array self.bytes = bytearray() @@ -507,8 +537,10 @@ elif hdr is Header.EXT_16: self.bytes_remaining = (buf[1] << 8) + buf[2] buf = buf[3:] ``` + {% endtab %} {% tab cont_not_set kotlin %} + ```kotlin // This is a new packet so start with empty array packet = ubyteArrayOf() @@ -531,6 +563,7 @@ when (Header.fromValue((buf.first() and Mask.Header.value).toInt() shr 5)) { } } ``` + {% endtab %} {% endlinkedTabs %} @@ -538,43 +571,69 @@ Append current packet to response and decrement bytes remaining. {% linkedTabs append_packet %} {% tab append_packet python %} + ```python # Append payload to buffer and update remaining / complete self.bytes.extend(buf) self.bytes_remaining -= len(buf) ``` + {% endtab %} {% tab append_packet kotlin %} + ```kotlin // Accumulate the payload now that headers are handled and dropped packet += buf bytesRemaining -= buf.size ``` + {% endtab %} {% endlinkedTabs %} -In the notification handler, we are then parsing if there are no bytes remaining. +In the notification handler, we are then enqueueing the received response if there are no bytes remaining. {% linkedTabs parse_if_done %} {% tab parse_if_done python %} + ```python if response.is_received: - response.parse() + ... + await received_responses.put(response) ``` + +and finally parsing the payload back in the main task after it receives the accumulated response from the queue which, +at the current TLV Response level, is just extracting the ID, status, and payload: + +```python +class TlvResponse(Response): + def parse(self) -> None: + self.id = self.raw_bytes[0] + self.status = self.raw_bytes[1] + self.payload = self.raw_bytes[2:] + +... + +response = await received_responses.get() +response.parse() +``` + {% endtab %} {% tab parse_if_done kotlin %} + ```kotlin -rsp.accumulate(data) -if (rsp.isReceived) { - rsp.parse() +if (response.isReceived) { + if (uuid == GoProUUID.CQ_COMMAND_RSP) { + CoroutineScope(Dispatchers.IO).launch { receivedResponses.send(response) } + } ... ``` + {% endtab %} {% endlinkedTabs %}
-
+



```mermaid! graph TD @@ -592,416 +651,277 @@ graph TD --- -We can see this in action when we send the _Get All Setting Values_ Query. - -{% note %} -Queries aren't introduced until the next tutorial so for now, just pay attention to the response. -{% endnote %} - -We send the command as such: +We can see this in action when we send the +[Get Hardware Info]({{site.baseurl}}/ble/features/query.html#get-hardware-info) Command: {% linkedTabs get_all_settings_values %} {% tab get_all_settings_values python %} + ```python -QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") -event.clear() -await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x01, 0x12])) -await event.wait() # Wait to receive the notification response +request_uuid = GoProUuid.COMMAND_REQ_UUID +request = bytearray([0x01, 0x3C]) +await client.write_gatt_char(request_uuid.value, request, response=True) +response = await received_responses.get() ``` + {% endtab %} {% tab get_all_settings_values kotlin %} + ```kotlin -val getCameraSettings = ubyteArrayOf(0x01U, 0x12U) -ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, getCameraSettings) -val settings = receivedResponse.receive() +val hardwareInfoRequest = ubyteArrayOf(0x01U, 0x3CU) +ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, hardwareInfoRequest) ``` + {% endtab %} {% endlinkedTabs %} -Then, in the notification handler, we continuously receive and accumulate packets until we have -received the entire response, at which point we notify the writer that the response is ready: +Then, in the notification handler, we continuously receive and accumulate packets (per UUID) until we have +received an entire response, at which point we perform common TLV parsing (via the `TlvResponse`'s `parse` method) +to extract Command ID, Status, and payload. Then we enqueue the received response to notify the writer that the response +is ready. Finally we reset the per-UUID response to prepare it to receive a new response. + +{% note %} +This notification handler is only designed to handle TlvResponses. This is fine for this tutorial since that is all +we will be receiving. +{% endnote %} + +{% linkedTabs parse_get_hardware_info %} +{% tab parse_get_hardware_info python %} -{% linkedTabs parse_get_all_settings_values %} -{% tab parse_get_all_settings_values python %} ```python - def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - response.accumulate(data) +request_uuid = GoProUuid.COMMAND_REQ_UUID +response_uuid = GoProUuid.COMMAND_RSP_UUID +responses_by_uuid = GoProUuid.dict_by_uuid(TlvResponse) +received_responses: asyncio.Queue[TlvResponse] = asyncio.Queue() - if response.is_received: - response.parse() +async def tlv_notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + response = responses_by_uuid[uuid] + response.accumulate(data) - # Notify writer that procedure is complete - event.set() + if response.is_received: + # If this is the correct handle, enqueue it for processing + if uuid is response_uuid: + logger.info("Received the get hardware info response") + await received_responses.put(response) + # Anything else is unexpected. This shouldn't happen + else: + logger.error("Unexpected response") + # Reset the per-UUID response + responses_by_uuid[uuid] = TlvResponse(uuid) ``` + {% endtab %} -{% tab parse_get_all_settings_values kotlin %} +{% tab parse_get_hardware_info kotlin %} + ```kotlin -private fun tlvResponseNotificationHandler(characteristic: UUID, data: UByteArray) { +private fun notificationHandler(characteristic: UUID, data: UByteArray) { ... - rsp.accumulate(data) - if (rsp.isReceived) { - rsp.parse() - - // Notify the command sender the the procedure is complete - response = null // Clear for next command - CoroutineScope(Dispatchers.IO).launch { receivedResponse.send(rsp) } + responsesByUuid[uuid]?.let { response -> + response.accumulate(data) + if (response.isReceived) { + if (uuid == GoProUUID.CQ_COMMAND_RSP) { + CoroutineScope(Dispatchers.IO).launch { receivedResponses.send(response) } + } + ... + responsesByUuid[uuid] = Response.muxByUuid(uuid) + } } +} ``` + {% endtab %} {% endlinkedTabs %} -{% note %} -We also first parse the response but that will be described in the next section. -{% endnote %} - We can see the individual packets being accumulated in the log: -{% linkedTabs print_get_all_settings_values %} -{% tab print_get_all_settings_values python %} +{% linkedTabs get_hardware_info_log %} +{% tab get_hardware_info_log python %} + ```console -INFO:root:Getting the camera's settings... -INFO:root:Received response at handle=62: b'21:25:12:00:02:01:09:03:01:01:05:0 -INFO:root:self.bytes_remaining=275 -INFO:root:Received response at handle=62: b'80:01:00:18:01:00:1e:04:00:00:00:0 -INFO:root:self.bytes_remaining=256 -INFO:root:Received response at handle=62: b'81:0a:25:01:00:29:01:09:2a:01:05:2 -INFO:root:self.bytes_remaining=237 -INFO:root:Received response at handle=62: b'82:2f:01:04:30:01:03:36:01:00:3b:0 -INFO:root:self.bytes_remaining=218 -INFO:root:Received response at handle=62: b'83:04:00:00:00:00:3e:04:00:00:00:0 -INFO:root:self.bytes_remaining=199 -INFO:root:Received response at handle=62: b'84:00:42:04:00:00:00:00:43:04:00:0 -INFO:root:self.bytes_remaining=180 -INFO:root:Received response at handle=62: b'85:4f:01:00:53:01:00:54:01:00:55:0 -INFO:root:self.bytes_remaining=161 -INFO:root:Received response at handle=62: b'86:01:28:5b:01:02:60:01:00:66:01:0 -INFO:root:self.bytes_remaining=142 -INFO:root:Received response at handle=62: b'87:00:6a:01:00:6f:01:0a:70:01:ff:7 -INFO:root:self.bytes_remaining=123 -INFO:root:Received response at handle=62: b'88:75:01:00:76:01:04:79:01:00:7a:0 -INFO:root:self.bytes_remaining=104 -INFO:root:Received response at handle=62: b'89:01:00:7e:01:00:80:01:0c:81:01:0 -INFO:root:self.bytes_remaining=85 -INFO:root:Received response at handle=62: b'8a:0c:85:01:09:86:01:00:87:01:01:8 -INFO:root:self.bytes_remaining=66 -INFO:root:Received response at handle=62: b'8b:92:01:00:93:01:00:94:01:02:95:0 -INFO:root:self.bytes_remaining=47 -INFO:root:Received response at handle=62: b'8c:01:00:9c:01:00:9d:01:00:9e:01:0 -INFO:root:self.bytes_remaining=28 -INFO:root:Received response at handle=62: b'8d:00:a2:01:00:a3:01:01:a4:01:00:a -INFO:root:self.bytes_remaining=9 -INFO:root:Received response at handle=62: b'8e:a8:04:00:00:00:00:a9:01:01' -INFO:root:self.bytes_remaining=0 -INFO:root:Successfully received the response +Getting the camera's hardware info... +Writing to GoProUuid.COMMAND_REQ_UUID: 01:3c +Received response at handle 47: 20:62:3c:00:04:00:00:00:3e:0c:48:45:52:4f:31:32:20:42:6c:61 +self.bytes_remaining=80 +Received response at handle 47: 80:63:6b:04:30:78:30:35:0f:48:32:33:2e:30:31:2e:30:31:2e:39 +self.bytes_remaining=61 +Received response at handle 47: 81:39:2e:35:36:0e:43:33:35:30:31:33:32:34:35:30:30:37:30:32 +self.bytes_remaining=42 +Received response at handle 47: 82:11:48:45:52:4f:31:32:20:42:6c:61:63:6b:64:65:62:75:67:0c +self.bytes_remaining=23 +Received response at handle 47: 83:32:36:37:34:66:37:66:36:36:31:30:34:01:00:01:01:01:00:02 +self.bytes_remaining=4 +Received response at handle 47: 84:5b:5d:01:01 +self.bytes_remaining=0 +Received the get hardware info response ``` + {% endtab %} -{% tab print_get_all_settings_values kotlin %} +{% tab get_hardware_info_log kotlin %} ```console -Writing characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b ==> 01:12 -Wrote characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 21:2B:12:00:02:01:04:03:01:05:05:01:00:06:01:01:0D:01:01:13 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 21:2B:12:00:02:01:04:03:01:05:05:01:00:06:01:01:0D:01:01:13 -Received packet of length 18. 281 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 80:01:00:18:01:00:1E:04:00:00:00:6E:1F:01:00:20:04:00:00:00 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 80:01:00:18:01:00:1E:04:00:00:00:6E:1F:01:00:20:04:00:00:00 -Received packet of length 19. 262 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 81:0A:25:01:00:29:01:09:2A:01:08:2B:01:00:2C:01:09:2D:01:08 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 81:0A:25:01:00:29:01:09:2A:01:08:2B:01:00:2C:01:09:2D:01:08 -Received packet of length 19. 243 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 82:2F:01:07:36:01:01:3B:01:04:3C:04:00:00:00:00:3D:04:00:00 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 82:2F:01:07:36:01:01:3B:01:04:3C:04:00:00:00:00:3D:04:00:00 -Received packet of length 19. 224 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 83:00:00:3E:04:00:12:4F:80:40:01:04:41:04:00:00:00:00:42:04 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 83:00:00:3E:04:00:12:4F:80:40:01:04:41:04:00:00:00:00:42:04 -Received packet of length 19. 205 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 84:00:00:00:00:43:04:00:12:4F:80:4B:01:00:4C:01:00:53:01:01 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 84:00:00:00:00:43:04:00:12:4F:80:4B:01:00:4C:01:00:53:01:01 -Received packet of length 19. 186 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 85:54:01:00:55:01:00:56:01:00:57:01:00:58:01:32:5B:01:03:66 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 85:54:01:00:55:01:00:56:01:00:57:01:00:58:01:32:5B:01:03:66 -Received packet of length 19. 167 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 86:01:08:67:01:03:69:01:00:6F:01:0A:70:01:64:72:01:01:73:01 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 86:01:08:67:01:03:69:01:00:6F:01:0A:70:01:64:72:01:01:73:01 -Received packet of length 19. 148 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 87:00:74:01:02:75:01:01:76:01:04:79:01:03:7A:01:65:7B:01:65 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 87:00:74:01:02:75:01:01:76:01:04:79:01:03:7A:01:65:7B:01:65 -Received packet of length 19. 129 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 88:7C:01:64:7D:01:00:7E:01:00:80:01:0D:81:01:02:82:01:69:83 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 88:7C:01:64:7D:01:00:7E:01:00:80:01:0D:81:01:02:82:01:69:83 -Received packet of length 19. 110 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 89:01:03:84:01:0C:86:01:02:87:01:01:8B:01:03:90:01:0C:91:01 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 89:01:03:84:01:0C:86:01:02:87:01:01:8B:01:03:90:01:0C:91:01 -Received packet of length 19. 91 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8A:00:92:01:00:93:01:00:94:01:01:95:01:02:96:01:00:97:01:00 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8A:00:92:01:00:93:01:00:94:01:01:95:01:02:96:01:00:97:01:00 -Received packet of length 19. 72 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8B:99:01:64:9A:01:02:9B:01:64:9C:01:64:9D:01:64:9E:01:01:9F -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8B:99:01:64:9A:01:02:9B:01:64:9C:01:64:9D:01:64:9E:01:01:9F -Received packet of length 19. 53 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8C:01:01:A0:01:00:A1:01:64:A2:01:00:A3:01:01:A4:01:64:A7:01 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8C:01:01:A0:01:00:A1:01:64:A2:01:00:A3:01:01:A4:01:64:A7:01 -Received packet of length 19. 34 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8D:04:A8:04:00:00:00:00:A9:01:01:AE:01:00:AF:01:01:B0:01:03 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8D:04:A8:04:00:00:00:00:A9:01:01:AE:01:00:AF:01:01:B0:01:03 -Received packet of length 19. 15 bytes remaining -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 8E:B1:01:00:B2:01:01:B3:01:03:B4:01:00:B5:01:00 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 8E:B1:01:00:B2:01:01:B3:01:03:B4:01:00:B5:01:00 -Received packet of length 15. 0 bytes remaining -Received the expected successful response -Got the camera's settings successfully +Getting the Hardware Info +Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 01:3C +Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 20:5B:3C:00:04:00:00:00:3E:0C:48:45:52:4F:31:32:20:42:6C:61 +Received response on CQ_COMMAND_RSP +Received packet of length 18. 73 bytes remaining +Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 80:63:6B:04:30:78:30:35:0F:48:32:33:2E:30:31:2E:30:31:2E:39 +Received response on CQ_COMMAND_RSP +Received packet of length 19. 54 bytes remaining +Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b +Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 81:39:2E:35:36:0E:43:33:35:30:31:33:32:34:35:30:30:37:30:32 +Received response on CQ_COMMAND_RSP +Received packet of length 19. 35 bytes remaining +Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 82:0A:47:50:32:34:35:30:30:37:30:32:0C:32:36:37:34:66:37:66 +Received response on CQ_COMMAND_RSP +Received packet of length 19. 16 bytes remaining +Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 83:36:36:31:30:34:01:00:01:01:01:00:02:5B:5D:01:01 +Received response on CQ_COMMAND_RSP +Received packet of length 16. 0 bytes remaining ``` + {% endtab %} {% endlinkedTabs %} -At this point the response has been accumulated. See the next section for how to parse it. +At this point the response has been accumulated. We then parse and log the payload using the +[Get Hardware Info]({{site.baseurl}}/ble/features/query.html#get-hardware-info) response documentation: -**Quiz time! 📚 ✏️** - -{% quiz - question="How can we know that a response has been completely received?" - option="A:::The stop bit will be set in the header" - option="B:::The response has accumulated length bytes" - option="C:::By checking for the end of frame (EOF) sentinel character" - correct="B" - info="The length of the entire response is parsed from the first packet. We - then accumulate packets, keeping track of the received length, until all of the bytes - have been received. A and C are just made up 😜." -%} +{% linkedTabs parse_get_hardware_info_payload %} +{% tab parse_get_hardware_info_payload python %} -## Parsing a Query Response - -This section is going to describe responses to to BLE status / setting queries. We don't actually -introduce such queries until [the next tutorial]({% link _tutorials/tutorial_4_ble_queries/tutorial.md %}) so -for now, only the parsing of the response is important. - -{% tip %} -While multi-packet responses are almost always Query Responses, they can also be from Command Complex -responses. In a real-world implementation, it is therefore necessary to check the received UUID to see -how to parse. -{% endtip %} - -Query Responses contain one or more TLV groups in their Response data. To recap, the generic response format is: - -| Header (length) | Query ID | Status | Response | -| --------------- | -------- | ------- | ---------------- | -| 1-2 bytes | 1 byte | 1 bytes | Length - 2 bytes | - -This means that query responses will contain an array of additional TLV groups in the "Response" field as such: - -| ID1 | Length1 | Value1 | ID2 | Length2 | Value 2 | ... | IDN | LengthN | ValueN | -| ------ | ------- | ------------- | ------ | ------- | ------------- | --- | ------ | ------- | ------------- | -| 1 byte | 1 byte | Length1 bytes | 1 byte | 1 byte | Length2 bytes | ... | 1 byte | 1 byte | LengthN bytes | - -Depending on the amount of query results in the response, this response can be one or multiple packets. -Therefore, we need to account for the possibility that it may always be more than 1 packet. - -We can see an example of such parsing in the response parse method as shown below: - ---- - -
-
- -We have already parsed the length when we were accumulating the packet. So the next step is to parse the Query -ID and Status: - -{% linkedTabs id_status_query_response %} -{% tab id_status_query_response python %} ```python -self.id = self.bytes[0] -self.status = self.bytes[1] +hardware_info = HardwareInfo.from_bytes(response.payload) +logger.info(f"Received hardware info: {hardware_info}") ``` -{% endtab %} -{% tab id_status_query_response kotlin %} -```kotlin -id = packet[0].toInt() -status = packet[1].toInt() -``` -{% endtab %} -{% endlinkedTabs %} -We then continuously parse **Type (ID) - Length - Value** groups until we have consumed the response. We are -storing each value in a hash map indexed by ID for later access. +where the parsing is done as such: -{% linkedTabs tlv_query_response %} -{% tab tlv_query_response python %} ```python -buf = self.bytes[2:] -while len(buf) > 0: - # Get ID and Length - param_id = buf[0] - param_len = buf[1] - buf = buf[2:] - # Get the value - value = buf[:param_len] - - # Store in dict for later access - self.data[param_id] = value + @classmethod + def from_bytes(cls, data: bytes) -> HardwareInfo: + buf = bytearray(data) + # Get model number + model_num_length = buf.pop(0) + model = int.from_bytes(buf[:model_num_length]) + buf = buf[model_num_length:] + # Get model name + model_name_length = buf.pop(0) + model_name = (buf[:model_name_length]).decode() + buf = buf[model_name_length:] + # Advance past deprecated bytes + deprecated_length = buf.pop(0) + buf = buf[deprecated_length:] + # Get firmware version + firmware_length = buf.pop(0) + firmware = (buf[:firmware_length]).decode() + buf = buf[firmware_length:] + # Get serial number + serial_length = buf.pop(0) + serial = (buf[:serial_length]).decode() + buf = buf[serial_length:] + # Get AP SSID + ssid_length = buf.pop(0) + ssid = (buf[:ssid_length]).decode() + buf = buf[ssid_length:] + # Get MAC address + mac_length = buf.pop(0) + mac = (buf[:mac_length]).decode() + buf = buf[mac_length:] + + return cls(model, model_name, firmware, serial, ssid, mac) +``` + +This logs as: - # Advance the buffer - buf = buf[param_len:] -``` -{% endtab %} -{% tab tlv_query_response kotlin %} -```kotlin -while (buf.isNotEmpty()) { - // Get each parameter's ID and length - val paramId = buf[0] - val paramLen = buf[1].toInt() - buf = buf.drop(2) - // Get the parameter's value - val paramVal = buf.take(paramLen) - // Store in data dict for access later - data[paramId] = paramVal.toUByteArray() - // Advance the buffer for continued parsing - buf = buf.drop(paramLen) -} +```console +Parsed hardware info: { + "model_name": "HERO12 Black", + "firmware_version": "H23.01.01.99.56", + "serial_number": "C3501324500702", + "ap_ssid": "HERO12 Blackdebug", + "ap_mac_address": "2674f7f66104" + } ``` -{% endtab %} -{% endlinkedTabs %} - -
-
-


+{% endtab %} +{% tab parse_get_hardware_info_payload kotlin %} -```mermaid! -graph TD - A[Parse Query ID] --> B[Parse Status] - B --> C{More data?} - C --> |yes|D[Get Value ID] - D --> E[Get Value Length] - E --> F[Get Value] - F --> C - C --> |no|G(done) +```kotlin +tlvResponse.parse() +val hardwareInfo = HardwareInfo.fromBytes(tlvResponse.payload) ``` -
-
- ---- - -In the tutorial demo, we then log this entire dict after parsing is complete as such (abbreviated for brevity): - -{% linkedTabs print_query_response %} -{% tab print_query_response python %} +where the parsing is done as such: -```console -INFO:root:Received settings -: { - "2": "09", - "3": "01", - "5": "00", - "6": "01", - "13": "01", - "19": "00", - "30": "00:00:00:00", - "31": "00", - "32": "00:00:00:0a", - "41": "09", - "42": "05", - "43": "00", - ... - "160": "00", - "161": "00", - "162": "00", - "163": "01", - "164": "00", - "165": "00", - "166": "00", - "167": "04", - "168": "00:00:00:00", - "169": "01" +```kotlin +fun fromBytes(data: UByteArray): HardwareInfo { + // Parse header bytes + var buf = data.toUByteArray() + // Get model number + val modelNumLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val model = buf.take(modelNumLength).toInt() + buf = buf.drop(modelNumLength).toUByteArray() + // Get model name + val modelNameLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val modelName = buf.take(modelNameLength).decodeToString() + buf = buf.drop(modelNameLength).toUByteArray() + // Advance past deprecated bytes + val deprecatedLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + buf = buf.drop(deprecatedLength).toUByteArray() + // Get firmware version + val firmwareLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val firmware = buf.take(firmwareLength).decodeToString() + buf = buf.drop(firmwareLength).toUByteArray() + // Get serial number + val serialLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val serial = buf.take(serialLength).decodeToString() + buf = buf.drop(serialLength).toUByteArray() + // Get AP SSID + val ssidLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val ssid = buf.take(ssidLength).decodeToString() + buf = buf.drop(ssidLength).toUByteArray() + // Get MAC Address + val macLength = buf.first().toInt() + buf = buf.drop(1).toUByteArray() + val mac = buf.take(macLength).decodeToString() + + return HardwareInfo(model, modelName, firmware, serial, ssid, mac) } ``` -{% endtab %} -{% tab print_query_response kotlin %} + +This logs as: ```console -{ - "2": "09", - "3": "01", - "5": "00", - "6": "01", - "13": "01", - "19": "00", - "24": "00", - "30": "00:00:00:6E", - "31": "00", - "32": "00:00:00:0A", - "37": "00", - "41": "09", - "42": "08", - "43": "00", - "44": "09", - "45": "08", - "47": "07", - ... - "115": "00", - "116": "02", - "117": "01", - "151": "00", - "153": "64", - "154": "02", - "155": "64", - "156": "64", - "157": "64", - "158": "01", - "159": "01", - "160": "00", - "161": "64", - "162": "00", - "163": "01", - "164": "64", - "167": "04", - "168": "00:00:00:00", - "169": "01", - "174": "00", - "175": "01", - "176": "03", - "177": "00", - "178": "01", - "179": "03", - "180": "00", - "181": "00" -} +Got the Hardware Info successfully: HardwareInfo( + modelNumber=1040187392, + modelName=HERO12 Black, + firmwareVersion=H23.01.01.99.56, + serialNumber=C3501324500702, + apSsid=GP24500702, + apMacAddress=2674f7f66104 +) ``` - {% endtab %} {% endlinkedTabs %} -We can see what each of these values mean by looking at the -[Open GoPro Interface](/ble/index.html#settings-quick-reference). - -For example: - -- ID 2 == 9 equates to Resolution == 1080 -- ID 3 == 1 equates to FPS == 120 - -{% quiz - question="How many packets are query responses?" - option="A:::Always 1 packet" - option="B:::Always multiple packets" - option="C:::Always 1 packet except for complex responses" - option="D:::Can be 1 or multiple packets" - correct="D" - info="Query responses can be one packet (if for example querying a specific -setting) or multiple packets (when querying many or all settings as in the example here). -See the next tutorial for more information on queries." -%} +**Quiz time! 📚 ✏️** {% quiz - question="Which field is not common to all responses?" - option="A:::length" - option="B:::status" - option="C:::ID" - option="D:::None of the Above" - correct="D" - info="Query responses can be one packet (if for example querying a specific -setting) or multiple packets (when querying many or all settings as in the example here). -See the next tutorial for more information on queries." + question="How can we know that a response has been completely received?" + option="A:::The stop bit will be set in the header" + option="B:::The response has accumulated length bytes" + option="C:::By checking for the end of frame (EOF) sentinel character" + correct="B" + info="The length of the entire response is parsed from the first packet. We + then accumulate packets, keeping track of the received length, until all of the bytes + have been received. A and C are just made up 😜." %} # Troubleshooting @@ -1015,9 +935,9 @@ See the first tutorial's Congratulations 🤙 {% endsuccess %} -You can now parse any TLV response that is received from the GoPro, at least if it is received uninterrupted. +You now know how to accumulate TLV responses that are received from the GoPro, at least if they are received uninterrupted. There is additional logic required for a complete solution such as checking the UUID the response is received on and storing a dict of response per UUID. At the current time, this endeavor is left for the reader. For a complete example of this, see the [Open GoPro Python SDK](https://gopro.github.io/OpenGoPro/python_sdk/). -To learn more about queries, go to the next tutorial. \ No newline at end of file +To learn about a different type of operation (Queries), go to the next tutorial. diff --git a/docs/_tutorials/tutorial_4_ble_queries/tutorial.md b/docs/_tutorials/tutorial_4_ble_queries/tutorial.md index bae4950d..c9d78edf 100644 --- a/docs/_tutorials/tutorial_4_ble_queries/tutorial.md +++ b/docs/_tutorials/tutorial_4_ble_queries/tutorial.md @@ -5,18 +5,31 @@ sidebar: lesson: 4 --- -# Tutorial 4: BLE Queries +# Tutorial 4: BLE TLV Queries -This document will provide a walk-through tutorial to implement the -[Open GoPro Interface](/ble/index.html) to query the camera's setting and status -information via BLE. +This document will provide a walk-through tutorial to use the Open GoPro Interface to query the camera's setting +and status information via BLE. -"Queries" in this sense are specifically procedures that: +[Queries]({{site.baseurl}}/ble/protocol/data_protocol.html#queries) in this sense are operations that are initiated by +writing to the Query [UUID]({{site.baseurl}}/ble/protocol/ble_setup.html#ble-characteristics) and receiving responses +via the Query Response [UUID]({{site.baseurl}}/ble/protocol/ble_setup.html#ble-characteristics). -- are initiated by writing to the Query UUID -- receive responses via the Query Response UUID. +A list of queries can be found in the [Query ID Table](http://localhost:4998/ble/protocol/id_tables.html#query-ids). -This will be described in more detail below. +It is important to distinguish between queries and +[commands]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}) because they each have different request +and response packet formats. + +{% note %} +This tutorial only considers sending these queries as one-off queries. That is, it does not consider state +management / synchronization when sending multiple queries. This will be discussed in a future lab. +{% endnote %} + +# Requirements + +It is assumed that the hardware and software requirements from the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}) +are present and configured correctly. {% tip %} It is suggested that you have first completed the @@ -26,38 +39,30 @@ It is suggested that you have first completed the through this tutorial. {% endtip %} -This tutorial only considers sending these queries as one-off commands. That is, it does not consider state -management / synchronization when sending multiple commands. This will be discussed in a future lab. - -# Requirements - -It is assumed that the hardware and software requirements from the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}) -are present and configured correctly. - # Just Show me the Demo(s)!! {% linkedTabs demo %} {% tab demo python %} -Each of the scripts for this tutorial can be found in the Tutorial 2 +Each of the scripts for this tutorial can be found in the Tutorial 4 [directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_4_ble_queries/). {% warning %} -Python >= 3.8.x must be used as specified in the requirements +Python >= 3.9 and < 3.12 must be used as specified in the requirements {% endwarning %} {% accordion Individual Query Poll %} You can test an individual query poll with your camera through BLE using the following script: + ```console -$ python ble_command_poll_resolution_value.py +$ python ble_query_poll_resolution_value.py ``` See the help for parameter definitions: ```console -$ python ble_command_poll_resolution_value.py --help -usage: ble_command_poll_resolution_value.py [-h] [-i IDENTIFIER] +$ python ble_query_poll_resolution_value.py --help +usage: ble_query_poll_resolution_value.py [-h] [-i IDENTIFIER] Connect to a GoPro camera, get the current resolution, modify the resolution, and confirm the change was successful. @@ -67,21 +72,22 @@ optional arguments: Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to ``` -{% endaccordion %} +{% endaccordion %} {% accordion Multiple Simultaneous Query Polls %} You can test querying multiple queries simultaneously with your camera through BLE using the following script: + ```console -$ python ble_command_poll_multiple_setting_values.py +$ python ble_query_poll_multiple_setting_values.py ``` See the help for parameter definitions: ```console -$ python ble_command_poll_multiple_setting_values.py --help -usage: ble_command_poll_multiple_setting_values.py [-h] [-i IDENTIFIER] +$ python ble_query_poll_multiple_setting_values.py --help +usage: ble_query_poll_multiple_setting_values.py [-h] [-i IDENTIFIER] Connect to a GoPro camera then get the current resolution, fps, and fov. @@ -91,23 +97,26 @@ optional arguments: Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to ``` -{% endaccordion %} +{% endaccordion %} {% accordion Registering for Query Push Notifications %} -You can test registering for querties and receiving push notifications with your camera through BLE using the following script: +You can test registering for querties and receiving push notifications with your camera through BLE using the following +script: + ```console -$ python ble_command_register_resolution_value_updates.py +$ python ble_query_register_resolution_value_updates.py ``` See the help for parameter definitions: ```console -$ python ble_command_register_resolution_value_updates.py --help -usage: ble_command_register_resolution_value_updates.py [-h] [-i IDENTIFIER] +$ python ble_query_register_resolution_value_updates.py --help +usage: ble_query_register_resolution_value_updates.py [-h] [-i IDENTIFIER] -Connect to a GoPro camera, register for updates to the resolution, receive the current resolution, modify the resolution, and confirm receipt of the change notification. +Connect to a GoPro camera, register for updates to the resolution, receive the current resolution, modify the resolution, +and confirm receipt of the change notification. optional arguments: -h, --help show this help message and exit @@ -115,6 +124,7 @@ optional arguments: Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first discovered GoPro will be connected to ``` + {% endaccordion %} {% endtab %} {% tab demo kotlin %} @@ -136,57 +146,53 @@ This will start the tutorial and log to the screen as it executes. When the tuto # Setup We must first connect as was discussed in the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). -We will also be using the **Response** class that was defined in the -[parsing responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}) tutorial to accumulate -and parse notification responses to the Query Response -[characteristic](/ble/index.html#services-and-characteristics). -Throughout this tutorial, the query information that we will be reading is the Resolution Setting (ID 0x02). {% linkedTabs notification_handler %} {% tab notification_handler python %} -Therefore, we have slightly changed the notification handler to update a global resolution variable as it -queries the resolution: + +We have slightly updated the notification handler from the previous tutorial to handle a `QueryResponse` instead of +a `TlvResponse` where `QueryResponse` is a subclass of `TlvResponse` that will be created in this tutorial. ```python -def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: +responses_by_uuid = GoProUuid.dict_by_uuid(QueryResponse) +received_responses: asyncio.Queue[QueryResponse] = asyncio.Queue() + +query_request_uuid = GoProUuid.QUERY_REQ_UUID +query_response_uuid = GoProUuid.QUERY_RSP_UUID +setting_request_uuid = GoProUuid.SETTINGS_REQ_UUID +setting_response_uuid = GoProUuid.SETTINGS_RSP_UUID + +async def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(client.services.characteristics[characteristic.handle].uuid) + response = responses_by_uuid[uuid] response.accumulate(data) + # Notify the writer if we have received the entire response if response.is_received: - response.parse() + # If this is query response, it must contain a resolution value + if uuid is query_response_uuid: + logger.info("Received a Query response") + await received_responses.put(response) + # If this is a setting response, it will just show the status + elif uuid is setting_response_uuid: + logger.info("Received Set Setting command response.") + await received_responses.put(response) + # Anything else is unexpected. This shouldn't happen + else: + logger.error("Unexpected response") + # Reset per-uuid Response + responses_by_uuid[uuid] = QueryResponse(uuid) +``` - if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: - resolution = Resolution(response.data[RESOLUTION_ID][0]) +{% note %} +The code above is taken from `ble_query_poll_resolution_value.py` +{% endnote %} - # Notify writer that the procedure is complete - event.set() -``` {% endtab %} {% tab notification_handler kotlin %} -Therefore, we have slightly updated the notification handler to only handle query responses: -```kotlin -fun resolutionPollingNotificationHandler(characteristic: UUID, data: UByteArray) { - GoProUUID.fromUuid(characteristic)?.let { - // If response is currently empty, create a new one - response = response ?: Response.Query() // We're only handling queries in this tutorial - } ?: return // We don't care about non-GoPro characteristics (i.e. the BT Core Battery service) - - Timber.d("Received response on $characteristic: ${data.toHexString()}") - - response?.let { rsp -> - rsp.accumulate(data) - if (rsp.isReceived) { - rsp.parse() - - // If this is a query response, it must contain a resolution value - if (characteristic == GoProUUID.CQ_QUERY_RSP.uuid) { - Timber.i("Received resolution query response") - } - ... -``` - -We are also defining a resolution enum that will be updated as we receive new resolutions: +We are defining a resolution enum that will be updated as we receive new resolutions: ```kotlin private enum class Resolution(val value: UByte) { @@ -216,21 +222,136 @@ section: - [Polling Query Information]({% link _tutorials/tutorial_4_ble_queries/tutorial.md %}#polling-query-information) - [Registering for query push notifications]({% link _tutorials/tutorial_4_ble_queries/tutorial.md %}#registering-for-query-push-notifications) +# Parsing a Query Response + +Before sending queries, we must first describe how Query response parsing differs from the Command response parsing +that was introduced in the previous tutorial. + +To recap, the generic response format for both Commands and Queries is: + +| Header (length) | Operation ID (Command / Query ID) | Status | Response | +| --------------- | --------------------------------- | ------- | ---------------- | +| 1-2 bytes | 1 byte | 1 bytes | Length - 2 bytes | + +Query Responses contain an array of additional TLV groups in the **Response** field as such: + +| ID1 | Length1 | Value1 | ID2 | Length2 | Value 2 | ... | IDN | LengthN | ValueN | +| ------ | ------- | ------------- | ------ | ------- | ------------- | --- | ------ | ------- | ------------- | +| 1 byte | 1 byte | Length1 bytes | 1 byte | 1 byte | Length2 bytes | ... | 1 byte | 1 byte | LengthN bytes | + +We will be extending the `TlvResponse` class that was defined in the +[parsing responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}) tutorial to perform common +parsing shared among all queries into a `QueryResponse` class as seen below: + +--- + +
+
+ +We have already parsed the length, Operation ID, and status, and extracted the payload in the `TlvResponse` class. +The next step is to parse the payload. + +Therefore, we now continuously parse **Type (ID) - Length - Value** groups until we have consumed the response. We are +storing each value in a hash map indexed by ID for later access. + +{% linkedTabs tlv_query_response %} +{% tab tlv_query_response python %} + +```python +class QueryResponse(TlvResponse): + ... + + def parse(self) -> None: + super().parse() + buf = bytearray(self.payload) + while len(buf) > 0: + # Get ID and Length of query parameter + param_id = buf[0] + param_len = buf[1] + buf = buf[2:] + # Get the value + value = buf[:param_len] + # Store in dict for later access + self.data[param_id] = bytes(value) + + # Advance the buffer + buf = buf[param_len:] +``` + +{% endtab %} +{% tab tlv_query_response kotlin %} + +```kotlin +while (buf.isNotEmpty()) { + // Get each parameter's ID and length + val paramId = buf[0] + val paramLen = buf[1].toInt() + buf = buf.drop(2) + // Get the parameter's value + val paramVal = buf.take(paramLen) + // Store in data dict for access later + data[paramId] = paramVal.toUByteArray() + // Advance the buffer for continued parsing + buf = buf.drop(paramLen) +} +``` + +{% endtab %} +{% endlinkedTabs %} + +
+
+ +


+ +```mermaid! +graph TD + A[Parse Query ID] --> B[Parse Status] + B --> C{More data?} + C --> |yes|D[Get Value ID] + D --> E[Get Value Length] + E --> F[Get Value] + F --> C + C --> |no|G(done) +``` + +
+
+ +{% quiz + question="How many packets are query responses?" + option="A:::Always 1 packet" + option="B:::Always multiple packets" + option="C:::Can be 1 or multiple packets" + correct="C" + info="Query responses can be one packet (if for example querying a specific +setting) or multiple packets (when querying many or all settings as in the example here)." +%} + +{% quiz + question="Which field is not common to all TLV responses?" + option="A:::length" + option="B:::status" + option="C:::ID" + option="D:::None of the Above" + correct="D" + info="All Commands and Query responses have a length, ID, and status." +%} + # Polling Query Information -It is possible to poll one or more setting / status values using the following -[commands](/ble/index.html#query-commands): +It is possible to poll one or more setting / status values using the following queries: -| Query ID | Request | Query | -| -------- | -------------------- | ------------ | -| 0x12 | Get Setting value(s) | len:12:xx:xx | -| 0x13 | Get Status value(s) | len:13:xx:xx | +| Query ID | Request | Query | +| -------- | ------------------------------------------------------------------------------------- | ------------ | +| 0x12 | [Get Setting value(s)]({{site.baseurl}}/ble/features/query.html#get-setting-values) | len:12:xx:xx | +| 0x13 | [Get Status value(s)]({{site.baseurl}}/ble/features/query.html#get-status-values) | len:13:xx:xx | -where **xx** are setting / status ID(s) and **len** is the length of the rest of the query (the number of query bytes plus one for the request ID byte). -There will be specific examples below. +where **xx** are setting / status ID(s) and **len** is the length of the rest of the query (the number of query bytes +plus one for the request ID byte). There will be specific examples below. {% note %} -Since they are two separate commands, combination of settings / statuses can not be polled simultaneously. +Since they are two separate queries, combination of settings / statuses can not be polled simultaneously. {% endnote %} Here is a generic sequence diagram (the same is true for statuses): @@ -240,7 +361,7 @@ sequenceDiagram participant PC as Open GoPro user device participant GoPro note over PC, GoPro: Connected (steps from connect tutorial) - PC ->> GoPro: Get Setting value(s) command written to Query UUID + PC ->> GoPro: Get Setting value(s) queries written to Query UUID GoPro ->> PC: Setting values responded to Query Response UUID GoPro ->> PC: More setting values responded to Query Response UUID GoPro ->> PC: ... @@ -251,86 +372,81 @@ The number of notification responses will vary depending on the amount of settin Note that setting values will be combined into one notification until it reaches the maximum notification size (20 bytes). At this point, a new response will be sent. Therefore, it is necessary to accumulate and then parse these responses as was described in -[parsing query responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}#query-responses) +[parsing query responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}#parsing-a-query-response) ## Individual Query Poll Here we will walk through an example of polling one setting (Resolution). -First we send the query command: +First we send the query: {% linkedTabs individual_send %} {% tab individual_send python %} + +{% note %} The sample code can be found in in `ble_query_poll_resolution_value.py`. -Let's first define the UUID's to write to and receive from: +{% endnote %} ```python -QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") -QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077") +query_request_uuid = GoProUuid.QUERY_REQ_UUID +request = bytes([0x02, 0x12, RESOLUTION_ID]) +await client.write_gatt_char(query_request_uuid.value, request, response=True) ``` -Then actually send the command: - -```python -event.clear() -await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x12, RESOLUTION_ID])) -await event.wait() # Wait to receive the notification response -``` {% endtab %} {% tab individual_send kotlin %} + ```kotlin val pollResolution = ubyteArrayOf(0x02U, 0x12U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) ``` + {% endtab %} {% endlinkedTabs %} -When the response is received in / from the notification handler, we update the global resolution variable: +Then when the response is received from the notification handler we parse it into individual query elements in the +`QueryResponse` class and extract the new resolution value. {% linkedTabs individual_parse %} {% tab individual_parse python %} -```python -def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - response.accumulate(data) - # Notify the writer if we have received the entire response - if response.is_received: - response.parse() - - # If this is query response, it must contain a resolution value - if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: - resolution = Resolution(response.data[RESOLUTION_ID][0]) +```python +# Wait to receive the notification response +response = await received_responses.get() +response.parse() +resolution = Resolution(response.data[RESOLUTION_ID][0]) ``` which logs as such: ```console -INFO:root:Getting the current resolution -INFO:root:Received response at handle=62: b'05:12:00:02:01:09' -INFO:root:self.bytes_remaining=0 -INFO:root:Resolution is currently Resolution.RES_1080 +Getting the current resolution + Writing to GoProUuid.QUERY_REQ_UUID: 02:12:02 +Received response at handle=62: b'05:12:00:02:01:09' +eceived the Resolution Query response +Resolution is currently Resolution.RES_1080 ``` {% endtab %} {% tab individual_parse kotlin %} + ```kotlin // Wait to receive the response and then convert it to resolution -resolution = Resolution.fromValue( - receivedResponse.receive().data.getValue(RESOLUTION_ID).first() -) +val queryResponse = (receivedResponses.receive() as Response.Query).apply { parse() } +resolution = Resolution.fromValue(queryResponse.data.getValue(RESOLUTION_ID).first()) ``` which logs as such: ```console - Polling the current resolution +Polling the current resolution Writing characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b ==> 02:12:02 Wrote characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:12:00:02:01:04 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:12:00:02:01:04 +Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:12:00:02:01:09 +Received response on CQ_QUERY_RSP Received packet of length 5. 0 bytes remaining -Received resolution query response -Camera resolution is RES_2_7K +Received Query Response +Camera resolution is RES_1080 ``` {% endtab %} @@ -341,46 +457,62 @@ has changed: {% linkedTabs individual_verify %} {% tab individual_verify python %} + +```python +while resolution is not target_resolution: + request = bytes([0x02, 0x12, RESOLUTION_ID]) + await client.write_gatt_char(query_request_uuid.value, request, response=True) + response = await received_responses.get() # Wait to receive the notification response + response.parse() + resolution = Resolution(response.data[RESOLUTION_ID][0]) +``` + +which logs as such: + ```console -INFO:root:Changing the resolution to Resolution.RES_2_7K... -INFO:root:Received response at handle=57: b'02:02:00' -INFO:root:self.bytes_remaining=0 -INFO:root:Command sent successfully -INFO:root:Polling the resolution to see if it has changed... -INFO:root:Received response at handle=62: b'05:12:00:02:01:07' -INFO:root:self.bytes_remaining=0 -INFO:root:Resolution is currently Resolution.RES_2_7K +Changing the resolution to Resolution.RES_2_7K... +Writing to GoProUuid.SETTINGS_REQ_UUID: 03:02:01:04 +Writing to GoProUuid.SETTINGS_REQ_UUID: 03:02:01:04 +Received response at GoProUuid.SETTINGS_RSP_UUID: 02:02:00 +Received Set Setting command response. +Polling the resolution to see if it has changed... +Writing to GoProUuid.QUERY_REQ_UUID: 02:12:02 +Received response at GoProUuid.QUERY_RSP_UUID: 05:12:00:02:01:04 +Received the Resolution Query response +Resolution is currently Resolution.RES_2_7K +Resolution has changed as expected. Exiting... ``` {% endtab %} {% tab individual_verify kotlin %} + ```kotlin - while (resolution != newResolution) { - ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) - resolution = Resolution.fromValue( - receivedResponse.receive().data.getValue(RESOLUTION_ID).first() - ) - Timber.i("Camera resolution is currently $resolution") - } +while (resolution != newResolution) { + ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, pollResolution) + val queryNotification = (receivedResponses.receive() as Response.Query).apply { parse() } + resolution = Resolution.fromValue(queryNotification.data.getValue(RESOLUTION_ID).first()) +} ``` which logs as such: ```console -Changing the resolution to RES_1080 -Writing characteristic b5f90074-aa8d-11e3-9046-0002a5d5c51b ==> 03:02:01:09 +Changing the resolution to RES_2_7K +Writing characteristic b5f90074-aa8d-11e3-9046-0002a5d5c51b ==> 03:02:01:04 Wrote characteristic b5f90074-aa8d-11e3-9046-0002a5d5c51b Characteristic b5f90075-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:02:00 -Received response on b5f90075-aa8d-11e3-9046-0002a5d5c51b: 02:02:00 -Command sent successfully +Received response on CQ_SETTING_RSP +Received packet of length 2. 0 bytes remaining +Received set setting response. Resolution successfully changed Polling the resolution until it changes Writing characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b ==> 02:12:02 -Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:12:00:02:01:09 -Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:12:00:02:01:09 -Received resolution query response +Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:12:00:02:01:04 +Received response on CQ_QUERY_RSP +Received packet of length 5. 0 bytes remaining +Received Query Response Wrote characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b -Camera resolution is currently RES_1080 +Camera resolution is currently RES_2_7K ``` {% endtab %} @@ -389,19 +521,24 @@ Camera resolution is currently RES_1080 ## Multiple Simultaneous Query Polls Rather than just polling one setting, it is also possible to poll multiple settings. An example of this is shown -below. It is very similar to the previous example except for the following: - -The query command now includes 3 settings: Resolution, FPS, and FOV. +below. It is very similar to the previous example except that the query now includes 3 settings: +[Resolution]({{site.baseurl}}/ble/features/settings.html#setting-2), +[FPS]({{site.baseurl}}/ble/features/settings.html#setting-3), +and [FOV]({{site.baseurl}}/ble/features/settings.html#setting-121). {% linkedTabs multiple_send %} {% tab multiple_send python %} + ```python RESOLUTION_ID = 2 FPS_ID = 3 FOV_ID = 121 -await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x04, 0x12, RESOLUTION_ID, FPS_ID, FOV_ID])) +request = bytes([0x04, 0x12, RESOLUTION_ID, FPS_ID, FOV_ID]) +await client.write_gatt_char(query_request_uuid.value, request, response=True) +response = await received_responses.get() # Wait to receive the notification response ``` + {% endtab %} {% tab multiple_send kotlin %} TODO @@ -409,25 +546,21 @@ TODO {% endlinkedTabs %} {% note %} -The length (first byte of the command) has been increased to 4 to accommodate the extra settings +The length (first byte of the query) has been increased to 4 to accommodate the extra settings {% endnote %} We are also parsing the response to get all 3 values: {% linkedTabs multiple_parse %} {% tab multiple_parse python %} -```python -def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - response.accumulate(data) - if response.is_received: - response.parse() - - if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: - resolution = Resolution(response.data[RESOLUTION_ID][0]) - fps = FPS(response.data[FPS_ID][0]) - video_fov = VideoFOV(response.data[FOV_ID][0]) +```python +response.parse() +logger.info(f"Resolution is currently {Resolution(response.data[RESOLUTION_ID][0])}") +logger.info(f"Video FOV is currently {VideoFOV(response.data[FOV_ID][0])}") +logger.info(f"FPS is currently {FPS(response.data[FPS_ID][0])}") ``` + {% endtab %} {% tab multiple_parse kotlin %} TODO @@ -444,43 +577,52 @@ They are then printed to the log which will look like the following: {% linkedTabs multiple_print %} {% tab multiple_print python %} + ```console -INFO:root:Received response at handle=62: b'0b:12:00:02:01:07:03:01:01:79:01:00' -INFO:root:self.bytes_remaining=0 -INFO:root:Resolution is currently Resolution.RES_2_7K -INFO:root:Video FOV is currently VideoFOV.FOV_WIDE -INFO:root:FPS is currently FPS.FPS_120 +Getting the current resolution, fps, and fov. +Writing to GoProUuid.QUERY_REQ_UUID: 04:12:02:03:79 +Received response at GoProUuid.QUERY_RSP_UUID: 0b:12:00:02:01:09:03:01:00:79:01:00 +Received the Query Response +Resolution is currently Resolution.RES_1080 +Video FOV is currently VideoFOV.FOV_WIDE +FPS is currently FPS.FPS_240 ``` + {% endtab %} {% tab multiple_print kotlin %} TODO {% endtab %} {% endlinkedTabs %} +In general, we can parse query values by looking at relevant documentation linked from the +[Setting]({{site.baseurl}}/ble/protocol/id_tables.html#setting-ids) or +[Status]({{site.baseurl}}/ble/protocol/id_tables.html#status-ids) ID tables. + +For example (for settings): + +- ID 2 == 9 equates to [Resolution]({{site.baseurl}}/ble/features/settings.html#setting-2) == 1080 +- ID 3 == 1 equates to [FPS]({{site.baseurl}}/ble/features/settings.html#setting-3) == 120 + ## Query All -It is also possible to query all settings / statuses by not passing any ID's into the the query command, i.e.: +It is also possible to query all settings / statuses by not passing any ID's into the the query, i.e.: | Query ID | Request | Query | | -------- | ---------------- | ----- | | 0x12 | Get All Settings | 01:12 | | 0x13 | Get All Statuses | 01:13 | -An example of this can be seen in the -[parsing query responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}#query-responses) -tutorial - **Quiz time! 📚 ✏️** {% quiz - question="How can we poll the encoding status and the resolution setting using one command?" - option="A:::Concatenate a 'Get Setting Value' command and a 'Get Status' command with the relevant ID's" - option="B:::Concatenate the 'Get All Setting' and 'Get All Status' commands." + question="How can we poll the encoding status and the resolution setting using one query?" + option="A:::Concatenate a 'Get Setting Value' query and a 'Get Status' query with the relevant ID's" + option="B:::Concatenate the 'Get All Setting' and 'Get All Status' queries." option="C:::It is not possible" correct="C" - info="It is not possible to concatenate commands. This would result in an unknown sequence of bytes + info="It is not possible to concatenate queries. This would result in an unknown sequence of bytes from the camera's perspective. So it is not possible to get a setting value and a status value in one - command. The Get Setting command (with resolution ID) and Get Status command(with encoding ID) must be + query. The Get Setting Query (with resolution ID) and Get Status Query (with encoding ID) must be sent sequentially in order to get this information." %} @@ -489,18 +631,18 @@ tutorial Rather than polling the query information, it is also possible to use an interrupt scheme to register for push notifications when the relevant query information changes. -The relevant [commands](/ble/index.html#query-commands) are: +The relevant queries are: -| Query ID | Request | Query | -| -------- | --------------------------------- | ------------ | -| 0x52 | Register updates for setting(s) | len:52:xx:xx | -| 0x53 | Register updates for status(es) | len:53:xx:xx | -| 0x72 | Unregister updates for setting(s) | len:72:xx:xx | -| 0x73 | Unregister updates for status(es) | len:73:xx:xx | +| Query ID | Request | Query | +| -------- | -------------------------------------------------------------------------------------------------------------------- | ------------ | +| 0x52 | [Register updates for setting(s)]({{site.baseurl}}/ble/features/query.html#register-for-setting-value-updates) | len:52:xx:xx | +| 0x53 | [Register updates for status(es)]({{site.baseurl}}/ble/features/query.html#register-for-status-value-updates) | len:53:xx:xx | +| 0x72 | [Unregister updates for setting(s)]({{site.baseurl}}/ble/features/query.html#unregister-for-setting-value-updates) | len:72:xx:xx | +| 0x73 | [Unregister updates for status(es)]({{site.baseurl}}/ble/features/query.html#unregister-for-status-value-updates) | len:73:xx:xx | where **xx** are setting / status ID(s) and **len** is the length of the rest of the query (the number of query bytes plus one for the request ID byte). -The Query ID's for push notification responses are as follows: +The [Query ID's]({{site.baseurl}}/ble/protocol/id_tables.html#query-ids) for push notification responses are as follows: | Query ID | Response | | -------- | ------------------------------- | @@ -535,7 +677,7 @@ That is, after registering for push notifications for a given query, notificatio be sent whenever the query changes until the client unregisters for push notifications for the given query. {% tip %} -The initial response to the Register command also contains the current setting / status value. +The initial response to the Register query also contains the current setting / status value. {% endtip %} We will walk through an example of this below: @@ -544,25 +686,18 @@ First, let's register for updates when the resolution setting changes: {% linkedTabs register_register %} {% tab register_register python %} -First, let's define the UUID's we will be using: - -```python -SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074") -SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075") -QUERY_REQ_UUID = GOPRO_BASE_UUID.format("0076") -QUERY_RSP_UUID = GOPRO_BASE_UUID.format("0077") -``` - -Then, let's send the register BLE message... ```python -event.clear() -await client.write_gatt_char(QUERY_REQ_UUID, bytearray([0x02, 0x52, RESOLUTION_ID])) -await event.wait() # Wait to receive the notification response +query_request_uuid = GoProUuid.QUERY_REQ_UUID +request = bytes([0x02, 0x52, RESOLUTION_ID]) +await client.write_gatt_char(query_request_uuid.value, request, response=True) +# Wait to receive the notification response +response = await received_responses.get() ``` {% endtab %} {% tab register_register kotlin %} + ```kotlin val registerResolutionUpdates = ubyteArrayOf(0x02U, 0x52U, RESOLUTION_ID) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, registerResolutionUpdates) @@ -572,50 +707,36 @@ ble.writeCharacteristic(goproAddress, GoProUUID.CQ_QUERY.uuid, registerResolutio {% endlinkedTabs %} and parse its response (which includes the current resolution value). This is very similar to the polling -example with the exception that the Query ID is now 0x52 (Register Updates for Settings). This can be seen in -the raw byte data as well as by inspecting the response's `id` property. +example with the exception that the Query ID is now 0x52 +([Register Updates for Settings]({{site.baseurl}}/ble/features/query.html#register-for-setting-value-updates)). +This can be seen in the raw byte data as well as by inspecting the response's `id` property. {% linkedTabs register_parse %} {% tab register_parse python %} -```python -def notification_handler(characteristic: BleakGATTCharacteristic, data: bytes) -> None: - logger.info(f'Received response at handle {characteristic.handle}: {data.hex(":")}') - - response.accumulate(data) - # Notify the writer if we have received the entire response - if response.is_received: - response.parse() - - # If this is query response, it must contain a resolution value - if client.services.characteristics[characteristic.handle].uuid == QUERY_RSP_UUID: - global resolution - resolution = Resolution(response.data[RESOLUTION_ID][0]) +```python +response.parse() +resolution = Resolution(response.data[RESOLUTION_ID][0]) +logger.info(f"Resolution is currently {resolution}") ``` This will show in the log as such: ```console -INFO:root:Registering for resolution updates -INFO:root:Received response at handle=62: b'05:52:00:02:01:07' -INFO:root:self.bytes_remaining=0 -INFO:root:Successfully registered for resolution value updates. -INFO:root:Resolution is currently Resolution.RES_2_7K +Registering for resolution updates +Writing to GoProUuid.QUERY_REQ_UUID: 02:52:02 +Received response at GoProUuid.QUERY_RSP_UUID: 05:52:00:02:01:09 +Received the Resolution Query response +Successfully registered for resolution value updates +Resolution is currently Resolution.RES_1080 ``` + {% endtab %} {% tab register_parse kotlin %} -```kotlin -fun resolutionRegisteringNotificationHandler(characteristic: UUID, data: UByteArray) { - ... - if (rsp.isReceived) { - rsp.parse() - - if (characteristic == GoProUUID.CQ_QUERY_RSP.uuid) { - Timber.i("Received resolution query response") - resolution = Resolution.fromValue(rsp.data.getValue(RESOLUTION_ID).first()) - Timber.i("Resolution is now $resolution") - ... +```kotlin +val queryResponse = (receivedResponses.receive() as Response.Query).apply { parse() } +resolution = Resolution.fromValue(queryResponse.data.getValue(RESOLUTION_ID).first()) ``` This will show in the log as such: @@ -624,34 +745,65 @@ This will show in the log as such: Registering for resolution value updates Writing characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b ==> 02:52:02 Wrote characteristic b5f90076-aa8d-11e3-9046-0002a5d5c51b +Characteristic b5f90077-aa8d-11e3-9046-0002a5d5c51b changed | value: 05:52:00:02:01:04 +Received response on CQ_QUERY_RSP +Received packet of length 5. 0 bytes remaining +Received Query Response +Camera resolution is RES_2_7K ``` + {% endtab %} {% endlinkedTabs %} We are now successfully registered for resolution value updates and will receive push notifications whenever -the resolution changes. We verify this in the demo by then changing the resolution. +the resolution changes. We verify this in the demo by then changing the resolution and waiting to receive the update. +notification.. {% linkedTabs register_response %} {% tab register_response python %} + +```python +target_resolution = Resolution.RES_2_7K if resolution is Resolution.RES_1080 else Resolution.RES_1080 +request = bytes([0x03, 0x02, 0x01, target_resolution.value]) +await client.write_gatt_char(setting_request_uuid.value, request, response=True) +response = await received_responses.get() +response.parse() + +while resolution is not target_resolution: + request = bytes([0x02, 0x12, RESOLUTION_ID]) + await client.write_gatt_char(query_request_uuid.value, request, response=True) + response = await received_responses.get() # Wait to receive the notification response + response.parse() + resolution = Resolution(response.data[RESOLUTION_ID][0]) +``` + This will show in the log as such: ```console -INFO:root:Successfully changed the resolution -INFO:root:Received response at handle=62: b'05:92:00:02:01:09' -INFO:root:self.bytes_remaining=0 -INFO:root:Resolution is now Resolution.RES_1080 +Changing the resolution to Resolution.RES_2_7K... +Writing to GoProUuid.SETTINGS_REQ_UUID: 03:02:01:04 +Received response at GoProUuid.SETTINGS_RSP_UUID: 02:02:00 +Received Set Setting command response. +Waiting to receive new resolution +Received response at GoProUuid.QUERY_RSP_UUID: 05:92:00:02:01:04 +Received the Resolution Query response +Resolution is currently Resolution.RES_2_7K +Resolution has changed as expected. Exiting... ``` + {% endtab %} {% tab register_response kotlin %} + ```kotlin -val newResolution = if (resolution == Resolution.RES_2_7K) Resolution.RES_1080 else Resolution.RES_2_7K -val setResolution = ubyteArrayOf(0x03U, RESOLUTION_ID, 0x01U, newResolution.value) +val targetResolution = if (resolution == Resolution.RES_2_7K) Resolution.RES_1080 else Resolution.RES_2_7K +val setResolution = ubyteArrayOf(0x03U, RESOLUTION_ID, 0x01U, targetResolution.value) ble.writeCharacteristic(goproAddress, GoProUUID.CQ_SETTING.uuid, setResolution) -val setResolutionResponse = receivedResponse.receive() +val setResolutionResponse = (receivedResponses.receive() as Response.Tlv).apply { parse() } // Verify we receive the update from the camera when the resolution changes -while (resolution != newResolution) { - receivedResponse.receive() +while (resolution != targetResolution) { + val queryNotification = (receivedResponses.receive() as Response.Query).apply { parse() } + resolution = Resolution.fromValue(queryNotification.data.getValue(RESOLUTION_ID).first()) } ``` @@ -668,6 +820,7 @@ Received response on b5f90077-aa8d-11e3-9046-0002a5d5c51b: 05:92:00:02:01:04 Received resolution query response Resolution is now RES_2_7K ``` + {% endtab %} {% endlinkedTabs %} @@ -714,9 +867,3 @@ Congratulations 🤙 {% endsuccess %} You can now query any of the settings / statuses from the camera using one of the above patterns. - -If you have been following these tutorials in order, here is an extra 🥇🍾 **Congratulations** 🍰👍 because you have -completed all of the BLE tutorials. - -Next, to get started with WiFI (specifically to enable and connect to it), -proceed to the next tutorial. diff --git a/docs/_tutorials/tutorial_5_ble_protobuf/tutorial.md b/docs/_tutorials/tutorial_5_ble_protobuf/tutorial.md new file mode 100644 index 00000000..3990b78f --- /dev/null +++ b/docs/_tutorials/tutorial_5_ble_protobuf/tutorial.md @@ -0,0 +1,506 @@ +--- +permalink: '/tutorials/ble-protobuf' +sidebar: + nav: 'tutorials' +lesson: 5 +--- + +# Tutorial 5: BLE Protobuf Operations + +This document will provide a walk-through tutorial to use the Open GoPro Interface to send and receive BLE +[Protobuf]({{site.baseurl}}/ble/protocol/data_protocol.html#protobuf) Data. + +{% tip %} +Open GoPro uses [Protocol Buffers Version 2](https://protobuf.dev/reference/protobuf/proto2-spec/) +{% endtip %} + +A list of Protobuf Operations can be found in the +[Protobuf ID Table](http://localhost:4998/ble/protocol/id_tables.html#protobuf-ids). + +{% note %} +This tutorial only considers sending these as one-off operations. That is, it does not consider state +management / synchronization when sending multiple operations. This will be discussed in a future lab. +{% endnote %} + +# Requirements + +It is assumed that the hardware and software requirements from the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}) are present and configured correctly. + +{% tip %} +It is suggested that you have first completed the +[connect]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements), +[sending commands]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}), and +[parsing responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}) tutorials before going +through this tutorial. +{% endtip %} + +# Just Show me the Demo(s)!! + +{% linkedTabs demo %} +{% tab demo python %} +Each of the scripts for this tutorial can be found in the Tutorial 5 +[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_2_send_ble_commands/). + +{% warning %} +Python >= 3.9 and < 3.12 must be used as specified in the requirements +{% endwarning %} + +{% accordion Protobuf Example %} + +You can see some basic Protobuf usage, independent of a BLE connection, in the following script: + +```console +$ python protobuf_example.py +``` + +{% endaccordion %} + +{% accordion Set Turbo Mode %} + +You can test sending Set Turbo Mode to your camera through BLE using the following script: + +```console +$ python set_turbo_mode.py +``` + +See the help for parameter definitions: + +```console +$ python set_turbo_mode.py --help +usage: set_turbo_mode.py [-h] [-i IDENTIFIER] + +Connect to a GoPro camera, send Set Turbo Mode and parse the response + +options: + -h, --help show this help message and exit + -i IDENTIFIER, --identifier IDENTIFIER + Last 4 digits of GoPro serial number, which is the last 4 digits of the default + camera SSID. If not used, first discovered GoPro will be connected to +``` + +{% endaccordion %} + +{% accordion Decipher Response Type %} + +TODO + +{% endaccordion %} + +{% endtab %} +{% tab demo kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +# Compiling Protobuf Files + +The Protobuf files used to compile source code for the Open GoPro Interface exist in the top-level +[protobuf](https://github.com/gopro/OpenGoPro/tree/main/protobuf) directory of the Open GoPro repository. + +It is mostly out of the scope of these tutorials to describe how to compile these since this process is clearly defined +in the per-language [Protobuf Tutorial](https://protobuf.dev/getting-started/). For the purposes of these tutorials +(and shared with the [Python SDK](https://gopro.github.io/OpenGoPro/python_sdk/)), the Protobuf files are compiled +using the Docker image defined in [.admin/proto_build](https://github.com/gopro/OpenGoPro/tree/main/.admin/proto_build). +This build process can be performed using `make protos` from the top level of this repo. + +{% note %} +This information is strictly explanatory. It is in no way necessary to (re)build the Protobuf files for these tutorials +as the pre-compiled Protobuf source code already resides in the same directory as this tutorial's example code. +{% endnote %} + +# Working with Protobuf Messages + +Let's first perform some basic serialization and deserialization of a Protobuf message. For this example, we are going +to use the [Set Turbo Transfer]({{site.baseurl}}/ble/features/control.html#set-turbo-transfer) operation: + +{% include figure image_path="/assets/images/tutorials/protobuf_doc.png" alt="protobuf_doc" size="40%" caption="Set Turbo Mode Documentation" %} + +Per the documentation, this operation's request payload should be serialized using the Protobuf message which can be found +either in [Documentation]({{site.baseurl}}/ble/protocol/protobuf.html#proto-requestsetturboactive): + +{% include figure image_path="/assets/images/tutorials/protobuf_message_doc.png" alt="protobuf_message_doc" size="40%" caption="RequestSetTurboActive documentation" %} + +or [source code](https://github.com/gopro/OpenGoPro/blob/main/protobuf/turbo_transfer.proto): + +```proto +/** + * Enable/disable display of "Transferring Media" UI + * + * Response: @ref ResponseGeneric + */ +message RequestSetTurboActive { + required bool active = 1; // Enable or disable Turbo Transfer feature +} +``` + +{% note %} +This code can be found in `protobuf_example.py` +{% endnote %} + +## Protobuf Message Example + +First let's instantiate the request message by setting the `active` parameter and log the serialized bytes: + +{% tip %} +Your IDE should show the Protobuf Message's API signature since type stubs were generated when compiling the Protobuf files. +{% endtip %} + +{% linkedTabs import %} +{% tab import python %} + +```python +from tutorial_modules import proto + +request = proto.RequestSetTurboActive(active=False) +logger.info(f"Sending ==> {request}") +logger.info(request.SerializeToString().hex(":")) +``` + +which will log as such: + +```console +Sending ==> active: false +08:00 +``` + +{% endtab %} +{% tab import kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +We're not going to analyze these bytes since it is the purpose of the Protobuf framework is to abstract this. However it is +important to be able to generate the serialized bytes from the instantiated Protobuf Message object in order to send +the bytes via BLE. + +Similarly, let's now create a serialized response and show how to deserialize it into a +[ResponseGeneric]({{site.baseurl}}/ble/protocol/protobuf.html#responsegeneric) object. + +{% linkedTabs import %} +{% tab import python %} + +```python +response_bytes = proto.ResponseGeneric(result=proto.EnumResultGeneric.RESULT_SUCCESS).SerializeToString() +logger.info(f"Received bytes ==> {response_bytes.hex(':')}") +response = proto.ResponseGeneric.FromString(response_bytes) +logger.info(f"Received ==> {response}") +``` + +which will log as such: + +```console +Received bytes ==> 08:01 +Received ==> result: RESULT_SUCCESS +``` + +{% endtab %} +{% tab import kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +{% note %} +We're not hard-coding serialized bytes here since it may not be constant across Protobuf versions +{% endnote %} + +# Performing a Protobuf Operation + +Now let's actually perform a Protobuf Operation via BLE. First we need to discuss additional non-Protobuf-defined +header bytes that are required for Protobuf Operations in the Open GoPro Interface. + +## Protobuf Packet Format + +Besides having a compressed payload as defined per the [Protobuf Specification](https://protobuf.dev/), Open GoPro +Protobuf operations also are identified by "Feature" and "Action" IDs. The top level message format (not including +the [standard headers]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}#accumulating-the-response)) +is as follows: + +| Feature ID | Action ID | Serialized Protobuf Payload | +| ---------- | --------- | --------------------------- | +| 1 Byte | 1 Byte | Variable Length | + +This Feature / Action ID pair is used to identify the Protobuf Message that should be used to serialize / deserialize +the payload. This mapping can be found in the +[Protobuf ID Table](http://localhost:4998/ble/protocol/id_tables.html#protobuf-ids). + +## Protobuf Response Parser + +Since the parsing of Protobuf messages is different than +TLV Parsing, we need to create a +`ProtobufResponse` class by extending the `Response` class from the +[TLV Parsing Tutorial]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}). This `ProtobufResponse` +`parse` method will: + +1. Extract Feature and Action ID's +2. Parse the Protobuf payload using the specified Protobuf Message + +{% linkedTabs import %} +{% tab import python %} + +{% note %} +This code can be found in `set_turbo_mode.py` +{% endnote %} + +```python +class ProtobufResponse(Response): + ... + + def parse(self, proto: type[ProtobufMessage]) -> None: + self.feature_id = self.raw_bytes[0] + self.action_id = self.raw_bytes[1] + self.data = proto.FromString(bytes(self.raw_bytes[2:])) +``` + +{% endtab %} +{% tab import kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +The accumulation process is the same for TLV and Protobuf responses so have not overridden the base `Response` class's +`accumulation` method and we are using the same notification handler as previous labs. + +## Set Turbo Transfer + +Now let's perform the [Set Turbo Transfer]({{site.baseurl}}/ble/features/control.html#set-turbo-transfer) operation and +receive the response. First, we build the serialized byte request in the same manner as +[above]({% link _tutorials/tutorial_5_ble_protobuf/tutorial.md %}#working-with-protobuf-messages)), then prepend the +Feature ID, Action ID, and length bytes: + +{% linkedTabs import %} +{% tab import python %} + +```python +turbo_mode_request = bytearray( + [ + 0xF1, # Feature ID + 0x6B, # Action ID + *proto.RequestSetTurboActive(active=False).SerializeToString(), + ] +) +turbo_mode_request.insert(0, len(turbo_mode_request)) +``` + +{% endtab %} +{% tab import kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +We then send the message, wait to receive the response, and parse the response using the Protobuf Message specified +from the Set Turbo Mode Documentation: [ResponseGeneric]({{site.baseurl}}/ble/protocol/protobuf.html#responsegeneric). + +{% linkedTabs import %} +{% tab import python %} + +```python +await client.write_gatt_char(request_uuid.value, turbo_mode_request, response=True) +response = await received_responses.get() +response.parse(proto.ResponseGeneric) +assert response.feature_id == 0xF1 +assert response.action_id == 0xEB +logger.info(response.data) +``` + +which will log as such: + +```console +Setting Turbo Mode Off +Writing 04:f1:6b:08:00 to GoProUuid.COMMAND_REQ_UUID +Received response at UUID GoProUuid.COMMAND_RSP_UUID: 04:f1:eb:08:01 +Set Turbo Mode response complete received. +Successfully set turbo mode +result: RESULT_SUCCESS +``` + +{% endtab %} +{% tab import kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +# Deciphering Response Type + +This same procedure is used for all [Protobuf Operations](<(http://localhost:4998/ble/protocol/id_tables.html#protobuf-ids)>). +Coupled with the information from previous tutorials, you are now capable of parsing any response received from the +GoPro. + +However we have not yet covered how to decipher the response type: Command, Query, Protobuf, etc. The algorithm to do +so is defined in the +[GoPro BLE Spec]({{site.baseurl}}/ble/protocol/data_protocol.html#decipher-message-payload-type) and reproduced here for reference: + +{% include figure image_path="/assets/images/plantuml_ble_tlv_vs_protobuf.png" alt="Message Deciphering" size="70%" caption="Message Deciphering Algorithm" %} + +## Response Manager + +We're now going to create a monolithic `ResponseManager` class to implement this algorithm to perform (at least initial) +parsing of all response types: + +{% linkedTabs import %} +{% tab import python %} + +{% note %} +The sample code below is taken from `decipher_response.py` +{% endnote %} + +The `ResponseManager` is a wrapper around a `BleakClient` to manage accumulating, parsing, and retrieving responses. + +First, let's create a non-initialized response manager, connect to get a `BleakClient` and initialize the manager by +setting the client: + +```python +manager = ResponseManager() +manager.set_client(await connect_ble(manager.notification_handler, identifier)) +``` + +Then, in the notification handler, we "decipher" the response before enqueueing it to the received response queue: + +```python +async def notification_handler(self, characteristic: BleakGATTCharacteristic, data: bytearray) -> None: + uuid = GoProUuid(self.client.services.characteristics[characteristic.handle].uuid) + logger.debug(f'Received response at {uuid}: {data.hex(":")}') + + response = self._responses_by_uuid[uuid] + response.accumulate(data) + + # Enqueue if we have received the entire response + if response.is_received: + await self._q.put(self.decipher_response(response)) + # Reset the accumulating response + self._responses_by_uuid[uuid] = Response(uuid) +``` + +where "deciphering" is the implementation of the above algorithm: + +```python +def decipher_response(self, undeciphered_response: Response) -> ConcreteResponse: + payload = undeciphered_response.raw_bytes + # Are the first two payload bytes a real Fetaure / Action ID pair? + if (index := ProtobufId(payload[0], payload[1])) in ProtobufIdToMessage: + if not (proto_message := ProtobufIdToMessage.get(index)): + # We've only added protobuf messages for operations used in this tutorial. + raise RuntimeError( + f"{index} is a valid Protobuf identifier but does not currently have a defined message." + ) + else: + # Now use the protobuf messaged identified by the Feature / Action ID pair to parse the remaining payload + response = ProtobufResponse.from_received_response(undeciphered_response) + response.parse(proto_message) + return response + # TLV. Should it be parsed as Command or Query? + if undeciphered_response.uuid is GoProUuid.QUERY_RSP_UUID: + # It's a TLV query + response = QueryResponse.from_received_response(undeciphered_response) + else: + # It's a TLV command / setting. + response = TlvResponse.from_received_response(undeciphered_response) + # Parse the TLV payload (query, command, or setting) + response.parse() + return response +``` + +{% warning %} +Only the minimal functionality needed for these tutorials have been added. For example, many Protobuf Feature / Action ID +pairs do not have corresponding Protobuf Messages defined. +{% endwarning %} + +{% endtab %} +{% tab import kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +After deciphering, the parsed method is placed in the response queue as a either a `TlvResponse`, `QueryResponse`, or +`ProtobufResponse`. + +## Examples of Each Response Type + +Now let's perform operations that will demonstrate each response type: + +{% linkedTabs import %} +{% tab import python %} + +```python +# TLV Command (Setting) +await set_resolution(manager) +# TLV Command +await get_resolution(manager) +# TLV Query +await set_shutter_off(manager) +# Protobuf +await set_turbo_mode(manager) +``` + +These four methods will perform the same functionality we've demonstrated in previous tutorials, now using our +`ResponseManager`. + +We'll walk through the `get_resolution` method here. First build the request and send it: + +```python +request = bytes([0x03, 0x02, 0x01, 0x09]) +request_uuid = GoProUuid.SETTINGS_REQ_UUID +await manager.client.write_gatt_char(request_uuid.value, request, response=True) +``` + +Then retrieve the response from the manager: + +```python +tlv_response = await manager.get_next_response_as_tlv() +logger.info(f"Set resolution status: {tlv_response.status}") +``` + +This logs as such: + +```console +Getting the current resolution +Writing to GoProUuid.QUERY_REQ_UUID: 02:12:02 +Received response at GoProUuid.QUERY_RSP_UUID: 05:12:00:02:01:09 +Received current resolution: Resolution.RES_1080 +``` + +Note that each example retrieves the parsed response from the manager via one of the following methods: + +- `get_next_response_as_tlv` +- `get_next_response_as_query` +- `get_next_response_as_response` + +{% tip %} +These are functionally the same as they just retrieve the next received response from the manager's queue and only +exist as helpers to simplify typing. +{% endtip %} + +{% endtab %} +{% tab import kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +# Troubleshooting + +See the first tutorial's +[troubleshooting section]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#troubleshooting). + +# Good Job! + +{% success %} +Congratulations 🤙 +{% endsuccess %} + +You can now accumulate, decipher, and parse any BLE response received from the GoPro. diff --git a/docs/_tutorials/tutorial_5_connect_wifi/tutorial.md b/docs/_tutorials/tutorial_5_connect_wifi/tutorial.md deleted file mode 100644 index ba37d08e..00000000 --- a/docs/_tutorials/tutorial_5_connect_wifi/tutorial.md +++ /dev/null @@ -1,370 +0,0 @@ ---- -permalink: '/tutorials/connect-wifi' -sidebar: - nav: 'tutorials' -lesson: 5 ---- - -# Tutorial 5: Connect WiFi - -This document will provide a walk-through tutorial to implement -the [Open GoPro Interface](/http) to enable the GoPro's WiFi Access Point (AP) so that it -can be connected to. It will also provide an example of connecting to the WiFi AP. - -{% tip %} -It is recommended that you have first completed the -[connecting]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}), -[sending commands]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}), and -[parsing responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}) tutorials before proceeding. -{% endtip %} - -# Requirements - -It is assumed that the hardware and software requirements from the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) -are present and configured correctly. - -The scripts that will be used for this tutorial can be found in the -[Tutorial 5 Folder](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_5_connect_wifi). - -# Just Show me the Demo(s)!! - -{% linkedTabs demo %} -{% tab demo python %} -Each of the scripts for this tutorial can be found in the Tutorial 2 -[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_5_connect_wifi/). - -{% warning %} -Python >= 3.8.x must be used as specified in the requirements -{% endwarning %} - -{% accordion Enable WiFi AP %} - -You can test querying the current Resolution on your camera through BLE using the following script: -```console -$ python wifi_enable.py -``` - -See the help for parameter definitions: - -```console -$ python wifi_enable.py --help -usage: wifi_enable.py [-h] [-i IDENTIFIER] [-t TIMEOUT] - -Connect to a GoPro camera via BLE, get WiFi info, and enable WiFi. - -optional arguments: - -h, --help show this help message and exit - -i IDENTIFIER, --identifier IDENTIFIER - Last 4 digits of GoPro serial number, which is the last 4 digits of the - default camera SSID. If not used, first discovered GoPro will be connected to - -t TIMEOUT, --timeout TIMEOUT - time in seconds to maintain connection before disconnecting. If not set, will - maintain connection indefinitely -``` -{% endaccordion %} - -{% endtab %} -{% tab demo kotlin %} -The Kotlin file for this tutorial can be found on -[Github](https://github.com/gopro/OpenGoPro/tree/main/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial5ConnectWifi.kt). - -To perform the tutorial, run the Android Studio project, select "Tutorial 5" from the dropdown and click on "Perform." -This requires that a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. You can -check the BLE status at the top of the app. - -{% include figure image_path="/assets/images/tutorials/kotlin/tutorial_5.png" alt="kotlin_tutorial_5" size="40%" caption="Perform Tutorial 5" %} - -This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click -"Exit Tutorial" to return to the Tutorial selection screen. - -{% endtab %} -{% endlinkedTabs %} - -# Setup - -We must first connect to BLE as was discussed in the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). We are also -using the same notification handler as was used in the -[sending commands tutorial]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}#setup) - -# Connecting to WiFi AP - -Now that we are connected via BLE, paired, and have enabled notifications, we can send the command to enable -the WiFi AP. - -Here is an outline of the steps to do so: - -```mermaid! -sequenceDiagram - participant PC as Open GoPro user device - participant GoProBLE - participant GoProWiFi - loop Steps from Connect Tutorial - GoProBLE-->>PC: Advertising - GoProBLE-->>PC: Advertising - note over PC: Scanning - PC->>GoProBLE: Connect - note over GoProBLE, PC: Connected - alt If not Previously Paired - PC ->> GoProBLE: Pair Request - GoProBLE ->> PC: Pair Response - else - - end - note over GoProBLE, PC: Paired - PC ->> GoProBLE: Enable Notifications on Characteristic 1 - PC ->> GoProBLE: Enable Notifications on Characteristic 2 - PC ->> GoProBLE: Enable Notifications on Characteristic .. - PC ->> GoProBLE: Enable Notifications on Characteristic N - note over GoProBLE, PC: Ready to Communicate - end - PC ->> GoProBLE: Read Wifi AP SSID - PC ->> GoProBLE: Read Wifi AP Password - PC ->> GoProBLE: Write to Enable WiFi AP - GoProBLE ->> PC: Response sent as notification - note over GoProWiFi: WiFi AP enabled - PC ->> GoProWiFi: Connect to WiFi AP -``` - -Essentially we will be finding the WiFi AP information (SSID and password) via BLE, enabling the WiFi AP via -BLE, then connecting to the WiFi AP. - -## Find WiFi Information - -Note that the process to get this information is different than all procedures described up to this point. -Whereas the previous command, setting, and query procedures all followed the Write Request-Notification -Response pattern, the WiFi Information is retrieved via direct Read Requests to BLE characteristics. - -### Get WiFi SSID - -The WiFi SSID can be found by reading from the WiFi AP SSID -[characteristic](/ble/index.html#services-and-characteristics) of the -WiFi Access Point service. - -First, let's send the read request to get the SSID (and decode it into a string). - -{% linkedTabs get_ssid %} -{% tab get_ssid python %} -Let's define the attribute to read from: - -```python -WIFI_AP_SSID_UUID = GOPRO_BASE_UUID.format("0002") -``` - -Then send the BLE read request: - -```python -ssid = await client.read_gatt_char(WIFI_AP_SSID_UUID) -ssid = ssid.decode() -``` - -{% tip %} -There is no need for a synchronization event as the information is available when the `read_gatt_char` method -returns. -{% endtip %} - -In the demo, this information is logged as such: - -```console -INFO:root:Reading the WiFi AP SSID -INFO:root:SSID is GP24500456 -``` -{% endtab %} -{% tab get_ssid kotlin %} -```kotlin -ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_SSID.uuid).onSuccess { ssid = it.decodeToString() } -Timber.i("SSID is $ssid") -``` - -In the demo, this information is logged as such: - -```console -Getting the SSID -Read characteristic b5f90002-aa8d-11e3-9046-0002a5d5c51b : value: 64:65:62:75:67:68:65:72:6F:31:31 -SSID is debughero11 -``` -{% endtab %} -{% endlinkedTabs %} - -### Get WiFi Password - -The WiFi password can be found by reading from the WiFi AP password -[characteristic](/ble/index.html#services-and-characteristics) of the -WiFi Access Point service. - -First, let's send the read request to get the password (and decode it into a string). - -{% linkedTabs get_password %} -{% tab get_password python %} -Let's define the attribute to read from: - -```python -WIFI_AP_PASSWORD_UUID = GOPRO_BASE_UUID.format("0003") -``` -Then send the BLE read request: - -{% tip %} -There is no need for a synchronization event as the information is available when the `read_gatt_char` method -returns. -{% endtip %} - -In the demo, this information is logged as such: - -```console -INFO:root:Reading the WiFi AP password -INFO:root:Password is g@6-Tj9-C7K -``` -{% endtab %} -{% tab get_password kotlin %} -```kotlin -ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_PASSWORD.uuid).onSuccess { password = it.decodeToString() } -Timber.i("Password is $password") -``` - -In the demo, this information is logged as such: - -```console -Getting the password -Read characteristic b5f90003-aa8d-11e3-9046-0002a5d5c51b : value: 7A:33:79:2D:44:43:58:2D:50:68:6A -Password is z3y-DCX-Phj -``` -{% endtab %} -{% endlinkedTabs %} - -## Enable WiFi AP - -Before we can connect to the WiFi AP, we have to make sure it is enabled. This is accomplished by using the -"AP Control" [command](/ble/index.html#commands-quick-reference): - -| Command | Bytes | -| ------------------ | :-----------------: | -| Ap Control Enable | 0x03 0x17 0x01 0x01 | -| Ap Control Disable | 0x03 0x17 0x01 0x00 | - -This is done in the same manner that we did in the -[sending commands tutorial]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}). - -Now, let's write the bytes to the "Command Request UUID" to enable the WiFi AP! - -{% linkedTabs enable_ap_send %} -{% tab enable_ap_send python %} -```python -event.clear() -await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x03, 0x17, 0x01, 0x01])) -await event.wait() # Wait to receive the notification response -``` - -{% success %} -We make sure to clear the synchronization event before writing, then pend on the event until it is set in -the notification callback. -{% endsuccess %} -{% endtab %} -{% tab enable_ap_send kotlin %} -```kotlin -val enableWifiCommand = ubyteArrayOf(0x03U, 0x17U, 0x01U, 0x01U) -ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, enableWifiCommand) -receivedData.receive() -``` -{% endtab %} -{% endlinkedTabs %} - -Note that we have received the "Command Status" notification response from the -Command Response characteristic since we enabled it's notifications in -[Enable Notifications]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#enable-notifications). This can -be seen in the demo log: - -{% linkedTabs enable_ap_print %} -{% tab enable_ap_print python %} -```console -INFO:root:Enabling the WiFi AP -INFO:root:Received response at handle=52: b'02:17:00' -INFO:root:Command sent successfully -INFO:root:WiFi AP is enabled -``` -{% endtab %} -{% tab enable_ap_print kotlin %} -```console -Enabling the camera's Wifi AP -Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:17:01:01 -Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b -Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:17:00 -Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:17:00 -Command sent successfully -``` -{% endtab %} -{% endlinkedTabs %} - -As expected, the response was received on the correct handle and the status was "success". - -## Establish Connection to WiFi AP - -{% linkedTabs connect_wifi %} -{% tab connect_wifi python %} -If you have been following through the `ble_enable_wifi.py` script, you will notice that it ends here such that -we know the WiFi SSID and password and the WiFi AP is enabled and ready to connect to. This is because there -are many different methods of connecting to the WiFi AP depending on your OS and the framework you are -using to develop. You could, for example, simply use your OS's WiFi GUI to connect. - -{% tip %} -While out of the scope of these tutorials, there is a programmatic example of this in the cross-platform -`WiFi Demo` from the [Open GoPro Python SDK](https://gopro.github.io/OpenGoPro/python_sdk/quickstart.html#wifi-demo). -{% endtip %} - -{% endtab %} -{% tab connect_wifi kotlin %} -Using the passwsord and SSID we discovered above, we will now connect to the camera's network: - -```kotlin -wifi.connect(ssid, password) -``` - -This should show a system popup on your Android device that eventually goes away once the Wifi is -connected. - -{% note %} -This connection process appears to vary drastically in time. -{% endnote %} -{% endtab %} -{% endlinkedTabs %} - -**Quiz time! 📚 ✏️** - -{% quiz - question="How is the WiFi password response received?" - option="A:::As a read response from the WiFi AP Password characteristic" - option="B:::As write responses to the WiFi Request characteristic" - option="C:::As notifications of the Command Response characteristic" - correct="A" - info="This (and WiFi AP SSID) is an exception to the rule. Usually responses - are received as notifications to a response characteristic. However, in this case, it is - received as a direct read response (since we are reading from the characteristic and not - writing to it)." -%} - -{% quiz - question="Which of the following statements about the GoPro WiFi AP is true?" - option="A:::It only needs to be enabled once and it will then always remain on" - option="B:::The WiFi password will never change" - option="C:::The WiFi SSID will never change" - option="D:::None of the Above" - correct="D" - info="While the WiFi AP will remain on for some time, it can and will eventually turn off so - it is always recommended to first connect via BLE and ensure that it is enabled. The password - and SSID will almost never change. However, they will change if the connections are reset via - Connections->Reset Connections." -%} - -# Troubleshooting - -See the first tutorial's -[troubleshooting section]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#troubleshooting). - -# Good Job! - -{% success %} -Congratulations 🤙 -{% endsuccess %} - -You are now connected to the GoPro's Wifi AP and can send any of the HTTP commands defined in the -[Open GoPro Interface](/http). Proceed to the next tutorial. diff --git a/docs/_tutorials/tutorial_6_connect_wifi/tutorial.md b/docs/_tutorials/tutorial_6_connect_wifi/tutorial.md new file mode 100644 index 00000000..d3b5e273 --- /dev/null +++ b/docs/_tutorials/tutorial_6_connect_wifi/tutorial.md @@ -0,0 +1,765 @@ +--- +permalink: '/tutorials/connect-wifi' +sidebar: + nav: 'tutorials' +lesson: 6 +--- + +# Tutorial 6: Connect WiFi + +This document will provide a walk-through tutorial to use the Open GoPro Interface to connect the GoPro to a Wifi +network either in Access Point (AP) mode or Station (STA) Mode. + +{% tip %} +It is recommended that you have first completed the +[connecting BLE]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}), +[sending commands]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}), +[parsing responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}), and +[protobuf]({% link _tutorials/tutorial_5_ble_protobuf/tutorial.md %}) tutorials before proceeding. +{% endtip %} + +# Requirements + +It is assumed that the hardware and software requirements from the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) +are present and configured correctly. + +The scripts that will be used for this tutorial can be found in the +[Tutorial 6 Folder](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi). + +# Just Show me the Demo(s)!! + +{% linkedTabs demo %} +{% tab demo python %} +Each of the scripts for this tutorial can be found in the Tutorial 6 +[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_6_connect_wifi/). + +{% warning %} +Python >= 3.9 and < 3.12 must be used as specified in the requirements +{% endwarning %} + +{% accordion Enable WiFi AP %} + +You can enable the GoPro's Access Point to allow it accept Wifi connections as an Access Point via: + +```console +$ python wifi_enable.py +``` + +See the help for parameter definitions: + +```console +$ python wifi_enable.py --help +usage: enable_wifi_ap.py [-h] [-i IDENTIFIER] [-t TIMEOUT] + +Connect to a GoPro camera via BLE, get its WiFi Access Point (AP) info, and enable its AP. + +options: + -h, --help show this help message and exit + -i IDENTIFIER, --identifier IDENTIFIER + Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. If not used, first + discovered GoPro will be connected to + -t TIMEOUT, --timeout TIMEOUT + time in seconds to maintain connection before disconnecting. If not set, will maintain connection indefinitely +``` + +{% endaccordion %} + +{% accordion Connect GoPro as STA %} + +You can connect the GoPro to a Wifi network where the GoPro is in Station Mode (STA) via: + +```console +$ python connect_as_sta.py +``` + +See the help for parameter definitions: + +```console +$ python connect_as_sta.py --help +Connect the GoPro to a Wifi network where the GoPro is in Station Mode (STA). + +positional arguments: + ssid SSID of network to connect to + password Password of network to connect to + +options: + -h, --help show this help message and exit + -i IDENTIFIER, --identifier IDENTIFIER + Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera SSID. + If not used, first discovered GoPro will be connected to +``` + +{% endaccordion %} + +{% endtab %} +{% tab demo kotlin %} +The Kotlin file for this tutorial can be found on +[Github](https://github.com/gopro/OpenGoPro/tree/main/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial5ConnectWifi.kt). + +To perform the tutorial, run the Android Studio project, select "Tutorial 6" from the dropdown and click on "Perform." +This requires that a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. You can +check the BLE status at the top of the app. + +{% include figure image_path="/assets/images/tutorials/kotlin/tutorial_6.png" alt="kotlin_tutorial_6" size="40%" caption="Perform Tutorial 6" %} + +This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click +"Exit Tutorial" to return to the Tutorial selection screen. + +{% endtab %} +{% endlinkedTabs %} + +# Setup + +For both cases, we must first connect to BLE as was discussed in the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). + +# Access Point Mode (AP) + +In AP mode, the GoPro operates as an Access Point, allowing wireless clients to connect and communicate using the +Open GoPro [HTTP API]({{site.baseurl}}/http). The HTTP API provides much of the same functionality as the BLE API as +well as some additional functionality. For more information on the HTTP API, see the next 2 tutorials. + +```plantuml! +left to right direction +rectangle AccessPoint { + frame GoPro as gopro +} +component client +gopro <--[dashed]--> client: BLE +gopro <----> client: WiFi +``` + +In order to connect to the camera in AP mode, after connecting via BLE, pairing, and enabling notifications, we must: + +- find the GoPro's WiFi AP information (SSID and password) via BLE, +- enable the WiFi AP via BLE +- connect to the WiFi AP. + +Here is an outline of the steps to do so: + +```mermaid! +sequenceDiagram + participant PC as Open GoPro user device + participant GoProBLE + participant GoProWiFi + loop Steps from Connect Tutorial + GoProBLE-->>PC: Advertising + GoProBLE-->>PC: Advertising + note over PC: Scanning + PC->>GoProBLE: Connect + note over GoProBLE, PC: Connected + alt If not Previously Paired + PC ->> GoProBLE: Pair Request + GoProBLE ->> PC: Pair Response + else + + end + note over GoProBLE, PC: Paired + PC ->> GoProBLE: Enable Notifications on Characteristic 1 + PC ->> GoProBLE: Enable Notifications on Characteristic 2 + PC ->> GoProBLE: Enable Notifications on Characteristic .. + PC ->> GoProBLE: Enable Notifications on Characteristic N + note over GoProBLE, PC: Ready to Communicate + end + PC ->> GoProBLE: Read Wifi AP SSID + PC ->> GoProBLE: Read Wifi AP Password + PC ->> GoProBLE: Write to Enable WiFi AP + GoProBLE ->> PC: Response sent as notification + note over GoProWiFi: WiFi AP enabled + PC ->> GoProWiFi: Connect to WiFi AP +``` + +The following subsections will detail this process. + +## Find WiFi Information + +First we must find the target Wifi network's SSID and password. + +{% note %} +The process to get this information is different than all other BLE operations described up to this point. +Whereas the previous command, setting, and query operations all followed the Write Request-Notification +Response pattern, the WiFi Information is retrieved via direct Read Requests to BLE characteristics. +{% endnote %} + +### Get WiFi SSID + +The WiFi SSID can be found by reading from the WiFi AP SSID +[characteristic]({{site.baseurl}}/ble/protocol/ble_setup.html#ble-characteristics) of the +WiFi Access Point service. + +Let's send the read request to get the SSID and decode it into a string. + +{% linkedTabs get_ssid %} +{% tab get_ssid python %} + +```python +ssid_uuid = GoProUuid.WIFI_AP_SSID_UUID +logger.info(f"Reading the WiFi AP SSID at {ssid_uuid}") +ssid = (await client.read_gatt_char(ssid_uuid.value)).decode() +logger.info(f"SSID is {ssid}") +``` + +{% tip %} +There is no need for a synchronization event as the information is available when the `read_gatt_char` method +returns. +{% endtip %} + +In the demo, this information is logged as such: + +```console +Reading the WiFi AP SSID at GoProUuid.WIFI_AP_SSID_UUID +SSID is GP24500702 +``` + +{% endtab %} +{% tab get_ssid kotlin %} + +```kotlin +ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_SSID.uuid).onSuccess { ssid = it.decodeToString() } +Timber.i("SSID is $ssid") +``` + +In the demo, this information is logged as such: + +```console +Getting the SSID +Read characteristic b5f90002-aa8d-11e3-9046-0002a5d5c51b : value: 64:65:62:75:67:68:65:72:6F:31:31 +SSID is debughero11 +``` + +{% endtab %} +{% endlinkedTabs %} + +### Get WiFi Password + +The WiFi password can be found by reading from the WiFi AP password +[characteristic]({{site.baseurl}}/ble/protocol/ble_setup.html#ble-characteristics) of the +WiFi Access Point service. + +Let's send the read request to get the password and decode it into a string. + +{% linkedTabs get_password %} +{% tab get_password python %} + +```python +password_uuid = GoProUuid.WIFI_AP_PASSWORD_UUID +logger.info(f"Reading the WiFi AP password at {password_uuid}") +password = (await client.read_gatt_char(password_uuid.value)).decode() +logger.info(f"Password is {password}") +``` + +{% tip %} +There is no need for a synchronization event as the information is available when the `read_gatt_char` method +returns. +{% endtip %} + +In the demo, this information is logged as such: + +```console +Reading the WiFi AP password at GoProUuid.WIFI_AP_PASSWORD_UUID +Password is p@d-NNc-2ts +``` + +{% endtab %} +{% tab get_password kotlin %} + +```kotlin +ble.readCharacteristic(goproAddress, GoProUUID.WIFI_AP_PASSWORD.uuid).onSuccess { password = it.decodeToString() } +Timber.i("Password is $password") +``` + +In the demo, this information is logged as such: + +```console +Getting the password +Read characteristic b5f90003-aa8d-11e3-9046-0002a5d5c51b : value: 7A:33:79:2D:44:43:58:2D:50:68:6A +Password is z3y-DCX-Phj +``` + +{% endtab %} +{% endlinkedTabs %} + +## Enable WiFi AP + +Before we can connect to the WiFi AP, we have to make sure the access point is enabled. This is accomplished via the +[AP Control command]({{site.baseurl}}/ble/features/control.html#set-ap-control): + +| Command | Bytes | +| ------------------ | :-----------------: | +| Ap Control Enable | 0x03 0x17 0x01 0x01 | +| Ap Control Disable | 0x03 0x17 0x01 0x00 | + +{% tip %} +We are using the same notification handler that was defined in the +[sending commands tutorial]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}#setup). +{% endtip %} + +Let's write the bytes to the "Command Request UUID" to enable the WiFi AP! + +{% linkedTabs enable_ap_send %} +{% tab enable_ap_send python %} + +```python +event.clear() +request = bytes([0x03, 0x17, 0x01, 0x01]) +command_request_uuid = GoProUuid.COMMAND_REQ_UUID +await client.write_gatt_char(command_request_uuid.value, request, response=True) +await event.wait() # Wait to receive the notification response +``` + +{% success %} +We make sure to clear the synchronization event before writing, then pend on the event until it is set in +the notification callback. +{% endsuccess %} +{% endtab %} +{% tab enable_ap_send kotlin %} + +```kotlin +val enableWifiCommand = ubyteArrayOf(0x03U, 0x17U, 0x01U, 0x01U) +ble.writeCharacteristic(goproAddress, GoProUUID.CQ_COMMAND.uuid, enableWifiCommand) +receivedData.receive() +``` + +{% endtab %} +{% endlinkedTabs %} + +Note that we have received the "Command Status" notification response from the +Command Response characteristic since we enabled it's notifications in +[Enable Notifications]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#enable-notifications). This can +be seen in the demo log: + +{% linkedTabs enable_ap_print %} +{% tab enable_ap_print python %} + +```console +Enabling the WiFi AP +Writing to GoProUuid.COMMAND_REQ_UUID: 03:17:01:01 +Received response at GoProUuid.COMMAND_RSP_UUID: 02:17:00 +Command sent successfully +WiFi AP is enabled +``` + +{% endtab %} +{% tab enable_ap_print kotlin %} + +```console +Enabling the camera's Wifi AP +Writing characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b ==> 03:17:01:01 +Wrote characteristic b5f90072-aa8d-11e3-9046-0002a5d5c51b +Characteristic b5f90073-aa8d-11e3-9046-0002a5d5c51b changed | value: 02:17:00 +Received response on b5f90073-aa8d-11e3-9046-0002a5d5c51b: 02:17:00 +Command sent successfully +``` + +{% endtab %} +{% endlinkedTabs %} + +As expected, the response was received on the correct UUID and the status was "success". + +## Establish Connection to WiFi AP + +{% linkedTabs connect_wifi %} +{% tab connect_wifi python %} +If you have been following through the `ble_enable_wifi.py` script, you will notice that it ends here such that +we know the WiFi SSID / password and the WiFi AP is enabled. This is because there +are many different methods of connecting to the WiFi AP depending on your OS and the framework you are +using to develop. You could, for example, simply use your OS's WiFi GUI to connect. + +{% tip %} +While out of the scope of these tutorials, there is a programmatic example of this in the cross-platform +`WiFi Demo` from the [Open GoPro Python SDK](https://gopro.github.io/OpenGoPro/python_sdk/quickstart.html#wifi-demo). +{% endtip %} + +{% endtab %} +{% tab connect_wifi kotlin %} +Using the passwsord and SSID we discovered above, we will now connect to the camera's network: + +```kotlin +wifi.connect(ssid, password) +``` + +This should show a system popup on your Android device that eventually goes away once the Wifi is +connected. + +{% warning %} +This connection process appears to vary drastically in time. +{% endwarning %} +{% endtab %} +{% endlinkedTabs %} + +**Quiz time! 📚 ✏️** + +{% quiz + question="How is the WiFi password response received?" + option="A:::As a read response from the WiFi AP Password characteristic" + option="B:::As write responses to the WiFi Request characteristic" + option="C:::As notifications of the Command Response characteristic" + correct="A" + info="This (and WiFi AP SSID) is an exception to the rule. Usually responses + are received as notifications to a response characteristic. However, in this case, it is + received as a direct read response (since we are reading from the characteristic and not + writing to it)." +%} + +{% quiz + question="Which of the following statements about the GoPro WiFi AP is true?" + option="A:::It only needs to be enabled once and it will then always remain on" + option="B:::The WiFi password will never change" + option="C:::The WiFi SSID will never change" + option="D:::None of the Above" + correct="D" + info="While the WiFi AP will remain on for some time, it can and will eventually turn off so + it is always recommended to first connect via BLE and ensure that it is enabled. The password + and SSID will almost never change. However, they will change if the connections are reset via + Connections->Reset Connections." +%} + +You are now connected to the GoPro's Wifi AP and can send any of the HTTP commands defined in the +[HTTP Specification]({{site.baseurl}}/http). + +# Station (STA) Mode + +Station Mode is where the GoPro operates as a [Station](), allowing +the camera to connect to and communicate with an Access Point such as a switch or a router. This is used, for example, +in the livestreaming and [camera on the home network]({% link _tutorials/tutorial_9_cohn/tutorial.md %}) (COHN) features. + +```plantuml! +left to right direction +rectangle Station { + frame GoPro as gopro +} +component client +rectangle AccessPoint { + component router +} +gopro <--[dashed]--> client: BLE +gopro <----> router: Wifi +``` + +{% tip %} +When the GoPro is in Station Mode, there is no HTTP communication channel to the Open GoPro client. The GoPro can still +be controlled via BLE. +{% endtip %} + +In order to configure the GoPro in Station mode, after connecting via BLE, pairing, and enabling notifications, we must: + +- scan for available networks +- connect to a discovered network, using the correct API based on whether or not we have previously connected to this + network + +The following subsections will detail these steps. All of the Protobuf operations are performed in the same manner as +in the [protobuf tutorial]({% link _tutorials/tutorial_5_ble_protobuf/tutorial.md %}) such as reusing the `ResponseManager`. + +## Scan for Networks + +{% warning %} +It is always necessary to scan for networks, regardless of whether you already have a network's information and know it +is available. Failure to do so follows an untested and unsupported path in the GoPro's connection state machine. +{% endwarning %} + +The process of scanning for networks requires several Protobuf Operations as summarized here: + +{% include figure image_path="/assets/images/plantuml_ble_scan_for_ssids.png" alt="scan_for_ssids" size="70%" caption="Scan For Networks" %} + +First we must request the GoPro to +[Scan For Access Points]({{site.baseurl}}/ble/features/access_points.html#scan-for-access-points): + +{% linkedTabs scan_for_networks %} +{% tab scan_for_networks python %} + +{% note %} +The code here is taken from `connect_as_sta.py` +{% endnote %} + +Let's send the [scan request]({{site.baseurl}}//ble/protocol/protobuf.html#requeststartscan) and then retrieve and parse +[notifications]({{site.baseurl}}/ble/protocol/protobuf.html#notifstartscanning) until we receive a notification where the +`scanning_state` is set to [SCANNING_SUCCESS]({{site.baseurl}}/ble/protocol/protobuf.html#enumscanning). +Then we store the `scan id` from the notification for later use in retrieving the scan results. + +```python +start_scan_request = bytearray( + [ + 0x02, # Feature ID + 0x02, # Action ID + *proto.RequestStartScan().SerializePartialToString(), + ] +) +start_scan_request.insert(0, len(start_scan_request)) +await manager.client.write_gatt_char(GoProUuid.NETWORK_MANAGEMENT_REQ_UUID.value, start_scan_request, response=True) +while response := await manager.get_next_response_as_protobuf(): + ... + elif response.action_id == 0x0B: # Scan Notifications + scan_notification: proto.NotifStartScanning = response.data # type: ignore + logger.info(f"Received scan notification: {scan_notification}") + if scan_notification.scanning_state == proto.EnumScanning.SCANNING_SUCCESS: + return scan_notification.scan_id +``` + +This will log as such: + +```console +Scanning for available Wifi Networks +Writing: 02:02:02 +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 06:02:82:08:01:10:02 +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 0a:02:0b:08:05:10:01:18:05:20:01 +Received scan notification: scanning_state: SCANNING_SUCCESS + scan_id: 1 + total_entries: 5 + total_configured_ssid: 1 +``` + +{% endtab %} +{% tab scan_for_networks kotlin %} +TODO +{% endtab %} +{% endlinkedTabs %} + +Next we must request the GoPro to +[return the Scan Results]({{site.baseurl}}/ble/features/access_points.html#get-ap-scan-results). +Using the `scan_id` from above, let's send the +[Get AP Scan Results]({{site.baseurl}}/ble/features/access_points.html#get-ap-scan-results) request, then +retrieve and parse the response: + +{% linkedTabs scan_for_networks %} +{% tab scan_for_networks python %} + +```python +results_request = bytearray( + [ + 0x02, # Feature ID + 0x03, # Action ID + *proto.RequestGetApEntries(start_index=0, max_entries=100, scan_id=scan_id).SerializePartialToString(), + ] +) +results_request.insert(0, len(results_request)) +await manager.client.write_gatt_char(GoProUuid.NETWORK_MANAGEMENT_REQ_UUID.value, results_request, response=True) +response := await manager.get_next_response_as_protobuf(): +entries_response: proto.ResponseGetApEntries = response.data # type: ignore +logger.info("Found the following networks:") +for entry in entries_response.entries: + logger.info(str(entry)) +return list(entries_response.entries) +``` + +This will log as such: + +```console +Getting the scanned networks. +Writing: 08:02:03:08:00:10:64:18:01 +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 20:76:02:83:08:01:10:01:1a:13:0a:0a:64:61:62:75:67:64:61:62 +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 80:75:67:10:03:20:e4:28:28:2f:1a:13:0a:0a:41:54:54:54:70:34 +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 81:72:36:46:69:10:02:20:f1:2c:28:01:1a:13:0a:0a:41:54:54:62 +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 82:37:4a:67:41:77:61:10:02:20:99:2d:28:01:1a:16:0a:0d:52:69 +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 83:6e:67:20:53:65:74:75:70:20:65:37:10:01:20:ec:12:28:00:1a +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 84:17:0a:0e:48:6f:6d:65:79:6e:65:74:5f:32:47:45:58:54:10:01 +Received response at GoProUuid.NETWORK_MANAGEMENT_RSP_UUID: 85:20:85:13:28:01 +Found the following networks: + ssid: "dabugdabug" + signal_strength_bars: 3 + signal_frequency_mhz: 5220 + scan_entry_flags: 47 + ssid: "ATTTp4r6Fi" + signal_strength_bars: 2 + signal_frequency_mhz: 5745 + scan_entry_flags: 1 + ssid: "ATTb7JgAwa" + signal_strength_bars: 2 + signal_frequency_mhz: 5785 + scan_entry_flags: 1 + ssid: "Ring Setup e7" + signal_strength_bars: 1 + signal_frequency_mhz: 2412 + scan_entry_flags: 0 + ssid: "Homeynet_2GEXT" + signal_strength_bars: 1 + signal_frequency_mhz: 2437 + scan_entry_flags: 1 +``` + +{% endtab %} +{% tab scan_for_networks kotlin %} +TODO +{% endtab %} +{% endlinkedTabs %} + +At this point we have all of the discovered networks. Continue on to see how to use this information. + +## Connect to Network + +Depending on whether the GoPro has already connected to the desired network, we must next perform either the +[Connect]({{site.baseurl}}/ble/features/access_points.html#connect-to-provisioned-access-point) or +[Connect New]({{site.baseurl}}/ble/features/access_points.html#connect-to-a-new-access-point) operation. +This will be described below but first, a note on fragmentation: + +### GATT Write Fragmentation + +Up to this point in the tutorials, all of the operations we have been performing have resulted in GATT write requests +guaranteed to be less than maximum BLE packet size of 20 bytes. However, depending on the SSID and password used in +the Connect New operation, this maximum size might be surpassed. Therefore, it is necessary to fragment the payload. This +is essentially the inverse of the +[accumulation algorithm]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}#parsing-multiple-packet-tlv-responses). +We accomplish this as follows: + +{% linkedTabs fragment %} +{% tab fragment python %} + +Let's create a generator to yield fragmented packets (`yield_fragmented_packets`) from a monolithic payload. First, +depending on the length of the payload, we create the +[header]({{site.baseurl}}/ble/protocol/data_protocol.html#packet-headers) for the first packet that specifies the +total payload length: + +```python +if length < (2**5 - 1): + header = bytearray([length]) +elif length < (2**13 - 1): + header = bytearray((length | 0x2000).to_bytes(2, "big", signed=False)) +elif length < (2**16 - 1): + header = bytearray((length | 0x6400).to_bytes(2, "big", signed=False)) +``` + +Then we chunk through the payload, prepending either the above header for the first packet or the continuation header +for subsequent packets: + +```python +byte_index = 0 +while bytes_remaining := length - byte_index: + # If this is the first packet, use the appropriate header. Else use the continuation header + if is_first_packet: + packet = bytearray(header) + is_first_packet = False + else: + packet = bytearray(CONTINUATION_HEADER) + # Build the current packet + packet_size = min(MAX_PACKET_SIZE - len(packet), bytes_remaining) + packet.extend(bytearray(payload[byte_index : byte_index + packet_size])) + yield bytes(packet) + # Increment byte_index for continued processing + byte_index += packet_size +``` + +Finally we create a helper method that we can reuse throughout the tutorials to use this generator to send GATT Writes +using a given Bleak client: + +```python +async def fragment_and_write_gatt_char(client: BleakClient, char_specifier: str, data: bytes): + for packet in yield_fragmented_packets(data): + await client.write_gatt_char(char_specifier, packet, response=True) +``` + +{% endtab %} + +{% tab fragment kotlin %} +TODO +{% endtab %} +{% endlinkedTabs %} + +{% tip %} +The safest solution would be to always use the above fragmentation method. For the sake of simplicity in these tutorials, +we are only using this where there is a possibility of exceeding the maximum BLE packet size. +{% endtip %} + +### Connect Example + +In order to proceed, we must first inspect the scan result gathered from the previous section to see which +connect operation to use. Specifically we are checking the +[scan_entry_flags]({{site.baseurl}}/ble/protocol/protobuf.html#responsegetapentries-scanentry) to see if the +[SCAN_FLAG_CONFIGURED]({{site.baseurl}}/ble/protocol/protobuf.html#proto-enumscanentryflags) bit is set. If the +bit is set (and thus we have already provisioned this network) then we must use +[Connect]({{site.baseurl}}/ble/features/access_points.html#connect-to-provisioned-access-point) . Otherwise we must use +[Connect New]({{site.baseurl}}/ble/features/access_points.html#connect-to-a-new-access-point): + +{% linkedTabs inspect_scan_result %} +{% tab inspect_scan_result python %} + +```python +if entry.scan_entry_flags & proto.EnumScanEntryFlags.SCAN_FLAG_CONFIGURED: + connect_request = bytearray( + [ + 0x02, # Feature ID + 0x04, # Action ID + *proto.RequestConnect(ssid=entry.ssid).SerializePartialToString(), + ] + ) +else: + connect_request = bytearray( + [ + 0x02, # Feature ID + 0x05, # Action ID + *proto.RequestConnectNew(ssid=entry.ssid, password=password).SerializePartialToString(), + ] + ) +``` + +{% endtab %} +{% tab inspect_scan_result kotlin %} +TODO +{% endtab %} +{% endlinkedTabs %} + +Now that we have the correct request built, we can send it (using our newly created fragmentation method) we can send it. +Then we will continuously receive +[Provisioning Notifications]({{site.baseurl}}/ble/protocol/protobuf.html#notifprovisioningstate) which should be checked until +the `provisioning_state` is set to +[PROVISIONING_SUCCESS_NEW_AP]({{site.baseurl}}/ble/protocol/protobuf.html#proto-enumprovisioning). + +{% warning %} +The final `provisioning_state` that we are looking for is always `PROVISIONING_SUCCESS_NEW_AP` both in the Connect and +Connect New use cases. +{% endwarning %} + +The procedure is summarized here: + +{% include figure image_path="/assets/images/plantuml_ble_connect_ap.png" alt="connect_ap" size="60%" caption="Connect to Already Configured Network" %} + +{% linkedTabs send_Connect %} +{% tab send_Connect python %} + +```python +await fragment_and_write_gatt_char(manager.client, GoProUuid.NETWORK_MANAGEMENT_REQ_UUID.value, connect_request) +while response := await manager.get_next_response_as_protobuf(): + ... + elif response.action_id == 0x0C: # NotifProvisioningState Notifications + provisioning_notification: proto.NotifProvisioningState = response.data # type: ignore + if provisioning_notification.provisioning_state == proto.EnumProvisioning.PROVISIONING_SUCCESS_NEW_AP: + return +``` + +{% endtab %} +{% tab send_Connect kotlin %} +TODO +{% endtab %} +{% endlinkedTabs %} + +At this point, the GoPro is connect to the desired network in Station Mode! + +**Quiz time! 📚 ✏️** + +{% quiz + question="True or False: When the GoPro is in Station Mode, it can be communicated with via both BLE and HTTP." + option="A:::True" + option="B:::False" + correct="B" + info="When the GoPro is in station mode, it is connected via WiFi to another Access Point; not connected via Wifi + to you (the client). However, it is possible to maintain the BLE connection in STA mode so that you can still control + the GoPro." +%} + +# Troubleshooting + +See the first tutorial's +[BLE troubleshooting section]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#troubleshooting) to troubleshoot +BLE problems. + +# Good Job! + +{% success %} +Congratulations 🤙 +{% endsuccess %} + +You have now connected the GoPro to a WiFi network in either AP or STA mode. + +To see how to make use of AP mode, continue to the next tutorial. + +To see how make use of STA mode, continue to the +[camera on the home network tutorial]({% link _tutorials/tutorial_9_cohn/tutorial.md %}). diff --git a/docs/_tutorials/tutorial_6_send_wifi_commands/tutorial.md b/docs/_tutorials/tutorial_7_send_wifi_commands/tutorial.md similarity index 87% rename from docs/_tutorials/tutorial_6_send_wifi_commands/tutorial.md rename to docs/_tutorials/tutorial_7_send_wifi_commands/tutorial.md index d45c244a..3f5022b3 100644 --- a/docs/_tutorials/tutorial_6_send_wifi_commands/tutorial.md +++ b/docs/_tutorials/tutorial_7_send_wifi_commands/tutorial.md @@ -2,17 +2,17 @@ permalink: '/tutorials/send-wifi-commands' sidebar: nav: 'tutorials' -lesson: 6 +lesson: 7 --- -# Tutorial 6: Send WiFi Commands +# Tutorial 7: Send WiFi Commands -This document will provide a walk-through tutorial to send Open GoPro -[HTTP commands](/http) to the GoPro. +This document will provide a walk-through tutorial to perform Open GoPro [HTTP Operations]({{site.baseurl}}/http) +with the GoPro. {% tip %} It is suggested that you have first completed the -[Connecting to Wifi]({% link _tutorials/tutorial_5_connect_wifi/tutorial.md %}) tutorial. +[Connecting to Wifi]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}) tutorial. {% endtip %} This tutorial only considers sending these commands as one-off commands. That is, it does not consider state management / @@ -20,35 +20,38 @@ synchronization when sending multiple commands. This will be discussed in a futu There are two types of responses that can be received from the HTTP commands: JSON and binary. This section will deal with commands that return JSON responses. For commands with binary responses (as well as commands with -JSON responses that work with the media list), see the [next tutorial]({% link _tutorials/tutorial_7_camera_media_list/tutorial.md %}). +JSON responses that work with the media list), see the +[next tutorial]({% link _tutorials/tutorial_8_camera_media_list/tutorial.md %}). # Requirements -It is assumed that the hardware and software requirements from the [connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) +It is assumed that the hardware and software requirements from the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) are present and configured correctly. The scripts that will be used for this tutorial can be found in the -[Tutorial 6 Folder](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands). +[Tutorial 7 Folder](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands). # Just Show me the Demo(s)!! {% linkedTabs demo %} {% tab demo python %} -Each of the scripts for this tutorial can be found in the Tutorial 2 -[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_6_send_wifi_commands/). +Each of the scripts for this tutorial can be found in the Tutorial 7 +[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_7_send_wifi_commands/). {% warning %} -Python >= 3.8.x must be used as specified in the requirements +Python >= 3.9 and < 3.12 must be used as specified in the requirements {% endwarning %} {% warning %} You must be connected to the camera via WiFi as stated in -[Tutorial 5]({% link _tutorials/tutorial_5_connect_wifi/tutorial.md %}#Establish Connection to WiFi APPermalink). +[Tutorial 5]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}#Establish Connection to WiFi APPermalink). {% endwarning %} -{% accordion Get State %} +{% accordion Get Camera State %} You can test querying the state of your camera with HTTP over WiFi using the following script: + ```console $ python wifi_command_get_state.py ``` @@ -64,12 +67,13 @@ Get the state of the GoPro (status and settings). optional arguments: -h, --help show this help message and exit ``` -{% endaccordion %} +{% endaccordion %} {% accordion Preview Stream %} You can test enabling the UDP preview stream with HTTP over WiFi using the following script: + ```console $ python wifi_command_preview_stream.py ``` @@ -87,13 +91,13 @@ optional arguments: ``` Once enabled the stream can be viewed at `udp://@:8554` (For more details see the View Stream tab in the -[Preview Stream]({% link _tutorials/tutorial_6_send_wifi_commands/tutorial.md %}#preview-stream) section below. +[Preview Stream]({% link _tutorials/tutorial_7_send_wifi_commands/tutorial.md %}#preview-stream) section below. {% endaccordion %} - {% accordion Load Preset Group %} You can test sending the load preset group command with HTTP over WiFi using the following script: + ```console $ python wifi_command_load_group.py ``` @@ -109,11 +113,13 @@ Load the video preset group. optional arguments: -h, --help show this help message and exit ``` + {% endaccordion %} {% accordion Set Shutter %} You can test sending the Set Shutter command with HTTP over WiFi using the following script: + ```console $ python wifi_command_set_shutter.py ``` @@ -129,12 +135,13 @@ Take a 3 second video. optional arguments: -h, --help show this help message and exit ``` -{% endaccordion %} +{% endaccordion %} {% accordion Set Setting %} You can test setting the resolution setting with HTTP over WiFi using the following script: + ```console $ python wifi_command_set_resolution.py ``` @@ -150,6 +157,7 @@ Set the video resolution to 1080. optional arguments: -h, --help show this help message and exit ``` + {% endaccordion %} {% endtab %} @@ -157,14 +165,15 @@ optional arguments: The Kotlin file for this tutorial can be found on [Github](https://github.com/gopro/OpenGoPro/tree/main/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial6SendWifiCommands.kt). -To perform the tutorial, run the Android Studio project, select "Tutorial 6" from the dropdown and click on "Perform." +To perform the tutorial, run the Android Studio project, select "Tutorial 7" from the dropdown and click on "Perform." This requires: -- a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. -- a GoPro is already connected via Wifi, i.e. that Tutorial 5 was already run. + +- a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. +- a GoPro is already connected via Wifi, i.e. that Tutorial 5 was already run. You can check the BLE and Wifi statuses at the top of the app. -{% include figure image_path="/assets/images/tutorials/kotlin/tutorial_6.png" alt="kotlin_tutorial_6" size="40%" caption="Perform Tutorial 6" %} +{% include figure image_path="/assets/images/tutorials/kotlin/tutorial_7.png" alt="kotlin_tutorial_7" size="40%" caption="Perform Tutorial 7" %} This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click "Exit Tutorial" to return to the Tutorial selection screen. @@ -175,7 +184,7 @@ This will start the tutorial and log to the screen as it executes. When the tuto # Setup We must first connect to The GoPro's WiFi Access Point (AP) as was discussed in the -[Connecting to Wifi]({% link _tutorials/tutorial_5_connect_wifi/tutorial.md %}) tutorial. +[Connecting to Wifi]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}) tutorial. # Sending HTTP Commands with JSON Responses @@ -211,6 +220,7 @@ suspend fun get(endpoint: String, timeoutMs: Long = 5000L): JsonObject { return prettyJson.parseToJsonElement(bodyAsString).jsonObject } ``` + {% endtab %} {% endlinkedTabs %} @@ -237,13 +247,13 @@ sequenceDiagram deactivate GoPro ``` -## Get State +## Get Camera State The first command we will be sending is -[Get State](/http#commands-quick-reference). This command will +[Get Camera State]({{site.baseurl}}/http#tag/Query/operation/OGP_GET_STATE). This command will return all of the current settings and values. It is basically a combination of the -[Get All Settings]({% link _tutorials/tutorial_4_ble_queries/tutorial.md %}#query-all) and -[Get All Statuses]({% link _tutorials/tutorial_4_ble_queries/tutorial.md %}#query-all) +[Get All Settings]({{site.baseurl}}/ble/features/query.html#get-setting-values) and +[Get All Statuses]({{site.baseurl}}/ble/features/query.html#get-status-values) commands that were sent via BLE. Since there is no way to query individual settings / statuses via WiFi (or register for asynchronous notifications when they change), this is the only option to query setting / status information via WiFi. @@ -252,11 +262,12 @@ The command writes to the following endpoint: `/gopro/camera/state` -Let's build the endpoint then send the GET request and check the response for errors. +Let's build the endpoint then perform the GET operation and check the response for errors. Any errors will raise an exception. {% linkedTabs get_state_send %} {% tab get_state_send python %} + ```python url = GOPRO_BASE_URL + "/gopro/camera/state" ``` @@ -265,11 +276,14 @@ url = GOPRO_BASE_URL + "/gopro/camera/state" response = requests.get(url) response.raise_for_status() ``` + {% endtab %} {% tab get_state_send kotlin %} + ```kotlin var response = wifi.get(GOPRO_BASE_URL + "gopro/camera/state") ``` + {% endtab %} {% endlinkedTabs %} @@ -277,6 +291,7 @@ Lastly, we print the response's JSON data: {% linkedTabs get_state_print %} {% tab get_state_print python %} + ```python logger.info(f"Response: {json.dumps(response.json(), indent=4)}") ``` @@ -317,8 +332,10 @@ INFO:root:Response: { "41": 9, "42": 5, ``` + {% endtab %} {% tab get_state_print kotlin %} + ```kotlin Timber.i(prettyJson.encodeToString(response)) ``` @@ -366,8 +383,8 @@ GET request to: http://10.5.5.9:8080/gopro/camera/state {% endtab %} {% endlinkedTabs %} -We can see what each of these values mean by looking at the -[Open GoPro Interface](/ble/index.html#settings-quick-reference). +We can see what each of these values mean by looking at relevant documentation in the `settings` or `status` object of the +[State]({{site.baseurl}}/http#schema/State) schema. For example (for settings): @@ -377,7 +394,7 @@ For example (for settings): ## Load Preset Group The next command we will be sending is -[Load Preset Group](/ble/index.html#commands-quick-reference), which is used +[Load Preset Group]({{site.baseurl}}/http#tag/Presets/operation/OGP_PRESET_SET_GROUP), which is used to toggle between the 3 groups of presets (video, photo, and timelapse). The preset groups ID's are: | Command | Bytes | @@ -386,14 +403,9 @@ to toggle between the 3 groups of presets (video, photo, and timelapse). The pre | Load Photo Preset Group | 1001 | | Load Timelapse Preset Group | 1002 | -{% note %} -It is possible that the preset GroupID values will vary in future cameras. The only absolutely correct way to know -the preset ID is to read them from the "Get Preset Status" protobuf command. A future lab will discuss protobuf -commands. -{% endnote %} - {% linkedTabs load_preset_group_send %} {% tab load_preset_group_send python %} + ```python url = GOPRO_BASE_URL + "/gopro/camera/presets/set_group?id=1000" ``` @@ -402,11 +414,14 @@ url = GOPRO_BASE_URL + "/gopro/camera/presets/set_group?id=1000" response = requests.get(url) response.raise_for_status() ``` + {% endtab %} {% tab load_preset_group_send kotlin %} + ```kotlin response = wifi.get(GOPRO_BASE_URL + "gopro/camera/presets/load?id=1000") ``` + {% endtab %} {% endlinkedTabs %} @@ -414,6 +429,7 @@ Lastly, we print the response's JSON data: {% linkedTabs load_preset_group_print %} {% tab load_preset_group_print python %} + ```python logger.info(f"Response: {json.dumps(response.json(), indent=4)}") ``` @@ -425,8 +441,10 @@ INFO:root:Loading the video preset group: sending http://10.5.5.9:8080/gopro/cam INFO:root:Command sent successfully INFO:root:Response: {} ``` + {% endtab %} {% tab load_preset_group_print kotlin %} + ```kotlin Timber.i(prettyJson.encodeToString(response)) ``` @@ -456,12 +474,12 @@ this by seeing the preset name in the pill at bottom middle of the screen. ## Set Shutter -The next command we will be sending is -[Set Shutter](/http#commands-quick-reference). which is +The next command we will be sending is [Set Shutter]({{site.baseurl}}/http#tag/Control/operation/OGP_SHUTTER). which is used to start and stop encoding. {% linkedTabs set_shutter_send %} {% tab set_shutter_send python %} + ```python url = GOPRO_BASE_URL + f"/gopro/camera/shutter/start" ``` @@ -470,11 +488,14 @@ url = GOPRO_BASE_URL + f"/gopro/camera/shutter/start" response = requests.get(url) response.raise_for_status() ``` + {% endtab %} {% tab set_shutter_send kotlin %} + ```kotlin response = wifi.get(GOPRO_BASE_URL + "gopro/camera/shutter/start") ``` + {% endtab %} {% endlinkedTabs %} @@ -488,12 +509,15 @@ This will log as such: {% linkedTabs set_shutter_print %} {% tab set_shutter_print python %} + ```console INFO:root:Turning the shutter on: sending http://10.5.5.9:8080/gopro/camera/shutter/start INFO:root:Command sent successfully ``` + {% endtab %} {% tab set_shutter_print kotlin %} + ```kotlin Timber.i(prettyJson.encodeToString(response)) ``` @@ -519,13 +543,13 @@ attempt to do so will result in an error response. ## Set Setting -The next command will be sending is [Set Setting](/http#settings-quick-reference). +The next command will be sending is [Set Setting]({{site.baseurl}}/http#tag/settings). This end point is used to update all of the settings on the camera. It is analogous to BLE commands like [Set Video Resolution]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}#set-the-video-resolution). It is important to note that many settings are dependent on the video resolution (and other settings). For example, certain FPS values are not valid with certain resolutions. In general, higher resolutions -only allow lower FPS values. Check the [camera capabilities](/ble/index.html#camera-capabilities) +only allow lower FPS values. Check the [camera capabilities]({{site.baseurl}}/http#tag/settings/Capabilities) to see which settings are valid for given use cases. Let's build the endpoint first to set the Video Resolution to 1080 (the setting_id and option value comes from @@ -533,6 +557,7 @@ the command table linked above). {% linkedTabs set_setting_send %} {% tab set_setting_send python %} + ```python url = GOPRO_BASE_URL + f"/gopro/camera/setting?setting=2&option=9" ``` @@ -541,11 +566,14 @@ url = GOPRO_BASE_URL + f"/gopro/camera/setting?setting=2&option=9" response = requests.get(url) response.raise_for_status() ``` + {% endtab %} {% tab set_setting_send kotlin %} + ```kotlin response = wifi.get(GOPRO_BASE_URL + "gopro/camera/setting?setting=2&option=9") ``` + {% endtab %} {% endlinkedTabs %} @@ -553,6 +581,7 @@ Lastly, we print the response's JSON data: {% linkedTabs set_setting_print %} {% tab set_setting_print python %} + ```python logger.info(f"Response: {json.dumps(response.json(), indent=4)}") ``` @@ -567,6 +596,7 @@ INFO:root:Response: {} {% endtab %} {% tab set_setting_print kotlin %} + ```kotlin Timber.i(prettyJson.encodeToString(response)) ``` @@ -592,12 +622,12 @@ screen: {% include figure image_path="/assets/images/tutorials/video_resolution.png" alt="Video Resolution" size="50%" caption="Video Resolution" %} -As a reader exercise, try using the [Get State] command to verify that the resolution has changed. +As a reader exercise, try using the [Get Camera State](#get-camera-state) command to verify that the resolution has changed. ## Preview Stream The next command we will be sending is -[Preview Stream](/http#commands-quick-reference). This command will +[Start Preview Stream]({{site.baseurl}}/http#tag/Preview-Stream/operation/OGP_PREVIEW_STREAM_START). This command will enable (or disable) the preview stream . It is then possible to view the preview stream from a media player. The commands write to the following endpoints: @@ -612,6 +642,7 @@ Any errors will raise an exception. {% linkedTabs preview_stream_send %} {% tab preview_stream_send python %} + ```python url = GOPRO_BASE_URL + "/gopro/camera/stream/start" ``` @@ -620,6 +651,7 @@ url = GOPRO_BASE_URL + "/gopro/camera/stream/start" response = requests.get(url) response.raise_for_status() ``` + {% endtab %} {% tab preview_stream_send kotlin %} TODO @@ -642,6 +674,7 @@ INFO:root:Starting the preview stream: sending http://10.5.5.9:8080/gopro/camera INFO:root:Command sent successfully INFO:root:Response: {} ``` + {% endtab %} {% tab preview_stream_print kotlin %} TODO @@ -720,6 +753,6 @@ Congratulations 🤙 {% endsuccess %} You can now send any of the HTTP commands defined in the -[Open GoPro Interface](/http) that return JSON responses. You +[Open GoPro Interface]({{site.baseurl}}/http) that return JSON responses. You may have noted that we did not discuss one of these (Get Media List) in this tutorial. Proceed to the next tutorial to see how to get and perform operations using the media list. diff --git a/docs/_tutorials/tutorial_7_camera_media_list/tutorial.md b/docs/_tutorials/tutorial_8_camera_media_list/tutorial.md similarity index 85% rename from docs/_tutorials/tutorial_7_camera_media_list/tutorial.md rename to docs/_tutorials/tutorial_8_camera_media_list/tutorial.md index b7b933b7..68a1f223 100644 --- a/docs/_tutorials/tutorial_7_camera_media_list/tutorial.md +++ b/docs/_tutorials/tutorial_8_camera_media_list/tutorial.md @@ -2,19 +2,18 @@ permalink: '/tutorials/camera-media-list' sidebar: nav: 'tutorials' -lesson: 7 +lesson: 8 --- -# Tutorial 7: Camera Media List +# Tutorial 8: Camera Media List -This document will provide a walk-through tutorial to send Open GoPro -[HTTP commands](/http) to the GoPro, specifically to get the media list -and perform operations on it (downloading pictures, videos, etc.) +This document will provide a walk-through tutorial to send Open GoPro [HTTP commands]({{site.baseurl}}/http) to the GoPro, +specifically to get the media list and perform operations on it (downloading pictures, videos, etc.) {% tip %} It is suggested that you have first completed the -[Connecting to Wifi]({% link _tutorials/tutorial_5_connect_wifi/tutorial.md %}) -and [Sending WiFi Commands]({% link _tutorials/tutorial_6_send_wifi_commands/tutorial.md %}) tutorials. +[Connecting to Wifi]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}) +and [Sending WiFi Commands]({% link _tutorials/tutorial_7_send_wifi_commands/tutorial.md %}) tutorials. {% endtip %} This tutorial only considers sending these commands as one-off commands. That is, it does not consider state @@ -23,32 +22,34 @@ management / synchronization when sending multiple commands. This will be discus # Requirements It is assumed that the hardware and software requirements from the -[connect tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) are present and +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) are present and configured correctly. The scripts that will be used for this tutorial can be found in the -[Tutorial 7 Folder](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list). +[Tutorial 8 Folder](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list). # Just Show me the Demo(s)!! {% linkedTabs demo %} {% tab demo python %} -Each of the scripts for this tutorial can be found in the Tutorial 2 -[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_7_camera_media_list/). +Each of the scripts for this tutorial can be found in the Tutorial 8 +[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_8_camera_media_list/). {% warning %} -Python >= 3.8.x must be used as specified in the requirements +Python >= 3.9 and < 3.12 must be used as specified in the requirements {% endwarning %} {% warning %} You must be connected to the camera via WiFi in order to run these scripts. You can do this by manually to the SSID and password listed on your camera or by leaving the -`Establish Connection to WiFi AP` script from [Tutorial 5]({% link _tutorials/tutorial_5_connect_wifi/tutorial.md %}#just-show-me-the-demos) running in the background. +`Establish Connection to WiFi AP` script from +[Tutorial 5]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}#just-show-me-the-demos) running in the background. {% endwarning %} {% accordion Download Media File %} You can downloading a file from your camera with HTTP over WiFi using the following script: + ```console $ python wifi_media_download_file.py ``` @@ -64,11 +65,13 @@ Find a photo on the camera and download it to the computer. optional arguments: -h, --help show this help message and exit ``` + {% endaccordion %} {% accordion Get Media Thumbnail %} You can downloading the thumbnail for a media file from your camera with HTTP over WiFi using the following script: + ```console $ python wifi_media_get_thumbnail.py ``` @@ -84,20 +87,22 @@ Get the thumbnail for a media file. optional arguments: -h, --help show this help message and exit ``` + {% endaccordion %} {% endtab %} {% tab demo kotlin %} The Kotlin file for this tutorial can be found on [Github](https://github.com/gopro/OpenGoPro/tree/main/demos/kotlin/tutorial/app/src/main/java/com/example/open_gopro_tutorial/tutorials/Tutorial7CameraMediaList.kt). -To perform the tutorial, run the Android Studio project, select "Tutorial 7" from the dropdown and click on "Perform." +To perform the tutorial, run the Android Studio project, select "Tutorial 8" from the dropdown and click on "Perform." This requires: -- a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. -- a GoPro is already connected via Wifi, i.e. that Tutorial 5 was already run. + +- a GoPro is already connected via BLE, i.e. that Tutorial 1 was already run. +- a GoPro is already connected via Wifi, i.e. that Tutorial 5 was already run. You can check the BLE and Wifi statuses at the top of the app. -{% include figure image_path="/assets/images/tutorials/kotlin/tutorial_7.png" alt="kotlin_tutorial_7" size="40%" caption="Perform Tutorial 7" %} +{% include figure image_path="/assets/images/tutorials/kotlin/tutorial_8.png" alt="kotlin_tutorial_8" size="40%" caption="Perform Tutorial 8" %} This will start the tutorial and log to the screen as it executes. When the tutorial is complete, click "Exit Tutorial" to return to the Tutorial selection screen. @@ -108,17 +113,16 @@ This will start the tutorial and log to the screen as it executes. When the tuto # Setup We must first connect to The GoPro's WiFi Access Point (AP) as was discussed in the -[Connecting to Wifi]({% link _tutorials/tutorial_5_connect_wifi/tutorial.md %}) tutorial. +[Connecting to Wifi]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}) tutorial. # Get Media List Now that we are are connected via WiFi, we will get the media list using the same procedure to send HTTP commands as in the -[previous tutorial]({% link _tutorials/tutorial_6_send_wifi_commands/tutorial.md %}). +[previous tutorial]({% link _tutorials/tutorial_7_send_wifi_commands/tutorial.md %}). -We get the media list via the -[Get Media List command](/http#commands-quick-reference). -This command will return a JSON structure of all of the media files (pictures, videos) on the camera with +We get the media list via [Get Media List]({{site.baseurl}}/http#tag/Media/operation/OGP_MEDIA_LIST). +This will return a JSON structure of all of the media files (pictures, videos) on the camera with corresponding information about each media file. Let's build the endpoint, send the GET request, and check the response for errors. Any errors will raise @@ -126,6 +130,7 @@ an exception. {% linkedTabs media_list_send %} {% tab media_list_send python %} + ```python url = GOPRO_BASE_URL + "/gopro/media/list" ``` @@ -134,11 +139,14 @@ url = GOPRO_BASE_URL + "/gopro/media/list" response = requests.get(url) response.raise_for_status() ``` + {% endtab %} {% tab media_list_send kotlin %} + ```python val response = wifi.get(GOPRO_BASE_URL + "gopro/media/list") ``` + {% endtab %} {% endlinkedTabs %} @@ -146,6 +154,7 @@ Lastly, we print the response's JSON data: {% linkedTabs media_list_print %} {% tab media_list_print python %} + ```python logger.info(f"Response: {json.dumps(response.json(), indent=4)}") ``` @@ -188,8 +197,10 @@ INFO:root:Response: { "s": "10725219" }, ``` + {% endtab %} {% tab media_list_print kotlin %} + ```kotlin Timber.i("Files in media list: ${prettyJson.encodeToString(fileList)}") ``` @@ -230,17 +241,18 @@ Complete media list: { ] } ``` + {% endtab %} {% endlinkedTabs %} -The media list format is defined in the -[Open GoPro Specification](/http#media-list-format). +The media list format is defined in the [Media Model]({{site.baseurl}}/http#schema/MediaList). We won't be rehashing that here but will provide examples below of using the media list. One common functionality is to get the list of media file names, which can be done as such: {% linkedTabs media_list_get_files %} {% tab media_list_get_files python %} + ```python print([x["n"] for x in media_list["media"][0]["fs"]]) ``` @@ -250,6 +262,7 @@ make a list of all of the names (**n** tag of each element) in the **fs** list. {% endtab %} {% tab media_list_get_files kotlin %} + ```kotlin val fileList = response["media"]?.jsonArray?.first()?.jsonObject?.get("fs")?.jsonArray?.map { mediaEntry -> @@ -258,12 +271,12 @@ val fileList = ``` That is: + 1. Access the JSON array at the **fs** tag at the first element of the **media** tag -1. Make a list of all of the names (**n** tag of each element) in the **fs** list. -2. Map this list to string and remove backslashes -3. -{% endtab %} -{% endlinkedTabs %} +2. Make a list of all of the names (**n** tag of each element) in the **fs** list. +3. Map this list to string and remove backslashes +4. {% endtab %} + {% endlinkedTabs %} # Media List Operations @@ -293,18 +306,20 @@ sequenceDiagram ## Download Media File +TODO Handle directory in media list. + The next command we will be sending is -[Download Media](/http#downloading-media). Specifically, we -will be downloading a photo. The camera must have at least one photo in its media list in order for this to -work. +[Download Media]({{site.baseurl}}/http#tag/Media/operation/OGP_DOWNLOAD_MEDIA). Specifically, we +will be downloading a photo. The camera must have at least one photo in its media list in order for this to work. First, we get the media list as in -[Get Media List]({% link _tutorials/tutorial_7_camera_media_list/tutorial.md %}#get-media-list) . +[Get Media List]({% link _tutorials/tutorial_8_camera_media_list/tutorial.md %}#get-media-list) . Then we search through the list of file names in the media list looking for a photo (i.e. a file whose name ends in **.jpg**). Once we find a photo, we proceed: {% linkedTabs download_media_find_jpg %} {% tab download_media_find_jpg python %} + ```python media_list = get_media_list() @@ -318,11 +333,13 @@ for media_file in [x["n"] for x in media_list["media"][0]["fs"]]: {% endtab %} {% tab download_media_find_jpg kotlin %} + ```kotlin val photo = fileList?.firstOrNull { it.endsWith(ignoreCase = true, suffix = "jpg") } ?: throw Exception("Not able to find a .jpg in the media list") Timber.i("Found a photo: $photo") ``` + {% endtab %} {% endlinkedTabs %} @@ -335,6 +352,7 @@ The endpoint will start with "videos" for both photos and videos {% linkedTabs download_media_send %} {% tab download_media_send python %} + ```python url = GOPRO_BASE_URL + f"videos/DCIM/100GOPRO/{photo}" ``` @@ -356,6 +374,7 @@ with open(file, "wb") as f: {% endtab %} {% tab download_media_send kotlin %} + ```kotlin return wifi.getFile( GOPRO_BASE_URL + "videos/DCIM/100GOPRO/$photo", appContainer.applicationContext @@ -371,6 +390,7 @@ This will log as such: {% linkedTabs download_media_print %} {% tab download_media_print python %} + ```console INFO:root:found a photo: GOPR0987.JPG INFO:root:Downloading GOPR0987.JPG @@ -381,6 +401,7 @@ INFO:root:receiving binary stream to GOPR0987.jpg... Once complete, the `GOPR0987_thumbnail.jpg` file will be available from where the demo script was called. {% endtab %} {% tab download_media_print kotlin %} + ```console Found a photo: GOPR0232.JPG Downloading photo: GOPR0232.JPG... @@ -393,8 +414,7 @@ Once complete, the photo will display in the tutorial window. ## Get Media Thumbnail -The next command we will be sending is -[Get Media thumbnail ](/http#downloading-media). +The next command we will be sending is [Get Media thumbnail ]({{site.baseurl}}/http#tag/Media/operation/OGP_MEDIA_THUMBNAIL). Specifically, we will be getting the thumbnail for a photo. The camera must have at least one photo in its media list in order for this to work. @@ -403,12 +423,13 @@ There is a separate commandto get a media "screennail" {% endnote %} First, we get the media list as in -[Get Media List]({% link _tutorials/tutorial_7_camera_media_list/tutorial.md %}#get-media-list) . +[Get Media List]({% link _tutorials/tutorial_8_camera_media_list/tutorial.md %}#get-media-list) . Then we search through the list of file names in the media list looking for a photo (i.e. a file whose name ends in **.jpg**). Once we find a photo, we proceed: {% linkedTabs thumbnail_find_jpg %} {% tab thumbnail_find_jpg python %} + ```python media_list = get_media_list() @@ -431,6 +452,7 @@ an exception. {% linkedTabs thumbnail_send %} {% tab thumbnail_send python %} + ```python url = GOPRO_BASE_URL + f"/gopro/media/thumbnail?path=100GOPRO/{photo}" ``` @@ -449,6 +471,7 @@ with open(file, "wb") as f: for chunk in request.iter_content(chunk_size=8192): f.write(chunk) ``` + {% endtab %} {% tab thumbnail_send kotlin %} TODO @@ -459,12 +482,14 @@ This will log as such: {% linkedTabs thumbnail_print %} {% tab thumbnail_print python %} + ```console INFO:root:found a photo: GOPR0987.JPG INFO:root:Getting the thumbnail for GOPR0987.JPG INFO:root:Sending: http://10.5.5.9:8080/gopro/media/thumbnail?path=100GOPRO/GOPR0987.JPG INFO:root:receiving binary stream to GOPR0987_thumbnail.jpg... ``` + {% endtab %} {% tab thumbnail_print kotlin %} TODO @@ -474,7 +499,7 @@ TODO # Troubleshooting See the previous tutorial's -[troubleshooting section]({% link _tutorials/tutorial_6_send_wifi_commands/tutorial.md %}#troubleshooting). +[troubleshooting section]({% link _tutorials/tutorial_7_send_wifi_commands/tutorial.md %}#troubleshooting). # Good Job! diff --git a/docs/_tutorials/tutorial_9_cohn/tutorial.md b/docs/_tutorials/tutorial_9_cohn/tutorial.md new file mode 100644 index 00000000..f92a0245 --- /dev/null +++ b/docs/_tutorials/tutorial_9_cohn/tutorial.md @@ -0,0 +1,514 @@ +--- +permalink: '/tutorials/cohn' +sidebar: + nav: 'tutorials' +lesson: 9 +--- + +# Tutorial 9: Camera on the Home Network + +This document will provide a walk-through tutorial to use the Open GoPro Interface to configure and demonstrate +the [Camera on the Home Network]({{site.baseurl}}/ble/features/cohn.html) (COHN) feature. + +{% tip %} +It is recommended that you have first completed the +[connecting BLE]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}), +[sending commands]({% link _tutorials/tutorial_2_send_ble_commands/tutorial.md %}), +[parsing responses]({% link _tutorials/tutorial_3_parse_ble_tlv_responses/tutorial.md %}), +[protobuf]({% link _tutorials/tutorial_5_ble_protobuf/tutorial.md %}), and +[connecting WiFi]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}) +tutorials before proceeding. +{% endtip %} + +# Requirements + +It is assumed that the hardware and software requirements from the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#requirements) +are present and configured correctly. + +The scripts that will be used for this tutorial can be found in the +[Tutorial 9 Folder](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_9_connect_wifi). + +# Just Show me the Demo(s)!! + +{% linkedTabs demo %} +{% tab demo python %} +Each of the scripts for this tutorial can be found in the Tutorial 9 +[directory](https://github.com/gopro/OpenGoPro/tree/main/demos/python/tutorial/tutorial_modules/tutorial_9_connect_wifi/). + +{% warning %} +Python >= 3.9 and < 3.12 must be used as specified in the requirements +{% endwarning %} + +{% accordion Provision COHN %} + +You can provision the GoPro for COHN to communicate via a network via: + +```console +$ python provision_cohn.py +``` + +See the help for parameter definitions: + +```console +$ python provision_cohn.py --help +usage: provision_cohn.py [-h] [-i IDENTIFIER] [-c CERTIFICATE] ssid password + +Provision COHN via BLE to be ready for communication. + +positional arguments: + ssid SSID of network to connect to + password Password of network to connect to + +options: + -h, --help show this help message and exit + -i IDENTIFIER, --identifier IDENTIFIER + Last 4 digits of GoPro serial number, which is the last 4 digits of the default camera + SSID. If not used, first discovered GoPro will be connected to + -c CERTIFICATE, --certificate CERTIFICATE + Path to write retrieved COHN certificate. +``` + +{% endaccordion %} + +{% accordion Communicate via COHN %} + +You can see an example of communicating HTTPS via COHN (assuming it has already been provisioned) via: + +```console +$ python communicate_via_cohn.py +``` + +See the help for parameter definitions: + +```console +$ python communicate_via_cohn.py --help +usage: communicate_via_cohn.py [-h] ip_address username password certificate + +Demonstrate HTTPS communication via COHN. + +positional arguments: + ip_address IP Address of camera on the home network + username COHN username + password COHN password + certificate Path to read COHN cert from. + +options: + -h, --help show this help message and exit +``` + +{% endaccordion %} + +{% endtab %} +{% tab demo kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +# Setup + +We must first connect to BLE as was discussed in the +[connecting BLE tutorial]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}). +The GoPro must then be connected to an access point as was discussed in the +[Connecting WiFi Tutorial]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}#station-sta-mode). +For all of the BLE operations, we are using the same `ResponseManager` class that was defined in the +[Protobuf tutorial]({% link _tutorials/tutorial_5_ble_protobuf/tutorial.md %}#response-manager). + +# COHN Overview + +The Camera on the Home Network feature allows the GoPro to connect (in +[Station Mode]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}#station-sta-mode)) to an Access Point (AP) +such as a router in order to be controlled over a local network via the [HTTP API]({{site.baseurl}}/http). + +In order to protect users who connect to a network that includes Bad Actors, COHN uses +[SSL/TLS](https://www.websecurity.digicert.com/security-topics/what-is-ssl-tls-https) so that command and responses are +sent securely encrypted via `https://` rather than `http://`. + +{% tip %} +Once COHN is provisioned it is possible to control the GoPro without a BLE connection by communicating via HTTPS over the +provisioned network. +{% endtip %} + +# Provisioning + +In order to use the COHN capability, the GoPro must first be provisioned for COHN via BLE. At a high level, the +provisioning process is as follows: + +- [Connect the GoPro to an access point]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}#station-sta-mode) +- Instruct the GoPro to create a COHN Certificate +- Get the created COHN certificate +- Get the COHN status to retrieve and store COHN credentials for future use + +A summary of this process is shown here and will be expanded upon in the following sections: + +{% include figure image_path="/assets/images/plantuml_ble_cohn_provision.png" alt="provision_cohn" size="70%" caption="Provision COHN" %} + +## Set Date Time + +While not explicitly part of of the provisioning process, it is important that the GoPro's date and time are correct +so that it generates a valid SSL certificate. This can be done manually through the camera UI or programatically +using the [Set Local Datetime]({{site.baseurl}}/ble/features/control.html#set-local-date-time) command. + +For the provisioning demo discussed in this tutorial, this is done programatically: + +{% linkedTabs set_date_time %} +{% tab set_date_time python %} + +{% note %} +The code shown here can be found in `provision_cohn.py` +{% endnote %} + +We're using the [pytz](https://pypi.org/project/pytz/) and [tzlocal](https://pypi.org/project/tzlocal/) libraries to +find the timezone offset and daylight savings time status. In the `set_date_time` method, we send the request and wait to +receive the successful response: + +```python +datetime_request = bytearray( + [ + 0x0F, # Command ID + 10, # Length of following datetime parameter + *now.year.to_bytes(2, "big", signed=False), # uint16 year + now.month, + now.day, + now.hour, + now.minute, + now.second, + *offset.to_bytes(2, "big", signed=True), # int16 offset in minutes + is_dst, + ] +) +datetime_request.insert(0, len(datetime_request)) +await manager.client.write_gatt_char(GoProUuid.COMMAND_REQ_UUID.value, datetime_request, response=True) +response = await manager.get_next_response_as_tlv() +``` + +which logs as: + +```console +Setting the camera's date and time to 2024-04-04 13:00:05.097305-07:00:-420 is_dst=True +Writing: 0c:0f:0a:07:e8:04:04:0d:00:05:fe:5c:01 +Received response at GoProUuid.COMMAND_RSP_UUID: 02:0f:00 +Successfully set the date time. +``` + +{% endtab %} +{% tab set_date_time kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +## Create the COHN Certificate + +Now that the GoPro's date and time are valid and it has been +[connected to an Access Point]({% link _tutorials/tutorial_6_connect_wifi/tutorial.md %}#station-sta-mode), we can +continue to provision COHN. + +Let's instruct the GoPro to [Create a COHN certificate]({{site.baseurl}}/ble/features/cohn.html#create-cohn-certificate). + +{% linkedTabs get_ssid %} +{% tab get_ssid python %} + +```python +create_request = bytearray( + [ + 0xF1, # Feature ID + 0x67, # Action ID + *proto.RequestCreateCOHNCert().SerializePartialToString(), + ] +) +create_request.insert(0, len(create_request)) +await manager.client.write_gatt_char(GoProUuid.COMMAND_REQ_UUID.value, create_request, response=True) +response := await manager.get_next_response_as_protobuf() +``` + +which logs as: + +```console +Creating a new COHN certificate. +Writing: 02:f1:67 +Received response at GoProUuid.COMMAND_RSP_UUID: 04:f1:e7:08:01 +COHN certificate successfully created +``` + +{% endtab %} +{% tab get_ssid kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +{% tip %} +You may notice that the provisioning demo first +[Clears the COHN Certificate]({{site.baseurl}}/ble/features/cohn.html#clear-cohn-certificate). This is is only to +ensure a consistent starting state in the case that COHN has already been provisioned. It is not necessary to clear +the certificate if COHN has not yet been provisioned. +{% endtip %} + +## Get the COHN Credentials + +At this point the GoPro has created the certificate and is in the process of provisioning COHN. We now need to get +the COHN credentials that will be used for HTTPS communication. These are: + +- COHN certificate +- Basic auth [username](https://en.wikipedia.org/wiki/Basic_access_authentication) +- Baisc auth [password](https://en.wikipedia.org/wiki/Basic_access_authentication) +- IP Address of COHN network + +We can immediately get the COHN certificate as such: + +{% linkedTabs get_ssid %} +{% tab get_ssid python %} + +```python +cert_request = bytearray( + [ + 0xF5, # Feature ID + 0x6E, # Action ID + *proto.RequestCOHNCert().SerializePartialToString(), + ] +) +cert_request.insert(0, len(cert_request)) +await manager.client.write_gatt_char(GoProUuid.QUERY_REQ_UUID.value, cert_request, response=True) +response := await manager.get_next_response_as_protobuf(): +cert_response: proto.ResponseCOHNCert = response.data # type: ignore +return cert_response.cert +``` + +{% endtab %} +{% tab get_ssid kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +For the remaining credentials, we need to wait until the COHN network is connected. That is, we need to +[Get COHN Status]({{site.baseurl}}/ble/features/cohn.html#get-cohn-status) until we receive a status where the +[state]({{site.baseurl}}/ble/protocol/protobuf.html#notifycohnstatus) is set to +[COHN_STATE_NetworkConnected]({{site.baseurl}}/ble/protocol/protobuf.html#proto-enumcohnnetworkstate). +This final status contains the remaining credentials: username, password, and IP Address. + +To do this, we first register to receive asynchronous COHN status updates: + +{% linkedTabs get_ssid %} +{% tab get_ssid python %} + +```python +status_request = bytearray( + [ + 0xF5, # Feature ID + 0x6F, # Action ID + *proto.RequestGetCOHNStatus(register_cohn_status=True).SerializePartialToString(), + ] +) +status_request.insert(0, len(status_request)) +await manager.client.write_gatt_char(GoProUuid.QUERY_REQ_UUID.value, status_request, response=True) +``` + +{% endtab %} +{% tab get_ssid kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +Then we continuously receive and check the updates until we receive the desired status: + +{% linkedTabs get_ssid %} +{% tab get_ssid python %} + +```python +while response := await manager.get_next_response_as_protobuf(): + cohn_status: proto.NotifyCOHNStatus = response.data # type: ignore + if cohn_status.state == proto.EnumCOHNNetworkState.COHN_STATE_NetworkConnected: + return cohn_status +``` + +This will all display in the log as such: + +```console +Checking COHN status until provisioning is complete +Writing: 04:f5:6f:08:01 +... +Received response at GoProUuid.QUERY_RSP_UUID: 20:47:f5:ef:08:01:10:1b:1a:05:67:6f:70:72:6f:22:0c:47:7a:74 +Received response at GoProUuid.QUERY_RSP_UUID: 80:32:6d:36:59:4d:76:4c:41:6f:2a:0e:31:39:32:2e:31:36:38:2e +Received response at GoProUuid.QUERY_RSP_UUID: 81:35:30:2e:31:30:33:30:01:3a:0a:64:61:62:75:67:64:61:62:75 +Received response at GoProUuid.QUERY_RSP_UUID: 82:67:42:0c:32:34:37:34:66:37:66:36:36:31:30:34 +Received COHN Status: + status: COHN_PROVISIONED + state: COHN_STATE_NetworkConnected + username: "gopro" + password: "Gzt2m6YMvLAo" + ipaddress: "192.168.50.103" + enabled: true + ssid: "dabugdabug" + macaddress: "2474f7f66104" +Successfully provisioned COHN. +``` + +{% endtab %} +{% tab get_ssid kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +Finally we accumulate all of the credentials and log them, also storing the certificate to a `cohn.crt` file: + +{% linkedTabs get_ssid %} +{% tab get_ssid python %} + +```python +credentials = await provision_cohn(manager) +with open(certificate, "w") as fp: + fp.write(credentials.certificate) + logger.info(f"Certificate written to {certificate.resolve()}") +``` + +```console +{ + "certificate": "-----BEGIN + CERTIFICATE-----\nMIIDnzCCAoegAwIBAgIUC7DGLtJJ61TzRY/mYQyhOegnz6cwDQYJKoZIhvcNAQ + EL\nBQAwaTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTYW4gTWF0\nZW8xDjAMBg + NVBAoMBUdvUHJvMQ0wCwYDVQQLDARIZXJvMRowGAYDVQQDDBFHb1By\nbyBDYW1lcmEgUm9vdDAeFw0y + NDA0MDQyMDAwMTJaFw0zNDA0MDIyMDAwMTJaMGkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMB + AGA1UEBwwJU2FuIE1hdGVvMQ4w\nDAYDVQQKDAVHb1BybzENMAsGA1UECwwESGVybzEaMBgGA1UEAwwR + R29Qcm8gQ2Ft\nZXJhIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC05o1QIN5r\n + PmtTntzpzBQvfq64OM1j/tjdNCJsyB9/ipPrPcKdItOy+5gZZF8iOFiw8cG8O2nA\nvLSIJkpQ6d3cuE + 48nAQpc1+jJzskM7Vgqc/i43OqnB8iTKjtNJgj+lJtreQBNJw7\nf00a0GbbUJMo6DhaW58ZIsOJKu3i + +w8w+LNEZECfDN6RMSmkYoLXaHeKAlvhlRYv\nxkNO7pB2OwhbD9awgzKVTiKvZ8Hrxl6lGlH5SHHimU + uo2O1yiNKDWv+MhirCVnup\nVvP/N5S+230KpXreEnHmo65fsHmdM11qYu8WJXGzOViCnQi24wgCuoMx + np9hAeKs\nVj4vxhyCu8gZAgMBAAGjPzA9MA8GA1UdEwQIMAYBAf8CAQAwCwYDVR0PBAQDAgGG\nMB0G + A1UdDgQWBBTYDT4QXVDsi23ukLr2ohJk5+8+gDANBgkqhkiG9w0BAQsFAAOC\nAQEAU4Z9120CGtRGo3 + QfWEy66BGdqI6ohdudmb/3qag0viXag2FyWar18lRFiEWc\nZcsqw6i0CM6lKNVUluEsSBiGGVAbAHKu + +fcpId5NLEI7G1XY5MFRHMIMi4PNKbJr\nVi0ks/biMy7u9++FOBgmCXGAdbMJBfe2gxEJNdyU6wjgGs + 2o402/parrWN8x9J+k\ndBgYqiKpZK0Fad/qM4ivbgkMijXhGFODhWs/GlQWnPeaLusRnn3T/w2CsFzM + kf0i\n6fFT3FAQBU5LCZs1Fp/XFRrnFMp+sNhbmdfnI9EDyZOXzlRS4O48k/AW/nSkCozk\nugYW+61H + /RYPVEgF4VNxRqn+uA==\n-----END CERTIFICATE-----\n", + "username": "gopro", + "password": "Gzt2m6YMvLAo", + "ip_address": "192.168.50.103" +} +Certificate written to C:\Users\user\gopro\OpenGoPro\demos\python\tutorial\tutorial_modules\tutorial_9_cohn\cohn.crt +``` + +{% endtab %} +{% tab get_ssid kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +{% success %} +Make sure to keep these credentials for use in the next section. +{% endsuccess %} + +# Communicating via COHN + +Once the GoPro has provisioned for COHN, we can use the stored credentials for HTTPS communication. + +For the setup of this demo, there is no pre-existing BLE or WiFi connection to the GoPro. We are only going to be using +HTTPS over the provisioned home network for communication. + +In order to demonstrate COHN communication we are going to +[Get the Camera State]({{site.baseurl}}/http#tag/Query/operation/OGP_GET_STATE). + +{% linkedTabs get_ssid %} +{% tab get_ssid python %} + +{% note %} +The code shown below is taken from `communicate_via_cohn.py`. The credentials logged and stored from the previous demo +must be passed in as command line arguments to this script. Run `python communicate_via_cohn.py --help` for usage. +{% endnote %} + +We're going to use the [requests](https://pypi.org/project/requests/) library to perform the HTTPS request. First let's +build the url using the `ip_address` CLI argument: + +```python +url = f"https://{ip_address}" + "/gopro/camera/state" +``` + +Then let's build the [basic auth token](https://www.debugbear.com/basic-auth-header-generator) from the `username` and +`password` CLI arguments: + +```python +token = b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") +``` + +Lastly we build and send the request using the above endpoint and token combined with the path to the certificate +from the CLI `certificate` argument: + +```python +response = requests.get( + url, + timeout=10, + headers={"Authorization": f"Basic {token}"}, + verify=str(certificate), +) +logger.info(f"Response: {json.dumps(response.json(), indent=4)}") +``` + +{% endtab %} +{% tab get_ssid kotlin %} + +TODO + +{% endtab %} +{% endlinkedTabs %} + +This should result in logging the complete cameras state, truncated here for brevity: + +```console +Sending: https://192.168.50.103/gopro/camera/state +Command sent successfully +Response: { + "status": { + "1": 1, + "2": 4, + "3": 0, + "4": 255, + "6": 0, + "8": 0, + "9": 0, + ... + "settings": { + "2": 1, + "3": 0, + "5": 0, + "6": 0, + "13": 0, + ... +``` + +See the +[sending Wifi commands tutorial]({% link _tutorials/tutorial_7_send_wifi_commands/tutorial.md %}) for more information +on this and other HTTP(S) functionality. + +**Quiz time! 📚 ✏️** + +# Troubleshooting + +See the first tutorial's +[troubleshooting section]({% link _tutorials/tutorial_1_connect_ble/tutorial.md %}#troubleshooting) to troubleshoot +any BLE problems. + +See the Sending Wifi Command tutorial's +[troubleshooting section]({% link _tutorials/tutorial_7_send_wifi_commands/tutorial.md %}#troubleshooting) to +troubleshoot HTTP communication. + +# Good Job! + +{% success %} +Congratulations 🤙 +{% endsuccess %} + +You have now provisioned COHN and performed an HTTPS operation. In the future, you can now communicate with the GoPro +over your home network without needing a direct BLE or WiFi connection. diff --git a/docs/assets/css/custom.css b/docs/assets/css/custom.css index b471e6c5..33b56bbb 100644 --- a/docs/assets/css/custom.css +++ b/docs/assets/css/custom.css @@ -147,6 +147,10 @@ img.mermaid { .md_column { display: flex; + + * { + max-width: none !important; + } } /* Github button for demo layout */ diff --git a/docs/assets/images/tutorials/complex_response_doc.png b/docs/assets/images/tutorials/complex_response_doc.png new file mode 100644 index 0000000000000000000000000000000000000000..0cbc58dbf8247e16c28dd86e9311ad485b260079 GIT binary patch literal 58495 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 diff --git a/docs/assets/images/tutorials/kotlin/tutorial_7.png b/docs/assets/images/tutorials/kotlin/tutorial_7.png index 0a120544869c8c5c271dfd8e8f26ee8a4419ec5e..1e3ff937258199dbdb8ab6ad74267423de151d49 100644 GIT binary patch literal 15180 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 literal 16730 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

OnMyhHC^ATq!sb9VVR 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 diff --git a/docs/assets/images/tutorials/kotlin/tutorial_8.png b/docs/assets/images/tutorials/kotlin/tutorial_8.png new file mode 100644 index 0000000000000000000000000000000000000000..0a120544869c8c5c271dfd8e8f26ee8a4419ec5e GIT binary patch literal 16730 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

OnMyhHC^ATq!sb9VVR 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 literal 0 HcmV?d00001 diff --git a/docs/assets/images/tutorials/protobuf_doc.png b/docs/assets/images/tutorials/protobuf_doc.png new file mode 100644 index 0000000000000000000000000000000000000000..7507b1ca3a5eecc0ea2069d5aac010116b33307d GIT binary patch literal 61593 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�B{F|#JUr^`QaWB4k{eo?G}I`0H|0axIrL5ru^CX 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

*}j!4{mE`b-&({QCMJ+tEt=+= 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

-C$1)CZV2%9 zbPd{h?K9%jPr8;~GH`mZtKDbL`L6i|blWV@Nb-TkmnKZ$(*j3EV6*6GP$GAr+;Ere z9^`3X!yW$_+xQ()0SPy3Mepu1yivzih(&|TLPk@?=NJ&r-u3GTLiOi^l;9&Opy;!k zMlkmw*rl5bW%o~->kZ~Yjs^;V=tJmACkpab;o9oe5$C*;TiXZ^TO6w~{A4ApRlxmG zZB{v>KinN95<)ZsLBX`sPN62rnI1MX3e}O}x3et2jr_x{sC(BY+2Izo9<84lHuI!! z0wj#L@hk$Cp+{ zN;>h+!_R6@H%8~}*u5^QqfYj|cc+1OqfiURhizE2UD23VZnI+D*6l`;?w*Eq`SdHthKFvD2 ziD1&BtKSn#b>y}cH^e5mD*W3`)~IXdv%zuj+9J*8Ijuaq7HnlCq04NZun(M z<^Mkz@xO*E{=0txhg;yV3&MMZ{~@%Re;CmJaWc=pUggT*QL9Gh)sYp%xYMd(=lMFE zG}rCx$qdv3M>PAb+(4y!=Ul}*Yf--ImC8Dpri=3~`S4TNx2X*uf6UMY+9`gRe9GHW zs>A&=_z`KO8f}uT(;M?7K++}-q|2kEsq<*~mRBpSWthckZ+#awom`Y>w=~1}r!)TG ziinTcM_fdr*`4mHdKe@|xamQ8{Km!Hes{GbVu3xmFs~qAq_)G%Sa#cV zgd$^n8pdNmR+J?*dyVIssr?<#G^YD!64N88^1tDC7=hPqj3<+`sLtyND?hJ^_; zDrzZ7$`0+`I16QO*J&5Cn_5WmDMzQMPo0LKyKDxzb6BifF$^?Oe(D=Q6F=ymBn|g= zGw{#TC;vl#wm{5W7C~cUp7tQ$Y!+TS(8?hg!`nybZe=1BSJ*v|Z*vvUdn8tS+r>Ac z8ZUpS`h@#s+MU>?#`{q{tGnja%hquTJ3@7zPfgu}@$7>IUXQ#oA#b3Fg_Kg-GutxN zhS2WAn??8bPFQmPw>Mm6Pt~+C57*+n&d917wPs^lwo|qo)|0bIP@w*yQq*FYJZb=s z?cE3xnu(w3e!MV~6T%NnGC(F+|=@ADu?BB!%B`9NJNm10U+E|**HBe3SKVz9f&U*UwAxHV5>d|7GT4evf|HLk|~oHQ?Cl%ZWh zHrpUD6J|V{5)nV~m??Iwh+r!whUfmf|gVE|8aGQEg--7m9Erq96 zRlV`vr)Z>mVp^TH?fZy8>73!)HIFZB6r#h;3EXY`%Y$WdgF7nKbqSnKSGp?Q+~+kq zoVDM|?kdK)1$JK&a)Yj2kH=<`7Qd$GRtJ%4FR(wr@%=zj&pM|x`{l+`_`)_gyDxZN zI1x4zT#QdudB9LvEYmtYGG&v9iZsNgWK>+NA1<===s)I2-$t$6lxT;R`%H#Y?*+Vz zn@?ZZ^2zBogYy#_0kn6mhIKN-y;^`do!U!{Y)a79Zoraf36UMlFG{ViFt2u=BlI~o zLC`3a`zYkBOCQ3)#W;&x!#3zKw(6Ml39wtBd$%pZwBy1%NM_g{P#8*$H5uY#<$o1{ zKfs|KVQcxYxzj1*XeOP6>mhZw{oPn&^`0s!j^c(!>L!xT72Ck%~`5AUS$REgZXNDF$=6^#lee3fM200R*Gc*g95Y3E805JGMQYSqn_Ix zCAAbf>~cP?%(xkNOinT=ntbVE04rOHVrQ-GOr$r?iX6*?-2J=CaH_kE*IN3Y*5)YX zCUP_4=DMk+voAnrqqxGGaAWe#ShND#b+&q;*M=S%1sgj0zvpJrQts%QzBmua5lw-` z9U`DJsPI6D<*<>He&5>85yfLgp`H%~vD3Zlu?r%<=hwas%Ngrh{%=`~rhmm`NJ&VN zf{+ilk9au3l>O}QUEw+ylY1SPeijE_wS)&tY()Xr877z~KMI#@rqUZ=7_@+alx8F4 zJ*3qi3!;PX*#>7AGBdOzpyVPoQIe);1Q)*FjT9_l$KTvnT|s-39jhy4S;uFuzsiJYphrlJ~G z5H(pXV6=*4LIOG2C*LLrZco0V?~*aEUhr!U6==!oVDbHt8NU*KO>1e$mXvXxr>Thl zS5F5j)Smcyno=87TgfT0jnsC_2#{#R7SH;g$3Hlv^ z7$s|HHg*k_qAFRyFZ?9Dw(7gT9B>0azsJl!G7r{ZhE*YqbFQ&!&a6S zTgP{~t5d&ox^MYivCog_AkMFA)|t8Q{CBh&oejD3KE9cyNi2$oiM+OHCtD?AVMX}# zJ^-|pHa8|$ciB}u(NN_3L90nw4c|=~?o&+oEpwN-r{r2sHJ4g34@iC{mQy%T%i7W7 zJ1SC5l^>?7{VcE;MEJOoSM2^4!sC^|nX`{4&tH)H{=%g+WqRTQwMNwiL(ZUyRKjcC zYPdVFLJFUInne@0tvE)<(?e)|2Ds)kmcn`Y@aE*lqTQlWdEJ>D?ZJ8d(pJOpOlHPJ zl{84t#d%<&Ia5`40`G$`T7C7Mm0ug5rn)8rXSRPft8RV_9d?Na-p!Qy!_HzXzRMyc z;fP(nkO>$N(bxX>^${vQtov9|9SjR*CuxHMq(gm1hw1IMvU3*4m_=-EOlw}>mN~gO zKLpa|qb29=zBP|!(voy3E2WF-p)P(rH|A4qN0hN9FB#$)(Xz>G2-I8|hE&_kHg_yH z)dcnvsYXZt_MIHielmF+^K&lcFfTv;of{uOs$H8=lL|W*lq5EyF0_yof7-1JcwW0c z9-x+pXTxJWj8a`OgJ3-V=(c!r&e9GOWN>H zf!h5(dL9Ae8ms!a$fk{~bDVp5TGWHoFf%MS5jcJF_tRkhMQ3#G*c~3-Z%^Mr8|&B} ztL(vn+VU(ouej^&yBx$qcU50%mTB!xprc;*M;Zw20Y7FACE_J0$~Z|jfhuE0-ISjN z+hT4Y=IW>F$H_t#UlAg%AHDKn`8tB5rFwx{ic2$ImXX&vCoMVs6ha$@e1M`pLESZ; z32e8w6C*^uMsP~eANE(%kft%ZJhIG8*jqmN6oEz_rlM;-6f@$&_xZtvAo1rl# z`}WP$V@qXyitGK(8iZFMZQu*^cDa>^pSK}L#LK=a z7BSAIA==DMVF<^7aBcM7I4X2fC2P=CD0#2Y2X-#CD|W|~JipWL&K7*#JvvoGzK*Mh zlXda)^*BVMIG16vM|-XP{?~Vsi4r6}ic*RC-80z?HdW@A-gsrO=nH?q6Za)BLenDB zdYnpVeXn5f#wM+<%hyt?l%w+13$tNGd6#MWwsBgzkUv!#?rYhn3VzvmzKvZjc?@vB zz%IU<7eK2I%#v4dI9R>R?&Dm#JPIuBr-oq;>P^}deery(k>1I&nBP2er#s|9T1u&d zU247*SUT#GJGg6RxBHqAWFhPpb`7)9wmtNdE`T6g~Y(j3(LQe zri2;>drWYxsno6gvzcI}xjiIMV^qt&WhrcYRubic+qHAY$;kH7_99_F>OwFnY9j4t z93H+p$1z)b+*xHV12{Nhl$0G7W!KGPle|!c(C2Z ze=5|#qinZjcn{Th5ICAh58*bHmKXAH^1R_$#s4Y8BwwD20Vl(Y3c?^8hucpIN2APU z*6*wOany~w1Y1w5d*LtV4xV%mo6I*g7d35gHgBCyBT;xLJY3De>?{Xb?xt`dgz;Gw z$77ubXW;8?x^hWkZ=>L{(B!4>M1zqskTYd+n)H^b!|cniiHYjLc`WR+oQzUl`&o#E zDw{CcBFf%o&*M0Yz9bTQpL96Ee77k5iLoE=1?lBZRf|i}jZH(TNji_$TDVJM57W^Y zbL!<2>hEt03yBLD5nk>WMYe}#=iFt~Z{6q)9W<*scsfjcD5>l6$(QUqm-jf4tB2sQ*n%bf!4)om+bXbHuHrqjrnRms}{Le@8Nk_SHIR^ zn%-;d;zgCL`(&W)q__G9u~26dZU8t1sv+q=Yq}&wKewWrp;)u@TrOh#m&(i{U7E^l zj;OhCn7Qo2`B3gbYzp*Kn6YpXgdEAwHUN`}^%h^URg0Z2U<}W&VeU$OSgiT^3cigP zKePj+y*@V^xegX&?Cu`==)WcX=O?dy3}@I8o%X>+vuRx9x3 zWcKU;7uX>3m>`E^YvD$0vrr(}@Xixk8Ap-aN)j{^QjuVicM;-tq#3GIdUpEJ=OCOk zx~bGFp-}EdoufI7g0V%&Ob{;`TkI3ZLJ|WWMsS?%PqY7sWYUVU<|I>rGI$3r^Z^uUCgA@Wcsr0Di&k?> zNv~I<>1hy235tDl-CSe|QUGnVuG=r}vs*dd&n``1q&?=m7*}wqF?Kn*nN893Zdz2| z90U~Y3hH+7D2tA*&k)_i^sKd&Jjk}fuyPL@N6#neIc_~bWO-EX|Kz2O{OQ=`OJQEf zI*;MST*!jMMJj!`%+@V1XOJZ6T>Ufkk8!gP&UA-VsqQZ}5u>8$*sL#a zj^Ru{bVB}mk0#h?=HLYR9HaGCn@eu#&U98fcLg|i#(s&x$h~=?Ku+z3xP&!xYWUnCc$bqck9hT2czenx*1@*;h-vW{<}nWIY9*>5IqA9VVmM zviyLd&(ElU-doqL3DBnYr#@1=bigN#J&5`2mf>oux`{-6CJ9>1x;efOo4D2%;M6s4GKi$sL(aJi&by4N5vIYIY4Q?u z;UbTp@25|hBFQoua2XccYu@yQ@luGweRyxyuNqIPM&F&lj!Pcw1HmM? z^YsOw$xa_0Ts+|U{Zr?yhq8l1MGu921_iO_>Baia_yo%3gm@KHdL!_8H@AWGca3_m zZ{8PWuz%>8<=NoJ(CI2_R9A1_+oK$2QBKY1jNRMWYre3iAQ~wQX^Gp{^8J{m#q+pk znxPnt)7^6y+QvcROfXwgZpJ{)8npmnd6!AIbKWiNSH>MDR#?r9c(w)K&qQN(Z zH|7h|wep_XJNM3F5ew}Ms0;UW5_s+qfwc5Po2FMh(s1gy$(Pbu!rg=jm05L~v8}19 zJUt$d^&#H+-tgCfH7rj*{9iqvBR@Ad7$jm^EDd=fEmQ}4g*!@0N3 z9IVca>1)6D1ReJ6zW;o3^;S4UMZ`Pgo3-l0I7;+EAqGu=xNeM>sHpY0bx|0Nb#U4V^b}u}Ek&CMVL2b3 zE(eF22;yNHLRgQl>=>NGxyux9s6;`(w_HILk$Lo1?9gU=UXyCu$gP+OTT5A&$0jOZUJTz1rp;X`n|COsTT;1k5raT`^fAxQZ z`%Y7Oa+*=m`M?+G!*oO+9@>u=M5|LH%TB*trZ@nDa(z$lgKIN0DH1bI?t9{GRrMsB zih-v%_PInmOww6&&Yf5_P&n6idGt*QqSZ*oRW0s#PCnk;)dOW`fCyUxXxG^nIy)ZX zq+c#j#9zPtRpe}QKLjs5KCESc-{qoh`~#`Wp{*6gL1anc#{MhZ!0fkxD&lmU3V5KE zzW(~#+&R;Vy*^VA{d0D}9~*clchBG_66wm&J!sVfO}nJzY%IU`)fj}FF#o)(C&k-j z?PF(XFz>x`m5<6N9c?bry2UH(WPU4)Lu!UT3Gr(KNe71$BaY0CjU5Z@l7(sh7YGQv z%%N~#?)hp%uNFl28n%^-hmTBQ*WHOCb~*raI&kDv0(%Y0+LsRx@;;nb-N0XYlkBYj z40&>vsp3~w4Wko(%0ATQJ4qVm0hwRj{IS57)urN?i@-;IPQYt3J9x;788_XRrYg1D zN}1!UY?=4!3+;F5TXzqETd<(Ht&oE`S%Q_dwx~~?S@;}wTZ9?{Uc2>Dhk+GVxzmI9 z&3FEU+!s$^l}K{5&MkI2f(aFZba+l8otV5!GDFzYMqU~;XJK45uhuq;fQf-=ts6qf z1!ps=s8tOcO4x{=Zu*dZJ=){_a_B#qD{GoaKbB%|Gk1@e@^ycYmE2%4w6I3<{5?6{ z?asX~GS=Zd&7B(Iz2t(vthAp=xQ0k?N?}8k>A*Zq=!m0DI|Fh>Fn+D0_uT3DX#5g2 zwPc0&Txwp<%E+(c{96s>KB=K(ZT7FK1nCd7dllh$4^ON-tv0@uIjk$QbC5&RsyOu^{?uuc+GMz`#vmcec16(E9_2l%i+B!*%6$ zVgS`%q}#LryFnKBE}KB()mh+523o>u(g%2kF}2+cWaBG(;g$m%F-UW&_8A5et<%QP zK?+G?0^VlX3V1XaJUCYZ7K)Xp`DtYc=GfaO*L@a+%w&_eRtwn&{{zSO}K+b>~f?!OvwEd^RE6 z?MoBoXc|S1xWmQ+qeV;eT6H~ZLiL+sw7%bYx*kF6TYZ=(e4@ev0?f`ms4ID^lPkMS zYhf0psHhE71~Z3oVn=C>n)o~y^V?ZjcYQB{pobWno4N7AMDfz5l1~#B1{4 z$!oP&NwZMp<^afKSUOGdbwIZm&BVhdr@JV6iI-b!ws~csV|Tkm9aKEEy20-(zkj`y zo7T&U)GBaz9^GpsLTPwQ_c5K5+vwJ&+~X?64Q9!fI!r6zWqvypC)RzQT&Od2MvCcd zVik9Gyt1%o746Gh9TZ3Wq3tI3WO1*l(b&Hy;#ZasNdd_$$q#|0_p@e$R0&juh@fT7 z`|#1erxcgXo@%&FQ&~-hqtN{=&2mW8tF)GsDjCUH<_@4;o)vCM{+U59ESci~FrpY8GA+sE8O zOUV_j$8C@3Kuw?FfJW7S?CCdT7QIP>69e%Fwf`V9bf*h)*~8!*Noirq@nYTUbt@K;ugXg1m} z5eVknWD=6Z9E~Z2T@(PUuLMHmW@2;LxK*O(uR*8u2Fshtv10v!3Iej?V?xSc+b6IA z|D)TGNCMRuBmgK%*qCYaxt8X`=g@fp)!D*|?TO?sqfc&^ZnhMgrrJ~O`E?RL5{jbL zL5S=(8-CY%Ic4<^#T(Pk!PwaJv=JQIF6>>us|NXL7{t09uduaj+!JT}Y;8Z`@0aAJ z$}WcnVP5vHe|H?F$HOYtE8T51gvriu+U56zJuZSUIxlHjGkJK?=qT?iX@>5P0V~;ZQ~L& ze1Rs)&3CJ#a()tsSp+My`DVdQM%)fdC=#3R2^A7kvUcp#e%mrxyF8?^xvZ`XrSI@u zBw%MDdYIm*)XA>OD*UHI(VM!?j*O@7b*0U9td#f~rP{9OK-7fxL!Ps0Hr>Oxg36y3 zjy_nFnD)KVHH)T}W|5D>MrGC+D?@x-rsn5ML%gN#=AHKZ0;k6>&PKqK6htt4F)IH8 z#JL62pqe=C&h^$lmm|zMbwzlthdgPry*{0Fjvk~4PAPAAOuFEfnUUap~(n}FN@+#1&W2O`q6CR!#ZoC!CsY?7Cw#$P-vviZusjc=CH^X-r_sdoMH z$)es6>NN0nxWwYi*!xL`Sv^s(btWKx>b#lO@W63d|DQ$*a-$wQYUQ9&!4!qeleX0 zBFFwNTx{Wj$ApBqB6RTMKjBijktD#tNs5DFJZx6MHf7OGp0})MZ$&u$g`b84{4>$v zr1G9}P+uYXxMK9Te1P;NFJ8n>1VFk!Iwp>84sJ0f5d7W01c4tEF9mje<~NW^LAYdQ zZ&|EP=t1UCo~QZH6+-2!9O-Qs=(L2zQpmRz&T}zkaT|LZ^=MwxCUli%F>ZpZm>O@x2)NSVDfB331q9-Tcl9Lf5Q= zl~Bb5>tAq-E`Bb99+j&W;fu}8%G@BD`!K|YH1FM?$D&jX=$_2dj`|he$Au<+Zy19% z59&*h=JlbCuGoFXPhq{VXd_d?pnmz)&xG;ANiVvRBNG&TYLMaL@U(}s&mVmyWPAex zw9_x#dI1f)tk^2i#(MnM{j4U*4TWGHpnAL)zWS!+LzYEl%q5E+vTEZaX?f!`sHAlzJ8s_PSQWcnttrlb$SKmm1z6P&2G=R zH(na8zwc}f#DoZ&UYPp>7v`^0#3&i1Nf-SOF5s@o1qlaT53VA~%UC45&1`2SBs-Qa z%-%0171@30a8Adp$&LkMWiB3IUv}&rxzh7L)i|c)=g=;h5{lfYwKe(E zbi`SPkemi<-LRxbhR*RYg7)Lse_0KGE^osp>d1987^A6oENK2OAnB{KnqPkJnpPKN z@_i5@X6U-f1!3t6QWW#*g~jwBA`1_qnkITAIh`? zDmZFo?Q@(oFzzd(-f{Kpf+W;dD%5@RK7-)OktyNZovUC0q?>D(c>n|AYy|PKRLxB_ zaQRPmbHUxs+@Yf>M!_^&!TLP*mUx06C%)aNXEO}dv-#cHpy1H?nq2PWej!ys@6Hv8 zp#KV%vzs%KZ>CPK)Kz*D!fMkO!&TualCnboV$j-3nZ(Mpode&~-3P--twx2Z>X)YNgn-cP0=Sm{ybgrN2H659v1~Y`K!g*`NR29a7JS=OP zke$D9alEjbfS~+x3_O(<|1tniXtnLLKXl-IEa`mU_h?8@KJQOqpjD3A?3gC5b3Zyf z__>GNePz9x5+udVSZAiei0Nbe;;LK8tiDG}))ARsLedgx7h?=_UrODLg-03j#; zy&c`VC7dhWPNyEsyal}Y|?-Ti!I5HA$-=$ z-Xif#+ukf5Cv`Ncf%Lm3$A{=`FTW9um%>bb{Pa9KSZuofb;ri1cuS($dEDhbz{E=m zb2?QWCh)?RjX}-pP2m6QlUqHG3C7YtT_WY)zaqv{pA~2`PX{ zCjJV4tHDSpT8{5LJ_ShM*I`3b_FdVlS1xrjYsTU{1X2lPn_{1KD8}Xzl zZ3VscYo9K~_%G*Xhjn5SldJyJL1X_H*L0M0Y$yEw-_8 zj88Raxu$sY!R}Naf)x_f?saIv>lY(GAHrlmzwl8B`)-J!wJL(8GN7$3g4uUbxPs8(;G`NP8AB1G@`CZgHwiKQ?;;PdnI zge&`t3;Jw@$o0kMeKh$^EN{}52~=Ufh^xOf-3~V)au84fX%l@G)+S0?lC$pmmrlpv z_uYyR6q%zeaEGq7Pe&{NqM!G8hPIFioI7-D6RfBXeJ7^1T%{ZORtzu8C1S$96Be^8 z5Z$YxqWg!$6hg|uTy7J5l=~oF{(XG+$lAE+ z0w_Uf_luM!wU*a|Uq=9sA)g3ks@fpdD zE0_+c_Y!$@-|xSF5!+inU)P0kk7rl!Au#jFi8#;&vY&L7K zsy?kq^I>z*k$*8Gt1@s~Q4M#2yB>-{ z`J{RX4lE-UFs=KkaH!gvfyIT++C3Nl^s6sk!=<$2Yuq!$!635|nu?<@3rEA<^s9dY ztx+`XOV~y=%4n2!of{p-W?M1F6nDAcjr|CR%B1Ozb?$CVzg`EXz?V9kqTq8CjE}3;`Xa2H$wiFXcX_=yl;DRV7i+l@6v0 z?0WVomAJcQ?em*pNYOJQTR+$8OYe3T%t9fD1u3t?>>;dFI*YrJUOHcAZ3c<0-~TMO zrPB3RI~b$2aQkf1>iUFY=#xLHB)D}l1erYbu3N|TD3Roth3U$!dv9J_P9MrC}=;J12_GGGEf8PY($$P z4$zvxL3sQ&AD1=U=q;)+m*AJYndj`U5b2V>KGQ>kjn?FRYK8FqbO;v+Gav8Dt*Iss zz&ze4OO8_-=dRZYzSJgx7E*M|Sp;&f`j$bJ3=B3q1M^Ejn{(12eaQ*G_)3eL?;?qh z_uqordB%JL3_a{EClTyXnNW06xsh_uj(}T0s-dkBrw?#Nqcs`VRKucJ3Z(;^ds)z8 zTR9&%)T)0Ul+hMWDW4la;=8sXvht>0HIe8Y%&k6h( z?=PBvK*Y+fJ=Id;%v_lR+k#rWZFwR!r`096Z$zZudiemfr31NIS|J+M3rtlnidEYC zL-6SsPn$ldUUMRI>lK&&i3psa(RN@&SJ$rww@9p`F+k+I`O|ljW~137S>CeDfuf`L zb^UZkqxEU)w~;K~UXR8yp1t2CyN$AdX2xNIwUPdz>i*?a5o4F}CU_qMPRZzSul$TT zG4XwcP9`No5V~7=NUu((fHPv;d38mMmwo`e)b!N99vF z7$W!exEI?>UrQRy+HG)&D$a8M>a+2Ar*L}*6sQt#zO+g3#}riCT*`8?V)j(}*g^nO+GNfd~Mlf>=2#~Y!6zD>anc!Mx4fXtlR8|ShkgwYbdMKk}( zTjDP@8?9Xot>JQtQ&yH0{%`X-2PrJ8h?b12SP9r~W+`x^GZUhP!mLj*`FQ z5@e7KUzL?kX#1V8cQa9haOwgM|x0m)%MquAuc4`v_kkQfPb%sH^G1e$D_nZeB(sr zahNbcMGfhoKMD^L17?W{la_D^IUve7ZNcd1FK1qCiS3<1$ko3@ogxHtuQxHS1OyuQ zG-?R1X0p?*&%3E$;;+;sz|kXIBe*~V8e5njgf?Z#?__Xt0$$FiF`vD7-a@)}Qc)d!)OgBvYpVWvD8%}E7yz4E-uA@SKDo!dfRf_ z)!vY^tTwz8$R2S5O&w6X8afeeifn3#0e*sk{w(OINmkI!B0@ugoBxe_%bocUimn)Y zR6Y1+v#Xor=J(CkmgZCF;%;h-Q<-9MNBefcJ->E8ZU0v=eNcxnQ@35xme`a&!y6XA ze%chNdgFBAegaW9-|)qAPlY$u(z(LA%mg+?jVwYs!igDN!ilNEYT@t^s(D2W+Q^BK z`G9UQ5oU}t%g|CJ-OCofu|@Js6(G&#jlt^((%9!L+5zX@ESJ8P_o5s)N~dBm6G~i7D zwCPf8*&i_-z329`o^PNMOPIReP9RLbk)hMgE@_5YpAH}yJN`0=1CVV^);bdQEy^q6 zsCal)b8Uzw)l0dxnDa==ke|8ji6pLj7U*3dFzjgu7+}kzg}AFL2hp=t+oC&r1gjUi z!ZRb04H9KfK#Hrx4|$x812t1y{#Ko*2A`A;DH+$J$lY*XE=Au+D6M&J?^;?7@@~#q1SR}EW`mF^MkJ>@qPO#AdA-mui&IMRPhgSD>xK zRO{tU{af_ksx$Mes>(L^!yffTQ9hU6Wc~rUQ0b;6fw?QIgmXOUCT{sb!mJEtryyH0 z&0fl-WSlqG*&U=1h0EuXOMP-j_}&;n^Oi1n>UFPHmkRw`nO}%-BcZP6;S#x9 zru`S?2XZWruMcrL=*tYLq$WQN5i#G6s9Dtc&u5?S~3L|f$c!xnn4eQNe53j3h%39OD#?v z(MV|s!yRJ}k|$ngrW!)vUtnZ<&4}=h;HSPzl9Se8#f&or?tQ%tl_zv5TUVdrrv*gz zO@$7k!f|K>4vXO2y{1*}Nw;OmCh)rr^rTC9xwX(qWXetCm3n$>+vQ-tm{@+% zCU>TP*+Bm3{QRU?$RncQy~()uY)yQ56-UajE=HD0k?DEo+~o(lhzAq7)yxtBY zXbmr4WcP#p8!e$*U+@sjlUrURY@x|h^~@$at?4%+ygoNe0z^dxa(3I*__|o}v}a6g zs=sWgqL`ddC%MB7m|dM;lst~o3dfXEe@Wf~R=-VYkr(Cb z%K7lR&ezJUSm!$4u9A~x>+0nTZu zFnR!0b$^a&7U$H`uS(Sn%b+^Sdy!xoNw=(AGQ@=wsVW`ObJ0B?r8DiJUr^HF1f@93 z=7?ft$b#s*!7O7W2g4#Jh}b-azk;-=i?Dx!b@(+ofyV2V<+k?F-}lt~wf$MLM;~0A zHJnq?8AiTKeS{Z2gmB$pRoPpIVP6P|DZWrwx5rE0P;O{RPKk#=@l_5Q&)0Kf&s(5d zzCQ?-zu`L^IqnGAGXo#CV2vuXit_^*^m5L*r?DRy?)HE3dnbkLZ7eBm*tyjP{C`7D z{bvyvZYlO}V9vV6U%Yl;W-e4q~dVaiJ}hP5k*mTQv95JT>}`9%iE;~^{fIb;0tT$gO=@ChdJ$-@^bZL;t25|puN z<<&9SD{|rXkgwnG8*9Gz>yTS#l4Wxa!<&q(`uF$4@Q7Z;FwNQfc=P-p9RT?aX-VPX zdN=P<6m2;sU@UL|@ z@aF?-eP)|`tdqT2NJfT{{R|0*)ycjVWB1nxyEU2*OD_=&L6$}bai0DSrUE|6;q9T@ z%L|uoH_7ZhlQ=K!d>_Xtw~fEzFjd2yL_I8x!bLFuoN+6yE z4GqD&qj2`~4MilGiq;tbr{7;TaRorlw&=AEv7%;VvN<>g>+2klZLa%p-)qp3n-DQC zB%^&OWU&?I&0USmohT6c(HF#3$WGx6 z)<-}kUNg4O`1#Q5Ch@2*A>I4-gp{ig63-80wb`lFBH68_Yd-5v{2fk%CwP>^e)6;g zv(cuU0MqI(a~q&Lkp51Zo#TA)?n)aZcNAdx{%bNAnjwB%T%(L9(d_rl3o^M@&n8ZkAM&`Sz4bsU47f*JQCAEUsE&)H z+_Dfgj)ra?<6xp8qf~b_1lKG?g-Abvv+-o!2j|*8eNy{SEq#Y_7J}%dHqj|fi|C;y zocC=%UUWd8>WP+DPU;~ZU5klnkasPonc)|)uccT|vPMcJDlLd9GTk0ts%w4qqb^iG z`l`Xd=iF*)9lN9$X5N!*{w=ZAaD8!^4jI#1m8>>qru8tp80$I=^f7u)KsabGdCu-xSKRVMF^bVvtOXb^<9vJ@D{BrHG zyM8vbJ53$Bm=HsqSCT>ihK-;@xFJ2?H)FFs z{1l_uMt)>OD6$Ir*27a3{Y_0+zI)>GYCHdW4G(QoZG3kYu^&z|6Mx_B>qs;A>9V3> z+p5eMzc_Bzs=f(;8jK&Mykugce|uK2o%|!%#fXwD=3W-~sS_(0g(z~6&pn?w+d9KO zqMnU)oZUFkq7!yZA||qZLb`Xvc0sDzH%Hk@Ck7ibi2;Ep-E#6(CMIDRyDh*I@cnwn zRS}m?918o|z(J(9HlDTCbs8Jt0LL=Zs}Yy=f(CR=lGj|vAH=h0{#={xd`$oY`jUoq+S2@RQ!;U~D10y5Y z$@zC6Ic|j|amDT94OI--Y`)|TY6QDbdJ~1X;pi_mHPQ9+)~dP}Oyl(~AVQNtGEO1F z!xuNYS*l@MWMTCM+Tdxq`d38MJUDjib&NLsSL@+O|9Ad1G>=lp3nyxoP+iO=tN}XfnmK^bb6#6$OSsS{nvqYx~7M zC930w7UiXvXZ}tt@`YT0ib62%*g6{2(>JeE3>_J=cdno5gkB<9clXR4i>G7Qg%8nx zDjSay!{)^JZ0=|@tE;sE|rF~D}8wU+C>ptGvT{#kJwQ5 zM63B1$Y5(!o+b{wG4q4cSHV0iH(5U>*6Y0gf$jBnkgX{wtsE4*Suv2*VqV z^y7&YyeKt5@QANg=20~J8cweDjl^5V>p5|!j6+)dI_AnV4HSAMYyPF~U9!`t@raJL zvW5K%!!=pj6dB=c1=7M&+LV#TAN|Qe0I1rqZK?CtiSAJezl8fbiBh`suC{4tcNu%m}nKn!Fs>9|CWg7BB)ITI#7ohTm zGRr$Dl4#tn_F`xgE$VC31`IT4yo*AL@_{j<TRJ^WFrs|% zbFkumuXFU(E&{9}$OT-bK$3Dvi=b5uHPK^*wc~$l9|nE7hr6I`3Y4}1(GdlSSD^aPx_xEUKy`@ zSVEj-V`w$a#Q1PPyZEXWjl`VxFAK#5&(_x6XN&M{KiMqTZd4T=gWL3=1;kFS@mHv$$jwpK%8TF;Eb*u-G0owm8 zjxI$cI+Iz8fof!_B5>E1ETnApn@;arJ{$QZgQ4_Zf7n)}o7CBYSqOtj%ZPGTlYwdS z^;4F`6g$HdZ3Wy~m0sT*aL%Y+VUgqTdDL(40H}G@jI8{TG)dGt)4hMsLf^JB3rF!qlUGlA zAB$SNr!>q-%K#WGzEiSmBd*9d4SJ+0pEx50daqE=h`7n+uab?|yFQ;ikJs7d>1NOFLKztwaqjPe?K;zQ9pwrKEaG8#rjW|b=C||~ zJL>xWug}7b^4=Q<=q#X4GDfKH_JvS`Ud7NcE|E-zyE0pK-zb}T{-mM?0^?2Sc{INN zYw~v6ls$c8gLj9zrJctuhJ>Zu_Z)Su%zJ5unstX>O!P=@YhSu@O{M3>oQB=A z=4q9k=t&`7f&7v&J&?`rtUk$UjjDx3JyZ}2xH%OEL%h$m;&J4-m)S)5yoq1c5- zwk7$ux1h;WJo&L`qh}mIAO28{D4#qzcX7e$xbG@7jLCTi!e(OJriR zkuG_(Hy}AfZ6O@D=CZM@h~K>Qq{lS*@E2aBfQao;F#F<1a-HiW$7)Tr!ilglpkQ^0 zt>@~!=@|>)gw0iT5&n@vg!KI_%byd+f;(q~4?ieX!AT14?2kn8B8xl8KxuR-V=Yt+ zT${vYi`LU=HWUwXbL1CDGQuCLt5ZIgH`l_mO?iDGk182bd4~=rr`QFiJS=4y(-|3I zUYJR5R)ZS94sGs-z}rqb5bk3o;BQ7q=Hk`&&zj?fIGe~&6!XID(wg206_v+z@|nsZ zcL<~0y!@@SP1;7#RIP0;|`&S>ohk0OzB3$48;$ifp2llEviW>dnMv1{pn*nbN$iy4HV%hj{5wu1YC+-NV!Fua&By70xUgYPwxS^ z&B)S@4u@>Kegt+D*gf14U*$CV8)HEP`xbfarx@$p;dd5aUy+(;g@LXWNB9J(NQA|Z zs!E8pUT-8UhIVaV-z)ub0z%Q8^kbR*AYlk!~YrfVwL(!NB36}cq? zS#8ES6*Mxcf`8KHzqCKTNe<0{Ven^`$3w2U-l@e3pRU+9JZ#t6)=BoR4r=}0(q^3R z_{YIVf`nY>f`0+|yO)bfX=fSoUz;zU!rmt7|CUgVv3BV4od~pU+@WN%E-s_s_>jWe zL(AsGgt)TL_g#8Vo3a(+wDBOOFTN0ex-%Y5)jAo=FXf-~5koGhKRiC@t>ZvKc}ZXn zsCv7n$-xlU70++*0rz0c^mN6Ezi^`Bvs&alcC)1QwWHwvpXYi&>wE5&vcB3$H;0)4 zmK=!DpC_)as3S-Qp@5{Zo(@1pir^&Pwy-MTK@QDXX4P&tg~uJE&MNRm@*%(FBQ&%GtU3UaON-)tu+C$eImO&_<1Qb37xv<8En{{0lMt=-{$OUp=h}*Y{bkuca(pV(j5u+{O1}uVN}6 zgMj%KjLhCexcqkj)<-&+Mm~XXuZ*$scz+N=?gOx@eGJ)W7=khp)DQJ>ay*d7u6)#EX?mqZOGz z9jrZ}6B_80aCa)Mm61kNv&Mg_uzDAC`cU!{#W?9+(bNCcA+`VUnBLuie?hA6zY50V zSO2-|?K8o08~)}0;w5kRAOFuB^x@;f72M;?c1Pq_D<_hF2CMX5UA|P-^2>h#@}Dq~ literal 0 HcmV?d00001 diff --git a/docs/ble/_static/css/badge_only.css b/docs/ble/_static/css/badge_only.css index 01515ab8..9cf316b6 100644 --- a/docs/ble/_static/css/badge_only.css +++ b/docs/ble/_static/css/badge_only.css @@ -1,4 +1,4 @@ /* badge_only.css/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */ -/* This copyright was auto-generated on Wed Mar 13 20:30:11 UTC 2024 */ +/* This copyright was auto-generated on Tue Apr 9 19:25:33 UTC 2024 */ .clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/docs/ble/_static/documentation_options.js b/docs/ble/_static/documentation_options.js index 202a4e2a..38dc7ee0 100644 --- a/docs/ble/_static/documentation_options.js +++ b/docs/ble/_static/documentation_options.js @@ -1,5 +1,5 @@ /* documentation_options.js/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */ -/* This copyright was auto-generated on Wed Mar 13 20:30:11 UTC 2024 */ +/* This copyright was auto-generated on Tue Apr 9 19:25:34 UTC 2024 */ const DOCUMENTATION_OPTIONS = { VERSION: '0.0.1', diff --git a/docs/ble/_static/jquery.js b/docs/ble/_static/jquery.js index b36282ae..0b386427 100644 --- a/docs/ble/_static/jquery.js +++ b/docs/ble/_static/jquery.js @@ -1,5 +1,5 @@ /* jquery.js/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */ -/* This copyright was auto-generated on Wed Mar 13 20:30:11 UTC 2024 */ +/* This copyright was auto-generated on Tue Apr 9 19:25:33 UTC 2024 */ /*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="

",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0
"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t
  • Query
  • Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Set Date / Time

    http://10.5.5.9:8080/gopro/camera/control/set_ui_controller

    Response samples

    Content type
    application/json
    { }

    Set Date / Time

    Limitations <hr> " class="sc-iKOmoZ sc-cCzLxZ WVNwY VEBGS">

    HERO12 Black HERO11 Black Mini - HERO11 Black -HERO10 Black - HERO9 Black

    + HERO11 Black

    Supported Protocols:

    • USB
    • @@ -3031,9 +3035,9 @@

      Limitations

    id
    integer <int32>

    Unique preset identifier

    -
    is_fixed
    boolean
    isFixed
    boolean

    Is this preset mutable?

    -
    is_modified
    boolean
    isModified
    boolean

    Has the preset been modified from the factory defaults?

    mode
    integer (EnumFlatMode)
    Enum: -1 4 5 12 13 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
    Limitations -
    Array of objects (PresetSetting)
    Array
    id
    integer <int32>
    Array of objects (PresetSetting)
    Array
    id
    integer <int32>

    Setting identifier

    -
    is_caption
    boolean
    isCaption
    boolean

    Does this setting appear on the Preset "pill" in the camera UI?

    value
    integer <int32>

    Setting value

    -
    title_id
    integer (EnumPresetTitle)
    Enum: 0 1 2 3 4 5 6 7 8 9 10 11 13 14 16 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 82 83 93 94
    titleId
    integer (EnumPresetTitle)
    Enum: 0 1 2 3 4 5 6 7 8 9 10 11 13 14 16 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 82 83 93 94
    Limitations
    -
    title_number
    integer <int32>
    titleNumber
    integer <int32>

    Preset title number

    -
    user_defined
    boolean
    userDefined
    boolean

    Is this preset user defined?

    -
    {
    • "icon": 0,
    • "id": 0,
    • "is_fixed": true,
    • "is_modified": true,
    • "mode": -1,
    • "setting_array": [
      ],
    • "title_id": 0,
    • "title_number": 0,
    • "user_defined": true
    }

    PresetGroup

    can_add_preset
    boolean
    {
    • "icon": 0,
    • "id": 0,
    • "isFixed": true,
    • "isModified": true,
    • "mode": -1,
    • "settingArray": [
      ],
    • "titleId": 0,
    • "titleNumber": 0,
    • "userDefined": true
    }

    PresetGroup

    canAddPreset
    boolean

    Is there room in the group to add additional Presets?

    icon
    integer (EnumPresetGroupIcon)
    Enum: 0 1 2 3 4 5 6 7
    Limitations
    -
    Array of objects (Preset)
    Array of objects (Preset)

    Array of Presets contained in this Preset Group

    Array
    icon
    integer (EnumPresetIcon)
    Enum: 0 1 2 3 4 5 6 7 8 9 10 11 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 58 59 60 61 62 63 64 65 66 67 70 71 73 74 75 76 77 78 79 1000 1001
    Limitations
    id
    integer <int32>

    Unique preset identifier

    -
    is_fixed
    boolean
    isFixed
    boolean

    Is this preset mutable?

    -
    is_modified
    boolean
    isModified
    boolean

    Has the preset been modified from the factory defaults?

    mode
    integer (EnumFlatMode)
    Enum: -1 4 5 12 13 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
    Limitations
    -
    Array of objects (PresetSetting)
    Array
    id
    integer <int32>
    Array of objects (PresetSetting)
    Array
    id
    integer <int32>

    Setting identifier

    -
    is_caption
    boolean
    isCaption
    boolean

    Does this setting appear on the Preset "pill" in the camera UI?

    value
    integer <int32>

    Setting value

    -
    title_id
    integer (EnumPresetTitle)
    Enum: 0 1 2 3 4 5 6 7 8 9 10 11 13 14 16 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 82 83 93 94
    titleId
    integer (EnumPresetTitle)
    Enum: 0 1 2 3 4 5 6 7 8 9 10 11 13 14 16 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 82 83 93 94
    Limitations
    -
    title_number
    integer <int32>
    titleNumber
    integer <int32>

    Preset title number

    -
    user_defined
    boolean
    userDefined
    boolean

    Is this preset user defined?

    -
    {
    • "can_add_preset": true,
    • "icon": 0,
    • "id": 1000,
    • "preset_array": [
      ]
    }

    PresetSetting

    id
    integer <int32>
    {
    • "canAddPreset": true,
    • "icon": 0,
    • "id": 1000,
    • "presetArray": [
      ]
    }

    PresetSetting

    id
    integer <int32>

    Setting identifier

    -
    is_caption
    boolean
    isCaption
    boolean

    Does this setting appear on the Preset "pill" in the camera UI?

    value
    integer <int32>

    Setting value

    -
    {
    • "id": 0,
    • "is_caption": true,
    • "value": 0
    }

    SingleMediaListItem

    cre
    required
    integer
    Example: "1696600109"
    {
    • "id": 0,
    • "isCaption": true,
    • "value": 0
    }

    SingleMediaListItem

    - + - +
    cre
    required
    integer
    Example: "1696600109"

    Creation time in seconds since epoch

    glrv
    integer
    Example: "817767"

    Low resolution video size

    @@ -6235,12 +6239,12 @@

    Limitations

    </thead> <tbody><tr> <td>0</td> -<td>OFF</td> +<td>Off</td> <td><img src="https://img.shields.io/badge/HERO11%20Black-ffe119" alt="HERO11 Black"><img src="https://img.shields.io/badge/HERO10%20Black-3cb44b" alt="HERO10 Black"><img src="https://img.shields.io/badge/HERO9%20Black-e6194b" alt="HERO9 Black"></td> </tr> <tr> <td>1</td> -<td>ON</td> +<td>On</td> <td><img src="https://img.shields.io/badge/HERO11%20Black-ffe119" alt="HERO11 Black"><img src="https://img.shields.io/badge/HERO10%20Black-3cb44b" alt="HERO10 Black"><img src="https://img.shields.io/badge/HERO9%20Black-e6194b" alt="HERO9 Black"></td> </tr> </tbody></table> @@ -6258,12 +6262,12 @@

    Limitations

    0OFFOff HERO11 BlackHERO10 BlackHERO9 Black
    1ONOn HERO11 BlackHERO10 BlackHERO9 Black
    @@ -8813,6 +8817,28 @@

    Limitations

    Charging
    +
    3
    integer
    Enum: 0 1

    Is an external battery connected?

    +

    HERO12 Black + HERO11 Black +HERO10 Black + HERO9 Black

    +
    4
    integer [ 0 .. 100 ]

    External battery power level in percent

    +

    HERO12 Black + HERO11 Black +HERO10 Black + HERO9 Black

    +
    5
    integer

    Unused

    6
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    7
    integer

    Unused

    8
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    12
    integer

    Unused

    13
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    14
    integer

    When broadcasting (Live Stream), this is the broadcast duration (seconds) so far; 0 otherwise

    +

    HERO12 Black +HERO11 Black + HERO10 Black +HERO9 Black

    +
    15
    integer

    (DEPRECATED) Number of Broadcast viewers

    +
    16
    integer

    (DEPRECATED) Broadcast B-Status

    17
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    18
    integer

    Unused

    19
    integer
    Enum: 0 1 2 3 4
    Limitations Completed +
    25
    integer

    Unused

    26
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    36
    integer

    Total number of group photos on sdcard

    +

    HERO11 Black + HERO10 Black +HERO9 Black

    +
    37
    integer

    Total number of group videos on sdcard

    +

    HERO11 Black Mini + HERO11 Black +HERO10 Black + HERO9 Black

    38
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    40
    string

    Current date/time (format: %YY%mm%dd%HH%MM%SS, all values in hex)

    41
    integer
    Enum: 0 1 2 3 4 5 6 7 8 9 10
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    43
    integer

    Current mode group (deprecated in HERO8)

    +
    44
    integer

    Current submode (deprecated in HERO8)

    45
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    46
    integer
    Enum: 0 1

    Are Video Protune settings currently factory default?

    +
    47
    integer
    Enum: 0 1

    Are Photo Protune settings currently factory default?

    +
    48
    integer
    Enum: 0 1

    Are Multishot Protune settings currently factory default?

    49
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    50
    integer

    Unused

    +
    51
    integer

    Unused

    +
    52
    integer

    Unused

    +
    53
    integer

    Unused

    54
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    57
    integer

    Time in milliseconds since system was booted

    58
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    61
    integer
    Enum: 0 1 2

    The current state of camera analytics

    +

    HERO12 Black + HERO11 Black Mini +HERO11 Black + HERO10 Black +HERO9 Black

    + + + + + + + + + + + + + + + + + + + +
    ValueMeaning
    0Not ready
    1Ready
    2On connect
    +
    62
    integer
    Value: 0

    The size (units??) of the analytics file

    +

    HERO11 Black Mini + HERO11 Black +HERO10 Black + HERO9 Black

    + + + + + + + + + + + +
    ValueMeaning
    0Value hard-coded by BOSS in libgpCtrlD/src/camera_status.cpp
    +
    63
    integer
    Enum: 0 1

    Is the camera currently in a contextual menu (e.g. Preferences)?

    +

    HERO11 Black Mini + HERO11 Black +HERO10 Black + HERO9 Black

    64
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    71
    integer

    The current video group flatmode (id)

    +
    72
    integer

    The current photo group flatmode (id)

    +
    73
    integer

    The current timelapse group flatmode (id)

    74
    integer
    Enum: 0 1 2
    Limitations " class="sc-iKOmoZ sc-cCzLxZ WVNwY jaVotg">

    Is the camera currently in First Time Use (FTU) UI flow?

    HERO10 Black HERO9 Black

    +
    80
    integer
    Enum: -1 0 1 2 3 4 8

    Secondary Storage Status (exclusive to Superbank)

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ValueMeaning
    -1Unknown
    0OK
    1SD Card Full
    2SD Card Removed
    3SD Card Format Error
    4SD Card Busy
    8SD Card Swapped
    81
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    84
    integer

    Current Capture Delay value (HERO7 only)

    85
    integer
    Enum: 0 1
    Limitations 270 degrees (laying on left side) +
    87
    integer
    Enum: 0 1

    Can camera use high resolution/fps (based on temperature)? (HERO7 Silver/White only)

    88
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    90
    integer
    Enum: 0 1

    Are current flatmode's Protune settings factory default?

    +

    HERO12 Black + HERO11 Black Mini +HERO11 Black + HERO10 Black +HERO9 Black

    +
    91
    integer
    Enum: 0 1

    Are system logs ready to be downloaded?

    +

    HERO12 Black + HERO11 Black Mini +HERO11 Black + HERO10 Black +HERO9 Black

    +
    92
    integer
    Enum: 0 1

    Is Timewarp 1x active?

    93
    integer
    Limitations <img src="https://img.shields.io/badge/HERO10%20Black-3cb44b" alt="HERO10 Black"> <img src="https://img.shields.io/badge/HERO9%20Black-e6194b" alt="HERO9 Black"></p> " class="sc-iKOmoZ sc-cCzLxZ WVNwY jaVotg">

    Is Scheduled Capture set?

    +

    HERO12 Black + HERO11 Black +HERO10 Black + HERO9 Black

    +
    109
    integer
    Enum: 0 1

    Is the camera in the process of creating a custom preset?

    HERO12 Black HERO11 Black HERO10 Black @@ -10615,7 +10919,7 @@

    Limitations

    HERO12 Black HERO11 Black Mini HERO11 Black

    -
    {
    • "settings": {
      },
    • "status": {
      }
    }

    VideoMetadata

    ao
    required
    string
    Enum: "auto" "wind" "stereo" "off"
    Example: "auto"
    {
    • "settings": {
      },
    • "status": {
      }
    }

    VideoMetadata

    ao
    required
    string
    Enum: "auto" "wind" "stereo" "off"
    Example: "auto"

    Audio option

    avc_profile
    required
    integer [ 0 .. 255 ]
    Example: "0"

    Advanced Video Code Profile

    @@ -11487,7 +11791,7 @@

    Limitations

    " class="sc-iKOmoZ sc-cCzLxZ WVNwY jaVotg">

    start index of range

    Array of objects (PresetGroup)

    Array of Preset Groups

    -
    Array
    can_add_preset
    boolean
    Array
    canAddPreset
    boolean

    Is there room in the group to add additional Presets?

    icon
    integer (EnumPresetGroupIcon)
    Enum: 0 1 2 3 4 5 6 7
    Limitations
    -
    Array of objects (Preset)
    Array of objects (Preset)

    Array of Presets contained in this Preset Group

    Array
    icon
    integer (EnumPresetIcon)
    Enum: 0 1 2 3 4 5 6 7 8 9 10 11 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 58 59 60 61 62 63 64 65 66 67 70 71 73 74 75 76 77 78 79 1000 1001
    Limitations
    id
    integer <int32>

    Unique preset identifier

    -
    is_fixed
    boolean
    isFixed
    boolean

    Is this preset mutable?

    -
    is_modified
    boolean
    isModified
    boolean

    Has the preset been modified from the factory defaults?

    mode
    integer (EnumFlatMode)
    Enum: -1 4 5 12 13 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
    Limitations
    -
    Array of objects (PresetSetting)
    Array
    id
    integer <int32>
    Array of objects (PresetSetting)
    Array
    id
    integer <int32>

    Setting identifier

    -
    is_caption
    boolean
    isCaption
    boolean

    Does this setting appear on the Preset "pill" in the camera UI?

    value
    integer <int32>

    Setting value

    -
    title_id
    integer (EnumPresetTitle)
    Enum: 0 1 2 3 4 5 6 7 8 9 10 11 13 14 16 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 82 83 93 94
    titleId
    integer (EnumPresetTitle)
    Enum: 0 1 2 3 4 5 6 7 8 9 10 11 13 14 16 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 82 83 93 94
    Limitations
    -
    title_number
    integer <int32>
    titleNumber
    integer <int32>

    Preset title number

    -
    user_defined
    boolean
    userDefined
    boolean

    Is this preset user defined?

    Response samples

    Content type
    application/json
    {
    • "customIconIds": [
      ],
    • "customTitleIds": [
      ],
    • "presetGroupArray": [
      ]
    }

    Load Preset by ID

    http://10.5.5.9:8080/gopro/camera/presets/get

    Response samples

    Content type
    application/json
    {
    • "customIconIds": [
      ],
    • "customTitleIds": [
      ],
    • "presetGroupArray": [
      ]
    }

    Load Preset by ID

    Limitations </thead> <tbody><tr> <td>0</td> -<td>OFF</td> +<td>Off</td> <td><img src="https://img.shields.io/badge/HERO11%20Black-ffe119" alt="HERO11 Black"><img src="https://img.shields.io/badge/HERO10%20Black-3cb44b" alt="HERO10 Black"><img src="https://img.shields.io/badge/HERO9%20Black-e6194b" alt="HERO9 Black"></td> </tr> <tr> <td>1</td> -<td>ON</td> +<td>On</td> <td><img src="https://img.shields.io/badge/HERO11%20Black-ffe119" alt="HERO11 Black"><img src="https://img.shields.io/badge/HERO10%20Black-3cb44b" alt="HERO10 Black"><img src="https://img.shields.io/badge/HERO9%20Black-e6194b" alt="HERO9 Black"></td> </tr> </tbody></table> @@ -15366,12 +15670,12 @@

    Limitations

    0 -OFF +Off HERO11 BlackHERO10 BlackHERO9 Black 1 -ON +On HERO11 BlackHERO10 BlackHERO9 Black @@ -17921,6 +18225,28 @@

    Limitations

    Charging +
    3
    integer
    Enum: 0 1

    Is an external battery connected?

    +

    HERO12 Black + HERO11 Black +HERO10 Black + HERO9 Black

    +
    4
    integer [ 0 .. 100 ]

    External battery power level in percent

    +

    HERO12 Black + HERO11 Black +HERO10 Black + HERO9 Black

    +
    5
    integer

    Unused

    6
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    7
    integer

    Unused

    8
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    12
    integer

    Unused

    13
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    14
    integer

    When broadcasting (Live Stream), this is the broadcast duration (seconds) so far; 0 otherwise

    +

    HERO12 Black +HERO11 Black + HERO10 Black +HERO9 Black

    +
    15
    integer

    (DEPRECATED) Number of Broadcast viewers

    +
    16
    integer

    (DEPRECATED) Broadcast B-Status

    17
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    18
    integer

    Unused

    19
    integer
    Enum: 0 1 2 3 4
    Limitations Completed +
    25
    integer

    Unused

    26
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    36
    integer

    Total number of group photos on sdcard

    +

    HERO11 Black + HERO10 Black +HERO9 Black

    +
    37
    integer

    Total number of group videos on sdcard

    +

    HERO11 Black Mini + HERO11 Black +HERO10 Black + HERO9 Black

    38
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    40
    string

    Current date/time (format: %YY%mm%dd%HH%MM%SS, all values in hex)

    41
    integer
    Enum: 0 1 2 3 4 5 6 7 8 9 10
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    43
    integer

    Current mode group (deprecated in HERO8)

    +
    44
    integer

    Current submode (deprecated in HERO8)

    45
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    46
    integer
    Enum: 0 1

    Are Video Protune settings currently factory default?

    +
    47
    integer
    Enum: 0 1

    Are Photo Protune settings currently factory default?

    +
    48
    integer
    Enum: 0 1

    Are Multishot Protune settings currently factory default?

    49
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    50
    integer

    Unused

    +
    51
    integer

    Unused

    +
    52
    integer

    Unused

    +
    53
    integer

    Unused

    54
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    57
    integer

    Time in milliseconds since system was booted

    58
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    61
    integer
    Enum: 0 1 2

    The current state of camera analytics

    +

    HERO12 Black + HERO11 Black Mini +HERO11 Black + HERO10 Black +HERO9 Black

    + + + + + + + + + + + + + + + + + + + +
    ValueMeaning
    0Not ready
    1Ready
    2On connect
    +
    62
    integer
    Value: 0

    The size (units??) of the analytics file

    +

    HERO11 Black Mini + HERO11 Black +HERO10 Black + HERO9 Black

    + + + + + + + + + + + +
    ValueMeaning
    0Value hard-coded by BOSS in libgpCtrlD/src/camera_status.cpp
    +
    63
    integer
    Enum: 0 1

    Is the camera currently in a contextual menu (e.g. Preferences)?

    +

    HERO11 Black Mini + HERO11 Black +HERO10 Black + HERO9 Black

    64
    integer
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    71
    integer

    The current video group flatmode (id)

    +
    72
    integer

    The current photo group flatmode (id)

    +
    73
    integer

    The current timelapse group flatmode (id)

    74
    integer
    Enum: 0 1 2
    Limitations " class="sc-iKOmoZ sc-cCzLxZ WVNwY jaVotg">

    Is the camera currently in First Time Use (FTU) UI flow?

    HERO10 Black HERO9 Black

    +
    80
    integer
    Enum: -1 0 1 2 3 4 8

    Secondary Storage Status (exclusive to Superbank)

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ValueMeaning
    -1Unknown
    0OK
    1SD Card Full
    2SD Card Removed
    3SD Card Format Error
    4SD Card Busy
    8SD Card Swapped
    81
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    84
    integer

    Current Capture Delay value (HERO7 only)

    85
    integer
    Enum: 0 1
    Limitations 270 degrees (laying on left side) +
    87
    integer
    Enum: 0 1

    Can camera use high resolution/fps (based on temperature)? (HERO7 Silver/White only)

    88
    integer
    Enum: 0 1
    Limitations HERO11 Black HERO10 Black HERO9 Black

    +
    90
    integer
    Enum: 0 1

    Are current flatmode's Protune settings factory default?

    +

    HERO12 Black + HERO11 Black Mini +HERO11 Black + HERO10 Black +HERO9 Black

    +
    91
    integer
    Enum: 0 1

    Are system logs ready to be downloaded?

    +

    HERO12 Black + HERO11 Black Mini +HERO11 Black + HERO10 Black +HERO9 Black

    +
    92
    integer
    Enum: 0 1

    Is Timewarp 1x active?

    93
    integer
    Limitations <img src="https://img.shields.io/badge/HERO10%20Black-3cb44b" alt="HERO10 Black"> <img src="https://img.shields.io/badge/HERO9%20Black-e6194b" alt="HERO9 Black"></p> " class="sc-iKOmoZ sc-cCzLxZ WVNwY jaVotg">

    Is Scheduled Capture set?

    +

    HERO12 Black + HERO11 Black +HERO10 Black + HERO9 Black

    +
    109
    integer
    Enum: 0 1

    Is the camera in the process of creating a custom preset?

    HERO12 Black HERO11 Black HERO10 Black @@ -19725,11 +20329,9 @@

    Limitations

    HERO11 Black

    Response samples

    Content type
    application/json
    {
    • "settings": {
      },
    • "status": {
      }
    }

    Get Date / Time

    http://10.5.5.9:8080/gopro/camera/state

    Response samples

    Content type
    application/json
    {
    • "settings": {
      },
    • "status": {
      }
    }

    Get Date / Time

    Limitations <hr> " class="sc-iKOmoZ sc-cCzLxZ WVNwY VEBGS">

    HERO12 Black HERO11 Black Mini - HERO11 Black -HERO10 Black - HERO9 Black

    + HERO11 Black

    Supported Protocols:

    • USB
    • @@ -20005,9 +20605,9 @@

      JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Aspect Ratio (108)

    http://10.5.5.9:8080/gopro/camera/setting?setting=134&option=3

    Response samples

    Content type
    application/json
    { }

    Aspect Ratio (108)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 3 4
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Aspect Ratio (192)

    http://10.5.5.9:8080/gopro/camera/setting?setting=108&option=4

    Response samples

    Content type
    application/json
    { }

    Aspect Ratio (192)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 3
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Auto Power Down (59)

    http://10.5.5.9:8080/gopro/camera/setting?setting=192&option=3

    Response samples

    Content type
    application/json
    { }

    Auto Power Down (59)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Bit Depth (183)

    http://10.5.5.9:8080/gopro/camera/setting?setting=59&option=12

    Response samples

    Content type
    application/json
    { }

    Bit Depth (183)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 2
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Bit Rate (182)

    http://10.5.5.9:8080/gopro/camera/setting?setting=183&option=2

    Response samples

    Content type
    application/json
    { }

    Bit Rate (182)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1
    Example: 1
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Controls (175)

    http://10.5.5.9:8080/gopro/camera/setting?setting=182&option=1

    Response samples

    Content type
    application/json
    { }

    Controls (175)

    HERO12 Black HERO11 Black

    @@ -20359,9 +20959,9 @@

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Duration (172)

    http://10.5.5.9:8080/gopro/camera/setting?setting=175&option=1

    Response samples

    Content type
    application/json
    { }

    Duration (172)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 2 3 4 5 6 7 8 9
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Easy Mode Speed (176)

    http://10.5.5.9:8080/gopro/camera/setting?setting=172&option=9

    Response samples

    Content type
    application/json
    { }

    Easy Mode Speed (176)

    HERO12 Black HERO11 Black

    @@ -21169,9 +21769,9 @@

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Enable Night Photo (177)

    http://10.5.5.9:8080/gopro/camera/setting?setting=176&option=137

    Response samples

    Content type
    application/json
    { }

    Enable Night Photo (177)

    HERO11 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Frames Per Second (3)

    http://10.5.5.9:8080/gopro/camera/setting?setting=177&option=1

    Response samples

    Content type
    application/json
    { }

    Frames Per Second (3)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Framing (193)

    http://10.5.5.9:8080/gopro/camera/setting?setting=3&option=13

    Response samples

    Content type
    application/json
    { }

    Framing (193)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 2
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    GPS (83)

    http://10.5.5.9:8080/gopro/camera/setting?setting=193&option=2

    Response samples

    Content type
    application/json
    { }

    GPS (83)

    HERO11 Black @@ -21407,12 +22007,12 @@

    JSON

    </thead> <tbody><tr> <td>0</td> -<td>OFF</td> +<td>Off</td> <td><img src="https://img.shields.io/badge/HERO11%20Black-ffe119" alt="HERO11 Black"><img src="https://img.shields.io/badge/HERO10%20Black-3cb44b" alt="HERO10 Black"><img src="https://img.shields.io/badge/HERO9%20Black-e6194b" alt="HERO9 Black"></td> </tr> <tr> <td>1</td> -<td>ON</td> +<td>On</td> <td><img src="https://img.shields.io/badge/HERO11%20Black-ffe119" alt="HERO11 Black"><img src="https://img.shields.io/badge/HERO10%20Black-3cb44b" alt="HERO10 Black"><img src="https://img.shields.io/badge/HERO9%20Black-e6194b" alt="HERO9 Black"></td> </tr> </tbody></table> @@ -21426,20 +22026,20 @@

    JSON

    0 -OFF +Off HERO11 BlackHERO10 BlackHERO9 Black 1 -ON +On HERO11 BlackHERO10 BlackHERO9 Black

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    HindSight (167)

    http://10.5.5.9:8080/gopro/camera/setting?setting=83&option=1

    Response samples

    Content type
    application/json
    { }

    HindSight (167)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Horizon Leveling (150)

    http://10.5.5.9:8080/gopro/camera/setting?setting=167&option=4

    Response samples

    Content type
    application/json
    { }

    Horizon Leveling (150)

    HERO11 Black

    path Parameters
    option
    required
    integer
    Enum: 0 2
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Horizon Leveling (151)

    http://10.5.5.9:8080/gopro/camera/setting?setting=150&option=2

    Response samples

    Content type
    application/json
    { }

    Horizon Leveling (151)

    HERO11 Black

    path Parameters
    option
    required
    integer
    Enum: 0 2
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Hypersmooth (135)

    http://10.5.5.9:8080/gopro/camera/setting?setting=151&option=2

    Response samples

    Content type
    application/json
    { }

    Hypersmooth (135)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Interval (171)

    http://10.5.5.9:8080/gopro/camera/setting?setting=135&option=100

    Response samples

    Content type
    application/json
    { }

    Interval (171)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 2 3 4 5 6 7 8 9 10
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Lapse Mode (187)

    http://10.5.5.9:8080/gopro/camera/setting?setting=171&option=10

    Response samples

    Content type
    application/json
    { }

    Lapse Mode (187)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 2 3 4 5 6 7
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Lens (121)

    http://10.5.5.9:8080/gopro/camera/setting?setting=187&option=7

    Response samples

    Content type
    application/json
    { }

    Lens (121)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Lens (122)

    http://10.5.5.9:8080/gopro/camera/setting?setting=121&option=11

    Response samples

    Content type
    application/json
    { }

    Lens (122)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Max Lens (162)

    http://10.5.5.9:8080/gopro/camera/setting?setting=122&option=102

    Response samples

    Content type
    application/json
    { }

    Max Lens (162)

    HERO11 Black @@ -22145,9 +22745,9 @@

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Max Lens Mod (189)

    http://10.5.5.9:8080/gopro/camera/setting?setting=162&option=1

    Response samples

    Content type
    application/json
    { }

    Max Lens Mod (189)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 2
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Max Lens Mod Enable (190)

    http://10.5.5.9:8080/gopro/camera/setting?setting=189&option=2

    Response samples

    Content type
    application/json
    { }

    Max Lens Mod Enable (190)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Media Format (128)

    http://10.5.5.9:8080/gopro/camera/setting?setting=190&option=1

    Response samples

    Content type
    application/json
    { }

    Media Format (128)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Photo Mode (191)

    http://10.5.5.9:8080/gopro/camera/setting?setting=128&option=26

    Response samples

    Content type
    application/json
    { }

    Photo Mode (191)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Profiles (184)

    http://10.5.5.9:8080/gopro/camera/setting?setting=191&option=1

    Response samples

    Content type
    application/json
    { }

    Profiles (184)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 2
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Resolution (2)

    http://10.5.5.9:8080/gopro/camera/setting?setting=184&option=2

    Response samples

    Content type
    application/json
    { }

    Resolution (2)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Time Lapse Digital Lenses (123)

    http://10.5.5.9:8080/gopro/camera/setting?setting=2&option=111

    Response samples

    Content type
    application/json
    { }

    Time Lapse Digital Lenses (123)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Trail Length (179)

    http://10.5.5.9:8080/gopro/camera/setting?setting=123&option=102

    Response samples

    Content type
    application/json
    { }

    Trail Length (179)

    HERO12 Black @@ -22741,9 +23341,9 @@

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Video Mode (180)

    http://10.5.5.9:8080/gopro/camera/setting?setting=179&option=3

    Response samples

    Content type
    application/json
    { }

    Video Mode (180)

    HERO11 Black

    path Parameters
    option
    required
    integer
    Enum: 0 101 102
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Video Mode (186)

    http://10.5.5.9:8080/gopro/camera/setting?setting=180&option=102

    Response samples

    Content type
    application/json
    { }

    Video Mode (186)

    HERO12 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 2
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Video Performance Mode (173)

    http://10.5.5.9:8080/gopro/camera/setting?setting=186&option=2

    Response samples

    Content type
    application/json
    { }

    Video Performance Mode (173)

    HERO10 Black

    path Parameters
    option
    required
    integer
    Enum: 0 1 2
    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Webcam Digital Lenses (43)

    http://10.5.5.9:8080/gopro/camera/setting?setting=173&option=2

    Response samples

    Content type
    application/json
    { }

    Webcam Digital Lenses (43)

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Wireless Band (178)

    http://10.5.5.9:8080/gopro/camera/setting?setting=43&option=4

    Response samples

    Content type
    application/json
    { }

    Wireless Band (178)

    HERO12 Black @@ -23023,9 +23623,9 @@

    JSON

    Responses

    Response Schema: application/json
    object

    Response samples

    Content type
    application/json
    { }

    Webcam

    http://10.5.5.9:8080/gopro/camera/setting?setting=178&option=1

    Response samples

    Content type
    application/json
    { }

    Webcam

    Webcam Stabilization " class="sc-iKOmoZ sc-cCzLxZ WVNwY jaVotg">

    default camera

    http://10.5.5.9:8080/gopro/webcam/stop

    Response samples

    Content type
    application/json
    { }